Thoughts on WWDC 2012

or lessons I learned from WWDC 2011


Let me preface this post by saying that I am no WWDC veteran; last year was my first WWDC.  Jeff LaMarche and Collin Donnell (among many others) are seasoned veterans who have written guides on what to bring and what to expect.  That said, here were some of my impressions after WWDC 2011.

Trip Planning

A little late for this considering WWDC 2012 is only little more than a week away, but here goes.  You want to arrive Sunday afternoon or earlier.  You’ll want to swing by Moscone on Sunday afternoon to get your conference badge, so that you’re ready to go on Monday morning.  Also, there will be plenty of social gatherings on Sunday night (some even earlier), so it’s a good opportunity to get your networking and socializing on.  At the end of the week Friday is only a half day and for the most part is repeated sessions, so when planning your return flight, remember that you don’t necessarily need to be there all day (and in fact they will kick you out at 4:15 pm).  I live on the east coast and last year I took the red-eye back Friday night, but there was really no need for me to hang around in San Francisco until 11 pm.  This year I’m flying out in the middle of the afternoon and should be home in bed by midnight.  Considering the usual post-WWDC exhaustion, that’s infinitely better than a night on a plane (especially if you have young children who won’t have mercy on you when you finally arrive home the following morning after a week away).

Packing

Definitely pack for a carry-on.  You don’t want to deal with checked luggage either on arrival or upon your return home (when you’ll no doubt be exhausted and possibly coming down with the WWDisease).  Besides, you just need casual clothes and your tech gear, so you can pack light and keep it simple.

You don’t need to bring a t-shirt for every day.  If you’re like me, you’ll probably buy at least one t-shirt (e.g. the official WWDC 2012 shirt) and win one or more others.  Just wear the new ones to finish out the week.

It’s ok to leave your workout clothes at home.  Usually on business trips I bring workout gear and try to spend an hour a day in the hotel gym.  At WWDC though chances are you’ll be at Moscone all day in sessions and labs, in restaurants and bars fueling up, networking, or socializing in the evenings, and what little time you do spend in your hotel you’ll need for sleep, or else you’ll want to be coding (either for your boss, your clients, or just to try out some of the new shiny bits).  Exercise can be put on hold for a week.

Tech Gear

Bring wall chargers and sync cables for all your iOS devices.  Consider bringing test devices to load up iOS betas on (I rely on my phone too much to install beta 1 builds on it, especially while on the road).  Make sure your test devices are synced to your travel laptop if you want or need to have any content on them.  Expect your iPhone battery to be running low at the end of every day.  If you don’t have one, you might want to buy a rechargeable backup battery such as a mophie juice pack (I just bought this one) to top off your phone at the end of the day.  Even if you don’t need it yourself, I’m sure you’ll meet others you can offer a quick charge to.

Conference

If you’re planning on waiting in line to get into the main room for the Monday keynote, think about leaving your laptop back at the hotel on Monday (especially if it’s one of the heavier models).  You definitely won’t need it for the keynote, and your feet will thank you after you’ve been standing for 4 or more hours.  (Last year I started waiting in the keynote line at 6 am and didn’t sit down until almost 10 am and regretted bringing my laptop.)

You won’t really be able to plan out your week until after the keynote when all the TBA sessions are finally announced.  When planning which sessions to attend, note that some sessions are offered multiple times, so check to see which is the best time for you to attend a particular talk.  Consider going to labs to work with Apple engineers instead of sessions.  The session videos will be available online not long after the conference, but the labs are a unique opportunity and precious commodity.  For sure if there are issues/questions you have regarding current or future projects, seek help at the appropriate labs.  Also, if there is a new technology/API announced that week that you’re keenly interested in, make sure not just to attend the sessions on that topic but to also attend the labs.  For example, last year I really regretted not going to any iCloud labs.  The Apple iCloud engineers could probably have saved me a ton of time and trouble in the weeks just after WWDC when I was struggling with the iCloud beta bits.  So yeah, labs, labs, and more labs.

The Indie Scene

This year I’m really excited and pleased to see an indie dev scene occur in parallel to WWDC.  Indie Dev Lab and Appsterdam are both hosting workplaces and talks that week.  It’s almost enough to make me wish I had opted out of the official event just so that I could spend full time there.  Anyway, there’s definitely an alternate way to network and learn during the day outside of Moscone, and I only hope to see it grow in future years.

