SwiftUI: TabView and NavigationView play nicely on iPhone but not on iPad? - ios

My app has a home screen showing a list of options and a few entirely different pages. Each leads to a dispatcher view with a tag indicating which page to show. I also want to be able to swipe between the pages. So I have this code (hugely simplified from the actual app, of course, but behaving in the same way):
struct Tab: Hashable {
let name: String
let id: Int
}
struct ContentView: View {
let tabs: [Tab] = [Tab(name: "first", id: 1), Tab(name: "second", id: 2), Tab(name: "third", id: 3)]
var body: some View {
NavigationView {
Form {
List (tabs, id: \.self) { tab in
NavigationLink(destination: Dispatcher(which: tab.id)) {
VStack (alignment: .leading) {
Text(tab.name).font(.headline).bold()
}
}
}
}.navigationTitle("test")
Text("hello")
}
}
}
struct Dispatcher: View {
var which: Int
#State private var tab = 2
var body: some View {
TabView(selection: $tab) {
Text("first page").tag(1)
Text("second page").tag(2)
Text("third page").tag(3)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.onAppear(perform: { tab = which })
}
}
On an iPhone, it works fine. But on an iPad, in either orientation, tapping any option in the home page takes me to the same sub-page, the one set by the initial value of the #State variable tab. The assignment in .onAppear is being executed, but the new value of tab is being ignored.
Any ideas gratefully received.
Jeremy

why not just do this:
struct Dispatcher: View {
#State var which: Int
var body: some View {
TabView(selection: $which) {
Text("first page").tag(1)
Text("second page").tag(2)
Text("third page").tag(3)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}

Now that workingdog has answered my question, can I perhaps expand it?
I implement his (or her, of course) solution on the iPad. I click on a selection in the NavigationView list (say, "second"). The clicked line highlights and the second page appears.
I then swipe to show another page (say "third"). That's fine.
But "second" remains highlighted in the NavigationView list, so I can't click on it to return to the second page.
Can I tell the NavigationView list to highlight the entry corresponding to the actual shown page? Or to highlight nothing?

Related

SwiftUI: How to “deep-link” to the second/third level of a list in a navigation view

I recently encountered an issue with "deep-linking" to the third level of a list in a navigation view.
Here is some background:
My app has a settings menu that is based on SwiftUI lists/forms
As typical for a menu you can jump from one list item to the next list (one level "deeper") - this is realized NavigationLink
Because the list on the top level is embedded in a navigation view, SwiftUI automatically adds a "back" button to the child views on the lower levels to get back to the first menu page
So far, so easy. Now, here is what I attempt to do (please also refer to the attached illustration):
I want to jump from the first view (potentially also from other unrelated views) directly to as sub-menu; i.e. a list that is on the second or third level of my menu
What I did so far is to set a programmatic navigation link that links directly to what is named "View 3" in the illustration. However, this does not provide the possibility to jump back to "View 2" and then to "View 1" via the back buttons in the navigation view
Now my question is: Is it possible to jump to "View 3" (potentially from anywhere in my application) and still be able to go back to "View 2" and then to "View 1" via the back buttons in the navigation view?
This is view 1:
struct SwiftUIView1: View {
var body: some View {
NavigationView {
VStack {
List {
NavigationLink("Link", destination: SwiftUIView2())
Text("TBD")
Text("TBD")
}
.navigationTitle("View 1")
Button("Jump to view 3", action: XXX) // <-- What to put here?
}
}
}
}
This is view 2:
struct SwiftUIView2: View {
var body: some View {
List {
NavigationLink("Link", destination: SwiftUIView3())
Text("TBD")
Text("TBD")
}
.navigationTitle("View 2")
}
}
This is view 3:
struct SwiftUIView3: View {
var body: some View {
Text("Hello world")
.navigationTitle("View 3")
}
}
Here is a visualization of what I want to achieve:
I would appreciate any ideas!
Not the best solution, but if you want to achieve a deep link and still be able to go back to #2 and then #1, you might try this solution.
It automatically goes from #1 to #3, however through #2. Otherwise you will not be able to get a back button and go back to #2. That's not possible as it is the default Navigation behaviour...
class NavigationManager: ObservableObject {
#Published var goToThird: Bool = false
}
struct ContentView: View {
#State var isActive: Bool = false
var navigationManager = NavigationManager()
var body: some View {
NavigationView {
VStack {
List {
NavigationLink("Link", destination: SwiftUIView2(manager: navigationManager), isActive: $isActive)
Text("TBD")
Text("TBD")
}
.navigationTitle("View 1")
Button("Jump to view 3", action: {
isActive = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
navigationManager.goToThird = true
}
}) // <-- What to put here?
}
}
}
}
struct SwiftUIView2: View {
#ObservedObject var manager: NavigationManager
var body: some View {
List {
NavigationLink("Link", destination: SwiftUIView3(), isActive: $manager.goToThird)
Text("TBD")
Text("TBD")
}
.navigationTitle("View 2")
}
}
struct SwiftUIView3: View {
var body: some View {
Text("Hello world")
.navigationTitle("View 3")
}
}

How to dismiss a presenting view to the root view of tab view in SwiftUI?

I'm using TabView on my home page. Let's just say I have 4 tabs.
On second tab, i can go to another view using NavigationLink and I go to another 2 views using NavigationLink. Then on the latest view, there is a button to present a view and i use .fullScreenCover (since I want to present it full screen).
In the presenting view, I add an X mark on the left side of the navigationBarItems to dismiss. I use #Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() to dismiss. But it only dismiss the presenting view to the previous view, while actually I want to dismiss it to the root of my view which is the 2nd tab of my TabView.
Is there a way to do this? Because I have looked up to some articles and nothing relevant especially in TabView context.
I also have a question tho:
Is it a right approach to use .fullScreenCover? Or is there another possible solution for example presenting a modal with full screen style (if there's any cause i'm not sure either).
Any suggestions will be very appreciated, thankyou in advance.
The presentationMode is one-level effect value, ie changing it you close one currently presented screen.
Thus to close many presented screens you have to implement this programmatically, like in demo below.
The possible approach is to use custom EnvironmentKey to pass it down view hierarchy w/o tight coupling of every level view (like with binding) and inject/call only at that level where needed.
Demo tested with Xcode 12.4 / iOS 14.4
struct ContentView: View {
var body: some View {
TabView {
Text("Tab1")
.tabItem { Image(systemName: "1.square") }
Tab2RootView()
.tabItem { Image(systemName: "2.square") }
}
}
}
struct Tab2RootView: View {
#State var toRoot = false
var body: some View {
NavigationView {
Tab2NoteView(level: 0)
.id(toRoot) // << reset to root !!
}
.environment(\.rewind, $toRoot) // << inject here !!
}
}
struct Tab2NoteView: View {
#Environment(\.rewind) var rewind
let level: Int
#State private var showFullScreen = false
var body: some View {
VStack {
Text(level == 0 ? "ROOT" : "Level \(level)")
NavigationLink("Go Next", destination: Tab2NoteView(level: level + 1))
Divider()
Button("Full Screen") { showFullScreen.toggle() }
.fullScreenCover(isPresented: $showFullScreen,
onDismiss: { rewind.wrappedValue.toggle() }) {
Tab2FullScreenView()
}
}
}
}
struct RewindKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var rewind: Binding<Bool> {
get { self[RewindKey.self] }
set { self[RewindKey.self] = newValue }
}
}
struct Tab2FullScreenView: View {
#Environment(\.presentationMode) var mode
var body: some View {
Button("Close") { mode.wrappedValue.dismiss() }
}
}
You have 2 options:
With .fullScreenCover you will have a binding that results in it being presented you can pass this binding through to the content and when the user taps on x set to to false
You can use the #Environment(\.presentationMode) var presentationMode then call presentationMode.wrappedValue.dismiss() in your button body.
Edit:
If you want to unwind all the way you should make the TabView be binding based. I like to use SceneStorage for this take a look at this post then you can access this SceneStorage value anywhere in your app to respond to it but also to update and change the navigation (this also has the benefit of providing you proper state restoration!)
If you make your TabView in this way:
struct ContentView: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
TabView(selection: $selectedTab) {
CarTrips()
.tabItem {
Image(systemName: "car")
Text("Car Trips")
}.tag(Tab.car)
TramTrips()
.tabItem {
Image(systemName: "tram.fill")
Text("Tram Trips")
}.tag(Tab.tram)
AirplaneTrips()
.tabItem {
Image(systemName: "airplane")
Text("Airplane Trips")
}.tag(Tab.airplaine)
}
}
}
enum Tab: String {
case car
case tram
case airplaine
}
Then deep within your app in the place you want to change the navigation you can create a button view.
struct ViewCarButton: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
Button("A Button") {
selectedTab = .car
}
}
}
This will forced the selected tab to be the car tab.
if instead of this you do not want to change tab but rather change what the navigation view is navigated to you can use the same concept for that, NavigationLink that's a binding if this binding is created using a #SceneStorage then in your ViewCarButton you can make changes to it that will change the navigation state.

