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.