If you’re going to be out in San Francisco the week of June 11-15, I hope to see you around.  I’d love to have a beer or grab a bite of sushi or perhaps eat way more meat than is healthy with you.  I’m super excited for it because I love what we do.

Anatomy of a page-flip animation


In this post I would like to do a close examination of my take on the page-flip animation.  As with my foray into folding animations, this started with my desire to come up with some cool examples of real world use for matrix transformations for my Enter The Matrix presentation for CocoaConf Chicago last March (and which I’ll be delivering a new and improved version of at CocoaConf DC next month).

The Basics

Essentially we have two views that we are transitioning between.  Each one is divided in two along either a vertical or horizontal axis and rendered as an image.  If we’re transitioning from A-B to C-D and flipping from right to left, then we will start by flipping up B (while revealing D beneath it).  At the halfway point B will disappear (and D will be fully revealed), and we will begin flipping down C and covering up D.  By the end A & B will be hidden and only C & D will remain.



For my own purposes I refer to the half of the old view that we flip up (B) as the “front page”.  The other half of the old view that remains in place (A) I call the “facing page”.  The half of the new view that we flip down (C) is the “back page” (i.e. it’s on the back side of the page we’re flipping), and the half of the new view that stays in place (D) is the “reveal page”.  Whether one of these halves is on the left or the right (or the top or the bottom) depends of course on the direction of the flip (and its orientation), so I just like to think in terms of which ones are moving and which are not.

As described above, this is a 2-stage animation.  First we flip the front page up until it’s vertical and disappears.  Then we flip the back page down from vertical to flat.

Setting the Stage

Before we execute either stage, we need to prepare our layer hierarchy for the animation.  We will create a view with 4 sublayers where each sublayer’s content is an image rendered from a half each of the new and old views.  The layers for the facing and reveal pages go beneath the layers for the front and back pages (although we won’t add the back page to the hierarchy just yet).


The 2 lower sublayers (facing and reveal) will not need to move.  At this point we can add our animation view and hide (or remove) the old view as we’ve replaced it with a (static) copy.

Stage 1

In a right-to-left page flip, the front page is on the right.  We anchor it on its left side (the “spine” of our imagined book) and rotate it about the y axis from 0° to -90°.

Stage 2

The back page in a right-to-left flip sits on the left and so we need to anchor it on its right side (again forming the spine in the middle of our view).  At the completion of the stage 1 animation we can remove the front page sublayer from the hierarchy and add in the back page sublayer, already pre-rotated to +90°.  (Note: because the 2 layers are anchored on opposite sides, the sign of the angle to which they need to be rotated to appear in the same position is also opposite.)


Then we simply rotate the back page to 0°.

Making it better

What we have so far, serves as a passable page-flip animation, but we can do better in making it more realistic.  By adding a shadow layer (a simple CALayer with background color set to black and opacity set to maybe 0.5) to the reveal page and then animating its opacity, the page beneath can be initially dark (shadowed by the front page) and then get gradually lighter as the front page moves towards vertical.  So let’s animate the opacity of the shadow layer on the reveal page from 0.5 to 0 throughout the stage 1 animation.


During stage 2 we can add a matching shadow layer to the facing page and animate its opacity from 0 to 0.5 as the back page lowers down on top of it to hide it.

Next it would be nice to create the impression of a crease, of a differentiation between the flat facing page and the flipping front page.  This can be achieved using a shadow layer on the front page, although it doesn’t need to be as dark.  We can animate its opacity from 0 to 0.1 during the stage 1 animation.  We’ll add a similar shadow to the back page for stage 2 and animate it from 0.1 back to 0.

Thinking about the shadows

The width of the flipping page during the animation follows a cosine path (although this is slightly distorted by the viewpoint of our perspective, see below).  So for example at 50% completion of the first flip, the page is rotated to 45° but its width is cos(45°) × width, or 70.7% of the width.  If we animate the shadow on the reveal page linearly, that means the shadow would be only at 50% of its maximum even though the page is still 70% covered; i.e. the intensity of the shadow is not proportional to the width of the page covering it.  The reveal page will only be half revealed when the animation is 2/3 completed (cos(60°) = 0.5) but by then a linear shadow would be at 1/3 of its maximum.  So I decided that the shadows on the reveal and facing pages should animate along cosine and sine paths respectively to achieve a more pleasing effect (using CAKeyframeAnimation, see code).  This effectively means the shadows stay darker longer, so I also reduced their maximum opacity to 0.333.  I think it’s ok to leave the front and back page shadows as simple linear animations.

