我尝试在 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()
}
这里是故事板答案的纯代码版本: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)
}
}
}