0

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:

enter image description here

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.

7
  • Currently, you have not given your scroll view a Height. Do you want MyView to have a maximum Height? So, if you have one single-line label, it will appear between your buttons and no scrolling... if you have 3 labels, they appear between your buttons and no scrolling... but if you have, say, 20 labels, MyView should only be tall enough to show maybe 8 labels, with scrolling? Commented May 24, 2022 at 14:53
  • Yes, lately there will be 2 labels with dynamic content, but now I don't understand, why it's not visible, I mean label. In real app the view was configured with text for label, but it seems still has height =0 Commented May 24, 2022 at 15:33
  • First, the code you posted creates a new titleLabel and adds it to the scroll view, and then your configure() func sets the text of the other titleLabel which was never added to the view hierarchy. Second, you have to give the scroll view some sort of height ... otherwise it will have a height of Zero. Will your labels be multi-line, so you will need to scroll them? Commented May 24, 2022 at 15:55
  • text set up correctly, the problem is at constraints. sorry, remove lines while copy-paste - there is a self.textLabel = textLabel before constraint is set Commented May 24, 2022 at 16:05
  • and I mentioned, that 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. Commented May 24, 2022 at 16:06

1 Answer 1

1

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:

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for your answer with explanation! Should say, that can't used it 100% for my problem, because it have some more things, related to issue, BUT using this logic I finally fix it. Shortly - ScrollView with StackView inside should now it's frame size, so as you propose - I calculate it outside of the view, and pass this value to the view, and use it at 'configure' method. Thank you for your help!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.