How to add an extra static cell to a Diffable Data Source? - swift

With reference to this question, I would like to do something similar. There were two ways I tried to replicate the answer for my diffable data source for my collection view.
The first way was to create another diffable data source but for the same collection view. The actual item cells would be configured when the indexpath.row is less than the count of my array while when it is equals to the count, then it will display my static cell.
//configure cells: categoryCollectionViewDataSource is the actual datasource for the items that I want to display.
categoryCollectionViewDataSource = UICollectionViewDiffableDataSource<Int, MistakeCategory>(collectionView: categoryCollectionView){
(collectionView, indexPath, mistakeCategory) in
if indexPath.row < (subjectSelected?.MistakeCategories.count)! {
let cell = self.categoryCollectionView.dequeueReusableCell(withReuseIdentifier: "CategoryCollectionViewCell", for: indexPath) as! CategoryCollectionViewCell
cell.setProperty(SubjectCategory: nil, MistakeCategory: self.subjectSelected?.MistakeCategories[indexPath.row])
return cell
} else {
return nil
}
}
//configure cells: staticCollectionViewDataSource is the data source for the cell that I want to display no matter what. it is displayed at the last row of the indexpath.
staticCellCollectionViewDataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: categoryCollectionView){
(collectionView, indexPath, mistakeCategory) in
if indexPath.row == subjectSelected?.MistakeCategories.count {
let cell = self.categoryCollectionView.dequeueReusableCell(withReuseIdentifier: "StaticCreateNewCategoryCollectionViewCell", for: indexPath) as! StaticCreateNewCategoryCollectionViewCell
return cell
} else {
return nil
}
}
And here is where I update my diffable data source which I run on my viewDidLoad after configuring my cells:
internal func updateCategoryCollectionView() {
var snapshot = NSDiffableDataSourceSnapshot<Int, MistakeCategory>()
snapshot.appendSections([0])
snapshot.appendItems(Array(subjectSelected!.MistakeCategories))
categoryCollectionViewDataSource.apply(snapshot) //error: 'attempt to insert section 0 but there are only 0 sections after the update'
var staticSnapshot = NSDiffableDataSourceSnapshot<Int,Int>()
staticSnapshot.appendSections([0])
staticSnapshot.appendItems([0])
staticCellCollectionViewDataSource.apply(staticSnapshot)
}
This results in the error 'attempt to insert section 0 but there are only 0 sections after the update'. I have tried to implement the functions of UICollectionViewDataSource but ran into the problem of how I can merge these two type of data sources.
As such, I am at a lost to what I can do to create a static custom cell at the end of my row.

