So I've asked a couple of questions regarding the UICollectionView. Understanding how it works, I'm trying to implement lazy loading to load 15 images onto the view controller. I found many examples 1, 2, 3...first and third examples deal with only one operation, second example I don't think uses operations at all, only threads. My question is would it be possible to use a NSOperation class and use/reuse operations? I read that you can't rerun operations but I think you are able to once you initialize them again. Here's my code:
view controller:
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20);
[layout setItemSize:CGSizeMake(75, 75)];
self.images = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout];
self.images.delegate = self;
self.images.dataSource = self;
[self.images registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"cellIdentifier"];
[self.view addSubview:self.images];
self.operation = [[NSOperationQueue alloc]init];
[self.operation addOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"getgallery.php?user=%#", userId] relativeToURL:[NSURL URLWithString:#"http://www.mywebsite.com/"]];
NSData *data = [NSData dataWithContentsOfURL:url];
//datasource for all images
self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
[self.images reloadData];
//reload collection view to place placeholders
}];
}];
- (void)viewDidAppear:(BOOL)animated{
//get the visible cells right away
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
self.processedImages = [[NSMutableDictionary alloc]initWithCapacity:visibleCellPaths.count];
}
#pragma mark - collection view
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
int i;
if (self.imagesGalleryPaths.count == 0)
i = 25;
else
i = self.imagesGalleryPaths.count;
return i;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
if ([visibleCellPaths containsObject:indexPath]) {
[self setUpDownloads:visibleCellPaths];
}
image.image = [UIImage imageNamed:#"default.png"];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}
return cell;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}
- (void)setUpDownloads:(NSArray *)visiblePaths{
//I want to pass the visiblePaths to the NSOperation class if visible cells changed
GalleryOps *gallery = [[GalleryOps alloc]init];
//I will use a dictionary to keep track of which indexPaths are being downloaded...
//...so there are no duplicate downloads
}
GalleryOps.m
#implementation GalleryOps
- (void)main{
//would I initialize all the operations here and perform them?
}
It's almost pointless to show the empty GalleryOps class because I have no idea how to initialize it with multiple operations. I know I have to override the main method, and once I get the image data from URL, I'll need to update the UI, for which I need a delegate method...another thing I don't yet know how to do but there are many examples to figure that out. My biggest question is how to pass the visible cells into this class and run multiple operations? When new visible cells come in, I'll run a check to see which to cancel, which to keep. Any advice here? Thanks in advance!
Looking at your proposed solution, it looks like you want to defer the question of making the operations cancelable. Furthermore, it looks like you want to defer the use of the cache (even though it's no more complicated than your NSMutableDictionary property).
So, setting that aside, your revised code sample has two "big picture" issues:
You can dramatically simplify the image retrieval process. The use of startOperationForVisibleCells and the two scroll delegates is unnecessarily complicated. There is a much simpler pattern in which you can retire those three methods (and achieve an even better UX).
Your cellForItemForIndexPath has a problem, that you're adding subviews. The issue is that cells are reused, so every time a cell is reused, you're adding more redundant subviews.
You really should subclass UICollectionViewCell (CustomCell in my example below), put the configuration of the cell, including the adding of subviews, there. It simplifies your cellForItemAtIndexPath and eliminates the problem of extra subviews being added.
In addition to these two major issues, there were a bunch of little issues:
You neglected to set maxConcurrentOperationCount for your operation queue. You really want to set that to 4 or 5 to avoid operation timeout errors.
You are keying your imageGalleryData with the NSIndexPath. The problem is that if you ever deleted a row, all of your subsequent indexes would be wrong. I suspect this isn't an issue right now (you're probably not anticipating deleting of items), but if you keyed it by something else, such as the URL, it's just as easy, but it is more future-proof.
I'd suggest renaming your operation queue from operation to queue. Likewise, I'd rename the UICollectionView from images (which might be incorrectly inferred to be an array of images) to something like collectionView. This is stylistic, and you don't have to do that if you don't want, but it's the convention I used below.
Rather than saving the NSData in your NSMutableDictionary called imageGalleryData, you might want to save the UIImage instead. This saves you from having to reconvert from NSData to UIImage (which should make the scrolling process smoother) as you scroll back to previously downloaded cells.
So, pulling that all together, you get something like:
static NSString * const kCellIdentifier = #"CustomCellIdentifier";
- (void)viewDidLoad
{
[super viewDidLoad];
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20);
[layout setItemSize:CGSizeMake(75, 75)];
// renamed `images` collection view to `collectionView` to follow common conventions
self.collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout];
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
// you didn't show where you instantiated this in your examples, but I'll do it here
self.imageGalleryData = [NSMutableDictionary dictionary];
// register a custom class, not `UICollectionViewCell`
[self.collectionView registerClass:[CustomCell class] forCellWithReuseIdentifier:kCellIdentifier];
[self.view addSubview:self.collectionView];
// (a) change queue variable name;
// (b) set maxConcurrentOperationCount to prevent timeouts
self.queue = [[NSOperationQueue alloc]init];
self.queue.maxConcurrentOperationCount = 5;
[self.queue addOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"getgallery.php?user=%#", userId] relativeToURL:[NSURL URLWithString:#"http://www.mywebsite.com/"]];
NSData *data = [NSData dataWithContentsOfURL:url];
//datasource for all images
self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
[self.collectionView reloadData];
}];
}];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagesGalleryPaths count]; // just use whatever is the right value here, don't make this unnecessarily smaller
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath];
NSString *key = self.imagesGalleryPaths[indexPath.row]; // I don't know whether this was simply array, or some nested structure, so tweak this accordingly
UIImage *image = self.imageGalleryData[key];
if (image) {
cell.imageView.image = image; // if we have image already, just use it
} else {
cell.imageView.image = [UIImage imageNamed:#"profile_default.png"]; // otherwise set the placeholder ...
[self.queue addOperationWithBlock:^{ // ... and initiate the asynchronous retrieval
NSURL *url = [NSURL URLWithString:...]; // build your URL from the `key` as appropriate
NSData *responseData = [NSData dataWithContentsOfURL:url];
if (responseData != nil) {
UIImage *downloadedImage = [UIImage imageWithData:responseData];
if (downloadedImage) {
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
self.imageGalleryData[key] = downloadedImage;
CustomCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = downloadedImage;
}
}];
}
}
}];
}
return cell;
}
// don't forget to purge your gallery data if you run low in memory
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
[self.imageGalleryData removeAllObjects];
}
Now, clearly I don't have access to your server, so I couldn't check this (notably, I don't know if your JSON is returning a full URL or just a filename, or whether there was some nested array of dictionaries). But I don't want to you to get too lost in the details of my code, but rather look at the basic pattern: Eliminate your looping through visible cells and responding to scroll events, and let cellForItemAtIndexPath do all the work for you.
Now, the one thing that I introduced was the concept of CustomCell, which is a UICollectionViewCell subclass that might look like:
// CustomCell.h
#import <UIKit/UIKit.h>
#interface CustomCell : UICollectionViewCell
#property (nonatomic, weak) UIImageView *imageView;
#end
and then move cell configuration and adding of the subview here to the #implementation:
// CustomCell.m
#import "CustomCell.h"
#implementation CustomCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.layer.borderWidth = 1;
self.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
[self addSubview:imageView];
_imageView = imageView;
}
return self;
}
#end
By the way, I still contend that if you want to do this properly, you should refer to my other answer. And if you don't want to get lost in those weeds of the proper implementation, then just use a third party UIImageView category that supports asynchronous image retrieval, caching, prioritizing network requests of visible cells, etc., such as SDWebImage.
You can use the queue and operations themselves to manage multiple operations. If I am reading your question correctly, you have one operation (get the list of image URLs from JSON) that you want to spawn child operations. You can do this by having the parent operation add the child operations to the queue, or by using dependent operations (child would have the parent as a dependancy).
For what you're trying to do you do not need to subclass NSOperation, NSBlockOperation should meet your needs. Subclassing NSOperation is trickier than it looks because of the KVO dependancies (it's very easy to get wrong).
But to the specifics of your question:
My question is would it be possible to use a NSOperation class and use/reuse operations? I read that you can't rerun operations but I think you are able to once you initialize them again
If you're initializing them again they're new objects (or at least, they should be). NSOperations can't be re-run because they have internal state - the tricky KVO bits I mention above. Once they go to "finished", that instance can't be returned to a clean state.
Operations should be fairly lightweight objects and there should not be any significant value in reusing them, and plenty of potential trouble. Creating new operations should be the way to go.
The Apple sample code "LazyTableImages" may give you some hints as how to accomplish what you're trying to do.
The constituent elements of an NSOperation-based lazy loading of images might include:
Create a dedicated NSOperationQueue that will be used for the download operations. Generally this is configured with a maxConcurrentOperationCount of 4 or 5 so that you enjoy concurrency, but so that you won't exceed the maximum number of concurrent network operations.
If you don't use this maxConcurrentOperationCount, with slow network connections (e.g. cellular), you risk having network requests time out.
Have a model object (e.g. an array) that backs your collection view or table view. This would generally only have some identifier for the image (e.g. the URL) not the image itself.
Implement a cache mechanism to store the downloaded images, to prevent the need to re-download images that have already been downloaded. Some implementations only do memory based cache (via NSCache). Others (e.g. SDWebImage) will do two tiers of cache, both memory (NSCache, for optimal performance) and a secondary persistent storage cache (so that when memory pressure forces you to purge the NSCache, you still have a rendition saved on the device so you don't have to re-retrieve it from the network). Others (e.g. AFNetworking) rely upon NSURLCache to cache the responses from the server into persistent storage.
Write a NSOperation subclass for downloading a single image. You want to make this cancelable operation. That implies two different design considerations
First, regarding the operation itself, you probably want to make a concurrent operation (see the Configuring Operations for Concurrent Execution section in the Concurrency Programming Guide).
Second, regarding the network request, you want a cancelable network request. If using NSURLConnection, this means using the delegate-based rendition (not any of the convenience methods). And if using NSURLConnection, the trick is that you have to schedule it in a run loop that persists. There are a number of tricks to accomplish this, but the easiest is to schedule it (with scheduleInRunLoop:forMode:) in the main run loop (though there are more elegant approaches), even though you will be running this from an operation in an NSOperationQueue. Personally I launch a new dedicated thread (like AFNetworking does) for this purpose, but the main run loop is easier and is fine for this sort of process.
If using NSURLSession, this process is conceivably a tad easier, because you can get away with using the completion block rendition of dataTaskWithRequest and not get into the delegate-based implementation if you don't want to. But this is iOS 7+ only (and if you need to do anything fancy like handle authentication challenge requests, you'll end up going the delegate-based approach anyway).
And combining those two prior points, the custom NSOperation subclass would detect when the operation is canceled and then cancel the network request and complete the operation.
By the way, instances of operations are never reused. You create a new operation instance for each image you are downloading.
By the way, if the images you've downloaded are large (e.g. they have dimensions greater than the number of pixels that the image view needs), you may want to resize the images before using them. When JPG or PNG images are downloaded, they are compressed, but when you use them in an image view they are uncompressed, usually require 4 bytes per pixel (e.g. a 1000x1000 image will require 4mb, even though the JPG is much smaller than that).
There are lots of image resizing algorithms out there, but here is one: https://stackoverflow.com/a/10859625/1271826
You will want a cellForItemAtIndexPath that then pulls the above pieces together, namely:
Check to see if the image is already in the cache, if so, use it.
If not, you will start a network request to retrieve the image. You might want to see if this cell (which may be a reused cell from your table view) already has an image operation already in progress, and if so, just cancel it.
Anyway, you can then instantiate a new NSOperation subclass for the downloading of the image and have the completion block update the cache and then also cell's image view.
By the way, when you asynchronously update the cell's image view, make sure the cell is still visible and that the cell hasn't been reused in the interim. You can do this my calling [collectionView cellForItemAtIndexPath:indexPath] (which should not be confused with the similarly named UICollectionViewDataSource method that you're writing here).
Those are the constituent parts of the process, and I'd suggest you tackle them one at a time. There's a lot involved in writing an elegant implementation of lazy loading.
The easiest solution is to consider using an existing UIImageView category (such as provided with SDWebImage) which does all of this for you. Even if you don't use that library, you'll might be able to learn quite a bit by reviewing the source code.
I figure my collection view is not something the user will come back to over and over again, just once in a while. So no reason to cache all the images.
The viewDidAppear is still the same, I get the visible cells right away. The reason why initially I put 25 cells into numberOfItems... is just to get the visible cells right away. So now my cellForItemAtIndexPath is this:
UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
image.image = [UIImage imageNamed:#"profile_default.png"];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}
return cell;
In the viewDidLoad I added this:
if (self.imagesGalleryPaths.count != 0) {
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
[self.images reloadData];
[self startOperationForVisibleCells];
}];
}
This is my startOperationForVisibleCells:
[self.operation addOperationWithBlock:^{
int i=0;
while (i < visibleCellPaths.count) {
NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
if (![self.imageGalleryData.allKeys containsObject:indexPath]) {
NSURL *url = [#"myurl"];
NSData *responseData = [NSData dataWithContentsOfURL:url];
if (responseData != nil) {
[self.imageGalleryData setObject:responseData forKey:indexPath];
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
UIImageView *image = [UIImageView new];
image.image = [UIImage imageWithData:responseData];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}];
}
}
i++;
}
}];
And that's how I update the cells one by one. Also when the user scrolls away:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
for (int i=0; i<visibleCellPaths.count; i++) {
NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
if ([self.imageGalleryData.allKeys containsObject:indexPath]) {
UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
UIImageView *image = [UIImageView new];
image.image = [UIImage imageWithData:[self.imageGalleryData objectForKey:indexPath]];
image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
[cell.contentView addSubview:image];
}else{
[self startOperationForVisibleCells];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
//same functions as previous
}
I am sure this is a very bad way of doing it but for now, it works. The images are loaded one by one and they stop loading when the user scrolls away.
I am trying to update the score of one of the labels on my custom cell after returning from a push navigation.
In my parent UITableViewController I have the code:
In ViewDidLoad
//These are mutable strings
self.disciplineScoreString1 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreString2 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreString3 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreArray = [[NSMutableArray alloc] init];
[self.disciplineScoreArray addObject:self.disciplineScoreString1];
[self.disciplineScoreArray addObject:self.disciplineScoreString2];
[self.disciplineScoreArray addObject:self.disciplineScoreString3];
So far so good.
In cellForRowAtIndexPath
//this is the custom subclass of UITableViewCell
DayTableViewCell *cell = (DayTableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//this is the custom label
cell.disciplineScoreLabel.text = [self.disciplineScoreArray objectAtIndex:indexPath.row];
return cell;
Still so far so good. Right now the label in each of the 3 cells is blank.
I now segue to a UIViewController from the first cell and I return from the child UIViewController successfully setting the string value of self.disciplineScoreString1 to #"10"which I NSLog'ged in the parent UITableViewController.
How do I update the label for the first cell now? I have tried reload data in ViewWillAppear but its not working.
Thankyou
EDIT
This is the code in the Child ViewController
In viewWillDisappear:
[super viewWillDisappear:animated];
[self calculateDisciplineScore];
NSInteger currentVCIndex = [self.navigationController.viewControllers indexOfObject:self.navigationController.topViewController];
DisciplineTableViewController *parent = (DisciplineTableViewController *)[self.navigationController.viewControllers objectAtIndex:currentVCIndex];
parent.disciplineScoreString1 = self.disciplineScoreText;
You must be changing the string, instead of modifying it... consider this example
NSMutableArray *array = [NSMutableArray array];
NSMutableString *disciplineScoreString1 = [NSMutableString stringWithFormat:#"original string"];
[array addObject:disciplineScoreString1];
[disciplineScoreString1 appendString:#" hello"]; // OK.. you're modifying the original string
NSLog(#"%#", [array objectAtIndex:0]);
disciplineScoreString1 = [NSMutableString stringWithFormat:#"new string"]; // WRONG!!
NSLog(#"%#", [array objectAtIndex:0]);
The ouput is:
2014-02-15 06:36:46.693 Hello[19493:903] original string hello
2014-02-15 06:36:46.694 Hello[19493:903] original string hello
The second example is wrong because you're creating a new string, and the object in the array is still pointing to the old original string.
EDIT:
// try this
[parent.disciplineScoreString1 replaceCharactersInRange:NSMakeRange(0, parent.disciplineScoreString1.length) withString:self.disciplineScoreText];
// instead of
parent.disciplineScoreString1 = self.disciplineScoreText;
Your current approach is quite rigid and not very standard. You would normally set up some kind of delegate to pass data from a child to a parent. It is very rarely correct that a child should know how to reach into the parent's innards and change it's data.
I would personally start with something like this
#interface ChildViewController : UIViewController
#property (nonatomic, copy) void (^onCompletion)(ChildViewController *viewController, NSString *disciplineText);
#end
Now I'm assuming you are dismissing by just calling [self.navigationController popViewControllerAnimated:YES] from within the child? This is a little odd as it means that the childViewController can now only ever be presented in a navigationController, this makes it less reusable.
What I would do is at the point where you normally call popViewControllerAnimated: I would call the completion instead
if (self.onCompletion) {
self.onCompletion(self, self.disciplineText);
}
Now the object who provides the onCompletion can decide how this controller get's removed and it's told about the data that we finished with, which enabled the controller to do what it wants with it.
In your case the parent controller would provide the completion as it knows how the child is presented so it will know how to dismiss it. It also know that it may want to do something with the data the child finished with
// Where you present the child
childViewController.disciplineText = self.self.disciplineScoreArray[index];
__weak __typeof(self) weakSelf = self;
childViewController.onCompletion = ^(ChildViewController *viewController, NSString *disciplineText){
weakSelf.disciplineScoreArray replaceObjectAtIndex:index withObject:disciplineText];
[self.navigationController popViewControllerAnimated:YES];
[weakSelf.tableView reloadRowsAtIndexPaths:#[ [NSIndexPath indexPathForRow:index inSection:0] ]
withRowAnimation:UITableViewRowAnimationFade];
};
I am having some trouble updating my UI using performSelectorOnMainThread. Here is my situation. In my viewDidLoad I set up an activity indicator and a label. Then I call a selector to retrieve some data from a server. Then I call a selector to update the UI after a delay. Here's the code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.reloadSchools = [[UIAlertView alloc] init];
self.reloadSchools.message = #"There was an error loading the schools. Please try again.";
self.reloadSchools.title = #"We're Sorry";
self.schoolPickerLabel = [[UILabel alloc]init];
self.schoolPicker = [[UIPickerView alloc] init];
self.schoolPicker.delegate = self;
self.schoolPicker.dataSource = self;
self.server = [[Server alloc]init];
schoolList = NO;
_activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self.view addSubview:_activityIndicator];
[self.view bringSubviewToFront:_activityIndicator];
[_activityIndicator startAnimating];
[NSThread detachNewThreadSelector: #selector(getSchoolList) toTarget: self withObject: nil];
[self performSelector:#selector(updateUI) withObject:nil afterDelay:20.0];
}
The selector updateUI checks to see if the data was retrieved, and calls a selector on the main thread to update the UI accordingly. Here is the code for these parts:
-(void)updateUI
{
self.schools = [_server returnData];
if(!(self.schools == nil)) {
[self performSelectorOnMainThread:#selector(fillPickerView) withObject:nil waitUntilDone:YES];
}
else {
[self performSelectorOnMainThread:#selector(showError) withObject:nil waitUntilDone:YES];
}
}
-(void)showError {
NSLog(#"show error");
[_activityIndicator stopAnimating];
[self.reloadSchools show];
}
-(void)fillPickerView {
NSLog(#"fill picker view");
schoolList = YES;
NSString *schoolString = [[NSString alloc] initWithData:self.schools encoding:NSUTF8StringEncoding];
self.schoolPickerLabel.text = #"Please select your school:";
self.shoolArray = [[schoolString componentsSeparatedByString:#"#"] mutableCopy];
[self.schoolPicker reloadAllComponents];
[_activityIndicator stopAnimating];
}
When the selector fillPickerView is called the activity indicator keeps spinning, the label text doesn't change, and the picker view doesn't reload its content. Can someone explain to me why the method I am using isn't working to update my ui on the main thread?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//load your data here.
dispatch_async(dispatch_get_main_queue(), ^{
//update UI in main thread.
});
});
First of all you should not be using detachNewThreadSelector. You should use GCD and submit your background task to an async queue. Threads are costly to create. GCD does a much better job of managing system resources.
Ignoring that, your code doesn't make a lot of sense to me. You submit a method, getSchoolList, to run on a background thread. You don't show the code that you are running in the background.
Then use performSelector:withObject:afterDelay to run the method updateUI on the main thread after a fixed delay of 20 seconds.
updateUI checks for self.schools, which presumably was set up by your background thread, and may or may not be done. If self.schools IS nil, you call fillPickerView using performSelectorOnMainThread. That doesn't make sense because if self.schools is nil, there is no data to fill the picker.
If self.schools is not nil, you display an error, again using performSelectorOnMainThread.
It seems to me that the logic on your check of self.schools is backwards. If it is nil you should display an error and if it is NOT nil you should fill the picker.
Next problem: In both cases you're calling performSelectorOnMainThread:withObject:waitUntilDone: from the main thread. Calling that method from the main thread doesn't make sense.
Third problem: It doesn't make sense to wait an arbitrary amount of time for a background task to run to completion, and then either succeed or fail. You won't have any idea what's going on for the full 20 seconds. If the background task finishes sooner, you'll never know.
Instead, you should have your background task notify the main thread once the task is done. That would be a valid use of performSelectorOnMainThread:withObject:waitUntilDone:, while calling it from the main thread is not. (Again, though, you should refactor this code to use GCD, not using threads directly.
It seems pretty clear that you are in over your head. The code you posted needs to be rewritten completely.
I have an app that makes web service calls to obtain data. I want to add an activity indicator that is visible when the app is fetching web service data. I have looked into other posts, and though I believe I am doing as the posts recommend, my indicator does not render on the screen. The object that makes the web service call is stateGauges. Here is my code:
- (void)viewDidLoad
{
[super viewDidLoad];
UIActivityIndicatorView *activityStatus = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(120, 230, 50, 50)];
activityStatus.center = self.view.center;
[self.view addSubview:activityStatus];
[activityStatus bringSubviewToFront:self.view];
[UIApplication sharedApplication].networkActivityIndicatorVisible = TRUE;
[activityStatus startAnimating];
stateGauges = [[GaugeList alloc] initWithStateIdentifier:stateIdentifier andType:nil];
[activityStatus stopAnimating];
}
Any suggestions? Thanks! V
Your problem is that your animation start is blocked by whatever you're doing in your GuagesList initializer.
When you tell the activity indicator to start animating, it doesn't immediately render to the screen but rather flags the view as needing an update on the next turn of the run loop. Your initializer then blocks the thread until its done, you call stopAnimating, and then the thread has a chance to update the indicator. By which point its already set to not animate.
The best solution is to perform your initializer on another thread using GCD. And be sure to switch back to the foreground thread before calling stopAnimating.
The usual pattern is do something like:
[activityStatus startAnimating];
// enqueue it
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
stateGauges = [[GaugeList alloc] initWithStateIdentifier:stateIdentifier andType:nil];
// now switch back to main thread
dispatch_async(dispatch_get_main_queue(), ^{
[activityStatus stopAnimating];
});
});
You'll want to verify the code as I had to type this from memory on a Windows machine.
take out
[activityStatus bringSubviewToFront:self.view];
because according to the docs bringSubviewToFront:
Moves the specified subview so that it appears on top of its siblings.
which isn't what you want. (another answer suggested you do [self.view bringSubviewToFront:activityStatus] instead.. that's fine, but generally this call is redundant, b/c
[self.view addSubview:activityStatus] adds the activityStatus to the end of the views in the self.view subviews array anyways)
if that still don't work.. basically put a break point right after you start animating, then type this into the console:
[[activityStatus superview] recursiveDescription]
recursiveDescription will give you a UI tree graph and basically tell you exactly where the activityIndicator view is.. you may have made an incorrect assumption about something.
Change
[activityStatus bringSubviewToFront:self.view];
To
[self.view bringSubviewToFront:activityStatus];
RemoteImageDownloader *imgView = (RemoteImageDownloader*)[cell viewWithTag:1];
if (imgView == nil)
{
imgView = [[RemoteImageDownloader alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, cell.frame.size.height)];
imgView.tag = 1;
[cell.contentView addSubview:imgView];
}
imgView.image = nil;
imgView.backgroundColor = [UIColor grayColor];
imgView.opQueue = self.opQueue;
//[imgView performSelector:#selector(DownloadRemoteImageforURL:withCachingOption:) withObject:[_marrImgUrl objectAtIndex:indexPath.row]];
if ([self checkDocDirectoryforFileName:[[_marrImgUrl objectAtIndex:indexPath.row] lastPathComponent]])
{
[imgView setImage:[UIImage imageWithData:[self checkDocDirectoryforFileName:[[_marrImgUrl objectAtIndex:indexPath.row] lastPathComponent]]]];
}
else
{
[imgView DownloadRemoteImageforURL:[_marrImgUrl objectAtIndex:indexPath.row] withCachingOption:NSURLRequestReloadRevalidatingCacheData isNeedtoSaveinDocumentDirectory:YES];
}
-(void)DownloadRemoteImageforURL:(NSString*)strURL withCachingOption:(NSURLRequestCachePolicy)urlCachePolicy isNeedtoSaveinDocumentDirectory:(BOOL)isNeedSave
{
ImageLoader *subCategoryImgLoader = [[[ImageLoader alloc] initWithUrl:[NSURL URLWithString:strURL]] autorelease];
subCategoryImgLoader.target = self;
subCategoryImgLoader.didFinishSelector = #selector(imageDownloadDidFinishwithData:andOperation:);
subCategoryImgLoader.didFailSelector = #selector(imageDownloadfailedwithErrorDesc:andOperation:);
[self.opQueue setMaxConcurrentOperationCount:2];
if ([self.opQueue operationCount] > 0)
{
NSOperation *lastOperation = [[self.opQueue operations] lastObject];
[subCategoryImgLoader addDependency:lastOperation];
}
[self.opQueue addOperation:subCategoryImgLoader];
if (_actIndicatorView)
{
[_actIndicatorView removeFromSuperview], _actIndicatorView = nil;
}
_actIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
_actIndicatorView.tag = 100;
_actIndicatorView.center = self.center;
[self addSubview:_actIndicatorView];
[_actIndicatorView startAnimating];
}
In the above code ImageLoader is the subclass of NSOperation. While I'm checking the operation count, I'm getting zero though I'm adding operations into it. Please let me know if I've done any mistakes. I'm not getting what mistakes I've done so I'm getting zero operation count.
I have created the instance of queue and it is created only once and I'm using the same instance instead of creating again and again. After adding any operation it is showing it has one operation but while I'm going to add another one then I'm getting zero count.
RemoteImageDownloader is a subclass of UIImageView. I have created that instance in a UIViewcontroller.
Hope now it will be easy to understand what I' doing actually.
Now I commented the line [self.opQueue setMaxConcurrentOperationCount:2];. Now it is getting the operation count. Can anyone tell me why so?
The most common reason for
"I am sending message x to property y of my object but it returns 0 and it shouldn't"
is that you haven't set a value for the property. i.e. in your case self.opQueue is nil.
Edit We have eliminated the above as the problem. However, what is below is still relevant.
Having said that, you also have a race condition since the operation count may change between your test for it being greater than 0 and adding the dependency (e.g. if the operation finishes).
You should probably do something like this:
NSOperation* lastOp = [[self.opQueue operations] lastObject];
if (lastOp != nil)
{
[subCategoryImgLoader addDependency:lastOp];
}
The documentation for operationCount contains
The value returned by this method reflects the instantaneous number of objects in the queue and changes as operations are completed. As a result, by the time you use the returned value, the actual number of operations may be different. You should therefore use this value only for approximate guidance and should not rely on it for object enumerations or other precise calculations.
(my bold)
I suspect that what is happening when you set the max operations to 2 is that, by the time you get back to your code for the second time, there really are no operations left on the queue.