I have a label that dynamically increases the width. I decided to add it to the ScrollView (so that the user can see any length of text in the label).
But I ran into a problem. What is the correct way to implement this? How am I trying to do it. How to make horizontal scroll view with dynamic width?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = .init(width: label.frame.width, height: label.frame.height)
}
func setupScrollView() {
view.addSubview(scrollView)
scrollView.addSubview(label)
let frameLayoutGuide = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 400),
frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -100),
frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -300),
])
let contentLayoutGuide = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
contentLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 400),
contentLayoutGuide.centerXAnchor.constraint(equalTo: frameLayoutGuide.centerXAnchor),
contentLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -300),
label.centerXAnchor.constraint(equalTo: contentLayoutGuide.centerXAnchor),
label.centerYAnchor.constraint(equalTo: contentLayoutGuide.centerYAnchor)
])
}
Sorry, but you're very mistaken about the use of scrollView.contentLayoutGuide
and scrollView.frameLayoutGuide
.
You want to constrain the scrollView
itself to its superview (the main view
in this case) and constrain the scroll view's subview(s) to the scrollView's .contentLayoutGuide
. You can then use the scrollView's .frameLayoutGuide
to help set the size(s) of its subview(s).
So, if you want a label centered in the scroll view, but then enable horizontal scrolling when the label is too wide to fit, you want to:
.contentLayoutGuide
and.frameLayoutGuide
Here's a quick example. It will cycle through different length strings for the label... scrolling will automatically be enabled or disabled, based on the resulting width of the label (all with auto-layout -- no size calculations needed):
class ViewController: UIViewController {
let scrollView = UIScrollView()
let label = UILabel()
let labelHolderView = UIView()
let sampleStrings: [String] = [
"Short (no scrolling).",
"A little longer (still no scrolling).",
"A much longer string, that will definitely require horizontal scrolling.",
"Just for kicks, let's make this string really, really long, to help demonstrate the benefits of using auto-layout!",
]
var stringIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
}
func setupScrollView() {
// add the label to the holder view
labelHolderView.addSubview(label)
// add the holder view to the scroll view
scrollView.addSubview(labelHolderView)
// add the scroll view to the view
view.addSubview(scrollView)
// all three need this
labelHolderView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// let's put a 100-pt tall scroll view
// 40-pts from the bottom
// 40-pts on each side
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -40.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -40.0),
scrollView.heightAnchor.constraint(equalToConstant: 100.0),
// constrain holder view to ContentGuide with Zero on all 4 sides
labelHolderView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
labelHolderView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
labelHolderView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
labelHolderView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// we only want horizontal scrolling, so constrain
// holder view height to sccroll view FrameGuide
labelHolderView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
// we want the label centered horizontally
// in the holder view and in the scroll view
// so set holder view width >= FrameGuide
labelHolderView.widthAnchor.constraint(greaterThanOrEqualTo: frameG.widthAnchor, constant: 0.0),
// center the label in the holder view
label.centerXAnchor.constraint(equalTo: labelHolderView.centerXAnchor),
label.centerYAnchor.constraint(equalTo: labelHolderView.centerYAnchor),
// as the label expands horizontally, we want at least
// 8-pts on each side
label.leadingAnchor.constraint(greaterThanOrEqualTo: labelHolderView.leadingAnchor, constant: 8.0),
label.trailingAnchor.constraint(lessThanOrEqualTo: labelHolderView.trailingAnchor, constant: -8.0),
])
// let's use some background colors so we can see the view frames
scrollView.backgroundColor = .yellow
labelHolderView.backgroundColor = .systemTeal
label.backgroundColor = .green
// add a button above the scroll view to cycle through our sample strings
let b = UIButton()
b.setTitle("Tap Me", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .systemGreen
b.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(b)
NSLayoutConstraint.activate([
b.bottomAnchor.constraint(equalTo: scrollView.topAnchor, constant: -20.0),
b.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.6),
b.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
])
b.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
// set the initial text
gotTap(nil)
}
@objc func gotTap(_ sender: Any?) -> Void {
label.text = sampleStrings[stringIndex % sampleStrings.count]
stringIndex += 1
}
}