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.
Related
This is more a conceptual query than coding. I have a custom activity indicator, a custom view. The only public API, the user will have is the init(onFrame frame: CGRect), startAnimating() and stopAnimating().
So, I want to know in the startAnimating method, should I create a thread whether main or DispatchQoS to run the animation.
Also, if I don't put the animation code in a thread, will it be automatically running on main thread?
All communication with a UIView must be on the main thread. All Core Animation is automatically performed on a background thread. So do not do any explicit multithreading in connection with CABasicAnimation.
I have a custom view within my ViewController that I want to update some info on. This info is being drawn asynchronously from Firebase.
I'm trying to use view.setNeedsDisplay or view.setNeedsLayout to update the view. But since each only changes the view after the next drawing cycle, how can I force the next drawing cycle to happen without forcing the user to leave the ViewController and come back?
Calling setNeedsDisplay or setNeedsLayout is sufficient to trigger “the next drawing cycle”, if you call it on the main thread. It sounds like you might not be calling it on the main thread. If you're not sure which thread you're calling it on, dispatch back to the main queue:
dispatch_async(dispatch_get_main_queue()) {
self.someView.setNeedsDisplay()
}
In my app I'm polling a web service for status updates, using a completionHandler block and making changes to the current view based on returned results when the callback executes.
- (void) tickTimer
{
[MyWebService myWebMethod:param1 completionHandler:^(NSString *result) {
// does view still exist?
[self myUpdateMethod];
// does property still exist?
self.theResult = result;
// does child view still exist?
_txtUpdate.text = result;
}];
}
But in the interim, it's possible that the view may have been unloaded as the user navigates elsewhere.
So a couple of questions:
What happens to a view when a new one is loaded and it gets pushed to the background? I imagine it gets garbage collected at some point, but how do I tell if it's still safe to access by any of the references above, and what would happen if it's not?
If the view does still exist, how do I tell if it is also still the foreground view?
So, blocks create strong references to all objects pointers that are referred to in their closure. Due to this, your block is going to force [self] to stay in memory until the block is destroyed. If this isn't the behavior you want you should create a weak pointer to self and refer to it inside of the block:
__weak typeof(self) weakSelf = self;
So a couple of questions:
What happens to a view when a new one is loaded and it gets pushed to
the background? I imagine it gets garbage collected at some point, but
how do I tell if it's still safe to access by any of the references
above, and what would happen if it's not?
If your view stays in the view hierarchy, it will stay in memory. Once there are no more references to the view it will be dealloced.
If you use a weak pointer like outlined above, then [weakSelf] will be nil if the view has been dealloced
If the view does still exist, how do I tell if it is also still the
foreground view?
I'm not sure what you mean by foreground view, but if you want to see if it's still in the view hierarchy then you can check the property -(UIView *)superview. If superview is nil, then it's not on the screen
If you use ARC right, it will not let you use deallocated viewcontroller.
You can use viewDidAppear and viewDidDisappear methods to know visible yours viewcontroller or not.
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.
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.