Swiftui: How to add Accessibility Identifier on navigationTitle - ios

How can we add the Accessibility Identifier to NaviagationTitle Text. I know for buttons/text/Image/stack views we can use .accessibility(identifier: “some_identifier”).
struct SomeView: View {
var body: some View {
VStack {
Text("Title Text")
.accessibility(identifier: "title")
}
.navigationTitle("title") //How to add accessibilityIdentifier to Navigation bar title?
//.navigationTitle(Text("title").accessibility(identifier: "title"))
}
}
unable to add the modifier to .navigationBarTitle(Text(“title”), displayMode: .inline). Accessibility Identifiers are required for XCUI automation testing.

I don't think this is possible in SwiftUI using .accessibility(identifier:) - it might be worth submitting feedback to Apple.
However, you can still access the navigation bar by its identifier - just the default identifier is the text:
.navigationTitle("title")
let app = XCUIApplication()
app.launch()
assert(app.navigationBars["title"].exists) // true
Alternatively, you can try to access UINavigationBar using a helper extension (adapted from here):
struct NavigationBarAccessor: UIViewControllerRepresentable {
var callback: (UINavigationBar?) -> Void
private let proxyController = ViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationBarAccessor>) -> UIViewController {
proxyController.callback = callback
return proxyController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationBarAccessor>) {}
typealias UIViewControllerType = UIViewController
private class ViewController: UIViewController {
var callback: (UINavigationBar?) -> Void = { _ in }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
callback(navigationController?.navigationBar)
}
}
}
Now you can access UINavigationBar from a SwiftUI view:
struct ContentView: View {
var body: some View {
NavigationView {
Text("text")
.navigationTitle("title")
.background(
NavigationBarAccessor {
$0?.accessibilityIdentifier = "id123"
}
)
}
}
}
Note that in the above example you set accessibilityIdentifier to the UINavigationBar itself and not to the title directly.

Related

How to add a title to a UIViewController that is pushed via a SwiftUI NavigationLink?

I am pushing a UIViewController that conforms to UIViewControllerRepresentable on to the navigation stack from a SwiftUI View via NavigationLink and I would like to set the title for the ViewController.
However, I am unable to and just get an empty title. I suspect this is a SwiftUI bug but was hoping that I am perhaps missing something.
Below are the lines I have tried that don't work.
title = "Title"
navigationItem.title = "Title"
Here is code to reproduce the issue
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink {
ViewControllerRepresentable()
} label: {
Text("Go to ViewController")
}
}
.navigationTitle(Text("SwiftUI View"))
}
}
}
struct ViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
func makeUIViewController(context: Context) -> ViewController {
ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "title"
title = "title"
}
}
Try setting the title in SwiftUI:
NavigationLink {
ViewControllerRepresentable()
.navigationTitle("My UIViewController")
} label: {
Text("Go to ViewController")
}
And then your can remove these lines from your view controller unless you intend to use it in UIKit as well (in which case just navigationItem.title should be enough):
navigationItem.title = "title"
title = "title"

How do I call a function written in UIKit/UIController/Storyboard from SwiftUI button

