How would you stall the program from executing? Doesn't sleep function do that? If it does, what's wrong with this code:
AsyncImageView *tmpImage = [imagesArray objectAtIndex:index];
int waitCounter = 0;
while (waitCounter < 10 && !tmpImage.imageIsLoaded) {
waitCounter++;
sleep(0.5);
}
NSLog(#"%i",waitCounter);
if (tmpImage.imageIsLoaded) {
return [tmpImage image];
} else {
return [UIImage imageNamed:#"img_noimg.png"];
}
The AsyncImageView class loads an image from an URL then sets imageIsLoaded property to true. This usually happens quite fast (under a second), but if I scroll though the images like a madman, then it doesn't have time to load (it loads 10 images ahead).
I've added the while to prevent that from happening, but the log displays multiple cases of waitCounter being 10 while there was no sleep time (the interface didn't freeze).
One approach to solve a problem like this is to use a completion handler. A completion handler signals the call-site when an asynchronous task is complete and passes the result of the task as a parameter to the completion handler.
Your AsyncImageView class does have indeed such a completion handler. It's a selector. Today, one would use a block instead. The principle is the same, though.
Continuing after an async task has finished is called "Continuation". With blocks, you code looks as follows:
typedef void (^completion_handler_block)(id result);
void doSomethingAsync(completion_handler_block)completionHandler;
- (void) foo {
doSomethingAsync(^(id result) {
// Continuation code
...
});
}
With blocks implementing continuation is especially easy, since it keeps the context (referenced variables defined in the call-site) within the block (closure).
Where are you doing this? in method cellForRowAtIndexPath ?
If so, you must not sleep here.
For loading an image asynchronously you must
1) pass the real UIImageView to your AsyncImageView and
2) when you load the UIImage from URL, instead of set *_imageIsLoaded = TRUE*, you can load it directly in the passed UIImageView using *_imageView.image = image*
For instance you can do something like this, taking into account that maybe you already have the image loaded, etc. Create this method in AsyncImageView like this
-(void) reloadWithImageView:(UIImageView*)imageView {
_imageView = imageView;
}
and when you later load the UIImage (probably in the connectionDidFinishLoading method)
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
connection = nil;
UIImage *image = [UIImage imageWithData:data];
_imageView.image = image;
}
Try This, I guess it will work. Instead of
sleep(0.5);
try
[NSThread sleepForTimeInterval:.5f];
Related
I have the following code to a run method that returns a UIImage made from a UIView that's created in separate UIView class file. The FlagMarkerView is a separate subclass of UIView and is referenced here. The problem is the line FlagMarker *markerView... throws the following Main Thread Checker Error and crashes the app.
UIView.init(coder:) must be used from the main thread.
The code used to work as is, but no longer works as I've updated the project to target iOS 11.
I tried wrapping the FlagMarkerView call in a dispatch_async(dispatch_get_main_queue(), ^{}); but that doesn't work because it doesn't get picked up by the return UIImage in the method. I also tried to use a -(void) method instead of returning a UIImage, but that’s hazard with the complexity of my project.
Is there a way I can create the newMarkerImage in - (void)updateMyFlagsWitAlert: and use dispatch_async(dispatch_get_main_queue(), ^{ for FlagMarkerView so newMarkerImage can be created in line from markerImage.
- (void)updateMyFlagsWitAlert:(BOOL)isAllowed{
for (AGSGraphic *graphic in weakSelf.flagOverlay.graphics) {
FlagModel *flagToUpdate = graphic.attributes[#"flag"];
UIImage *newMarkerImage =
[weakSelf markerImageForFlag:flagToUpdate withDetail:detail];
}
}
- (UIImage *)markerImageForFlag:(FlagModel *)flag withDetail:(BOOL)withDetail {
// This line crashes app
FlagMarkerView *markerView = [[[NSBundle mainBundle] loadNibNamed:#"FlagMarkerView" owner:self options:nil] objectAtIndex:0];
[markerView setFlag:flag];
UIImage *markerImage = [markerView imageWithDetail:withDetail];
[markerView layoutIfNeeded];
return markerImage;
}
You should probably redesign your code to be async, but the quickest way I could think of is something like:
__block FlagMarkerView *markerView;
dispatch_sync(dispatch_get_main_queue(), ^{
markerView = [[[NSBundle mainBundle] loadNibNamed:#"FlagMarkerView" owner:self options:nil] objectAtIndex:0];
[markerView setFlag:flag];
UIImage *markerImage = [markerView imageWithDetail:withDetail];
[markerView layoutIfNeeded];
});
return markerImage;
This will make your code run in the main thread, and wait for it to finish.
It is not a good design, you should probably design your code better and not rely on a return value.
Instead, your code could use a block as a parameter that will receive the result after it was processed in the main thread, instead of waiting for it using a semaphore.
But sometimes, if you just need something to work - this solution could be helpful.
The following code-change solved my problem. I found the problem is not with markerImageForFlag at all even though the Main Thread Checker highlighted the markerView. I believe the weakSelf reference put the task on the background thread, which was ok for awhile, but doesn't work anymore. All I had to do is put the dispatch_sync, not dispatch_async, on the call, newMarkerImage. Thanks to #Rob and #Moshe! See below.
- (void)updateMyFlagsWitAlert:(BOOL)isAllowed{
for (AGSGraphic *graphic in weakSelf.flagOverlay.graphics) {
FlagModel *flagToUpdate = graphic.attributes[#"flag"];
__block UIImage *newMarkerImage = nil;
dispatch_sync(dispatch_get_main_queue(), ^{
newMarkerImage = [weakSelf markerImageForFlag:flagToUpdate withDetail:(scale < markerThreshold)];
});
}
}
I am making an app that shows an animated UIImageView as a custom way of indicating that the app is busy. I'm using an NSOperationQueue for file uploads, and I'd like the UIImageView to be shown when there is something in the queue. When every operation in the queue completes, I want to remove the UIImageView.
I thought that this is something really easy to do, but I've been stuck now for the past hour. Showing the UIImageView is really easy, but I can't seem to remove it. It's probably something really simple that I'm just overlooking. Here's my code. Thank you! :)
- (void)viewDidLoad {
//set up the uiimageview
self.spinnerView = [[UIImageView alloc] initWithFrame:CGRectMake([[UIScreen mainScreen] bounds].size.width-44,0,44,44)];
self.spinnerView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"0.gif"],
[UIImage imageNamed:#"1.gif"],
[UIImage imageNamed:#"2.gif"],
[UIImage imageNamed:#"3.gif"],
[UIImage imageNamed:#"4.gif"], nil];
self.spinnerView.animationDuration = 0.5f;
self.spinnerView.tag = 998;
self.spinnerView.animationRepeatCount = 0;
[self.view addSubview: self.spinnerView];
//set up the queue
self.uploadQueue = [[NSOperationQueue alloc] init];
[self.uploadQueue setMaxConcurrentOperationCount:1];
//set up observer for the queue
[self.uploadQueue addObserver:self forKeyPath:#"operationCount" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)newUpload:(NSData*)data {
[self.spinnerView startAnimating];
//....
//request is a NSURLRequest that's set up in this method
[NSURLConnection sendAsynchronousRequest:request queue:self.uploadQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
}];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (object == self.uploadQueue && [keyPath isEqualToString:#"operationCount"]) {
if (self.uploadQueue.operationCount == 0) {
[self.spinnerView stopAnimating];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
Am I doing this correctly? Is there a better way to do it? I've been stuck here for a while and am starting to think that perhaps it's not the UIImageView that's messing up, but rather the way that I'm adding NSURLRequests to the NSOperationQueue.
Thanks again!
Why don't you try https://github.com/samvermette/SVProgressHUD or https://github.com/jdg/MBProgressHUD ? They were made exactly for that purpose (showing modal loading window while doing some asynchronous job). And they are both easy to use and easy customizable for your images and many other options.
The documentation for sendAsynchronousRequest:queue:completionHandler: says that an operation is only added to the specified operation queue after the asynchronous URL request has completed. This operation is just a completion block.
So I do't think you are really adding adding operations in the way you intend to your queue. The URL requests will be running on their own threads outside the queue, only the completion blocks are put on the queue. If you haven't specified anything in the completion block itself then perhaps it is not even added to the queue at all?
Either way I don't think you are adding 5 URL operations to the queue, which then execute one after the other with operationCount == 5, 4, 3, 2, 1, 0. You are more likely firing 5 simultaneous URL requests with their completion blocks being added to the queue in an indeterminate sequence after their URL requests happen to finish.
To do what I think you intend to do, you could:
Write a "concurrent" NSOperation subclass that contains and
manages an NSURLConnection and NSURLRequest etc.
Use AFNetworking.
Continue with sendAsynchronousRequest:queue:completionHandler: but use the
completion handler of one operation to start the next request, and
the completion handler of the final request to stop the spinner.
You could just use the main queue here as the only work being done
in the queue is starting the next operation or stoping the spinner. The actual work of the URL Request is done on it's own thread anyway.
Writing your own concurrent NSOperation is a bit tricky, I wrote one myself, but I probably should have just used AFNetworking. Option 3 is probably the quickest if it meets your needs.
Add this to your .h file: UIImageView *spinnerView; In your .m file, you would want something like this in your -(void)newUpload:
if (code that says file uploads are done) {
spinnerView.hidden = YES;
}
I'm new to GCD and blocks and am easing my way into it.
Background: I'm working on a lazy loading routine for a UIScrollView using the ALAssetsLibrary. When my UIScrollView loads I populate it with the aspectRatioThumbnails of my ALAssets and then as the user scrolls, I call the routine below to load the fullScreenImage of the ALAsset that is currently being displayed. It seems to work.
(if anyone has a better lazy loading routine please post a comment. I've looked at all I could find plus the WWDC video but they seem to deal more with tiling or have much more complexity than I need)
My question: I use a background thread to handle loading the fullScreenImage and when that is done I use the main thread to apply it to the UIImageView. Do I need to use the main thread? I've seen that all UIKit updates need to happen on the main thread but I am not sure if that applies to a UIImageView. I was thinking it does, since it is a screen element but then I realized that I simply didn't know.
- (void)loadFullSizeImageByIndex:(int)index
{
int arrayIndex = index;
int tagNumber = index+1;
ALAsset *asset = [self.assetsArray objectAtIndex:arrayIndex];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage *tmpImage = [[UIImage alloc] initWithCGImage:asset.defaultRepresentation.fullScreenImage];
if ([weakSelf.scrollView viewWithTag:tagNumber] != nil){
dispatch_async(dispatch_get_main_queue(), ^{
if ([weakSelf.scrollView viewWithTag:tagNumber]!= nil){
UIImageView * tmpImageView = (UIImageView*)[weakSelf.scrollView viewWithTag:tagNumber];
tmpImageView.image = tmpImage;
}
});
}
});
}
Yes, you need to use the main thread whenever you're touching UIImageView, or any other UIKit class (unless otherwise noted, such as when constructing UIImages on background threads).
One commentary about your current code: you need to assign weakSelf into a strong local variable before using it. Otherwise your conditional could pass, but then weakSelf could be nilled out before you actually try to use it. It would look something like
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage *tmpImage = [[UIImage alloc] initWithCGImage:asset.defaultRepresentation.fullScreenImage];
__strong __typeof__(weakSelf) strongSelf = weakSelf;
if ([strongSelf.scrollView viewWithTag:tagNumber] != nil){
dispatch_async(dispatch_get_main_queue(), ^{
__strong __typeof__(weakSelf) strongSelf = weakSelf;
if ([strongSelf.scrollView viewWithTag:tagNumber]!= nil){
UIImageView * tmpImageView = (UIImageView*)[strongSelf.scrollView viewWithTag:tagNumber];
tmpImageView.image = tmpImage;
}
});
}
});
Technically you don't need to do this in the first conditional in the background queue, because you're only dereferencing it once there, but it's always a good idea to store your weak variable into a strong variable before touching it as a matter of course.
If you need to render the image in UIImageView, you need to do this in main thread. It will not work unless you do it in main queue as shown in your code. Same is the case with any UI rendering.
if ([weakSelf.scrollView viewWithTag:tagNumber]!= nil){
UIImageView * tmpImageView = (UIImageView*)[weakSelf.scrollView viewWithTag:tagNumber];
tmpImageView.image = tmpImage;
}
As per Apple documentation,
Threading Considerations: Manipulations to your application’s user interface must occur on the main thread. Thus, you should always call
the methods of the UIView class from code running in the main thread
of your application. The only time this may not be strictly necessary
is when creating the view object itself but all other manipulations
should occur on the main thread.
Yes you need to use the main thread, since any UI changes needs to be done in the main thread.
As for using the GCD it is used to take the advantage of the Multi Cores on the device.
As for the strong and weak self
strong self: you might want a strong self, for in you code
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
(<your class> *) *strongSelf = weakSelf;
UIImage *tmpImage = [[UIImage alloc] initWithCGImage:asset.defaultRepresentation.fullScreenImage];
if ([strongSelf.scrollView viewWithTag:tagNumber] != nil){
dispatch_async(dispatch_get_main_queue(), ^{
if ([strongSelf.scrollView viewWithTag:tagNumber]!= nil){
UIImageView * tmpImageView = (UIImageView*)[strongSelf.scrollView viewWithTag:tagNumber];
tmpImageView.image = tmpImage;
}
});
}
});
say you have a view which make a API call and it takes time, so you switch back to another view but you still want the image to be downloaded then use strong since the block owns the self so the image is downloaded.
weak self: if in the above situation you dont want the image to download once you move to a different view then use weak self, since the block doesn't own any self.
If you will not use strongSelf in the code suggested by Kevin Ballard then it might lead to a crash because of weak getting nilled out.
Also a good practice would be to even check for strong being non nil at the point of creating
strongSelf = weakSelf
if(strongSelf)
{
// do your stuff here
}
Hi I'm working through the Stanford iOS development class. I have a question regarding threading. I understand UIKit calls should be handled by the main thread. I was wondering if something like this is legal?
- (UIImage *)mapViewController:(MapViewController *)sender imageForAnnotation:(id<MKAnnotation>)annotation {
FlickrPhotoAnnotation *fpa = (FlickrPhotoAnnotation *) annotation;
NSURL *url = [FlickrFetcher urlForPhoto:fpa.photo format:FlickrPhotoFormatSquare];
__block UIImage *image;
dispatch_queue_t downloadQ = dispatch_queue_create("download queue", NULL);
dispatch_async(downloadQ, ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
dispatch_async(dispatch_get_main_queue(), ^{
image = [UIImage imageWithData:data];
});
}
});
dispatch_release(downloadQ);
return image;
}
or I should just return NSData and handle all the threading in the calling method?
Thanks
Your code won't do what you want.
You are putting an asynchronous block into a queue, and then immediately returning from the method. You are not guaranteed that the block will actually run before you return -- in fact, the odds are that it won't.
So you'll return the current value of image. Since you didn't initialize it, it's probably garbage. Whoever calls this method will try to use a garbage pointer to an image, and (if you're lucky) crash. If you had initialized it to nil:
__block UIImage *image = nil;
that would be a little more polite.
The problem here is: your method must return a UIImage, so you must wait for the time it takes to make a fully constructed UIImage before you return. There is zero benefit to doing this on some other thread -- you're still waiting the same amount of time, and switching threads just adds overhead.
In order to load the image in a usefully asynchronous way, you need some way to asynchronously tell the caller when the image is done loading, via a callback to a delegate method or block. For example, look at NSURLConnection in Foundation. It has an older method that calls back via a delegate, and a newer method (+sendAsynchronousRequest:queue:completionHandler:) that calls back via a block.
I concur with CodaFi's comment. Actually, images can be created off the main thread. UIView creation and manipulation must be done on the main thread, but UIImage is not a UIView though. Furthermore, the runtime is likely to give you a warning or error if you try to manipulate the displaying UI on another thread (it did for me when I accidentally updated a UITableView on another thread).
The problem is that I manage scrollView with lots of tiles in it.
Each visible tile display image loaded from URL or (after first URL load) cached file in background. Invisible tiles recycles (set new frame and redraw).
Image load depends on tile position.
With long range scroll there is multiple redraw called for each tile: each tile loads (and display) different image several times before display the correct one.
So problem is to cancel all previously added operations for tile before add new.
I subclass NSInvocationOperation just to contain context object to detect operation attached to and before add new operation I canceling all operation for same tile:
-(void)loadWithColler:(TileView *)coller {
if (queue == nil) {
queue = [NSOperationQueue new];
}
NSInvocationOperationWithContext *loadImageOp = [NSInvocationOperationWithContext alloc];
[loadImageOp initWithTarget:self selector:#selector(loadImage:) object:loadImageOp];
[loadImageOp setContext:coller];
[queue setSuspended:YES];
NSArray *opers = [queue operations];
for (NSInvocationOperationWithContext *nextOperation in opers) {
if ([nextOperation context] == coller) {
[nextOperation cancel];
}
}
[queue addOperation:loadImageOp];
[queue setSuspended:NO];
[loadImageOp release];
}
And in operation itself I check isCancelled:
-(void)loadImage:(NSInvocationOperationWithContext *)operation {
if (operation.isCancelled) return;
TileView *coller = [operation context];
/* TRY TO GET FILE FROM CACHE */
if (operation.isCancelled) return;
if (data) {
/* INIT WITH DATA IF LOADED */
} else {
/* LOAD FILE FROM URL AND CACHE IT */
}
if (operation.isCancelled) return;
NSInvocationOperation *setImageOp = [[NSInvocationOperation alloc] initWithTarget:coller selector:#selector(setImage:) object:cachedImage];
[[NSOperationQueue mainQueue] addOperation:setImageOp];
[setImageOp release];
}
But it is do nothing. Some times early returns works but tiles still load many images before the correct one.
So how could I success? And could this lots of unneeded operations cause delays on main thread when scrolling? (Because delays are exists and I do not know why...all load in background..)
Update:
With NSLog:
isCancelled while executing:
>
cancel loadImage method for:
>
So canceling work.
Now I save reference to last operation in TileView object and perform setImage operation only if invoked operation is equal to TileView operation.
Doesn't make any difference...
Looks like there IS number of operations to load different images to one tile invoked one after another.
Any another suggestions?
For clearance:
There is singleton DataLoader (all code from it). And all tiles has call to it in drowRect:
[[DataLoader sharedDataLoader] loadWithColler:self];
Update:
NSInvocationOperation subclass:
#interface NSInvocationOperationWithContext : NSInvocationOperation {
id context;
}
#property (nonatomic,retain,readwrite) id context;
#end
#implementation NSInvocationOperationWithContext
#synthesize context;
- (void)dealloc
{
[context release];
[super dealloc];
}
#end
Thanks a lot for any help!
SOLUTION:
From answer below: need to subclass from NSOperation
As I subclass NSOperation and put all loadImage: code into it "main" method (just move all code here and nothing else) and all work just perfect!
As about scroll delaying: it occurs cause loading images to UIImageView (it takes long time because of decompress and rasterize (as I understood).
So better way is to use CATiledLayer. It loads data in background and do it much faster.
The delays on main thread is due to a the mode of the runloop while you scroll. I suggest you to watch the WWDC2011 networking app sessions. I don't know if it is fine to subclass an NSInvocationOperation that is a concrete subclass of NSOperation. I will subclass NSOperation instead. For my experience if you like to avoid sluggish scrolling, you should create NSOperation subclasses that load their main on a specific thread for networking operation (you must create it). There is a wonderful sample code from apple https://developer.apple.com/library/ios/#samplecode/MVCNetworking/Introduction/Intro.html
The way that NSOperationQueue works with respect to "setSuspended" is that it won't start to run newly added NSOperations added to it after that point, and won't start to run any that are currently in it that haven't started running yet. Are you sure your operations you're trying to cancel haven't already started yet?
Also - does your NSOperation subclass correctly deal with Key Value Observing? Concurrent Queue subclassed NSOperations have to call willChangeValueForKey and didChangeValueForKey for some properties here - but doesn't look like that's the issue as your queue doesn't set isConcurrent. Just FYI if you go that route.