Swift: Add empty element in table view (Core Data) - ios

I want to add a 'new element' cell to my UITableView so that after text typing and tapping 'Enter' the new entry must be created. Something like here but only in the end of the list. How can I do this? In my UITableViewController I use NSFetchedResultsController that handles operations with Core Data.

You will need to amend the tableView delegate and datasource methods to include the extra row:
Add another prototype cell to your storyboard to represent your "extra" row. Design it accordingly (subclassing if necessary) and assign it a different cell identifier.
Amend numberOfRowsInSection to return the FRC sectionInfo count plus one
Amend cellForRowAtIndexPath to dequeue with the different cell identifier if indexPath.row is equal to the FRC sectionInfo count (ie. the last row). Configure the cell accordingly.
Amend didSelectRowAtIndexPath to trigger the code to add the new entry.
Check/amend all other delegate/datasource methods (eg. editingStyleForRowAtIndexPath) to ensure they check the indexPath.row and handle the extra row appropriately.

Related

Difference between tableView.cellForRow(at:) and tableView.dataSource?tableView(tableView:cellForRowAt:)

I'm unit testing on a tableView whether it renders a cell.
And I found that tableView.cellForRow(at:) returns nil, while tableView.dataSource?tableView(tableView:cellForRowAt:) returns the right cell.
Here's my unit test code.
it("renders one option text") {
let indexPath = IndexPath(row: 0, section: 0)
let cell = sut.tableView.dataSource?.tableView(sut.tableView, cellForRowAt: indexPath)
let cell2 = sut.tableView.cellForRow(at: indexPath)
expect(cell?.textLabel?.text).toEventually(equal("A1")) // test suceeded
expect(cell2?.textLabel?.text).toEventually(equal("A1")) // test failed
}
So I'm curious about the difference of the two methods.
Apple's document says that tableView.cellForRow(at:) returns nil if the cell is not visible, so I'v understood that tableView.cellForRow(at:) returns nil when it's under unit testing,
but I'm not sure the time order of the two methods being called and when tableView.cellForRow(at:) get the right value(cell).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
This method is used to generate or dequeue the cells as required by tableView. This is not the UITableView member method. Instead, it is a protocol method and another object, which will be a data source, will implement and return the value. So it will always return a cell whether we are unit testing or while debugging the app.
tableView.cellForRow(at:)
This method is not the generator method. It is a member method of UITableView as a utility method for eg. for getting selected row we use tableView.selectedRow. So it is supposed to return cell for any indexPath.
As we know UITableView doesn't create cells equal to rows drawn. Suppose you wanted to draw 100 rows then UITableView only create few extra cells apart from cells which are visible. So if you pass any indexPath which is not among the visible rows then practically that cell doesn't exist. Because tableview is waiting for you to scroll and reuse the unused cells. So whether you are doing unit testing or working on app it will always show nil for cells which are not visible.
tableView.dataSource?tableView(tableView:cellForRowAt:) will always dequeue a new cell. It isn't the one on display unless tableView is the one that called it.

Does indexpath.row take care of iterating an array?

I'm working through a UITableView tutorial and I've become curious about array iteration as I implement the UITableViewDataSource methods. When we call indexPath.row, is this calling a for loop for us behind the scenes? I'm asking because months back when I was learning to use data from a webservice (I've forgotten the exact steps of how I did it precisely) but I believe I needed to iterate over the array in order to present the information in the console.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an instance of UITableViewCell, with default appearance
let cell = UITableViewCell.init(style: .value1, reuseIdentifier: "UITableViewCell")
// Set the text on the cell with the description of the item instance that is at the nth index of the allItems array, where n = row this cell will appear in on the tableview
let item = itemStore.allItems[indexPath.row]
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = "$\(item.valueInDollars)"
return cell
}
Am I correct in thinking that the indexPath.row method is iterating over the array for us behind the scenes?
let item = itemStore.allItems[indexPath.row]
No, calling indexPath.row does not iterate through all rows for you. It's just a variable sent to cellForRowAt that you can access and use to create your cell. It contains information about which row in which section the function cellForRowAt was called for.
However, cellForRowAt is actually called every time a cell is going to be visible on your tableVIew. Imagine you have a dataSource with 100 items and it is possible to only see 10 cells at a time. When the tableView gets initially loaded, cellForRowAt will get called 10 times. Then, if you scroll your tableView to show 3 more cells, cellForRowAt will get called 3 more times.
First of all the tutorial seems to be pretty bad. Table view cells are supposed to be reused
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
The workflow to display table view cells is:
The framework calls numberOfRowsInSection (and also numberOfSections)
For each section/row pair in the range of the visible cells it calls cellForRowAt and passes the index path in the second parameter.
No. The indexPath is just a struct with a section and a row. You use those to directly look up the information from your array. No iteration is happening.
cellForRowAt is only called for the cells that are on screen, so the order they are called depends on which direction you are scrolling.
In fact, the UITableView doesn't even know if you are using an array, or generating the information on the fly. What you do with the indexPath row and section is up to you.
Check the documentation of UITableView and cellForRowAtIndexpath functions.
An index path locating a row in tableView.
IndexPath will be a structure, which helps you to get the specific location from a nested array. In your case of the table view, rows inside the section.
cellForRow will return a cell for particular index path(in the specified section and row)
So indexPath.section will give the section number for which the cell is going to be modified and returned to the data source. And Inside the section, and indexPath.row will be the corresponding row inside that section.
index path documentation
tableView-CellForRowAt: IndexPath
UITableView DataSource Documentation
[update]
If you want to add multiple sections, add another dataSource numberOfSections, and return the number of sections count from it.
I can link to a create a table view tutorial from Apple, but it's a long document, you can skip the starting and read the To display a section in your table view.
If you want, you can use a 2D array for keeping sections and rows. And in numberOfSections method return array.count, and in numberOfRows:inSection, return array[section].count
More examples,
example
example
thanks,
J

