UICollectionView state restoration: restore all UICollectionViewCells - ios

I searched a lot through Google and SO, so please forgive me, if this question has already been answered!
The problem:
I have a UICollectionView with n UICollectionViewCells. Each cell contains a UIView from a XIB file. The Views are used for data entry, so all cells have a unique reuseIdentifier. Each View has also a unique restorationIdentifier. Everything works in normal usage, but not when it comes to state restoration:
The first 3 or 4 cells are getting restored properly because they are visible on the screen on startup, but the remaining cells, which are not visble, are not getting restored.
Current solution:
So I've discovered so far that a View is only restored if it's added to userinterface at startup.
My current working solution is to set the height of all cells to 1 in the process of restoring. Now every cell is loaded and all views are restored.
When applicationFinishedRestoringState() is called, I reload the CollectionView with the correct height.
Now my question is: I'm not happy with this solution, is there a more clean way to achieve restoring of all the UIViews?

I think you are getting a bit confused between your data model and your views. When first initialised, your table view is constructed from a data model, pulling in stored values in order to populate whatever is in each cell. However, your user does not interact directly with the data model, but with the view on the screen. If the user changes something in the table view, you need to signal that change back up to the view controller so that it can record the change to the data model. This means in turn that if the view needs to be recreated the view controller has the information it needs to rebuild whatever was in the table when your app entered the background.
I have put together a simple gitHub repository here: https://github.com/mpj-chandler/StateManagementDemo
This comprises a CustomTableViewController class which manages a standard UITableView populated with CustomTableViewCells. The custom cells contain three switch buttons, allowing the state of each cell to be represented by an array of Boolean values.
I created a delegate protocol for the cells such that if any of the switches is tripped, a signal is sent back to the view controller:
protocol CustomTableViewCellDelegate {
func stateDidChange(sender: CustomTableViewCell) -> Void
}
// Code in CustomTableViewCell.swift:
#objc fileprivate func switched(sender: UISwitch) -> Void {
guard let index : Int = switches.index(of: sender) else { return }
state[index] = sender.isOn
}
// The cell's state is an observed parameter with the following didSet method:
fileprivate var state : [Bool] = Array(repeating: false, count: 3) {
didSet {
if state != oldValue, let _ = delegate {
delegate!.stateDidChange(sender: self)
}
}
}
CustomTableViewController is registered to the CustomTableViewCellDelegate protocol, so that it can record the change in the model as follows:
// Code in CustomTableViewController.swift
//# MARK:- CustomTableViewCellDelegate methods
internal func stateDidChange(sender: CustomTableViewCell) -> Void {
guard let indexPath : IndexPath = tableView.indexPath(for: sender) else { return }
guard indexPath.row < model.count else { print("Error in \(#function) - cell index larger than model size!") ; return }
print("CHANGING MODEL ROW [\(indexPath.row)] TO: \(sender.getState())")
model[indexPath.row] = sender.getState()
}
You can see here that the function is set up to output model changes to the console.
If you run the project in simulator and exit to the home screen and go back again you will see the state of the tableView cells is preserved, because the model reflects the changes that were made before the app entered the background.
Hope that helps.

Related

Swift TableView Contents Disappear and Flash When Reloaded

I'm building a chat app with a table view of various channels that people can chat on.
If a new message (channel) comes in, I update my data store, and then reload the table view. In this particular instance, if a user accepts an invite to a channel; that channel gets added to the data store.
Every time a new channel gets added, this method gets called:
#objc func processChannels() {
if self.airlockStore.unPinnedChannelDataObjects.isEmpty == true {
return
} else {
let pinnedSection = self.tableViewAdaptor!.sections[0] as! TableViewAdaptorSection<ChannelTableViewCell, ChannelDataObject>
let unPinnedSection = self.tableViewAdaptor!.sections[1] as! TableViewAdaptorSection<ChannelTableViewCell, ChannelDataObject>
unPinnedSection.items = airlockStore.unPinnedChannelDataObjects
pinnedSection.items = airlockStore.pinnedChannelDataObjects
}
updateTableView()
}
And subsequently, this one gets called:
#objc func updateTableView() {
tableViewAdaptor?.tableView.isUserInteractionEnabled = true
tableViewAdaptor?.tableView.reloadData()
}
However, I consistently get a flash / the contents of the table view temporarily disappear before then being displayed in the manner expected.
Have read several posts regarding flashes when loading a table view, but haven't been able to solve this yet.
I was looking at reloading individual rows in the table view, but the problem is; there's no guarantee of where the index in my list of channels a new channel will be added to.
Have you tried not reloading after every update of tableview?
On the other hand, it may not be important but why you set isUserInteractionEnabled = true so many times.
Use this before and after adding/deleting row in table view:
tableview.beginupdates()
// adding/deleting row
tableview.endupdates()

How to retain previous selection in collectionview after reload in Apple TV

