Bagaimana mengembalikan gesture gesek di SwiftUI perilaku yang sama seperti di UIKit (interactivePopGestureRecognizer)

9

Pengenal isyarat pop interaktif harus memungkinkan pengguna untuk kembali ke tampilan sebelumnya di tumpukan navigasi ketika mereka menggesek lebih dari setengah layar (atau sesuatu di sekitar garis-garis). Di SwiftUI gerakan itu tidak dibatalkan ketika geseknya tidak cukup jauh.

SwiftUI: https://imgur.com/xxVnhY7

UIKit: https://imgur.com/f6WBUne


Pertanyaan:

Apakah mungkin untuk mendapatkan perilaku UIKit saat menggunakan tampilan SwiftUI?


Mencoba

Saya mencoba menyematkan UIHostingController di dalam UINavigationController tetapi itu memberikan perilaku yang sama persis seperti 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) {}
}
Casper Zandbergen
sumber

Jawaban:

4

Saya akhirnya mengabaikan default NavigationViewdan NavigationLinkuntuk mendapatkan perilaku yang diinginkan. Ini kelihatannya sangat sederhana sehingga saya harus mengabaikan sesuatu yang dilakukan oleh tampilan SwiftUI default?

NavigationView

Saya membungkus dengan UINavigationControllersuper sederhana UIViewControllerRepresentableyang memberikan UINavigationControllertampilan konten SwiftUI sebagai environmentObject. Ini berartiNavigationLink nantinya dapat mengambil bahwa selama itu di pengontrol navigasi yang sama (pengontrol tampilan yang disajikan tidak menerima objek environment) yang persis apa yang kita inginkan.

Catatan: NavigationView perlu .edgesIgnoringSafeArea(.top)dan saya belum tahu cara mengaturnya di struct sendiri. Lihat contoh jika nvc Anda terpotong di atas.

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

NavigasiLink

Saya membuat NavigationLink khusus yang mengakses lingkungan UINavigationController untuk mendorong UIHostingController yang menjadi hosting tampilan berikutnya.

Catatan: Saya belum mengimplementasikan selectiondan isActiveyang dimiliki SwiftUI.NavigationLink karena saya belum sepenuhnya mengerti apa yang mereka lakukan. Jika Anda ingin membantu dengan itu, silakan komentar / 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)
    }
}

Ini menyelesaikan gesekan kembali tidak berfungsi dengan benar pada SwiftUI dan karena saya menggunakan nama NavigationView dan NavigationLink seluruh proyek saya segera beralih ke ini.

Contoh

Dalam contoh saya menunjukkan presentasi modal juga.

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

Sunting: Saya mulai dengan "Ini tampak sangat sederhana sehingga saya harus mengabaikan sesuatu" dan saya pikir saya menemukannya. Ini sepertinya tidak mentransfer EnvironmentObjects ke tampilan berikutnya. Saya tidak tahu bagaimana NavigationLink standar melakukannya sehingga untuk saat ini saya secara manual mengirim objek ke tampilan berikutnya di mana saya membutuhkannya.

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

Edit 2:

Ini memperlihatkan pengontrol navigasi untuk semua tampilan di dalam NavigationViewdengan melakukan @EnvironmentObject var nvc: UINavigationController. Cara untuk memperbaikinya adalah dengan membuat environmentObject yang kita gunakan untuk mengelola navigasi kelas pribadi-pribadi. Saya memperbaiki ini di intinya: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb

Casper Zandbergen
sumber
Tipe argumen 'UINavigationController' tidak sesuai dengan tipe yang diharapkan 'ObservableObject'
stardust4891
@kejodion Saya lupa menambahkan itu ke posting stackoverflow tapi itu di intinya:extension UINavigationController: ObservableObject {}
Casper Zandbergen
Ini memperbaiki bug swipe kembali yang saya alami tetapi sayangnya tampaknya tidak mengakui perubahan untuk mengambil permintaan dan apa pun cara Navigasi default tidak.
stardust4891
@kejodion Ah itu terlalu buruk, saya tahu bahwa solusi ini memiliki masalah dengan environmentObjects. Tidak yakin apa yang Anda minta permintaan. Mungkin membuka pertanyaan baru.
Casper Zandbergen
Yah saya punya beberapa permintaan pengambilan yang secara otomatis diperbarui di UI ketika saya menyimpan konteks objek yang dikelola. Untuk beberapa alasan mereka tidak berfungsi ketika saya menerapkan kode Anda. Saya BENAR-BENAR berharap mereka melakukannya, karena ini memperbaiki masalah menggesek kembali saya telah mencoba untuk memperbaiki selama berhari-hari.
stardust4891
1

Anda dapat melakukan ini dengan turun ke UIKit dan menggunakan UINavigationController Anda sendiri.

Pertama buat SwipeNavigationControllerfile:

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

Ini sama dengan yang SwipeNavigationControllerdisediakan di sini , dengan penambahan pushSwipeBackView()fungsi.

Fungsi ini membutuhkan apa SwipeBackHostingControlleryang kita definisikan sebagai

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

Kami kemudian menyiapkan aplikasi SceneDelegateuntuk menggunakan 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()
    }

Akhirnya gunakan di 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)
    }
}
neptune
sumber
1
SwipeNavigationController kustom Anda sebenarnya tidak mengubah apa pun dari perilaku UINavigationController default. Untuk func navController()mengambil vc dan kemudian mendorong vc sendiri sebenarnya adalah ide bagus dan membantu saya memecahkan masalah ini! Saya akan menjawab jawaban yang lebih ramah SwiftUI, tetapi terima kasih atas bantuan Anda!
Casper Zandbergen