Say I have a subclass of UIView which I will call AnimatableView, where my implementation of layoutSubviews lays out some internal content.
AnimatableView also has the method commenceAnimationLoop which looks something like this:
- (void) commenceAnimationLoop {
NSOperation *animationLoop = [[AnimationLoopOperation alloc]init];
[queue addOperation:animationLoop]; //queue is a NSOperationQueue iVar of AnimatableView
[animationLoop release];
}
The AnimationLoopOperation is a subclass of NSOperation whose main method will start an infinite animation which only exits once the isCancelled property becomes YES.
The problem I am having is in my ViewController where I setup and animate an instance of AnimatableView like so:
- (void) viewDidLoad {
AnimatableView *animatableView = [AnimatableView alloc]initWithFrame:someFrame];
[self.view addSubview: animatableView];
[animatableView commenceAnimationLoop];
[animatableView release];
}
The problem is that the NSOperation created in the commenceAnimationLoop executes before my instance of AnimatableView has had chance to call layoutSubviews (meaning that it's content has not yet been generated and laid out).
I am making an educated guess that the layoutSubviews method is called later (when the ViewController is ready to render its view hierarchy?), but that the commenceAnimationLoop method spawns a thread that immediately executes in the background.
To summarise - what would be the best way to make my animationLoop operation wait for the view to layout before commencing?
To provide some high level context for what I am trying to achieve - I am using NSOperation to start a continuous animation. The idea being that I can provide an endAnimation method which would likely call cancelAllOperations on my queue. All this is done in the hopes I can provide a way of starting and then interrupting an infinite animation with the two methods commenceAnimationLoop and endAnimation. Perhaps I am barking up the wrong tree going down the route of multi-threading?
If you need layoutSubviews to run before your operation then you should't start the operation queue before it that. You can prevent an operation queue from starting additional operations using [myOperationQueue setSuspended:YES];.
Since it will only prevent the queue from starting new operations you should suspend the queue before you add your operations to it.
After you view has loaded and layoutSubviews has run you can resume the operation queue again using [myOperationQueue setSuspended:NO];. One place to do this could be in viewDidAppear.
Related
I'm calling a method which belongs to custom delegate class on viewDidLoad but it starts from [sampleProtocol startSampleProcess], starts from sleep(5), before it show me view controller and label1.
CustomDelegate *sampleProtocol = [[CustomDelegate alloc]init];
sampleProtocol.delegate = self;
[self.label1 setText:#"Processing..."];
[sampleProtocol startSampleProcess];
startSampleProcess method is below;
-(void)startSampleProcess{
sleep(5);
[self.delegate processCompleted];
}
processCompleted method is also below;
-(void)processCompleted{
[self.label1 setText:#"Process Completed"];
}
It just set a label on viewcontroller, go to another class and do something simple (etc: sleep) and come back to view controller and set label again. I didn't try custom delegate before so it would be great if you help me on what I'm missing.
The problem is that you are calling sleep on the main thread.
Here's how an iOS app works:
Wait until something interesting happens.
Process it.
Go back to step 1.
The app has something called a runloop that receives messages from the system about touches, timers, etc. Every time it gets a message, it runs some code, often provided by you. When you call the sleep function, it suspends the current thread. When the thread is suspended, the run loop can't process new events until the sleep is done.
When you change something onscreen, you add an event to the run loop that says the screen needs to be redrawn. So, this is what is happening in your application:
You change the label text. A redraw event is now added to the runloop.
You sleep for 5 seconds, meaning the runloop can't process new events.
5 seconds later, the thread wakes up and changes the label's text.
Control finally gets back to the run loop.
The run loop processes the redraw event, changing the label's text.
If the task needs to be a long-running task, you can do it in a background thread:
-(void)startSampleProcess {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_NORMAL, 0) ^{ //run this code in the background so it doesn't block the runloop
sleep(5);
dispatch_async(dispatch_get_main_thread(), ^{ //after the task is done, call the delegate function back on the main thread since UI updates need to be done on the main thread
[self.delegate processCompleted];
});
});
}
I'm trying to implement the iOS Background Fetch API in an app. The app downloads JSON from a server, calls -[UICollectionView reloadData], and for every cell, and image is downloaded asynchronously in -collectionView:cellForItemAtIndexPath:.
In my initial implementation, I would call the completion handler passed by the system into application:performFetchWithCompletionHandler after I called reloadData. The app snapshot in the multitasking view would then display empty cells, because the images wouldn't have been downloaded yet. To solve that, I removed the completion handler call after reloadData, wrote a little structure keeping track of which cells' images have been downloaded, and only after a certain number have been downloaded, I would call the completion handler.
I did this using a completionBlock property on the view controller that reloads the images. The app delegate sets that property, then calls a reload method on that view controller, which then calls its completion handler property. I looks like this:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
WSViewController *viewController = (WSViewController *)navController.topViewController;
viewController.completionBlock = ^(BOOL success, BOOL newData) {
if (!success) {
completionHandler(UIBackgroundFetchResultFailed);
} else if (success) {
if (newData) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}
};
[viewController reload];
}
During testing, I found that performing a background fetch in relatively rapid succession would cause the completion handler not to be called. That's easily explained, because the completionBlock property is overwritten and the old one won't get called.
So, as advised in WWDC 2013 Session 204 "What's New With Multitasking", I removed the property and decided to pass the completion handler all the way through my code. -reload is now -reloadWithCompletionBlock: etc.
But now I'm stuck on how to implement that in the App Delegate. The View Controller has a delegate, and one of its methods, - (void)didFinishDownloadingImages, is implemented by the App Delegate. That's the point where I want to call the completion handler. But I can't, since there is no way to get to the completion handler without storing it in a property, defeating why I was doing it this way in the first place.
Any thoughts on how to solve this?
use NSOperationQueue and move your blocks of code into a subclass of NSOperation -- this will help you handle situations like blocking, discarding multiple requests and otherwise handling all those types of situations.
So, rather than write inline blocks, I'd move to an operation queue which seems like some lifting, but actually makes this much easier to handle properly.
In almost every UIViewController I have a bunch of AFHTTPRequestOperations and I have to properly handle any kind of cancel (pressing cancel button, going back in UINavigationController's stack, etc.). I was wondering if creating one NSOperationQueue per each UIViewController and adding to it all operations called within controller would be a proper way to go? I was aiming for cancelling all operations [[NSOperationQueue mainQueue] cancelAllOperations] but this will kill all operations already started, especially those called from previous UIViewController. Or should I create property for each operation, call it in viewWillDissappear:(BOOL)animated and set if statement for cancel state in success block?
AFHTTPRequestOperationManager instances are cheap to create and each has its own operation queue, so it is easy to cancel all of a given UIViewController's operations:
- (void)dealloc {
[self.requestOperationManager.operationQueue cancelAllOperations];
}
This will cancel any request created through self.requestOperationManager. You can create the AFHTTPRequestOperationManager in your UIViewController's init method.
I recommend cancelling operations in your view controller's dealloc method, as you know it will no longer be needed.
Tried David Caunt answer, but didn't work for me. dealloc wasn't called when I pushed back button, so I guess the best way to cancel operations is to call it in viewWillDisappear: (my bad was to use && not || - silly mistake, but easiest things are often hardest to find)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController && self.isBeingDismissed){
DDLogDebug(#"back or dismissed!");
[self.manager.operationQueue cancelAllOperations]; }
}
The method below is called on a non-main thread, to be specific, in a recording audio queue callback
- (void)myMethod
{
//...
dispatch_async(dispatch_get_main_queue(), ^{
[myGraphView setNeedsDisplayInRect:CGRectMake(a, b, c, d)];
NSLog(#"Block called");
});
//...
}
where myGraphView is a custom UIView object. For what I know, setNeedsDisplayInRect: should be called on main thread which is why I have dispatch_async... in place. Now the problem is the method - (void)drawRect:(CGRect)rect I implemented for myGraph is never called even though the NSLog in the block has been called for many times.
There are a few possibilities here.
From the Class Reference:
Note: If your view is backed by a CAEAGLLayer object, this method has
no effect. It is intended for use only with views that use native
drawing technologies (such as UIKit and Core Graphics) to render their
content.
The other option, which is probably the cause in this case, has to do with the actual geometry. If the provided rectangle is invalid or off screen, the call does nothing. I would suggest you verify the that the rectangle is being calculated as it should be.
Thanks to #Neal's answer which led me to find out that myGraphView was, after it had been alloc-inited the first time, alloc-inited again. However, unlike the first alloc-init after which I added myGraphView to its superview, I forgot to do so after the second alloc-init.
The lesson I've learned here is that when a view is not doing what it's expected to do, such as not being displayed or updated, always check this third possibility where you forget to add it back to its superview after it's got alloc-inited again somewhere in your code. Also, if the view has a delegate you would tend to forget to set it as well.
I ran into an interesting iOS problem today involving a CATiledLayer. This only happend on the device - not in the simulator.
My view draws in its CALayer via the drawLayer: inContext: delegate callback. This layer has a CATiledLayer-derived sublayer, which does its own drawing in an overridden drawInContext: method.
Both layers are rendering pdf content via CGContextDrawPDFPage(). (The CALayer draws a low res version, while the CATiledLayer sublayer draws hi-res content over the top.)
I ran into a scenario where I would be done with the view - would remove it from its superview and release it. dealloc() is called on the view. Sometime later, the CATiledLayer's drawInContext: method would be called (on a background thread), by the system. It would draw, but on return from the method Springboard would crash, and in doing so, bring down my app as well.
I fixed it by setting a flag in the CATiledLayer, telling it not to render anymore, from the view's dealloc method.
But I can only imagine there is a more elegant way. How come the CATiledLayer drawInContext: method was still called after the parent layer, and the parent-layer's view were deallocated? What is the correct way to shut down the view so this doesn't happen?
The slow, but best way to fix is to also set view.layer.contents = nil. This waits for the threads to finish.
Set view.layer.delegate to nil before releasing the view.
-(void)drawLayer:(CALayer *)calayer inContext:(CGContextRef)context {
if(!self.superview)
return;
...
UPDATE: As I recall, there were problems with this in older versions of iOS when it came to CATiledLayers, but setting the delegate to nil before dealloc is now the way to go. See: https://stackoverflow.com/a/4943231/2882
Spent quite a long time on this. My latest approach is to declare a block variable and assign to self in viewWillDisappear method. Then invoke the setContents call onto a global dispatch queue - no need to lock up the main thread. Then when setContents returns invoke back on to the main thread and set the block variable to nil, which should ensure that the view controller is released on the main thread. One caveat though, I have found that it is prudent to use dispatch_after for the invoke onto the main thread as the global dispatch queue retains the view controller until it exits its block, which means you can have a race condition between it exiting (and releasing the view controller) and the main thread block setting the block variable to nil) which could lead to deallocation on the global dispatch queue thread.