Hi in my Apple TV application i have one left collectionview right collectionview.Like splitview.When ever i focus cell on left data will refresh on right and when i select any cell in right collection view i am refreshing left and right collectionviews with new data (Like next level).And when on click on menu i will refresh both collectionviews with old data (Like coming to previous level). I want to highlight cell in left collectionview with red colour but i am reloading left collectionview while going forward or coming backward so always first cell is highlighting with Red colour. Can anyone suggest how to maintain previous selection in left collection-views because i am using only one collectionview for left menu and just reloading data.
The easiest way to retain focus in a UITableView or UICollectionView is to use UICollectionView.remembersLastFocusedIndexPath = true. This will automatically restore focus to the last focused item in a collection/table view and also automatically focus on the first item if there was no previously focused item or if the collection view data is reloaded.
If you need more control, the next level is to set UICollectionView.remembersLastFocusedIndexPath = false and use UICollectionViewDelegate.indexPathForPreferredFocusedView from your UIViewController instead. This method is only called whenever focus changes to a collection view programmatically though (but not if focus changes to a collection view as a result of TV remote interaction).
Now to ensure that indexPathForPreferredFocusedView is called when you switch between the left and right collection views using a TV remote, you will need to intercept shouldUpdateFocusInContext to override focus switches between the left and right collection view programmatically:
override func shouldUpdateFocusInContext( ... ) -> Bool {
if let nextView: UIView = context.nextFocusedView, let previousView: UIView = context.previouslyFocusedView{
if (nextView.isDescendant(of:leftCollectionView) && previousView.isDescendant(of:rightCollectionView)){
setFocusTo(leftCollectionView) // will invoke delegate indexPath method
return false // prevent system default focus change in favor of programmatic change
}
else if (nextView.isDescendant(of:rightCollectionView && previousView.isDescendant(of:leftCollectionView){
setFocusTo(rightCollectionView) // will invoke delegate indexPath method
return false
}
}
return true
}
internal var focusedView: UIView?
internal func setFocusTo(_ view:UIView){
focusedView = view
setNeedsFocusUpdate()
}
override var preferredFocusEnvironments -> [UIFocusEnvironment]{
return focusedView != nil ? [focusedView!] : super.preferredFocusEnvironments
}
func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
...
}
Alternatively, instead of using setFocusTo( collectionView ) + indexPathForPreferredFocusedView, you can just use setFocusTo( collectionViewCell ). Overriding indexPathForPreferredFocusedView is more robust though since it catches all cases where focus shifts for reasons other than user interaction (ex: system focus update due to an alert displaying + dismissing)

Who is calling reloadData method for UITableView

It can sounds weird but I don't understand why my tableView is showing cells.
I got array of items that should be shown in cells but I don't run reloadData method of my tableView anywhere in my code. It seems that some of app components or maybe frameworks inside app is calling reloadData method and I want to find out which one?
How it can be done?
A table view loads itself the first time it is added to the window hierarchy. You don't need an explicit call to reloadData for the table to load itself initially.
If you want to see how this is really done, put a breakpoint on your table view data source methods and bring up your table view. Look at the stack trace in the debugger to see the sequence of events.
If your data preparation takes some time and you do not want the table view to show any data initially you could use an approach like this:
class TableViewController: UITableViewController {
var someDataSource: [Any]!
var dataSourcePrepared = false {
didSet {
tableView.reloadData()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
guard dataSourcePrepared else { return 0 }
return someDataSource.count
}
func doSomePreparationStuff() {
// ...
// ...
someDataSource = ["Some", "Content"]
dataSourcePrepared = true
}
}
In this case I used a Bool variable dataSourcePrepared which is false initially. As soon as you have prepared your content set it to true and the table view gets reloaded.

Clearing Firebase observations from a UITableViewCell

In all iOS classes that use Firebase you will have code like this,
private func clearObservations() {
// your method for clearing observations, probably something like
blah blah. removeAllObservers()
}
In view controllers, it's essential that you call this in viewWillDisappear (or viewDidDisappear)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
clearObservations()
}
That's fine.
Assume that you have created an observation in a UITableViewCell.
What is the best place in a cell to "clear observations" ?
Note that prepareForReuse is useless, try it.
The only approach we've found is
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
clearObservations()
}
super.willMove(toSuperview: newSuperview)
}
Seems flakey/bizarre though.
What's the deal on this?
Update
Note while "XY Answers" are interesting and informative, if anyone knows the answer to the question that would be great also!
Preface
This was an attempt to answer the question but the question was misunderstood. I'll leave it here as it does have some relevance regarding observers, handles and tableView cell interaction.
While you can go through those gyrations, it's not really needed in most use cases.
For example, if you add and observer to a node, there wouldn't necessarily be a someRef? variable hanging around. So here we are watching the Posts node for new posts
let postsRef = self.ref.child("Posts")
postsRef.observe(.childAdded, with: { snapshot in
print(snapshot) //add the post to the dataSource and reloadTableview/cell
})
Here's another example of watching for any posts that are changed by uid_2
let postsRef = self.ref.child("Posts")
let queryRef = postsRef.queryOrdered(byChild: "poster_id").queryEqual(toValue: "uid_2")
queryRef.observe(.childChanged) { (snapshot) in
print(snapshot) //change the post in the dataSource and reloadTableview/cell
}
No class vars are needed for this functionality and nothing needs be nil'd. The point here being that you do not have to have class vars to get observing functionality and you do not need to keep a handle for every observer (keep reading)
In view controllers, it's essential that you call this
(someRef?.removeAllObservers()) in viewWillDisappear (or Did)..
will use Firebase in the cells of tables.
To clarify; I wouldn't want to put Firebase observers in the cells of tables. The observers should be in whichever viewController controls the tableView that has cells. Cells should pull data from the dataSource array (which is backed by Firebase)
There are some circumstances where you may want to remove all observers, again no need to have a class var or a need to nil a var.
let postsRef = self.ref.child("Posts")
postsRef.removeAllObservers()
There are times when a specific observer needs to be removed (in the case where a node has observers on it's child nodes for example), and in those cases, we store a handle to that observer as say, a class var (keeping them in an array is a tidy way to do it)
class ViewController: UIViewController {
var myPostHandle : DatabaseHandle?
func addObserver() {
let postsRef = self.ref.child("Posts")
self.myPostHandle = postsRef.observe(.childAdded, with: { snapshot in
print(snapshot)
})
func stopObserving() {
if self.myPostHandle != nil {
let postsRef = self.ref.child("Posts")
postsRef.removeObserver(withHandle: self.myPostHandle) //remove only the .childAdded observer
}
}
}
Again though, once the observer is removed, the handle would go out of scope once the class closes.
Tableviews that contain cells are backed by a dataSource and that dataSource get's it's data from firebase. When something is added, changed or removed from Firebase, your app is notified and the array is updated and then the cell refreshed. No need for an observer in the cell itself.
There's no need to add dozens of observers (in the cells) - add one central observer and let it keep the array current. Refresh tableView only when something changes.
EDIT
To Address a comment regarding the use of removeAllObservers: code is worth 1000 words:
Create a new Firebase project with two button actions. Here's the code for button0 which adds an observer to a node:
func button0() {
let testRef = self.ref.child("test_node")
testRef.observe( .value) { snapshot in
print(snapshot)
}
}
when this button0 is clicked, from there on, any adds, changes, or deletes to the test node will print it's contents to the log.
func button1() {
let testRef = self.ref.child("test_node")
testRef.removeAllObservers()
}
This will remove all observers for the node specified. Once clicked, no events will print to the console.
Try it!
It is not right to clear observations in cell and therefore there is not a best place to do it in cell, because, firstly, this approach contradicts MVC pattern. Views only responsible for displaying content and they should only contain code that describes how they must be draw. And in the view controller you give the content for showing by views. Usually content has provided by your model. So controller connects views and model. In your case, when you place clearObservations() in cell class, you also have someRef as a class property, so you have a model in your view class and this is incorrect.
Secondly, if you try to clear observations in table cell you definitely make logic of showing some content in table in wrong way. Cell only show data that has to be generated by some object that conforms to UITableViewDataSource protocol and implements protocol methods. For instance, in cellForRow method you generate cell and setup it with some content from array. This array is generated from model (Firebase service). Your view controller may be this data source object. You have to include array property to controller class and someRef, than you fill array and reload table data. If controller's view disappeared you clear observations, but you do it only inside view controller (in viewWillDisappear()).
Overall, all manipulations with someRef you should do in view controller and therefore "clear observations" also inside controller.

Save a UITextField into a UICollectionViewCell

I have an app that allows the user to create categories. A save screen appears with a UITextField. Although, I do not know how to save the user's entry, and allow a new UICollectionViewCell to be added into the current UICollectionView that contains the UITextField words that the user typed. Thanks!! This is also in Swift. This would be used in a UITableView: clothes.name = self.nameTextField.text ,but how would I convert this to a UICollectionView? **clothes is a variable
Your collection view has a data source where you initially have the model for your cells. In your case it's probably an array. The collection view looks at this data source and returns cells based on the entries contained in this data source.
What I am trying to get at is the following:
In order to show the newly created cell, you'll need to update your data source (I'll call it "the blueprint specification" the collection view adheres to) by adding the new cell's "specification" (I think I'm wording this in a more complex fashion than it actually is). Here is an example (If we assume that your data source is an array):
categoryArray.append(CategoryModel(title: yourTextField.text)) // update the data source
After you make changes to the data source you can tell the collection view to reload it's data:
collectionView.reloadData()
That will "parse" the data source to display the cells anew.
Let me know if anything is unclear.
EDIT
Regarding your comment -- given that you use a UINavigationController -- , you can achieve displaying the title like this:
You are probably using this method to go to your next view controller:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let category = categoriesArray[indexPath.row]
let detailVC = DetailViewController()
detailVC.title = category.title // your title
self.navigationController?.pushViewController(detailVC, animated: true, completion: nil)
}
Alternatively, in your DetailViewController you can do:
class DetailViewController : UIViewController{
var category : Category!
override func viewWillAppear(){
super.viewWillAppear()
self.title = self.category.title
}
}

Resources