Simplifying cellForRowAt in TableView - ios

I am writing a personal iOS app to keep track of some things. my app is working well but i am turning my attention to tidying up the code and cleaning things up. in my tableview, one of the cells is uicollectionview that depending on which collectionviewcell I select, a custom tableviewcell is loaded in the same table. At this time I have about a dozen items in my collectionview that i can select from and in turn one of about a dozen different tableviewcells to load. each cell collects different bits of info.
everything is working as i expect it but i don't like the fact that throughout this tableviewcontroller, i have many repetitive sections based on all the tableviewcells i have to handle
override func viewDidLoad() {
super.viewDidLoad()
// register the various tablecells
tableView.register(UINib(nibName: "eventO2TableViewCell", bundle: nil), forCellReuseIdentifier: "eventO2TableViewCell")
...
tableView.register(UINib(nibName: "eventTmpTableViewCell", bundle: nil), forCellReuseIdentifier: "eventTmpTableViewCell")
tableView.register(UINib(nibName: "eventDXTableViewCell", bundle: nil), forCellReuseIdentifier: "eventDXTableViewCell")
similarly cellForRowAt is very big (i.e a switch statement, a dozen cases , each with a corresponding
switch selectedIndexPath.row { // the index of the uicollectionviewcell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "eventTmpTableViewCell", for: indexPath) as! eventTmpTableViewCell
return cell
...
case 11:
let cell = tableView.dequeueReusableCell(withIdentifier: "eventO2TableViewCell", for: indexPath) as! eventO2TableViewCell
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "eventDXTableViewCell", for: indexPath) as! eventDXTableViewCell
return cell
}
and in
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
another switch statement with a dozen case evaluation to figure out which cell was used and pull out the information i need to save.
was contemplating the idea that was raised in this similar question Is it possible to store custom UITableViewCell into Array? but curious if there are other suggestions ? still consider myself a beginner in this space. thanks

I created extensions for register and deque in UITableView and identifier in UITableViewCell, here is some sample code:
UITableViewExtension.swift
public extension UITableView {
func register<CellClass: UITableViewCell>(_ cellClass: CellClass.Type) {
register(cellClass, forCellReuseIdentifier: cellClass.identifier)
}
func dequeue<CellClass: UITableViewCell>(
_ cellClass: CellClass.Type,
for indexPath: IndexPath,
setup: ((CellClass) -> Void)? = nil) -> UITableViewCell {
let cell = dequeueReusableCell(withIdentifier: cellClass.identifier, for: indexPath)
if let cell = cell as? CellClass {
setup?(cell)
}
return cell
}
}
UITableViewCell.swift
extension UITableViewCell {
static var identifier: String {
return String(describing: self)
}
}
Then in ViewController.swift
class Cell1: UITableViewCell {
// ...
}
class Cell2: UITableViewCell {
// ...
}
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
let cells = [Cell1.self, Cell2.self]
tableView.register(cells)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeue(Cell1.self, for: indexPath) { cell in
// customise cell
}
}
}
Note: As you have asked I'm storing UITableViewCells into cells array and get my job done. Even though there are minor changes in our implementations, I hope this will help.

You can create a protocol DequeueInitializable and write its extension like this
import Foundation
import UIKit
protocol DequeueInitializable {
static var reuseableIdentifier: String { get }
}
extension DequeueInitializable where Self: UITableViewCell {
static var reuseableIdentifier: String {
return String(describing: Self.self)
}
static func dequeue(tableView: UITableView) -> Self {
guard let cell = tableView.dequeueReusableCell(withIdentifier: self.reuseableIdentifier) else {
return UITableViewCell() as! Self
}
return cell as! Self
}
static func register(tableView: UITableView) {
let cell = UINib(nibName: self.reuseableIdentifier, bundle: nil)
tableView.register(cell, forCellReuseIdentifier: self.reuseableIdentifier)
}
}
Then in you cell class you confirm to that protocol
class Cell1: UITableViewCell, DequeueInitializable { }
class Cell2: UITableViewCell, DequeueInitializable { }
Now you can register and dequeue cell easily
return Cell1.dequeue(tableView: tableView)
to register
Cell1.register(tableView: tableView)