I am a SwiftUI newbie struggling to add SwiftUI functionality to my existing UIKit/Storyboard code. I would like to call a UIKit function from SwiftUI button. Greatly appreciate your help. Here is the relevant code simplified for this discussion.
From the code below, I'd like to call the functions startAction() and stopAction() from the If statement in SwiftUI...
if (startStop_flag) {
//******** call StartAction()
} else {
//******* call StopAction()
}
The entire code below.
Some context: when the app is run, the bottom half of the screen will show "UIkit Storyboard View" and show the button "Open Swift Container View". When the user clicks this button, the SwiftUI container view will open up. This view will display "This is a swiftUI view" and display a button "Start/Stop". When the user clicks this button, StartAction() or StopAction() needs to be called. These two functions reside in the UIViewController. I hope I am clear with the problem and the request.
ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var nativeView: UIView!
#IBOutlet weak var nativeView_Label: UILabel!
#IBOutlet weak var nativeView_openSwiftViewBtn: UIButton!
#IBOutlet weak var containerView_forSwiftUI: UIView!
#IBSegueAction func embedSwiftUIView(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(coder: coder, rootView: SwiftUIView2(text: "Container View"))
}
var toggleOpenCloseContainerView : Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
containerView_forSwiftUI.isHidden = true
}
#IBAction func openContainerView_touchInside(_ sender: Any) {
if (toggleOpenCloseContainerView) {
containerView_forSwiftUI.isHidden = false
toggleOpenCloseContainerView = false
nativeView_openSwiftViewBtn.setTitle("Close Swift Container View", for: .normal)
} else {
containerView_forSwiftUI.isHidden = true
toggleOpenCloseContainerView = true
nativeView_openSwiftViewBtn.setTitle("Open Swift Container View", for: .normal)
}
}
// These two functions need to be called from the SwiftUI's button.
func startAction() {
print("Start Action called from SwiftUI's button")
}
func stopAction() {
print("Stop Action called from SwiftUI's button")
}
}
The swiftUI functions are in this file
struct SwiftUIView2: View {
var text: String
#State var startStop_flag: Bool = true
var body: some View {
VStack {
Text(text)
HStack {
Image(systemName: "smiley")
Text("This is a SwiftUI View")
Spacer()
Button("\(startStop_flag ? "Start": "Stop")") {
startStop_flag = !startStop_flag
if (startStop_flag) {
//******** call StartAction()
} else {
//******* call StopAction()
}
} .padding()
.background(Color.red)
.cornerRadius(40)
.foregroundColor(.white)
.padding(5)
.overlay(
RoundedRectangle(cornerRadius: 40)
.stroke(Color.red, lineWidth: 1)
)
}
}
.font(.largeTitle)
.background(Color.blue)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView2(text: "Sample Text")
}
}
You can use closures for this. First, define and call them inside your SwiftUI view.
struct SwiftUIView2: View {
var text: String
var startAction: (() -> Void) /// define closure 1
var stopAction: (() -> Void) /// define closure 2
...
...
Button("\(startStop_flag ? "Start": "Stop")") {
startStop_flag = !startStop_flag
if (startStop_flag) {
//******** call StartAction()
startAction()
} else {
//******* call StopAction()
stopAction()
}
}
}
Then, just assign the closure's contents inside ViewController.swift.
#IBSegueAction func embedSwiftUIView(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(
coder: coder,
rootView: SwiftUIView2(
text: "Container View",
startAction: { [weak self] in
self?.startAction()
},
stopAction: { [weak self] in
self?.stopAction()
}
)
)
}
Here is the easiest way to open UIkit ViewController from SwiftUI Button Press.
Button("Next Page") {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootViewController = storyboard.instantiateViewController(withIdentifier: "ProfileVC")
if let window = UIApplication.shared.windows.first {
window.rootViewController!.present(rootViewController, animated: true)
}
}
}

How can I change the status bar text color per view in SwiftUI?

