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