Configure Custom TableViewCell Outside of TableView? - ios

I have a TableView with a custom cell that requires rather lengthy configuration and is used more than once in my app. I would like to avoid duplicated code and just configure the cell in one place. Can I create a function like this?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "betterPostCell", for: indexPath) as! BetterPostCell
return configureCell(cell)
}
Ideally, I would be able to put configureCell in my BetterPostCell class. Is this possible?

Yes, you can do it, and it's a nice way to keep your table view code from blowing up, especially if you have many different types of cells in one table view.
In your BetterPostCell class, create a method called configure like so:
func configure() {
//configure your cell
}
Then in your cellForRowAt method, just call that method from your cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "betterPostCell", for: indexPath) as! BetterPostCell
cell.configure()
return cell
}

You can create a protocol with a configure function and associated type Cell. Using protocol extensions, you can add default implementations for different cell types, and additional methods.
protocol CellConfigurable {
associatedType Cell
func configure(_ cell: Cell)
}
extension CellConfigurable where Cell == SomeTableViewCell {
func configure(_ cell: SomeTableViewCell) {
...
}
}

try this code to create CustomCell:-
class CustomCell: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.initViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.perform(#selector(self.initViews), with: self, afterDelay: 0)
}
//MARK: Init views
func initViews() {
//Add your code
}
//MARK: Layout subviews
override func layoutSubviews() {
super.layoutSubviews()
// Here you can code for layout subviews
}
//MARK: Update all valuesw model
func updateWithModel(_ model: AnyObject) {
//here you can update values for cell
}
}
//Call CustomCell in Tableview class
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomCell
cell.updateWithModel(items[indexPath.row])
return cell
}

Related

Creating tableView inside tableView

I've created a tableView with prototype cells. Inside each of these prototype cells is another tableView with different prototype cells. I've linked this all together fine, but I'm having trouble modifying the innermost prototype cells. Here is why.
Here is the relevant code:
class ViewController: UIViewController, AVAudioRecorderDelegate, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "outerCell") as! outerCell
//would obviously make some modification to cell here, like cell.title = "test" or something
let cell2 = cell.commentTableView.dequeueReusableCell(withIdentifier: "innerCell") as! innerCell
cell2.commentText.text = "sus"
//NEED TO DIFFERENTIATE HERE ON HOW TO KNOW WHICH CELL TO RETURN
//e.g. NEED TO RETURN either cell1 or cell2, depending on the tableView
}
My code for outerCell looks like this:
import UIKit
class outerCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var commentTableView: UITableView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
commentTableView.delegate = self
commentTableView.dataSource = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "innerCell", for: indexPath) as! commentCell
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
}
See, the main problem is, both these table views work fine and all, but, in the first chunk of code, if I just do something like,
if tableView == self.tableView{
return cell }
else ...
this won't work, as tableView always seems to be self.tableView.
How can I modify my code so that I can actually impact the text displayed in the inner cell, and the outer cell, in the same block of code?
Also, please note, I know that, based on the example given here, there is no need for these nested cells. I've just simplified the code here to focus on what's important - my actual code has a lot of stuff happening in both the inner and outer cell.
Thank you, any help would be appreciated.
you need to first create two different cell classes.
In outer class :
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SearchPreferredJobTableViewCell
cell.responseCreateBookingObj = { [unowned self] (returnObject) in
DispatchQueue.main.async {
tableView.beginUpdates()
}
// do your logic
DispatchQueue.main.async {
cell.contentView.layoutIfNeeded()
tableView.endUpdates()
} }
return cell
}
// other cell class
Declare variable
var responseCreateBookingObj : APIServiceSuccessCallback?
// send callback from you want to send
guard let callBack = self.responseCreateBookingObj else{
return
}
callBack(true as AnyObject)
// also do in when user scroll it'll manage
tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath){
DispatchQueue.main.async {
tableView.beginUpdates()
}
// do your logic
DispatchQueue.main.async {
cell.contentView.layoutIfNeeded()
tableView.endUpdates()
}
}

Why does UITableView cache cells when reloading data?

