Thursday, June 23, 2011

Dynamic view reflection using CAReplicatorLayer



If you've done UIView reflection on iOS before, you've probably used a method that creates an image from the current view and redraws it with a gradient underneath the view. For many applications this is sufficient, but it has problems: it's slow, and for some objects (e.g. UIWebView) you're really not sure when the object is done drawing so you have to tweak it with arbitrary timers to create the reflection after the object is done rendering. Yuk.

An alternative is to use a CAReplicatorLayer (It was mentioned in a WWDC session this year). The CAReplicatorLayer lets you create copies of your main layer that update in real time. You specify the number of layers and then you specify their offset attributes from the primary layer.

Below is some code that uses the CAReplicatorLayer to reflect a view. The important stuff in this example is in the view's "layoutSubviews" method. Also note that to create a CAReplicatorLayer for your subview, you've got to override the subview's "layerClass" method as shown here.

Try it with a UIWebView or other scrollable, dynamically updating subview - it's pretty cool :)

UPDATE: In the interest of completeness, I've reworked some of the code to be a little more usable...



#import "Reflector.h"
#import <QuartzCore/QuartzCore.h>



@interface MySubview : UIWebView
@end

@implementation MySubview

+ (Class) layerClass
{
        return [CAReplicatorLayer class];
}

@end


@implementation Reflector

MySubview *subview;
CAGradientLayer *gl=nil;
BOOL hasReflector;

- (void)setup
{
        hasReflector = YES;
        
        [self setBackgroundColor:[UIColor blackColor]];
        subview = [[MySubview alloc] initWithFrame:self.bounds];
        [self addSubview:subview];
        [subview setBackgroundColor:[UIColor clearColor]];
        
        // . . . do whatever with the subview to create content
        [subview setScalesPageToFit:YES];
        [subview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.flickr.com"]]];

        
}

- (void)layoutSubviews
{
        
        if (hasReflector){
                int gap = 4; // gap between the subview and its reflection
                
                float h1 = ceil(self.frame.size.height *.6);         // subview height
                float h2 = self.frame.size.height - (h1 + gap);        // reflection height
                                
                // size the subview to make space for the reflection
                [subview setFrame:CGRectMake(0, 0, self.frame.size.width, h1)];
                
                // since the replicated layers will be sized using a scale
                // transform, we need to translate our absolute heights into
                // a scalar.
                double scale = (h2/h1);
                
                // configure the subviews replicator layer.  just two instances - the first is the
                // "real-life" rendering of the subview, the 2nd is the reflection
                CAReplicatorLayer *l = (CAReplicatorLayer *) subview.layer;
                l.instanceCount = 2;
                
                // position the instance transform.  the reflection instance will be
                // scaled by "scale" and is centered within the space of the original
                // instance, thus we compute "delta" by taking the original height, 
                // subtracting out the reflection layer size, and then dividing by half.
        
                
                double delta = (h1 - h2) / 2.0 ;
                CATransform3D t = CATransform3DMakeTranslation(0, (h1+gap)-delta, 0);
                t = CATransform3DRotate(t, M_PI, 1, 0, 0);
                t = CATransform3DScale(t, 1, scale, 1);
                
                l.instanceTransform = t;
                
                if (gl == nil){
                        // add a black gradient layer
                        gl = [CAGradientLayer layer];
                        CGColorRef c1 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:.5] CGColor];
                        CGColorRef c2 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:1] CGColor];
                        [gl setColors:[NSArray arrayWithObjects:(id)c1, (id)c2, nil]];
                        
                        [self.layer addSublayer:gl];
                }
                
                // position the gradient layer over the replication layer 2nd instance
                [gl setAnchorPoint:CGPointMake(0, 0)];
                [gl setFrame:CGRectMake(0, h1 + gap, self.frame.size.width, h2)];
                
        }
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
        self = [super initWithCoder:aDecoder];
        if (self){
                [self setup];
        }
        return self;
}
- (void)dealloc {
    [super dealloc];
}


@end

Thursday, June 16, 2011

Setting up a Mac OSX partition for beta development

In the past I've waited until final (GA) versions of MacOS, Xcode, iOS, etc, have been released so that I didn't pollute my development environment with potentially bad software. I've usually got one or two projects in development at any given time and I couldn't take the risk that I'd get into a state where I couldn't build and release the final project.