Antialiasing

Did you notice anything wrong with the screenshots above?  There are probably several things, but one of them is the jagged edge of the flipping page.


Because we’re using static images to perform our animation, it’s a simple matter to antialiase the edges in an efficient manner, which gives us this:


Thinking More About the Shadows

But a shadow on the front page doesn’t really make sense and darkening the entire page just to suggest a crease seems like overkill.  Instead we can use a CAGradientLayer to darken the page just near the crease, but leave the bulk of the page unmodified.  We’ll keep the animation on the shadow layer’s opacity to ease the effect in (alternatively, we could animate the gradient’s colors by increasing their alpha).

Timing Curves

Because this is a 2 stage animation, we need to pay some attention to the timing curves.  While we may want an Ease In Ease Out timing curve over the entire animation, that means using an Ease In curve over the first half and an Ease Out curve over the second half.  If we Ease In and Out over each half, we will end up with an uncomfortable pause in the middle of our page flip.  (Ok, it would probably only be uncomfortable to me having to watch such an animation but still.)

Perspective

For our animation to appear 3d at all, we will need to set the m34 component (skew) of the main layer’s transform.  If we don’t, our page flip will be completely flat and rather odd looking.

No perspective: m34 = 0

Skew is defined as – 1 / z where z is the height of the viewer in points above the flat plane of the view.  Choosing the right skew will be dependent upon the dimensions of the views you are animating.  Your skew should be proportional to your page width (horizontal flip) or height (vertical flip).  If z it too low, your page animation will be blown out of proportion.

Too much perspective: m34 = -1/225 or -1/(1.5 × page width)

If z is too high, your page animation won’t have enough “pop” to it.

Too little perspective: m34 = -1/1800 or -1/(12 × page width)

I find that anything in the range of 4-5 × width works pretty well for z, but you can experiment and adjust the animation to your taste.

Inverse perspective

If you invert your perspective (use a positive value instead of the typical negative one), you can get a pretty cool effect.  It doesn’t really make physical sense as a page-flip illusion, but is nonetheless interesting.  (Note:I’ve removed the shadow on the reveal page for this effect.)

Anchor Point

The anchor point of the layer being animated determines the view point of the perspective.  By default the anchor point is {0.5, 0.5} which corresponds to the midpoint of the layer.  That’s why the flipping page sticks out an equal distance above and below.  But if we change our anchor point, we could also change our perspective on our animation and thus alter its look and feel.

View from below – Anchor Point {0.5, 1.5}

View from above – Anchor Point {0.5, -0.5}

Room for Improvement

And now we have a fairly decent page-flip animation.  We could probably improve it further by adding realistic shadows for the front and back pages to cast upon the reveal and facing pages respectively (instead of a uniform shadow).  We could just set shadow properties on some of the sublayers, but that would involve a separate off-screen rendering pass, so we would need to also set the shadowPath appropriately and animate that.  A possible compromise improvement might be to use a CAGradientLayer (darkest by the edge of the covering page) for the reveal and facing page shadows and animate locations (or startPoint and endPoint).

Resources

  • MPFlipViewController – a fully touch gesture-enabled, attribution-licensed container controller, that uses all of the techniques discussed above.
  • MPFoldTransition project with demo app and code you are free to use to easily add these same animations to your own iOS apps.
  • Enter The Matrix project with lots of matrix transformation examples, including touch gesture-enabled versions of both flip and fold animations (the project has now been updated to incorporate the shadow techniques described above).
  • Edge Antialiasing post with a discussion of what it is, why you’d want to do it, and how to do it efficiently.  Also with its own sample project.
  • If you’re not doing WWDC this year, come see me speak about these types of things at CocoaConf DC (and learn from a great speaker lineup).

Credits

I’d like to thank Shawn Welch for starting me down the path of investigating page-flip animations at the Voices That Matter iOS conference in Boston last November, and providing a demo project to get me going.

MPFlipTransition – add flip transitions to your app


I’ve added flip transitions to my MPFoldTransition project on GitHub. It provides a class you can use to add a page-flipping transition to your application in just a single line of code (in most cases).

Update: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction


Controls whether the page flips from right to left (Forward) or left to right (Backward).

Orientation


Sets whether the page flip is horizontal or vertical.

Perspective


