วิธีการปัดนิ้วกลับด้วยท่าทางใน SwiftUI มีพฤติกรรมเช่นเดียวกับใน UIKit (interactivePopGestureRecognizer)


9

ระบบจดจำลายนิ้วมือแบบอินเทอร์แอคทีฟควรอนุญาตให้ผู้ใช้ย้อนกลับไปยังมุมมองก่อนหน้าในการนำทางแบบสแต็กเมื่อพวกเขาปัดหน้าจอมากกว่าครึ่งหนึ่งของหน้าจอ ใน SwiftUI ท่าทางไม่ได้ถูกยกเลิกเมื่อการกวาดนิ้วนั้นไม่ไกลพอ

SwiftUI: https://imgur.com/xxVnhY7

UIKit: https://imgur.com/f6WBUne


คำถาม:

เป็นไปได้ไหมที่จะรับพฤติกรรมของ UIKit ในขณะที่ใช้มุมมอง SwiftUI


ความพยายามในการ

ฉันพยายามฝัง UIHostingController ไว้ภายใน UINavigationController แต่นั่นให้พฤติกรรมเดียวกับ 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) {}
}

คำตอบ:


4

ฉันสิ้นสุดการเอาชนะค่าเริ่มต้นNavigationViewและNavigationLinkเพื่อให้ได้พฤติกรรมตามที่ต้องการ ดูเหมือนง่ายมากที่ฉันต้องมองสิ่งที่ SwiftUI เริ่มต้นทำ

NavigationView

ฉันห่อUINavigationControllerในแบบง่ายสุดUIViewControllerRepresentableที่ให้UINavigationControllerมุมมองเนื้อหา SwiftUI เป็น environmentObject ซึ่งหมายความว่าNavigationLinkสามารถคว้าภายหลังได้ตราบใดที่มันอยู่ในตัวควบคุมการนำทางเดียวกัน (ตัวควบคุมมุมมองที่นำเสนอไม่ได้รับ environmentObjects) ซึ่งเป็นสิ่งที่เราต้องการอย่างแท้จริง

หมายเหตุ: NavigationView ต้องการ.edgesIgnoringSafeArea(.top)และฉันไม่ทราบวิธีการตั้งค่าใน struct เอง ดูตัวอย่างว่า nvc ของคุณถูกตัดที่ด้านบน

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

ฉันสร้าง NavigationLink แบบกำหนดเองที่เข้าถึงสภาพแวดล้อม UINavigationController เพื่อผลัก UIHostingController ที่โฮสต์มุมมองถัดไป

หมายเหตุ:ฉันไม่ได้ติดตั้งselectionและisActiveSwiftUI.NavigationLink มีเพราะฉันยังไม่เข้าใจอย่างเต็มที่ถึงสิ่งที่พวกเขาทำ หากคุณต้องการความช่วยเหลือโปรดแสดงความคิดเห็น / แก้ไข

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)
    }
}

วิธีนี้จะช่วยให้การปัดย้อนกลับไม่ทำงานอย่างถูกต้องใน SwiftUI และเนื่องจากฉันใช้ชื่อ NavigationView และ NavigationLink โครงการทั้งหมดของฉันเปลี่ยนไปใช้สิ่งเหล่านี้ทันที

ตัวอย่าง

ในตัวอย่างฉันแสดงการนำเสนอ modal เกินไป

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")
        }
    }
}

แก้ไข: ฉันเริ่มด้วย "สิ่งนี้ดูเรียบง่ายจนฉันต้องมองอะไรบางอย่าง" และฉันคิดว่าฉันพบมัน ดูเหมือนจะไม่ถ่ายโอน EnvironmentObjects ไปยังมุมมองถัดไป ฉันไม่ทราบว่า NavigationLink เริ่มต้นทำเช่นนั้นได้อย่างไรตอนนี้ฉันส่งวัตถุไปยังมุมมองถัดไปที่ฉันต้องการด้วยตนเอง

NavigationLink(destination: Text("Detail").environmentObject(objectToSendOnToTheNextView)) {
    Text("Show detail")
}

แก้ไข 2:

นี้ exposes ตัวควบคุมการนำทางไปยังทุกมุมมองภายในโดยการทำNavigationView @EnvironmentObject var nvc: UINavigationControllerวิธีในการแก้ไขปัญหานี้คือการทำให้สิ่งแวดล้อมวัตถุประสงค์ที่เราใช้เพื่อจัดการการนำทางของคลาสไฟล์ส่วนบุคคล ฉันแก้ไขสิ่งนี้ในส่วนสำคัญ: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb


อาร์กิวเมนต์ชนิด 'UINavigationController' ไม่สอดคล้องกับประเภทที่คาดไว้ 'ObservableObject'
stardust4891

@kejodion ฉันลืมที่จะเพิ่มไปยังโพสต์ stackoverflow แต่มันอยู่ในส่วนสำคัญ:extension UINavigationController: ObservableObject {}
Casper Zandbergen

แก้ไขข้อผิดพลาดการปัดย้อนกลับที่ฉันพบ แต่น่าเสียดายที่ดูเหมือนว่าจะไม่ยอมรับการเปลี่ยนแปลงในการดึงข้อมูลการร้องขอและอะไรก็ตามที่ NavigationView เริ่มต้นทำ
stardust4891

@kejodion อาแย่มากฉันรู้ว่าวิธีนี้มีปัญหากับ environmentObjects ไม่แน่ใจว่าคำขอข้อมูลที่คุณหมายถึงอะไร อาจเปิดคำถามใหม่
Casper Zandbergen

ฉันมีคำขอการดึงข้อมูลหลายครั้งซึ่งจะได้รับการอัปเดตโดยอัตโนมัติใน UI เมื่อฉันบันทึกบริบทวัตถุที่จัดการ ด้วยเหตุผลบางอย่างพวกเขาไม่ทำงานเมื่อฉันใช้รหัสของคุณ ฉันหวังว่าพวกเขาจะทำได้เพราะนี่เป็นปัญหาการปัดย้อนกลับที่ฉันพยายามแก้ไขมาหลายวันแล้ว
stardust4891

1

คุณสามารถทำได้โดยลงไปที่ UIKit และใช้ UINavigationController ของคุณเอง

สร้างSwipeNavigationControllerไฟล์ก่อน:

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
    }
}

นี่คือสิ่งเดียวกันที่SwipeNavigationControllerให้ไว้ที่นี่ด้วยการเพิ่มpushSwipeBackView()ฟังก์ชั่น

ฟังก์ชั่นนี้ต้องการสิ่งSwipeBackHostingControllerที่เรานิยามว่าเป็น

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
    }
}

จากนั้นเราตั้งค่าแอพSceneDelegateให้ใช้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()
    }

ในที่สุดใช้มันใน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)
    }
}

1
SwipeNavigationController ที่กำหนดเองของคุณไม่ได้เปลี่ยนแปลงอะไรเลยจากพฤติกรรม UINavigationController เริ่มต้น การfunc navController()คว้า vc จากนั้นผลัก vc ด้วยตัวคุณเองจริง ๆ แล้วเป็นความคิดที่ดีและช่วยฉันหาปัญหานี้ออกมา! ฉันจะตอบคำตอบที่เป็นมิตรกับ SwiftUI มากขึ้น แต่ขอบคุณสำหรับความช่วยเหลือของคุณ!
Casper Zandbergen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.