
Question:
I need to run a complex custom animation in an iOS app. For this, I wrote a function that needs to be called repeatedly and uses the current time-stamp to calculate positions, colors, shadows, etc. of UIView elements on the screen.
There seem to be a whole bunch of different approaches I could use to have this function called:
<ul><li><a href="https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html" rel="nofollow">Multi-Threading</a></li> <li><a href="https://developer.apple.com/Library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/index.html" rel="nofollow">Timers</a></li> <li><a href="https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html" rel="nofollow">Dispatch Queues</a></li> <li>God knows what else... :)</li> </ul>I tried calling my animation-function from a separate thread first, but while the thread does run, I don't see any screen updates until I trigger a refresh manually with a device rotation, so I must be missing some step where I call the update functions from inside the GUI Thread instead of my own or invalidating the View or something... But I don't even know if this is the best approach...
What is the preferred way to keep calling a function (for an animation, for example) as quickly as possible (or with a small delay of 10ms or so) without blocking the GUI and in such a way that if this function, for example, changes the background color or position of a view, the screen gets updated?
If possible, I would like to use a method that is as backward-compatible as possible, so preferably something that doesn't use any features introduced in iOS 8.1 (exaggeration)... :)
<em>Aside:</em>
<em>Sorry for not posting a code example. I'm using RoboVM and don't want to "scare off" any answers from true XCode developers. Also, this is more of a general conceptual question rather than a specific bug-fix.</em>
Answer1:I've found the best performance from CADisplayLink
.
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- (void)displayLinkTick {
// Update your animation.
}
Don't forget to teardown when you're destroying this view or else you'll have your displayLinkTick
called until your application exits:
[displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
Alternatively, if you're using (or convert to) CALayer
, your subclass would return YES
from needsDisplayForKey:
on your animating key. Then, in your CALayer
subclass' display
method, you'd apply the changes that your self.presentationLayer
has for your animation.
@property (assign) CGFloat myAnimatingProperty;
@implementation MyAnimatingLayer : CALayer
+ (BOOL)needsDisplayForKey:(NSString *)key {
if ([key isEqualToString:@"myAnimatingProperty"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
- (void)display {
if ([self.animationKeys containsObject:@"myAnimatingProperty"]) {
CGFloat currentValue = self.presentationLayer.myAnimatingProperty;
// Update.
}
}
@end
This second way will allow you to link in with the built-in easing functions really easily.
Answer2:In case someone else is looking for a solution for RoboVM, here you go:
import org.robovm.apple.coreanimation.CADisplayLink;
import org.robovm.apple.foundation.NSObject;
import org.robovm.apple.foundation.NSRunLoop;
import org.robovm.apple.foundation.NSString;
import org.robovm.objc.Selector;
import org.robovm.objc.annotation.BindSelector;
import org.robovm.rt.bro.annotation.Callback;
// Requires iOS 3.1
public abstract class DisplayRefreshTimer extends NSObject implements Runnable {
private static final Selector REFRESH = Selector.register("displayRefresh:");
private static final NSString RUNMODE = new NSString("kCFRunLoopDefaultMode");
public DisplayRefreshTimer() {
CADisplayLink displayLink = CADisplayLink.create(this, REFRESH);
displayLink.addStrongRef(this); // Don't garbage collect "this"
displayLink.addToRunLoop(NSRunLoop.getCurrent(), RUNMODE); // Start calling
}
@Callback @BindSelector("displayRefresh:")
private static void displayRefresh(DisplayRefreshTimer __self__) {
if (__self__!=null) __self__.run();
}
}
Just sub-class this, override run()
and instantiate:
new DisplayRefreshTimer() {
@Override
public void run() {
// Do your magic here...
}
};
Done...
<em>Note: The constant String "kCFRunLoopDefaultMode"
might change and should instead be read from the NSDefaultRunLoopMode
constant that should be provided by NSRunLoop
. For some reason RoboVM removed access to this constant (<a href="https://stackoverflow.com/questions/26721462/nsrunloopcommonmodes-constant-in-robovm" rel="nofollow">some details here</a>). While I would think it's unlikely, Apple might decide to change this constant in the future, in which case apps based in this code will break.</em>