Determines whether the page flips towards the user (Normal) or away from the user (Reverse).

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using flip transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    flipStyle:(MPFlipStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFlipStyle:(MPFlipStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  flipStyle:MPFlipStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFlipStyle:MPFlipStyleDirectionBackward 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite direction bit (Forward or Backward), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using flip transitions:

- (void)pushViewController:(UIViewController *)viewController
                 flipStyle:(MPFlipStyle)style;

- (UIViewController *)popViewControllerWithFlipStyle:
    (MPFlipStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    flipStyle:MPFlipStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFlipStyle:MPFlipStyleDirectionBackward];

Tip: pop your view controller using a style with the opposite direction bit (Forward or Backward) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFlipTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFlipStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFlipStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve, shadow effects, or skew), you can initialize your own instance of MPFlipTransition, set the properties as desired, and then call the perform: method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack flip segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFlipModalSegue, MPFlipNavPushSegue, or MPFlipNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only because I wrote it with ARC and included storyboard support.

Anatomy of a folding animation

In this post I want to dive into the minutiae of how a good-looking folding animation can be achieved, and also how I was doing it wrong the first time I took a stab at creating one.  Earlier this year I started looking into fold animations as an example of advanced use of matrix transforms to animate views for my Enter The Matrix presentation for CocoaConf Chicago.

(For a similar in-depth look at page-flip animations see Anatomy of a page-flip animation.)

Initial Efforts


It seemed relatively straight-forward.  Given 4 “panels” (A,B,C,D if you will), fold B & C inward until they disappear while sliding A & D together until they meet.  So the first step was rotating B around the x-axis away from the user along its top edge, and the same for C except along its bottom edge.

Of course that leaves a gap between B & C, so you need to move B down and C up by just the right amount and also move A & D to keep pace with them.  I quickly discovered that the height of panels B & C during rotation is a cosine function of the angle of rotation and not linear.  However, due to perspective issues B & C don’t disappear (are completely on edge) at 90° the way you would expect.  Because you are simultaneously translating and rotating the panels, at 90° the panels are actually slightly over rotated relative to the viewer.  To get them to just disappear, you need to rotate them to arctan((z × 2) / y) where z is the height of the viewer (see perspective below) and y is the height of the panel (or roughly 83.9° in my case).  So you have to rotate the panels from 0 to 83.9° while animating the vertical offset of panels A-D from 0 to cos(±90°) × height.  This roughly works, but if you examine the animation closely, you either see a small gap open between 2 of the 4 panels or else an overlap of a few pixels between 2 of the panels.

Enter CATransformLayer

It turns out, I was thinking about this all wrong.  What I needed to use in order to keep all 4 of my panels together was CATransformLayers.  I was pointed in the right direction by this StackOverflow answer.  As Apple’s documentation says, “CATransformLayers are used to create true 3D layer hierarchies”.  Instead of treating all 4 panels as separate UIViews that need to be transformed independently, we will create the panels as 4 sublayers that are organized together along with CATransformLayers into the proper sublayer hierarchy within a single UIView.  The transform layers keep each panel layer properly adjoined to its neighbors.  To follow along with code, see my MPFoldTransition project (which was the culmination of this dive into folding animations), but in this post here I’ll be talking more about the big picture rather than code details.

I ended up building a layer hierarchy with all 4 panels contained within a single view, and panels B & C (the 2 folding panels) contained within the bounds of a single parent layer (the purpose of which will become clear in a bit).  The difference is that now all these layers are connected together relative to one another in a true 3D hierarchy, so when I rotate panel B away from the user, all 4 panels rotate like so:


In order to compensate for this rotation and to keep panel A flat, I need to rotate panel A in the opposite direction along its bottom edge by the same amount.


Then panel C needs to be rotated towards the user by double the angle of rotation of panel B.


And finally in order to remain flat, panel D needs to be rotated away from the user by the angle of rotation of panel B.  Notice how panels A & D need to be rotated just so that they appear to remain flat throughout the entire animation.  This is of course because each panel is being rotated relative to its parent layer and not relative to the plane of the entire view.

Perspective

This gives us all 4 panels well-connected with no gaps or overlaps, but we’re still not finished.  If you look at the last 2 pictures above you may notice a perspective issue; i.e. that panels B & C are not of equal height.  It’s more noticeable the closer you get to 90°.


This is a perspective issue similar to the one I faced in my original design.  To fix it, at the same time we animate the rotation of these 4 panels, we need to also animate the height of the 2 folding panels B & C to 0.  This keeps the 2 panels of equal height throughout the animation.  If you wish for the border between B & C to remain in a fixed position vertically (as I did), then you actually need to animate the height using a cosine curve rather than a simple linear one (see the code for the CAKeyframeAnimation implementation).

Shadows


Now that perspective is tackled, the next issue is one of improving the 3D illusion by dimming the folding panels as they collapse.  This is easily achieved by adding a sublayer to each folding panel layer and animating its opacity.


This helps, but when the 2 folding panels have the same background color (as they usually do), then they seem to be a single panel (morphing into an hourglass shape instead of folding away from us), which detracts from the 3D illusion.  To solve this, I adjusted the maximum opacity of the 2 shadow layers to be slightly different (set one  to be 90% of the other).  This provides a nice contrast between the 2 folding panels as seen here:

The proper (amount of) perspective

The perspective value that you set as the m34 component of your CATransform3D struct is = -1/z where z is the height of the viewer in points from the view’s surface.  Changing this value dramatically affects the illusion of perspective.  If 0 (z approaching infinity), everything is flat.

No perspective: m34 = 0

If too small (viewer still too far away) then the perspective is too subtle.

Too little perspective: m34 = -1/1800 or -1/(12 × height)

If too large (viewer too close) then the perspective is exaggerated and distorted

Too much perspective: m34 = -1/150 or -1/(1 × height)

I prefer to adjust z relative to the dimension of the objects I am animating. In this case I’ve been happy with 4.666667 × height. I think any value in the range from 4 to 5 yields attractive results.

Other Minutiae

Of course being the OCD-type, I just had to antialiase the edges of the folding panels so that they’re not jagged, even though my default animation duration is only 300 ms.


A different cool effect (which I termed “cubic” for wont of a better description) can be achieved if you rotate panels A & D at a fixed 90° angle relative to their neighbors instead of animating them to remain flat.  The effect is akin to rotating a block- one side appears while another disappears.


If the folding panels (B & C) are created by splitting a single view in 2, and that view has an odd height (in pixels, not points) then once again you see flickering of gaps between panels during the animation. To solve this I make both panels of integer pixel height. e.g. when splitting a view 99 pixels tall, I’ll make the panels 50 and 49 pixels tall instead of 49.5. (Note that on a Retina display 49.5 points is just fine because that’s really 99 pixels.)

Update: Gradient Shadows

I recently switched to using CAGradientLayers for the shadows.  I think these look much better than the solid color shadows, and they also create the impression of a crease between the two folding panels (because the light end of one gradient will be juxtaposed with the dark end of the other).

However, if the bottom sleeve (D) has the same background color as the lower folding panel (C), you end up with no crease if the gradient shadow blends all the way to clear.

Fixing this is simple – just set the end color of the lower gradient to around [[UIColor blackColor] colorWithAlphaComponent:0.25] instead of clear.  This will create enough color difference to suggest a crease while not darkening the lower panel overly much.

This isn’t necessary for the cubic effect, because then panels A and D will have their own shadow gradients.

To achieve the proper effect, I animate the opacity of the shadows on panels B & C along a cosine path so that their intensity is (inversely) proportional to the height of the panel.  For the cubic effect, the shadows on panels A & D are similarly animated along a sine path (again so as to be inversely proportional to panel height).  Using a simple linear animation would lead to a discrepancy between the strength of the shadows and the positions of the panels.

Resources

  • MPFoldTransition project with demo app and code you are free to use to easily add these same animations to your own iOS apps.
  • Enter The Matrix project with lots of matrix transformation examples, including both flip and fold animations (the project has now been updated to incorporate the techniques described above).
  • Edge Antialiasing post with a discussion of what it is, why you’d want to do it, and how to do it efficiently.  Also with its own sample project.
  • If you’re going to be on the East coast in June, come see me speak about these types of things at CocoaConf DC (and of course learn from a great lineup of even better speakers).

MPFoldTransition – add fold transitions to your app


I’ve put a project I call MPFoldTransition up on GitHub. It provides a class you can use to add a fold transition to your application in just a single line of code (in most cases).
What is a fold transition? It’s an animation such as shown above (or as popularized by the Clear todo list app) where an object folds in upon itself until it disappears and the content surrounding it moves in to fill the gap. Except in this case, I’m doing it for an entire UIView or UIViewController and not just a single UITableViewCell. (For an in-depth analysis of what goes into making a folding animation, read my post here.)

Update: The GitHub project now includes flip transitions as well!

Update 2: The folding effect has been improved with gradient shadows.

Update 3: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction


Controls whether the current view collapses inward (Fold) or if the next view expands outward (Unfold).

Mode


Determines whether the next view slides in flat (Normal) or rotates in at right angles to the collapsing halves of the previous view (Cubic).

Orientation


Sets whether the fold is vertical or horizontal.

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using fold transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    foldStyle:(MPFoldStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFoldStyle:(MPFoldStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  foldStyle:MPFoldStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFoldStyle:MPFoldStyleUnfold 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite fold bit (Fold or Unfold), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using fold transitions:

- (void)pushViewController:(UIViewController *)viewController
                 foldStyle:(MPFoldStyle)style;

- (UIViewController *)popViewControllerWithFoldStyle:
    (MPFoldStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    foldStyle:MPFoldStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFoldStyle:MPFoldStyleUnfold];

Tip: pop your view controller using a style with the opposite fold bit (Fold or Unfold) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFoldTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFoldStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFoldStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve), you can initialize your own instance of MPFoldTransition, set the properties as desired, and then call the perform method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack fold segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFoldModalSegue, MPFoldNavPushSegue, or MPFoldNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only for now because I wrote it with ARC and included storyboard support.

WWDC 2012 and indie conferences


Of course the big news yesterday was Apple’s announcement of WWDC 2012 and then it’s subsequent record sellout in a little under 2 hours.  I was fortuitous enough to have the means and opportunity to secure a ticket for myself, but many developers were not.

Fortunately for those wishing to participate in the technical instruction and peer networking of an iOS/Mac tech conference, there are a number of excellent indie conferences.  These conferences have the benefits of being both more intimate (i.e. easier to network) and more affordable than WWDC while still having great technical content.  Two excellent American conferences are CocoaConf (at which I will be speaking in June), a roaming conference that occurs in various underserved (in the technical conference sense) cities, and 360iDev which has now settled in beautiful Denver, CO every September.  Scott McAlister has compiled a list of 2012 iOS tech conferences on his blog here.

So if you crave that conference fix but didn’t get a ticket to WWDC this year (or even if you did), don’t offer to legally change your name, just go attend one of the many outstanding indie conferences instead.

CocoaConf DC 2012

I am extremely pleased to announce that I will be speaking at CocoaConf DC (actually in Herndon, VA) at the end of June.  This time I will be giving two presentations: one on container view controllers and the other on matrix transformations.

There are a slew of expert speakers on the schedule, and I’m really looking forward to attending as well as speaking.  At CocoaConf Chicago last month I met a bunch of great developers, watched some excellent presentations, learned many things, and just generally renewed my enthusiasm for iOS development.  And the conference hotel is just a few miles from the Air & Space Museum where Discovery now resides.  So book now while early bird tickets at $200 off are still available!


Title: Implementing Custom Container View Controllers

Abstract: iOS 5 introduced the ability to create your own custom container view controllers. Prior to iOS 5 you had to use only the stock controllers (tab, navigation, splitview, etc.) or attempt to roll your own, which was a complex endeavor and often hacky. Custom container view controllers are a great way to give your app a unique look and feel. Learn how to implement your own custom container view controller using the new API. We’ll build a page-flipping controller and cover the various gotchas that can arise along the way. The final product will be an open-source controller that you are free to use in your own apps or just study and take apart.



Title: Enter The Matrix

Abstract: Matrix transformations can make your user interfaces come to life: translate, scale, and rotate.  Each on its own is relatively simple and straightforward.  Yet many developers are daunted when 2 or more operations need to be combined.  What if you need to rotate or zoom about an off-center (or even off-screen) point?  How do you combine multiple transformations into a single animation?  How do you make advanced, polished 3D animations such as folding and flipping views?  Learn everything you need to know to get started with complex matrix transformations in CoreGraphics and CoreAnimation.  Tons of demos and full open-source source code provided.

On the importance of setting shadowPath

It’s super easy to add drop shadows to any view in iOS. All you need to do is

  1. add QuartzCore framework to your project (if not there already)
  2. import QuartzCore into your implementation file
  3. add a line such as [myView.layer setShadowOpacity:0.5]

and voilà, your view now has a drop shadow.


However, the easy way is rarely the best way in terms of performance.  If you have to animate this view (and especially if it’s part of a UITableViewCell) you will probably notice stutters in the animation.  This is because calculating the drop shadow for your view requires Core Animation to do an offscreen rendering pass to determine the exact shape of your view in order to figure out how to render its drop shadow.  (Remember, your view could be any complex shape, possibly even with holes in it.)

To convince yourself of this, turn on the Color Offscreen-Rendered option in the Simulator’s Debug menu.


Alternately, target a physical device, launch Instruments (⌘I), choose the Core Animation template, select the Core Animation instrument, and check the Color Offscreen-Rendered Yellow option.


Then in the Simulator (or on your device) you will see something like this:


Which indicates that something (in our case the drop shadow) is forcing an expensive offscreen rendering pass.

The quick fix

Fortunately, fixing the drop shadow performance is typically almost as easy as adding a drop shadow.  All you need to do is provide Core Animation with some information about the shape of your view to help it along.  Calling setShadowPath: on your view’s layer does exactly that:

[myView.layer setShadowPath:[[UIBezierPath 
    bezierPathWithRect:myView.bounds] CGPath]];

(Note: your code will vary depending on the actual shape of your view.  UIBezierPath has many convenience methods, including bezierPathWithRoundedRect:cornerRadius: in case you’ve rounded the corners of your view.)

Now run it again and confirm that the yellow wash for offscreen-rendered content is gone.

The catch

You will need to update the layer’s shadowPath each time the bounds of your view change.  And if you’re animating a change to bounds, then you will also need to animate the change to the layer’s shadowPath to match.  This will need to be a CAAnimation because UIView cannot animate shadowPath (which is a property on CALayer).  Fortunately, it is straight-forward to animate from one CGPath to another (from the old to new shadowPath) via CAKeyframeAnimation.

On the importance of setting contentScaleFactor in CATiledLayer-backed views

If you look at any samples for CATiledLayer (such as ZoomingPDFViewer), you will invariably see code like this:

// to handle the interaction between CATiledLayer and high // resolution screens, we need to manually set the tiling view's // contentScaleFactor to 1.0. (If we omitted this, it would be 2.0 // on high resolution screens, which would cause the CATiledLayer // to ask us for tiles of the wrong scales.)
pdfView.contentScaleFactor = 1.0

Without this line, on retina devices such as iPhone 4/4S or the latest iPad, your view will probably ask for 4x as many tiles as are necessary.  (Note that retina screens have 4x as many pixels as their non-retina counterparts, so unless you are doubling the dimensions of CATiledLayer tileSize for retina, your view will already ask for 4x as many tiles as for non-retina, so in this case your retina view would ask for 16x as many tiles as its non-retina version.)

That’s all well and good.  The point I wanted to make in this post was simply a reminder that if you are doing any view juggling with your CATiledLayer-backed views (such as dequeueing them for reuse as pages in an infinite scroll view), you need to set contentScaleFactor each time you add your view to the view hierarchy.  Otherwise, it will take on the contentScaleFactor of its new superview and it will start asking for the wrong tile sizes.

Update: It’s not just view juggling. Any time your CATiledLayer-backed view or any view higher up in its view hierarchy is added to a parent view hierarchy, then contentScaleFactor of your view will reset to 2 on a retina device.  This includes switching tabs in a UITabBarController or pushing a new view controller onto the navigation stack of a UINavigationController.  The way I’ve handled this is by overriding viewWillAppear: in the UIViewController that contains the CATiledLayer-backed view.

Update 2: Aaron Farnham wrote me to suggest that you simply override didMoveToWindow in your CATiledLayer-backed UIView and call setContentScaleFactor:1 there.  Like so:

- (void)didMoveToWindow {
    self.contentScaleFactor = 1.0;
}

Efficient Edge Antialiasing

This trick is an oldie, but still worth writing about I think.  The problem is that when a view’s edges are not straight (e.g. the view has been rotated), the edges are not antialiased by default and appear jagged.

Non-antialiased view on left, anti-aliased view on right

Detail of jagged non-antialiased edge

 

One Solution

Antialiasing is the process whereby a view’s edges are blended with the colors of the layer below it.  Antialiasing for view edges can be enabled systemwide by setting the UIViewEdgeAntialiasing flag in your app’s info.plist, but as the documentation warns, this can have a negative impact on performance (because it requires Core Animation to sample pixels from the render buffer beneath your layer in order to calculate the blending).

An Alternate Solution

If the view in question is static content (or can be rendered temporarily as static content during animation), then there is a more efficient alternative.  If you render the view as a UIImageView with a 1 point transparent boundary on all sides, then UIImageView will handle it for you (Core Animation will not have to sample the render buffer beneath your view layer).

Detail of smooth antialiased edge

 

How It Works

UIImageView has been highly optimized by Apple to work with the GPU, and one of the things it does is interpolate pixels within the image when the image is rotated or scaled.  Examine the UIImageView below- the outer edge is jagged, but the inner boundaries between the yellow and purple are properly interpolated by UIImageView (compare it to the UIView on the left in the first image near the top of this article).

UIImageView with jagged outer edges but smooth inner edges

Essentially what happens when you add the 1 point transparent margin around the outer edges of the UIImageView is that the visible border becomes internal pixels and UIImageView interpolates them with the neighboring transparent pixels just as it does for the rest of the image, thus eliminating the need to anti-aliase the edges with the layer below it.  The resulting image (now with partially transparent edge pixels) can now be rendered directly over the layer beneath it.

UIImageView with transparent edge- now all visible edges are smooth inner edges

 

How to render UIView as UIImage

You just create an image context, draw your view (or subset thereof) into the context, and get an image back.  This method lets you specify the exact frame (in the view’s coordinates) you want rendered.  Pass in view.bounds to render the entire view or pass a smaller rect to render just a subset (useful for splitting up views for animations).

+ (UIImage *)renderImageFromView:(UIView *)view withRect:(CGRect)frame
{
    // Create a new context of the desired size to render the image
    UIGraphicsBeginImageContextWithOptions(frame.size, YES, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Translate it, to the desired position
    CGContextTranslateCTM(context, -frame.origin.x, -frame.origin.y);
    // Render the view as image
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];
    // Fetch the image
    UIImage *renderedImage = UIGraphicsGetImageFromCurrentImageContext();
    // Cleanup
    UIGraphicsEndImageContext();
    return renderedImage;
}

How to add a transparent edge to UIImage

Again you just create an image context (this time slightly larger than your image), draw the original image into it (offset by a certain amount), then get the new larger image back.

+ (UIImage *)renderImageForAntialiasing:(UIImage *)image withInsets:(UIEdgeInsets)insets
{
    CGSize imageSizeWithBorder = CGSizeMake([image size].width + insets.left + insets.right, [image size].height + insets.top + insets.bottom);

    // Create a new context of the desired size to render the image
    UIGraphicsBeginImageContextWithOptions(imageSizeWithBorder, NO, 0);

    // The image starts off filled with clear pixels, so we don't need to explicitly fill them here	
    [image drawInRect:(CGRect){{insets.left, insets.top}, [image size]}];

    // Fetch the image   
    UIImage *renderedImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return renderedImage;
}

Putting it all together

But of course why create 2 image contexts and render twice when we can do it in a single step?

+ (UIImage *)renderImageFromView:(UIView *)view withRect:(CGRect)frame transparentInsets:(UIEdgeInsets)insets
{
    CGSize imageSizeWithBorder = CGSizeMake(frame.size.width + insets.left + insets.right, frame.size.height + insets.top + insets.bottom);
    // Create a new context of the desired size to render the image
    UIGraphicsBeginImageContextWithOptions(imageSizeWithBorder, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Clip the context to the portion of the view we will draw
    CGContextClipToRect(context, (CGRect){{insets.left, insets.top}, frame.size});
    // Translate it, to the desired position
    CGContextTranslateCTM(context, -frame.origin.x + insets.left, -frame.origin.y + insets.top);

    // Render the view as image
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];

    // Fetch the image   
    UIImage *renderedImage = UIGraphicsGetImageFromCurrentImageContext();

    // Cleanup
    UIGraphicsEndImageContext();

    return renderedImage;
}

Some things to remember

  1. Be sure to expand the size of your image view’s bounds to account for the transparent edges.  e.g. if the original image is 200 x 200 then resize to 202 x 202.  Otherwise (depending on its content mode) the image might shrink to fit its new size in its old bounds.
  2. This solution doesn’t work particularly well if the image is being scaled down.  You need to have 1 pixel of transparent edge at the scaled size, so if you are scaling by 0.25 you would need 4 points of transparent margin at the full image size.  But even then the results are often unsatisfactory.  Rasterization fixes it, but requires an additional expensive off-screen rendering pass.

Sample code

I created a simple sample project to demonstrate all this.  It has a regular UIView, a UIImageView copy with transparent edges, and a play/pause button to slowly rotate both views.  It’s on GitHub.

Note: the detail images were taken from the excellent xScope app by the Iconfactory.