fullScreenCover not being dismissed inside a Navigation- or TabView - swiftui

I am trying to dismiss a fullscreenCover(..) that is nested within a TabView and a NavigationView. I want it to be dismissed by invaliding a parent-view of the view invoking fullScreenCover(..).
For example this view hierarchy works fine and dismisses fullScreenCover on invaliding view X:
X -> A -> fullScreenCover
This view hierarchy does not dismiss fullScreenCover on invalidating view X:
X -> NavigationView -> A -> fullScreenCover
It seems like fullScreenCover uses UIKit under the hood, and that the PresentationHostingController does not get dismissed properly when a NavigationView is used. I provided some example code that shows the problem:
struct ContentView: View {
#State var viewFullScreen: Bool = false
#State var showViewA: Bool = true
var body: some View {
if showViewA {
NavigationView{
Button("Trigger fullscreen") { viewFullScreen.toggle() }
.fullScreenCover(isPresented: $viewFullScreen) {
Button("Go to B", action: {
self.showViewA = false //Not working when embedded in NavigationView
})
}
}
} else {
Text("View B")
}
}
}

Related

Swift UI #FocusState triggering NavigationLink Twice

What am I doing wrong here? Tapping on the navigation link triggers the navigation, then the focusState value updates, causing body to run and trigger the navigation link again.
How do I prevent the link from being triggered twice causing my destination views init to fire twice?
import SwiftUI
struct ContentView: View {
#State var text: String = "text"
#FocusState var focussed: Bool
#State var isActive: Bool = false
var body: some View {
NavigationView {
List {
TextField("", text: $text)
.focused($focussed)
.onChange(of: focussed) { _ in }
let _ = Self._printChanges()
NavigationLink("Tap Me", destination: MyViewTwo(), isActive: $isActive)
}
}
}
}
struct MyViewTwo: View {
init() {
print("Init Called")
}
var body: some View {
Text("Hello View 2")
}
}

Sheet is Only Presented Once in SwiftUI

I have an app which presents a sheet. It works for the first time but when I click on it again it does not work. I am making isPresented false when you dismiss a sheet but when I tap on the Filter button again, it does not show the sheet.
ContentView
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
NavigationView {
List(1...20, id: \.self) { index in
Text("\(index)")
}.listStyle(.plain)
.navigationTitle("Hotels")
.toolbar {
Button("Filters") {
isPresented = true
}
}
.sheet(isPresented: $isPresented) {
isPresented = false
} content: {
FilterView()
}
}
}
}
FilterView:
import SwiftUI
struct FilterView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
ZStack {
Text("FilterView")
Button {
// action
presentationMode.wrappedValue.dismiss()
} label: {
Text("Dismiss")
}
}
}
}
struct FilterView_Previews: PreviewProvider {
static var previews: some View {
FilterView()
}
}
A couple of things to note from my experience.
Firstly, when using the isPresented binding to show a sheet, you don't need to reset the bound value in a custom onDismiss handler to reset it to false - that's handled for you internally by SwiftUI as part of the dismiss action.
So your modifier can be simplified a little:
.sheet(isPresented: $isPresented) {
FilterView()
}
Secondly, when running an app in the Simulator I've noticed that when you come back to the main view after dismissing a sheet you have to interact with the app somehow before clicking on the toolbar button, or the action won't trigger.
In cases like this, just scrolling the list up or down a little would be enough, and then the toolbar button works as you'd expect.
I've not encountered the same thing while running apps on a physical device – whether that's because the bug isn't present, or just that it's a lot easier to interact with the app in some microscopic form of gesture, I couldn't say.

Navigation + Tabview + Sheet broken in iOS 15

It looks like Navigation + TabView + Sheet is broken in iOS 15.
When I do this:
ContentView -> DetailView -> Bottom Sheet
When the bottom sheet comes up, the Detail view is automatically popped off the stack:
https://www.youtube.com/watch?v=gguLptAx0l4
I expect the Detail view to stay there even when the bottom sheet appears. Does anyone have any idea on why this happens and how to fix it?
Here is my sample code:
import Combine
import SwiftUI
import RealmSwift
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
TabItemView(num: 1)
.tabItem {
Text("One")
}
TabItemView(num: 2)
.tabItem {
Text("Two")
}
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)")) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
This works on iOS 14 btw
UPDATE 1:
Tried #Sebastian's suggestion of putting NavigationView inside of TabView. While this fixed the nav bug, it fundamentally changed the behavior (I don't want to show the tabs in DetailView).
Also tried his suggestion of using Introspect to set navigationController.hidesBottomBarWhenPushed = true on the NavigationLink destination, but that didn't do anything:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}.tabItem {
Text("Two")
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)").introspectNavigationController { navigationController in
navigationController.hidesBottomBarWhenPushed = true
}) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
You need to flip how you nest TabView & NavigationView. Instead of nesting several TabView views inside a NavigationView, use the TabView as the parent component, with a NavigationView for each tab.
This is how the updated ContentView would look like:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}
.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}
.tabItem {
Text("Two")
}
}
}
}
This makes sense and is more correct: The tabs should always be visible, but you want to show a different navigation stack with different content in each tab.
That it worked previously doesn't make it more correct - SwiftUI probably just changed its mind on dealing with unexpected situations. That, and the lack of error messages in these situations, is the downside of using a framework that tries to render anything you throw at it!
If the goal is specifically to hide the tabs when pushing a new view on a NavigationView (e.g., when tapping on a conversation in a messaging app), you have to use a different solution. Apple added the UIViewController.hidesBottomBarWhenPushed property to UIKit to support this specific use case.
This property is set on the UIViewController that, when presented, should not show a toolbar. In other words: Not the UINavigationController or the UITabBarController, but the child UIViewController that you push onto the UINavigationController.
This property is not supported in SwiftUI natively. You could set it using SwiftUI-Introspect, or simply write the navigation structure of your application using UIKit and write the views inside in SwiftUI, linking them using UIHostingViewController.

