I've been experimenting with TabView and tabViewStyle and I've run into a problem with my code I can't figure out.
In the code below, when the app opens up on my device I start on the HomeScreen() (as expected) but if I tap on Profile in the top bar, the tab navigation doesn't happen. The Profile text turns red (indicating that pageIndex has been updated), but for reasons I can't figure out, the TabView isn't updating accordingly.
BUT, if I open the app and tap on Settings in the top bar, the tab navigation happens as expected.
Swiping works as expected, no issues there.
Have I missed something obvious?
Steps to reproduce:
Copy code into xcode
Run on simulator / canvas / device
Tap Profile (don't swipe or tap anything else)
Profile will turn red, but the page won't be animated left to the Profile screen.
If you tap Settings or swipe any direction, tapping Profile will work as expected.
import SwiftUI
struct SwipeNavigation2: View {
#State var pageIndex = 1
var body: some View {
NavigationView {
TabView(selection: self.$pageIndex) {
// The screen to the "left" of the Home screen
ProfileScreen()
.tag(0)
// The screen we want the app to load on
HomeScreen()
.tag(1)
// The screen to the "right" of the Home screen
SettingsScreen()
.tag(2)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation(.spring()) {
pageIndex = 0
}
} label: {
Text("Profile")
.foregroundColor(pageIndex == 0 ? .red : .primary)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.spring()) {
pageIndex = 2
}
} label: {
Text("Settings")
.foregroundColor(pageIndex == 2 ? .red : .primary)
}
}
}
}
}
}
private struct ProfileScreen: View {
var body: some View {
Text("Profile screen")
}
}
private struct HomeScreen: View {
var body: some View {
Text("Home screen")
}
}
private struct SettingsScreen: View {
var body: some View {
Text("Settings screen")
}
}
Edit:
I've taken some of the suggestions and amended the code as such:
struct SwipeNavigation2: View {
#State var pageIndex = 0
var body: some View {
NavigationView {
TabView(selection: self.$pageIndex) {
ProfileScreen()
.tag(0)
HomeScreen()
.tag(1)
SettingsScreen()
.tag(2)
}
.onAppear {
pageIndex = 1
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation(.spring()) {
pageIndex = 0
}
} label: {
Text("Profile")
.foregroundColor(pageIndex == 0 ? .red : .primary)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.spring()) {
pageIndex = 2
}
} label: {
Text("Settings")
.foregroundColor(pageIndex == 2 ? .red : .primary)
}
}
}
}
}
}
Edit 1:
Here's a recording from my simulator (Xcode14.1), on an iPhone 14. You'll see once the recording starts, I tap on Profile (which turns it red), but the TabView isn't moving me to the correct page.
https://imgur.com/a/B9QiYDM
Edit 2:
It gets weirder. I've tested the following devices in XCode simulator:
iPhone 13 (doesn't work)
iPhone 13 Mini (doesn't work)
iPhone 14 (doesn't work)
iPhone 14 Pro (works)
iPhone 14 Pro Max (works)
Move the onAppear modifier on the tab with the index set at init (in your case the Profile View.
import SwiftUI
struct AdamView: View {
#State var pageIndex: Int = 0
var body: some View {
NavigationView {
TabView(selection: self.$pageIndex) {
Text("Profile")
.onAppear {
pageIndex = 1
}
.tag(0)
Text("Home")
.tag(1)
Text("Settings")
.tag(2)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation(.spring()) {
pageIndex = 0
}
} label: {
Text("Profile")
.foregroundColor(pageIndex == 0 ? .red : .primary)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.spring()) {
pageIndex = 2
}
} label: {
Text("Settings")
.foregroundColor(pageIndex == 2 ? .red : .primary)
}
}
}
}
}
}
Another solution is to use the task modifier instead. With the task, you can keep the onAppear on the TabView but I noticed we see the Profile View for a very short time before showing the Home tab.
Related
I have two views embedded in a TabView and a third view activated by a ToolbarItem in a navigationStack.
problem 1)
When I tap on plus button I navigate to my addView, but I still can see the tabs at the bottom.
problem 2)
after many test I found that if put the tabView code in MainView Inside a NavigationStack, I solve problem 1) but each time I dismiss from a detailView from a row in ContentView, the navigation Item disappears.
the main view for the tabview
struct MainView: View {
var body: some View {
TabView {
ContentView()
.tabItem {
Label("List", systemImage: "list.dash")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gearshape.fill")
}
}
}
}
the ContentView (a list of lessons, navigationDestination goes to a detail view)
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest (sortDescriptors: [
SortDescriptor(\.lessonNuber, order: .reverse)
], predicate: nil) var lessons: FetchedResults<Lesson>
#State var showAddView = false
var body: some View {
NavigationStack {
VStack {
List {
ForEach(lessons, id: \.self) { lesson in
NavigationLink {
DetailView(lesson: lesson)
} label: {
HStack {
Text("\(lesson.lessonNuber)")
.font(.title)
Text( "\(lesson.un_notion)")
.font(.body)
}
}
}
}
// .background(
// NavigationLink(destination: AddView(), isActive: $showAddView) {
// AddView()
// }
// )
.navigationDestination(isPresented: $showAddView) {
AddView()
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
showAddView = true
} label: {
Label("Add Lesson", systemImage: "plus")
}
}
}
}
.padding()
}
}
}
In the files App after you pressed the Select button the tabBar switch to the toolbar.
How can I do this with swiftUI?(Switch between tabBar and toolbar)
struct tabBar: View {
var body: some View {
TabView {
ContentView().tabItem {
Image(systemName: "paperplane.fill")
Text("tabBar")
}
}
.toolbar {
ToolbarItem(placement: .bottomBar, content: {
Button(action: {
}){
Text("toolBar1")
}
})
ToolbarItem(placement: .bottomBar, content: {
Button() {
} label: {
Text("toolBar2")
}
})
}
}
}
For iOS 16 and above, you can use the toolbar(_:for:) method to toggle between visible and hidden states. Also, don't forget to wrap the view in a NavigationView, or else, setting the visibility of the bottom toolbar won't work.
import SwiftUI
struct ContentView: View {
#State var shouldShowTabBar = false
var body: some View {
NavigationView {
TabView {
Group {
Button("Switch Between Tab Bar and Toolbar") {
shouldShowTabBar.toggle()
}
.tabItem {
Label("Tab 1", systemImage: "list.dash")
}
Text("Tab 2")
.tabItem {
Label("Tab 2", systemImage: "square.and.pencil")
}
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
} label: {
Text("Toolbar Button")
}
}
}
.toolbar(shouldShowTabBar ? .visible : .hidden, for: .tabBar)
.toolbar(shouldShowTabBar ? .hidden : .visible, for: .bottomBar)
}
}
}
}
If you have to support iOS 14 and 15, you can check every item if it should be visible and hide/show them one by one.
import SwiftUI
struct ContentView: View {
#State var shouldShowTabBar = false
var body: some View {
NavigationView {
TabView {
Group {
Button("Switch Between Tab Bar and Toolbar") {
shouldShowTabBar.toggle()
}
.tabItem {
if shouldShowTabBar {
Label("Tab 1", systemImage: "list.dash")
}
}
Text("Tab 2")
.tabItem {
if shouldShowTabBar {
Label("Tab 2", systemImage: "square.and.pencil")
}
}
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
if !shouldShowTabBar {
Button {
} label: {
Text("Toolbar Button")
}
}
}
}
}
}
}
}
I have a TabView with three views (triangle, square and circle) nested inside a navigation view and link. TabView works fine. I'd like to have toolbar buttons for only a specific tabview; say circle. The toolbar modifier adds the buttons on all the tabviews.
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink() {
TabView {
Text("triangle")
.tabItem {
Label("triangle", systemImage: "triangle")
}
Text("square")
.tabItem {
Label("square", systemImage: "square")
}
Text("circle")
.tabItem {
Label("circle", systemImage: "circle")
}
}
.navigationTitle("Tabs")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("About") {
print("About tapped!")
}
Button("Help") {
print("Help tapped!")
}
}
}
} label: {
Text("Hello!")
}
.navigationTitle("Title")
}
}
}
How can I set this up to only show toolbar buttons on one tabview only?
I suppose a secondary option (way less preferred) may be to disable the buttons on tabviews where they are not needed (if possible).
It can be done with a selection state for TabView and making visibility of specific toolbar buttons depending on that state.
Here is a demo. Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
#State private var selection = 0 // << here !!
var body: some View {
NavigationView {
NavigationLink() {
TabView(selection: $selection) { // << here !!
Text("triangle")
.tabItem {
Label("triangle", systemImage: "triangle")
}.tag(0)
Text("square")
.tabItem {
Label("square", systemImage: "square")
}.tag(1)
Text("circle")
.tabItem {
Label("circle", systemImage: "circle")
}.tag(2)
}
.navigationTitle("Tabs")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if selection == 2 { // << here !!
Button("About") {
print("About tapped!")
}
Button("Help") {
print("Help tapped!")
}
}
}
}
} label: {
Text("Hello!")
}
.navigationTitle("Title")
}
}
}
Test code in GitHub
I have a SwiftUI Home screen:
import SwiftUI
struct HomeView: View {
#State private var navigateToSettingsView : Bool = false
var body: some View {
NavigationView {
if navigateToSettingsView {
NavigationLink(destination: UserSettingsView(), isActive: $navigateToSettingsView) {
EmptyView()
}
.navigationTitle("Home") // This overrides the word Back with Home on the child back button
.toolbar {
ToolbarItem(placement: .principal) {
VStack {
Text("User Settings").font(.headline)
}
}
}
}
else {
NavigationLink(destination: Text("Hello, I am HomeView child screen")) {
homeScreen
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: {
navigateToSettingsView = true
}) {
Image(systemName: "gearshape")
}
}
ToolbarItemGroup(placement: .navigationBarLeading) {
Text("App Name")
}
}
}
}
}
}
extension HomeView {
private var homeScreen: some View {
VStack {
Text("Hello, I am HomeView")
}
.frame(maxWidth: .infinity)
}
}
The UserSettingsView is just basic right now:
import SwiftUI
struct UserSettingsView: View {
var body: some View {
VStack {
Text("I am a UserSettingsView")
}
}
}
What I am struggling with is setting the title of the child screen when the user clicks on the Gear icon. The ToolbarItem seems to be ignored. How do you set the title of the child screen so that it has a title of User Settings?
I believe you can achieve what you want using a cleaner approach.
The example below replaces the Button with another NavigationLink. It maintains the Home text instead of Back, but it also shows a title in the user settings screen. The only catch is that the title is in the center of the top area, not in the leading position.
Note that the #State variable does not exist anymore.
struct Example: View {
var body: some View {
NavigationView {
NavigationLink(destination: Text("Hello, I am HomeView child screen")) {
Text("I'm a home screen")
}
.navigationTitle("Home")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
NavigationLink(destination: UserSettingsView().navigationTitle("User settings")) {
Image(systemName: "gearshape")
}
}
ToolbarItemGroup(placement: .navigationBarLeading) {
Text("App Name")
}
}
}
}
}
I'm trying to setup a toolbar in watchos which displays a button when I scroll the view down. Everything works, the button scrolls down and I can navigate to another page. When returning however, I would like the scrollview to be in the same position as when the app loads, so without the button being visible.
The code I have now is:
struct ContentView: View {
#State private var selectedPage: String? = nil
var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(0..<100) {
Text("Row \($0)")
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Settings") {
selectedPage = "Settings"
}
}
}
.background(
NavigationLink(destination: SettingsView(), tag: "Settings",selection: $selectedPage) {}
.hidden()
)
.navigationTitle {
Text("Navigation")
}
}
}
}
I Have tried using a scrollViewReader, but think I'm looking into the wrong directing as it allows the scrollView to go to a certain position, but the toolbar seems to be no part of it and stays in view. When reading the scrollviews offset position (not in code but check gif), the offset is 0 when the button is not visible (initial state). When scrolling down the offset goes up, but when scrolling up until the button becomes visible the offset of the scrollview is 0 as well.
struct ContentView: View {
#State private var selectedPage: String? = nil
var body: some View {
ScrollView {
ScrollViewReader { reader in
VStack(alignment: .leading) {
ForEach(0..<100) { i in
Text("Row \(i)")
.id(i)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Settings") {
selectedPage = "Settings"
}
}
}
.background(
NavigationLink(destination: SettingsView(), tag: "Settings",selection: $selectedPage) {}
.hidden()
)
.onAppear {
withAnimation {
reader.scrollTo(0, anchor: .top)
}
}
.navigationTitle {
Text("Navigation")
}
}
}
}
}
}
When choosing 1 as value to scroll to the button is pushed back to the top and in the list id 1 is selected.
.onAppear {
withAnimation {
reader.scrollTo(1, anchor: .top)
}
}
So it's working with id 1 and higher, but when using id 0 the view is not reset.
So how to reset the view to the initial state with the button being hidden on top ?
Solved it simply by adding a #State property which is true when loading the view and which changes to false if navigated away from the view and then conditionally show the item within the .toolbar view modifier.
#State var loadedMainView = true
var body: some view {
ScrollView {
// .... code
}
.toolbar {
ToolbarItem {
if loadedMainView {
Button("My Button") {
}
}
}
.onAppear {
loadedMainView = true
}
.onDissappear {
loadedMainView.toggle()
}