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

落爺英雄遲暮 提交于 2021-02-05 12:22:11

问题


I just wasted an hour with what looks like an iOS 14 SDK regression when running on iOS 12 so I thought i'd post this in case it helps others.

Summary: When pinning the frame of the UIScrollView with the frameLayoutGuide as we've always done, this breaks on iOS 12 when the scrollview is contained within another view (in my case in a child viewcontroller, not sure if that matters).

Setup: View setup, all views using autolayout:

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

Constraints that lead to Symptom:

  1. scrollview's frameLayoutGuide pinned to top/bottom/leading/trailing of scrollviewContainerView
  2. contentView pinned to top/bottom/leading/trailing of scrollview's contentLayoutGuide
  3. because this is a vertical scrollview: contentView's widthAnchor pinned to scrollView's frameLayoutGuide

Symptom: On iOS 14, everything looks fine. On iOS 12 when scrolling, the size of the UIScrollView itself changes, seemingly ignoring the frameLayoutGuide constraints (even though they're still there: I couldn't see any difference when debugging the view hierarchy between iOS 12's resulting set of constraints and iOS 14). This makes things look very very broken.


回答1:


To help understand UIScrollView, Frame Layout Guide and Content Layout Guide...

Frame Layout Guide and Content Layout Guide were introduced in iOS 11, in part to eliminate the ambiguity of frame vs content, as well as to allow adding "non-scrolling" elements to the scroll view.

For each of the following examples, I'm adding and constraining the scroll view like this:

    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
    

Now, consider the "old" way of adding content:

    // 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),
        
    ])

Fairly straightforward, although a little ambiguous. Am I making the stack view only as tall as the scroll view, by constraining its bottom anchor? That's what I'd expect if it's a subview of any other UIView!

So, consider the "new" way of doing it:

    // 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),
        
    ])
    

Now, it's very clear that I am using the stack view to define the "scrollable content" by constraining it to the .contentLayoutGuide and using the scroll view's actual frame (its .frameLayoutGuide) to define the stack view's width.

In addition, suppose I want a UI element - such as an image view - to be inside the scroll view, but I don't want it to scroll? The "old way" would require adding the image view as a sibling view overlaid on top of the scroll view.

But now, by using .frameLayoutGuide, I can add it as a "non-scrolling" element inside the scroll view:

    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),
        
    ])
    

Now, any elements I constrain to the scrollView's .contentLayoutGuide will scroll, but the image view constrained to the scrollView's .frameLayoutGuide will remain in place.



来源:https://stackoverflow.com/questions/64556853/uiscrollview-ignores-framelayoutguide-constraints-on-ios-12-when-built-with-ios

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!