RunLoop with UITableView lock up - iphone

I'm writing an iPad app and have a problem with ui responsiveness / lock up.
I have a UITableView with items, when an item is clicked, it goes out on the internet and fetches that item and displays it. The rest of the program (not shown) will use the item downloaded, so only one item can be downloaded at a time.
Fetching the item can take a long time. During that time I want the user to still be able to scroll around the UITableView, but not to be able to select anything until the previous item clicked has been downloaded.
I know this can be done using threads,blocks, and callbacks, but right now I don't have time to do that (impossible time constraint).
I thought a quick way would be to download it sequentially and use run loops and a flag like this two step process:
When the user clicks on a table cell didSelectRowAtIndexPath is called, in there I set a global flag, then call the download method to download the item. If the user clicks on another item (before the download is completed, it will see the flag checked and exit that function without downloading anything. Basically this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
static BOOL alreadyInHere=FALSE;
if (alreadyInHere) return;
alreadyInHere=TRUE;
....
downloadItem(...);
ShowAndUseItem(...);
alreadyInHere=FALSE;
}
This allows the user to select only one item at a time.
To allow the user to be able to still scroll around the UITableView during the long download, I put in a run loop in the downloadItem(...) method shown above like this...
-(void) downloadItem(....)
{
BOOL downloading=TRUE;
callFunctionsToStartdownload(...); //
while (downloading) {
downloading=DownloadSomeBytes(...);
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, YES);
}
}
The result of (1) and (2) is that the user can still scroll around the UITableView during the sequential download, the alreadyInHere flag prevents them from selecting something and starting another download.
This works in most cases, but around 50% of the time, during the download, the UITableView becomes unresponsive (can't scroll to other items in the table), and even after the download didSelectRowAtIndexPath is never called again when you click on something making theUITableView basically locked up.
My question is, did I do the runLoop correctly?
I know there are other ways to do it, but I have to do it using this general method for now because of other reasons.
Thanks

You should not try to download or do any other potentially long activity in youR tableview didSelectRow method. Exit (return) from this UI method after setting up an asynchronous download so that you don't lock up the UI.
Locking out simultaneous downloads is still ok, but you want be careful to reset state after errors or timeouts.

Related

Perform time consuming tasks inside UITableViewCell, pausing on scrolling

I have TableView with customs cell representing events. It looks very close to first and third image here.
As you can see (sorry for small resolution) on some events there are photos of friends that are going to participate.
Unfortunately information about friends is not loaded with other information about events.
So after I got list of events I can make request to load list of friends that are going to participate in each event.
Right now I use code like
class EventCell : UITableViewCell {
var eventForCell : Event? {
didSet {
eventTitleLabel.text = eventForCell.title
eventDateLabel.text = eventForCell.date
presentFriends(eventID : eventForCell.id)
}
}
func presentFriends(eventID : Int) {
//searching for friends for event with specific eventID
.......
.......
//for every friend found adding UIImageView in cell
for friend in friendsOnEvent {
let avatar = UIImageView(....)
self.addSubview(avatar)
}
}
}
This code works but photos are not presented in smooth way. Also if you scroll list fast they start to blink. Maybe it is even not necessary to load them if user scrolls fast list of events. So I have two questions:
How can I make smooth scrolling experience taking in consideration that presenting friends for every event can take time and sometimes it finishes after cell was scrolled away.
If I had loaded list of events and already presenting cells with them. How can I update those cells after I get information about friends that are going to participate?
When user is scrolling and I am creating async tasks to display some images in cell I think I should use weak reference to self and maybe check it not to equal nil so task would be canceled if cell is not visible now. How should it be done?
Update:
I found info about tableView(_:prefetchRowsAt:) method inside UITableViewPrefetchingDataSource protocol, should I use it for this case? Maybe someone has some experience with it?
1. (Re)creating a view objects during cellForRowAt is generally a bad practice. From the screenshot I assume that there is a limit to how many avatars are there on a single cell - I would recommend creating all the UIImageView objects in the cell initializer, and then in presentFriends just set images to them, and either hide the unused ones (isHidden = true) or set their alpha to 0 (of course, don't forget to unhide those that are used).
2. If you are using SDWebImage to load images, implement prepareForReuse and cancel current downloads to get a bit of performance boost during scrolling, and prevent undefined behaviour (when you try to set another image while the previous one was not yet downloaded). Based on this question, this one and this one I would expect something like:
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.sd_cancelCurrentImageLoad()
}
Or you can use [this gist][4] for an inspiration.
P.S.: Also, you will have to count with some blinking, since the images are downloaded from web - there will always be some delay. By caching you can get instantly those that were already downloaded, but new ones will have delay - there is no way to prevent that (unless you preload all the images that can appear in tableView before presenting tableView).
P.S.2: You can try to implement prefetching using [UITableViewDataSourcePrefetching][6]. This could help you out with blinking caused by downloading the avatars from web. This would make things a bit more complicated, but if you really want to remove that blinking you will have to get your hands dirty.
First of all, as you can see from the documentation, prefetchRowsAt does not give you a cell object - thus you will have to prefetch images to your model object instead of simply using sd_setImage on the UIImageView object at a given cell. Maybe the aforementioned gist would help you out with downloading images to model.
Now also as the documentation states:
Loading Data Asynchronously
The tableView(_:prefetchRowsAt:) method is not necessarily called for every cell in the table view. Your implementation of tableView(_:cellForRowAt:) must therefore be able to cope with the following potential situations:
Data has been loaded via the prefetch request, and is ready to be displayed.
Data is currently being prefetched, but is not yet available.
Data has not yet been requested.
Simply said, when you are in cellForRowAt, you cannot rely on prefetchRowsAt being called - it might have been called, and it might not have been called (or maybe the call is in progress).
Documentation continues to suggest using Operation as a backing tool for downloading the resources - prefetchRowsAt could create Operation objects that would be responsible for downloading avatars for a given row. cellForRowAt can then ask the model for the Operation objects for the given row - if there are not any, it means that prefetchRowsAt was not called for that row, and you have to create Operation objects at that point. Otherwise you can use the operations created by prefetchRowsAt.
Anyway, if you decide that it is worth it, you can use this tutorial as an inspiration for implementing prefetching.
You can use UITableViewDataSourcePrefetching as you mentioned.
It's a protocol that calls your prefetch data source when some cells are going to be displayed but are not on the screen yet.
This way you can prepare all the resources that takes time to load before they are presented.
You just have to implement:
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath])
and fetch the data related to the cells from all of the indexpaths.
Beware it's only available since iOS10.
I personally use AlamofireImage with a cache so images that have already been downloaded aren't fetched twice, there's plenty of alternatives but it's a good practice to use cached images on this kind of scenario.

