I am trying to set up an UITableView with sections using the new UITableViewDiffableDataSource within an UITableViewController.
Everything seems to work fine except setting the section header titles.
According to Apple's documentation, UITableViewDiffableDataSource conforms to UITableViewDataSource, so I was expecting this to be possible.
I have tried:
overriding tableView(_ tableView:, titleForHeaderInSection section:)
in the UITableViewController class
subclassing UITableViewDiffableDataSource and implementing tableView(_ tableView:, titleForHeaderInSection section:) in the subclass
but both ways lead to no result (Xcode 11 and iOS13 beta 3).
Is there currently a way to set the section header titles using UITableViewDiffableDataSource?
Providing code example on #particleman's explanations.
struct User: Hashable {
var name: String
}
enum UserSection: String {
case platinum = "Platinum Tier"
case gold = "Gold Tier"
case silver = "Silver Tier"
}
class UserTableViewDiffibleDataSource: UITableViewDiffableDataSource<UserSection, User> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let user = self.itemIdentifier(for: IndexPath(item: 0, section: section)) else { return nil }
return self.snapshot().sectionIdentifier(containingItem: user)?.rawValue
}
}
Update: Starting in beta 8, you can now implement tableView(_ tableView:, titleForHeaderInSection section:) in a UITableViewDiffableDataSource subclass and it works properly.
The default behavior for populating the header title has always been a little strange to have in the data source. With UITableViewDiffableDataSource, Apple seems to be acknowledging such by not providing the default string-based behavior; however, the UITableViewDelegate methods continue to work as before. Implementing tableView(_:viewForHeaderInSection:) by initializing and returning a UILabel with the desired section title and implementing tableView(_:heightForHeaderInSection:) to manage the desired height works.
Let me suggest quite a flexible universal solution:
Declare a subclass:
class StringConvertibleSectionTableViewDiffibleDataSource<UserSection: Hashable, User: Hashable>: UITableViewDiffableDataSource<UserSection, User> where UserSection: CustomStringConvertible {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionIdentifier(for: section)?.description
}
}
Usage example:
class ComitsListViewController: UITableViewController {
private var diffableDataSource = StringConvertibleSectionTableViewDiffibleDataSource<String, Commit>(tableView: tableView) { (tableView, indexPath, commit) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "Commit", for: indexPath)
cell.configure(with: commit)
return cell
}
}
You are not limited just to String thought. You can control what to display as section title by implementing description var of CustomStringConvertible protocol for your section type.
After you init self.dataSource to a 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 its already set.
If you do it this way then it makes sense why the class wasn't open in the early iOS 13 betas.
Related
I have a UITableView where the section header view contains a UITextField that the user can key in values. When I want to save the value that user keyed in, I need to access the table section header.
I tried using the method headerView(forSection:), and I put headerView(forSection: 0)! as a test to make sure that the section header definitely exists, but every time when I run the programme, the line where the method is returns an error "unexpectedly found nil while unwrapping an optional value".
I have also tried to tag the UITextField, but when I attempt to access the section header using certainTableView.viewWithTag(_ tag:), and cast the resulting UIView to UITextField, there is an error thrown at the line where casting happens, saying "Could not cast value of type 'UITableView' to 'UITextField'."
Both errors look pretty bizarre to me. In the first case, I made sure that the section header is existent, yet the method returned nil. In the second case, the returned tagged view should be of type UITableViewHeaderFooterView, yet the type is UITableView. Can someone suggest any possible reason why this is so and how to correct?
Or alternatively, is there any better approach to access the section header view so that I can save the text keyed in in the UITextField?
Below is the detail that I used in the first case.
In func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?, I returned a UIView that contains a UITextField. In the table view which I need to save the keyed-in user data, I have a button that, when the user taps it, data are saved. Within that selector, I have the following:
//save item
let a = addItemTableView.headerView(forSection: 0)!
let b = a.subviews[0] as! UITextField
let c = b.text ?? ""
someNSManagedObject.text = c
The error I mentioned above appears at the first line.
After reading the post in this question, I got a hint and found out that, in func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? method, I should return a UITableViewHeaderFooterView instead of the previously UIView. As I changed the returned object's type to UITableViewHeaderFooterView, with everything else remaining the same, (i.e. add a UITextField to the UITableViewHeaderFooterView) I managed to access the UITextField in the UITableViewHeaderFooterView via headerView(forSection:) method.
To Be even clearer, given in the apple's documentation that the function declaration is func headerView(forSection section: Int) -> UITableViewHeaderFooterView?, showing that the returned value is UITableViewHeaderFooterView, in order to access the header view using the method, the returned value from func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? should be UITableViewHeaderFooterView instead of any other generic UIView or UIView subclasses.
Hope this helps.
First add a property to your model textFieldData to save textField data in.
Then in your HeaderViewClass add a closure var textFieldDidChange: ((String) -> Void)?
class HeaderViewClass: UITableViewHeaderFooterView {
// MARK:- IBOutlets
#IBOutlet weak var textField: UITextField!
// MARK:- Variable
var textFieldDidChange: ((String) -> Void)?
// MARK:- Functions
func fill(with model: Model) {
textField.delegate = self
textField.text = model.textFieldData
}
}
extension HeaderViewClass: UITextFieldDelegate {
func textFieldDidChange(_ textField: UITextField) {
textFieldDidChange?(textField.text)
}
}
In your viewController class
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: PlaceOrderFooterView.id) as! PlaceOrderFooterView
footerView.fill(with: models[section])
footerView.textFieldDidChange = { [unowned self] text in
self.models[section].textFieldData = text
}
return footerView
}
Now your model object has the updated value whenever any textField data is changed.
In your method
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {}
when you return your custom header containing UITextField , assign it's delegate to your controller, this way you'll get all keyed values by user under UITextFieldDelegate function
Here is my implementation of tableView(_:cellForRowAt:):
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let index = indexPath.section
let weekDay = WeekDays.day(at: index)
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
return UITableViewCell()
}
Here is my code for my custom table view cell:
class NotSelectedCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = .red
self.textLabel?.numberOfLines = 0
self.textLabel?.textAlignment = .center;
self.textLabel?.text = "Not Available"
}
}
I've also tried initializing custom cell cell = NotSelectedCell() the result is the same. The content isn't shown. dataSource or viewDelegate aren't the problem as I'm working with UITableViewController.
Here's an image
The problem is awakeFromNIB "prepares the receiver for service after it has been loaded from an Interface Builder archive, or nib file." But you're instantiating this programmatically, so that method isn't called. You could theoretically move the code to init(style:reuseIdentifier:), make sure to call super in your implementation, and do any additional customization after that point.
But, you generally wouldn't programmatically instantiate cells when using static cells. (It's the point of static cells, that IB takes care of everything for you.) You generally don't implement UITableViewDataSource at all when using static cells.
I would advise using dynamic table and have two cell prototypes, one with reuse identifier of "NotAvailable" and one with "Available" (or whatever identifiers you want). Then programmatically instantiate the cell with the appropriate identifier. (By the way, this also has the virtue that your cell with "NotAvailable" can be designed entirely in IB, and no code is needed, for that cell at least.) This way, the storyboard takes care of instantiating the appropriate cell.
So, here I have two cell prototypes in my dynamic table, one for "not available" and one for "available":
Then the code would look at the model to figure out which to instantiate:
// for the complicated cell where I want to show details of some window of availability, add IBOutlets for that cell's labels
class AvailableCell: UITableViewCell {
#IBOutlet weak var startLabel: UILabel!
#IBOutlet weak var stopLabel: UILabel!
#IBOutlet weak var doctorLabel: UILabel!
}
// some super simple model to represent some window of availability with a particular doctor in that office
struct Availability {
let start: String
let stop: String
let doctor: String
}
class ViewController: UITableViewController {
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
let available = ...
override func numberOfSections(in tableView: UITableView) -> Int {
return days.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return available[days[section]]?.count ?? 1
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return days[section]
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// see if there are any available windows for the given day, if not, return "not available" cell
guard let availabilities = available[days[indexPath.section]] else {
return tableView.dequeueReusableCell(withIdentifier: "NotAvailable", for: indexPath)
}
// otherwise, proceed with the more complicated "Available" cell where I have to populate various labels and the like
let cell = tableView.dequeueReusableCell(withIdentifier: "Available", for: indexPath) as! AvailableCell
let availability = availabilities[indexPath.row]
cell.startLabel.text = availability.start
cell.stopLabel.text = availability.stop
cell.doctorLabel.text = availability.doctor
return cell
}
}
And that would yield:
Now, clearly, I just whipped up a super primitive model, and didn't do any UI design in the "available" cell prototype other than inserting three labels. But it illustrates the idea: If your dynamic table has multiple unique cell designs, just implement cell prototypes for each with unique identifiers and instantiate the appropriate one. And this way, you enjoy full cell reuse, minimize how much visual design you have to do programmatically, etc.
You are not supposed to use the cellForRow:atIndexPath method when using static cells. The cells are static, so the loading flow is different. What i'd suggest is to connect the cells individually from the interface builder to your view controller.
STILL, if you want to do it this way you have to get your cells by calling "super" since that's the class who is actually generating your static cells.
UITableView with static cells without cellForRowAtIndexPath. How to set clear background?
EDIT:
I just noticed that this is wrong:
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
You have to use the "dequeueReusable" method or something. Then again, these are STATIC Cells, so you should just be linking the cells directly from the interface builder.
I've been searching for awhile without luck. I am trying to find an example of a View Controller with a UITableView that has sections. The examples I've see are all dealing with a Table View Controller which I cannot use as I have need of buttons in the same view which control the content of the table view. Anyone have an example, know of an example or have an idea about to implement such? Thanks.
Edit
I've got a table view in a view controller, get the data from an api call, separate the sections and data in an array of a struct. I then send this to be bound to the table view. Doing so throws
[UIView tableView:numberOfRowsInSection:]: unrecognized selector sent to instance
but I don't understand where the problem is.
Code for the tablview
//MARK: Tableview delegates
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let count = incidentDataSection?.count{
return count
}
return 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (incidentDataSection?.count)! > 0{
return incidentDataSection![section].incidents.count
}
return 0
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return incidentDataSection?[section].title
}
/*
func tableView(tableView: UITableView, iconForHeaderInSection section: Int) -> UIImage? {
return incidentDataSection?[section].icon
}*/
//if clicked, will openn details view passing in the details
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//let incidentDetails = incidentData?[indexPath.row]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let section = incidentDataSection?[indexPath.section] {
let cell = tableView.dequeueReusableCell(withIdentifier: "IncidentTableViewCell") as! IncidentTableViewCell
cell.roadNameLabel.text = section.incidents[indexPath.row].RoadWay
cell.whenLabel.text = section.incidents[indexPath.row].DateCreated
cell.statusLabel.text = section.incidents[indexPath.row].DateCleared
return cell
}
return UITableViewCell()
}
incidentDataSection is an array of a struct which has the section title and the different items.
Answer
Though I received some fairly good feedback, the cause was actually a typo. Looking closely at
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return incidentDataSection?[section].title
}
you'll notice the problem is that there is no underscore before tableView:. What was happening is that the datasource and delegate were skipping over the functions since with and without call different protocols in swift 3. Thanks to thislink I was able to figure out the cause. My bad for forgetting to mention this was in Swift 3. Might had saved everyone some time.
You need a tableview instance in your view controller.
Implement the protocols UITableViewDelegate, UITableViewDataSource in your view controller as a UITableViewController.
Don't forget bind the tableview in XIB with tableview in the class.
Look this sample:
class Sample01ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
tableView?.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.tableView?.reloadData()
}
// ...
You have the required methods implemented, however it sounds like you need to "subclass" or "subcribe" to the UITableView's delegate and dataSource. By using:
class MyViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView : UITableView!
}
Now that you have those protocols you will need to set your tableView's delegate and dataSource to your viewController. You can do this using storyboard by drag and drop, or inside of your viewDidLoad() which is what I always do because it is easy for other developers to see from the start of opening your code where your delegate and dataSources are assigned to. Using:
#override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Then your delegate methods and dataSource methods in your viewcontroller will be called for that tableView. Then you can add the IBOutlets to UIButton/UILabel/UISwitch, etc... and do what you will with your ViewController without being limited to simply using a table view inside of that view controller. I Almost always use this methods when using UITableViews/UICollectionViews even if I set the tableView/collectionView to be the size of the whole view because I like the freedom of using a UIViewController over a UITableViewController/UICollectionViewController.
*Note numberOfRows() is not required but I always override it as well, just kind of a habit at this point. Also you sound new to iOS development, so if you aren't already, the next thing I would look into after getting your tableView up and running is pulling your data from your API on a background thread to keep your mainThread open for user response on your UI, DispatchQueue. This is really important if you are displaying images from the API.
I'd like to get started using swift to make a small list based application. I was planning on using two table view controllers to display the two lists, and was wondering if it were possible to have them share a common data source.
Essentially the data would just be an item name, and two integers representing the amount of the item owned vs needed. When one number increases, the other decreases, and vice versa.
I figured this might be easiest to do using a single data source utilized by both table view controllers.
I did some googling on shared data sources and didn't find anything too useful to help me implement this. If there are any good references for me to look at please point me in their direction!
You can create one data source class and use it in both view controllers:
class Item {
}
class ItemsDataSource: NSObject, UITableViewDataSource {
var items: [Item] = []
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
//setup cell
// ...
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
}
class FirstViewController : UITableViewController {
var dataSource = ItemsDataSource()
override func viewDidLoad() {
self.tableView.dataSource = dataSource
self.tableView.reloadData()
}
}
class SecondViewController : UITableViewController {
var dataSource = ItemsDataSource()
override func viewDidLoad() {
self.tableView.dataSource = dataSource
self.tableView.reloadData()
}
}
use singleton design pattern, it means both tables will get data source from the same instance
class sharedDataSource : NSObject,UITableViewDataSource{
static var sharedInstance = sharedDataSource();
override init(){
super.init()
}
//handle here data source
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
}
}
var tableOne = UITableView();
var tableTwo = UITableView();
tableOne.dataSource = sharedDataSource.sharedInstance;
tableTwo.dataSource = sharedDataSource.sharedInstance;
The first argument to the delegate method is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
}
At that point, your one Datasource delegate can decide which table view is wanting a cell, for example, and return results accordingly.
I've got an issue where my tableView isn't updating based on the datasource correctly. I'm using a tabbed application structure with Storyboards.
My overall goal here is to have a tableView on the second tab display items that are removed from an array stored in a struct. The items are added to the array from the first tab.
There are 2 ViewControllers (1 for the interface for scrolling through items and selecting to remove them, and 1 to handle the tableView) and 2 Views (1 for the interface for scrolling through items and removing them and 1 for the tableView). The first tab is for providing the interface for removing the items and the second tab is for the tableView.
The remove and add to the array functionality works, just not the displaying it in the tableView.
Currently, if I hard code items in my "removed items" array, they are displayed in the tableView. The problem is that as I add items to the array from my removeItem function in the first ViewController, the tableView does not update, only the hard coded items are shown.
This makes me assume that I have my datasource and delegate setup correctly, since the tableView is getting it's data from the intended datasource. The issue is it's not updating as the user updates the array with new items.
I've tried using self.tableView.reloadData() with no success. I might not be calling in the correct location though.
I'm not sure where the disconnect is.
Here is my second view controller that controls the tableView
class SecondViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let cellIdentifier = "cellIdentifier"
var removedTopicsFromList = containerForRemovedTopics()
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView?.registerClass(UITableViewCell.self, forCellReuseIdentifier: self.cellIdentifier)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// UITableViewDataSource methods
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return removedTopicsFromList.removedTopics.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier) as UITableViewCell
cell.textLabel!.text = self.removedTopicsFromList.removedTopics[indexPath.row]
return cell
}
Here is the struct where the removed phrases are stored
struct containerForRemovedTopics {
var removedTopics: [String] = []
}
structure instances are always passed by value. So if your code is something like:
var removedTopicsFromList = secondViewController.removedTopicsFromList
removedTopicsFromList.removedTopics.append("SomeTopic")
secondViewController.reloadData()
then you are changing the different structure.
Maybe you got stuck with this problem I guess.