I have a Scroll View, in which I have a Stack View. In the Stack View I have arranged subviews of either UITextView or UILabel elements. All is done programmatically, without storyboard.
The Scroll View appears and I can scroll it nicely. But unfortunately it scrolls not only vertically (top to bottom) but also horizontally (to the right, out the screen) which I don't want to (this is the reason I have numberOfLines set on the UILabel too, tried to set equal width to the scroll and stack views as the stack view's left/right attributes are connected to the view).
If it's important, this function is called either in viewDidLoad or upon touching a button later.
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let leftConstraintScroll = NSLayoutConstraint(item: scrollView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
let rightConstraintScroll = NSLayoutConstraint(item: scrollView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0)
let topConstraintScroll = NSLayoutConstraint(item: scrollView, attribute: .top, relatedBy: .equal, toItem: selectedTabIndicator, attribute: .bottom, multiplier: 1, constant: 10)
let bottomConstraintScroll = NSLayoutConstraint(item: scrollView, attribute: .bottom, relatedBy: .equal, toItem: editButton, attribute: .top, multiplier: 1, constant: 0)
view.addConstraints([leftConstraintScroll, rightConstraintScroll, topConstraintScroll, bottomConstraintScroll])
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 10
stackView.isLayoutMarginsRelativeArrangement = true
stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10)
// Several elements are added like this (UITextView):
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.delegate = self
textView.isScrollEnabled = false
textView.font = UIFont.systemFont(ofSize: 15)
textView.backgroundColor = Constants.COLOR_P
textView.textColor = .black
textView.text = "XXX"
stackView.addArrangedSubview(textView)
// Or UILabel:
var label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .justified
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 15)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .justified
paragraphStyle.hyphenationFactor = 1.0
paragraphStyle.firstLineHeadIndent = 0
paragraphStyle.headIndent = 15
let hyphenAttribute = [NSAttributedString.Key.paragraphStyle: paragraphStyle]
let attributedString = NSMutableAttributedString(string: "XXXXX", attributes: hyphenAttribute)
label.attributedText = attributedString
stackView.addArrangedSubview(label)
scrollView.addSubview(stackView)
let leftConstraint = NSLayoutConstraint(item: stackView, attribute: .left, relatedBy: .equal, toItem: scrollView, attribute: .left, multiplier: 1, constant: 0)
let rightConstraint = NSLayoutConstraint(item: stackView, attribute: .right, relatedBy: .equal, toItem: scrollView, attribute: .right, multiplier: 1, constant: 0)
let topConstraint = NSLayoutConstraint(item: stackView, attribute: .top, relatedBy: .equal, toItem: scrollView, attribute: .top, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: scrollView, attribute: .bottom, multiplier: 1, constant: 0)
scrollView.addConstraints([leftConstraint, rightConstraint, topConstraint, bottomConstraint, bottomConstraint])
Note: selectedTabIndicator and editButton are above and below the scroll view respectively.
First note: when posting code, post some actual code. Your code refers to scrollView
and recipeScrollView
which, I assume, are the same scroll view. Also, try to post complete information - your code also refers to selectedTabIndicator
and editButton
, neither of which have been identified nor described in your question.
Second note: start using more modern constraint syntax. For example:
NSLayoutConstraint.activate([
recipeScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
recipeScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
recipeScrollView.topAnchor.constraint(equalTo: selectedTabIndicator.bottomAnchor, constant: 10.0),
recipeScrollView.bottomAnchor.constraint(equalTo: editButton.topAnchor, constant: 0.0),
])
is much easier to use (and to read) than:
let leftConstraintScroll = NSLayoutConstraint(item: recipeScrollView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
let rightConstraintScroll = NSLayoutConstraint(item: recipeScrollView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0)
let topConstraintScroll = NSLayoutConstraint(item: recipeScrollView, attribute: .top, relatedBy: .equal, toItem: selectedTabIndicator, attribute: .bottom, multiplier: 1, constant: 10)
let bottomConstraintScroll = NSLayoutConstraint(item: recipeScrollView, attribute: .bottom, relatedBy: .equal, toItem: editButton, attribute: .top, multiplier: 1, constant: 0)
view.addConstraints([leftConstraintScroll, rightConstraintScroll, topConstraintScroll, bottomConstraintScroll])
Third note: respect the Safe Area... so your leading constraint should be:
recipeScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0.0)
and so on.
Fourth note: constrain your scroll view's content to the .contentLayoutGuide
, not to the scroll view itself.
To solve your "horizontal scrolling" issue, instead of setting the label
and textView
widths, set the width of the stack view relative to the scroll view's .frameLayoutGuide
:
stackView.widthAnchor.constraint(equalTo: recipeScrollView.frameLayoutGuide.widthAnchor, constant: 0.0)
Here is your code, edited with those tips. I put a blue view near the top to be the selectedTabIndicator
and a blue button near the bottom to be the editButton
:
class AnotherScrollViewController: UIViewController, UITextViewDelegate {
var recipeScrollView: UIScrollView!
var stackView: UIStackView!
var textView: UITextView!
var selectedTabIndicator: UIView!
var editButton: UIButton!
var editButtonBottom: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
selectedTabIndicator = UIView()
selectedTabIndicator.backgroundColor = .blue
selectedTabIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(selectedTabIndicator)
editButton = UIButton()
editButton.backgroundColor = .blue
editButton.setTitle("Edit", for: [])
editButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(editButton)
recipeScrollView = UIScrollView()
recipeScrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(recipeScrollView)
let g = view.safeAreaLayoutGuide
editButtonBottom = editButton.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0)
NSLayoutConstraint.activate([
selectedTabIndicator.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
selectedTabIndicator.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
selectedTabIndicator.widthAnchor.constraint(equalToConstant: 200.0),
selectedTabIndicator.heightAnchor.constraint(equalToConstant: 4.0),
//editButton.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 4.0),
editButtonBottom,
editButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
recipeScrollView.topAnchor.constraint(equalTo: selectedTabIndicator.bottomAnchor, constant: 10.0),
recipeScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
recipeScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
recipeScrollView.bottomAnchor.constraint(equalTo: editButton.topAnchor, constant: 0.0),
])
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 10
stackView.isLayoutMarginsRelativeArrangement = true
stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10)
// Several elements are added like this (UITextView):
textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.delegate = self
textView.isScrollEnabled = false
textView.font = UIFont.systemFont(ofSize: 15)
textView.backgroundColor = .cyan // Constants.COLOR_P
textView.textColor = .black
textView.text = "XXX"
stackView.addArrangedSubview(textView)
// Or UILabel:
var label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .justified
label.backgroundColor = .green // so we can easily see the label frame
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 15)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .justified
paragraphStyle.hyphenationFactor = 1.0
paragraphStyle.firstLineHeadIndent = 0
paragraphStyle.headIndent = 15
let hyphenAttribute = [NSAttributedString.Key.paragraphStyle: paragraphStyle]
let labelString = "This is the string for the label. It will wrap if it is too long to fit in the allocated width."
//let attributedString = NSMutableAttributedString(string: "XXXXX", attributes: hyphenAttribute)
let attributedString = NSMutableAttributedString(string: labelString, attributes: hyphenAttribute)
label.attributedText = attributedString
stackView.addArrangedSubview(label)
recipeScrollView.addSubview(stackView)
let contentG = recipeScrollView.contentLayoutGuide
let frameG = recipeScrollView.frameLayoutGuide
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
stackView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
])
recipeScrollView.backgroundColor = .red
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
editButton.addTarget(self, action: #selector(self.editButtonTapped), for: .touchUpInside)
}
@objc func editButtonTapped() -> Void {
if textView.isFirstResponder {
textView.resignFirstResponder()
} else {
textView.becomeFirstResponder()
}
}
@objc func adjustForKeyboard(notification: Notification) {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardScreenEndFrame = keyboardValue.cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
print(keyboardViewEndFrame.height)
var c: CGFloat = -4.0
if notification.name != UIResponder.keyboardWillHideNotification {
c -= (keyboardViewEndFrame.height - view.safeAreaInsets.bottom)
}
editButtonBottom.constant = c
editButton.setTitle(c == -4 ? "Edit" : "Done", for: [])
}
}