【问题标题】:Updating an @EnvironmentObject var to pass data to PageViewController in SwiftUI results in loss of swiping between ViewControllers在 SwiftUI 中更新 @EnvironmentObject 变量以将数据传递给 PageViewController 会导致 ViewController 之间的滑动丢失
【发布时间】:2020-01-19 19:14:49
【问题描述】:

在我的 SwiftUI 应用程序中,我目前有一个使用 UIKit 实现的 PageViewController。它遵循 Apple 的 SwiftUI UIKit 集成 tutorials 中概述的传统 SwiftUI - UIKit 实现。

我有填充 UIViewController 的数据,在传递给 PageViewController 的控制器数组中,由 @Environment 变量提供。在应用程序的另一个屏幕中,您可以发出导致 Environment 对象更新的操作,从而触发位于 PageViewController 中的 ViewController 的重新渲染。

但是,这种重新渲染会导致问题,因为 ViewController 是使用新标识符重新制作的,因此无法在 Class Coordinator pageViewController 函数的 parent.controller 数组中找到 viewController 的索引。这会导致索引默认为 nil 并禁用更新后的 viewController 上的任何滑动。我仍然可以使用页面控制点在 viewController 之间导航,但我想确定如何更新 parent.controller 数组以包含新的 ViewController 并丢弃旧的。

经过数小时的搜索、调试和修补,我无法找到一种方法来重置父控制器数组,用新的 ViewController 替换已丢弃的旧视图。下面是 PageViewController 的代码。

import SwiftUI
import UIKit

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

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.view.backgroundColor = UIColor.clear
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        return pageViewController
    }

    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [controllers[currentPage]], direction: .forward, animated: false)
    }

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

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

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController? {
            print(parent.controllers)
            print(viewController)
            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 + 1 == parent.controllers.count {
                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
            }
        }
    }
}

在更新填充给定 ViewController 数据的环境状态后,导致重新渲染,协调器类可以打印一个控制器数组,其中包括旧 ViewController 以及下面注释代码中的新 ViewController 但我有还没有找到一种可靠的方法来确保新的 ViewController 有效地替换旧的。

struct PageViewController: UIViewControllerRepresentable {
     ...

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate { 
        ...

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController? {
            print(parent.controllers)
            // prints out array of ViewControllers including old ViewController that has now been 
            // discarded 
            // [<_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c93de0>, 
            // <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c94be0>, 
            // <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c96830>, 
            // <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c976b0>]
            print(viewController)
            // prints out new ViewController that does not exist within the parent.controllers array
            // and hence nil is returned from the guard
            // <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd593721710>
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return parent.controllers.last
            }
            return parent.controllers[index - 1]
        }

        ...
}

任何有关此问题的帮助或指导将不胜感激!

【问题讨论】:

  • 嘿,@DanielBarclay,你能分享你的代码,包括环境变量吗?在我的情况下,要么是页面没有在 PageViewController 中更新,要么是我在控制器中添加 ObservableObject 然后在页面中断之间滑动。 UUID hack 在我的情况下不起作用。

标签: uiviewcontroller swiftui uipageviewcontroller


【解决方案1】:

最近几个小时遇到同样的问题,我找到了解决方法。

这似乎是一个错误。如果你查看 UIViewControllerRepresentable 循环,它应该从 Init > Coordinator > makeUIViewController > updateUIViewController 开始。

但是如果 PageViewController 中的页面得到更新(我们的例子)。页面由 SwiftUI 重新创建,很好,但是 PageViewController 从 Init > updateUIViewController 得到一个内部错误的创建周期。没有 makeUIViewController 也没有协调器。以某种方式创建了一个新的 UIPageViewController(如何?)导致您注意到的差异。

要解决此问题并强制重新创建 PageViewController,请在您的页面视图代码中添加 .id(UUID),例如:

struct SGPageView<Page: View>: View {
    var viewControllers: [UIHostingController<Page>]
    @Binding var currentPage:Int

    init(_ views: [Page], currentPage:Binding<Int>) {
        self.viewControllers = views.map { UIHostingController(rootView: $0) }
        self._currentPage = currentPage
    }

    var body: some View {
        PageViewController(controllers: viewControllers, currentPage: $currentPage).id(UUID())
    }
}

它会起作用,但您会注意到页面的滚动位置已重置。另一个错误;)

【讨论】:

  • 非常感谢您找到并分享这个解决方法 Fabrice,它在我的应用程序中运行得非常好。正如您在上面很好地解释的那样,重新创建后 PageViewController 的行为方式很奇怪。请注意,我自己没有注意到滚动位置错误
  • 我有完全相同的症状 - 在 PageViewController 中滑动不起作用,同时点击 PageControls 起作用。我尝试了 uuid 解决方案,但没有任何区别。你能在调试方面指出我正确的方向吗?我在 Coordinator() 中添加了 @ObservedObject viewModel 以接收新数据。
  • 你拯救了我的一天?
【解决方案2】:

认为问题在于协调器:

当您在 pageviewcontroller 中更新控制器时,Coordinator 的 parent 属性保持不变(并且因为它是一个结构,所以他的所有属性)。因此,您可以在 updateUIViewController 方法中简单地添加这行代码:

context.coordinator.parent = self

另外,请记住pageViewController.setViewControllers的动画会发生,所以你必须将动画设置为false,或者正确处理。

还有很多其他方法可以解决这个问题(我写了最直观的解决方案), 重要的是要知道错误来自哪里。

【讨论】:

  • 非常感谢您的回答。尽管将动画设置为 false 会导致第一次滑动时崩溃,但效果很好:Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Don't know about flushed view &lt;_TtGC7SwiftUI14_UIHostingViewVS_7AnyView_: 0x7fa37e425d50;
  • 尝试将方法放入 DispatchQueue.main.async
  • 无论如何,最好的办法是把所有的属性都放在coordinator中,然后用context.coordinator访问它们或者创建一个符合observableobject的viewmodel。任何可以删除重复的解决方案都更好。
  • @LorenzoFiamingo 将其放入 DispatchQueue.main.async 也不起作用,它会引发与 OP 上述相同的错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-01
  • 2021-08-18
  • 1970-01-01
  • 2011-08-09
  • 2015-10-26
  • 1970-01-01
  • 2017-04-18
相关资源
最近更新 更多