Adding Cells to Dynamic UITableView DataSource | Swift - swift

I have a dynamic UITableViewDataSource that takes in a Model and a UITableViewCell
I would like to insert a different UITableViewCell into the 'nth' (10th for example) row... (for Google Ads)
DATASOURCE
class ModelTableViewDataSource<Model, Cell: UITableViewCell>: NSObject, UITableViewDataSource {
typealias CellConfigurator = (Model, Cell) -> Void
var models: [Model]
private let reuseIdentifier: String
private let cellConfigurator: CellConfigurator
init(models: [Model], reuseIdentifier: String, cellConfigurator: #escaping CellConfigurator) {
self.models = models
self.reuseIdentifier = reuseIdentifier
self.cellConfigurator = cellConfigurator
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! Cell
cellConfigurator(model, cell)
return cell
}
}
I then add an extension for each Model
MODEL
extension ModelTableViewDataSource where Model == SomeModel, Cell == SomeCell {
static func make(for someModel: [SomeModel], reuseIdentifier: String = "Identifier") -> ModelTableViewDataSource {
return ModelTableViewDataSource(models: someModel, reuseIdentifier: reuseIdentifier) { (someModel, someCell) in
someCell.model = someModel
}
}
}
What is the best way to implement this keeping the re-usable functionality of the UITableViewDataSource

I thought of this:
create a subclass
override numberOfRowsInSection to return number of rows + 1
override cellForRowAt to handle the 10th row
class ModelTableViewDataSourceWithAd: ModelTableViewDataSource {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return super.tableView(tableView, numberOfRowsInSection:section) + 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.row == 9) {
// return Google ad cell
} else {
return super.tableView(tableView, cellForRowAt:indexPath)
}
}
}
and then the creator:
extension ModelTableViewDataSource where Model == SomeModel, Cell == SomeCell {
// ...
static func makeWithAd(for someModel: [SomeModel], reuseIdentifier: String = "Identifier") -> ModelTableViewDataSource {
return ModelTableViewDataSourceWithAd(models: someModel, reuseIdentifier: reuseIdentifier) { (someModel, someCell) in
someCell.model = someModel
}
}
}

Related

Issue displaying search results

Essentially I have tableview with JSON data and a search controller for the user to quickly find values from the response. While the tableview data loads initially when attempting to search I get the error index out of range. The error occurs inside the cellForRowAt function. The following is what I am currently doing:
var sections = [customerListSection]()
var structure = [customerList]()
var searchsections = [customerListSection]()
var searchstruct = [customerList]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
let searchsection = searchsections[section]
if isFiltering() {
return searchsection.count
}
return section.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = sections[indexPath.section].items[indexPath.row]
let customerList: customerList
if isFiltering() {
customerList = searchstruct[indexPath.row]
} else {
customerList = item
}
return cell
}
struct customerListSection {
let customerType : String
var items : [customerList]
}
struct customerList: Decodable {
let customerid: Int
let customer: String
let type: String
}
From your code, it seems that there are multiple sections, so you need to implement numberOfSections(in:)method.
If you do not implement this method, the table configures the table with one section.)
https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614860-numberofsections
And since the number of rows is the number of items, not the number of sections, the tableView(_:numberOfRowsInSection:) method should return items.count.
The following is the modified code:
override func numberOfSections(in tableView: UITableView) -> Int {
if isFiltering() {
return searchsections.count
}
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return searchsections[section].items.count
}
return sections[section].items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let customerList: customerList
if isFiltering() {
customerList = searchsections[indexPath.section].items[indexPath.row]
} else {
customerList = sections[indexPath.section].items[indexPath.row]
}
cell.textLabel?.textAlignment = .left
cell.selectionStyle = .none
cell.textLabel?.font = .boldSystemFont(ofSize: 20)
cell.textLabel?.numberOfLines = 0
cell.textLabel?.lineBreakMode = .byWordWrapping
cell.textLabel!.text = customerList.customer
return cell
}

Need help adding a section to TableViewController array

