2

I added a stackView to a scrollView. I set the width, X, and Y constraints of the scrollView same as main view, with a fixed height of 50. For the stack constraints, I did the same thing but relative to the scrollView instead of the view.

My issue is when I add UIImageViews to my stack (all images are 50 x 50). I need the stack to show only the first three UIImageViews, and scroll horizontally if there are more than 3. So far, my stack always shows all the UIImageViews.

Any suggestion is appreciated. Been working on this for 2 days now. THANKS!

  • Check out this answer: https://stackoverflow.com/a/35136217/14351818 – aheze Mar 01 '21 at 18:50
  • Why would the stack view show only 3 images if the images are 50 points wide and the stack view / scroll view is the width of the main view? All of them fit in the main view so all of them are visible at once. A scroll view only scrolls if the content is bigger than the scroll view. Otherwise there is no reason to scroll. – matt Mar 01 '21 at 20:30
  • Also I'm unclear what the stack view is for in this story. This sounds a lot more like a collection view to me... – matt Mar 01 '21 at 20:32
  • Are you setting up your views in Storyboard? or via code? – DonMag Mar 01 '21 at 20:33
  • @matt I'm trying to make a horizontal bar (tab bar if you will) with 3 visible tabs, and scrollable when more than 3 tabs are available. – Jadiel Alfonso Mar 01 '21 at 22:22

1 Answers1

1

What you probably want to do...

  • constrain all 4 sides of the stack view to the scroll view's Content Layout Guide
  • constrain the Height of the stack view equal to the Height of the scroll view's Frame Layout Guide
  • do NOT constrain the Width of the stack view
  • set the stack view's Distribution to Fill

Create a "tab view" - here's an example with a 50 x 50 centered image view, rounded top corners and a 1-pt outline:

enter image description here

We can create that with this simple class:

class MyTabView: UIView {
    
    let imgView = UIImageView()
    
    init(with image: UIImage) {
        super.init(frame: .zero)
        imgView.image = image
        commonInit()
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        imgView.translatesAutoresizingMaskIntoConstraints = false
        // light gray background
        backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        addSubview(imgView)
        NSLayoutConstraint.activate([
            // centered
            imgView.centerXAnchor.constraint(equalTo: centerXAnchor),
            imgView.centerYAnchor.constraint(equalTo: centerYAnchor),
            // 50x50
            imgView.widthAnchor.constraint(equalToConstant: 50.0),
            imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
        ])
        
        // a little "styling" for the "tab"
        clipsToBounds = true
        layer.cornerRadius = 12
        layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        layer.borderWidth = 1
        layer.borderColor = UIColor.darkGray.cgColor
    }

}

For each "tab" that we add to the stack view, we'll set its Width constraint equal to the scroll view's Frame Layout Guide widthAnchor with multiplier: 1.0 / 3.0. That way each "tab view" will be 1/3rd the width of the scroll view:

enter image description here

enter image description here

enter image description here

With 1, 2 or 3 "tabs" there will be no horizontal scrolling, because they all fit within the frame.

Once we have more than 3 "tabs" the stack view's width will exceed the width of the frame, and we'll have horizontal scrolling:

enter image description here

Here's the view controller I used for that. It creates 9 "tab images"... starts with a single "tab"... each tap will ADD a "tab" until we have all 9, at which point each tap will REMOVE a "tab":

class StackAsTabsViewController: UIViewController {
    