If you were to call tableView.reloadData() on a table with custom UITableViewCell, then it will cache those cells when you reload data. (It is discussed here: https://github.com/nicklockwood/FXForms/issues/92)
If you continue reloading data, after a while, there could be 30/40 cells that are cached and retained. My question is: why is that?
In particular:
1) What possible usage could there be to retain cells that have since been lost after reloading the table?
2) What could be done to prevent/reduce the memory cache storing the cells?
3) Most importantly, why does this happen? Why would cells be retained when reloadData() is called?
Example
To give you a basic example in code, click on one of the cells, then, when you see the objects in memory, there are more retained cells:
ViewController.Swift
import UIKit
class ViewController: UIViewController {
let tableViewController = QuizTableViewController(style: .grouped)
override func viewDidLoad() {
super.viewDidLoad()
self.setupTableViewController()
}
private func setupTableViewController() {
self.view.addSubview(tableViewController.view)
let topAnchor = self.tableViewController.view.topAnchor.constraint(equalTo: self.view.topAnchor)
let leadingAnchor = self.tableViewController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor)
let trailingAnchor = self.tableViewController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
let bottomAnchor = self.tableViewController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
NSLayoutConstraint.activate([topAnchor, leadingAnchor, trailingAnchor, bottomAnchor])
}
}
QuizTableViewController.Swift
import UIKit
class QuizTableViewController: UITableViewController {
override init(style: UITableViewStyle) {
super.init(style: style)
self.view.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = QuizCell(subText: "HELLO WORLD")
cell.textLabel?.text = "THIS IS WHAT IS DISPLAYED"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.reloadData()
}
}
QuizCell.Swift
import UIKit
class QuizCell: UITableViewCell {
weak var subText: UILabel?
init(subText: String) {
self.subText = UILabel()
self.subText?.text = subText
super.init(style: .default, reuseIdentifier: "QuizCell")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("This is called after the cache is full.")
}
}
The problem is that you are not using the proper approach to creating table cells. You are explicitly creating a new instance of QuizCell in the cellForRowAt. What you should be doing is dequeueing a reusable cell using dequeueReusableCell. Then your issue goes away because the table will only keep the minimum number of cells needed.
Please find any one of the countless tutorials on using table views for proper examples.
Like #rmaddy mentioned, the problem is how you are dequeuing the cell in cellForRowAt function. Instead, change it to something similar to:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "QuizCell", for: indexPath) as! QuizCell
cell.textLabel?.text = "THIS IS WHAT IS DISPLAYED"
return cell
}

Dynamic Table View within Static Table View Cell

I'm fairly new into Swift programming and right now I'm implementing a dynamic table view within a static table view's cell. I know there are plenty of solutions on stackoverflow already but I realised that most of them are in Obj-C which I'm not very familiar with it yet.
Basically, I have a TableView that is set as dynamic in one of the cell of a static table view which is part of the main table view controller. The problem I am having now is there doesn't seem to be a way to implement the data source functions without declaring them for the static table view. I have declared an #IBOutlet for the dynamic table (let's call it dynamicTableView in this scenario).
I have managed to get the override func numberOfSections(in tableView: UITableView) working by returning 1 if the tableView is not dynamicTableView as in the following code:
override func numberOfSections(in tableView: UITableView) -> Int {
if tableView == dynamicTableView {
return data.count
}
else {
return 1
}
}
However, the problem I am having now is implementing the override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath). I have no idea what to be returned if the tableView parameter is not dynamicTableView, but for the static table view.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == dynamicTableView {
let cell = tableView.dequeueReusableCell(withIdentifier: "dynamic", for: indexPath) as! dynamicTableViewCell
cell.update(data[indexPath.row]) // A helper function declared in the dynamicTableViewCell.swift
return cell
}
else {
// What to return here?
}
}
Thanks!
Edit: What I meant was I can't seem to have a cellForRowAt data source function that does not affect my static table view.
If there is a value in numberForRows then you have to retutn a cell like this
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == bloggerReviewTableView {
let cell = tableView.dequeueReusableCell(withIdentifier: "dynamic", for: indexPath) as! dynamicTableViewCell
cell.update(data[indexPath.row]) // A helper function declared in the dynamicTableViewCell.swift
return cell
}
else {
// What to return here?
let cell = tableView.dequeueReusableCell(withIdentifier: "other", for: indexPath) as! OtherTableCell
return cell
}
}
//
but if the return is zero then there is no need for the if statement inside cellForRowAt as it won't be called for the other table
If the static tableview cells are fairly distinct, they can be individually subclassed.
The dynamic tableview/collectionview can be added in required subclass of static tableview cell.
//class for static tableview
let reviewCellId = "reviewCell"
class StaticTableClass: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
//register static cell classes
tableView.register(ReviewCell.self, forCellReuseIdentifier: reviewCellId)
//..
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reviewCellId, for: indexPath)
return cell
}
}
Create a separate ReviewCell class which will contain the dynamic UITableView like so.
This way one class will handle methods of only one tableview.
class ReviewCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource {
lazy var dynamicTableView: UITableView = {
let tv = UITableView()
tv.delegate = self
tv.dataSource = self
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
addSubview(dynamicTableView)
dynamicTableView.register(UITableViewCell.self, forCellReuseIdentifier: "dynamicCellId")
}
// add further tableview methods in here
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
}
}

How to go to another View Controller when an element inside table view is being tapped?