How to deselect a list button within a NavigationLink

I have a list with one of the list items in a NavigationLink because it needs to move to a detail view once tapped. When I come back from that detail view the list button is still in a selected state. Prior to SwiftUI, I'd just tell the .isSelected to equal false but I can't figure out how to do that in SwiftUI?
List {
NavigationLink(destination: SettingsStartdayView()){
HStack {
Text("Start Day Notification")
Spacer()
Text(startDayNotificationSetting)
.font(.subheadline)
.foregroundColor(Color.gray)
.multilineTextAlignment(.trailing)
}
}
}
Starting List View
After Detail View, Back
This View is being loaded into the main app's view through:
NavigationView{
TabView(selection: $isSelectedTab) {
SettingsView()
.tabItem{
}.tag(1)
Here's a public project that has a complete example of what I'm dealing with: https://gitlab.com/jammyman34/test-sounds-project
Go to the Settings tab and then click the top list item to go to the detail page. Notice how the list you clicked stays selected when you go back. It doesn't clear unless you change to the other tab.
The issue here is the Text which is above the List in SettingsView - a bug reported here.
Instead, you can use a native navigation title and attach it to the TabView.
struct SettingsHomeView: View {
#State var startDayNotificationSetting: String = "8:30AM"
#State var appVersion: String = "0.01"
var body: some View {
// no `Text` above `List`
List {
NavigationLink(destination: SettingsStartdayView()){
HStack {
Text("Start Day Notification")
Spacer()
Text(startDayNotificationSetting)
.font(.subheadline)
.foregroundColor(Color.gray)
.multilineTextAlignment(.trailing)
//Image(systemName: "chevron.right")
}
}
}
}
}
struct ContentView: View {
#State private var isSelectedTab = 1 // select the first tab
var body: some View {
NavigationView{
TabView(selection: $isSelectedTab) {
// ...
}
// control displaying the title depending on the `isSelectedTab`
.navigationTitle("Settings")
.navigationBarHidden(isSelectedTab == 1)
}
}
}

SwiftUI - Why is TabView and its contents sometimes not refreshing when I change tabs on macOS?

I'm trying to make a tabbed application on macOS with SwiftUI, and I have an odd issue with TabView.
When I have two tabs with TextFields each and save their text states to their respective private variables, something odd happens: When I switch from tab A to tab B after entering text into tab A's TextField, the tab indicator shows that I am still on tab A, but the content shows tab B's content. When I click on the button for tab B once again, it will still show tab B's content. Furthermore, when I press the button for tab A afterward, it will show the content of tab A, but the indicator for the tab still shows that I am on tab B.
What might I possibly be doing wrong?
Here is an example that illustrates my issue:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView
{
TabAView()
.tabItem({Text("Tab A")})
TabBView()
.tabItem({Text("Tab B")})
}
}
}
struct TabAView: View {
#State private var text = ""
var body: some View {
VStack{
Text("Tab A")
TextField("Enter", text: $text)
}
}
}
struct TabBView : View {
#State private var text = ""
var body: some View {
VStack {
Text("Tab B")
TextField("Enter", text: $text)
}
}
}
Here's a screen capture of the issue occurring:
This is definitely a bug in SwiftUI's implementation of TabView. But you can easily work around the problem by binding to TabView selection and setting the current tab manually like so:
struct ContentView: View {
#State private var currentTab = 0
var body: some View {
TabView(selection: $currentTab)
{
TabAView()
.tabItem({Text("Tab A")})
.tag(0)
.onAppear() {
self.currentTab = 0
}
TabBView()
.tabItem({Text("Tab B")})
.tag(1)
.onAppear() {
self.currentTab = 1
}
}
}
}
This bug only seems to manifest itself when the user changes tabs while a TextField has focus.
If you make the above changes to your code, it will work as expected.

Handle Tab Selection SwiftUI

Following tutorials, I have the following code to show a tab view with 3 tab items all with an icon on them, When pressed they navigate between the three different views. This all works fine, however, I want to be able to handle the selection and only show views 2 or 3 if certain criteria are met.
Is there a way to get the selected value and check it then check my own criteria and then show the view is criteria is met, or show an alert if it is not saying they can't use that view at the moment.
Essentially I want to be able to intercept the selection value before it switches out the view, maybe I need to rewrite all of this but this is the functionality I'm looking for as this is how I had my previous app working using the old framework.
#State private var selection = 1
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
}
}
You can do it by changing the value of selection on tap. You can use .onAppear() method for a particular tab to check your condition:
#State private var selection = 1
var conditionSatisfied = false
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
}
}

Resources