0

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:

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)... :)

Aside:

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.

Markus A.
  • 12,349
  • 8
  • 52
  • 116

2 Answers2

1

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.

Ian MacDonald
  • 13,472
  • 2
  • 30
  • 51
  • That looks like an interesting approach. It says in the docs that the displayLinkTick method you defined above will get called "when the screen is updated". Does this mean that I also somehow need to keep invalidating the screen? Or does this happen automatically? Also, can I then simply set properties like position and background color and such on UIViews from within the displayLinkTick method? Or do I need to call some other method to actually trigger the repainting of the screen? – Markus A. Nov 03 '14 at 18:01
  • Your `displayLinkTick` will be called when the OS updates the screen; it is unrelated to your view hierarchy or invalidating bits of the screen. You will be able to set any properties you wish within this function, though you should be careful not to do any long-running processes. – Ian MacDonald Nov 03 '14 at 18:04
  • Gotcha. I'll go try it! :) – Markus A. Nov 03 '14 at 18:04
  • Man... this selector-model that Apple uses for simple callbacks is really making my life a living hell here... :) For some reason my displayLinkTick method is not called. All I need to do is to create the CADisplayLink and add it to the run-loop, correct? Or do I need to un-pause something as well? Unfortunately RoboVM is not providing me with access to the NSRunLoopCommonModes-constant, do you know what its actual String-value is? I think this might be my issue... – Markus A. Nov 03 '14 at 19:28
  • The string value is currently `kCFRunLoopCommonModes`, though there's nothing stating that it couldn't change whenever they feel like it. The implementation is private -- I only know this value because I used the debugger to print the string. If RoboVM is restricting your usage of fundamental APIs (i.e. anything in an iOS framework), you might want to consider not using RoboVM. – Ian MacDonald Nov 03 '14 at 19:34
  • BEAUTIFUL!!!! IT WORKS!!! Yes! You're the MAN! :) Thank you! :) The weird thing is: It only works if I use `kCFRunLoopDefaultMode`. But that runs just fine, so I'm tempted to not care (should I?). The reason I'm using RoboVM is that I have a huge library of stuff already written in Java and I'd like to not have to port everything to Objective-C every time I make a change. Right now, I can write an app **once**!!! in Java and it will run in a JVM, on Android, on iOS, and in the web-browser with only 10 lines of code each. It's awesome. I'd rather fight a bit with RoboVM than give that up. :) – Markus A. Nov 03 '14 at 20:01
0

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...

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 (some details here). 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.

Community
  • 1
  • 1
Markus A.
  • 12,349
  • 8
  • 52
  • 116
  • the current version of RoboVM (2.3.1) allows this: `displayLink.addToRunLoop(NSRunLoop.getCurrent(), NSRunLoopMode.Default);` – Clyde May 20 '17 at 01:34