I have a UIView inside a TableViewCell,when users tapped in this UIView,it go to another view controller and pass a data along.
So inside TableViewCellClass I done the following :
I set up a protocol and detected the "Tap" gesture,when user table the UIView.This part is working fine:
protocol MyDelegate : class {
func runThisFunction(myString : String)
}
class MyTableViewCell: UITableViewCell {
weak var delegate : MyDelegate?
...other code here
//here detect the tap
let tap = UITapGestureRecognizer(target: self, action: #selector(hereTapped))
self.myUIElementInThisCell.addGestureRecognizer(tap)
}
#objc func hereTapped(sender: UITapGestureRecognizer? = nil){
self.delegate?.runThisFunction(myString: "I am a man")
}
So in my view controller which contain this TableView,I done the following :
I extend out the MyDelegate as subclass,and then attach the protocol function inside it as below
class MyViewController: UIViewController,MyDelagate{
func runThisFunction(myString : String) {
print("Tapped in view controller")
self.performSegue(withIdentifier: "MySegue",sender : self)
}
override func viewDidLoad() {
super.viewDidLoad()
let tableViewCell = MyTableViewCell()
tableViewCell.delegate = self
}
}
Result:
After done all the stuff above,when I tapped the UIView,it didnt perform the segue as stated in MyViewControllerClass,even the print() command also didnt execute.
So what I missing out? Please give me a solution.Thanks
The problem is that the delegate for MyTableViewCell instances is not defined.
When you do:
override func viewDidLoad() {
super.viewDidLoad()
let tableViewCell = MyTableViewCell()
tableViewCell.delegate = self
}
You are setting a delegate for an object that will be destroyed just when the method viewDidLoad() finishes.
Solution 1
In order to avoid this, you have to set the delegate inside the cellForRow method.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! MyTableViewCell
cell.delegate = self
// Other configurations
return cell
}
Solution 2
You can also use the UITableViewDelegate methods in order to capture the user interaction with the cells.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
self.performSegue(withIdentifier: "MySegue",sender : self)
}
This way you avoid all the MyDelegate protocol thing. This would be my preferred option.
The problem is that these 2 lines:
let tableViewCell = MyTableViewCell()
tableViewCell.delegate = self
are not related to the shown cells in the table , it's a cell created on the fly so
set delegate in cellForRow for the cell that you will actually waiting a delegate trigger from them
cell.delegate = self
like this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = areaSettTable.dequeueReusableCell(withIdentifier:cellID) as! MyTableViewCell
cell.delegate = self
}
Problem is in your viewDidLoad:
// you create a new cell
let tableViewCell = MyTableViewCell()
// set its delegate
tableViewCell.delegate = self
// and then the cell is not used for anything else
Basically you are not setting the delegate for the cells that are being presented, but for another instance that you create in viewDidLoad.
You have to set a delegate in cellForRowAt to make sure the proper cells get the delegate set:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "yourIdentifier", for: indexPath) as! MyTableViewCell
cell.delegate = self
return cell
}
This will set the delegate for those cells, that are presented.
Alternatively, I would recommend using didSelectRowAt from UITableViewDelegate (if your MyViewController implements it):
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 { // only if it is the first cell (I assume that MyTableViewCell is first)
runThisFunction(myString: "I am a man")
}
}
Delegation in not very good here, indeed if you want to change object that didn't passed to the cell explictitly.
My advice is to use closure:
typealias CellTapHandler = (String)->()
class CustomCell: UITableViewCell {
var handler: CellTapHandler?
#objc func hereTapped(sender: UITapGestureRecognizer? = nil) {
handler?("String or whatever you want to get back from cell.")
}
//...
}
and set up it from view controller
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "yourIdentifier", for: indexPath)
(cell as? MyTableViewCell).handler = { [weak self] string in }
return cell
}
PS: As I said, usually you want to pass object related to cell further. It's difficult to do with delegation, as you had to pass to cell some additional token, to determine object by the cell, o pass object itself breaking Model-View separation paradigm. But it can be done easily with closures:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = self.myData[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "yourIdentifier", for: indexPath)
(cell as? MyTableViewCell).handler = { [weak self] string in
self.performSegue(withIdentifier: "MySegue",sender : object)
}
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (let myObj = sender as? MyObjType, let subsequentVC = segue.destination as? NextViewController) {
subsequentVC.selectedMyObject = myObj
}
}

Swift remove data stored in UITableViewController from UITableViewCell

I have a custom class that extends UIViewController and contains a table view, with an array that stores data that populates the table view. For my custom cells, I have a button that when pressed, should remove that cell from the table view. However, I haven't been able to find a way to remove the data from the array in the table view controller and reload the data from the cell's class. Some of the answers I've seen on similar posts suggest notifications or delegation, but due to the structure of my app (tab controller) and that I already use notifications for another feature, the former is inefficient, and I don't know how to use the latter in this situation.
This is the code for the custom cell class:
class CustomCell: UITableViewCell {
#IBOutlet var label: UILabel!
#IBOutlet var removeButton: UIButton!
#IBAction func remove(sender: AnyObject) {
// don't know what to put here
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
And here is the table view controller class:
class CustomController: UIViewController, UITableViewDataSource {
#IBOutlet var table: UITableView!
var data: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
table.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: CustomCell = table.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
// put content in
return cell
}
}
The array data is what I want to access from remove in CustomCell. The closest I've gotten is using self.superview.superview.superview in CustomCell, which returns the view that CustomController controls. However, I have no way to get an instance of CustomController without instantiating a new one. How do I modify a variable in CustomController from the CustomCell class?
In your Custom Cell add this
class CustomCell: UITableViewCell {
var customControllerReference: CustomController?
}
And in your Custom Controller in cellForRowAtIndexPath add this
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: CustomCell = table.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
cell.customControllerReference = self
return cell
}

Resources