SwiftUI Tabbar persists navigationBarTitle position in tab's views - swiftui

How can I stop a tab's scrollview's offset being affect by other tab's offset?
I don't want to force the scroll view to the top every time you show a new tab, but just want the new tabs to be not affected by the scroll position of the last tab I viewed.
import SwiftUI
enum Tab {
case First, Second, Third
var title: String {
switch self {
case .First:
return "First"
case .Second:
return "Second"
case .Third:
return "Third"
}
}
}
struct ContentView: View {
#State var selectedTab = Tab.First
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
FirstView()
.tabItem {
Text("First")
}.tag(Tab.First)
SecondView()
.tabItem {
Text("Second")
}.tag(Tab.Second)
ThirdView()
.tabItem {
Text("Third")
}.tag(Tab.Third)
}.navigationBarTitle(selectedTab.title, displayMode: .automatic)
.navigationBarHidden(false)
}
}
}
struct FirstView: View {
let data = [1,2,3,4,5,6,7,8,9,10]
var body: some View {
ScrollView(showsIndicators: true) {
VStack {
ForEach(data, id: \.self) { item in
Text("\(item)")
.frame(minWidth: 0, idealWidth: 100, maxWidth: .infinity, minHeight: 0, idealHeight: 100, maxHeight: .infinity, alignment: .center)
}
}
}
}
}
struct SecondView: View {
let data = [1,2,3,4,5,6,7,8,9,10]
var body: some View {
ScrollView(showsIndicators: true) {
VStack {
ForEach(data, id: \.self) { item in
Text("\(item)")
.frame(minWidth: 0, idealWidth: 100, maxWidth: .infinity, minHeight: 0, idealHeight: 100, maxHeight: .infinity, alignment: .center)
}
}
}
}
}
struct ThirdView: View {
let data = [1,2,3,4,5,6,7,8,9,10]
var body: some View {
ScrollView(showsIndicators: true) {
VStack {
ForEach(data, id: \.self) { item in
Text("\(item)")
.frame(minWidth: 0, idealWidth: 100, maxWidth: .infinity, minHeight: 0, idealHeight: 100, maxHeight: .infinity, alignment: .center)
}
}
}
}
}

It is because you use one NavigationView, so it preserves own state. Make NavigationView independent for each tab.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var selectedTab = Tab.First
var body: some View {
TabView(selection: $selectedTab) {
NavigationView {
FirstView()
.navigationBarTitle(Tab.First.title)
}
.tabItem {
Text("First")
}.tag(Tab.First)
NavigationView {
SecondView()
.navigationBarTitle(Tab.Second.title)
}
.tabItem {
Text("Second")
}.tag(Tab.Second)
NavigationView {
ThirdView()
.navigationBarTitle(Tab.Third.title)
}
.tabItem {
Text("Third")
}.tag(Tab.Third)
}
}
}

Related

SwiftUI - How to add a pageTabView inside a scrollview with PinnedViews

I had one problem while using SwiftUI. I have implemented a sectionHeader using PinnedView which is currently scrollable all up and down, has a header area, and is LazyVStack. Below is the implementation to show the corresponding content.
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
Section {
Text("Header")
.frame(maxWidth: .infinity, minHeight: 300)
.background(Color.red)
}
Section {
LazyVStack {
ForEach(0...100, id: \.hashValue) { num in
Text("\(num)")
}
}
} header: {
ZStack {
Color.black.ignoresSafeArea()
Text("Section Header")
.frame(maxWidth: .infinity, minHeight: 50)
.background(Color.black)
}
}
}
}
}
}
https://imgur.com/a/xYIFzgy
However, I did not stop here and declared a TabView to enable horizontal paging scrolling in the corresponding content area as shown below, but nothing was displayed in the Contents area.
struct ContentView: View {
#State private var currentIndex = 0
var body: some View {
ScrollView {
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
Section {
Text("Header")
.frame(maxWidth: .infinity, minHeight: 300)
.background(Color.red)
}
Section {
TabView(selection: $currentIndex) {
LazyVStack {
ForEach(0...100, id: \.hashValue) { num in
Text("A: \(num)")
}
}
.tag(0)
LazyVStack {
ForEach(0...100, id: \.hashValue) { num in
Text("B: \(num)")
}
}
.tag(1)
}
} header: {
ZStack {
Color.black.ignoresSafeArea()
Text("Section Header")
.frame(maxWidth: .infinity, minHeight: 50)
.background(Color.black)
}
}
}
}
}
}
https://imgur.com/a/TvKVoIR
I have found through debugging that if I declare a TabView inside a ScrollView, the Contents area doesn't show anything. Can you give me an idea on how to use the stickyHeader as in the above example to make it horizontally paging?
Thanks
add These Modifiers to the TabView:
TabView {
...
}
.frame(height: UIScreen.main.bounds.height)
.tabViewStyle(.page(indexDisplayMode: .never))

Show BottomPlayerView above TabView in SwiftUI

