Wrapped SwiftUI View popping itself - swiftui

I have a SwiftUI View wrapped in a UIHostingController that's then pushed onto the nav stack from a UIViewController.
It's pushed in this way:
let controller = TransactionDetailsHostingController(transactionRecord: record)
navigationController?.pushViewController(controller, animated: true)
I want a custom back button that will pop the view from the navstack and I tried the technique used here and elsewhere: SwiftUI - Is there a popViewController equivalent in SwiftUI?
Unfortunately, it doesn't work. The wrapped view doesn't pop from the navstack. I need a solution that works on iOS 13.

There are two approaches for pop to view the controller.
Use closure. Create closure inside the SwiftUI view and call from the controller.
Here is the demo.
struct SwiftUIView: View {
var onBack: () -> Void
var body: some View {
Button(action: {
onBack()
}, label: {
Text("Go to back")
})
}
}
class ViewController: UIViewController {
#IBAction func onNavigation(_ sender: UIButton) {
let controller = UIHostingController(rootView: SwiftUIView(onBack: {
self.navigationController?.popViewController(animated: true)
}))
navigationController?.pushViewController(controller, animated: true)
}
}
Find the top most controller and pop. You can find top controller from here
And then use like this
UIApplication.shared.topMostViewController()?.navigationController?.popViewController(animated: true)

Related

Navigate from one embedded UIViewController in swiftUI to another embedded UIViewcontroller in swift UI

I have a UIViewController which is embedded in swiftUI View
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(overlayShow: OverlayShow())
}
}
struct OnboardingWrapper: UIViewControllerRepresentable {
var overlayShow: OverlayShow
func makeUIViewController(context: Context) -> ReturnSomthing {
let controller = ReturnSomthing(shareConfig: onboardingConfig)
controller.showOverlay = overlayShow
return controller
}
func updateUIViewController(_ uiViewController: ReturnSomthing, context: Context) {
}
public typealias UIViewControllerType = ReturnSomthing
}
I want to be able to Navigate directly from SwiftUI to another SwiftUI view with embedded UIViewController
SO, I did on a view of SwiftUI
struct ContentView: View {
#ObservedObject var overlayShow: OverlayShow = OverlayShow()
var body: some View {
NavigationView {
NavigationLink(destination: OverlayView(overlay: LoadingUnknownOverlayViewContainer(description: "it is spinning :0", designModel: LoadingOverlayDesignModel(type: .unknown))), isActive: $overlayShow.isShowingOverlay) {
OnboardingWrapper(overlayShow: overlayShow)
}
}
}
}
the condition for navigation to be active is the overlayShow.isShowingOverlay is true. this variable is controlled in UIViewController with an ObservableObject.
the problem is that no matter the value of overlayShow.isShowingOverlay wherever I tap the first UIViewController which is OnboardingWrapper the navigation is activated and goes to the next page.
To be more clear I post a screen recording.
Please how can I solve this problem.
It sounds like you are seeing OnboardingWrapper and when you click on it, you are seeing OverlayView.
Perhaps this would help:
var body: some View {
NavigationView {
OnboardingWrapper(overlayShow: overlayShow)
NavigationLink(destination: OverlayView(
overlay:LoadingUnknownOverlayViewContainer(
description: "it is spinning :0",
designModel: LoadingOverlayDesignModel(type: .unknown))),
isActive: $overlayShow.isShowingOverlay) {
EmptyView()
}
}
}
OnboardingWrapper will appear, then when overlayShow.isShowingOverlay is true, the OverlayView should be presented.

How to pop multiple views off a navigation stack?

Looking for some guidance on a simple way to pop multiple views off a navigation stack in SwiftUI.
I have 4 views chained together using NavigationLink. At the last view I would like to jump back to the initial ContentView, popping all the other views off the stack. I don't want to use the "Back" button on the NavigationBar of each view to achieve this.
Thanks in advance.
Bob.
'''
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: BView()) {
Text("This is View A, now go to View B.")
}
}
}
}
}
struct BView: View {
var body: some View {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
struct CView: View {
var body: some View {
NavigationLink(destination: DView()) {
Text("This is View C, now go to View D.")
}
}
}
struct DView: View {
var body: some View {
// The following line adds ContentView onto the existing navigation stack. Instead, I want to pop the previous views off the stack, leaving me back at ContentView.
NavigationLink(destination: ContentView()) {
Text("This is View D, now jump back to View A.")
}
}
}
'''
It's not really "popping" views off of the stack, but your SceneDelegate can set the rootViewController to any View you want (see line 28 of default SceneDelegate.swift). In your case you want it to be ContentView again.
For example in your SceneDelegate add something like:
func toContentView() {
let contentView = ContentView()
window?.rootViewController = UIHostingController(rootView: contentView)
}
Then in DView, change the NavigationLink to a Button that just does:
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.toContentView()
If you have multiple scenes, you'll need a bit more.
Making Cenk Bilgen's answer more generic.
struct RootView {
static func change(to view: AnyView) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate else {
return
}
let contentView = view
sceneDelegate.window?.rootViewController = UIHostingController(rootView: contentView)
}
}
Usage:
RootView.change(to: AnyView(DashboardView()))

Disable swipe-back for a NavigationLink SwiftUI

