Search code examples
iosslideruisliderrangeslider

finding a tableView cells superView


I am trying to create a range slider that has labels representing the sliders handle value. I have the slider enabled but when I try to add the labels to the sliders subview, my app crashes with the error

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

The slider is inside of a tableViewCell and I am initializing this cell inside of the tableView VC with the code below,

if indexPath.section == 2 {
        let costRangeCell = AgeRangeCell(style: .default, reuseIdentifier: nil)

        let contentView = costRangeCell.rangeSlider.superview! 

// my declaration of contentView is where my app is crashing.

        costRangeCell.rangeSlider.minimumValue = 0
        costRangeCell.rangeSlider.maximumValue = 100
        costRangeCell.rangeSlider.lowValue = 0
        costRangeCell.rangeSlider.highValue = 100
        costRangeCell.rangeSlider.minimumDistance = 20

        let lowLabel = UILabel()
        contentView.addSubview(lowLabel)
        lowLabel.textAlignment = .center
        lowLabel.frame = CGRect(x:0, y:0, width: 60, height: 20)

        let highLabel = UILabel()
        contentView.addSubview(highLabel)
        highLabel.textAlignment = .center
        highLabel.frame = CGRect(x: 0, y: 0, width: 60, height: 20)

        costRangeCell.rangeSlider.valuesChangedHandler = { [weak self] in
          
        let lowCenterInSlider = CGPoint(x:costRangeCell.rangeSlider.lowCenter.x, y: costRangeCell.rangeSlider.lowCenter.y - 30)
        let highCenterInSlider = CGPoint(x:costRangeCell.rangeSlider.highCenter.x, y: costRangeCell.rangeSlider.highCenter.y - 30)
        let lowCenterInView = costRangeCell.rangeSlider.convert(lowCenterInSlider, to: contentView)
        let highCenterInView = costRangeCell.rangeSlider.convert(highCenterInSlider, to: contentView)

        lowLabel.center = lowCenterInView
        highLabel.center = highCenterInView
        lowLabel.text = String(format: "%.1f", costRangeCell.rangeSlider.lowValue)
        highLabel.text = String(format: "%.1f", costRangeCell.rangeSlider.highValue)
        }
        
        costRangeCell.rangeSlider.addTarget(self, action: #selector(handleMinAgeChange), for: .valueChanged)
        let minAge = user?.minSeekingCost ?? SettingsViewController.defaultMinSeekingCost
        costRangeCell.rangeLabel.text = " $\(minAge)"
        return costRangeCell
    }

Is there a different way for me to gain access to the cells range slider superView?

ageRange class,

class AgeRangeCell: UITableViewCell {

let rangeSlider: AORangeSlider = {
    let slider = AORangeSlider()
    slider.minimumValue = 20
    slider.maximumValue = 200
    return slider
}()


let rangeLabel: UILabel = {
    let label = costRangeLabel()
    label.text = "$ "
    return label
}()




class costRangeLabel: UILabel {
    override var intrinsicContentSize: CGSize {
        return .init(width: 80, height: 50)
    }
}

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    contentView.isUserInteractionEnabled = true

    let overallStackView = UIStackView(arrangedSubviews: [
        UIStackView(arrangedSubviews: [rangeLabel, rangeLabel]),
        ])
    overallStackView.axis = .horizontal
    overallStackView.spacing = 16
    addSubview(overallStackView)
    overallStackView.anchor(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor, padding: .init(top: 16, left: 16, bottom: 16, right: 16))
    
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

  }

AORangeSlider is a custom Slider.


Solution

  • Took a look at AORangeSlider...

    You want to implement your label tracking inside your custom cell... not in your controller class.

    Here's a simple implementation, based on the code you supplied in your question:

    class AgeRangeCell: UITableViewCell {
        let rangeSlider: AORangeSlider = {
            let slider = AORangeSlider()
            slider.minimumValue = 0
            slider.maximumValue = 100
            return slider
        }()
    
        let lowLabel = UILabel()
        let highLabel = UILabel()
    
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            
            lowLabel.textAlignment = .center
            lowLabel.frame = CGRect(x:0, y:0, width: 60, height: 20)
            
            highLabel.textAlignment = .center
            highLabel.frame = CGRect(x: 0, y: 0, width: 60, height: 20)
            
            [rangeSlider, lowLabel, highLabel].forEach {
                contentView.addSubview($0)
            }
            
            rangeSlider.translatesAutoresizingMaskIntoConstraints = false
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                rangeSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                rangeSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                rangeSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                rangeSlider.heightAnchor.constraint(equalToConstant: 40.0),
            ])
            
            // avoid auto-layout complaints
            let c = rangeSlider.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
            c.priority = UILayoutPriority(rawValue: 999)
            c.isActive = true
    
    
            rangeSlider.valuesChangedHandler = { [weak self] in
                guard let `self` = self else {
                    return
                }
                let lowCenterInSlider = CGPoint(x:self.rangeSlider.lowCenter.x, y: self.rangeSlider.lowCenter.y - 30)
                let highCenterInSlider = CGPoint(x:self.rangeSlider.highCenter.x, y: self.rangeSlider.highCenter.y - 30)
                let lowCenterInView = self.rangeSlider.convert(lowCenterInSlider, to: self.contentView)
                let highCenterInView = self.rangeSlider.convert(highCenterInSlider, to: self.contentView)
                
                self.lowLabel.center = lowCenterInView
                self.highLabel.center = highCenterInView
                self.lowLabel.text = String(format: "%.1f", self.rangeSlider.lowValue)
                self.highLabel.text = String(format: "%.1f", self.rangeSlider.highValue)
            }
            
        }
    }
    
    class RangeTableViewController: UITableViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // register your "slider" cell
            tableView.register(AgeRangeCell.self, forCellReuseIdentifier: "ageRangeCell")
    
            // register any other cell classes you'll be using
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: "plainCell")
    
        }
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 3
        }
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            if section == 2 {
                return 1
            }
            return 2
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            
            if indexPath.section == 2 {
                let cell = tableView.dequeueReusableCell(withIdentifier: "ageRangeCell", for: indexPath) as! AgeRangeCell
    
                cell.rangeSlider.minimumValue = 0
                cell.rangeSlider.maximumValue = 100
                cell.rangeSlider.lowValue = 0
                cell.rangeSlider.highValue = 100
                cell.rangeSlider.minimumDistance = 20
    
                return cell
            }
            
            let cell = tableView.dequeueReusableCell(withIdentifier: "plainCell", for: indexPath)
            cell.textLabel?.text = "\(indexPath)"
            return cell
        }
    
        override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
            return "Section Header: \(section)"
        }
    }
    

    That code will produce this:

    enter image description here