I want to add a third section to the this UITableViewController. I've tried this :
var listOfCauses:[[String]] = [
["Causes", "", ""],
["Cause 1" , "small descript", "links"],
["Cause 2", "small descript", "links"]
]
When I run the application table view the links subheading doesn't show. How do I fix this? Thanks in advance! Here is my full code for the UITableViewController below:
import UIKit
class CausesTableViewController: UITableViewController {
// I want to add the third section to this array
var listOfCauses:[[String]] = [
["Causes", ""],
["Cause 1" , "small descript"],
["Cause 2", "small descript"]
]
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int {
// return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listOfCauses.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell( withIdentifier: "cellIdentifier")
if cell == nil {
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cellIdentifier")
}
cell?.textLabel?.font = UIFont.boldSystemFont(ofSize: 23)
cell!.textLabel!.text = listOfCauses[indexPath.row][0]
cell?.detailTextLabel?.textColor = .darkGray
cell?.detailTextLabel?.font = UIFont.systemFont(ofSize: 17.0)
cell!.detailTextLabel!.text = listOfCauses[indexPath.row][1]
return cell!
}
}
Update these methods
override func numberOfSections(in tableView: UITableView) -> Int {
// return the number of sections
return listOfCauses.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listOfCauses[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell( withIdentifier: "cellIdentifier")
if cell == nil {
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cellIdentifier")
}
cell?.textLabel?.font = UIFont.boldSystemFont(ofSize: 23)
cell!.textLabel!.text = listOfCauses[indexPath.section][indexPath.row]
cell?.detailTextLabel?.textColor = .darkGray
cell?.detailTextLabel?.font = UIFont.systemFont(ofSize: 17.0)
cell!.detailTextLabel!.text = listOfCauses[indexPath.row][1]
return cell!
}

How to navigate to another view controller from datasource class

I have a UIViewController with a UITableView. I have separate class for UITableViewDataSource and UITableViewDelegate. How can I navigate to another view controller when a cell is selected?
class ViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = TableViewDataSource()
}
}
class TableViewDataSource: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//How to push SecondVC from Viewcontroller?
}
}
class TableViewDataSource: UITableViewDataSource, UITableViewDelegate {
var parentViewController : UIViewController?
func init(parentController : UIViewController){
self.parentViewController = parentController
}
// rest of your code
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//How to push SecondVC from Viewcontroller?
let storyboard = UIStoryboard(name: "Stroybaordname", bundle: nil)
let secondVC = storyboard.instantiateViewController(withIdentifier: "ViewControllerName")
parentViewController.present(secondVC, animated: true, completion: nil)
}
}
Try this way
If I had to do this I would do it using a separate delegate method. If I am missing the context pardon me.
class ViewController: UIViewController, TableViewDataSourceDelegate {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
let customTableDataSource = TableViewDataSource()
customTableDataSource.delegate = self
// Setting the custom delegate
tableView.delegate = customTableDataSource
}
func cellSelected(at: IndexPath) {
// Navigate from here.
}
}
/// Protocol helps to communicate with the TableViewDataSource to the user of it.
protocol TableViewDataSourceDelegate {
func cellSelected(at: IndexPath)
}
class TableViewDataSource: UITableViewDataSource, UITableViewDelegate {
var delegate: TableViewDataSourceDelegate?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.cellSelected(at: indexPath)
}
}
You can create a blocks to access it in your ViewController class.
//MARK:- MODULES
import UIKit
//MARK:- TYPEALIAS
typealias ListCellConfigureBlock = (_ cell : AnyObject? , _ item : AnyObject? , _ indexpath: IndexPath?) -> ()
typealias DidSelectedRow = (_ indexPath : IndexPath) -> ()
typealias ScrollViewDidScroll = (_ scrollView : UIScrollView) -> ()
typealias ViewForHeaderInSection = (_ section : Int) -> UIView?
typealias DidDeselectedRow = (_ indexPath : IndexPath) -> ()
typealias CanEditRowAt = (_ indexPath : IndexPath) -> Bool
typealias CommitEditingStyle = (_ commiteditingStyle: UITableViewCellEditingStyle, _ indexPath : IndexPath) -> ()
typealias HeightForRowAt = (_ indexPath : IndexPath) -> CGFloat
//MARK:- CLASS
class TableViewDataSource: NSObject {
//MARK:- PROPERTIES
var _section: [String]?
var sectionCount: Array<AnyObject>?
var rowCount : Array<AnyObject>?
var cellIdentifier : String?
var tableView : UITableView?
var configureCellBlock : ListCellConfigureBlock?
var aRowSelectedListener : DidSelectedRow?
var ScrollViewListener : ScrollViewDidScroll?
var viewforHeaderInSection : ViewForHeaderInSection?
var headerHeight : CGFloat?
var aRowDeselectedListener : DidDeselectedRow?
var aRowEditListener : CanEditRowAt?
var aRowCommitListener : CommitEditingStyle?
var aRowHeightListener : HeightForRowAt?
init (tableView : UITableView?, cellIdentifier : String?, sectionCount: Array<AnyObject>?, rowCount : Array<AnyObject>? , height : HeightForRowAt? , configureCellBlock : ListCellConfigureBlock? , aRowSelectedListener : #escaping DidSelectedRow,aRowDeselectedListener: #escaping DidDeselectedRow ,aRowCommitListener: #escaping CommitEditingStyle, DidScrollListener : ScrollViewDidScroll?) {
self.tableView = tableView
self.sectionCount = sectionCount
self.rowCount = rowCount
self.cellIdentifier = cellIdentifier
self.aRowHeightListener = height
self.configureCellBlock = configureCellBlock
self.aRowSelectedListener = aRowSelectedListener
self.aRowDeselectedListener = aRowDeselectedListener
self.aRowCommitListener = aRowCommitListener
self.ScrollViewListener = DidScrollListener
}
override init() {
super.init()
}
}
//MARK:- DELEGATE, DATASOURCE
extension TableViewDataSource : UITableViewDelegate , UITableViewDataSource{
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = cellIdentifier else { fatalError("Cell identifier not provided") }
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: identifier , for: indexPath) as UITableViewCell
cell.selectionStyle = UITableViewCellSelectionStyle.none
if let block = self.configureCellBlock , let item: AnyObject = self.rowCount?[(indexPath as NSIndexPath).row]{
block(cell , item , indexPath as IndexPath?)
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let block = self.aRowSelectedListener{
block(indexPath)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.rowCount?.count ?? 0
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.sectionCount?.count ?? 0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let block = self.aRowHeightListener else { return 0.0 }
return block(indexPath)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let block = viewforHeaderInSection else { return nil }
return block(section)
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self._section?[section]
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return headerHeight ?? 0.0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let block = self.ScrollViewListener{
block(scrollView)
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let block = self.aRowDeselectedListener {
block(indexPath)
}
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
guard let block = self.aRowEditListener else { return false }
return block(indexPath)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if let block = self.aRowCommitListener{
block(editingStyle, indexPath)
}
}
}
You can use it like:
//Create a variable:
var dataSource = TableViewDataSource()
fileprivate func setUpDataSource() throws {
DispatchQueue.main.async { [weak self] in
dataSource = TableViewDataSource.init(tableView: tblView, cellIdentifier: CellId.chatUserTVCell.rV, sectionCount: arrSection as Array<AnyObject>, rowCount: arrSearchChatListing , height: { (indexPath) -> CGFloat in
return 80.0
}, configureCellBlock: { (cell, item, indexPath) in
guard let _cell = cell as? ChatUserTableCell, let _item = item as? ChatListing, let row = indexPath?.row else { return }
_cell.chatListing = _item
try? self?.shouldRequestPaging(row: row)
}, aRowSelectedListener: { (indexPath) in
let vc = StoryboardScene.Home.instantiateChatTextController()
try? self?.pushVC(vc)
}, aRowDeselectedListener: { (indexPath) in
debugPrint("Deselected")
}, aRowCommitListener: { (editingStyle, indexPath) in
debugPrint("aRowCommitListener")
}, DidScrollListener: { (scrollView) in
debugPrint("DidScrollListener")
})
tblView.delegate = dataSource
tblView.dataSource = dataSource
tblView.reloadData()
}
}
Hope it helps :)

