A popover in NavigationLink does not work properly - ios

When you tap Button2 below, the popover closes immediately for the first time only.
Here is the reproducible code.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("Button1", destination: ChildView())
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ChildView: View {
#State var isPresented = false
var body: some View {
Button("Button2") {
isPresented = true
}
.popover(isPresented: $isPresented) {
Text("Sheet")
}
}
}
Xcode12.4
iPad Pro 11inch / iOS14.0
Screen Capture Video
https://gist.github.com/shtnkgm/d8ba240f588d3e73ea13c1ff2e6cb1b5#gistcomment-3640543

Related

SwiftUI - Attempt to present SwiftUI.PlatformAlertController... whose view is not in the window hierarchy

While this question has been asked previously, none of the threads deal with the latest SwiftUI and iOS15+.
I am seeing this error in console when navigating to a second level in a custom tab view, where the NavigationView is instantiated outside of the custom tab view.
The full error is:
[Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x7f8962874800> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f896080dac0> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f896080dac0>) whose view is not in the window hierarchy
The hierarchy is as follows:
Login Page (NavigationView instantiated here) -> Tap NavigationLink
from this view
Custom Tab View is displayed -> Tap any tab and see
the correct view
Tap a NavigationLink from within one of these views
and the alert works perfectly
Tap a NavigationLink from within that view and the error is shown and no alerts work
I know it's a lot of code, but I have been asked to provide code that will compile, and since this is quite a few different files to make this happen, I stripped everything down to the bare minimum so the issue could be recreated.
Here's the code:
App:
import SwiftUI
#main
struct TestApp: App {
#StateObject var viewRouter: ViewRouter
init() {
let viewRouter = ViewRouter()
_viewRouter = StateObject(wrappedValue: viewRouter)
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewRouter)
}
}
}
ViewRouter:
import Foundation
class ViewRouter: ObservableObject {
#Published var accessView: DisplayView = .home
}
enum DisplayView {
case settings
case home
}
ContentView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: HomeView()
) {
Text("Go To Tab View")
}
.isDetailLink(false)
}
}
}
HomeView:
import SwiftUI
struct HomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
switch viewRouter.accessView {
case .home:
UserHome()
.navigationBarHidden(true)
case .settings:
UserSettings()
.navigationBarHidden(true)
}
}
}
struct HomeNavView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 30) {
HStack(alignment: .bottom) {
Image(systemName: "House")
.onTapGesture {
viewRouter.accessView = .home
}
.frame(maxWidth: .infinity)
Image(systemName: "Person")
.onTapGesture {
viewRouter.accessView = .settings
}
.frame(maxWidth: .infinity)
}
}
}
.ignoresSafeArea()
.zIndex(1)
}
}
UserHome:
import SwiftUI
struct UserHome: View {
var body: some View {
Text("User Home View")
HomeNavView()
}
}
UserSettings:
import SwiftUI
struct UserSettings: View {
var body: some View {
NavigationLink(
destination: SectionView()
) {
Text("Go To Section View")
}
}
}
SectionView (if you try to display an alert here, it works just fine):
import SwiftUI
struct SectionView: View {
var body: some View {
NavigationLink(
destination: AlertView()
) {
Text("Go To Alert View")
}
}
}
AlertView (This is where you see the error in console and the alert does not work):
import SwiftUI
struct AlertView: View {
#State private var alertTest: Bool = false
var body: some View {
Button("Show Alert") {
alertTest = true
}
.alert(
"Alert",
isPresented: $alertTest,
actions: {
Button("OK", role: .cancel) {
}
}, message: {
Text("The alert is working!")
})
}
}

SwiftUI Modal Inherits SearchBar during Sheet Presentation

Consider the following example with a list and a button wrapped in a HStack that opens up a sheet:
struct ContentView: View {
#State var text: String = ""
#State var showSheet = false
var body: some View {
NavigationView {
List {
HStack {
button
}
Text("Hello World")
}
.searchable(text: $text)
}
}
var button: some View {
Button("Press", action: { showSheet = true })
.sheet(isPresented: $showSheet) {
modalView
}
}
var modalView: some View {
NavigationView {
List {
Text("Test")
}
}
}
}
On press of the button, a modal is presented to the user. However, the searchable modifier gets passed to the modal, see this video.
Now if the HStack is removed, everything works fine:
List {
button
Text("Hello World")
}
In addition, everything works also fine if the modal is not a NavigationView:
var modalView: some View {
List {
Text("Test")
}
}
Does somebody know what the problem here might be or is it once again one of those weird SwiftUI bugs?
putting the sheet, outside of the button and the List, works for me. I think .sheet is not meant to be inside a List, especially where searchable is operating.
struct ContentView: View {
#State var text: String = ""
#State var showSheet = false
var body: some View {
NavigationView {
List {
HStack {
button
}
Text("Hello World")
}
.searchable(text: $text)
}
.sheet(isPresented: $showSheet) {
modalView
}
}
var button: some View {
Button("Press", action: { showSheet = true })
}
var modalView: some View {
NavigationView {
List {
Text("Test")
}
}
}
}
Another workaround is to use navigationBarHidden = true, but then you must live without the navigation bar in the sheet view.
var modalView: some View {
NavigationView {
List {
Text("Test")
}
.navigationBarHidden(true)
}
}
Btw, on iPadOS it helps to use .searchable(text: $text, placement: .sidebar)

SwiftUI - How can I dismiss a view to root and then push a second view immediately afterwards?

