Swift PS: Want to switch between different views in a subview according to different scenarios in UITableview

Urvashi
5 min readMay 4, 2024

Hola reader, just wanted to share a sample project I created for solving the above problem statement.Though an easy one to solve which can be implemented through various approach but how I solved was a way I didn’t find directly over internet so thought of sharing it.Hope it helps all beginners like me in iOS. Lets get started :)

First let me elaborate the problem statement: It is basically creating a table view where we have to change a subview inside our custom cell on basis of a CTA click.Below was the demanded feature:

Feature to be implemented

To implement this I followed the following procedure:

  • create a custom cell with the an empty subview which will be our dynamic view superview.

a) Below is the custom cell which has container view which takes all the space of its content view.We create a function to configure cell with label text and the subview we want to add to our dynamic view by first removing all the subviews added to it then adding our subview.

import UIKit

class CustomTableViewCell: UITableViewCell {
var containerView = ContainerView()

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}

//Adding container to cells content view
private func commonInit() {
contentView.addSubview(containerView)

//Adding constraints to all ui elements
NSLayoutConstraint.activate(staticConstraints())
}

func staticConstraints() -> [NSLayoutConstraint]{
var constraints = [NSLayoutConstraint]()
constraints.append(contentsOf: [containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0),
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0),
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0),
]

)

constraints.append(contentsOf: [
containerView.name.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10),
containerView.name.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 12),
])

constraints.append(contentsOf: [
containerView.dynamicView.centerYAnchor.constraint(equalTo: containerView.name.centerYAnchor),
containerView.dynamicView.widthAnchor.constraint(equalToConstant: 150),
containerView.dynamicView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
containerView.dynamicView.topAnchor.constraint(equalTo: containerView.topAnchor),
containerView.dynamicView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
])
return constraints
}

func configureCell(name: String,subView : UIView) {
containerView.name.text = name

containerView.dynamicView.subviews.forEach({$0.removeFromSuperview()})

containerView.dynamicView.addSubview(subView)
subView.translatesAutoresizingMaskIntoConstraints = false

var constraints = [NSLayoutConstraint]()

constraints.append(contentsOf: [
subView.topAnchor.constraint(equalTo: containerView.dynamicView.topAnchor),
subView.bottomAnchor.constraint(equalTo: containerView.dynamicView.bottomAnchor),
subView.leadingAnchor.constraint(equalTo: containerView.dynamicView.leadingAnchor),
subView.trailingAnchor.constraint(equalTo: containerView.dynamicView.trailingAnchor)
])
NSLayoutConstraint.activate(constraints)
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}

}

b) Our container view contains a label and a UIView(our dynamic view holder).Below is the container view implementation:

class ContainerView : UIView{

@IBOutlet weak var dynamicView: UIView!
@IBOutlet weak var name: UILabel!

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}

override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}

func initSubviews() {
let nib = UINib(nibName: "ContainerView", bundle: nil)
nib.instantiate(withOwner: self, options: nil)
dynamicView.translatesAutoresizingMaskIntoConstraints = false
name.translatesAutoresizingMaskIntoConstraints = false
name.numberOfLines = 3
self.addSubview(name)
self.addSubview(dynamicView)
self.translatesAutoresizingMaskIntoConstraints = false
}
}

c) Now we create all the views which we need to switch with our dynamic view here we have only two views : DynamicView1 and DynamicView2.Below are the implementations:

import Foundation
import UIKit

protocol DynamicViewProtocol {
func dynamicViewChange(point: CGPoint,child: UIView)
func deleteRow(point: CGPoint,child: UIView)
}

class DynamicView1: UIView{

@IBOutlet weak var yesButton: UIButton!

@IBOutlet weak var noButton: UIButton!

var delegate: DynamicViewProtocol?
var noTap : UIGestureRecognizer?
var yesTap : UIGestureRecognizer?

@objc func noAction() {
let p = (noTap?.location(in: noButton))!
delegate?.dynamicViewChange(point: p,child: noButton)
}

@objc func yesAction() {
let p = (yesTap?.location(in: yesButton))!
delegate?.deleteRow(point: p,child: yesButton)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}

override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}