I had the same problem with diffable data sources. Instead of creating a different data source and snapshot, I solved it by appending a default [AnyHashable] data to my snapshot where the static cell was wanted. In your case it would be after you append your usual MistakeCategories data.
To do this you need to initialize your data source as
categoryCollectionViewDataSource = UICollectionViewDiffableDataSource<Int, AnyHashable>(collectionView: categoryCollectionView){
(collectionView, indexPath, dataItem) in
and when using the dataItem object you need to cast
if let mistakeCategory = dataItem as? MistakeCategory {
// return your default cell here
}
and after the MistakeCategory cells
if indexPath.row == subjectSelected?.MistakeCategories.count {
let cell = self.categoryCollectionView.dequeueReusableCell(withReuseIdentifier: "StaticCreateNewCategoryCollectionViewCell", for: indexPath) as! StaticCreateNewCategoryCollectionViewCell
return cell
}
And finally when you apply your snapshot, you also need to use it as
var snapshot = NSDiffableDataSourceSnapshot<Int, AnyHashable>()
snapshot.appendSections([0])
snapshot.appendItems(Array(subjectSelected!.MistakeCategories))
//append your static value here
snapshot.appendItems([AnyHashable(0)])
categoryCollectionViewDataSource.apply(snapshot)
Disclaimer: This is an extremely hard-coded solution and I am looking for more elegant methods to do this, but I couldn't find any yet.

DiffableDataSource accepts enums for item and/or section. This allows you to easily have as many cell and section types as you need - just define them in an enum, register cells for them, and then point datasource to the right cell based on the enum value passed in.
The example below only has two types of items and one section, but you can use the same idea for different section types as well.
//Define as many types of items as you need
enum CollectionViewItem: Hashable {
case actualItem(ActualItem.ID) //You must to have a unique associated value if you want the datasource to be able to differentiate items. In this case I'm just using the item ID.
case staticButtonItem
}
//Register cells for each type of item
actualItemCellRegistration = UICollectionView.CellRegistration<YourDesiredCellTypeHere, CollectionViewItem> { (cell, indexPath, item) in
//Register cell
}
staticButtonCellRegistration = UICollectionView.CellRegistration<YourDesiredCellTypeHere, CollectionViewItem> { (cell, indexPath, item) in
//Register cell
}
//Point the datasource at the right registration based on the enum case
dataSource = UICollectionViewDiffableDataSource<Int, CollectionViewItem>(collectionView: collectionView){ (collectionView, indexPath, item) in
switch item {
case .actualItem:
return collectionView.dequeueConfiguredReusableCell(using: actualItemCellRegistration, for: indexPath, item: item)
case .staticButtonItem:
return collectionView.dequeueConfiguredReusableCell(using: staticButtonCellRegistration, for: indexPath, item: item)
}
}
//Make your snapshot with your defined items
func makeNewSnapshot(with data: [YourDataType]) {
var snapshot = NSDiffableDataSourceSnapshot<Int, CollectionViewItem>()
//Add your single section
snapshot.appendSections([0])
//Add your static cell
snapshot.appendItems(CollectionViewItem.staticButtonItem, toSection: 0)
//Add your actual items
let itemsForView = data.map {CollectionViewItem.actualItem($0.ID)}
snapshot.appendItems(itemsForView, toSection: 0)
dataSource.apply(snapshot)
}

Related

Is applying NSDiffableDataSourceSnapshot broken?

I have a problem with applying NSDiffableDataSourceSnapshot to UICollectionViewDiffableDataSource. Imagine this situation: I have two items and I want to remove the second one and also I want to reload all other items in that section.
I do it like this:
var snapshot = oldSnapshot
if let item = getterForItemToDelete() {
snapshot.deleteItems([item])
}
snapshot.reloadItems(otherItems)
But then in cell provider of data source app crashes since it tries to get cell for item identifier for which I no longer have data so I have to return nil:
let dataSource = MyDataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, identifier in
guard let viewModel = self?.viewModel.item(for: identifier) else { return nil }
...
}
What is strange is that when I try to debug it and print my items when I apply snapshot, it prints one item:
(lldb) print snapshot.itemIdentifiers(inSection: .mySection)
([App.MyItemIdentifier]) $R0 = 1 value {
[0] = item (item = "id")
}
But, immediately after this, in cell provider I get that I have two items, which I don't
(lldb) print self?.dataSource.snapshot().itemIdentifiers(inSection: .mySection)
([App.MyItemIdentifier]) $R1 = 2 values {
[0] = item (item = "ckufpa58100041ps6gmiu5zl6")
[1] = item (item = "ckufpa58100051ps69yrgaspv")
}
What is even more strange is that this doesn't happen when I have 3 items and I want to delete one and reload the others.
One workaround which solves my problem is to return empty cell instead of nil in cell provider:
let dataSource = MyDataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, identifier in
guard let viewModel = self?.viewModel.item(for: identifier) else {
return collectionView.dequeueReusableCell(withReuseIdentifier: "UICollectionViewCell", for: indexPath)
}
...
}
And the last strange thing, when I do this and then I look to View Hierarchy debugger, there is just one cell, so it seems that the empty cell gets removed.
Does anybody know what could I do wrong or is this just expected behavior? Since I didn't find any mention of providing cells for some sort of optimalizations, animations or something in the documentation.
You shouldn't be removing items from the snapshot. Remove them from your array. Create the dataSource again with the updated array, and call the snapshot with this newly created dataSource. CollectionView will automatically update with the new data. To put it more simply, change the array, then applySnapshot() again.