Related

How do I use tableView.indexPathForRow(at: touchPoint) with sections

I use sections to load messages(viewForFooterInSection) and rows to load the reply of specific messages if any.
Previously I was using a long press gesture on the tableView to detect a touch on the tableView and return the indexPath using tableView.indexPathForRow(at: touchPoint), however I have not found a similar method to get indexPath of long pressed cell
Can anyone help?
I am not sure why you are going for cell-level gesture when you have already achieved getting indexPath using gesture on tableview. In case you are trying to get cell from indexPath then you can try like
guard let cell = tableView.cellForRow(at: indexPath) else { return }
Anyhow coming to answer for your question, we can do the following way to get indexPath from cell-level.
protocol CustomCellDelegate: AnyObject {
func longPressAction(onCell: CustomCell)
}
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
let lg = UILongPressGestureRecognizer(target: self, action: #selector(longPress))
lg.minimumPressDuration = 0.5
lg.delaysTouchesBegan = true
self.addGestureRecognizer(lg)
}
#objc func longPress(gestureReconizer: UILongPressGestureRecognizer) {
if gestureReconizer.state != UIGestureRecognizer.State.ended {
return
}
delegate?.longPressAction(onCell: self)
}
}
And in your tableview cell for row method, assign the delegate.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as? CustomCell else { return UITableViewCell() }
cell.delegate = self
return cell
}
And confirm to the CustomCellDelegate protocol in your viewController.
extension ViewController: CustomCellDelegate {
func longPressAction(onCell: CustomCell) {
guard let indexPath = tableView.indexPath(for: onCell) else { return }
print(indexPath.section, indexPath.row)
}
}

How can I add sections to my UITableViewCell?

How can I add sections to my UITableViewCell with the Bond framework?
self.viewModel.items.bind(to: self.tableView) { (item, indexPath, tableView) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ListCell.self),
for: indexPath) as! ListCell
cell.item = item[indexPath.row]
return cell
}.dispose(in: self.bag)
Regarding the source code,
if you want to override just a title, you should override this class and implement correspond logic for
open class TableViewBinderDataSource<Changeset: SectionedDataSourceChangeset>: NSObject, UITableViewDataSource
but if you want to implement totally custom view, this is much complicated. I don't think that this is possible for this library. the reason is that you should override UITableViewDelegate, but it is used in public protocol ReactiveExtensions that cannot be overridden.
You must write this class
class CustomSection<Changeset: SectionedDataSourceChangeset>: TableViewBinderDataSource<Changeset>, UITableViewDelegate where Changeset.Collection == Array2D <String, ListItemViewModel> {
#objc func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return changeset?.collection[sectionAt: section].metadata
}
and in viewDidload of your ViewController you must call this function.
private func setupViewModel() {
let sectionBindingDatSource: CustomSection = CustomSection<TreeChangeset>{ (changeset, indexPath, tableView) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ListCell.self), for: indexPath) as! ListCell
cell.item = changeset.sections[indexPath.section].items[indexPath.row]
return cell
}
self.viewModel.sections.bind(to: self.tableView, using: sectionBindingDatSource)
}
and if you want to override function of TableViewDataSourse and customize section you must set delegate
self.tableView.delegate = sectionBindingDatSource

How to create a various type of cells in a single tableView?

