UIScrollView:按钮始终位于底部,除非内容太大,否则应该进一步向下

问题描述 投票:0回答:1

我尝试在 UIScrollView 中获得一个带有一些标签在彼此下方的布局,例如标题和描述。按钮应该位于视图的最底部。当内容太大(例如太多描述)时,按钮应该滚动到看不见的地方并位于描述文本旁边。但我无法让它工作,我最近的尝试如下:

//
//  KitchenSinkView.swift
//  MBViews
//
//  Created by Stefan Walkner on 26.04.24.
//

import UIKit

class KitchenSink: UIView {

    private let scrollView = UIScrollView()
    private let titleLabel = UILabel()
    private let contentLabel = UILabel()
    private let actionButton = UIButton(type: .system)

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
        setupConstraints()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupViews()
        setupConstraints()
    }

    private func setupViews() {
        backgroundColor = .white

        // Setup scrollView
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(scrollView)

        // Setup titleLabel
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.text = "Title"
        titleLabel.font = UIFont.boldSystemFont(ofSize: 20)
        titleLabel.backgroundColor = .blue
        titleLabel.setContentHuggingPriority(.required, for: .vertical)

        // Setup contentLabel
        contentLabel.translatesAutoresizingMaskIntoConstraints = false
        contentLabel.text = """
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        asdf
        """
        contentLabel.numberOfLines = 0
        contentLabel.backgroundColor = .green
        contentLabel.setContentHuggingPriority(.required, for: .vertical)

        // Setup actionButton
        actionButton.translatesAutoresizingMaskIntoConstraints = false
        actionButton.setTitle("Tap Me", for: .normal)
        actionButton.backgroundColor = .red
        actionButton.setContentHuggingPriority(.required, for: .vertical)

        // Add subviews to scrollView
        scrollView.addSubview(titleLabel)
        scrollView.addSubview(contentLabel)
        scrollView.addSubview(actionButton)
    }
    private var buttonBottomConstraint: NSLayoutConstraint?

    private func setupConstraints() {
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.leftAnchor.constraint(equalTo: leftAnchor),
            scrollView.rightAnchor.constraint(equalTo: rightAnchor),
            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),

            titleLabel.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 20),
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),

            contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
            contentLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            contentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),

            actionButton.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 20),
            actionButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            actionButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20)
        ])

        buttonBottomConstraint = actionButton.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -20)
        buttonBottomConstraint?.isActive = true
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        adjustButtonPosition()
    }

    private func adjustButtonPosition() {
        let scrollViewHeight = scrollView.frame.size.height
        let contentHeight = titleLabel.intrinsicContentSize.height + contentLabel.intrinsicContentSize.height + 60 // total vertical padding + button height

        if contentHeight < scrollViewHeight {
            // If the content + padding is less than the height of the scroll view, anchor button to bottom of the screen
            buttonBottomConstraint?.isActive = false
            buttonBottomConstraint = actionButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20)
            buttonBottomConstraint?.isActive = true
        } else {
            // Content is enough to require scrolling, anchor button to content
            buttonBottomConstraint?.isActive = false
            buttonBottomConstraint = actionButton.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -20)
            buttonBottomConstraint?.isActive = true
        }
    }
}

class KitchenSinkViewController: UIViewController {

    private var customView: KitchenSink!

    override func loadView() {
        customView = KitchenSink()
        view = customView
    }
}

#Preview {
    KitchenSinkViewController()
}

enter image description here

uiscrollview uikit
1个回答
0
投票

这里是故事板答案的纯代码版本:https://stackoverflow.com/a/50864054/6257435

做法是:

  • 使用“内容”视图作为滚动视图的基础视图
  • 像我们通常在“内容”视图中那样约束标题和内容标签
  • 像我们通常在“内容”视图中那样约束按钮前导/尾随/底部
  • 将按钮的顶部 >= 约束到内容标签的底部
  • 约束“内容”视图的高度等于滚动视图的框架布局指南的高度,优先级低于要求,并且 >= 滚动视图的框架布局指南的高度。这会
    • 允许“内容”视图长高,而且
    • 防止“内容”视图高度缩小到比滚动视图短

所以,随着 contentLable 的高度增长...

  • 如果其底部和按钮顶部之间仍有空间,则不发生变化
  • 当它变得足够高时,它将向下“推”按钮,这将扩大“内容”视图的高度,从而允许垂直滚动

所以 - 示例代码:

class ViewController: UIViewController {
    
    private var customView: KitchenSink!
    
    override func loadView() {
        customView = KitchenSink()
        view = customView
    }
    
}

class KitchenSink: UIView {
    
    private let scrollView = UIScrollView()
    private let titleLabel = UILabel()
    private let contentLabel = UILabel()
    private let addButton = UIButton(type: .system)
    
    private let myContentView = UIView()
    