What is a UIGobblerGestureRecognizer?

I have just a regular UITableView, and I ran this code:
UITableView *tableView = [[UITableView alloc] init];
for(UIGestureRecognizer *gesture in tableView.gestureRecognizers)
{
NSString *className = NSStringFromClass([gesture class]);
NSLog(#"ClassName:%#", className);
}
One of the output lines is: ClassName:UIGobblerGestureRecognizer
Surprisingly Google has nothing on this. Anyone have any idea what it is?
Most likely this is an internal class that Apple uses. I've come across custom subclasses of UIGestureRecognizers that Apple created for some specific use. I'm sure they have needed to create custom gesture recognizers for various reasons, just as I have and not all of those classes are exposed for us to use.
Check out http://oleb.net/blog/2013/02/new-undocumented-apis-ios-6-1/
BJ Homer believes UIGobblerGestureRecognizer is used to avoid
recognition while animations are in progress. Otherwise, it’s
inactive. In an interesting Twitter conversation, Filippo Bigarella
and Conrad Kramer discovered that UIGobblerGestureRecognizer can
“gobble” touches in order to prevent other gesture recognizers from
receiving them in certain situations. What situations those are, I
don’t know.
I'm very sure it is used to prevent normal interaction while a particular cell is showing a delete confirmation button, and recognise any touch down as triggering that cell to return to a non-editing state.
It has this method and I'm assuming that excludedView is the cell that is showing a delete confirmation button, since you can normally still interact with cells in this state.
- (id)initWithTarget:(id)arg1 action:(SEL)arg2 excludedView:(id)arg3;
https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UIGobblerGestureRecognizer.h
In short, from what I've read and what my experiments have shown, the "gobbler" seems to gobble up the swipes and touches on a table view (actually table cells) when a state transition (initiated by the user's touch or swipe) is in progress, so that the state transition can be completed before the user can touch the table again. Apple may use it in other cases but it is on the table view that I have observed the gobblers.
Now the long story: Suppose your table view implements a "drawer" on the table cell, like Apple's mail app or message app. When you open the drawer with a back swipe and take an action on any of the buttons in the drawer, all is well. But if you just close the draw with a forth swipe, you'll likely find that your next back swipe on a random cell doesn't work. But if you keep doing the back swipes, the next swipe usually will work again to show the drawer. Simply put, if you just open and close the drawer on random cells by using swipes, you'll find sometimes the drawer doesn't open.
I see this behavior on my table and thought I did something wrong. I tried many things and eventually implemented my own subclass of UITableView which also supports UIGestureRecognizerDelegate. In my subclass I implemented the delegate's shouldBeRequiredToFailByGestureRecognizer function, just to print out the gestureRecognizer and otherGestureRecognizer pairs. Then I found that when the back swipe is recognized, the gobbler is NOT present in the pairs. But when the back swipe is not working, the gobbler definitely IS present.
The general opinion on the web is that the gobbler is used to prevent the user from causing another state transition on the table while one transition is already in progress. That is fine if the user indeed takes some action (by touching a button in the drawer). But when the user just closes the drawer, the gobbler should be cancelled. Or the gobbler should be added ONLY when the user takes an action. After my realization, I went on to try my theory on Apple's apps. I already knew the Mail app behaves perfectly responding to every swipe. But the Message app behaves intermittently to repeated drawer opening swipes, much like my app. So I guess the developers of Mail are more careful and used internal knowledge to get it right. My observation is done on iOS 8.4 on iPhone 6 and iPad 2. And I believe the same gobbler issue dates back at least from the first iOS 8 release because I know my app had the issue from day 1 (months ago) but I just got around to look into the problem.
it should definitely be part of private API ..
i will suggest to stay out of it

