I have some code below that creates a custom navigation stack that enables full screen swipes to navigation away from the view. I want to support iOS 17 with this custom Navigation stack but some of the on change modifiers are only for iOS 17.
How can I modify .onChange(of: isEnabled, initial: true) { oldValue, newValue in
which is inside of fileprivate struct FullSwipeModifier: ViewModifier {
to work with iOS16 and have the same functionality? I tried using an onReceive
modifier instead but I couldn't get that to work.
Additionally where would be the best spot to execute a function when a view is swiped away. I don't want the function to execute mid swipe but rather at the end of the swipe when the view has been popped from the navigation stack. I can definitely use a onDisappear
but I'd rather not for my specific case.
import SwiftUI
struct ContentView: View {
@State private var isEnabled: Bool = false
var body: some View {
FullSwipeNavigationStack {
NavigationLink("Leading Swipe View") {
Text("hello").enableFullSwipePop(isEnabled)
}
}
}
}
struct FullSwipeNavigationStack<Content: View>: View {
@ViewBuilder var content: Content
/// Full Swipe Custom Gesture
@State private var customGesture: UIPanGestureRecognizer = {
let gesture = UIPanGestureRecognizer()
gesture.name = UUID().uuidString
gesture.isEnabled = false
return gesture
}()
var body: some View {
NavigationStack {
content
.background {
AttachGestureView(gesture: $customGesture)
}
}
.environment(\.popGestureID, customGesture.name)
.onReceive(NotificationCenter.default.publisher(for: .init(customGesture.name ?? "")), perform: { info in
if let userInfo = info.userInfo, let status = userInfo["status"] as? Bool {
customGesture.isEnabled = status
}
})
}
}
extension View {
@ViewBuilder
func enableFullSwipePop(_ isEnabled: Bool) -> some View {
self
.modifier(FullSwipeModifier(isEnabled: isEnabled))
}
}
/// Custom Environment Key for Passing Gesture ID to it's subviews
fileprivate struct PopNotificationID: EnvironmentKey {
static var defaultValue: String?
}
fileprivate extension EnvironmentValues {
var popGestureID: String? {
get {
self[PopNotificationID.self]
}
set {
self[PopNotificationID.self] = newValue
}
}
}
/// Helper View Modifier
fileprivate struct FullSwipeModifier: ViewModifier {
var isEnabled: Bool
/// Gesture ID
@Environment(\.popGestureID) private var gestureID
func body(content: Content) -> some View {
content
.onChange(of: isEnabled, initial: true) { oldValue, newValue in
guard let gestureID = gestureID else { return }
NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: [
"status": newValue
])
}
.onDisappear(perform: {
guard let gestureID = gestureID else { return }
NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: [
"status": false
])
})
}
}
/// Helper Files
fileprivate struct AttachGestureView: UIViewRepresentable {
@Binding var gesture: UIPanGestureRecognizer
func makeUIView(context: Context) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
/// Finding Parent Controller
if let parentViewController = uiView.parentViewController {
if let navigationController = parentViewController.navigationController {
/// Checking if already the gesture has been added to the controller
if let _ = navigationController.view.gestureRecognizers?.first(where: { $0.name == gesture.name }) {
print("Already Attached")
} else {
navigationController.addFullSwipeGesture(gesture)
print("Attached")
}
}
}
}
}
}
fileprivate extension UINavigationController {
/// Adding Custom FullSwipe Gesture
/// Special thanks for this SO Answer
/// https://stackoverflow.com/questions/20714595/extend-default-interactivepopgesturerecognizer-beyond-screen-edge
func addFullSwipeGesture(_ gesture: UIPanGestureRecognizer) {
guard let gestureSelector = interactivePopGestureRecognizer?.value(forKey: "targets") else { return }
gesture.setValue(gestureSelector, forKey: "targets")
view.addGestureRecognizer(gesture)
}
}
fileprivate extension UIView {
var parentViewController: UIViewController? {
sequence(first: self) {
$0.next
}.first(where: { $0 is UIViewController}) as? UIViewController
}
}
You can try this solution, hope it solves your query.
First, replace the FullSwipeModifier
code with the following code for iOS 16 compatibility.
fileprivate struct FullSwipeModifier: ViewModifier {
var isEnabled: Bool
/// Gesture ID
@Environment(\.popGestureID) private var gestureID
func body(content: Content) -> some View {
content
.onAppear {
guard let gestureID = gestureID else { return }
NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: [
"status": isEnabled
])
}
.onDisappear {
guard let gestureID = gestureID else { return }
NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: [
"status": false
])
}
}
}
You can use the .onDisappear
modifier in your ContentView with a condition, it will be called when the view is fully swiped.
struct ContentView: View {
@State private var isEnabled: Bool = false
var body: some View {
FullSwipeNavigationStack {
NavigationLink("Leading Swipe View") {
Text("hello")
.enableFullSwipePop(isEnabled)
.onDisappear {
// Check here, if the view is being dismissed due to a swipe
if !isEnabled {
// "View swiped away!"
print("View swiped away!")
}
}
}
}
}
}