how to remove the cell from uitableview cell

Im trying to dynamically arranging table view when user select "type 3". It works when user select "type 3", "type 3-1" would be added in the tableview. However the program crashed when user select other than type3-1. I dont know how can I execute the "rows.remove(at:2)" before the override function is called. Any suggestion would appreciate!
class GuestViewController: UITableViewController {
var rows:[[[String:Any]]] = [[["type":RowType.DetailContent,
"subType":DCType.DCRightContent,
"name":CPFFields.CID,
"content":"9637"],
["type":RowType.DetailContent,
"subType":DCType.DCInput,
"name":CPFFields.VISIA]],
[["type":RowType.DetailTextView,
"CPFType":CPFFields.UV,
"title":CPFFields.preferenceTitle]],
[["type":RowType.DetailContent,
"subType":DCType.DCSelection,
"name":CPFFields.Phototherapy,
"title":CPFFields.anestheticTitle],
["type":RowType.DetailTextView,
"CPFType":CPFFields.Phototherapy,
"title":CPFFields.preferenceTitle]],
]
var isNewGuestSelected : Bool = false
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rows[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = rows[indexPath.section][indexPath.row]
let type = item["type"] as! RowType
if type == RowType.DetailContent
{
let cell = tableView.dequeueReusableCell(withIdentifier: "DetailNameCell", for: indexPath) as! DetailContentCell
let cpfType = item["name"] as? CPFFields ?? .Customer
cell.name.text = CPFFields.localizedString(from: cpfType)
if let field = item["title"] as? CPFFields
{
cell.name.text = CPFFields.localizedString(from: field)
}
cell.moreSlectionLeftSpace = true
var content:String? = ""
cell.type = cpfType
switch cpfType {
case .CID:
content = (profile?.birthDate.dateFromDateString?.stringForPaitentId ?? "") + (profile?.name ?? "")
case .CT:
content = ""
if let profile = profile
{
content = CPFCustomerType.localizedString(from: profile.type)
//New Guest
if(content == CPFCustomerType.type1.rawValue){
rows[0].insert(["type":RowType.DetailContent,
"subType":DCType.DCRightContent,
"name":CPFFields.CID,
"content":"9637"], at: 1)
isNewGuestSelected = true
} else{
if isNewGuestSelected == true{
rows[0].remove(at: 1)
isNewGuestSelected = false
}
}
}
let subType = item["subType"] as! DCType
cell.setcontentType(type: subType, content: content)
return cell
}
I expected not to see "rows[0][2]" after running "rows[0].remove(at:1)".
However the log is printing
rows[0][0]
rows[0][1]
rows[0][2]
then
it crashed at "let item = rows[indexPath.section][indexPath.row]"
because it is out of range
You are modifying your content while rendering, thus after numberOfRows:inSection: was called. Therefore the tableView is trying to access an element that no longer exists, since you removed it.
Your cycle:
→ number of rows 4
→ removed item, contents now has 3 items
→ cell for item 0
→ cell for item 1
→ cell for item 2
- cell for item 3 → crash
Consider replacing the logic you have here outside of the cellForRow method, and doing these operations before you reload your tableView.
You should use the tableView:cellForRow:atIndexPath strictly for dequeueing your cells and configuring them; not for modifying the underlying data store since funky things like you're experiencing now can happen.
If you provide a bit more context I can probably tell you where to place your code to fix this issue.
Actually, the solution is quite simple. I just added tableView.reloadData() after removing the array, and the UI can then be updated.
if isNewGuestSelected == true{
rows[0].remove(at: 1)
isNewGuestSelected = false
tableView.reloadData()
}

displaying two different types of cells in cellforitem swift

I have two arrays, one with Strings and one with a custom object.
I've created two different cells respectively. I've added the contents of the two arrays into a third generic array Any. I use the third array (combinedArray) in cellForItem.
var customObjectArray: [customObject] = []
var stringArray = [String]()
var combinedArray = [Any]()
if combinedArray[indexPath.row] is CustomObject {
cell1.LabelName.text = customObjectArray[indexPath.row].caption
cell1.iconView.image = customObjectArray[indexPath.row].icon
return cell1
} else {
let stringName = stringArray[indexPath.row]
cell2.LabelName.setTitle(stringName for: UIControlState())
return cell2
}
let's say customObjectArray has 13 objects and stringObjectArray has 17 objects. I want a separate counter for each array so that it populates them correctly. The way it's working now:
the combinedArray populates all of one type first (i.e. 13 customObjectArray first), then the next type second (i.e 17 stringObjects). The order the combined array isn't necessarily important as I will probably shuffle things around at some point before getting to cellforitem. So when cellforItem goes through the first 13 objects, indexpath.row = 14, and when it gets to the second type of object, it's skipping the first 13 objects and displaying stringObject's 14th element (obviously).
I can't figure out how to start at the beginning of the second array instead of indexPath.row's current position.
I might be totally off base here and likely should be using two sections or something to that nature, but I'm relatively new to iOS dev, so any guidance is appreciated.
Another option would be to enclose the different data types in an enum and add all the data to your combined array in whatever order you would like.
enum DataHolder {
case stringType(String)
case customType(CustomObject)
}
var combinedArray: [DataHolder]
This gives you a single type for the data and a way to distingue between the cell types.
Inside of the cellForItem perform a switch on the combinedArray
switch combinedArray[indexPath.row] {
case .stringType(let stringValue):
let cell = tableView.dequeueReusableCell(withIdentifier: "StringCell", for: indexPath) as! StringObjectCell
cell.labelName.text = stringValue
case .customType(let customData):
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomObjectCell
cell.labelName.text = customData.caption
cell.iconView.image = customData.icon
Use always only one array with a more specific type than Any
Create a protocol including all properties and functions both types have in common for example
protocol CommonType {
var labelName : String { get }
// var ...
// func
}
and make the types adopt the protocol.
Then declare the single array with that type to be able to add both static types
var commonArray = [CommonType]()
In cellForItem determine the cell type by conditional downcasting the type
let common = commonArray[indexPath.row]
if common is CustomObject {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomObject", for: indexPath) as! CustomObjectCell
cell.LabelName.text = common.labelName
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "Other", for: indexPath) as! OtherCell
cell.LabelName.text = common.labelName
return cell
}
indexPath math is cumbersome and error-prone.
If you want to display 2 different types of data in your UITableView you can simply create a wrapping enum whose cases will represent the type. By the case you will always know how to configure the cell.
Example:
Let's say you need to show Facebook posts in your UITableView and there are 3 types of posts: text, image and video. Your enum would like this:
enum Post {
case text(String)
case image(URL)
case video(URL)
}
Simple enough, right?
You need to have your UITableViewCell subclasses for each type of data (or single and configure it properly, but I recommend to have separate for many reasons).
Now all you need to do is keep array of Posts and in your UITableViewDataSource construct the cell you need.
let post = self.posts[indexPath].row
switch post {
case .text(let text):
let cell = // dequeue proper cell for text
// configure the cell
cell.someLabel.text = text
return cell
case .image(let url):
let cell = // dequeue proper cell for image
// configure the cell
cell.loadImage(url)
return cell
case .video(let url):
let cell = // dequeue proper cell for video
// configure the cell
cell.videoView.contentURL = url
return cell
}

Selecting Multiple Table View Cells At Once in Swift

I am trying to make an add friends list where the user selects multiple table view cells and a custom check appears for each selection. I originally used didSelectRowAtIndexPath, but this did not give me the results I am looking for since you can highlight multiple cells, but unless you unhighlight the original selected row you cannot select anymore. I then tried using didHighlighRowAtIndexPath, but this doesn't seem to work because now I am getting a nil value for my indexPath. Here is my code:
override func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! AddedYouCell
let currentUser = PFUser.currentUser()?.username
let username = currentCell.Username.text
print(currentCell.Username.text)
let Friends = PFObject(className: "Friends");
Friends.setObject(username!, forKey: "To");
Friends.setObject(currentUser!, forKey: "From");
Friends.saveInBackgroundWithBlock { (success: Bool,error: NSError?) -> Void in
print("Friend has been added.");
currentCell.Added.image = UIImage(named: "checked.png")
}
}
How can I solve this? Thanks
I'm not going to write the code for you, but this should help you on your way:
To achieve your goal, you should separate the data from your views (cells).
Use an Array (i.e. friendList) to store your friend list and selected state of each of them, and use that Array to populate your tableView.
numberOfCellsForRow equals friendList.count
In didSelectRowAtIndexPath, use indexPath.row to change the state of your view (cell) and set the state for the same index in your Array
In cellForRowAtIndexpath, use indexPath.row to retrieve from the Array what the initial state of the cell should be.

tableView.cellForRowAtIndexPath(indexPath) return nil

I got a validation function that loop through my table view, the problem is that it return nil cell at some point.
for var section = 0; section < self.tableView.numberOfSections(); ++section {
for var row = 0; row < self.tableView.numberOfRowsInSection(section); ++row {
var indexPath = NSIndexPath(forRow: row, inSection: section)
if section > 0 {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! MyCell
// cell is nil when self.tableView.numberOfRowsInSection(section) return 3 for row 1 and 2
// ... Other stuff
}
}
}
I'm not really sure what I'm doing wrong here, I try double checking the indexPath row and section and they are good, numberOfRowsInSection() return 3 but the row 1 and 2 return a nil cell... I can see my 3 cell in the UI too.
Anybody has an idea of what I'm doing wrong?
My function is called after some tableView.reloadData() and in viewDidLoad, is it possible that the tableview didn't finish reloading before my function is executed event though I didn't call it in a dispatch_async ??
In hope of an answer.
Thank in advance
--------------------------- Answer ------------------------
Additional explanation :
cellForRowAtIndexPath only return visible cell, validation should be done in data model. When the cell is constructed in
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
It should change itself according to the validation state.
As stated in the documentation, cellForRowAtIndexPath returns:
An object representing a cell of the table, or nil if the cell is not visible or indexPath is out of range.
Hence, unless your table is fully displayed, there are some off screen rows for which that method returns nil.
The reason why it returns nil for non visible cells is because they do not exist - the table reuses the same cells, to minimize memory usage - otherwise tables with a large number of rows would be impossible to manage.
So, to handle that error just do optional binding:
// Do your dataSource changes above
if let cell = tableView.cellForRow(at: indexPath) as? MyTableViewCell {
// yourCode
}
If the cell is visible your code got applied or otherwise, the desired Cell gets reloaded when getting in the visible part as dequeueReusableCell in the cellForRowAt method.
I too experienced the issue where cellForRowAtIndexPath was returning nil even though the cells were fully visible. In my case, I was calling the debug function (see below) in viewDidAppear() and I suspect the UITableView wasn't fully ready yet because part of the contents being printed were incomplete with nil cells.
This is how I got around it: in the viewController, I placed a button which would call the debug function:
public func printCellInfo() {
for (sectionindex, section) in sections.enumerated() {
for (rowIndex, _) in section.rows.enumerated() {
let cell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionindex))
let cellDescription = String(describing: cell.self)
let text = """
Section (\(sectionindex)) - Row (\(rowIndex)): \n
Cell: \(cellDescription)
Height:\(String(describing: cell?.bounds.height))\n
"""
print(text)
}
}
}
Please note that I'm using my own data structure: the data source is an array of sections, each of them containing an array of rows. You'll need to
adjust accordingly.
If my hypothesis is correct, you will be able to print the debug description of all visible cells. Please give it a try and let us know if it works.