I'm currently developing an app which has features like half Uber, half Tinder.
I would like to create a TableView for editing profile to make it look like a view of Tinder's profile editing view.
I successfully made a first cell for picking up profile image and second cell for textLabel.
I want to make a third cell which has an editable TextView(not textField so users can enter several lines of words) But the codes aren't working as I intended.
It seems like although I made a textView in my Custom Cell Class and set it to my third tableView cell in my tableViewController class, it doesn't appear on my simulator.
I'm still not quite sure what's the good way to make several types of cell for a single tableView so if there's other better way to do it, I would love to know.
My codes related to this issue is below.
ViewController
//EditProfileViewController For The Profile Edit view
class EditProfileViewController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
TableViewController
//MyTableViewController for the EditProfileViewController
class EditProfileTableViewController: UITableViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UITextViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 5
} else if section == 1 {
return 2
} else {
return 4
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//link my custom Cell here
if let cell = tableView.dequeueReusableCell(withIdentifier: "MyCustomCell", for: indexPath) as? MyCellTableViewCell {
if indexPath.section == 0 {
if indexPath.row == 0 {
//this row is for an Image Picker
cell.textLabel?.text = "edit profile image"
} else if indexPath.row == 1 {
//this row is just like a title of third cell which is editable self description
cell.textLabel?.text = "Describe yourself"
cell.isUserInteractionEnabled = false
} else if indexPath.row == 2 {
//this row is an editable self description
cell.addSubview(cell.selfDescritionTextView)
cell.selfDescritionTextView.delegate = self
cell.selfDescritionTextView.isEditable = true
cell.selfDescritionTextView.text = "Enter some descriptions here"
}
}
else if indexPath.section == 1 {
cell.textLabel?.text = "section 1"
}
else if indexPath.section == 2 {
cell.textLabel?.text = "section 2"
}
return cell
} else {
//Just in case
return UITableViewCell()
}
}
}
TableViewCell
//MyCellTableViewCell
class MyCellTableViewCell: UITableViewCell {
//create a textView for self description
var selfDescritionTextView = UITextView()
//init
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: "MyCustomCell")
selfDescritionTextView = UITextView(frame: self.frame)
self.addSubview(selfDescritionTextView)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
If you need to know more codes than what I showed here, Please let me know.
I'm not obsessed with this method to create my edit profile View so if anyone knows other ways better than this, I would LOVE to know as well.
Thanks
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.register(UINib(nibName: TableViewCell1.NibName, bundle: nil), forCellReuseIdentifier: TableViewCell1.Identifier)
tableView.register(UINib(nibName: TableViewCell2.NibName, bundle: nil), forCellReuseIdentifier: TableViewCell2.Identifier)
tableView.register(UINib(nibName: TableViewCell3.NibName, bundle: nil), forCellReuseIdentifier: TableViewCell3.Identifier)
}
You'll have 3 Custom Table View Cells, with its subviews, respectively:
class MyCellTableViewCell1: UITableViewCell {
static var NibName: String = String(describing: TableViewCell1.self)
static var Identifier: String = "TableViewCell1"
func configure() { ... }
}
Then:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
// link my custom Cell here.
if indexPath.row == 0, let dequeuedCell = tableView.dequeueReusableCell(withIdentifier: TableViewCell1.Identifier, for: indexPath) as? TableViewCell1 {
dequeuedCell.configure()
cell = dequeuedCell
}
else if indexPath.row == 1, let dequeuedCell = tableView.dequeueReusableCell(withIdentifier: TableViewCell2.Identifier, for: indexPath) as? TableViewCell2 {
dequeuedCell.configure()
cell = dequeuedCell
}
else if indexPath.row == 2, let dequeuedCell = tableView.dequeueReusableCell(withIdentifier: TableViewCell3.Identifier, for: indexPath) as? TableViewCell3 {
dequeuedCell.configure()
cell = dequeuedCell
}
else {
cell = UITableViewCell()
}
return cell
}
Register multiple nib files like this , but it in viewDidLoad
tableView.register(UINib(nibName: "TableViewCell1", bundle: nil), forCellReuseIdentifier: CellIdentifier1)
tableView.register(UINib(nibName: "TableViewCell2", bundle: nil), forCellReuseIdentifier: CellIdentifier2)
tableView.register(UINib(nibName: "TableViewCell3", bundle: nil), forCellReuseIdentifier: CellIdentifier3)

Break error while creating a calendar

Have break error Thread 1: EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode==0x0). No errors with build, just when run, have a break
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let calendars = self.calendars {
return calendars.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
//error happens here
if self.calendars != nil {
let calendarName = self.calendars?[(indexPath as NSIndexPath).row].title
cell.textLabel?.text = calendarName
} else {
cell.textLabel?.text = "Unknown Calendar Name"
}
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! UINavigationController
let addCalendarVC = destinationVC.viewControllers[0] as! AddCalendarViewController
addCalendarVC.delegate = self
}
func calendarDidAdd() {
self.loadCalendars()
self.refreshTableView()
}
}
tableView.dequeueReusableCell(withIdentifier: "Cell")!
You are unwrapping an optional value which might be nil in the first place. Cell might not have been created yet especially if you haven't registered the cell's class with that identifier so it'll crash first time table tries to populate the cell. You should first check if cell is nil:
var cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "Cell")
}
...
The immediate red flag I see is here:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
When dequeueing reusable cells, I like to wrap them in guard statements, so my app doesn't crash. It also tells me a bit more information when something does go wrong:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else {
print("couldn't dequeue a reusable cell for identifier Cell in \(#function)")
return UITableViewCell()
}
This crash could be for a few reasons. You may have forgotten to register the reuse identifier, but if you're using storyboards this is handled for you. There may simply be a typo or you forgot to enter a reuse identifier for that cell.

How to update DetailView

I have a swift app based on Master-Detail template. Every row in MasterView table is based on custom cell received from a nib. Every cell includes UIlabel and UIbutton. The logic of the app is following. If user taps on a row DetailView shows some details depending on selected row. The button on the row does not call tableView(_, didSelectRowAtIndexPath). If user taps on the button inside a row only an image belongs to DetailView should be changed (other elements on DetailView remain the same) but it isn't. If I select another row and than select previous row back, changed image is shown on the DetailView as it was foreseen. The question is how to redraw the image in the DetailView just by tapping on the button.
I've tried to do following but with no success:
class MasterViewCell: UITableViewCell {
weak var detailViewController: DetailViewController?
#IBAction func buttonTap(sender: AnyObject) {
//method to set new image
detailViewController!.setNewImage()
detailViewController!.view.setNeedsDisplay()
}
}
class MasterViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "itemCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "Cell")
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? MasterViewCell
cell?.detailView = self.detailViewController
return cell!
}
You need to use a handler
typealias ButtonHandler = (Cell) -> Void
class Cell: UITableViewCell {
var changeImage: ButtonHandler?
func configureButton(changeImage: ButtonHandler?) {
self.changeImage = changeImage
}
#IBAction func buttonTap(sender: UIButton) {
changeImage?(self)
}
}
And in your MasterView
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! Cell
cell.configureButton(setNewImage())
return cell
}
private func setNewImage() -> ButtonHandler {
return { [unowned self] cell in
let row = self.tableView.indexPathForCell(cell)?.row //Get the row that was touched
//set the new Image
}
}
SOURCE: iOS Swift, Update UITableView custom cell label outside of tableview CellForRow using tag
I've found the solution. I've used protocol-delegate mechanism. Now the code is:
//protocol declaration:
protocol MasterViewCellDelegate: class {
func updateImage(sender: MasterViewCell, detVC: DetailViewController)
}
// cell class
class MasterViewCell: UITableViewCell {
weak var masterViewCellDelegate: MasterViewCellDelegate? // protocol property
weak var masterViewController: MasterViewController? {
didSet {
// set delegate
self.masterViewDelegate = masterViewController!.detailViewController
}
}
#IBAction func buttonTap(sender: AnyObject) {
var detVC: DetailViewController?
if let split = masterViewController!.splitViewController {
let controllers = split.viewControllers
detVC = (controllers[controllers.count - 1] as! UINavigationController).topViewController as? DetailViewController
}
// call delegate
masterViewCellDelegate?.updateImage(self, detVC: detVC)
}
class MasterViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "itemCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "Cell")
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? MasterViewCell
cell?.masterViewController = self
return cell!
}
// declare detailviewcontroller as delegate
class DetailViewController: UIViewController, MasterViewCellDelegate {
func updateImage(sender: MasterViewCell, detVC: DetailViewController){
detVC.setNewImage()
}
}
It may well be that this solution is excessively complex, but it works and easy could be adapted for various purposes.

Resources