How can I disable the swipe-back gesture in SwiftUI? The child view should only be dismissed with a back-button.
By hiding the back-button in the navigation bar, the swipe-back gesture is disabled. You can set a custom back-button with .navigationBarItems()
struct ContentView: View {
var body: some View {
NavigationView{
List{
NavigationLink(destination: Text("You can swipe back")){
Text("Child 1")
}
NavigationLink(destination: ChildView()){
Text("Child 2")
}
}
}
}
}
struct ChildView: View{
#Environment(\.presentationMode) var presentationMode
var body:some View{
Text("You cannot swipe back")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button("Back"){self.presentationMode.wrappedValue.dismiss()})
}
}
I use Introspect library then I just do:
import SwiftUI
import Introspect
struct ContentView: View {
var body: some View {
Text("A view that cannot be swiped back")
.introspectNavigationController { navigationController in
navigationController.interactivePopGestureRecognizer?.isEnabled = false
}
}
}
Only complete removal of the gesture recognizer worked for me.
I wrapped it up into a single modifier (to be added to the detail view).
struct ContentView: View {
var body: some View {
VStack {
...
)
.disableSwipeBack()
}
}
DisableSwipeBack.swift
import Foundation
import SwiftUI
extension View {
func disableSwipeBack() -> some View {
self.background(
DisableSwipeBackView()
)
}
}
struct DisableSwipeBackView: UIViewControllerRepresentable {
typealias UIViewControllerType = DisableSwipeBackViewController
func makeUIViewController(context: Context) -> UIViewControllerType {
UIViewControllerType()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
class DisableSwipeBackViewController: UIViewController {
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent?.parent,
let navigationController = parent.navigationController,
let interactivePopGestureRecognizer = navigationController.interactivePopGestureRecognizer {
navigationController.view.removeGestureRecognizer(interactivePopGestureRecognizer)
}
}
}
You can resolve the navigation controller without third party by using a UIViewControllerRepresentable in the SwiftUI hierarchy, then access the parent of its parent.
Adding this extension worked for me (disables swipe back everywhere, and another way of disabling the gesture recognizer):
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
This answer shows how to configure your navigation controller in SwiftUI (In short, use UIViewControllerRepresentable to gain access to the UINavigationController). And this answer shows how to disable the swipe gesture. Combining them, we can do something like:
Text("Hello")
.background(NavigationConfigurator { nc in
nc.interactivePopGestureRecognizer?.isEnabled = false
})
This way you can continue to use the built in back button functionality.
Setting navigationBarBackButtonHidden to true will lose the beautiful animation when you have set the navigationTitle.
So I tried another answer
navigationController.interactivePopGestureRecognizer?.isEnabled = false
But It's not working for me.
After trying the following code works fine
NavigationLink(destination: CustomView()).introspectNavigationController {navController in
navController.view.gestureRecognizers = []
}
preview
The following more replicates the existing iOS chevron image.
For the accepted answer.
That is replace the "back" with image chevron.
.navigationBarItems(leading: Button("Back"){self.presentationMode.wrappedValue.dismiss()})
With
Button(action: {self.presentationMode.wrappedValue.dismiss()}){Image(systemName: "chevron.left").foregroundColor(Color.blue).font(Font.system(size:23, design: .serif)).padding(.leading,-6)}

SwiftUI - navigationBarBackButtonHidden - swipe back gesture?

if I set a custom Back Button (which everyone wants, hiding the ugly text ;-) ) and using .navigationBarBackButtonHidden, the standard Swipe Back gesture on the navigation controller does not work. Is there a way to get this back and having a custom back button?
For Example:
NavigationView {
NavigationLink(destination: DummyViewer())
{
Text("Go to next view"
}
}
struct DummyViewer: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Text("Hello, World!").navigationBarBackButtonHidden(true)
.navigationBarItems(leading:
Button(action: { self.presentationMode.wrappedValue.dismiss()}) {
Text("Custom go back")
}
)
}
}
If I do so, I cannot swipe back to the previous view, seems the gesture is then disabled... How to get it back?
BR
Steffen
Nothing I found about creating a custom NavigationView worked but I found that by extending UINavigationController I was able to have a custom back button and the swipe back gesture.
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
I would like to integrate the answer given by Nick Bellucci to make the code also works in other circumstances, e.g. when the child view of the NavigationView is a ScrollView, or a View that is listening for Drag gestures.
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
// To make it works also with ScrollView
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
true
}
}
I've just created a hack which will not animate view but it works
extension View {
func onBackSwipe(perform action: #escaping () -> Void) -> some View {
gesture(
DragGesture()
.onEnded({ value in
if value.startLocation.x < 50 && value.translation.width > 80 {
action()
}
})
)
}
}
Usage
struct TestView: View {
#Environment (\.presentationMode) var mode
var body: some View {
VStack {
Color.red
}
.onBackSwipe {
mode.wrappedValue.dismiss()
}
}
}
You can set the title to an empty string. So back bar button title will be empty:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: Text("Here you are")) {
Text("Next").navigationBarTitle("")
}
}
}
}
You can set the title onAppear or onDisappear if you need to.
If it's still actual, here I answered, how to set custom back button and save swipe back gesture.

Disable drag to dismiss in SwiftUI Modal

I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}