I'm looking for a way to change the text color of the status bar that allows a different text color to be used for each view.
I've seen this Q&A, but it's not what I'm looking for. I'm not looking for solutions that only allow for one status bar text color for all views. I want to change the status bar text color for each view. For example, one view might have a dark background and so I need light text. I might navigate to another view with a light background, so now I need dark text. The suggested duplicate answer only returns .lightContent, which means that the status bar text color cannot change dynamically when I move to a different view.
This answer here works on my machine, but it's not performant. A comment under it corroborates this. The lag is unacceptable, so this solution is not good.
Other solutions I've seen so far cause this particular error:
Compiling failed: extensions of generic classes cannot contain '#objc' members
I've also tried using an Environment Object inside my Custom Controller:
import SwiftUI
/// Allows for the status bar colors to be changed from black to white on the dark gray title bar
class Controller<ContentView> : UIHostingController<ContentView> where ContentView : View {
#EnvironmentObject var statusBarTextColor: StatusBarTextColor
lazy var isDark: Bool = self.statusBarTextColor.isDark
override var preferredStatusBarStyle: UIStatusBarStyle {
return isDark ? .lightContent : .darkContent
}
}
This results in the error:
Thread 1: Fatal error: No ObservableObject of type StatusBarTextColor found. A View.environmentObject(_:) for StatusBarTextColor may be missing as an ancestor of this view.
Inside my SceneDelegate file, I do specify the StatusBarTextColor environmentObject:
window.rootViewController = Controller(
rootView: Home()
.environmentObject(PostData())
.environmentObject(CardPosition())
.environmentObject(StatusBarTextColor())
)
And this is the ObservableObject itself:
import Combine
import SwiftUI
final class StatusBarTextColor: ObservableObject {
#Published var isDark: Bool = true
}
If I were to guess why this doesn't work, I'd say it's because the Controller gets initialized before StatusBarTextColor is available.
The more I look into this problem, the more I think there isn't a solution. I've gone through just about every article, answer, and video on the subject. They all either use a Controller to only return .lightContent, or use storyboards and multiple controllers, which isn't what I'm using.
You can use the solution you found here, but instead of using onDisappear, which will have a delay for the color change until the view is completely gone, you can create a view modifier called onWillDisappear that exposes viewWillDisappear. The color change will happen as sooner.
Usage:
struct MyClass: View {
#Environment(\.localStatusBarStyle) var statusBarStyle
// ...
SomeView()
}.onAppear {
self.statusBarStyle.currentStyle = .darkContent
}
.onWillDisappear {
self.statusBarStyle.currentStyle = .lightContent
}
}
Code:
import SwiftUI
class HostingController<Content>: UIHostingController<Content> where Content: View {
private var internalStyle = UIStatusBarStyle.lightContent
#objc override dynamic open var preferredStatusBarStyle: UIStatusBarStyle {
get {
internalStyle
}
set {
internalStyle = newValue
self.setNeedsStatusBarAppearanceUpdate()
}
}
override init(rootView: Content) {
super.init(rootView:rootView)
LocalStatusBarStyleKey.defaultValue.getter = { self.preferredStatusBarStyle }
LocalStatusBarStyleKey.defaultValue.setter = { self.preferredStatusBarStyle = $0 }
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
class LocalStatusBarStyle { // style proxy to be stored in Environment
fileprivate var getter: () -> UIStatusBarStyle = { .default }
fileprivate var setter: (UIStatusBarStyle) -> Void = {_ in}
var currentStyle: UIStatusBarStyle {
get { self.getter() }
set { self.setter(newValue) }
}
}
// Custom Environment key, as it is set once, it can be accessed from anywhere
// of SwiftUI view hierarchy
struct LocalStatusBarStyleKey: EnvironmentKey {
static let defaultValue: LocalStatusBarStyle = LocalStatusBarStyle()
}
extension EnvironmentValues { // Environment key path variable
var localStatusBarStyle: LocalStatusBarStyle {
get {
return self[LocalStatusBarStyleKey.self]
}
}
}
struct WillDisappearHandler: UIViewControllerRepresentable {
func makeCoordinator() -> WillDisappearHandler.Coordinator {
Coordinator(onWillDisappear: onWillDisappear)
}
let onWillDisappear: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<WillDisappearHandler>) -> UIViewController {
context.coordinator
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WillDisappearHandler>) {
}
typealias UIViewControllerType = UIViewController
class Coordinator: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
struct WillDisappearModifier: ViewModifier {
let callback: () -> Void
func body(content: Content) -> some View {
content
.background(WillDisappearHandler(onWillDisappear: callback))
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
self.modifier(WillDisappearModifier(callback: perform))
}
}
See original post with onWillDisappear code here
In your SceneDelegate you inject StatusBarTextColor() to the Home view. However, you declared the EvironmentObject in Controller.

How do you access UIViewControllerRepresentable NavigationBar property?

In SwiftUI if you are transitioning using a NavigationLink() into a UIViewControllerRepresentable how would you; say, add buttons or change the title property on the navigationbar.
This is what I am doing right now:
import SwiftUI
/// Controls the actual action performed by the button upon taps.
struct CatagoryButton: View {
#State var isPresenting :Bool = false
var company : Company?
var text : String
var body: some View {
NavigationLink(destination: UIKitWrapper(company: self.company, storyboardPointer: self.text)
.navigationBarTitle(self.text)
.edgesIgnoringSafeArea(.all),
isActive: self.$isPresenting,
label: {
Button(action: {
self.isPresenting.toggle()
}){
ZStack {
ButtonShadowLayer(text: text)
GradientBackground()
.mask(ButtonBaseLayer())
CircleAndTextLayer(text: text)
}
}
})
}
}
Here is the struct for my representable.
import SwiftUI
/// Wraps UIKIT instance in a representable that swiftUI can present.
struct UIKitWrapper: UIViewControllerRepresentable {
//Specify what type of controller is being wrapped in an associated type.
typealias UIViewControllerType = UIViewController
//Company property passed from parent view. Represents the company the user selected from main view.
private var company : Company
//Determines which viewcontroller will be presented to user. This string corresponds to the name of the storyboard file in the main bundle.
private var storyboardPointer : String
init(company: Company?, storyboardPointer: String) {
guard let company = company else {fatalError()}
self.company = company
self.storyboardPointer = storyboardPointer
}
func makeUIViewController(context: Context) -> UIViewControllerType {
//Find user defined storyboard in bundle using name.
let storyboard = UIStoryboard(name: storyboardPointer, bundle: .main)
//Downcast returned controller to protocol AccessControllerProtocol. This step is required because we are not sure which storyboard will be accessed. Potential storyboard controllers that can be called all conform to this protocol.
//FIXME: Remove fatalError and create error enum asap.
guard let viewController = storyboard.instantiateInitialViewController() as? AccessControllerProtocol else { fatalError() }
//Assign user selected company object to instance property on incoming viewController.
viewController.company = company
//Return UINavigationController with storyboard instance view controller as root controller.
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Finally, here is one of the classes that use the representable.
import UIKit
class OrdersViewController: UIViewController, AccessControllerProtocol {
var company : Company!
#IBOutlet var companyNameLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setBackgroundColor()
companyNameLabel.text = company.name
self.navigationController?.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.tapRightBarButton))]
}
func setBackgroundColor(){
let backgroundGradient = BackgroundGradientSetter()
let viewWithGradient = backgroundGradient.setGradientToView(with: [DarkBlueHue_DEFAULT,LightBlueHue_DEFAULT], size: view.bounds)
view.addSubview(viewWithGradient)
view.sendSubviewToBack(viewWithGradient)
}
#objc func tapRightBarButton(){
}
}
No matter what I do this button doesn't show up. I'm not sure if I need to put this in a makeCoordinator() or if there is just something I am missing. If anyone has insight please let me know!
If it isn't available in viewDidLoad, try calling your setupNavigation() in viewWillAppear()
In your case navigationController is not available yet on viewDidLoad, try instead as in below demo module
Tested & works with Xcode 11.2 / iOS 13.2
class MyUIController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.navigationController?.navigationBar.topItem?.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.onAdd(_:)))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// might be needed to remove injected item here
}
#objc func onAdd(_ sender: Any?) {
print(">> tapped add")
}
}
struct MyInjector: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyInjector>) -> MyUIController {
MyUIController()
}
func updateUIViewController(_ uiViewController: MyUIController, context: UIViewControllerRepresentableContext<MyInjector>) {
}
}
struct DemoNavigationBarUIButton: View {
var body: some View {
NavigationView {
MyInjector()
.navigationBarTitle("Demo")
}
}
}
struct DemoNavigationBarUIButton_Previews: PreviewProvider {
static var previews: some View {
DemoNavigationBarUIButton()
}
}

