SwiftUI에서 스 와이프 제스처를 UIKit (interactivePopGestureRecognizer)에서와 동일한 동작으로 되 돌리는 방법


9

대화식 팝 제스처 인식기는 사용자가 화면의 절반 이상 (또는 해당 선 주위)을 스 와이프 할 때 탐색 스택에서 이전보기로 되돌아 갈 수 있도록해야합니다. SwiftUI에서 스 와이프가 충분하지 않으면 제스처가 취소되지 않습니다.

SwiftUI : https://imgur.com/xxVnhY7

UIKit : https://imgur.com/f6WBUne


질문:

SwiftUI 뷰를 사용하는 동안 UIKit 동작을 얻을 수 있습니까?


시도

UINavigationController 내에 UIHostingController를 포함하려고 시도했지만 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

SwiftUI 컨텐츠 뷰에 ​​environmentObject로 제공 UINavigationController하는 매우 간단한 UIViewControllerRepresentable것으로 래핑 UINavigationController합니다. 이것은 NavigationLink나중에 동일한 네비게이션 컨트롤러 (표시 된 뷰 컨트롤러가 environmentObjects를 수신하지 않음)에있는 한 정확히 우리가 원하는 것을 얻을 수 있음을 의미합니다.

참고 : NavigationView가 필요 .edgesIgnoringSafeArea(.top)하지만 구조체 자체에서 NavigationView 를 설정하는 방법을 모르겠습니다. 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

다음보기를 호스팅하는 UIHostingController를 푸시하기 위해 UINavigationController 환경에 액세스하는 사용자 정의 NavigationLink를 작성합니다.

참고 : 나는를 구현하지 않은 selectionisActiveSwiftUI.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라는 이름을 사용하기 때문에 전체 프로젝트가 즉시 전환되었습니다.

이 예에서는 모달 프레젠테이션도 보여줍니다.

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 :

이렇게하면 내비게이션 컨트롤러가를 NavigationView수행하여 내부의 모든보기에 노출 됩니다 @EnvironmentObject var nvc: UINavigationController. 이를 해결하는 방법은 네비게이션을 파일 개인 클래스로 관리하는 데 사용하는 environmentObject를 만드는 것입니다. 나는 이것을 요점에서 고쳤다 : https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb


인수 유형 '은 UINavigationController은'예상 유형 'ObservableObject'을 준수하지 않는
stardust4891

@kejodion 나는 그것을 stackoverflow 포스트에 추가하는 것을 잊었지만 그것은 요점에 있었다 :extension UINavigationController: ObservableObject {}
Casper Zandbergen

그것은 내가 경험했던 뒤로 스 와이프 버그를 수정했지만 불행히도 가져 오기 요청에 대한 변경 사항을 인정하지 않는 것으로 보이며 기본 NavigationView 가하는 방식이 아닙니다.
stardust4891

@ kejodion 아 너무 나쁘다, 나는이 솔루션에 environmentObjects에 문제가 있음을 알고있다. 가져 오기 요청이 무엇을 의미하는지 잘 모르겠습니다. 새로운 질문을 여길 수도 있습니다.
캐스퍼 잔드 베르겐

관리 객체 컨텍스트를 저장할 때 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를 밀어 내게는이 문제의 아웃내는 데 도움합니다! 좀 더 SwiftUI 친화적 인 답변을 드리겠습니다.하지만 도와 주셔서 감사합니다!
캐스퍼 잔드 베르겐
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.