0
votes

I have the following code to setup subviews of UIScrollView. I first create a subclass of UIScrollView called MyScrollView. I then add a view called contentView as a subview. I add all other views to scroll view as subviews of this contentView. The issue is scrollView doesn't scrolls after setting the following code with auto-layout constraints.

public class MyScrollView:UIScrollView {


  private var contentView:UIView!

   override init(frame: CGRect) {
    super.init(frame: frame)
    self.translatesAutoresizingMaskIntoConstraints = false
    self.isScrollEnabled = true
    self.isDirectionalLockEnabled = true
    self.showsHorizontalScrollIndicator = true
    self.showsVerticalScrollIndicator = false
    self.decelerationRate = .normal
    self.delaysContentTouches = false
    self.bouncesZoom = true
    
    setupSubviews()
}


  private func setupSubviews() {

     contentView = UIView()
    contentView.backgroundColor = UIColor.clear
    contentView.translatesAutoresizingMaskIntoConstraints = false
    contentView.isUserInteractionEnabled = true
    
    self.addSubview(contentView)
    
    contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
    contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
    contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
    contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
    contentView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: CGFloat(19), constant: CGFloat(5)).isActive = true
    contentView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 2.0, constant: CGFloat(5)).isActive = true
    
   // self.contentSize = CGSize(width: 10000, height: 2*frame.height)


    let subviewWidth = CGFloat(450)
    let subviewHeight = CGFloat(20)

    //Add other subviews to contentView
    (0...4).compactMap { i in
        (0...19).compactMap { j in
             let x = CGFloat(j)*(subviewWidth + 5.0) + 5.0
            let y = CGFloat(i)*(subviewHeight + 5.0) + 5.0
             let subview = UIView(frame: CGRect(x: x, y: y, width: subviewWidth, height: subviewHeight))
                contentView.addSubview(subview)
      }
  }

    

The issue is that scroll view doesn't scrolls when autolayout constraints are set as above. Without constraints set, it scrolls but after zooming, scrolling gets disabled once again. I just have this set in scroll view delegate.

  public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
    return scrollView.subviews.first //contentView
}
1
Do some additional searching / researching to learn about how UIScrollView works. If you're using auto-layout for the scroll view's content (which, you should be doing), you never want to set .contentSize -- that's just wrong. - DonMag
Ok that works, I need to dynamically alter contentView's width and height anchor as and when I add more subviews to UIScrollView. But now it seems contentSize is not immediately updated when I set the constraints. I need to query contentSize property for various reasons. Do I need to wait to query it? - Deepak Sharma
To get help with this, you need to show some code. "I need to dynamically alter contentView's width and height anchor as and when I add more subviews" ... That's a pretty common task... How are you doing that? Are you properly changing constraints when you add subviews? Without seeing what you are doing (your code), there is no way to tell you what you're doing wrong. - DonMag
I updated the question. As of now I am just adding subviews in a loop the way shown. Later on, I will dynamically add or delete these subviews. - Deepak Sharma
So... are you NOT using auto-layout for the subviews you will be adding? - DonMag

1 Answers

1
votes

For your approach...

You want to constrain your contentView to the scroll view's Content Layout Guide. This will automatically determine the "scrollable" area.

Since you're not using auto-layout for the contentView's subviews, you'll need to update the .contentView Width and Height constraints each time you add a new subview.

Here's an example. We'll create a MyScrollView at 40-pts from Top / Leading / Trailing with a Height of 240-pts .

The subview will have a Green background, the contentView will have a Blue background, and the scroll view will have a Red background (so we can easily see the frames).

We'll start with just ONE subview to make it easy to see what happens. At first, with only one small subview, there will be no scrolling.

Each time we tap anywhere, we'll add a new Subview to the contentView, with a Yellow background, and update the contentView's Width and Height constraints as needed. You'll see that the Blue content view gets bigger to match the subviews. As soon as the subviews cause the content view to be larger than the width or height of the scroll view, scrolling will be automatic.

public class MyScrollView: UIScrollView {
    
    private var contentView:UIView!
    
