Search code examples
uiscrollviewscrollablecontentsizeintrinsic-content-sizesafearea

Add UIScrollView which fulfills superview and add its content which fulfills everything but safe area


In short words I want achieve the following: enter image description here

Content is shown behind bottom safe area but user can scroll it. This implemented with UITableView which has such behavior by default but I need to do that with UIScrollView with subviews.

As I understand UIScrollView should fulfills its superview. This UIScrollView contains "content view" which fulfills its superview too. And this "content view" contains for example UIStackView with constraints (it is just as example - I set constraints in XIB):

stackView.bottomAnchor.constraint(greaterThanOrEqualTo: stackView.superview!.bottomAnchor)
        stackView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.bottomAnchor)

But after these actions scrolling becomes broken.

Tried to change contentInsetAdjustmentBehavior but it works strange and UIScrollView's contentSize is stretched even when there is enough space.


Solution

  • You want to constrain the scroll view's "content" to the scroll view's Content Layout Guide

    Take a look at this example ... it adds a "full-view" scroll view, adds a vertical stack view to the scroll view and then adds 10 image views to the stack view:

    class ViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            title = "Wizard"
            
            let scrollView = UIScrollView()
            let stackView = UIStackView()
            
            stackView.axis = .vertical
            stackView.spacing = 8
            
            stackView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(stackView)
            view.addSubview(scrollView)
            
            let cg = scrollView.contentLayoutGuide
            let fg = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain all 4 sides of scroll view to view
                scrollView.topAnchor.constraint(equalTo: view.topAnchor),
                scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
    
                // constrain all 4 sides of stack view to scroll view's Content Layout Guide
                stackView.topAnchor.constraint(equalTo: cg.topAnchor),
                stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
                stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
                stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
    
                // constrain stack view width to scroll view's Frame Layout Guide width
                stackView.widthAnchor.constraint(equalTo: fg.widthAnchor),
                
            ])
            
            // generate 10 image views with images and add to the stack view
            for i in 0..<10 {
                let img = genImage(sz: .init(width: 200.0, height: 200.0), num: i)
                let v = UIImageView(image: img)
                v.heightAnchor.constraint(equalToConstant: 200.0).isActive = true
                stackView.addArrangedSubview(v)
            }
            
        }
        
        func genImage(sz: CGSize, num: Int) -> UIImage {
            let colors: [UIColor] = [
                .systemRed, .systemGreen, .systemBlue,
                .cyan, .green, .yellow,
            ]
            
            let renderer = UIGraphicsImageRenderer(size: sz)
            guard let nImg = UIImage(systemName: "\(num).circle.fill") else { fatalError() }
            let img = renderer.image { ctx in
                colors[num % colors.count].setFill()
                ctx.fill(.init(origin: .zero, size: sz))
                nImg.draw(in: .init(origin: .zero, size: sz))
            }
            
            return img
        }
    
    }
    

    Looks like this:

    enter image description here

    and after scrolling a bit:

    enter image description here

    and scrolled all the way to the bottom:

    enter image description here


    Edit - in response to comments...

    A couple problems with your XIB approach.

    Issue One: the "ambigious scrollable height" has nothing to do with the fact that you're working with a XIB instead of Storyboard.

    Suppose we setup a view controller like this:

    enter image description here

    We see the Red missing/ambiguous constraints error indicator.

    A UIView has no size until we've given it a size. If we run the app like that, we see this:

    enter image description here

    Pink scroll view, but no sign of the Blue "content" view.

    Now, as the Developer, I know that at run-time I'm going to add one or more views (such as a stack view with some big labels) to that Blue view - with proper constraints - to get my desired UI:

    enter image description here

    enter image description here

    and it works fine.

    When using Storyboard / Interface Builder to layout the views, I know what I'm going to do at run-time, but IB doesn't.

    This is an IB error/warning that we could safely ignore ... but, since we generally don't want to ignore things, the solution is to give that Blue "content" view an Intrinsic Content Size Placeholder.

    Find this near the bottom of the Size Inspector pane:

    enter image description here

    and change it:

    enter image description here

    to get this:

    enter image description here

    Now, without any other changes to the interface, the Red indicator is gone:

    enter image description here

    The actual Width and Height values you use are irrelevant ... they simply tell IB that we know what we're doing.

    Issue Two: trying to get the scroll view to fill the entire screen - including the safe-areas - while also getting the scroll view's content to respect the safe-areas:

    enter image description here

    That behavior is only automatic if the scroll view is the first subview of the controller's view.

    If you create a View XIB and add a scroll view to it, you have to "extract" the scroll view at run-time.

    An easier approach is to create an Empty XIB, and add a UIScrollView as its "base" view.

    I put up a project at https://github.com/DonMag/Sample20231206 that you can take a look at.