Search code examples
iosswiftuiscrollviewautolayout

How do I create a robust ScrollView with both horizontal and vertical paging using autoLayout?


Situation

So I'm trying to make a UIScrollView that is basically a menu navigation bar that a user can navigate by swiping between menu items, where there are pages vertically laid out, and sub-pages horizontally laid out. Mockup: enter image description here

The way I'm doing this is by creating a UIScrollView whose frame is the size of one UILabel, and I have isPagingEnabled set to true. I then tried adding a UIStackView, with each row indicating a page, and the contents of each row being a sub-page. I set the scrollView.contentSize to be the size of the UIStackView. The problem is that all my label's frames are zeros all through and the UIScrollView doesn't work.

:/

I really wanted to avoid getting help as I felt like I could do this by myself but I've spent two days on this and I've lost all hope.

Code

Here is the code where I add the labels. It's called upon the scrollview's superview's init (because the UIScrollView is in a custom UIView I call crossNavigation View).

private func addScrollViewLabels() {
    
    //Get Max Number of Items in a Single Row
    var maxRowCount = -1
    for item in items {
        if (item.contents.count > maxRowCount) {maxRowCount = item.contents.count}
    }
    
    self.rowsStackView.axis = .vertical
    self.rowsStackView.distribution = .fillEqually
    self.rowsStackView.alignment = .fill
    self.rowsStackView.translatesAutoresizingMaskIntoConstraints = false
    self.scrollView.addSubview(rowsStackView)
    
    for i in 0 ..< items.count {
        let row = items[i].contents
        let rowView = UIView()
        self.rowsStackView.addArrangedSubview(rowView)
        var rowLabels : [UILabel] = []
        //First Label
        rowLabels.append(UILabel())
        rowView.addSubview(rowLabels[0])
        
        NSLayoutConstraint(item: rowLabels[0], attribute: .leading, relatedBy: .equal, toItem: rowView, attribute: .leading, multiplier: 1.0, constant: 0.0).isActive = true
        NSLayoutConstraint(item: rowLabels[0], attribute: .top, relatedBy: .equal, toItem: rowView, attribute: .top, multiplier: 1.0, constant: 0.0).isActive = true
        NSLayoutConstraint(item: rowLabels[0], attribute: .bottom, relatedBy: .equal, toItem: rowView, attribute: .bottom, multiplier: 1.0, constant: 0.0).isActive = true
        NSLayoutConstraint(item: rowLabels[0], attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: 0.55, constant: 0.0).isActive = true
        //Middle Labels
        for j in 1 ..< row.count {
            rowLabels.append(UILabel())
            rowView.addSubview(rowLabels[j])
            
            //Stick it to it's left
            NSLayoutConstraint(item: rowLabels[j], attribute: .leading, relatedBy: .equal, toItem: rowLabels[j-1], attribute: .trailing, multiplier: 1.0, constant: 0.0).isActive = true
            //Stick top to rowView
            NSLayoutConstraint(item: rowLabels[j], attribute: .top, relatedBy: .equal, toItem: rowView, attribute: .top, multiplier: 1.0, constant: 0.0).isActive = true
            //Row Height is equal to rowView's Height
            NSLayoutConstraint(item: rowLabels[j], attribute: .height, relatedBy: .equal, toItem: rowView, attribute: .height, multiplier: 1.0, constant: 0.0).isActive = true
            //rowLabels[j].width = containerView.width * 0.55 (so other labels can peek around it)
            NSLayoutConstraint(item: rowLabels[j], attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: 0.55, constant: 0.0).isActive = true
        }
        
        //lastLabel.trailing = rowView.trailing
        NSLayoutConstraint(item: rowLabels[row.count-1], attribute: .trailing, relatedBy: .equal, toItem: rowView, attribute: .trailing, multiplier: 1.0, constant: 0.0).isActive = true
    }

    //Constraints for stack view:
    //rowsStackView.height = scrollView.height * items.count
    NSLayoutConstraint(item: rowsStackView, attribute: .height, relatedBy: .equal, toItem: scrollView, attribute: .height, multiplier: CGFloat(self.items.count), constant: 0.0).isActive = true
    //rowsStackView.height = scrollView.height * items.count
    NSLayoutConstraint(item: rowsStackView, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .leading, multiplier: 1.0, constant: 0.0).isActive = true
    NSLayoutConstraint(item: rowsStackView, attribute: .top, relatedBy: .equal, toItem: scrollView, attribute: .top, multiplier: 1.0, constant: 0.0).isActive = true
    NSLayoutConstraint(item: rowsStackView, attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: 1.0, constant: 0.0).isActive = true
    
    self.scrollView.contentSize = rowsStackView.frame.size
}

Solution

  • Take a look at this demo project I made based off your mock up. It sets up the framework for everything you want to do.

    Examine the view hierarchy and you'll understand where to configure the layout to make it exactly how you want.

    The last step is to tweak the paging so you get the snap behavior you desire. There are many ways to do this but it is a bit involved.

    Good Luck!

    ScreenShot