    // contentView's Width and Height constraints
    //  we'll update the .constant values when we add subviews
    private var cvWidthConstraint: NSLayoutConstraint!
    private var cvHeightConstraint: NSLayoutConstraint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        self.translatesAutoresizingMaskIntoConstraints = false
        self.isScrollEnabled = true
        self.isDirectionalLockEnabled = true
        self.showsHorizontalScrollIndicator = true
        self.showsVerticalScrollIndicator = false
        self.decelerationRate = .normal
        self.delaysContentTouches = false
        self.bouncesZoom = true
        
        setupSubviews()
        
    }
    
    private func setupSubviews() {
        
        contentView = UIView()
        contentView.backgroundColor = UIColor.clear
        contentView.translatesAutoresizingMaskIntoConstraints = false
        contentView.isUserInteractionEnabled = true
        
        self.addSubview(contentView)
        
        // constrain contentView to scroll view's Content Layout Guide
        //  this determines the "scrollable" area
        contentView.leadingAnchor.constraint(equalTo: self.contentLayoutGuide.leadingAnchor).isActive = true
        contentView.trailingAnchor.constraint(equalTo: self.contentLayoutGuide.trailingAnchor).isActive = true
        contentView.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor).isActive = true

        // create contentView's Width and Height constraints
        cvWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0.0)
        cvHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 0.0)
        
        // activate them
        cvWidthConstraint.isActive = true
        cvHeightConstraint.isActive = true
        
        //Add other subviews to contentView
        
        // we'll start with ONE subview, so we can easily see what's happening
        let subviewWidth = CGFloat(240)
        let subviewHeight = CGFloat(20)

        let subview = UILabel(frame: CGRect(x: 5, y: 5, width: subviewWidth, height: subviewHeight))
        subview.textAlignment = .center
        subview.text = "First"
        subview.backgroundColor = .green
        contentView.addSubview(subview)

        // so we can see the frames
        self.backgroundColor = .red
        self.contentView.backgroundColor = .blue
        
        // update the contentView constraints
        updateContent()
    }
    
    private func updateContent() -> Void {
        // array of subviews
        let views = contentView.subviews
        // get the
        //  max Y of the subview frames
        //  max X of the subview frames
        guard let maxYValue = views.lazy.map({ $0.frame.maxY }).max(),
              let maxXValue = views.lazy.map({ $0.frame.maxX }).max()
        else { return }
        
        // update contentView Width and Height constraints
        cvWidthConstraint.constant = maxXValue + 5.0
        cvHeightConstraint.constant = maxYValue + 5.0
    }
    
    func addLabel(frame _frame: CGRect, text: String) -> Void {

        // add a new subview
        let subview = UILabel(frame: _frame)
        subview.textAlignment = .center
        subview.text = text
        subview.backgroundColor = .yellow
        contentView.addSubview(subview)
        
        // update the contentView constraints
        updateContent()

    }
}


class ExampleViewController: UIViewController {
    
    let myScrollView = MyScrollView()
    
    var count: Int = 1
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(myScrollView)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([

            // constrain custom scroll view Top / Leading / Trailing
            //  40-pts from the safe-area edges
            myScrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            myScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            myScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            // scroll view Height: 240-pts
            myScrollView.heightAnchor.constraint(equalToConstant: 240.0),
        ])
        
        // add tap gesture recognizer so we can add a new subview
        //  every time we tap
        let t = UITapGestureRecognizer(target: self, action: #selector(self.testAddSubview))
        view.addGestureRecognizer(t)
    }
    
    @objc func testAddSubview() -> Void {
        let s = "New Subview \(count)"
        let x: CGFloat = CGFloat(count) * 60.0
        let y: CGFloat = CGFloat(count) * 35.0
        myScrollView.addLabel(frame: CGRect(x: x, y: y, width: 200, height: 30), text: s)
        count += 1
    }
}

On launch - one subview - no scrolling:

enter image description here

After adding one new subview - blue content view is larger, but not big enough for scrolling:

enter image description here

After adding 4 new subviews - now we have scrolling:

enter image description here