当使用 iOS 14.1 SDK 构建时,UIScrollView 会忽略 iOS 12 上的 frameLayoutGuide 约束

UIScrollView ignores frameLayoutGuide constraints on iOS 12 when built with iOS 14.1 SDK

当 运行 在 iOS 12 上时,我只是浪费了一个小时看起来像 iOS 14 SDK 回归,所以我想我会 post 这个以防万一它可以帮助他人。

总结:当我们像往常一样用 frameLayoutGuide 固定 UIScrollView 的框架时,这会中断 iOS 12 当滚动视图包含在另一个视图中时(在我的例子中是 child viewcontroller,不确定这是否重要)。

设置: 查看设置,所有视图都使用自动布局:

UIView "scrollviewContainerView" [fixed/unchanging size]
 |
 \- UIScrollView "scrollView"
     |
     \ UIView "contentView"

导致症状的约束:

  1. scrollview 的 frameLayoutGuide 固定到 scrollviewContainerView
  2. 的 top/bottom/leading/trailing
  3. contentView 已固定到滚动视图 contentLayoutGuide
  4. 的 top/bottom/leading/trailing
  5. 因为这是一个垂直滚动视图:contentView 的 widthAnchor 固定到 scrollView 的 frameLayoutGuide

症状:在iOS14,一切正常。在 iOS 12 滚动时,UIScrollView 本身的大小发生变化,似乎忽略了 frameLayoutGuide 约束(即使它们仍然存在:调试时我看不出有任何区别查看 iOS 12 的约束结果集和 iOS 14 之间的层次结构。这让事情看起来非常非常破碎。

帮助理解UIScrollViewFrame Layout GuideContent Layout Guide...

Frame Layout GuideContent Layout Guide 是在 iOS 11 中引入的,部分是为了消除框架与内容的歧义,以及允许添加“non-scrolling”滚动视图的元素。

对于以下每个示例,我都像这样添加和限制滚动视图:

    let scrollView: UIScrollView = UIScrollView()
    
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    
    view.addSubview(scrollView)
    
    // respect safe area
    let g = view.safeAreaLayoutGuide
    
    NSLayoutConstraint.activate([
        
        // constrain scrollView 20-pts on each side
        scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
        scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
        scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
        scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
        
    ])
    
    // so we can see the scroll view's frame
    scrollView.backgroundColor = .red
    

现在,考虑添加内容的“旧”方式:

    // call a func to get a vertical stack view with 30 labels
    let stackView = createStackView()
    
    scrollView.addSubview(stackView)
    
    NSLayoutConstraint.activate([

        // old method
        // constrain stack view to scroll view itself
        //  this defines the "scrollable" content
        
        // stack view Top / Leading / Trailing / Bottom to scroll view
        stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0),
        stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0),
        stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0),
        stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0),
        
        // we want vertical scrolling, so we want our content to be only as wide as
        // the scroll view itself
        stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: 0.0),
        
    ])

相当直截了当,虽然有点模棱两可。我是否通过约束其底部锚点使堆栈视图仅与滚动视图一样高?如果它是任何其他 UIView!

的子视图,这就是我所期望的

因此,请考虑“新”方法:

    // call a func to get a vertical stack view with 30 labels
    let stackView = createStackView()
    
    scrollView.addSubview(stackView)
    
    let contentG = scrollView.contentLayoutGuide
    let frameG = scrollView.frameLayoutGuide
    
    NSLayoutConstraint.activate([
        
        // constrain stack view to scroll view's Content Layout Guide
        //  this defines the "scrollable" content
        
        // stack view Top / Leading / Trailing / Bottom to scroll view's Content Layout Guide
        stackView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
        stackView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
        stackView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
        stackView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
        
        // we want vertical scrolling, so we want our content to be only as wide as
        //  the scroll view's Frame Layout Guide
        stackView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
        
    ])
    

现在,很明显我使用堆栈视图通过将其限制为 .contentLayoutGuide 并使用滚动视图的实际 frame[=48= 来定义“可滚动内容” ](其 .frameLayoutGuide)来定义堆栈视图的宽度。

此外,假设我想要一个 UI 元素 - 例如图像视图 - 滚动视图中,但我不希望它滚动? “旧方法”需要将图像视图添加为覆盖在滚动视图顶部的同级视图。

但是现在,通过使用 .frameLayoutGuide,我可以将其添加为滚动视图中的“non-scrolling”元素:

    guard let img = UIImage(named: "scrollSample") else {
        fatalError("could not load image")
    }
    let imgView = UIImageView(image: img)
    imgView.translatesAutoresizingMaskIntoConstraints = false
    
    scrollView.addSubview(imgView)
    
    NSLayoutConstraint.activate([
        
        // constrain image view to the Frame Layout Guide
        //  at top-right, with 20-pts Top / Trailing "padding"
        imgView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 20.0),
        imgView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: -20.0),
        // 120 x 120 pts
        imgView.widthAnchor.constraint(equalToConstant: 120.0),
        imgView.heightAnchor.constraint(equalToConstant: 120.0),
        
    ])
    

现在,我约束到 scrollView .contentLayoutGuide 的任何元素都将滚动,但约束到 scrollView .frameLayoutGuide 的图像视图将保留在原位。