func initSubviews() {
let nib = UINib(nibName: "DynamicView1", bundle: nil)
nib.instantiate(withOwner: self, options: nil)

yesButton.translatesAutoresizingMaskIntoConstraints = false
noButton.translatesAutoresizingMaskIntoConstraints = false


noTap = UITapGestureRecognizer(target: self, action: #selector(noAction))
noButton.addGestureRecognizer(noTap!)

yesTap = UITapGestureRecognizer(target: self, action: #selector(yesAction))
yesButton.addGestureRecognizer(yesTap!)

self.addSubview(yesButton)
self.addSubview(noButton)
self.translatesAutoresizingMaskIntoConstraints = false

var constraints = [NSLayoutConstraint]()
constraints.append(contentsOf: [
yesButton.leadingAnchor.constraint(equalTo: self.leadingAnchor,constant: 10),
yesButton.trailingAnchor.constraint(equalTo: noButton.leadingAnchor, constant: -10),
yesButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20)
])

constraints.append(contentsOf: [
noButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 10),
noButton.bottomAnchor.constraint(equalTo: self.bottomAnchor,constant: -20)
])
NSLayoutConstraint.activate(constraints)
}

}

class DynamicView2: UIView{

var delegate: DynamicViewProtocol?
var tap : UIGestureRecognizer?
var subView : UIButton = {
let button = UIButton()
button.setTitle("Remove", for: .normal)
button.setTitleColor(.brown, for: .normal)
return button
}()

@objc func btnAction(){
let p = (tap?.location(in: self))!
delegate?.dynamicViewChange(point: p,child: self)
}


required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}

override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}

func initSubviews(){
subView.translatesAutoresizingMaskIntoConstraints = false;
tap = UITapGestureRecognizer(target: self, action: #selector(btnAction))
subView.addGestureRecognizer(tap!)
self.addSubview(subView)
var constraints = [NSLayoutConstraint]()
constraints.append(contentsOf: [
subView.leadingAnchor.constraint(equalTo: self.leadingAnchor,constant: 10),
subView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10),
subView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20),
subView.widthAnchor.constraint(equalToConstant: 70)
])

NSLayoutConstraint.activate(constraints)
}

}
  • Now create a view controller where we will place our tableview.
import UIKit

class ViewController: UIViewController {

// UI Elements
@IBOutlet weak var tableView: UITableView!

// View Model
var viewModel : [String] = ["Urvashi","Vaishnavi","Anchita"]

//Storing state of each row element
var defaultState: [Bool] = [true,true,true]

override func viewDidLoad() {
super.viewDidLoad()

//register the custom cell to tableview
tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: "CustomCellId")

//setting datasource and delegate of our tableview
tableView.dataSource = self
tableView.delegate = self

tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 80
// Do any additional setup after loading the view.
}
}

extension ViewController: UITableViewDelegate,UITableViewDataSource{

//implementing protocol functions
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellId", for: indexPath) as? CustomTableViewCell else { return UITableViewCell() }

if defaultState[indexPath.row] {
let subView = DynamicView2()
subView.delegate = self
cell.configureCell(name: viewModel[indexPath.row], subView: subView)
} else {
let subView = DynamicView1()
subView.delegate = self
cell.configureCell(name: viewModel[indexPath.row], subView: subView)
}
return cell
}

}

extension ViewController: DynamicViewProtocol {

//Logic used in below functions: from the tap point in our dynamic view converting it into the coordinate system of tableView and then finding the table row tapped for further implementation

func deleteRow(point: CGPoint, child: UIView) {
let p = child.convert(point, to: self.tableView)
if let indexPath = tableView.indexPathForRow(at: p){
viewModel.remove(at: indexPath.row)
defaultState.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}

func dynamicViewChange(point: CGPoint,child : UIView) {
let p = child.convert(point, to: self.tableView)
if let indexPath = tableView.indexPathForRow(at: p){
defaultState[indexPath.row].toggle()
tableView.reloadRows(at: [indexPath], with: .none)
}
}

}

Now comes the logic part.What we have done above is we added the subview to each cell dynamicView depending upon its default state position: 0 for dynamicView1 and 1 for dynamicView2 state.

For each dynamicView we want to add we have created an onTap function in which we send the tap point local to that view to its delegate.Here, our delegate(sort of listener) is our view controller.We take the point and convert it into the coordinate of our tableview and find the indexpath of the cell where that point is,we change the state of the view and reload that cell u sing the indexpath.The reload cell inturns calls the cellForRowAt function where the view is added to our cell dynamicview according its default state.So finally we achieved the functionality we wanted.

Our functionality is ready to go ^^

Thank you for reading it till the end.Hope it helps you in getting you close to the solution you are searching for.See you in my next blog :)

--

--

Urvashi
0 Followers

A CSE working professional who loves to study and code :)