How do you add a second section with a different type of prototype cell to a tableview in Swift?

I have a tableview with 2 prototype cells in a view controller. I want each cell to display data from different arrays.
Below is my code. I can get the tableview to show jobs or schools, but not both. I don't understand how to make the table display cell1 (jobs) and then cell2 (schools) when each cell contains different data sources.
screenshot
Import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let jobs = ["McDonalds", "Hardees", "Taco Bell"]
let schools = ["Univ of CO", "Univ of TX", "Univ of CA"]
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return jobs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "JobsCell", for: indexPath) as! Jobs
let job = jobs[indexPath.row]
cell.jobLbl.text = job
return cell
}
}
Answer: I fixed this by adding numberOfSections function (thanks Robert). Updated code below if anyone else has this question:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let jobs = ["McDonalds", "Hardees", "Taco Bell"]
let schools = ["Univ of CO", "Univ of TX", "Univ of CA", "Univ of Camdenton"]
override func viewDidLoad() {
super.viewDidLoad()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0) {
return jobs.count
} else {
return schools.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "JobsCell", for: indexPath) as! Jobs
let job = jobs[indexPath.row]
cell.jobLbl.text = job
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "SchoolsCell", for: indexPath) as! Schools
let school = schools[indexPath.row]
cell.schoolLbl.text = school
return cell
}
}
}
It sounds like you want to display one section with cells for jobs followed by a second section with cells for schools. If that's the case, you'll need to set the number of sections to 2, then rewrite your delegate functions to respond appropriately depending on the section number.
So numberOfRowsInSection is going to have to check the section number, then return the number of rows for that specific section. And cellForRowAt will need to check the section, then set up and return either a job or a school cell for the given row.
Here's what that would look like in your example:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let jobs = ["McDonalds", "Hardees", "Taco Bell"]
let schools = ["Univ of CO", "Univ of TX", "Univ of CA"]
override func viewDidLoad() {
super.viewDidLoad()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch(section) {
case 0: return jobs.count
default: return schools.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch(indexPath.section) {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "JobsCell", for: indexPath) as! Jobs
let job = jobs[indexPath.row]
cell.jobLbl.text = job
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "SchoolsCell", for: indexPath) as! Schools
let school = schools[indexPath.row]
cell.schoolLbl.text = school
return cell
}
}
}
And here's the output in the simulator:

UITableVC No section header content shown

converting my code to swift ONLY so no use of storyboards. After rewriting the code (no storyboard usage) the header information is NOT shown !!
If I change the viewForHeaderInSection return value from:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "headerId") as! ListsVCHeader
cell.fillHeader(header: listTitles[section])
return cell.contentView
}
to:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "headerId") as! ListsVCHeader
cell.fillHeader(header: listTitles[section])
return cell // ==> removed .contectView
}
the header content is shown BUT the section header moves left performing a swipe gesture that I use to initiate a row delete. Any suggestions? Find the relevant code below.
class ListsVC: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(ListsVCHeader.self, forCellReuseIdentifier: "headerId")
// register other Cells
}
override func numberOfSections(in tableView: UITableView) -> Int {
return listTitles.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int)
-> String? {
return listTitles[section]
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "headerId") as! ListsVCHeader
cell.fillHeader(header: listTitles[section])
return cell.contentView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return HeaderSectionHeight
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
// mycode
default:
break
}
}
}
the Section Header cell
class ListsVCHeader: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// add subviews and constraints
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func fillHeader (header: String) {
name.text = header
}
}
Change your register in viewDidLoad to:
tableView.register(ListsVCHeader.self, forHeaderFooterViewReuseIdentifier: "headerId")
Your class ListsVCHeader should be changed to:
class ListsVCHeader: UITableViewHeaderFooterView
and last change the following code:
let cell = tableView.dequeueReusableCell(withIdentifier: "headerId") as! ListsVCHeader
to
let cell = tableView.dequeueReusableHeaderFooterView(withIdentifier: "headerId) as! ListsVCHeader