I know that some people move around their /Developer directory but that seemed a little risky to me; the possibility is high that I'd forget which state I was in at any given time. Also it doesn't help with new OS versions.

When I was at WWDC last week, someone suggested partitioning the drive and saving new stuff there. That's turned out to be a great idea. (so far, anyways)

To start with, I found a nice online reference for how to create a partition on the "for dummies" site. While the instructions in there didn't exactly match what I saw onscreen, they were close enough to get me going.

Here's what I did:

BTW, I make no warranties, express or implied, that this will work for you. Anytime you mess around with partitioning a disk you run the risk of completely screwing up your system so this is just an explanation of what worked for me.

Anyways, to create the new partition, I did the following:

  1. Went into Applications->Utilities->Disk Utility
  2. Selected the main disk on my macbook air.
  3. Selected the Partition button. At this point, a nice graphic of my disk use showed up.
  4. Selected the little "+" at the bottom left of the Partition Layout. That allowed me to then size my new partition.
  5. I made the new partition about 100 GB since I intended on using this for all beta stuff going forward.
  6. I called the new partition "Beta Software" and selected "Apply".


Next, I installed Mac OSX 10.7 (Lion) (BETA) into the new partition.

Then, to boot from the new partition, I selected the Startup Disk from Settings and selected my "Beta Software" partition. Clicked "Restart" and I was in the new environment. Anytime I want to switch back, I just change the startup disk. Sweet.

One thing I've noticed after restarting in 10.7, was that the "Beta Software" partition wasn't listed in "Finder". To do that I had to right-click on "Macintosh HD", select "Open enclosing folder", and then drag "Beta Software" into "Devices". (Maybe that'll change with the GA version of Lion).

So the good news is that I can now verify our software on early release code without screwing up my current development environment. The bad news is I've already found a major issue with our software running on iOS5. Hopefully they'll fix it so I don't have to :)

Wednesday, June 8, 2011

Merge image function

Here's a simple method in a UIImage category that I'm using to merge two images together. It's based mostly on the code here but is cleaned up and allows a few options.

UIImage+Utils.m

// NOTE! this method should only be called from the main thread because of
// UIGraphicsGetImageFromCurrentImageContext();
- (UIImage *)merge:(UIImage *)image atRect:(CGRect)pos overlay:(BOOL)overlay;
{
        UIGraphicsBeginImageContext(self.size);
        
        UIImage *bottom = (overlay)?self:image;
        UIImage *top = (overlay)?image:self;
        
        CGRect lf = CGRectMake(0, 0, self.size.width, self.size.height);
        
        CGRect bottomRect = (overlay)?lf:pos;
        CGRect topRect = (overlay)?pos:lf;
        
        [bottom drawInRect:bottomRect];
        [top drawInRect:topRect];

        UIImage *destImage = UIGraphicsGetImageFromCurrentImageContext();    
        UIGraphicsEndImageContext();
        return destImage;        
}

Thursday, June 2, 2011

duh: multiple calls to viewDidLoad

Somehow I've been writing iPhone apps for nearly three years, yet still didn't "get" (or perhaps just didn't remember) that a UIViewController's viewDidLoad method may be called multiple times. This means that I have occasionally been putting code into viewDidLoad that I assumed would only be run once per UIViewController instance.

Wrong wrong wrong.

Even beginner iOS coders know that if a memory warning occurs, hidden views are unloaded to free space for new objects - What I didn't think about (or remember) is that when the freed view needs to be used again, those views are reloaded using viewDidLoad.

Here's an example of the problem:

In the app I'm working on, I'm programmatically adding a subview in viewDidLoad. That subview's purpose is to display a picture and allow users to select from the camera roll or UIImagePickerController - To support various operations, my subview includes a data object which is initialized in viewDidLoad. In normal operations, when the parent view controller exits, it saves off the URL of the picture from the subview's data object.

However, on a device (it works great on the simulator), when the UIImagePickerController is shown, it creates a memory warning which causes my subview to be freed. When the UIImagePickerController is released, it then calls viewDidLoad again to recreate that subview, thus losing any modifications (e.g. URL) to the data object.

So that I don't forget this again, this goes into the save.txt file.