简体   繁体   中英

ios Swift: ScrollView with dynamic content programmatic layout

Need to create custom view, just 2 buttons and some content between. Problem is about create correct layout using scrollView and subviews with dynamic content. For example, if there will be only one Label. What is my mistake? Now label isn't visible, and view looks like:

在此处输入图像描述

Here is code:

view inits this way:

let view = MyView(frame: .zero)
view.configure(with ...) //here configures label text
selv.view.addSubView(view)

public final class MyView: UIView {
    private(set) var titleLabel: UILabel?

    override public init(frame: CGRect) {
        let closeButton = UIButton(type: .system)
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        (button setup)

        let scrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.alwaysBounceVertical = false

        let contentLayoutGuide = scrollView.contentLayoutGuide        

        let titleLabel = UILabel()
    titleLabel.translatesAutoresizingMaskIntoConstraints = false
        (label's font and alignment setup)        

        let successButton = UIButton(type: .system)
        successButton.translatesAutoresizingMaskIntoConstraints = false
        (button setup)

        super.init(frame: frame)        

        addSubview(closeButton)
        addSubview(scrollView)
        addSubview(successButton)
        scrollView.addSubview(titleLabel)  

self.textLabel = textLabel

  

        let layoutGuide = UILayoutGuide()
        addLayoutGuide(layoutGuide)

        NSLayoutConstraint.activate([
            layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
            trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),

            layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
            bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),

            closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
            layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
            closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
            closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
            closeButton.heightAnchor.constraint(equalToConstant: 33),

            scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
            scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
            scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
            scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),

            successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
            layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
            successButton.heightAnchor.constraint(equalToConstant: 48),
            layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),

            titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
            titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
        ])
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }    

    public func configure(with viewModel: someViewModel) {
        titleLabel?.text = viewModel.title        
    }
}

If I'll add scrollView frameLayoutGuide height:

scrollView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 150),

, then all looks as expected, but I need to resize this label and all MyView height depending on content.

A UIScrollView is designed to automatically allow scrolling when its content is larger than its frame.

By itself, a scroll view has NO intrinsic size. It doesn't matter how many subviews you add to it... if you don't do something to set its frame, its frame size will always be .zero .

If we want to get the scroll view's frame to grow in height based on its content we need to give it a height constraint when the content size changes.

If we want it to scroll when it has a lot of content, we also need to give it a maximum height.

So, if we want MyView height to be max of 1/2 the screen (view) height, we constrain its height (in the controller) like this:

myView.heightAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)

and then constrain the scroll view height in MyView like this:

let svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true

Here is a modification to your code - lots of comments in the code so you should be able to follow.

First, an example controller:

class MVTestVC: UIViewController {
    
    let myView = MyView()
    
    let sampleStrings: [String] = [
        "Short string.",
        "This is a longer string which should wrap onto a couple lines.",
        "Now let's use a really, really long string. This will make the label taller, but still not enough to require vertical scrolling.",
        "We want to see what happens when we DO need scrolling.\n\nSo, let's use a long string, with some embedded newlines.\n\nThis will make the label tall enough that it would exceed one-half the screen height, so we can see that we do, in fact, get vertical scrolling.",
    ]
    var strIndex: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .gray
        myView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myView)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            // 20-points on each side
            myView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            myView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            // centered vertically
            myView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            // max 1/2 screen (view) height
            myView.heightAnchor.constraint(lessThanOrEqualTo: g.heightAnchor, multiplier: 0.5),
            
        ])
        
        myView.backgroundColor = .white
        myView.configure(with: sampleStrings[0])
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        strIndex += 1
        myView.configure(with: sampleStrings[strIndex % sampleStrings.count])
    }
}

and the modified MyView class:

public final class MyView: UIView {
    
    private let titleLabel = UILabel()
    private let scrollView = UIScrollView()
    
    // this will be used to set the scroll view height
    private var svh: NSLayoutConstraint!
    
    override public init(frame: CGRect) {

        super.init(frame: frame)
        
        let closeButton = UIButton(type: .system)
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        //(button setup)
        closeButton.setTitle("X", for: [])
        closeButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.alwaysBounceVertical = false
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        //(label's font and alignment setup)
        titleLabel.font = .systemFont(ofSize: 24.0, weight: .light)
        titleLabel.numberOfLines = 0

        let successButton = UIButton(type: .system)
        successButton.translatesAutoresizingMaskIntoConstraints = false
        //(button setup)
        successButton.setTitle("Success", for: [])
        successButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
        addSubview(closeButton)
        addSubview(scrollView)
        addSubview(successButton)
        scrollView.addSubview(titleLabel)
        
        let layoutGuide = UILayoutGuide()
        addLayoutGuide(layoutGuide)
        
        let contentLayoutGuide = scrollView.contentLayoutGuide
        
        NSLayoutConstraint.activate([
            layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
            trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
            
            layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
            bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
            
            closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
            layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
            closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
            closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
            closeButton.heightAnchor.constraint(equalToConstant: 33),
            
            scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
            scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
            
            successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
            layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
            successButton.heightAnchor.constraint(equalToConstant: 48),
            layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
            
            // constrain the label to the scroll view's Content Layout Guide
            titleLabel.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor, constant: 16),
            titleLabel.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -16),
            titleLabel.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor, constant: -16),
            
            // label needs a width anchor, otherwise we'll get horizontal scrolling
            titleLabel.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
        ])
        
        layer.cornerRadius = 12
        
        // so we can see the framing
        scrollView.backgroundColor = .red
        titleLabel.backgroundColor = .green
    }
    
    public override func layoutSubviews() {
        super.layoutSubviews()
        
        // we want to update the scroll view's height constraint when the text changes
        if let c = svh {
            c.isActive = false
        }
        // on initial layout, the scroll view's content size will still be zero
        //  so force another layout pass
        if scrollView.contentSize.height == 0 {
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
        }
        // constrain the scroll view's height to the height of its content
        //  but with a less-than-required priority so we can use a maximum height
        svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
        svh.priority = .required - 1
        svh.isActive = true
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //public func configure(with viewModel: someViewModel) {
    //  titleLabel.text = viewModel.title
    //}
    public func configure(with str: String) {
        titleLabel.text = str
        // force the scroll view to update its layout
        scrollView.setNeedsLayout()
        scrollView.layoutIfNeeded()
        // force self to update its layout
        self.setNeedsLayout()
        self.layoutIfNeeded()
    }
}

Each tap anywhere on the screen will cycle through a few sample strings to change the text in the label, giving us this:

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM