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
Related
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)
I created a custom class named myCustomCell and it inherits UITableViewCell. I declared the two variables in the class. It gave me crashing when the tableview is called. so, I added "guard" in tableview(_:cellForRowAT). I don't see any error but I don't see anything in cell. Would anyone help me with it?
import UIKit
class tableviewtest: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell:myCustomCell = tableview.dequeueReusableCell(withIdentifier: cellIdentifier) as? myCustomCell else {
return UITableViewCell()
}
if searching {
cell.myLableCell1?.text = searchArr[indexPath.row]
targetStringItem = searchArr[indexPath.item]
if let indexForSelectedEng = eng.firstIndex(of: targetStringItem) {
showTheIndex = indexForSelectedEng
cell.myLableCell2?.text = String(showTheIndex)
}
}
else {
cell.myLableCell1?.text = copiedArray[indexPath.row]
targetStringItem = copiedArray[indexPath.item]
if let indexForSelectedEng = eng.firstIndex(of: targetStringItem) {
showTheIndex = indexForSelectedEng
cell.myLableCell2?.text = String(showTheIndex)
}
}
return cell
}
class myCustomCell:UITableViewCell {
#IBOutlet weak var myLableCell1: UILabel!
#IBOutlet weak var myLableCell2: UILabel!
}
It's a classic design mistake and reveals the (practical) uselessness of the conditional downcast.
The class of the custom cell in Interface Builder must be set to myCustomCell and the Identifier of the cell must be set to the proper value, too. Then you can remove the guard expression.
And please name classes and structs with starting uppercase letter.
Side note:
There is a lot of redundant code, the method can be reduced to
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableview.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! myCustomCell
targetStringItem = searching ? searchArr[indexPath.row] : copiedArray[indexPath.row]
cell.myLableCell1?.text = targetStringItem
if let indexForSelectedEng = eng.firstIndex(of: targetStringItem) {
showTheIndex = indexForSelectedEng
cell.myLableCell2?.text = String(showTheIndex)
}
return cell
}
I have a UITableView, Each UITableViewCell has its own Async task (Webservice call) , the cell should be notified when its task finishes in order to update the labels, the task is called every 30seconds. I don't want to realod the whole UITableView everytime.
Here is what I have done so far:
class ViewModel {
var name, result: String
var url: String
init () {
let timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true, block: self.startUpdating())
}
func startUpdating() {
let dispatchQueue = DispatchQueue(label: "startUpdating", qos:.utility)
dispatchQueue.async{
self.callWebservice()
// how can i notify my cell about the new changes
}
}
func callWebservice(){
//call web service and update name and result
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let vm = viewModels[indexPath.row]
cell.textLabel = vm.name
cell.detailTextLabel = vm.result
return cell
}
You can update one particular cell instead of reloading the whole TableView.
yourTableView.beginUpdates()
yourTableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
yourTableView.endUpdates()
A smart solution is Key Value Observing.
The model must be a subclass of NSObject
The observed properties must be marked as #objc dynamic
class ViewModel : NSObject {
#objc dynamic var name, result: String
...
In Interface Builder set the style of the cell to custom, add two labels and set AutoLayout constraints if needed
Create a new UITableViewCell subclass, name it ResultCell and add the two labels and the two observation properties
class ResultCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var resultLabel: UILabel!
var nameObservation : NSKeyValueObservation?
var resultObservation : NSKeyValueObservation?
}
In Interface Builder set the class of the custom cell to ResultCell and connect the labels to the custom cell
In the controller in cellForRow add the observers
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ResultCell
let vm = viewModels[indexPath.row]
cell.nameLabel!.text = vm.name
cell.resultLabel!.text = vm.result
cell.nameObservation = vm.observe(\.name, changeHandler: { (model, _) in
cell.nameLabel!.text = model.name
})
cell.resultObservation = vm.observe(\.result, changeHandler: { (model, _) in
cell.resultLabel!.text = model.result
})
return cell
}
And you have to remove the observers in didEndDisplaying
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let resultCell = cell as! ResultCell
resultCell.nameObservation = nil
resultCell.resultObservation = nil
}
The benefit is that any change of name and result in the model will update the cell when it's on screen.
I want to reuse a UITableViewCell in my app, but I get this error: Unexpectedly found nil while implicitly unwrapping an Optional value.
I find that this is because the UI things in UITableViewCell is nil, so my app crashed.
My UITableViewCell code is like this:
class WordListCell: UITableViewCell {
#IBOutlet weak var wordListCoverImage: UIImageView!
#IBOutlet weak var wordListName: UILabel!
#IBOutlet weak var wordListInfo: UILabel!
var wordList: WordList? {
didSet {
updateUI()
}
}
private func updateUI() {
wordListName.text = wordList?.name
wordListInfo.text = wordList?.description
wordListCoverImage = UIImage()
}
}
I create it in the storyboard and link the outlet to the code in the other TableView.
But this time, I want to reuse the cell in a new TableView which is all created by code, so I don't know how to initialize the UI things.
The new UITableView code is like this:
tableView.delegate = self
tableView.dataSource = self
tableView.register(WordListCell.self, forCellReuseIdentifier: "wordListCell")
//the delegate
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return wordLists.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let wordList = wordLists[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "wordListCell", for: indexPath)
if let wordListCell = cell as? WordListCell {
wordListCell.wordList = wordList
}
return cell
}
Please tell me how to reuse the cell.Thanks!
Okay so I think what you are doing wrong is when you create a custom tableView cell, you are not assigning a UIImage. So instead try doing this wordListCoverImage = UIImage(named: wordList.imageName).
Now also in your tableView class inside viewDidLoad() apart from adding
tableView.delegate = self
tableView.dataSource = self
tableView.register(WordListCell.self, forCellReuseIdentifier: "wordListCell")
Then at let cell = tableView.dequeueReusableCell(withIdentifier: "wordListCell", for: indexPath) downcast it as a custom cell class like so.
let cell = tableView.dequeueReusableCell(withIdentifier: "wordListCell", for: indexPath) as! WordListCell
And then finaly under that set the cell.delegate = self
I hope this helps!
I have a tableview with one textfield in each cell. I added a target like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customLevelCell") as! LevelTableViewCell
cell.cellTextField.addTarget(self, action: #selector(ViewController.TextfieldEditAction), for: .editingDidEnd)
return cell
}
But found out that I'm not able to use the indexpath.row / sender.tag to get the specific textfield text
#objc func TextfieldEditAction(sender: UIButton) {
}
So my question is how can I get the text after the user has edited one of the textfields.
Also how can i get the indexpath.row or sender.tag which will be used to collect the text they added to that specific textfield.
The easiest way to handle this is probably to use a delegate protocol…
In your cell
protocol LevelTableViewCellDelegate: class {
func levelTableViewCell(_ levelTableViewCell: LevelTableViewCell, didEndEditingWithText: String?)
}
class LevelTableViewCell: UITableViewCell {
#IBOutlet private weak var cellTextField: UITextField!
var delegate: LevelTableViewCellDelegate?
override func awakeFromNib() {
cellTextField.addTarget(self, action: #selector(didEndEditing(_:)), for: .editingDidEnd)
}
#objc func didEndEditing(_ sender: UITextField) {
delegate?.levelTableViewCell(self, didEndEditingWithText: sender.text)
}
}
In your view controller
class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LevelTableViewCell") as! LevelTableViewCell
cell.delegate = self
return cell
}
}
extension TableViewController: LevelTableViewCellDelegate {
func levelTableViewCell(_ levelTableViewCell: LevelTableViewCell, didEndEditingWithText: String?) {
let indexPath = tableView.indexPath(for: levelTableViewCell)
// Now you have the cell, indexPath AND the string
}
Also, note that the view outlet is be private. You'll find that you write cleaner code if you follow this rule
Following is the extension of UIView that can be used to get the cell or indexPath of the cell enclosing textField
extension UIView {
var tableViewCell : UITableViewCell? {
var subviewClass = self
while !(subviewClass is UITableViewCell){
guard let view = subviewClass.superview else { return nil }
subviewClass = view
}
return subviewClass as? UITableViewCell
}
func tableViewIndexPath(_ tableView: UITableView) -> IndexPath? {
if let cell = self.tableViewCell {
return tableView.indexPath(for: cell)
}
return nil
}
}
Example :-
#objc func TextfieldEditAction(sender: UITextField) {
//replace tableView with the name of your tableView
guard let indexPath = sender.tableViewIndexPath(tableView) else {return}
}