0
votes

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.

2

2 Answers

0
votes

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: [])
    }
    
}
0
votes

I could solve it, maybe not the best solution, so I leave the question still open for a while, for better solutions.

Basically, I provided a widthAnchor constraint to each UILabel and UITextView when I created them, before adding them to the stack view.

label.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 20).isActive = true