How to give back swipe gesture in SwiftUI the same behaviour as in UIKit (interactivePopGestureRecognizer)

The interactive pop gesture recognizer should allow the user to go back the the previous view in navigation stack when they swipe further than half the screen (or something around those lines). In SwiftUI the gesture doesn't get canceled when the swipe wasn't far enough.
SwiftUI: https://imgur.com/xxVnhY7
UIKit: https://imgur.com/f6WBUne
Question:
Is it possible to get the UIKit behaviour while using SwiftUI views?
Attempts
I tried to embed a UIHostingController inside a UINavigationController but that gives the exact same behaviour as NavigationView.
struct ContentView: View {
var body: some View {
UIKitNavigationView {
VStack {
NavigationLink(destination: Text("Detail")) {
Text("SwiftUI")
}
}.navigationBarTitle("SwiftUI", displayMode: .inline)
}.edgesIgnoringSafeArea(.top)
}
}
struct UIKitNavigationView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationController {
let host = UIHostingController(rootView: content())
let nvc = UINavigationController(rootViewController: host)
return nvc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}
I ended up overriding the default NavigationView and NavigationLink to get the desired behaviour. This seems so simple that I must be overlooking something that the default SwiftUI views do?
NavigationView
I wrap a UINavigationController in a super simple UIViewControllerRepresentable that gives the UINavigationController to the SwiftUI content view as an environmentObject. This means the NavigationLink can later grab that as long as it's in the same navigation controller (presented view controllers don't receive the environmentObjects) which is exactly what we want.
Note: The NavigationView needs .edgesIgnoringSafeArea(.top) and I don't know how to set that in the struct itself yet. See example if your nvc cuts off at the top.
struct NavigationView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationController {
let nvc = UINavigationController()
let host = UIHostingController(rootView: content().environmentObject(nvc))
nvc.viewControllers = [host]
return nvc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}
extension UINavigationController: ObservableObject {}
NavigationLink
I create a custom NavigationLink that accesses the environments UINavigationController to push a UIHostingController hosting the next view.
Note: I didn't implement the selection and isActive that the SwiftUI.NavigationLink has because I don't fully understand what they do yet. If you want to help with that please comment/edit.
struct NavigationLink<Destination: View, Label:View>: View {
var destination: Destination
var label: () -> Label
public init(destination: Destination, #ViewBuilder label: #escaping () -> Label) {
self.destination = destination
self.label = label
}
/// If this crashes, make sure you wrapped the NavigationLink in a NavigationView
#EnvironmentObject var nvc: UINavigationController
var body: some View {
Button(action: {
let rootView = self.destination.environmentObject(self.nvc)
let hosted = UIHostingController(rootView: rootView)
self.nvc.pushViewController(hosted, animated: true)
}, label: label)
}
}
This solves the back swipe not working correctly on SwiftUI and because I use the names NavigationView and NavigationLink my entire project switched to these immediately.
Example
In the example I show modal presentation too.
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 30) {
NavigationLink(destination: Text("Detail"), label: {
Text("Show detail")
})
Button(action: {
self.isPresented.toggle()
}, label: {
Text("Show modal")
})
}
.navigationBarTitle("SwiftUI")
}
.edgesIgnoringSafeArea(.top)
.sheet(isPresented: $isPresented) {
Modal()
}
}
}
struct Modal: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 30) {
NavigationLink(destination: Text("Detail"), label: {
Text("Show detail")
})
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Dismiss modal")
})
}
.navigationBarTitle("Modal")
}
}
}
Edit: I started off with "This seems so simple that I must be overlooking something" and I think I found it. This doesn't seem to transfer EnvironmentObjects to the next view. I don't know how the default NavigationLink does that so for now I manually send objects on to the next view where I need them.
NavigationLink(destination: Text("Detail").environmentObject(objectToSendOnToTheNextView)) {
Text("Show detail")
}
Edit 2:
This exposes the navigation controller to all views inside NavigationView by doing #EnvironmentObject var nvc: UINavigationController. The way to fix this is making the environmentObject we use to manage navigation a fileprivate class. I fixed this in the gist: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb
You can do this by descending into UIKit and using your own UINavigationController.
First create a SwipeNavigationController file:
import UIKit
import SwiftUI
final class SwipeNavigationController: UINavigationController {
// MARK: - Lifecycle
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// This needs to be in here, not in init
interactivePopGestureRecognizer?.delegate = self
}
deinit {
delegate = nil
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
duringPushAnimation = true
super.pushViewController(viewController, animated: animated)
}
var duringPushAnimation = false
// MARK: - Custom Functions
func pushSwipeBackView<Content>(_ content: Content) where Content: View {
let hostingController = SwipeBackHostingController(rootView: content)
self.delegate = hostingController
self.pushViewController(hostingController, animated: true)
}
}
// MARK: - UINavigationControllerDelegate
extension SwipeNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
}
// MARK: - UIGestureRecognizerDelegate
extension SwipeNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == interactivePopGestureRecognizer else {
return true // default value
}
// Disable pop gesture in two situations:
// 1) when the pop animation is in progress
// 2) when user swipes quickly a couple of times and animations don't have time to be performed
let result = viewControllers.count > 1 && duringPushAnimation == false
return result
}
}
This is the same SwipeNavigationController provided here, with the addition of the pushSwipeBackView() function.
This function requires a SwipeBackHostingController which we define as
import SwiftUI
class SwipeBackHostingController<Content: View>: UIHostingController<Content>, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.delegate = nil
}
}
We then set up the app's SceneDelegate to use the SwipeNavigationController:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let hostingController = UIHostingController(rootView: ContentView())
window.rootViewController = SwipeNavigationController(rootViewController: hostingController)
self.window = window
window.makeKeyAndVisible()
}
Finally use it in your ContentView:
struct ContentView: View {
func navController() -> SwipeNavigationController {
return UIApplication.shared.windows[0].rootViewController! as! SwipeNavigationController
}
var body: some View {
VStack {
Text("SwiftUI")
.onTapGesture {
self.navController().pushSwipeBackView(Text("Detail"))
}
}.onAppear {
self.navController().navigationBar.topItem?.title = "Swift UI"
}.edgesIgnoringSafeArea(.top)
}
}

Resources