Search code examples
iosswiftuiscrollviewuistackview

UISlider in not working properly in UIStackView


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)
 }}

Solution

  • 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.