Swiftui searchable Cancel button - swiftui

I have a NavigationView with .searchable, when clicking Cancel I want to go to the previous View like Back button.
İ use #Environment(\.isSearching), it is running, I can print Searching canceled but how to navigate to the previous View ?

You can simply listen to the changes made to isSearching, and pop your view using #Environment(\.presentationMode):
.onChange(of: isSearching) { newValue in
guard !newValue else {return}
presentationMode.wrappedValue.dismiss()
//for iOS 16 +, use dismiss.callAsFunction()
}
Note: presentationMode is first deprecated in iOS 15+, so for newer versions use #Environment(\.dismiss).

If you are using iOS 13+, you can use #Environment(\.dismiss) var dismiss
And then use dismiss() in the action

Related

How do you fix Xcode 14 warning: NavigationLink presenting a value must appear inside a NavigationContent-based NavigationView. Link will be disabled

Since installing Xcode 14, I am now getting the following error message printed in my console:
NavigationLink presenting a value must appear inside a
NavigationContent-based NavigationView. Link will be disabled.
My app is structured as follows:
I have View A wrapped in a NavigationView. The Navigation View has a navigation link inside it that links to View B.
I have View B that doesn't have a Navigation View, but has a navigation link to View C. View B inherits the navigation view defined in View A
The warning is printed when I press the back button on View B, popping back to View A.
The warning goes away when I wrap View B in a NavigationView, but this of course now displays View B in two Navigation Views, which is not what I want.
I'm unsure why this warning is printing, because View B inherits the NavigationView defined in View A.
I had the same issue. Adding a check for iOS16 and using the new navigationstack if true fixed it for me.
WindowGroup {
if #available(iOS 16.0, *) {
NavigationStack {
ContentView()
}
} else {
// Fallback on earlier versions
NavigationView {
ContentView()
}
}
}
Will be deprecated in ios16,
The official documentation link is provided here, you can view the specific details
Deprecated
Use NavigationStack and NavigationSplitView instead. For more
information, see Migrating to new navigation types.
https://developer.apple.com/documentation/swiftui/navigationview
Try to add .navigationViewStyle(.stack) to the NavigationView. It helps in my case.
NavigationView {
// View A
}
.navigationViewStyle(.stack)

Can a NavigationLink perform an async function when navigating to another view?

I'm trying to create a navigation link in SwiftUI that logs in a user and navigates to the next screen.
I've tried using .simultaneousGesture as shown below, based on this solution. When I have it perform a simple action (e.g. print("hello")), the code works fine and navigates to the next page, but when I have it perform my authState.signUp function (which is async), it calls the function but doesn't navigate to the next page.
Is there a different way I should be approaching this?
NavigationLink(destination: NextView()) {
Text("Create account")
}
.simultaneousGesture(TapGesture().onEnded {
authState.signUp(user: user)
})
you can use a selection arg of navigationLink
add this var
#State var trigger: Bool? = nil
and use
NavigationLink(destination: NextView(), tag: true,
selection: $trigger ) {
}
when ever your other job (login) is done toggle() the trigger and navigationLink fires.

NavigationLink doesn't fire after FullScreenCover is dismissed

I have a button in a view (inside a NavigationView) that opens a full screen cover - a loading screen while some data is processing. When the cover is dismissed, I want to automatically route to the next view programmatically. I'm using a NavigationLink with a tag and selection binding, and the binding value updates when the cover is dismissed, but the routing doesn't happen unless I tap that same "open modal" button again.
import SwiftUI
struct OpenerView: View {
#EnvironmentObject var viewModel: OpenerViewModel
#State private var selection: Int? = nil
#State private var presentLoadingScreen = false
var body: some View {
VStack {
NavigationLink(destination: SecondScreen(), tag: 1, selection: $selection) { EmptyView() }
Button(action: {
viewModel.frequency = 0
self.presentLoadingScreen.toggle()
}, label: {
Text("Open cover")
}).buttonStyle(PlainButtonStyle())
}
.navigationBarTitle("Nav title", displayMode: .inline)
.fullScreenCover(isPresented: $presentLoadingScreen, onDismiss: {
self.selection = 1
}, content: ModalView.init)
}
}
struct ModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Howdy")
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
presentationMode.wrappedValue.dismiss()
}
}
}
}
The first time I hit the Button, the cover opens. Inside the cover is just a DispatchQueue.main.asyncAfter which dismisses it after 2 seconds. When it's dismissed, the onDismiss fires, but then I have to hit the button again to route to SecondScreen.
Any ideas?
Edit: Added the modal's View
I got it working with some changes to the code, and I'm sharing here along with what I think is happening.
I believe the problem is a race condition using a #State boolean to toggle the cover and the navigation. When the cover is being dismissed, my main OpenerView is being recreated - to be expected with state changes. Because of this, I try to set the #State var selection to trigger the navigation change, but before it can do so, the view is recreated with selection = nil.
There seem to be two ways to solve it in my case:
Move the cover boolean to my view model - this worked, but I didn't want it there because it only applied to this view and it's a shared view model for this user flow. Plus, when the modal is dismissed, you see the current OpenerView for a brief flash and then get routed to the SecondScreen.
Keep the cover boolean in #State, but trigger the navigation change in the button immediately after setting the boolean to open the modal. This worked better for my use case because the modal opens, and when it closes, the user is already on the next screen.
I had a similar problem where I was trying to draw a view after dismissing a fullScreenCover. I kept getting an error that said that the view had been deallocated since it was trying to draw to the fullScreenCover.
I used Joe's hints above to make this work. Specifically:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
viewToShow()
}
I had previously tried onChange, onDisappear, onAppear - but none of those fit the use case I needed.

