Calling reloadData through delegate - ios

I have a table view which conforms to custom protocol FoodItemProtocol and it implements its funciton:
func foodItemWasTaggedAsFavorite() {
tableView?.reloadData()
print("foodItemWasTaggedAsFavorite")
}
After foodItem is tagged as favorite, this function is called and print statement is executed, however table view is never reloaded.
I realized I don't actually need to use delegation for this, it works fine if I call to reloadData() in viewDidAppear(). But still I'd like to know why it's not working through delegation? I've even tried to call reloadData() on main thread like this:
dispatch_async(dispatch_get_main_queue()) {
tableView?.reloadData()
}
But I got same result.

If you are calling the delegate method from a different view controller, tableView will be nil.
To check this, modify foodItemWasTaggedAsFavorite to be:
if let tableView = tableView {
tableView.reloadData()
print("foodItemWasTaggedAsFavorite")
}
Now check if the print statement is being printed. I'm pretty sure it won't, because tableView is nil.
However, in viewDidAppear:, the table view has already been loaded, so it isn't nil.
Also, there is no reason to reload the data if the table view isn't on screen anyways.

Related

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.

Calling a function once after tableview.reloadData

Every time I reload my table by calling
tableview.reloadData()
I want to call a specification function like
myFunction()
I was wondering, instead of stacking these two function next one after another everywhere in my code like
tableview.reloadData()
myFunction()
Is there a smart and clean way of calling myFunction every time tableview reloads?
There is no delegate method to give you a callback when reloadData() has completed, but to make it cleaner you could do a couple of different things.
You could create your own function like this:
func reloadTable() {
tableView.reloadData()
myFunction()
//plus anything else you want to accomplish
}
Then you call that function everywhere in one line instead of repeating your code.
Alternatively, you could subclass UITableView and override the reloadData() method, adding your additional functionality.
one way is to use inheritance. just implement your own tableview class and reload the reloadData function of UITableView.
class YourTableView: UITableView {
override func reloadData() {
super.reloadData()
myFunction()
}
func myFunction() {
//do something
}
}
then declare YourTableView instead of UITableView

Why is my custom collection view cell unexpectedly nil after selecting cell in code?

So I have a colectionView of images, and when something happens in the background, I might try to select a specific custom collectionViewCell using the method:
self.collectionView.selectItemAtIndexPath(indexPathToReload, animated: true, scrollPosition: UICollectionViewScrollPosition.CenteredVertically), which works fine, the collectionView scrolls to the desired location.
However, if I then try to actually update the appearance of the cell as it's been updated by calling self.collectionView(self.collectionView, didSelectItemAtIndexPath: indexPathToReload) I get an unexpectedly nil cell when I then try to create the cell in didSelectItemAtIndexPath.
I partially understand why this method of updating cells is unsafe (as I've read elsewhere in researching this question like here in one of the answers.)
Thus, the crash makes me assume that cells are not part of the visible cells on the screen, which is why the cell is nil when I try to create it. But this doesn't make sense as I also assume that the cells have to be created in order to be scrolled to, which as I said works fines because they are created as expected and can be interacted with without issue.
So why is my cell nil? Or why is my collection view not thinking that the cell that was scrolled to not part of the visible cells? And if the reason is obvious, then how can I make a cell update it's appearance when I select it in code?
EDIT: Code in context
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.selectItemAtIndexPath(indexPathToReload, animated: true, scrollPosition: UICollectionViewScrollPosition.CenteredVertically)
self.collectionView(self.collectionView, didSelectItemAtIndexPath: indexPathToReload)
return
}
As I've said, this pretty much is the context. In the first line I may scroll to an index that is not visible on the screen. If I do this, and then the second line of code executes, the cell that is created in the delegate method that is called is unexpectedly nil.
In order to fix this issue, I had to use kind of a hacky workaround, which although works seems almost too dirty, and I don't know why this issue hasn't been addressed by Apple (i.e why selectItemAtIndexPath doesn't call the delegate method didSelectItemAtIndexPath). Anyways, what I ended up doing was when I needed to update my selected cell in background, I first got the index and set a bool to show a cell was selected in code:
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.selectItemAtIndexPath(indexPathToReload, animated: true, scrollPosition: UICollectionViewScrollPosition.CenteredVertically)
let cell = self.collectionView.cellForItemAtIndexPath(indexPathToReload) as? ListingCollectionViewCell
if cell != nil {
self.collectionView(self.collectionView, didSelectItemAtIndexPath: indexPathToReload)
return
} else {
self.buttonSelectedInCode = true
self.indexPathSelectedInCode = indexPathToReload
return
}
}
Above, I had to try to create the cell for the specified index path. If the cell isn't nil then I know that the cell is visible, and it's safe to call on the delegate didSelectItemAtIndexPath. However, if the cell is nil, then I have to set up my bool and index, and wait for the scroll view to call the delegate method, as shown below.
Then, I further implemented scrollViewDidEndScrollAnimation and used the call of this delegate method to then select my cell in code, as follows:
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
if buttonSelectedInCode {
self.collectionView(self.collectionView, didSelectItemAtIndexPath: self.indexPathSelectedInCode)
}
}

