UITableViewDiffableDataSource: how to get a section index - ios

I'm trying to adopt the new iOS 13 UITableViewDiffableDataSource and I've hit a snag; I can't work how to implement
func sectionIndexTitles(for tableView: UITableView) -> [String]?
That's a data source method, not a delegate method. So, now that the data source is the UITableViewDiffableDataSource, it needs to implement that method. But it doesn't.
I tried subclassing UITableViewDiffableDataSource and adding an implementation of sectionIndexTitles, but my implementation was never called:
class MyDataSource : UITableViewDiffableDataSource<String,String> {
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return self.snapshot().sectionIdentifiers // not called
}
}
Has anyone solved this one? I'll file it as a bug just in case.

You need to subclass UITableViewDiffableDataSource and then overwrite
func sectionIndexTitles(for tableView: UITableView)
by yourself.
To enable the index' functionality though, you have to also overwrite
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int)
Here is an example how I implemented it:
import UIKit
import MediaPlayer
class TableViewDiffableDataSource: UITableViewDiffableDataSource<String, MPMediaItemCollection> {
var sectionTitles = [String]()
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return sectionTitles
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionTitles[section]
}
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return sectionTitles.firstIndex(of: title) ?? 0
}
}
Credits go to Steve Breen, who led me in the right direction.

After you init self.dataSource to UITableViewDiffableDataSource (which sets itself to the tableView.dataSource) set the tableView.dataSource back to self, i.e. the UITableViewController subclass. Now in your numberOfSectionsInTableView and numberOfRowsInSection methods forward those to self.dataSource and return its info (this is the composition pattern). Now your UITableViewController just implements its section titles as normal since it is the table's data source.
I believe UITableViewDiffableDataSource should not be setting itself as the dataSource if one is already set but I guess they designed it to work in the least error prone way because with UITableViewController added to a storyboard it's set as the dataSource and delegate by default.

Related

Why headerView is always nil in numberOfRowsInSection?

I have to make a dynamic datasource for a table, that satisfies the criteria:
we do not know exact number of sections
we do not know exact title for header in section
we do not now how many rows it will be in each section each time
To detect number of rows per section I have the following code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let header = tableView.headerView(forSection: section)
return self.servicesData?.filter({ $0.clubName == header?.textLabel?.text }).count ?? 0
}
where servicesData is the object array that is to be filtered by the header title aka clubName. Titles are gathered from the object array.
For some reason I always get nil when try to access the header. So far I tried calling tableView(_:titleForHeaderInSection:) directly in numberOfRowsInSection but obviously with no luck.
Feedback much appreciated.
It is nil because it is not created at that moment. You should not use tableView.headerView(forSection: section) in numberOfRows. You should use the model from where you get the title to use it to filter in the array.
You should fetch all of your data before populating the table view and first. Then you should implement:
func numberOfSections(in tableView: UITableView) -> Int {
return headerModels.count
}
where headerModels it's an array of objects with your title and it's items like:
class HeaderModel {
let title: String
let items: [YourItem]
}
and then:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return headerModels[section].items.count
}
So, as per #shurtugal 's solution it is possible to pass-in an array of objects to populate the table. I thought that creating a utility class for this very case may be too much. So here's the slightly altered way:
Consider a dictionary var servicesData = [String: [CustomObject]]() that is populated each time the ViewController shows up. The dictionary can have indefinite number of keys and indefinite number of values inside each key. Algorithm of populating the dictionary is taken from this answer.
Thereby, UITableViewDatasource methods can be defined like this:
func numberOfSections(in tableView: UITableView) -> Int {
self.servicesData.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Array(self.servicesData.values)[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
/* cell creation code code */
/// sample line
cell?.someLabel.text = Array(self.servicesData.values)[indexPath.section][indexPath.row]
return cell
}

UITableview Sections Indexing Click Event

Is there any way to get click event of the table view section indexing?
I have researched lots but not got any appropriate suggestion.
does anyone know how to get click event action of indexing?
I want to get click event of this blue marked indexing in the below image.
You can use the tableView(_:sectionForSectionIndexTitle:at:) method to return the appropriate section index when a index title is clicked.
let sectionArr = ["a","a","b","c","d","d"]
func numberOfSections(in tableView: UITableView) -> Int {
return sectionArr.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionArr[section]
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return Array(Set(sectionArr))//["a","b","c","d"]
}
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
if let index = sectionArr.firstIndex(where: { $0 == title }) {
return index//0 for a, 2 for b, 3 for c, 4 for d
}
return 0
}
You can use a custom view as the index view of the UITableView as shown in below reference :
Custom UITableView section index
This is in Objective-c but I think it is a great reference as that has the touchesBegan, touchesMoved, touchesEnded and touchesCancelled events and you can set the user experience with these as you would like. Also it would be very easy to add customised behaviours in this custom indexing view.
Hope this helps.

