How exactly to subclass CALayer and use a custom property? - iphone

I am trying to create a subclass of CALayer with a custom index property that I can both animate and change directly in order to display a different picture based on the index.
In the header, I declared:
#property NSUInteger index;
In the implementation, I overrode needDisplayForKey:
+ (BOOL)needsDisplayForKey:(NSString *)key
{
if ([key isEqualToString:#"index"])
return YES;
else
return [super needsDisplayForKey:key];
}
Now, in the display method, I want to display a picture based on the index. However, during an animation, the value of self.index never changes, so I have to query the value of the presentation layer, contrary to the example on Providing CALayer Content by Subclassing:
- (void)display
{
NSUInteger presentationIndex = [(CustomLayer *)[self presentationLayer] index];
self.contents = (id)[[images objectAtIndex:presentationIndex] CGImage];
}
The problem is, if I do that, I cannot set the index value directly outside of an animation, because it will only change the model layer, and the display method explicitly queries the presentation layer.
If I add an initialization method that copies the value of index, it works:
- (id)initWithLayer:(id)layer
{
self = [super initWithLayer:layer];
if (self) {
if ([layer isKindOfClass:[CustomLayer class]])
self.index = [(CustomLayer *)layer index];
}
return self;
}
However, after or before an animation, there is always a 1 image glitch because the presentation or the model value don't match (depending if I set index to the destination value or not).
Surprisingly, it seems like the drawInContext: method always has
the right value for [self index], but it is not the method I want to use since I just set the content property with an image.
I get different behaviors depending on the way I implement the index property. If I use #dynamic index (which works, even though the documentation doesn't say that custom property getters/setters would be dynamically implemented), display is called every time the value of index is changed. If I use #synthesize or implement a setter, display is not called, so I would need to change content in the setter too.
Should I use an instance variable? Should I use the dynamic implementation? Or should I use setValue:forKey: instead?
As you can see, I am a bit confused about how to get the result I want, and how to correctly implement a subclass of CALayer with a custom property. Any help, and explanations, would be appreciated!

There is an excellent tutorial on the web that explains how to create a custom CALayer subclass with animatable properties. I haven't had occasion to try it yet, but I bookmarked it. Here is the link:
Animating Pie Slices Using a Custom CALayer
It looks like the main tricks are:
Use #dynamic, not #synthesize for the animatable properties of your custom CALayer.
Override actionForKey:, initWithLayer:, and needsDisplayForKey:, and perhaps drawInContext:

Related

C4: Add panning to an object other than "self"

I watched the C4 tutorial on adding a pan gesture to an object and animating it to return to its original position when the panning is finished. I'm trying to add this to three individual objects. I have it working with one object so far to move it and reset it to a CGPoint, but for it to work, I have to add the pan gesture to "self", not the object. For reference, I'm pretty much using the code from here:
http://www.c4ios.com/tutorials/interactionPanning
If I add the gesture to the object itself, sure, it pans around, but then it just leaves itself at the last touch location. However, I'm assuming that leaving the gesture on "self" will affect more than just the object I want to move, and I want to be able to move the three objects individually.
I'm using roughly the same modification to the "move" method that's used in the example:
-(void)move:(UIPanGestureRecognizer *)recognizer {
[character move:recognizer];
if (recognizer.state == UIGestureRecognizerStateEnded) {
[character setCenter: charStartOrigin];
}
}
And then a new method to spawn the object:
-(void)createCharacters {
character = [C4Shape ellipse:charStart];
[character addGesture:PAN name:#"pan" action:#"move:"];
[self.canvas addShape:character];
}
The example link you are working from is sneaky. Since I knew that there was only going to be one object on the canvas I knew I could make it look like I was panning the label. This won't work for multiple objects, as you have already figured out.
To get different objects to move independently, and recognize when they are done being dragged, you need to subclass the objects and give them their own "abilities".
To do this I:
Subclass C4Shape
Add custom behaviour to the new class
Create subclassed objects on the canvas
The code for each step looks like the following:
subclassing
You have to create a subclass that gives itself some behaviour. Since you're working with shapes I have done it this way as well. I call my subclass Character, its files look like this:
Character.h
#import "C4Shape.h"
#interface Character : C4Shape
#property (readwrite, atomic) CGPoint startOrigin;
#end
I have added a property to the shape so that I can set its start origin (i.e. the point to which it will return).
Character.m
#import "Character.h"
#implementation Character
-(void)setup {
[self addGesture:PAN name:#"pan" action:#"move:"];
}
-(void)move:(UIGestureRecognizer *)sender {
if(sender.state == UIGestureRecognizerStateEnded) {
self.center = self.startOrigin;
} else {
[super move:sender];
}
}
#end
In a subclass of a C4 object, setup gets called in the same way as it does for the canvas... So, this is where I add the gesture for this object. Setup gets run after new or alloc/init are called.
The move: method is where I want to override with custom behaviour. In this method I catch the gesture recognizer, and if it's state is UIGestureRecognizerStateEnded then I want to animate back to the start origin. Otherwise, I want it to move: like it should so I simply call [super move:sender] which runs the default move: method.
That's it for the subclass.
Creating Subclassed Objects
My workspace then looks like the following:
#import "C4WorkSpace.h"
//1
#import "Character.h"
#implementation C4WorkSpace {
//2
Character *charA, *charB, *charC;
}
-(void)setup {
//3
CGRect frame = CGRectMake(0, 0, 100, 100);
//4
frame.origin = CGPointMake(self.canvas.width / 4 - 50, self.canvas.center.y - 50);
charA = [self createCharacter:frame];
frame.origin.x += self.canvas.width / 4.0f;
charB = [self createCharacter:frame];
frame.origin.x += self.canvas.width / 4.0f;
charC = [self createCharacter:frame];
//5
[self.canvas addObjects:#[charA,charB,charC]];
}
-(Character *)createCharacter:(CGRect)frame {
Character *c = [Character new];
[c ellipse:frame];
c.startOrigin = c.center;
c.animationDuration = 0.25f;
return c;
}
#end
I have added a method to my workspace that creates a Character object and adds it to the screen. This method creates a Character object by calling its new method (I have to do it this way because it is a subclass of C4Shape), turns it into an ellipse with the frame I gave it, sets its startOrigin, changes its animationDuration.
What's going on with the rest of the workspace is this (NOTE: the steps are marked in the code above):
I #import the subclass so that I can create objects with it
I create 3 references to Character objects.
I create a frame that I will use to build each of the new objects
For each object, I reposition frameby changing its origin and then use it to create a new object with the createCharacter: method I wrote.
I add all of my new objects to the canvas.
NOTE: Because I created my subclass with a startOrigin property, I am able within that class to always animate back to that point. I am also able to set that point from the canvas whenever I want.

loading custom slider from NIB vs from code: no subviews present when loaded from code

I am trying to create a custom UISlider. I need to insert subviews (like a label) to self. I'm using the
[self insertSubview:_label1 atIndex:2];
method to do so. Everything works fine when I create my slider in IB and assign my PWSlider class to it.
However, if I try to create my PWSlider from code, there are no subviews present when I do my inits (adding my subviews).
For the init in code I override initWithFrame, for the NIB-case I tried both, awakeFromNib as well as initWithCoder - same result.
When I debug with
NSLog(#"subview count: %d", [self.subviews count]);
the result is that loaded from NIB I get 3 subviews, loaded programmatically I get 0.
Any ideas?
I assume your PWSlider is inherited from UISlider and also assume you have code which is something like:
-(id)initWithFrame:(CGRect)r {
self = [super initWithFrame:r];
if(self) {
// add your subviews here, starting from index 0
}
return self;
}
Did you debug and check that your initWithFrame gets called?
Also, if you don't care at which locations your subviews are, use addSubview instead of insertSubview:atIndex:.

Annotation Sub Class custom initWithCoordinate

I've subclassed MKAnnotation so that i can assign objects to each annotation, this is so that when the rightCalloutAccessoryView is clicked i can push a navigation controller with the object passed to it and display the objects data in another view.
This all works great apart from one thing, i've extended upon initWithCoordinate like so:
-(id)initWithCoordinate:(CLLocationCoordinate2D)coord andObject:(NSManagedObject *)object {
[self setPlace:object];
coordinate = coord;
title = [place valueForKey:#"name"];
subtitle = [place valueForKey:#"address"];
return self;
}
Although everything is working great i'm recieving the warning:
NO '-initWithCoordinate:andObject:' method found
Which means i'm doing something wrong somewhere, what is the correct way to go about adding upon initWithCoorinate?
Put the prototype -(id)initWithCoordinate:(CLLocationCoordinate2D)coord andObject:(NSManagedObject *)object in .h file.

How to change the UIImage of a UIImageView from a subview?

I want to change an image on a view, from a popup dialog of 4-6 icons (imagine like changing your image on a messenger application).
The way I implement this modal popup is by creating a new view at IB, with opacity on the background, and then I load this as a subview:
IconsViewController *iconsViewController = [[IconsViewController alloc] initWithNibName:#"IconsView" bundle:nil];
[self.view addSubview:iconsViewController.view];
So, when the user touches an icon, I have
- (IBAction)iconIsSelected:(id)sender {
switch ([sender tag]) {
case 1:
[(ParentViewController*)[self superview] changeIcon];
break;
case 2:
// same here..
break;
default:
break;
}
[self.view removeFromSuperview];
[self release];
}
The changeIcon just sets the image to a corresponding icon.
As you can guess, this is not working - the changeIcon message never works.
I can't understand what am I doing wrong, any help much appreciated!
You have a few choices here...
First one is create a property on your IconsViewController of type ParentViewController*, for example:
#property (readwrite,nonatomic,assign) ParentViewController* parentController; // weak reference
To break this down further:
readwrite because we want to be able to access the value via [self parentController] but also change it via [iconsViewController setParentController:self]
nonatomic because I'm not too worried about threading
assign to make it a "weak reference" where the parent will not be retained by the child. If they each retain the other, it could lead to memory leaks later because unless explicitly released you'd end up with a retain circle causing neither object to hit a zero retain count.
When you load from nib, set the property:
IconsViewController *iconsViewController = [[IconsViewController alloc] initWithNibName:#"IconsView" bundle:nil];
iconsViewController.parentController = self;
Then, call to it from inside of iconIsSelected like this:
[[self parentController] changeIcon];
Alternatively, you can create a delegate protocol:
#protocol IconViewSelectedDelegate (NSObject)
- (void) changeIcon;
#end
And use that protocol as a property, instead of the parent view controller type. This is more abstract, but it keeps the design cleaner. The parent view controller would then implement that delegate protocol, as one of many others.
Another option is to use NSNotificationCenter and publish/subscribe to events from your dynamic view. This is the "loosest" coupling between the two objects, but it might be overkill for this scenario.
The superview of a view is a view, not a view controller, yet you cast the superview to be of class ParentViewController. If the view has no superview, it returns nil, and message to nil are no-ops (which explains why you don't crash there).
BTW, that [self release] at the end is highly suspicious.

Why does UITextView use string rather than mutable string for its text property?

Any idea from a code design standpoint why does the internal implementation of UITextView uses an NSString but not an NSMutableString when its content is meant to change often?
From a general coding point of view:
When setting a property the property setter method is called. That way the control is able to notice when the property is changed, so that it can redraw the control with the new content.
If the property is a mutable object, you can change its contents and the control will not get any notification that this has happened, so it doesn't know that the control needs to be redrawn.
It's a general pattern in Cocoa to pass around immutable objects instead of allowing outside classes access private mutable variables. You'll see the same thing with collections classes like NSArray and NSDictionary.
Of course, there's no reason you can't change what it points to! Because the member is just a pointer, you can replace the string with an NSMutableString yourself if you want.
This might be a more efficient approach if you want to append a lot of text to the view.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[myTextView setText:[[NSMutableString alloc] init]];
}
return self;
}
Just be sure to still call setText to because as #Guffa explained in his answer, otherwise the view won't know to redraw itself.
- (void)appendText:(NSString*)text
{
NSMutableString *dispText = (NSMutableString*)[myTextView text];
[dispText appendString:text];
[myTextView setText:dispText]; // notify myTextView of text change!
}