I'm learning swiftUI and I want to make a music app.
I created a view which going to be above the tabView, but I want it to be shown only if user start playing a music.
My App, I use ZStack for bottomPlayer, and I share the bottomPlayer variable through .environmentObject(bottomPlayer) so the child views can use it:
class BottomPlayer: ObservableObject {
var show: Bool = false
}
#main
struct MyCurrentApp: App {
var bottomPlayer: BottomPlayer = BottomPlayer()
var audioPlayer = AudioPlayer()
var body: some Scene {
WindowGroup {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabBar()
if bottomPlayer.show {
BottomPlayerView()
.offset(y: -40)
}
}
.environmentObject(bottomPlayer)
}
}
}
The BottomPlayerView (above the TabView)
struct BottomPlayerView: View {
var body: some View {
HStack {
Image("cover")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text("Artist")
.foregroundColor(.orange)
Text("Song title")
.fontWeight(.bold)
}
Spacer()
Button {
print("button")
} label: {
Image(systemName: "play")
}
.frame(width: 60, height: 60)
}
.frame(maxWidth: .infinity, maxHeight: 60)
.background(Color.white)
.onTapGesture {
print("ontap")
}
}
}
My TabView:
struct TabBar: View {
var body: some View {
TabView {
AudiosTabBarView()
VideosTabBarView()
SearchTabBarView()
}
}
}
And In my SongsView, I use the EnvironmentObject to switch on the bottomPlayerView
struct SongsView: View {
#EnvironmentObject var bottomPlayer: BottomPlayer
var body: some View {
NavigationView {
VStack {
Button {
bottomPlayer.show = true
} label: {
Text("Show Player")
}
}
.listStyle(.plain)
.navigationBarTitle("Audios")
}
}
}
The problem is the bottomPlayer.show is actually set to true, but doesn't appear ...
Where I am wrong?
In your BottomPlayer add theĀ #Published attribute before the show boolean.
This creates a publisher of this type.
apple documentation

No fullScreenCover animation on close button

I have a strange behavior.
When I close my fullScreenCover, I have no animation on it. On the other hand, I have the animation at the opening. I don't understand why I have no close animation.
Example codes
import SwiftUI
struct ContentView: View {
#EnvironmentObject var myViewModel: MyViewModel
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 10) {
ForEach(myViewModel.data, id: \.self) { data in
ChildView(data: data)
}
}
}
.onAppear() {
self.myViewModel.getData()
}
.navigationBarTitle("My ContentView")
}
}
import SwiftUI
struct ChildView: View {
#State var data: Data
#State var showModal = false
var body: some View {
Button(action: {
self.showModal.toggle()
}, label: {
VStack {
Text("\(data.name)")
.font(.body)
.frame(width: 180, height: 150/2, alignment: .bottom)
VStack {
Text("\(Image(systemName: "checkmark.bubble")) 15")
Text("\(Image(systemName: "clock")) 20'")
}
.frame(width: 180, height: 150/2, alignment: .bottomTrailing)
}
.cornerRadius(10)
.frame(minWidth: 0, maxWidth: 180, minHeight: 150, maxHeight: 150, alignment: .center)
})
.fullScreenCover(isPresented: $showModal) {
OtherView()
}
}
}
import SwiftUI
struct OtherView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
HStack {
Text("CLOSE")
Spacer()
Image(systemName: "checkmark")
.resizable()
.frame(width: 30, height: 30)
.padding()
}
}
}
.navigationBarTitle("My Modal", displayMode: .inline)
}
}
}
Thanks.

iOS 15 SwiftUI Conditionals on a view with Navigation View makes NavigationBar config to be ignore if navigationViewStyle stack