Swiftui navigation to notification view and back to detail view of a list

Expected use case
Tab B -> list View -> Detail View
On tapping notification
Tab B -> list view -> Detail view -> Notification detail view
navigating back
Tab B -> list view -> Detail View
but it working like below
Tab B -> list View -> Detail View
On tapping notification
Tab B -> list view -> Detail view -> Notification detail view
navigating back
Tab B -> list view
struct ContentView: View {
let todoPublisher = NotificationCenter.default.publisher(for: NSNotification.Name("Detail"))
#State var show: Bool = false
#State var navigationTitle: String = "First"
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Detail(), isActive: self.$show) { Text("")}.hidden()
// HiddenNavigationLink(destination: Detail(), isActive: self.$show)
TabView() {
FirtstView(navigationTitle: self.$navigationTitle)
.tabItem {
Image(systemName: "1.circle")
Text("First")
}.tag(0)
ListView(navigationTitle: self.$navigationTitle)
// ListView()
.tabItem {
Image(systemName: "2.circle")
Text("Second")
}.tag(1)
}
}
.navigationBarTitle(navigationTitle)
}
.onReceive(todoPublisher) {notification in
self.show = true
}
}
}
Here is the list view code
struct ListView: View {
#Binding var navigationTitle: String
var body: some View {
List {
ForEach(0..<5) {data in
NavigationLink(destination: DetailView()) {
Text("Text for row \(data)")
}
}
}
.onAppear() {
self.navigationTitle = "Second"
}
}
}
A NavigationLink always has a parent, ie. a view that contains the NavigationLink. When you tap on the back arrow it will pop the NavigationLink and navigates back to this parent view.
In your case you're presenting a hidden NavigationLink from the ContentView. Which means that the ContentView becomes a parent of the NavigationLink.
If you want the NavigationLink's back arrow to navigate to the DetailView instead, you need to initiate the NavigationLink from the DetailView.
Which means that your hidden NavigationLink needs to be presented from a DetailView:
struct DetailView: View {
...
var body: some View {
...
NavigationLink(destination: Detail(), isActive: self.$show) {
EmptyView()
}.hidden()
}
}

PresentationMode.dismiss weird behaviour when using multiple NavigationLinks inside ForEach

My app has 4 views (let's call them View_A[root] -> View_B -> View_C -> View_D). The navigation between them was made using NavigationView/NavigationLink.
When I call self.presentationMode.wrappedValue.dismiss() from the last view(View_D) I expect it to dismiss the current view (D) only, but for some reason it dismissed ALL the views and stops at view A (root view).
That's weird.
I spent a couple of hours trying to figure out what's going on there and I found that
- if I remove "ForEach" from "View_A" it works correctly and only the last view is dismissed. Even though ForEach gets just 1 static object in this example.
The second weird thing is that
- if I don't change "self.thisSession.stats" to false it also works correctly dismissing only the last view.
This is super weird as View_A (as far as I understand) is not dependent on thisSession environment variable.
Any ideas on how to prevent View_C and View_B from being dismissed in this case? I wanna end up at View_C after clicking the link, not at View_A.
Any help is appreciated, it took me a while to find out where it comes from but I'm not smart enough to proceed any further ;)
import SwiftUI
struct A_View: View {
#EnvironmentObject var thisSession: CurrentSession
var body: some View {
NavigationView {
VStack {
Text("View A")
ForEach([TestObject()], id: \.id) { _ in
NavigationLink(destination: View_B() ) {
Text("Move to View B")
}
}
}
}
}
}
struct View_B: View {
var body: some View {
NavigationView {
NavigationLink(destination: View_C()
) {
Text("GO TO VIEW C")
}
}
}
}
struct View_C: View {
var body: some View {
ZStack {
NavigationView {
NavigationLink(destination: View_D()) {
Text("GO TO VIEW D")
}
}
}
}
}
struct View_D: View {
#EnvironmentObject var thisSession: CurrentSession
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
ZStack {
VStack {
Button(action: {
self.thisSession.stats = false
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Return!")
}
}
}
}
}
class CurrentSession: ObservableObject {
#Published var stats: Bool = false
#Published var user: String = "user"
}
struct TestObject: Identifiable, Codable {
let id = UUID()
}
Your issue is with:
NavigationView
There is only supposed to be one NavigationView in an entire view stack. Try removing the NavigationView from views B and C