A settings page is what generally every app will need after some time if it is growing and to make a settings page for an app is very difficult trust me. I still remember when I first got a task of creating a settings page for an app I was confused what should I do I need a sliderView for some settings then I need pickerView for some other settings so making it in one tableView was looking like an impossible task but after some advice from experts I finally created a tableView which had dynamic cells having a different type of elements inside it and they controlled the main settings of my app and I will like to share it in this blog because it is not mine its given by our superior developers to all the growing developers.
We will assume we have a video playing app and we need to create a settings VC to control the playing of video like volume level, quality of the video, and speed of the video. So for volume control, we will use UISliderView for quality change we will use UISegmentControl and for speed control, we will use UIPickerView all will be in one tableView and will be able to control the settings of our app so let’s get started.
First, we will create a tableView and assign dataSource, and delegate it to the viewController.(Note: we are using a lazy variable as with define and call closure of a variable, so the ‘Self’ is not available without it being lazy)
lazy var tableView:UITableView = {
let tbl = UITableView()
tbl.translatesAutoresizingMaskIntoConstraints = false
tbl.dataSource = self
tbl.delegate = self
return tbl
}()
Now we will create a tableViewCell and a label inside it, that will show the name of the setting.
class settingsCell:UITableViewCell {
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
We will now give constraints to our tableView and label that will be inside the initializers.
func initTableView() {
view.addSubview(tableView)
tableView.rowHeight = 50
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
func initTitleLabel() {
contentView.addSubview(titleLabel)
titleLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
titleLabel.leadingAnchor.constraint(equalTo:contentView.layoutMarginsGuide.leadingAnchor).isActive = true
}
We have given fix height of 50 for our cell and to the titleLabel but we will make it dynamic just trust me for now.
Now registering the cell from tableView and giving data to dataSource of tableView we will see the cells coming.
But now how to make it dynamic! we will see that now.
So to make it dynamic we need to know which type of setting we will show in a particular cell so for that we will create an enum for all possible types of controls we want, in our case, it’s 3.
enum CellType {
case sliderView
case segmentController
case pickerView
}
So to make it dynamic we will create and declare a UIView like this in our cell class
private(set) var settingsView: UIView = UIView()
Now we will create functions to initialize our settings view which are slider, segment, and pickerView but we will not declare them separately we will just declare them to our settingsView variable so there will only be one settingsView at a time.
func initSlider() {
settingsView = UISlider()
settingsView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(settingsView)
settingsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: contentView.bounds.width * 0.20).isActive = true
settingsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: -(contentView.bounds.width * 0.20)).isActive = true
settingsView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
}
func initSegment() {
settingsView = UISegmentedControl(items: ["low","medium","high"])
settingsView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(settingsView)
settingsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: contentView.bounds.width * 0.20).isActive = true
settingsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: -(contentView.bounds.width * 0.20)).isActive = true
settingsView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
}
func initPicker() {
settingsView = UIPickerView()
settingsView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(settingsView)
settingsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
settingsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
settingsView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
}
Now declaring a variable of currentType in our cell class (we will use it in cellForRow) and make a function to know which type was selected so according to that we will initialize our cell type
var currentCellType:CellType?
func initTheSettingView() {
if let type = currentCellType {
switch type {
case .sliderView:
initSlider()
case .segmentController:
initSegment()
case .pickerView:
initPicker()
}
}
}
So now our views are being created dynamically according to the type we provide in cellForRow so for that, we will make some variables that will keep our data type of settings as our settings will not change dynamically (still we can do that but that topic is for another day).
let settingsName = ["Volume Level","Video playback quality","playback speed"]
let settingCellType:[settingsCell.CellType] = [.sliderView,.segmentController,.pickerView]
And in our cellForRow we will just take these values from indexPath.row and we are good to go but still, there is a problem how we will expand the cells according to the tap of the user to show the settings selection view for that we need to know the height of the cell component so for that, we will create an array of heights and will append height value of a cell from cellForRow so finally our cellForRow will look something like this
var heights:[CGFloat] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "settingsCell") as! settingsCell
let name = settingsName[indexPath.row]
cell.titleLabel.text = name
cell.currentCellType = settingCellType[indexPath.row]
cell.initTitleLabel()
heights.append(cell.settingsView.bounds.height + 50)
return cell
}
Now we will implement two more methods didSelectRow and height for row and a variable for tracking whether if the cell has expanded or not and according to that in heightForRow we will give height to our cells here is how to do that
var expandedCells:[Int] = []
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let indx = expandedCells.firstIndex(of: indexPath.row){
expandedCells.remove(at: indx)
} else {
expandedCells.append(indexPath.row)
}
tableView.beginUpdates()
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if !expandedCells.contains(indexPath.row) {
return 50
} else {
return heights[indexPath.row]
}
}
So finally this is what we get by implementing this