UIPageViewController 在 SwiftUI 中滑动 1 次后停止工作

Posted

技术标签:

【中文标题】UIPageViewController 在 SwiftUI 中滑动 1 次后停止工作【英文标题】:UIPageViewController stops working after 1 swipe in SwiftUI 【发布时间】:2020-06-08 12:59:19 【问题描述】:

我正在关注 Apple 的 SwiftUI 教程,并在“与 UIKit 交互”(https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit) 下制作了一个 UIPageController。

在教程中,他们使用 State 属性来更新 PageView 中的 currentPage 变量。我想进行一些实验,并决定将PageView 嵌入到不同的视图中,并在该父视图中更新currentPage 属性。因此,很自然地,我将PageView 中的@State 属性更改为@Binding 属性,以便更新父级中的currentPage

但是,当我这样做时,UIPageController 停止工作,并在第二次滑动时继续从 pageViewController(_:viewControllerBefore:) -> UIViewController? 返回 nil

这是代码,我将添加cmets以使其不那么繁琐:

struct ContentView: View 
    @State var currentPage = 0  // This is the new @State variable which should get updated.

    var body: some View 
        VStack 
            PageView([Text("First page"), Text("Second Page"), Text("Third Page")],
                      currentPage: self.$currentPage)  // Passed a binding to the State variable since I want that to be updated when the page changes.
            Text("Page number: \(currentPage)")  // Removing this fixes the problem, but I need it here.
        
    

struct PageView<Page: View>: View 
    var viewControllers: [UIHostingController<Page>]
    @Binding var currentPage: Int  // This used to be an @State property, but I changed it to a binding since the currentPage variable is now being updated elsewhere (In the view above).

    init(_ views: [Page], currentPage: Binding<Int>) 
        self.viewControllers = views.map  UIHostingController(rootView: $0)   // Simply wrapping all the views inside ViewControllers for UIPageViewController to use.
        self._currentPage = currentPage  // Assigning the binding.
    

    var body: some View 
        PageViewController(controllers: self.viewControllers, currentPage: self.$currentPage)
    

struct PageViewController: UIViewControllerRepresentable 
    var controllers: [UIViewController]
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator   // Makes the coordinator, irrelevant to the problem
        Coordinator(self)
    
    func makeUIViewController(context: Context) -> UIPageViewController  
        // I don't think the problem lies here. 
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)

        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        return pageViewController
    
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) 
        // I don't think the problem lies here. 
        pageViewController.setViewControllers(
            [self.controllers[currentPage]], direction: .forward, animated: true)
    

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate 
        var parent: PageViewController

        init(_ pageViewController: PageViewController) 
            self.parent = pageViewController
        

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? 
            // This is where I think the problem lies. When the line below is evaluated, nil is returned on the second swipe when it shouldn't be. It works on the first swipe but on the second swipe nil is returned and therefore nothing is presented.
            guard let index = parent.controllers.firstIndex(of: viewController) else 
                return nil
            
            if index == 0 
                return parent.controllers.last
            
            return parent.controllers[index - 1]
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? 
            guard let index = parent.controllers.firstIndex(of: viewController) else 
                return nil
            
            if index == parent.controllers.count - 1 
                return parent.controllers.first
            
            return parent.controllers[index + 1]
        
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) 
            if completed, let visibleViewController = pageViewController.viewControllers?.first, let index = parent.controllers.firstIndex(of: visibleViewController) 
                parent.currentPage = index
            
        
    

另外,ContentView 中删除 Text 可以解决问题,但我需要它 - 所以删除它不是一个选项。

如果有人能解释如何解决我的问题,我将不胜感激。 如果你太懒,不需要提供代码示例,简单的解释也应该完成工作。

提前致谢!

【问题讨论】:

适用于 Xcode 11.4 / ios 13.4 @Asperi 嘿!感谢您指出了这一点。我忘了在 ContentView 中添加一个 VStack 来重现问题,我很抱歉。你现在可以看一下吗? 【参考方案1】:

PageViewbody 更新期间在Text 刷新时重新创建。避免这种情况的解决方案是将PageView 确认为Equatable

最简单的修复演示如下(仅修改部分)。使用 Xcode 11.4 / iOS 13.4 测试

注意:使用相同的PageView,因为没有提供PageView2

// ...
var body: some View 
    VStack 
        PageView([Text("First page"), Text("Second Page"), Text("Third Page")],
                  currentPage: self.$currentPage)
                  .equatable()                // << here !!
        Text("Page number: \(currentPage)")
    



struct PageView<Page: View>: View, Equatable  // << confirm !!
   static func == (lhs: PageView<Page>, rhs: PageView<Page>) -> Bool 
       return true // just for demo, make it meaningful on some property
   

   // .. other code goes here

【讨论】:

以上是关于UIPageViewController 在 SwiftUI 中滑动 1 次后停止工作的主要内容,如果未能解决你的问题,请参考以下文章

UIPageViewController 背景在 Objective C iOS 中从不透明

获取 UIPageViewController 的滚动位置

在故事板中使用 UIPageViewController

如何在 UIPageViewController 和孩子之间正确传递对象

使用 UIPageViewController 在多个视图控制器之间滑动

带有 Peeking 的 UIPageViewController