Message users while large table loads

I am processing several large RSS feeds and displaying results in a TableView. The processing starts after the user clicks on the relevant tab. All works well, but as it takes a couple of seconds to process, the NIB and Table don't load until the processing finishes and it looks like the iPhone has seized up. I have added an Activity indicator to the NIB, but because it doesn't load until the table is ready to display, it appears too late to be of any use.
Does anyone have any ideas how to display a message to a user while the table builds/loads? I have tried loading a UIView first and adding the Table as a subview but, again, both seem to load only after the table is ready.
Guidance appreciated.
It's kind of hard to guess what's going on from your description but it looks like your calls aren't asynchronous. Here's what you should be doing in your code:
Make all calls asynchronous. You said your phone is seizing up. Makes it sound like your requests and responses are happening on the main thread. There are many libraries you could use to handle asynchronous calls. ASIHTTPRequest for one example....
Don't wait for the data to come in before displaying the tableView. It's a design principle that you load as much of the UI as possible so that the user has something to look at while your data loads up in the background. What you should be doing is initializing an NSMutableArray to hold the data. Initially this array will contain no objects. This is the array that you use in your data source methods: Use array size for numberOfRowsInSection and use the array objects in cellForRowAtIndexPath. Once your RSS feed XML comes in and is parsed, store that in your arrays and call [tableView reloadData]. This way you don't leave your users looking at a blank screen. (Another good practice is when the array size is zero, show one cell in your tableview that says "data is loading" or something).
When you first initialize and load up your table and then fire off those RSS feed requests, that's where you show an activity indicator view on the tableView. Stop animating the indicator when the RSS data comes in and your tableView reloads.
These are the standard steps you should follow while showing non local data in a tableview. Makes for a smooth user experience.
Like I said before, it seems from your question that your calls are not asynchronous. If I'm wrong, correct me and let's take it from there...

Have can I show a progresss indication when I'm doing my work before the tableview is loaded?

