Animations in iOS are easy, except when they’re not. I learned this the hard way making SKBounceAnimation
, when I learned about how explicit animations are so different and weird compared to implicit animations.
Today, I learned a new lesson. You would think something like
mySublayer.cornerRadius = 3;
myView.frame = CGRectMake(0, 0, 100, 100);
[UIView animateWithDuration:0.25 animations:^{
mySublayer.cornerRadius = 5;
myView.frame = CGRectMake(50, 50, 100, 100);
}];
would just work. But apparently you can’t perform sublayer animations in an animateWithDuration:
block, so your CALayer
just picks its own duration and timing function, and animates it anyway. This can be a subtle effect, but if you flip the Toggle Slow Animations
flag in the Simulator, it becomes super obvious, since CATransactions
aren’t affected by that menu item.
The solution is to create a separate CATransaction
mySublayer.cornerRadius = 3;
myView.frame = CGRectMake(0, 0, 100, 100);
CGFloat duration = 0.18f;
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] ];
mySublayer.cornerRadius = 5;
[CATransaction commit];
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationCurveEaseInOut animations:^{
myView.frame = CGRectMake(50, 50, 100, 100);
} completion:^(BOOL finished) { }];
Your CATransaction can be inside your animations
block if you like. Apple warns that
This type of mixing should not be used in situations where precise timing is needed.
but it looks pretty dead-on to me. The only information about doing this kind of thing is found in three paragraphs on the Apple documentation site, found here.
I’ve fixed the graphical bugs in SKInnerShadowLayer
. See below.
SKInnerShadowLayer
is a CAGradientLayer
subclass that adds properties to create an inner shadow on a given layer.
Find at it GitHub: github.com/khanlou/SKInnerShadowLayer
Usage
SKInnerShadowLayer
takes the graphical properties of a CAGradientLayer
that let you set the shadow, gradient, and border of a layer, and adds four properities that let you control the look of an inner shadow for the layer.
These properties are:
@property CGColorRef innerShadowColor;
@property CGSize innerShadowOffset;
@property CGFloat innerShadowRadius;
@property CGFloat innerShadowOpacity;
They behave similarly to their drop shadow counterparts.
Technique
The technique for drawing the inner shadow is simple. The layer:
creates a path for rounded rect of the layer
clips to this rounded rect
creates a larger path around the rounded rect
sets the shadow properties
draws a shadow behind this shape
and this creates the illusion of an inner shadow.
Animations
These properties are all fully animatable. There is an example of a layer with an inner shadow opacity animation in the demo app. Being able to easily create animations is the immediate advantage of using CoreGraphics
to draw an inner shadow instead of using an image resource.
Update
There were small bugs with the drawing of the inner shadow, but they were due to the default value for contentsScale
on a CALayer
being set to 1.0
by default, even on Hi-DPI/Retina devices. Setting it to 2.0
fixed the problem. I also found this incredible sentence, from the Apple documentation: >
The contentScale default value is 1.0. In certain restricted cases, the value may set the value to 2.0 on hi-dpi devices.
If you have any questions, contact me at @khanlou or soroush@khanlou.com.
FTWStartupLaunch drop-in class for Mac OS X that lets you set your program to launch at login.
Usage is very simple. FTWStartupLaunch
is a class with two class methods.
+ (BOOL) willLaunchAtStartup;
is a quick way to query the system on whether or not an app is in the list of programs that launch on startup for that user.
+ (void) shouldLaunchAtStartup:(BOOL)shouldLaunchAtStartup;
lets you quickly set whether your app should launch at startup for that user.
Find it at GitHub: github.com/FTW/FTWStartupLaunch
Credits
This is based on the code found here bdunagan.com/2010/09/25/cocoa-tip-enabling-launch-on-startup/, with the APIs cleaned up a little bit and turned into a standalone class.
I had the pleasure and privilege of presenting a Blitz Talk at SecondConf this year. For those that don’t know, a Blitz Talk is a strictly enforced presentation format, where you have 5 minutes to show 20 slides, and the slides advance every 15 seconds automatically. It’s a brutal format, since there’s no slowing down. My talk was on SKBounceAnimation, which is an explicit animation that makes it dead-simple to create a bounce animation. It was really well received, and I was honestly blown away by the number of people that came by and talked to me about the animation.
A few folks have asked me to share the slides that I used. They’re pretty minimal, so if you have any questions about the content or derivations, please feel free to ask me questions via email (soroush@khanlou.com) or Twitter ((@khanlou).
Find the slides below in PDF format.
Relevant other blog posts:
Sorry I haven’t had any Good Fridays in the last 3 weeks. I hope Kanye’s GOOD Friday from two weeks ago was able to tide you over. I’m at SecondConf for the weekend, so this week’s update is gonna be smaller.
SKReachability is a singleton that gives quick access to information about the connection.
Usage is simple. Call [SKReachability sharedReachability]
to initialize the singleton. Set the host
property to test connection to a specific host. It defaults to the Google homepage.
Subscribe to notifications for reachability changes:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kSKReachabilityChangedNotification object:nil];
and then you can access the properties radioPoweredOn
, connectedToInternet
, connectedViaWifi
, and connectedToHost
when the notification is fired. You can also access them at any point to determine the state of the network.
SKReachability uses Apple’s Reachability, so make sure you have that, and SystemConfiguration.framework
.
Find it on GitHub: github.com/khanlou/SKReachability
I’ve been playing a little with CATransform3D
and moving things around with perspectives. Transformation matrices are still a little bit beyond me, but I’ve figured out a few things and got a Flipboard-esque page turn working within about two hours. This guide is particularly well-written: milen.me/technical/core-animation-3d-model/.
The main thing that was tripping me up was the location of the anchor point. To create a perspective effect, you set the m34
property of the transform to something small, like 1/2000. However, as the “page” was swinging open, the perspective was only skewing the layer on the bottom (and not on the top).
Not fully understanding how the transformation matrix worked, I was looking for a property that corresponds to m34
for the other top. A little trial and error showed that the layer’s anchorPoint
property, which I had set to the top left, was controlling it. The Apple guide for layer geometry describes how it affects scaling and rotating, but it seems obvious in retrospect that it would control the perspective as well.
Having a smoothly scrolling table view with images that load from the internet is a surprisingly complex task.
The Problem
If you’re building an app for any kind of web service, you’ve certainly come across the situation where you want to load images for a tableview from the internet, while the user is scrolling. It’s a really hard problem to solve, and the solution you use will be dependent mostly on style. There are a couple of techniques in the newer versions of iOS that make this somewhat simpler, but the solution gets complex as you add affordances for a properly-scrolling tableview. (For the purposes of this blog post, a performant, properly-scrolling tableview involves something that at the least doesn’t block the users input, and at best scrolls at 60 frames per second.)
The wrongest you can get it
The worst thing you can do is synchronous loading.
cell.imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
-dataWithContentsOfURL:
doesn’t return until the URL connection is over, which can take up to 2 minutes in the worst case (when the host isn’t available and the connection times out). Until that method returns, the UI is completely blocked. The user can’t do anything, the app won’t respond to any touches, including any for scrolling your tableview. Unacceptable.
A little better
A better solution is to subclass UIImageView
, and set up a non-blocking NSURLConnection
within the image view that takes the url, gets data for it, and creates the image view. This can have some unexpected results (giving the same image two different URLS, since the cells are getting reused), results an an extra view being attached to your cell, and generally gets messy.
All aboard
Grand Central Dispatch, added in iOS 4 and Leopard, gives us some handy tools for pushing work to other queues so they don’t block the main one from processing input events. We can get the data for the image, and hop back to the main queue to actually set the image (since almost all UIKit classes must be accessed from the main queue).
cell.imageView.image = [UIImage imageNamed:@"icn_default"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData *data = [NSData dataWithContentsOfURL:imageURL];
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
});
This is good. Non-blocking, no extra classes needed, supports placeholder images, and is neat.
A small improvement that we need to make is to change anything accessing cell
to a call to [tableView cellForRowAtIndexPath:indexPath]
, since cell
might have been reused by the time that the URL connection is complete, causing it to update the wrong row. cellForRowAtIndexPath:
will return nil
if the intended cell is off the screen.
Let’s do some more
This will try to download the image every the cell shown, even if it’s already been downloaded. It has no caching. Using something simple like [FTWCache](https://github.com/FTW/FTWCache)
, we can store these images locally and pull them when we need them.
NSString *key = [imageURL.absoluteString MD5Hash];
NSData *data = [FTWCache objectForKey:key];
if (data) {
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
} else {
cell.imageView.image = [UIImage imageNamed:@"icn_default"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData *data = [NSData dataWithContentsOfURL:imageURL];
[FTWCache setObject:data forKey:key];
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
[tableView cellForRowAtIndexPath:indexPath].imageView.image = image;
});
});
}
Better still. Now we have caching, along with the other Fs and Bs we got earlier.
Package it up
This is a hot mess of code to keep in each cellForRowAtIndexPath:
method, though, and we should try to package it up into a UIImageView
category.
#import
static char URL_KEY;
@implementation UIImageView(Network)
@dynamic imageURL;
- (void) loadImageFromURL:(NSURL*)url placeholderImage:(UIImage*)placeholder cachingKey:(NSString*)key {
self.imageURL = url;
self.image = placeholder;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *imageFromData = [UIImage imageWithData:data];
[FTWCache setObject:UIImagePNGRepresentation(imageFromData) forKey:key];
UIImage *imageToSet = imageFromData;
if (imageToSet) {
if ([self.imageURL.absoluteString isEqualToString:url.absoluteString]) {
dispatch_async(dispatch_get_main_queue(), ^{
self.image = imageFromData;
});
}
}
self.imageURL = nil;
});
}
- (void) setImageURL:(NSURL *)newImageURL {
objc_setAssociatedObject(self, &URL_KEY, newImageURL, OBJC_ASSOCIATION_COPY);
}
- (NSURL*) imageURL {
return objc_getAssociatedObject(self, &URL_KEY);
}
@end
Since we can’t refer to our tableView to get its cell, and we can’t add synthesized properties to an Objective-C category, we can use Objective-C’s associative properties to store a reference to an object that is associated to another object. In this case, we store the image’s URL, and check to make sure that it hasn’t changed since the network connection has started. If it has changed, that means the cell (and therefore imageView
) has been reused and we shouldn’t update the image.
Extra credit
What else can we do? We can do image resizing. If you control the source of the images, you can resize on the server. This is ideal, since resizing on the device is pretty slow. Image resizing is pretty hard to do properly on the device, especially with the way UIImage
works. If you do want to do image resizing on the device, take a look at UIImage+Resize
, a category made in 2009, and doing that in the background queue will assure that it doesn’t lock up your UI.
There’s more still. Even with the UIImageView
category, the download is happening regardless and can’t be cancelled, since you put it on the queue with no reference to it. To be able to cancel it, you need to keep track of an NSOperationQueue
and the operations therein. It gets significantly more complicated, and the implementation for that is beyond the scope of this blog post. If you are going to be reusing the images, like on a Twitter client for example, it might be smart to let it download anyway.
You should also be profiling the scrolling performance of your exact code to understand what the fastest method for your code is. If you choose to go the NSOperation route, note that there is some cost to allocating and releasing NSOperation
objects, and that can lose you frames every second doing that.
Update
I’ve uploaded a gist with a simple category for this technique: https://gist.github.com/khanlou/4998479
This is the newest in a series called Good Friday, where I open-source a new, (somewhat) cool thing every Friday for the next few weeks.
The open-sourcing bonanza continues, this week with FTWCache. FTWCache is a dead simple caching class I wrote for the FTW iOS App. It has two class methods,
+ (void) setObject:(NSData*)data forKey:(NSString*)key;
and
+ (id) objectForKey:(NSString*)key;
and you can reset the cache with
+ (void) resetCache;
It stores the files in your Caches folder in its own folder. The default expiration time is 7 days, but you can modify that if you need. I wrote a good blog post that I’ll be posting about on Monday about how to use this in conjunction with Grand Central Dispatch to download and show asynchronous images in a UITableView.
It takes NSData objects, so anything you can format in that way (audio files, images, simple text) can be store in the cache. The system will also periodically flush the Caches folders of apps if it finds it needs more space.
Find FTWCache on GitHub at github.com/FTW/FTWCache and be sure to hit me up on Twitter or via email if you use it.
SKBounceAnimation
is a CAKeyframeAnimation
subclass that creates an animation for you based on start and end values and a number of bounces. It’s based on the math and technology in this blog post, which in turn was based partially on Matt Gallagher’s work here.
Check it out on GitHub: github.com/khanlou/SKBounceAnimation.
Usage
Basic code is simple:
SKBounceAnimation *bounceAnimation = [SKBounceAnimation animationWithKeyPath:@"position.y"];
bounceAnimation.fromValue = [NSNumber numberWithFloat:view.center.x];
bounceAnimation.toValue = [NSNumber numberWithFloat:300];
bounceAnimation.duration = 0.5f;
bounceAnimation.delegate = self;
bounceAnimation.numberOfBounces = 2;
[bouncingView.layer setValue:animation.toValue forKeyPath:animation.keyPath];
[view.layer addAnimation:bounceAnimation forKey:@"someKey"];
SKBounceAnimation
is an explicit animation, so you have to give the layer its final value before you add the animation. If you don’t, the layer will snap back to its original state after the animation is over.
Math
The math is simple. Check out the blog post and the informational post preceding it for exact details, but essentially the system behaves with oscillating exponential decay in the form of the equation: x = Ae^(-αt)•cos(ωt) + B
.
A is the difference between start and end values, B is the end value, α is determined by the number of frames required to get the exponential decay portion to close enough to 0, and ω is determined by the number of periods required to get the desired number of bounces.
Extras
shouldOvershoot
is a property that you can change. It defaults to YES; if you set it to NO, the animation will bounce as if it were hitting a wall, instead of overshooting the target value and bouncing back. It looks a lot like the Anvil effect in Keynote.
I’d like to add another property that would make the object stretch away from its original location and then bounce back, the way the photo button in iOS 5.1+ works, but I haven’t had a chance to implement yet.
Demo app
The demo app contains demos for several different animations that are supported by SKBounceAnimation
.
One-axis animation: Using a keypath like
position.x
, we can animate along one axis.Two-axis animation: Using a keypath like
position
,SKBounceAnimation
will generate a path, and your layer will follow it.Size: Using the
bounds
keypath, we can make the size increase. The center of the size increase is determined byanchorPoint
, which can be moved. It defaults to the center of the layer.Color: I have no idea why anyone would want to bounce a color animation, but I was feeling whimsical, so I added support for this as well.
Scale: Using a
CATransform3D
struct and thetransform
keypath, we can scale objects. This is very useful to create an effect like UIAlerts bouncing in.Scale & Rotate: Using multiple CATransform3Ds on top of each other, we can do super weird effects like scale and rotating. They look really cool.
Rect: The last demo creates two
SKBounceAnimations
with two differentkeyPath
s (position
andbounds
) but attaches them to the same layer. The effect looks like aframe
animation.
Future work
SKBounceAnimation
doesn’t support the byValue
property yet. I also would like to add a property like stretchAndBounceBack
.
_This is the first in a series called Good Friday. I’m gonna be open-sourcing a new, (somewhat) cool thing every Friday for the next few weeks. _
In building the FTW Mac app, we needed access to some information about each computer that it ran on, so that we could identify it from within games. UIDevice is so handy on iOS, and with Erica Sadun’s UIDevice+Extension, it becomes even better. Instead of writing a couple of little functions to get the MAC address and computer name and model, I sat down and filled in the blanks.
What I ended up with was FTWDevice, and near-perfect drop-in replacement for UIDevice, built for Cocoa on the Mac. Check it out at GitHub: https://github.com/FTW/FTWDevice.
There are a few notes and caveats with it. Because NSHost has +name
and +localizedName
, I stuck with those conventions in FTWDevice
. This is a little different from UIDevice’s implementation, so be wary of that.
There are a few integer overflow issues with the RAM and hard drive space functions, and I’d appreciate a pull request if you have an idea about how to make that better. Gestalt is also being deprecated in 10.8, so if anyone has a better way of getting the system version, I’d love to hear that as well.
Other than that, use and enjoy! I’d love to hear if you use this anywhere, so make sure to let me know if you find it useful.