Cách cung cấp lại cử chỉ vuốt trong SwiftUI tương tự như trong UIKit (InteractivePopGestureRecognizer)


9

Trình nhận dạng cử chỉ pop tương tác sẽ cho phép người dùng quay lại chế độ xem trước đó trong ngăn xếp điều hướng khi họ vuốt hơn một nửa màn hình (hoặc một cái gì đó xung quanh các dòng đó). Trong SwiftUI, cử chỉ không bị hủy khi thao tác vuốt không đủ xa.

SwiftUI: https://imgur.com/xxVnhY7

UIKit: https://imgur.com/f6WBUne


Câu hỏi:

Có thể có được hành vi UIKit trong khi sử dụng chế độ xem SwiftUI không?


Nỗ lực

Tôi đã cố gắng nhúng UIhostingControll bên trong UINavestionControll nhưng điều đó mang lại hành vi chính xác giống như 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) {}
}

Câu trả lời:


4

Tôi đã kết thúc việc ghi đè mặc định NavigationViewNavigationLinkđể có được hành vi mong muốn. Điều này có vẻ đơn giản đến mức tôi phải xem một cái gì đó mà các chế độ xem SwiftUI mặc định làm gì?

Điều hướng

Tôi gói gọn UINavigationControllertrong một siêu đơn giản UIViewControllerRepresentablecung cấp cho UINavigationControllerchế độ xem nội dung SwiftUI dưới dạng môi trường. Điều này có nghĩa là NavigationLinksau này có thể lấy được miễn là trong cùng một bộ điều khiển điều hướng (trình điều khiển chế độ xem được trình bày không nhận được môi trường môi trường), đó chính xác là những gì chúng ta muốn.

Lưu ý: NavigationView cần .edgesIgnoringSafeArea(.top)và tôi chưa biết cách đặt nó trong cấu trúc. Xem ví dụ nếu nvc của bạn bị cắt ở đầu.

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

Điều hướng

Tôi tạo một NavigationLink tùy chỉnh truy cập vào các môi trường UINavestionControll để đẩy UIhostingControll lưu trữ khung nhìn tiếp theo.

Lưu ý: Tôi đã không triển khai selectionisActiveSwiftUI.NavulationLink có vì tôi chưa hiểu hết những gì họ làm. Nếu bạn muốn giúp với điều đó xin vui lòng bình luận / chỉnh sửa.

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

Điều này giải quyết thao tác vuốt ngược không hoạt động chính xác trên SwiftUI và vì tôi sử dụng tên NavigationView và NavigationLink, toàn bộ dự án của tôi đã chuyển sang những cái này ngay lập tức.

Thí dụ

Trong ví dụ tôi cũng trình bày phương thức.

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

Chỉnh sửa: Tôi bắt đầu với "Điều này có vẻ đơn giản đến mức tôi phải nhìn ra thứ gì đó" và tôi nghĩ rằng tôi đã tìm thấy nó. Điều này dường như không chuyển môi trường môi trường sang chế độ xem tiếp theo. Tôi không biết NavigationLink mặc định làm điều đó như thế nào nên bây giờ tôi tự gửi các đối tượng đến chế độ xem tiếp theo nơi tôi cần chúng.

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

Chỉnh sửa 2:

Điều này cho thấy bộ điều khiển điều hướng cho tất cả các khung nhìn bên trong NavigationViewbằng cách thực hiện @EnvironmentObject var nvc: UINavigationController. Cách để khắc phục điều này là tạo môi trườngObject mà chúng ta sử dụng để quản lý điều hướng một lớp fileprivate. Tôi đã sửa lỗi này trong ý chính: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb


Loại đối số 'UINavestionControll' không phù hợp với loại dự kiến ​​'ObservableObject'
stardust4891

@kejodion Tôi quên thêm nó vào bài viết stackoverflow nhưng nó nằm trong ý chính:extension UINavigationController: ObservableObject {}
Casper Zandbergen

Nó đã sửa một lỗi vuốt ngược mà tôi gặp phải nhưng thật không may, nó dường như không thừa nhận các thay đổi đối với các yêu cầu tìm nạp và không chú ý đến cách mà NavigationView mặc định thực hiện.
stardust4891

@kejodion Ah thật tệ, tôi biết rằng giải pháp này có vấn đề với môi trường môi trường. Không chắc chắn những gì bạn yêu cầu. Có lẽ mở một câu hỏi mới.
Casper Zandbergen

Vâng, tôi có một số yêu cầu tìm nạp tự động được cập nhật trong giao diện người dùng khi tôi lưu bối cảnh đối tượng được quản lý. Vì một số lý do, chúng không hoạt động khi tôi triển khai mã của bạn. Tôi thực sự muốn họ đã làm, bởi vì điều này đã khắc phục sự cố vuốt ngược mà tôi đã cố gắng khắc phục trong nhiều ngày.
stardust4891

1

Bạn có thể làm điều này bằng cách hạ xuống UIKit và sử dụng UINavlationControll của riêng bạn.

Đầu tiên tạo một SwipeNavigationControllertệp:

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

Điều này cũng SwipeNavigationControllerđược cung cấp ở đây , với việc bổ sung pushSwipeBackView()chức năng.

Hàm này yêu cầu một hàm SwipeBackHostingControllermà chúng ta định nghĩa là

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

Sau đó, chúng tôi thiết lập ứng dụng SceneDelegateđể sử dụng 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()
    }

Cuối cùng sử dụng nó trong 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
SwipeNavlationControll tùy chỉnh của bạn không thực sự thay đổi bất cứ điều gì từ hành vi UINavestionControll mặc định. Việc func navController()lấy vc và sau đó tự đẩy vc thực sự là một ý tưởng tuyệt vời và giúp tôi tìm ra vấn đề này! Tôi sẽ trả lời một câu trả lời thân thiện hơn SwiftUI, nhưng cảm ơn bạn đã giúp đỡ!
Casper Zandbergen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.