    // just for this example, so we can "grow/shrink" the contentLabel
    private var minLines: Int = 6
    private var maxLines: Int = 15
    private var numLines: Int = 6
    private var isAdding: Bool = true
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() {
        setupViews()
        setupConstraints()
    }
    
    private func setupViews() {

        // Setup titleLabel
        titleLabel.text = "Title"
        titleLabel.font = UIFont.boldSystemFont(ofSize: 36)
        titleLabel.backgroundColor = .cyan
        
        // Setup contentLabel
        contentLabel.text = Array(1...numLines).map { "Line \($0)" }.joined(separator: "\n")
        contentLabel.numberOfLines = 0
        contentLabel.backgroundColor = .green
        // let's use a large font so we don't have to click the button too many times
        contentLabel.font = UIFont.boldSystemFont(ofSize: 48)
        
        // Setup actionButton
        addButton.setTitle("Add Line", for: .normal)
        addButton.backgroundColor = .yellow
        addButton.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)

        for v in [titleLabel, contentLabel, addButton] {
            v.setContentHuggingPriority(.required, for: .vertical)
            v.setContentCompressionResistancePriority(.required, for: .vertical)
            v.translatesAutoresizingMaskIntoConstraints = false
            myContentView.addSubview(v)
        }

        myContentView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(myContentView)
        
        // Setup scrollView
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(scrollView)

    }
    
    private func setupConstraints() {
        
        let g = safeAreaLayoutGuide
        let cg = scrollView.contentLayoutGuide
        let fg = scrollView.frameLayoutGuide
        
        // "content" view height equal to frame layout guide height
        //      priority .defaultHigh, so it can be broken
        // this will prevent the button from "moving up"
        let mcvHeight = myContentView.heightAnchor.constraint(equalTo: fg.heightAnchor)
        mcvHeight.priority = .defaultHigh
        
        NSLayoutConstraint.activate([
            
            // constrain all 4 sides of scroll view to safe area
            scrollView.topAnchor.constraint(equalTo: g.topAnchor),
            scrollView.leftAnchor.constraint(equalTo: g.leftAnchor),
            scrollView.rightAnchor.constraint(equalTo: g.rightAnchor),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
            // title label top/leading/trailing to "content" view
            titleLabel.topAnchor.constraint(equalTo: myContentView.topAnchor, constant: 20),
            titleLabel.leadingAnchor.constraint(equalTo: myContentView.leadingAnchor, constant: 20),
            titleLabel.trailingAnchor.constraint(equalTo: myContentView.trailingAnchor, constant: -20),
            
            // content label leading/trailing to "content" view, top to titleLabel bottom
            contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
            contentLabel.leadingAnchor.constraint(equalTo: myContentView.leadingAnchor, constant: 20),
            contentLabel.trailingAnchor.constraint(equalTo: myContentView.trailingAnchor, constant: -20),
            
            // button top greater-than-or-equal to content label bottom
            addButton.topAnchor.constraint(greaterThanOrEqualTo: contentLabel.bottomAnchor, constant: 20.0),
            
            // button leading/trailing/bottom to "content" view
            addButton.leadingAnchor.constraint(equalTo: myContentView.leadingAnchor, constant: 20),
            addButton.trailingAnchor.constraint(equalTo: myContentView.trailingAnchor, constant: -20),
            addButton.bottomAnchor.constraint(equalTo: myContentView.bottomAnchor, constant: -20),

            // "content" view all 4 sides to content layout guide
            myContentView.topAnchor.constraint(equalTo: cg.topAnchor),
            myContentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
            myContentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
            myContentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
            
            // "content" view width to frame layout guide
            myContentView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: 0.0),

            // "content" view height greater-than-or-equal to frame layout guide
            //  so it can grow
            myContentView.heightAnchor.constraint(greaterThanOrEqualTo: fg.heightAnchor),
            
            // activate mcv height constraint
            mcvHeight,
            
        ])
        
        // so we can see the framing
        self.backgroundColor = .systemYellow
        scrollView.backgroundColor = .systemBlue
        myContentView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
    }
    
    @objc func btnTap(_ sender: UIButton) {
        if isAdding {
            numLines += 1
        } else {
            numLines -= 1
        }
        // update text in contentLabel
        contentLabel.text = Array(1...numLines).map { "Line \($0)" }.joined(separator: "\n")

        // toggle adding / removing lines on button tap
        if numLines == maxLines {
            isAdding = false
            addButton.setTitle("Remove Line", for: .normal)
        } else if numLines == minLines {
            isAdding = true
            addButton.setTitle("Add Line", for: .normal)
        }

        // let's auto-scroll to keep the button visible
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            let r: CGRect = .init(x: 0.0, y: self.scrollView.contentSize.height - 1.0, width: 1.0, height: 1.0)
            self.scrollView.scrollRectToVisible(r, animated: true)
        }
    }
    
}
© www.soinside.com 2019 - 2024. All rights reserved.