仅当样式设置为 'Scroll' 时 UIPageViewController 中的约束异常

Constraint exception in UIPageViewController only when style is set to 'Scroll'

我正在使用 UIPageView 允许用户滚动图像集合,并且在 UIPageViewController 设置为 Scroll 过渡样式时出现约束错误(没有共同的祖先) , 但当它设置为 Page Curl.

时则不会

到目前为止,我已经尝试将抛出错误的约束的锚点(我已经确定了哪些是罪魁祸首)更改为不同的东西(尝试了 Safe AreaSuperview),但什么都没有改变。我对 Swift 比较陌生,对 UIPageViewController 的工作原理知之甚少,所以很有可能我只是按错误的顺序调用而没有意识到。我有点困惑为什么错误是 发生在 Scroll 过渡样式而不是 Page Curl,因为在我看来它们应该类似地工作...还对为什么约束锚点锚定到其自己的父视图时不在同一视图层次结构中感到困惑?

这是 UIPageViewController 代表:

class ImageDetailPageViewController: UIPageViewController {

    // MARK: Properties

    var images: [UIImage]!
    var index: Int!

    var navbarHeight: CGFloat!

    fileprivate lazy var pages: [UIViewController] = {
        return getPages()
    }()

    // MARK: Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        self.dataSource = self
        self.delegate = self

        self.view.translatesAutoresizingMaskIntoConstraints = false

        setViewControllers([pages[index]], direction: .forward, animated: true, completion: nil)
    }

    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }

    // MARK: Methods

    fileprivate func getPages() -> [ImageView] {
        var pages = [ImageView]()

        let storyboard = UIStoryboard(name: "ImageDetail", bundle: nil)

        for image in images {
            let imageDetail = storyboard.instantiateViewController(withIdentifier: "ImageDetail") as! ImageView
            imageDetail.image = image

            pages.append(imageDetail)
        }

        return pages
    }
}

// MARK: Extensions

extension ImageDetailPageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = pages.firstIndex(of: viewController) else {
            return nil
        }

        let previousIndex = viewControllerIndex - 1

        guard previousIndex >= 0 else {
            return pages.last
        }

        guard pages.count > previousIndex else {
            return nil
        }

        return pages[previousIndex]
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {       
        guard let viewControllerIndex = pages.firstIndex(of: viewController) else {
            return nil
        }

        let nextIndex = viewControllerIndex + 1

        guard nextIndex < pages.count else {
            return pages.first
        }

        guard pages.count > nextIndex else {
            return nil
        }

        return pages[nextIndex]
    }

    func presentationCount(for pageViewController: UIPageViewController) -> Int {
        return pages.count
    }

    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        return index
    }
}

extension ImageDetailPageViewController: UIPageViewControllerDelegate { }

页面视图 (ImageDetail) 中的视图控制器非常简单。它由锚定到 Safe AreaUIScrollView 和锚定到 UIScrollViewUIImageView 组成。

当我尝试在页面视图中滚动到下一页时,应用程序崩溃,并抛出以下错误:

*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors <NSLayoutYAxisAnchor:0x600001b72400 "UIImageView:0x7ff44c56caf0.top"> and <NSLayoutYAxisAnchor:0x600001b77e40 "UIScrollView:0x7ff44d164800.top"> because they have no common ancestor.  Does the constraint or its anchors reference items in different view hierarchies?  That's illegal.'

设置为Page Curl时,效果很好。视图控制器在页面视图之外单独加载时也可以完美呈现。

编辑: 这是 ImageDetail 视图的代码:

class ImageView: UIViewController, UIScrollViewDelegate {

    // MARK: Properties

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet var zoomGestureRecognizer: UITapGestureRecognizer!

    // imageView contraints
    @IBOutlet var imageViewBottomConstraint: NSLayoutConstraint!
    @IBOutlet var imageViewLeadingConstraint: NSLayoutConstraint!
    @IBOutlet var imageViewTopConstraint: NSLayoutConstraint!
    @IBOutlet var imageViewTrailingConstraint: NSLayoutConstraint!

    var image: UIImage!

    // MARK: Overrides

    override func viewDidLoad() {
        super.viewDidLoad()

        // set the image
        if let image = image {
            imageView.image = image
        } else {
            // log error
            os_log("Error. Image Detail opened without an image loaded.", log: OSLog.default, type: .error)

            // dismiss view
            self.dismiss(animated: false, completion: nil)
        }

        // assume scrollView delegate
        scrollView.delegate = self
        scrollView.maximumZoomScale = 2

        // set imageView snapshot mode
        imageView.snapshotView(afterScreenUpdates: true)
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        updateMinZoomScaleForSize(view.compatibleSafeAreaLayoutGuide.layoutFrame.size)
        updateConstraintsForSize(view.compatibleSafeAreaLayoutGuide.layoutFrame.size)
        scrollView.setZoomScale(scrollView.minimumZoomScale, animated: false)
    }

    // MARK: Methods

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }

    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateConstraintsForSize(view.compatibleSafeAreaLayoutGuide.layoutFrame.size)
    }

    fileprivate func updateConstraintsForSize(_ size: CGSize) {
        let yOffset = max(0, (view.compatibleSafeAreaLayoutGuide.layoutFrame.height - imageView.frame.height) / 2)
        imageViewTopConstraint.constant = yOffset
        imageViewBottomConstraint.constant = yOffset

        let xOffset = max(0, (view.compatibleSafeAreaLayoutGuide.layoutFrame.width - imageView.frame.width) / 2)
        imageViewLeadingConstraint.constant = xOffset
        imageViewTrailingConstraint.constant = xOffset

        // activate constraints
        imageViewTopConstraint.isActive = true
        imageViewBottomConstraint.isActive = true
        imageViewLeadingConstraint.isActive = true
        imageViewTrailingConstraint.isActive = true

        view.layoutIfNeeded()
    }

    fileprivate func updateMinZoomScaleForSize(_ size: CGSize) {
        let widthScale = view.compatibleSafeAreaLayoutGuide.layoutFrame.width / imageView.bounds.width
        let heightScale = view.compatibleSafeAreaLayoutGuide.layoutFrame.height / imageView.bounds.height
        let minScale = min(widthScale, heightScale)

        scrollView.minimumZoomScale = minScale
        scrollView.zoomScale = minScale
    }

    fileprivate func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
        // create and size view window
        var zoomRect = CGRect.zero
        zoomRect.size.height = imageView.frame.size.height
        zoomRect.size.width = imageView.frame.size.width

        // center on tapped point
        let newCenter = imageView.convert(center, from: view)
        zoomRect.origin.x = newCenter.x - (zoomRect.size.width / 2.0)
        zoomRect.origin.y = newCenter.y - (zoomRect.size.height / 2.0)

        // return rect
        return zoomRect
    }

    // MARK: Actions

    @IBAction func tapToZoom(_ sender: UITapGestureRecognizer) {
        if scrollView.zoomScale == scrollView.minimumZoomScale {
            scrollView.zoom(to: zoomRectForScale(scrollView.maximumZoomScale, center: zoomGestureRecognizer.location(in: zoomGestureRecognizer.view)), animated: true)
        } else {
            scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
        }
    }
}

以及相应的故事板: Storyboard screenshot

了解到这一行 imageView.snapshotView(afterScreenUpdates: true) 明确告诉渲染 UIImageView 的外观,如果失败则快照可能没有可见内容。

我不确定您要达到什么目的,但注释掉该行肯定会解决您的问题。如果您需要任何帮助来渲染您想要的 UI 或其他东西,请告诉我。

阅读官方documentation了解更多详情。