ios Swift:具有动态内容编程布局的 ScrollView

ios Swift: ScrollView with dynamic content programmatic layout

需要创建自定义视图,只有 2 个按钮和一些内容。问题是关于使用 scrollView 和具有动态内容的子视图创建正确的布局。 例如,如果只有一个标签。 我的错误是什么? 现在标签不可见,视图看起来像:

代码如下:

以这种方式查看初始化:

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        
    }
}

如果我将添加 scrollView frameLayoutGuide 高度:

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

,然后一切看起来都符合预期,但我需要根据内容调整此标签和所有 MyView 的高度。

UIScrollView 设计为当内容大于框架时自动允许滚动。

滚动视图本身具有 NO 固有大小。你添加多少子视图并不重要...如果你不做something来设置它的框架,它的框架大小将永远是.zero.

如果我们想让滚动视图的框架根据其内容增加高度,我们需要在内容大小改变时给它​​一个高度限制。

如果我们想让它在内容很多的时候滚动,我们还需要给它一个最大高度。

所以,如果我们希望 MyView 高度最大为屏幕(视图)高度的 1/2,我们可以像这样限制它的高度(在控制器中):

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

然后像这样在 MyView 中限制滚动视图高度:

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

这是对您的代码的修改 - 代码中有很多注释,因此您应该能够理解。

首先,一个示例控制器:

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

和修改后的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()
    }
}

每次点击屏幕上的任意位置都会循环显示一些示例字符串以更改标签中的文本,给我们这个: