1

Let me explain what I want to achieve. I want to create a slider and horizontal list of buttons inside UIStackView and UIScrollView so that buttons can scroll and then UISlider and UIScrollview will be placed inside vertical UIStackView. But the problem is I can scroll the UISlider but the buttons horizontally seem stuck or overlapped with the UIScrolView horizontal and it was not working I tried everything but not able to fix it. I wanted to do it programmatically. Any Help is really helpful

class ViewController: UIViewController {
    private var stackView: UIStackView!
    private var stackViewNew: UIStackView!
    let x: CGFloat = 10
    let width: CGFloat = UIScreen.main.bounds.width - 20
    var y: CGFloat =  10
    var i = 0
    let step:Float=10 
    let scrollView: UIScrollView = {
       let v = UIScrollView()
       v.translatesAutoresizingMaskIntoConstraints = true
       v.frame =  CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: 200)
      return v
     }()
     private var stackViewFilter: UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = true
        v.axis = .vertical
        v.backgroundColor = .black
        v.alpha = 0.8
        v.frame =  CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: 330)
        v.frame.origin = CGPoint(x:0 , y: UIScreen.main.bounds.height - 330)
        v.distribution = .equalSpacing
        v.spacing = 10.0
        return v
     }()

     let horizontalStackView : UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = true
        v.axis = .horizontal
        v.backgroundColor = .systemPink
        v.frame = CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: 200)
        v.distribution = .equalSpacing
        v.spacing = 10.0
        return v
    }()
    
    override func viewDidLoad() {
       super.viewDidLoad()
       createBottomFilter()
    }
    
    @objc func createBottomFilter(){
        
 
        /*---------- Slider Section ----------*/
        let mySlider = UISlider(frame:CGRect(x: 40, y: 10, width: 200, height: 60))
        mySlider.minimumValue = 0
        mySlider.maximumValue = 100
        mySlider.isContinuous = true
        mySlider.tintColor = UIColor.green
        mySlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
        mySlider.translatesAutoresizingMaskIntoConstraints = true
        
        UIView.animate(withDuration: 0.8) {
            mySlider.setValue(80.0, animated: true)
        }
    
        self.view.addSubview(stackViewFilter)
       
        stackViewFilter.addArrangedSubview(mySlider)
        stackViewFilter.addArrangedSubview(scrollView)
        
        self.view.addSubview(scrollView)

        mySlider.leadingAnchor.constraint(equalTo: stackViewFilter.leadingAnchor, constant: 8).isActive = true
        mySlider.trailingAnchor.constraint(equalTo: stackViewFilter.trailingAnchor, constant: 8).isActive = true
        mySlider.topAnchor.constraint(equalTo: stackViewFilter.bottomAnchor, constant: 10).isActive = true
      constraintBottom = mySlider.bottomAnchor.constraint(equalTo: scrollView.topAnchor, constant: 40)
        constraintBottom?.isActive = true
        
        scrollView.leftAnchor.constraint(equalTo: stackViewFilter.leftAnchor, constant: 0.0).isActive = true
        scrollView.topAnchor.constraint(equalTo: mySlider.bottomAnchor, constant: 8.0).isActive = true
        scrollView.rightAnchor.constraint(equalTo: stackViewFilter.rightAnchor, constant: 80.0).isActive = true
        scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 8.0).isActive = true
        
        // add the stack view to the scroll view
        scrollView.addSubview(horizontalStackView)
       
        horizontalStackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 0.0).isActive = true
        horizontalStackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 0.0).isActive = true
        horizontalStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -30.0).isActive = true
                
        let b = generateButton(title: "Btn 1", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        b.translatesAutoresizingMaskIntoConstraints = true

        let b1 = generateButton(title: "Btn 2", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        b1.translatesAutoresizingMaskIntoConstraints = true

        let b2 = generateButton(title: "Btn 3", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
      
        b2.translatesAutoresizingMaskIntoConstraints = true

        let b3 = generateButton(title: "Btn 4", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        b3.translatesAutoresizingMaskIntoConstraints = true

        let b4 = generateButton(title: "Btn 5", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        b4.translatesAutoresizingMaskIntoConstraints = true

        let b5 = generateButton(title: "Btn 6", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        b5.translatesAutoresizingMaskIntoConstraints = true

       
        horizontalStackView.addArrangedSubview(b)
        horizontalStackView.addArrangedSubview(b1)
        horizontalStackView.addArrangedSubview(b2)
        horizontalStackView.addArrangedSubview(b3)
        horizontalStackView.addArrangedSubview(b4)
        horizontalStackView.addArrangedSubview(b5)
        horizontalStackView.alignment = .center
        
    }
    
    @objc func generateButton(title: String, selectedTitle: String? = nil, iconName: String, scaledToSize newSize: CGSize) -> UIButton {
        let iconName: UIImage? = imageWithImage(UIImage(named: iconName), scaledToSize:CGSize(width: newSize.width, height: newSize.height))
    iconName?.withTintColor(.white)

        let button = UIButton.vertical(padding: 3)
        button.frame = CGRect(x: x, y: y, width: width, height: 80)
        button.setImage(iconName, for: .normal)
        button.layer.zPosition = 1
        button.setTitle(title, for: .normal)
        button.setTitle(selectedTitle, for: .selected)
        self.view.addSubview(button)
        i += 1
        y += button.frame.height
        return button
    }
    
 
}
class VerticalButton: UIButton {
    override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
    let titleRect = super.titleRect(forContentRect: contentRect)
    let imageRect = super.imageRect(forContentRect: contentRect)

     return CGRect(x: 0,
              y: contentRect.height - (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2 - titleRect.size.height,
              width: contentRect.width,
              height: titleRect.height)
    }

    override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
       let imageRect = super.imageRect(forContentRect: contentRect)
       let titleRect = self.titleRect(forContentRect: contentRect)

       return CGRect(x: contentRect.width/2.0 - imageRect.width/2.0,
               y: (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2,
              width: imageRect.width,
              height: imageRect.height )
    }

    private let padding: CGFloat
    init(padding: CGFloat) {
       self.padding = padding
       super.init(frame: .zero)
       self.titleLabel?.textAlignment = .center
    }

    required init?(coder aDecoder: NSCoder) { fatalError() }
}

extension UIButton {
    static func vertical(padding: CGFloat) -> UIButton {
   return VerticalButton(padding: padding)
 }}
user2572661
  • 132
  • 1
  • 7

1 Answers1

1

It's a little difficult, because you didn't show an image of what you want to achieve.

Also, your code is missing you imageWithImage(...) function, so we can't run it directly to see exactly what you're getting.

However, this may help you on your way...

You are doing many things incorrectly -- mixing explicit frame settings with stack views (which use auto-layout); adding views to the wrong place; setting constraints where you shouldn't be; etc.

Hers is your code to hopefully get close to what you're after. I added comments explaining what should't be there, and commented out your existing code so you can see the differences:

class ViewController: UIViewController {

    var i = 0

    // these will not be used
    //  private var stackView: UIStackView!
    //  private var stackViewNew: UIStackView!
    //  let x: CGFloat = 10
    //  let width: CGFloat = UIScreen.main.bounds.width - 20
    //  var y: CGFloat =  10
    //  let step:Float=10
    
    let scrollView: UIScrollView = {
        let v = UIScrollView()
        // we will want to use auto-layout
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    private var stackViewFilter: UIStackView = {
        let v = UIStackView()
        // we will want to use auto-layout
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .vertical
        v.backgroundColor = .black
        v.alpha = 0.8
        // use .fill instead of .equalSpacing
        v.distribution = .fill
        v.spacing = 10.0
        return v
    }()
    
    let horizontalStackView : UIStackView = {
        let v = UIStackView()
        // we will want to use auto-layout
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .horizontal
        v.backgroundColor = .systemPink
        // you want the buttons to be equal sizes,
        // so use .fillEqually instead of .equalSpacing
        v.distribution = .fill
        v.spacing = 10.0
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        createBottomFilter()
    }
    
    @objc func createBottomFilter(){
        
        /*---------- Slider Section ----------*/
        // we will want to use auto-layout
        // so no need to set a frame here
        //let mySlider = UISlider(frame:CGRect(x: 40, y: 10, width: 200, height: 60))
        let mySlider = UISlider()
        mySlider.translatesAutoresizingMaskIntoConstraints = false

        mySlider.minimumValue = 0
        mySlider.maximumValue = 100
        mySlider.isContinuous = true
        mySlider.tintColor = UIColor.green
        mySlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
        
        UIView.animate(withDuration: 0.8) {
            mySlider.setValue(80.0, animated: true)
        }
        
        // respect safe-area
        let g = view.safeAreaLayoutGuide
        
        self.view.addSubview(stackViewFilter)

        // constrain stackViewFilter
        NSLayoutConstraint.activate([
            stackViewFilter.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            stackViewFilter.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            stackViewFilter.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            stackViewFilter.heightAnchor.constraint(equalToConstant: 330.0),
        ])

        stackViewFilter.addArrangedSubview(mySlider)
        stackViewFilter.addArrangedSubview(scrollView)
        
        // just added scrollView as an arrangedSubview of stackViewFilter
        // so don't add it to the view
        //self.view.addSubview(scrollView)
        
        // slider is in a stack view, so don't set any positioning constraints
        
        //mySlider.leadingAnchor.constraint(equalTo: stackViewFilter.leadingAnchor, constant: 8).isActive = true
        //mySlider.trailingAnchor.constraint(equalTo: stackViewFilter.trailingAnchor, constant: 8).isActive = true
        //mySlider.topAnchor.constraint(equalTo: stackViewFilter.bottomAnchor, constant: 10).isActive = true
        
        //constraintBottom = mySlider.bottomAnchor.constraint(equalTo: scrollView.topAnchor, constant: 40)
        //constraintBottom?.isActive = true
        
        // scrollView is in a stack view, so don't set any positioning constraints
        //scrollView.leftAnchor.constraint(equalTo: stackViewFilter.leftAnchor, constant: 0.0).isActive = true
        //scrollView.topAnchor.constraint(equalTo: mySlider.bottomAnchor, constant: 8.0).isActive = true
        //scrollView.rightAnchor.constraint(equalTo: stackViewFilter.rightAnchor, constant: 80.0).isActive = true
        //scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 8.0).isActive = true
        
        // but we can set the scrollView's height constraint here
        scrollView.heightAnchor.constraint(equalToConstant: 200.0).isActive = true
        
        // add the stack view to the scroll view
        scrollView.addSubview(horizontalStackView)
        
        // constrain scrollView contents to the scrollView's contentLayoutGuide
        //horizontalStackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 0.0).isActive = true
        //horizontalStackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 0.0).isActive = true
        //horizontalStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -30.0).isActive = true
        NSLayoutConstraint.activate([
            horizontalStackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            horizontalStackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            horizontalStackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
            horizontalStackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: 30.0),
        ])
        
        // views added to stackView as arrangedSubview automatically use auto-layout
        // so no sense setting .translatesAutoresizingMaskIntoConstraints = true
        
        let b = generateButton(title: "Btn 1", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        //b.translatesAutoresizingMaskIntoConstraints = true
        
        let b1 = generateButton(title: "Btn 2", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        //b1.translatesAutoresizingMaskIntoConstraints = true
        
        let b2 = generateButton(title: "Btn 3", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        //b2.translatesAutoresizingMaskIntoConstraints = true
        
        let b3 = generateButton(title: "Btn 4", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        //b3.translatesAutoresizingMaskIntoConstraints = true
        
        let b4 = generateButton(title: "Btn 5", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        //b4.translatesAutoresizingMaskIntoConstraints = true
        
        let b5 = generateButton(title: "Btn 6", selectedTitle: "Filter \(i)", iconName: "hellen", scaledToSize:CGSize(width: 90.0, height: 90.0))
        //b5.translatesAutoresizingMaskIntoConstraints = true
        
        
        horizontalStackView.addArrangedSubview(b)
        horizontalStackView.addArrangedSubview(b1)
        horizontalStackView.addArrangedSubview(b2)
        horizontalStackView.addArrangedSubview(b3)
        horizontalStackView.addArrangedSubview(b4)
        horizontalStackView.addArrangedSubview(b5)
        
        // alignment should be .fill, not .center
        horizontalStackView.alignment = .fill
        
        // because we set horizontalStackView.distribution = .fillEqually
        // we only need to set a width constraint on the first button
        b.widthAnchor.constraint(equalToConstant: 90.0).isActive = true
        
        // buttons should all be 90x90 ?
        [b, b1, b2, b3, b4, b5].forEach { btn in
            btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true
        }
        
    }
    
    @objc func generateButton(title: String, selectedTitle: String? = nil, iconName: String, scaledToSize newSize: CGSize) -> UIButton {
        let iconName: UIImage? = imageWithImage(UIImage(named: iconName), scaledToSize:CGSize(width: newSize.width, height: newSize.height))
        iconName?.withTintColor(.white)
        
        let button = UIButton.vertical(padding: 3)
        // buttons in stack view will use auto-layout,
        // so no need to set frames here
        //button.frame = CGRect(x: x, y: y, width: width, height: 80)
        button.setImage(iconName, for: .normal)
        button.layer.zPosition = 1
        button.setTitle(title, for: .normal)
        button.setTitle(selectedTitle, for: .selected)
        
        // button will be added to stack view
        //self.view.addSubview(button)
        
        i += 1
        
        // not sure why this was here to begin with...
        // you want a horizontal row of buttons, so changing the
        // y position makes no sense
        //y += button.frame.height
        
        return button
    }
    
    
}

class VerticalButton: UIButton {
    override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
        let titleRect = super.titleRect(forContentRect: contentRect)
        let imageRect = super.imageRect(forContentRect: contentRect)
        
        return CGRect(x: 0,
                      y: contentRect.height - (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2 - titleRect.size.height,
                      width: contentRect.width,
                      height: titleRect.height)
    }
    
    override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
        let imageRect = super.imageRect(forContentRect: contentRect)
        let titleRect = self.titleRect(forContentRect: contentRect)
        
        return CGRect(x: contentRect.width/2.0 - imageRect.width/2.0,
                      y: (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2,
                      width: imageRect.width,
                      height: imageRect.height )
    }
    
    private let padding: CGFloat
    init(padding: CGFloat) {
        self.padding = padding
        super.init(frame: .zero)
        self.titleLabel?.textAlignment = .center
    }
    
    required init?(coder aDecoder: NSCoder) { fatalError() }
}

extension UIButton {
    static func vertical(padding: CGFloat) -> UIButton {
        return VerticalButton(padding: padding)
    }
}

Here's how it looks, using a system "doc" image for the buttons, since I don't know what you're doing with iconName... the horizontal buttons can be scrolled:

enter image description here

If that's close to what you want, you should be able to tweak values after reviewing the code.

DonMag
  • 69,424
  • 5
  • 50
  • 86