I allow the user to manage records on other views. I set a flag if certain changes are made.
Then on the flag (where the data changes will have an impact) I run some methods / queries which create the data which is used in my table view(s). This workload currently happens in viewWillAppear(s).
This could take a few seconds and I'd like to show my progress indicator view which I wrote today, it uses a transparent view with a activity indicator in the center of the view.
[self performSelectorInBackground:#selector(startupStuff) withObject:sender];
However, viewWillAppear won't wait while I run the the work in the background.
Ideally I'm looking for a quick fix to work around this problem.
Any ideas ?
However, viewWillAppear won't wait while I run the the work in the background.
That's the whole point of it, isn't it? At the end of your startupStuff method, you should call another method on the main thread (with performSelectorOnMainThread:...) that is used to (a) inform the controller that your data is ready, (b) reload the table view and (c) dismiss your progress indicator view.

iPhone/Cocoa Touch - problem with multithreading (Stanford Presence 3 project)

I have been solving the Stanford free iPhone course project called Presence 3 (found on the stanford site: www.stanford.edu/class/cs193p/cgi-bin/downloads.php, which pulls data from twitter for users, which are stored in a plist. A UIActivityIndicator (spinner) is visible while the data is loading. Once the data has been loaded, a TableView displays the users in a list with their photos, and user statuses show up when a user's cell is clicked. I can successfully display the TableView with the photos and bring up another view controller with statuses when clicked. But when I add in the spinner, my program crashes. I set up my program almost identically to the ThreadedFlickrTableView example project, which can also be found at the same link above (sorry, I'm a new user and can only post one link), which works. I put breakpoints in my code to see where the problem was, and I found that the program crashes when it is loading a cell in the cellForRowAtIndexPath method, specifically when it is retrieving the photo from the appropriate array (followeesPhotoURLs). This is because the array is empty - the photos were never downloaded since the main thread decides to execute the cell-loading method before the thread dedicated to downloading from the internet finishes executing (it does start executing).
I looked on the auditors discussion group page for the course and found that someone else had the same problem, but the thread never resolved the issue, and I emailed to no avail:
http://groups.google.com/group/iphone-appdev-auditors/browse_thread/thread/ccfc6ae99b4cf45d/ef1b8935e749c7c2?hl=en&lnk=gst&q=presence3#ef1b8935e749c7c2
My first rule of UITableView is never report sections or rows that aren't ready (with something, even if only a placeholder) because it will crash every time.
The spinner is spinning while the resource is loading. So you are waiting for a resource that may or may not be ready because you don't know the exact state of your background process. How about setting a value in your main thread indicating that things are not ready. Then when your secondary thread finishes loading things you can do performSelectorOnMainThread to cause some main thread function to set the value to indicate you can proceed. Until the value says proceed, your main thread does not try to access those values the secondary thread might be touching. Maybe your cells will display "loading" or similar until the data is ready, or you will just add cells as they become ready.
One more thing - ONLY the main thread can touch the UI. Nothing in UIKit is thread safe unless explicitly stated. The changes to the progress indicator must be handled by the main thread, it should start the indicator and stop it (probably when your secondary thread notifies "done", as above).
I just finished working through this today and here's the sequence of events that I observed:
Your table view will load up with nothing because your arrays contain nothing
Your thread will go and retrieve Twitter data
Your table view will be refreshed with the Twitter data [self.tableView reloadData]
If you're like me, you try to set the user name using something similar to
cell.textLabel.text = [[userInfo objectAtIndex:indexPath.row] valueForKey:#"name"];
I think that because it is very specific, the application really tries to look for it and doesn't return null if it doesn't find anything, unlike the code in the example which is
cell.text = [photoNames objectAtIndex:indexPath.row];
...so your application errors out the first time it tries to load data with empty arrays.
The way I worked around this is to create an array that loads the user names from the property list at the very beginning so I know how many entries there should be in my array of content. The key part is to create a condition before setting up your cell so that you know you have all the information that you need such as...
// Set up the cell...
if ([userInfo count] == [userList count]) {
userInfo is my array of dictionaries with the data that comes from Twitter.
userList is my array of values from the property list.