ios swift parse: methods with async results

When I go to a viewController I call within my viewDidAppear Method a function:
override func viewDidAppear(animated: Bool) {
getLessons()
}
This methods loads from parse.com a list of data I want to use in a pickerView.
The function itself:
func getLessons(){
var query = PFQuery(className:"Lesson")
query.orderByAscending("name")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
var name = object["name"] as String
self.languagePickerKeys.append(object.objectId)
self.languagePickerValues.append(name)
self.selectedLanguage.text = self.languagePickerValues.first // set the first lessons name into the text field
self.selectedLessonObjectId = self.languagePickerKeys.first // set the first objectId for the lesson
self.languagePicker?.reloadAllComponents()
}
} else {
// Log details of the failure
println("\(error.userInfo)")
}
}
println("getLessons done")
}
The thing is, that the textfield is empty, as the getLesson() gets the data async and the data is not available to the textfield.
I also tried to put the getLesson into the viewDidAppear method, but this doesn't help me, the textfield is empty anyway.
What can I do, to have the data from the getLessons() method ready and loaded its first value into my textfield when the view is shown to the user?
You certainly have to get the data from asyncTask before setting it to pickerView.
Here's the ViewController lifecycle after instantiation:
Preparation if being segued to.
Outlet setting
Appearing and Disappearing.
So, you have two options:
Load the data in previous ViewController and then perform the segue. You need to follow these steps for it.
a. Create a segue from previous ViewController to your ViewController.
b. Call the function when you want to go next ViewController which fetches the data, and the end (after getting the data) call performSegueWithIdentifier which will lead to your ViewController.
c. Set the data in prepareForSegue
let navigationController = segue.destinationViewController as UINavigationController
navigationController.data = yourData //you got from async call
Now when you reach your ViewController, you are sure that your data is present, and you can set it to your pickerView.
If you want to do it in the same ViewController: here's is the lifeCycle of ViewController:so you need to call your function in viewDidLoad, and always set your pickerView after completion of the async network call.
Make sure that you initiate all changes to the UI from the main thread e.g. like so:
dispatch_async(dispatch_get_main_queue(), {
selectedLanguage.text = languagePickerValues.first
self.languagePicker?.reloadAllComponents()
})
The problem is that findObjectsInBackgroundWithBlock is an asynchronous method, so even if you fire it in the ViewDidLoad you will never know when you will receive the response data and you can't be sure that the data will be ready by the time you view appear.
I think you have just 2 possibility:
The first one is to load the data in the previous view controller and then just pass the data that got ready to you view controller.
The second is to use a synchronous method (the findobject method maybe?) and put the call in a method that is fired BEFORE the view appear (like the viewWillAppear: method). But your view will stuck for a moment (I think) while the data is retreiving... However this second solution probably resolve your problem but using synchronous method to retrieve data from a slower data source is usually bad design solution.
D.

Resources