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

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.

Related

How it is possible to dismiss a view from a subtracted subview in SwiftUI

Whenever my code gets too big, SwiftUI starts acting weird and generates an error:
"The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
So I started breaking up my code into Extracted Subviews, one of the problems I came across is how to dismiss a view from a subtracted subview.
Example: we have here LoginContentView this view contains a button when the button is clicked it will show the next view UsersOnlineView.
struct LoginContentView: View {
#State var showUsersOnlineView = false
var body: some View {
Button(action: {
self.showUsersOnlineView = true
}) {
Text("Show the next view")
}
.fullScreenCover(isPresented: $showUsersOnlineView, content: {
UsersOnlineView()
})
}
On the other hand, we have a button that is extracted to subview, to dismiss the modal and go back to the original view:
import SwiftUI
struct UsersOnlineView: View {
var body: some View {
ZStack {
VStack {
CloseViewButton()
}
}
}
}
struct CloseViewButton: View {
var body: some View {
Button(action: {
// Close the Modal
}) {
Text("Close the view")
}
}
}
Give the sbview the state property that defines if the view is shown.
struct CloseViewButton: View {
#Binding var showView: Bool
var body: some View {
Button(
ShowView = false
}) {
Text("Close the view")
}
}
}
When you use the sub view give it the property
CloseButtonView(showView: $showOnlineView)
To allow the sub view to change the isShown property it needs to get a binding.
On the presentation mode. I think this only works with Swiftui presentations like sheet and alert.
The simplest solution for this scenario is to use presentationMode environment variable:
struct CloseViewButton: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Close the view")
}
}
}
Tested with Xcode 12.1 / iOS 14.1

SwiftUI - How to close the sheet view, while dismissing that view

I want to achieve the function. Like, "Look up" view that is from Apple.
My aim is when the sheet view push another view by navigation, the user can tap the navigation item button to close the sheet view. Like, this below gif.
I try to achieve this function.
I found a problem that is when the user tap the "Done" button. The App doesn't close the sheet view. It only pop the view to parent view. Like, this below gif.
This is my code.
import SwiftUI
struct ContentView: View {
#State var isShowSheet = false
var body: some View {
Button(action: {
self.isShowSheet.toggle()
}) {
Text("Tap to show the sheet")
}.sheet(isPresented: $isShowSheet) {
NavView()
}
}
}
struct NavView: View {
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView()) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("Done")
}
)
}
}
How did I achieve this function? :)
Please help me, thank you. :)
UPDATE: Restored original version - provided below changes should be done, as intended, to the topic starter's code. Tested as worked with Xcode 13 / iOS 15
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 11.2 / iOS 13.2)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var showingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
}.sheet(isPresented: $isShowSheet) {
NavView()
.environment(\.showingSheet, self.$isShowSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.showingSheet) var showingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
self.showingSheet?.wrappedValue = false
}
)
}
}
I haven't tried SwiftUI ever, but I came from UIKit + RxSwift, so I kinda know how binding works. I read quite a bit of sample codes from a SwiftUI Tutorial, and the way you dismiss a modal is actually correct, but apparently not for a navigation stack.
One way I learned just now is use a #Binding var. This might not be the best solution, but it worked!
So you have this $isShowSheet in your ContentView. Pass that object to your NavView struct by declaring a variable in that NavView.
ContentView
.....
}.sheet(isPresented: $isShowSheet) {
NavView(isShowSheet: self.$isShowSheet)
}
NavView
struct NavView: View {
#Binding var isShowSheet: Bool
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView(isShowSheet: self.$isShowSheet)) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
and finally, do the same thing to your subView.
NavSubView
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isShowSheet: Bool
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
//self.presentationMode.projectedValue.wrappedValue.dismiss()
self.isShowSheet = false
}){
Text("Done")
}
)
}
}
Now as you can see, you just need to send a new signal to that isShowSheet binding var - false.
self.isShowSheet = false
Voila!
Here's an improved version of Asperi's code from above since they won't accept my edit. Main credit goes to them.
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 13 / iOS 15)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var isShowingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
#State var isShowingSheet = false
...
Button("open sheet") {
isShowingSheet?.wrappedValue = true
}
// note no $ in front of isShowingSheet
.sheet(isPresented: isShowingSheet ?? .constant(false)) {
NavView()
.environment(\.isShowingSheet, self.$isShowingSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.isShowingSheet) var isShowingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
isShowingSheet?.wrappedValue = false
}
)
}
}

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.

How to navigate from one view to another in Swift UI on a click of button

I want to push from one view to another in SwiftUI on a click of button. Likewise we navigate from one controller to another controller in storyboard.
But in SwiftUI how to achieve this.
You can solve it by using NavigationLink
What is NavigationLink ?
NavigationLink: Creates a button on the right hand side of each cell and triggers presentation to the detail view
Apple Link:
https://developer.apple.com/documentation/swiftui/navigationlink
Sample Projects
Apple
https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation
Github
https://github.com/appbrewery/H4X0R-News-iOS13-SwiftUI-Completed
How to Implement:
import SwiftUI
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarkData) { landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
*Note: "LandmarkDetail" is your destination view.
You can also download the sample project from apple (link mentioned above) to understand it clearly
There are two ways to achieve that.
The first one is with "isActive" binding.
struct ViewA: View {
#State private var isActive = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: NextView(),
isActive: $isActive) {
Button(action: {
self.isActive = true
}) {
Text("Push Next View")
}
}
}
}
}
}
You place the view which you want to push in the NavigationLink's destination.
The second way is again with NavigationLink, but using "tag" and "selection". There the navigation is activated when the selection matches the tag.

How to preserve pushed view when moving between tabs?

I have noticed that TabView has another behavior than UITabBarController, the view stack is cleared each time the user moves from one tab to another.
I have created a small sample to illustrate my case:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
NavView(id: "A").tabItem { Text("Alpha") }.tag(0)
NavView(id: "B").tabItem { Text("Beta") }.tag(1)
}
}
}
struct NavView: View {
var id: String
var body: some View {
NavigationView {
List {
NavigationLink(destination: Text("Detail \(id)")) { Text(id) }
}
}
}
}
The detail view is gone since the NavView is recreated after a visit to the other tab.
Question: Is there a way to recreate the old behavior, that a presented detail view is still present after a visit on another tab?

Resources