    let stackView: UIStackView = {
        let v = UIStackView()
        v.axis = .horizontal
        v.distribution = .fill
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    // a label to show what's going on
    let statusLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    // array to hold our "tab" images
    var images: [UIImage] = []

    // we'll add a "tab" on each tap
    //  until we reach the end of the images array
    //  then we'll remove a "tab" on each tap
    //  until we're back to a single "tab"
    var isAdding: Bool = true

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add the "status" label
        view.addSubview(statusLabel)
        
        // add stackView to scrollView
        scrollView.addSubview(stackView)
        
        // add scrollView to view
        view.addSubview(scrollView)
        
        // respect safe area
        let g = view.safeAreaLayoutGuide
        
        // scrollView Content and Frame Layout Guides
        let contentG = scrollView.contentLayoutGuide
        let frameG = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // constrain scrollView Top / Leading / Trailing
            scrollView.topAnchor.constraint(equalTo: g.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            // height = 58 (image will be 50x50, so a little top and bottom padding)
            scrollView.heightAnchor.constraint(equalToConstant: 58.0),
            
            // constrain stackView all 4 sides to scrollView Content Layout Guide
            stackView.topAnchor.constraint(equalTo: contentG.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
            
            // stackView Height equal to scrollView Frame Height
            stackView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
            
            // statusLabel in the middle of the view
            statusLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 40.0),
            statusLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            statusLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0)
            
        ])
        
        // let's create 9 images using SF Symbols
        for i in 1...9 {
            guard let img = UIImage(systemName: "\(i).circle.fill") else {
                fatalError("Could not create images!!!")
            }
            images.append(img)
        }
        
        // add the first "tab view"
        self.updateTabs()
        
        // tap anywhere in the view
        let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
        view.addGestureRecognizer(t)

    }
    
    @objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
        updateTabs()
    }
    
    func updateTabs() -> Void {
        
        if isAdding {

            // get the next image from the array
            let img = images[stackView.arrangedSubviews.count]
            
            // create a "tab view"
            let tab = MyTabView(with: img)
            // add it to the stackView
            stackView.addArrangedSubview(tab)
            let frameG = scrollView.frameLayoutGuide
            NSLayoutConstraint.activate([
                // each "tab view" is 1/3rd the width of the scroll view frame
                tab.widthAnchor.constraint(equalTo: frameG.widthAnchor, multiplier: 1.0 / 3.0),
                // each "tab view" is the same height as the scroll view frame
                tab.heightAnchor.constraint(equalTo: frameG.heightAnchor),
            ])

        } else {

            stackView.arrangedSubviews.last?.removeFromSuperview()

        }

        if stackView.arrangedSubviews.count == 1 {
            isAdding = true
        } else if stackView.arrangedSubviews.count == images.count {
            isAdding = false
        }
        
        updateStatusLabel()
        
    }
    
    func updateStatusLabel() -> Void {
        
        // we'll do this async, to make sure the views have been updated
        DispatchQueue.main.async {
            let numTabs = self.stackView.arrangedSubviews.count
            var str = ""
            if self.isAdding {
                str += "Tap anywhere to ADD a tab"
            } else {
                str += "Tap anywhere to REMOVE a tab"
            }
            str += "\n\n"
            str += "Number of tabs: \(numTabs)"
            str += "\n\n"
            if numTabs > 3 {
                str += "Tabs WILL scroll"
            } else {
                str += "Tabs will NOT scroll"
            }
            self.statusLabel.text = str
        }
        
    }

}

Play with it, and see if that's what you're going for.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Very helpful! Thank you so much! This is along the lines of what I need. I got it to scroll horizontally based on your answer but the stack is messing the views inside of it (see attached). Gotta figure that out, but thanks again! – Jadiel Alfonso Mar 02 '21 at 20:47
  • @JadielAlfonso -- *"... messing the views inside of it (see attached)"* ... did you mean to include a screen-shot? – DonMag Mar 03 '21 at 13:02
  • I did, but clearly didn't attach the file :). I got it to work based on your answer though. I'm trying to figure out how to include MyTabView as part of the VC. My implementation defines a layout (which includes MyTabView, the stack, and scrollView + the logic), and a constructor which will be called whenever the tabs are needed. Problem is I need to define that the actual ImageView is 50X50. You nailed it though, so thanks again! – Jadiel Alfonso Mar 03 '21 at 17:06