Couldn't find anything relating to this issue in SwiftUI.
I have three views currently, RootView, DetailView1 and DetailView2. RootView will feature a button to push and show DetailView1, inside DetailView1 there will be a NavigationLink to dismiss DetailView1 to RootView and push DetailView2.
struct DetailView1: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
NavigationLink(destination: DetailView2()) {
Text("Tap to dismiss DetailView and show DetailView2")
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
struct DetailView2: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(
"This is DetailView2",
action: { self.presentationMode.wrappedValue.dismiss() }
)
}
}
struct RootView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView1())
{ Text("This is root view. Tap to go to DetailView") }
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}
Expected behaviour:
User presses NavigationLink in DetailView1, the view is dismissed to RootView and DetailView2 is pushed up.
Current behaviour:
User presses NavigationLink in DetailView1, the view is dismissed to RootView, DetailView2 is not pushed.
You can do this by passing the active state of the second view. And change the #State value. It's a similar concept to the completion block in UIKit.
DetailView1
struct DetailView1: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Binding var isActiveDetails2: Bool //<-- Here
var body: some View {
VStack {
Button("Tap to dismiss DetailView and show DetailView2") {
isActiveDetails2 = true //<-- Here
}
}
}
}
RootView
struct RootView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var isActiveDetails2: Bool = false
var body: some View {
VStack {
NavigationLink(destination: DetailView1(isActiveDetails2: $isActiveDetails2))
{ Text("This is root view. Tap to go to DetailView") }
NavigationLink(destination: DetailView2(), isActive: $isActiveDetails2) { //<-- Here
EmptyView()
}
}
}
}

SwiftUI transition from modal sheet to regular view with Navigation Link

I'm working with SwiftUI and I have a starting page. When a user presses a button on this page, a modal sheet pops up.
In side the modal sheet, I have some code like this:
NavigationLink(destination: NextView(), tag: 2, selection: $tag) {
EmptyView()
}
and my modal sheet view is wrapped inside of a Navigation View.
When the value of tag becomes 2, the view does indeed go to NextView(), but it's also presented as a modal sheet that the user can swipe down from, and I don't want this.
I'd like to transition from a modal sheet to a regular view.
Is this possible? I've tried hiding the navigation bar, etc. but it doesn't seem to make a difference.
Any help with this matter would be appreciated.
You can do this by creating an environmentObject and bind the navigationLink destination value to the environmentObject's value then change the value of the environmentObject in the modal view.
Here is a code explaining what I mean
import SwiftUI
class NavigationManager: ObservableObject{
#Published private(set) var dest: AnyView? = nil
#Published var isActive: Bool = false
func move(to: AnyView) {
self.dest = to
self.isActive = true
}
}
struct StackOverflow6: View {
#State var showModal: Bool = false
#EnvironmentObject var navigationManager: NavigationManager
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: self.navigationManager.dest, isActive: self.$navigationManager.isActive) {
EmptyView()
}
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}
}
}
.sheet(isPresented: self.$showModal) {
secondView(isPresented: self.$showModal).environmentObject(self.navigationManager)
}
}
}
struct StackOverflow6_Previews: PreviewProvider {
static var previews: some View {
StackOverflow6().environmentObject(NavigationManager())
}
}
struct secondView: View {
#EnvironmentObject var navigationManager: NavigationManager
#Binding var isPresented: Bool
#State var dest: AnyView? = nil
var body: some View {
VStack {
Text("Modal view")
Button(action: {
self.isPresented = false
self.dest = AnyView(thirdView())
}) {
Text("Press me to navigate")
}
}
.onDisappear {
// This code can run any where but I placed it in `.onDisappear` so you can see the animation
if let dest = self.dest {
self.navigationManager.move(to: dest)
}
}
}
}
struct thirdView: View {
var body: some View {
Text("3rd")
.navigationBarTitle(Text("3rd View"))
}
}
Hope this helps, if you have any questions regarding this code, please let me know.

onDisappear not called when a modal View is dismissed

I rely on the SwiftUI's .onDisappear to perform some logic but it is not being called when the user dismisses a modally presented view with the swipe gesture. To reproduce
Present a view modally a "ChildView 1"
On this view, push a "ChildView 2" as a navigation child
Swipe down to dismiss the modal view.
The .onDisappear of "ChildView 2" is not called.
Sample code to reproduce
import SwiftUI
struct ContentView: View {
#State var isShowingModal
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
}
}
}
struct ChildView: View {
let title: Int
var body: some View {
NavigationLink(destination: ChildView(title: title + 1)) {
Text("Show Child")
}
.navigationBarTitle("View \(title)")
.onAppear {
print("onAppear ChildView \(self.title)")
}
.onDisappear {
print("onDisappear ChildView \(self.title)")
}
}
}
The output is:
onAppear ChildView 1
onAppear ChildView 2
onDisappear ChildView 1
If you're looking for logic to occur when the actual modal is dismissed, you're going to want to call that here, where I print out Modal Dismissed:
struct ContentView: View {
#State var isShowingModal = false
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
.onDisappear {
print("Modal Dismissed")
}
}
}
}
struct ContentView: View {
#State var isShowingModal = false
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
}
}
}
in this code, you have NavigationView, and when presenting sheet, you push there another NavigationView. This is the source of trouble
You don't need any NavigationView to present modals. If you like to present another modal from modal, you can use
import SwiftUI
struct ContentView: View {
var body: some View {
ChildView(title: 1)
}
}
struct ChildView: View {
#State var isShowingModal = false
let title: Int
var body: some View {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal \(title)").font(.largeTitle)
}
.sheet(isPresented: $isShowingModal) {
ChildView(title: self.title + 1)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
UPDATE
from Apple
Human Interface Guidelines
Modality is a design technique that presents content in a temporary mode that’s separate from the user's previous current context and requires an explicit action to exit

Resources