been searching for this everywhere and can't find anything around this, I believe is a bug, maybe is not.
I need NavigationView with .navigationViewStyle(.stack) to have it stacked on the iPad and make it look the same as the iphone, now suppose you have this view:
import SwiftUI
struct ContentView: View {
#State var isShowingProfile = false
#State var isNavigationViewShowing = true
var body: some View {
if isNavigationViewShowing {
NavigationView {
VStack {
Button("Simple view") {
isNavigationViewShowing = false
}
.padding()
Button("Profile navigation") {
isShowingProfile = true
}
.padding()
NavigationLink(
destination: ProfileView(),
isActive: $isShowingProfile
) {
EmptyView()
}
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
.background(Color.gray)
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
} else {
VStack {
Button("Show NavigationView"){
isNavigationViewShowing = true
}
.padding()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
).background(Color.yellow)
}
}
}
struct ProfileView: View {
var body: some View {
Text("This is a profile")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Well this just show this 3 simple views:
The navigationView when you start
The Profile view if you tap on "Profile navigation"
Finally the Simple view which is trigger by the conditional state pressing "Simple view"
Up to here is all fine and good.
The problem is when navigate to the "Simple view" and then tap "Show NavigationView" to navigate back to the NavigtionView.
The app opens the first view (NavigationView), but the NavigationView ignores the .navigationBarHidden(true) and just show a big empty space on the top. In fact, it would ignore things like .navigationBarTitleDisplayMode(.inline) and just show the large version of the navigationBar
This is working correctly in all iOS 14.x, but on iOS 15.0 seems broken. The behaviour continues to be the same on iOS 15.1 beta.
Any idea whats going on? I'm not really interested in changing the conditionals on the view, because real life app is more complex.
Also, I tried ViewBuilder without any success. And if I take out .navigationViewStyle(.stack) it works all fine on iOS 15, but then the view on the iPad is with the side menu.
Thanks a lot for any tip or help, you should be able to reproduce in simulator and real device.
Video of the explained above
I think the better solution all around is to not have the NavigationView be conditional. There is no reason your conditional can't just live in the NavigationView. You just don't ever want the bar to show. Therefore, this code would seem to meet the requirements:
struct ContentView: View {
#State var isShowingProfile = false
#State var isNavigationViewShowing = true
var body: some View {
NavigationView {
Group {
if isNavigationViewShowing {
VStack {
Button("Simple view") {
isNavigationViewShowing = false
}
.padding()
Button("Profile navigation") {
isShowingProfile = true
}
.padding()
NavigationLink(
destination: ProfileView(),
isActive: $isShowingProfile
) {
EmptyView()
}
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
.background(Color(UIColor.systemGray6))
} else {
VStack {
Button("Show NavigationView"){
isNavigationViewShowing = true
}
.padding()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
).background(Color.yellow)
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
}
I used Group simply to put the .navigationBarHidden(true) in the correct place so the code would compile.
Is this the behavior you are looking for?
import SwiftUI
struct ContentView: View {
#State private var isShowingProfile = false
#State private var showSimple = false
var body: some View {
NavigationView {
VStack {
Button("Simple view") {
showSimple = true
}
.padding()
Button("Profile navigation") {
isShowingProfile = true
}
.padding()
NavigationLink(destination: ProfileView(), isActive: $isShowingProfile) {
EmptyView()
}
}
.fullScreenCover(isPresented: $showSimple, onDismiss: {
print("Dismissed")
}, content: {
SimpleView()
})
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
.background(Color.gray)
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
}
struct ProfileView: View {
var body: some View {
Text("This is a profile")
}
}
struct SimpleView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button("Show NavigationView") {
presentationMode.wrappedValue.dismiss()
}
.padding()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
).background(Color.yellow)
}
}

SwiftUI not centering when in ZStack

I am trying to put together a view that consists of a top header view, a bottom content view, and a view that sits on top centered on the line splitting the two views. I figured out I need an alignment guide within a ZStack to position the middle view but I am having problems getting the items in the lower content view centered without a gap.
This code:
extension VerticalAlignment {
struct ButtonMid: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[.bottom]
}
}
static let buttonMid = VerticalAlignment(ButtonMid.self)
}
struct ContentView: View {
var body: some View {
VStack {
ZStack(alignment: Alignment(horizontal: .center, vertical: .buttonMid)) {
HeaderView()
.frame(maxWidth: .infinity, minHeight: 200, idealHeight: 200, maxHeight: 200, alignment: .topLeading)
// BodyView()
// .alignmentGuide(.buttonMid, computeValue: { dimension in
// return dimension[VerticalAlignment.top]
// })
Color.red
.frame(width: 380, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.alignmentGuide(.buttonMid, computeValue: { dimension in
return dimension[VerticalAlignment.center]
})
}
BodyView()
}
.edgesIgnoringSafeArea(.top)
}
}
struct HeaderView: View {
var body: some View {
Color.green
}
}
struct BodyView: View {
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
BodyContent()
Spacer()
}
Spacer()
}
.background(Color.blue)
}
}
struct BodyContent: View {
var body: some View {
VStack {
Text("Line 1")
Text("Line 2")
Text("Line 3")
}
}
}
give you this:
which centers the lower content they way I want it however it leaves a gap between the upper and lower views. If I uncomment the BodyView code in the ZStack and comment it out in the VStack like so:
struct ContentView: View {
var body: some View {
VStack {
ZStack(alignment: Alignment(horizontal: .center, vertical: .buttonMid)) {
HeaderView()
.frame(maxWidth: .infinity, minHeight: 200, idealHeight: 200, maxHeight: 200, alignment: .topLeading)
BodyView()
.alignmentGuide(.buttonMid, computeValue: { dimension in
return dimension[VerticalAlignment.top]
})
Color.red
.frame(width: 380, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.alignmentGuide(.buttonMid, computeValue: { dimension in
return dimension[VerticalAlignment.center]
})
}
// BodyView()
}
.edgesIgnoringSafeArea(.top)
}
}
gives you:
which leaves the content uncentered. How can I keep it centered? I tried putting it in a GeometryReader and that had the same results.
You don't need a custom VerticalAlignment. Instead you can put the middle view as an overlay and align it to the top border of the bottom view:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
HeaderView()
.frame(height: 200)
BodyView()
.overlay(
Color.red
.frame(width: 380, height: 50)
.alignmentGuide(.top) { $0[VerticalAlignment.center] },
alignment: .top
)
}
.edgesIgnoringSafeArea(.top)
}
}