Missing Destination when pop to Root View

I have referred to this Stack Overflow thread SwiftUI How to Pop to root view.
However it doesn't work for my case, of course no answer fits each persons use case perfectly so one always has to modify the answer slightly but of course keeping to the overall outline of the solution.
I have looked at this thread How to popup multiple view off navigation stack, but I am not sure resetting the scene is the best option? There has got to be a "normal" way?
The solution I went with is use an ObservableObject and set it as an EnvironmentObject of the root view.
The navigation stack grows by 4 views:
RootView().environmentObject(AppSettings.shared)
FirstView()
SecondView()
ThirdView()
FourthView()
The NavigationLink isActive state for the FirstView is defined in AppSettings.shared, all the other states are found in the subsequent view and not in AppSettings.
For example:
FirstView -> SecondView the SecondView isActive state is defined in the ViewModel of the FirstView, and so on and so forth.
What I am trying to achieve is to pop to RootView from the FourthView. So on the FourthView there is an environmentObject variable of type AppSettings (passed down as an EnvironmentObject from RootView) and on button press, toggle RootView -> FirstView isActive state to false.
It toggles but there's no navigation.
However, in the debug console this is the error
Trying to pop to a missing destination at /Library/Caches/com.apple.xbs/Sources/Monoceros/Monoceros-42.24.100/Shared/NavigationBridge_PhoneTV.swift:205
From my understanding toggling that state to false should trigger a navigation back, but in the Stack Overflow thread there was a post to use #State variable from RootView -> FirstView and in the EnvironmentObject have a variable moveToDashbord. Then on the RootView add a .onReceive modifier to listen to moveToDashboard publisher and then trigger #State variable. But again that also results in the same debug console message.
In short all solutions result in a missing destination console message. Is this because the navigation is too deep?
This is an iPad only project, and the navigationView style is set to StackedNavigationStyle.
System Details:
Xcode 11.6
iOS/ PadOS Target 13.0 (so not using SwiftUI 2.0, if that is a thing)
Code examples:
This is the SceneDelegate which sets the AppSettings as system wide EnvironmentObject.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = LoginView().environmentObject(AppSettings.shared)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
This is an example of the navigationLink to the RootView
NavigationLink(destination: RootView().navigationBarBackButtonHidden(true), isActive: self.$loginViewModel.loginSuccess) {
EmptyView()
}
This is an example of navigationLink from RootView -> FirstView:
NavigationLink(destination: FirstView().environmentObject(FirstViewModel()), isActive: self.$appSettings.firstView) {
EmptyView()
}.isDetailLink(false)
Note. I had to change the actual names of the Views for clarification sake.
I found the issue, on one of the views there was a UIViewControllerRepresentable wrapped UIAlertController. It was structured as a view modifier with provision for content.
This basically wrapped over the original view which ended up breaking the navigation stack.
The solution was to rather wrap a UIViewController which is presented by the View and in that ViewController resides the UIAlertController.
This solution for the UIAlertController was used and adapted SwiftUI - UIAlertController

Create floating view with custom outline like in Notability

How to create such a view floating view with a custom boarder as shown in the picture? And such that it disappears as soon as the user clicks outside of the view.
Normally you would do that with a Popover like this:
#State var isPresented = false
var body: some View {
Button(action: {
self.isPresented = true
}) {
Text("Press me")
}.popover(isPresented: $isPresented, arrowEdge: .top) {
Text("Pop!") // You can put you own custom view here for the popover
}
}
Although it works as intended on the iPad (and I believe tvOS too, but I haven't tested it), it does not work properly with the current version of SwiftUI (as of 10/12/2019) on iPhones. Currently, the above code will just result in a somewhat glitchy modal on an iPhone, which I don't think is the intended function of it on iPhones. Apple's documentation for popover isn't very helpful right now, but here it is anyway.
For you information .popover is unabailable in tvOS.