using a Struct to change tableViewCell from TableViewController

In my app's chat feature, I use a UITableView to present chat history.
Cells are subclasses of UITableViewCell. The class receives a Chat object that contains all the info it needs to build the cell.
I have a Firebase listener that fires when the "last message" is changed. It changes a UILabel text to "new Message!" and then a protocol/delegate reloads the table view. Now I need to change the label back to "".
When the user clicks the cell, I use didSelectRowAt to segue to JSQMessagesViewController after preparing for segue. My idea is to use prepare for segue or even didSelectRowAt itself to change a Boolean variable in the UITableViewCell that corresponds to that chat Object. Navigating back to the tableViewController will trigger reloadData.
Each ChatObject has a unique ID that I can access. After firing the listener, I will set this Boolean to one value. After clicking the cell, I will set it to another. the state of the variable will tell the IBOutlet what text to show. In the UITableViewCell class, I use:
var chat: Chat! {
didSet {
self.updateUI()
}
}
}
So in self.updateUI() I make necessary changes, but can I make properties in the UITableViewCell mutable/visible from the UITableViewController?
How can I make properties in the UITableViewCell that are mutable from the UITableViewController
You don't. You make changes in your model and tell the table view to reload its data.
So in self.updateUI() I make necessary changes, but can I make properties in the UITableViewCell mutable from the UITableViewController?
I don't think you're trying to do what you say you are. In the case that you are trying to access the properties of your custom UITableViewCell subclass, you might want to use cellForRowAtIndexPath(). Then cast the result to whatever class name you have for your UITableViewCell subclass.
Example:
If I want to change the 5th row outside of our table view dataSource, I could do something like this.
let fifthIndexPath = IndexPath(row: 5, section: 0)
let thisCell = myTableView.cellForRowAtIndexPath(at indexPath: fifthIndexPath) as! MyCustomTableViewCell
thisCell.property = value
See:
https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614861-tableview

Create IBAction for CollectionViewCell

I've got a collection view with multiple cells created programically. I want to update a variable to a different value when the user taps a specific cell. So eg: Cell 1 is tapped -> var test = "cell1" , cell2 is tapped var test = "cell2". Usually I'd just create an IBAction by dragging from the storyboard but I'm not sure how to do it in this case.
I'm using Swift 3.1
To add interactivity to UITableViews, UICollectionViews, and other kinds of views which display collections of data, you can't use Storyboard actions, as the content is generated dynamically during runtime, and the Storyboard can only work for static content.
Instead, what you need to do is set your UICollectionView's delegate property to an object that implements the UICollectionViewDelegate protocol. One of the methods defined as part of the protocol is the collectionView(_:didSelectItemAt:) method. This method will get called whenever the user selects (taps) a collection view cell with the IndexPath to that cell as an argument. You can update your variable in that method. Just remember to deselect the cell after handling the tap by using the deselectItem(at:) method on your UICollectionView.
There are UICollectionView delegate that you need to implement. It goes like this
did select item at index path will give index path of the cell that was selected. Using indexpath or any other property of the data source array you are using, you can modify the variable value.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if let cell = collectionView.cellForItemAtIndexPath(indexPath) {
//check your condition and modify the variable value depending on index path or any other property you are referring to.
}
}

iOS tableview cell is empty at random

Screenshot of weird behavior
The screenshot tells is quite well. I have a tableview with dynamic custom cells. I added a println for one of the contents of the cell to check, if the labels are set. I can see in the debug log, that each cell has its content. Still, on the device there are empty cells at random, which means, the row, where no content appears, is changing a lot. Even just scrolling up and down makes the second row disappear, but the third row is filled. Scrolling again turns this around again. If I close the app and start it again, every row is filled correctly.
Here is the code for the cell generation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Return a count picker cell
if countPickerTableRow == indexPath.row {
...
}
// Return a normal wish list entry cell
else {
let article = wishListEntries[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! WOSArticleCell
// Correct the order in case a count picker cell was inserted
var row = indexPath.row
if countPickerTableRow != -1 && indexPath.row > countPickerTableRow {
row--
}
cell.setThePreviewImage(UIImage(data: article.thumbnail))
cell.setArticleName(article.name)
cell.setArticleDescription(article.text)
cell.setArticleNumber(article.number)
cell.setArticleCount(article.count as Int)
cell.setOrderInTable(row)
cell.setTableViewController(self)
cell.setNeedsDisplay()
cell.setInputAccessoryView(numberToolbar) // do it for every relevant textfield if there are more than one
println(String(indexPath.row) + " " + cell.nameLabel.text!)
return cell
}
}
In the custom cell class there is nothing special. Just a few outlets to the labels.
Here is a screen of the storyboard:
Storyboard
Can anyone please help me finding out what is going on here? I can't find the reason why the debug log can output the contents of a cell, but the device is not able to render them.
You should change the logic of your code. If the PickerCell comes up just call reloadData() and reload everything in the tableview. If the amount of rows you have is small this won’t be an issue and it’s not an expensive operation as you are not doing any heavy calculating during display.
If you need to update only a single cell because of changes you made in the PickerCell then you should be calling reloadRowsAtIndexPaths:withRowAnimation: with the indexPath of the cell to be updated.
Your issue is with your subclass WOSArticleCell. Have you implemented prepareForUse()? If you have, are you setting any properties to nil?
UITableViewCell Class Reference
Discussion
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.

Resources