Static TableView gets NSRangeException

I have a UITableViewController with static cells, and when I run the app and click on the button that leads to it, I get a SIGABRT signal in AppDelegate.
I tried to find unused outlets, but it didn't work.
Here is the Console Log:
The UITableViewController Code:
import UIKit
import os.log
class SettingsTableViewController: UITableViewController {
// MARK: Properties
#IBOutlet weak var noteDisplayKindSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
notedisplayKindSwitch.setOn(Settings._displaynotesAsNames, animated: false)
}
#IBAction func ChangeNoteDisplayKind(_ sender: UISwitch) {
Settings._displayNotesAsNames = sender.isOn
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
}
The UITableViewController in the Storyboard:
The Connections:
What am I doing wrong?
Thanks.
You are using static cells. There is no reason to implement numberOfSections and numberOfRowsInSection because those are specified by the static layout in the Storyboard.
Because you have implemented:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
you are telling iOS that every section has 3 rows, which is a lie. So iOS tries to access the second row of your first section and crashes with array index out of range because that section has just 1 row.
So, delete the implementations of numberOfSections and numberOfRowsInSection and you should be good to go.
Read the error message, and please do not post images of code.
Somewhere in your tableView:cellforRowAtIndexPath method, there's an empty array but you're trying to access the element at index 1.

Swift 3: Tableview datasource methods "Overriding non-open instance method outside of its defining module" error

I have an open BaseViewController class in core framework that has tableview datasource methods implemented. Let's say I've another class (outside the module) ClassA with BaseViewController as it's superclass. When I try to override tableview datasource methods, it's throwing this error Overriding non-open instance method outside of its defining module.
BaseViewController looks like this
open class BaseViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
...
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
public func numberOfSections(in tableView: UITableView) -> Int {
return 0
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
ClassA
import CustomCoreFramework
class ClassA : BaseViewController {
// throws an error
public override func numberOfSections(in tableView: UITableView) -> Int {
return tableViewListItems.count
}
}
I suppose the open class methods should be accessible outside the module. I tried changing the tableview methods access specifiers to public and different combinations but nothing seems to work.
The BaseViewController’s methods should be declared open.
This is discuss in the thread in reference.
See What is the 'open' keyword in Swift?

Override non-dynamic class delaration [duplicate]

This question already has answers here:
Overriding methods in Swift extensions
(6 answers)
Closed 1 year ago.
With new Xcode 8.3, I get the error:
Cannot override a non-dynamic class declaration from an extension
from the line of code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
How I can avoid this warning?
Extensions can add new functionality to a type, but they cannot override existing functionality.
You have two options:
Move your method inside the Class-scope, instead of extension.
Add dynamic type to your method (where it's defined), like example.
Example:
#objc open dynamic func onLog(_ message: String) -> Void {
print("Info: \(message)");
}
which means, you can't not override like super class delegate method in extension.
the way you can do is mv extension override method to subclass declare.
final class DEMO, UITableViewDataSource, UITableViewDelegate {
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return sectionHeader
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40
}
}

Resources