Custom property animation with actionForKey: how do I get the new value for the property? - core-animation

In a CALayer subclass I'm working on I have a custom property that I want to animate automatically, that is, assuming the property is called "myProperty", I want the following code:
[myLayer setMyProperty:newValue];
To cause a smooth animation from the current value to "newValue".
Using the approach of overriding actionForKey: and needsDisplayForKey: (see following code) I was able to get it to run very nicely to simply interpolate between the old and and new value.
My problem is that I want to use a slightly different animation duration or path (or whatever) depending on both the current value and the new value of the property and I wasn't able to figure out how to get the new value from within actionForKey:
Thanks in advance
#interface ERAnimatablePropertyLayer : CALayer {
float myProperty;
}
#property (nonatomic, assign) float myProperty;
#end
#implementation ERAnimatablePropertyLayer
#dynamic myProperty;
- (void)drawInContext:(CGContextRef)ctx {
... some custom drawing code based on "myProperty"
}
- (id <CAAction>)actionForKey:(NSString *)key {
if ([key isEqualToString:#"myProperty"]) {
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:key];
theAnimation.fromValue = [[self presentationLayer] valueForKey:key];
... I want to do something special here, depending on both from and to values...
return theAnimation;
}
return [super actionForKey:key];
}
+ (BOOL)needsDisplayForKey:(NSString *)key {
if ([key isEqualToString:#"myProperty"])
return YES;
return [super needsDisplayForKey:key];
}
#end

You need to avoid custom getters and setters for properties you want to animate.
Override the didChangeValueForKey: method. Use it to set the model value for the property you want to animate.
Don't set the toValue on the action animation.
#interface MyLayer: CALayer
#property ( nonatomic ) NSUInteger state;
#end
-
#implementation MyLayer
#dynamic state;
- (id<CAAction>)actionForKey: (NSString *)key {
if( [key isEqualToString: #"state"] )
{
CABasicAnimation * bgAnimation = [CABasicAnimation animationWithKeyPath: #"backgroundColor"];
bgAnimation.fromValue = [self.presentationLayer backgroundColor];
bgAnimation.duration = 0.4;
return bgAnimation;
}
return [super actionForKey: key];
}
- (void)didChangeValueForKey: (NSString *)key {
if( [key isEqualToString: #"state"] )
{
const NSUInteger state = [self valueForKey: key];
UIColor * newBackgroundColor;
switch (state)
{
case 0:
newBackgroundColor = [UIColor redColor];
break;
case 1:
newBackgroundColor = [UIColor blueColor];
break;
case 2:
newBackgroundColor = [UIColor greenColor];
break;
default:
newBackgroundColor = [UIColor purpleColor];
}
self.backgroundColor = newBackgroundColor.CGColor;
}
[super didChangeValueForKey: key];
}
#end

Core Animation calls actionForKey: before updating the property value. It runs the action after updating the property value by sending it runActionForKey:object:arguments:. The CAAnimation implementation of runActionForKey:object:arguments: just calls [object addAnimation:self forKey:key].
Instead of returning the animation from actionForKey:, you can return a CAAction that, when run, creates and installs an animation. Something like this:
#interface MyAction: NSObject <CAAction>
#property (nonatomic, strong) id priorValue;
#end
#implementation MyAction
- (void)runActionForKey:(NSString *)key object:(id)anObject arguments:(NSDictionary *)dict {
ERAnimatablePropertyLayer *layer = anObject;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key];
id newValue = [layer valueForKey:key];
// Set up animation using self.priorValue and newValue to determine
// fromValue and toValue. You could use a CAKeyframeAnimation instead of
// a CABasicAnimation.
[layer addAnimation:animation forKey:key];
}
#end
#implementation ERAnimatablePropertyLayer
- (id <CAAction>)actionForKey:(NSString *)key {
if ([key isEqualToString:#"myProperty"]) {
MyAction *action = [[MyAction alloc] init];
action.priorValue = [self valueForKey:key];
return action;
}
return [super actionForKey:key];
}
You can find a working example of a similar technique (in Swift, animating cornerRadius) in this answer.

You can store the old and new values in CATransaction.
-(void)setMyProperty:(float)value
{
NSNumber *fromValue = [NSNumber numberWithFloat:myProperty];
[CATransaction setValue:fromValue forKey:#"myPropertyFromValue"];
myProperty = value;
NSNumber *toValue = [NSNumber numberWithFloat:myProperty];
[CATransaction setValue:toValue forKey:#"myPropertyToValue"];
}
- (id <CAAction>)actionForKey:(NSString *)key {
if ([key isEqualToString:#"myProperty"]) {
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:key];
theAnimation.fromValue = [[self presentationLayer] valueForKey:key];
theAnimation.toValue = [CATransaction objectForKey:#"myPropertyToValue"];
// here you do something special.
}
return [super actionForKey:key];
}

Related

how can i display score in other scene?

I have to show score with SKLabel in gameOverScene. how can i show score in GameOverScene Label? I am tried, please help me.
My game scene codes here. You can see all details about score in down stair.
MyScene.m
#interface MyScene ()<SKPhysicsContactDelegate>
#property NSUInteger score;
#end
-(void)setupUI
{
self.score = 0;
SKLabelNode *scoreLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
scoreLabel.fontColor = [SKColor redColor];
scoreLabel.fontSize = 20.0;
scoreLabel.text = #"SCORE: 0";
scoreLabel.name = #"scoreLabel";
scoreLabel.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
scoreLabel.position = CGPointMake(self.size.width/2, self.size.height - scoreLabel.frame.size.height);
[self addChild:scoreLabel];
}
-(void)adjustScoreBy:(NSUInteger)points {
self.score += points;
SKLabelNode* score = (SKLabelNode*)[self childNodeWithName:#"scoreLabel"];
score.text = [NSString stringWithFormat:#"SCORE: %lu", (unsigned long)self.score];
}
- (void)gameOver
{
GameOverScene *gameOverScene = [GameOverScene sceneWithSize:self.size];
[self.view presentScene:gameOverScene transition:[SKTransition pushWithDirection:SKTransitionDirectionLeft duration:0.5]];
}
GameOverScene.h
#interface GameOverScene : SKScene
#property NSUInteger *score;
#end
GameOverScene.m
#implementation GameOverScene
{
SKLabelNode *scoreLabel;
}
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.backgroundColor = [SKColor colorWithRed:1.5 green:1.0 blue:0.5 alpha:0.0];
[self addStartButton];
[self addRateButton];
[self addBackButton];
[self addScoreLabel];
}
return self;
}
-(void)addScoreLabel
{
scoreLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
scoreLabel.text = [NSString stringWithFormat:#"SCORE: %lu", (unsigned long)self.score];
scoreLabel.position = CGPointMake(500, 50);
scoreLabel.name = #"gameOverScore";
[self addChild:scoreLabel];
}
There are several approaches to do this.
You could use a singleton class to handle that.
Other option would be to create a public score property in GameOverScene, and then pass the score value of MyScene to GameOverScene, something like this:
In GameOverScene.h add a score property
#interface GameOverScene : SKScene
#property NSUInteger score;
#end
Then in you gameOver method set the score value
- (void)gameOver
{
GameOverScene *gameOverScene = [GameOverScene sceneWithSize:self.size];
gameOverScene.score = self.score;
[self.view presentScene:gameOverScene transition:[SKTransition pushWithDirection:SKTransitionDirectionLeft duration:0.5]];
}
In GameOverScene create didMoveToView
- (void)didMoveToView:(SKView *)view
{
[self addScoreLabel];
}
You just need a property in your next scene (target class) and call it from (void)gameOver (in the source class)
Add it int your target class like this
#property int score;
then use it like this in your source class
gameOverScene.score = self.score
You will use this kind of stuff a lot for when moving data around.

CCLayer is null

I am trying to change the color of a CCLayerColor called bgColorLayer, however when I check to see if it is initialized it returns null. I have a color picker that calls the setBGColor: method. I know the colorpicker is calling the method and it is spitting out the correct colors. I am just at a loss as to why the bgColorLayer is null.
This is Cocos2D for Mac.
Any thoughts on why?
In my AppDelegeate method I have an IBOUTLET that is is tied to the NSColorWell
- (IBAction)colorwellBackground:(id)sender {
NSLog(#"Color Well: %#", [sender color]);
// Yes I know the sender color isn’t passing the correct value
AnimationViewerLayer * bkg = [AnimationViewerLayer alloc];
[bkg setBGColor:[sender color]];
}
AnimationViewerLayer.h
#interface AnimationViewerLayer : CCLayer
{
CCLayerColor * bgColorLayer;
}
+ (CCScene *) scene;
#end
AnimationViewLayer.m
#import "AnimationViewerLayer.h"
#implementation AnimationViewerLayer
+(CCScene *) scene
{
CCScene *scene = [CCScene node];
AnimationViewerLayer *layer = [AnimationViewerLayer node];
[scene addChild: layer];
return scene;
}
-(id) init
{
if( (self=[super init])) {
float red = 25.0 * 255;
bgColorLayer = [CCLayerColor layerWithColor:ccc4(57, 109, 58, 255)];
[self addChild:bgColorLayer z:1];
}
return self;
}
- (void) setBGColor: (ccColor3B) color{
NSLog(#"SET BG COLOR");
[bgColorLayer setColor:ccRED];
}
- (void) dealloc {
[super dealloc];
}
#end

Singleton not updating variable immediately

I have a singleton here is the header file:
#import <Foundation/Foundation.h>
#interface Shared : NSObject
{
NSString *messages;
}
#property (nonatomic, retain) NSString *messages;
+ (Shared*)sharedInstance;
#end
Here is the implementation:
#import "Shared.h"
static Shared* sharedInstance;
#implementation Shared
#synthesize messages;
+ (Shared*)sharedInstance
{
if ( !sharedInstance)
{
sharedInstance = [[Shared alloc] init];
}
return sharedInstance;
}
- (id)init
{
self = [super init];
if ( self )
{
messages = [[NSString alloc] init];
}
return self;
}
#end
The problem is when the I use
[Shared sharedInstance].messages = someVariable;
I can use
NSLog([Shared sharedInstance].messages);
and it shows the right output, but when i check from another class, NSLog doesn't show any output. I have the NSLog in the viewDidLoad method of another class, so when I click a button to go to the next view, it should output the value of the string, but it only works the second time. If the variable is set to dog, first it outputs nothing, then when I close the view and try again, it outputs dog. however, if I then change the variable to cat, it will output dog, and on the next attempt, output cat. I want it to update immediately, rather than remain one behind all the time.
EDIT: Here's the code from the other classes
This particular section is from a view controller class in the method
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
//Omitted, just preparing the DB, and emptying the array.
if ([db open])
{
FMResultSet *s = [db executeQueryWithFormat:#"SELECT ShabadID FROM Shabad WHERE Gurmukhi LIKE %#", currentLine];
while ([s next])
{
lineID = [s intForColumn:#"ShabadID"];
}
s = [db executeQueryWithFormat:#"SELECT Gurmukhi, ShabadID FROM Shabad WHERE ShabadID LIKE %i", lineID];
while ([s next])
{
//NSLog([s stringForColumn:#"Gurmukhi"]);
[paragraphArray addObject:[s stringForColumn:#"Gurmukhi"]];
}
Text = #"";
for (int i = 0; i<[paragraphArray count]; i++)
{
Text = [Text stringByAppendingFormat:#"%#\n", [paragraphArray objectAtIndex:i]];
}
[Shared sharedInstance].messages = Text;
}
Then in the another class, where I want the text to appear, in the viewDidLoad method,
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog([Shared sharedInstance].messages);
UITextView *myUITextView = [[UITextView alloc] initWithFrame:CGRectMake(0,30,310,450)];
myUITextView.text = [Shared sharedInstance].messages;
myUITextView.textAlignment = NSTextAlignmentCenter;
myUITextView.textColor = [UIColor blackColor];
myUITextView.font = [UIFont fontWithName:#"GurbaniLipiLight" size:24];
[myUITextView setBackgroundColor:[UIColor clearColor]];
myUITextView.editable = NO;
myUITextView.scrollEnabled = YES;
[ScrollerView addSubview:myUITextView];
}
Sure the NSLog doesn't show up right, but neither does the text in the textview, it does the same thing the NSLog does.
There is an assumption here about what order things happen in that's not quite right. Assuming there's a segue involved in this, didSelectRowAtIndexPath: is called after the new view controller is prepared but before it's displayed. Moving code to viewWillAppear: or viewDidAppear: delays execution until after the calling controller has set new data.
The other approach for communication between controllers that use a segue, is to use prepareForSegue: in the first controller to set data that the second controller needs. That way it should be available when the view is loaded.

How do I use UIPageViewController to load separate XIBs?

I'm delving into the new world of UIPageViewControllers and there are a lot of tutorials out there, however all of them seem to create one view, and then just use new instances of it with different content.
I'd really like to be able to create multiple XIBs and then just chain them together with the UIPageViewController but it's too new and I can't get my head around the way it works.
Well, here's a long answer that you should be able to copy and paste. (This code was adapted from Erica Sadun (https://github.com/erica/iOS-5-Cookbook))
First, create a new class of type UIPageViewController. Call it BookController. Now paste the following code in your .h file.
// Used for storing the most recent book page used
#define DEFAULTS_BOOKPAGE #"BookControllerMostRecentPage"
#protocol BookControllerDelegate <NSObject>
- (id) viewControllerForPage: (int) pageNumber;
#optional
- (void) bookControllerDidTurnToPage: (NSNumber *) pageNumber;
#end
#interface BookController : UIPageViewController <UIPageViewControllerDelegate, UIPageViewControllerDataSource>
+ (id) bookWithDelegate: (id) theDelegate;
+ (id) rotatableViewController;
- (void) moveToPage: (uint) requestedPage;
- (int) currentPage;
#property (assign) id <BookControllerDelegate> bookDelegate;
#property (nonatomic, assign) uint pageNumber;
and in your .m file:
#define IS_IPHONE ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
#define SAFE_ADD(_Array_, _Object_) {if (_Object_ && [_Array_ isKindOfClass:[NSMutableArray class]]) [pageControllers addObject:_Object_];}
#define SAFE_PERFORM_WITH_ARG(THE_OBJECT, THE_SELECTOR, THE_ARG) (([THE_OBJECT respondsToSelector:THE_SELECTOR]) ? [THE_OBJECT performSelector:THE_SELECTOR withObject:THE_ARG] : nil)
#pragma Utility Class - VC that Rotates
#interface RotatableVC : UIViewController
#end
#implementation RotatableVC
- (void) loadView
{
[super loadView];
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
self.view.backgroundColor = [UIColor whiteColor];
}
- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return YES;
}
#end
#pragma Book Controller
#implementation BookController
#synthesize bookDelegate, pageNumber;
#pragma mark Debug / Utility
- (int) currentPage
{
int pageCheck = ((UIViewController *)[self.viewControllers objectAtIndex:0]).view.tag;
return pageCheck;
}
#pragma mark Page Handling
// Update if you'd rather use some other decision style
- (BOOL) useSideBySide: (UIInterfaceOrientation) orientation
{
BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation);
return isLandscape;
}
// Store the new page and update the delegate
- (void) updatePageTo: (uint) newPageNumber
{
pageNumber = newPageNumber;
[[NSUserDefaults standardUserDefaults] setInteger:pageNumber forKey:DEFAULTS_BOOKPAGE];
[[NSUserDefaults standardUserDefaults] synchronize];
SAFE_PERFORM_WITH_ARG(bookDelegate, #selector(bookControllerDidTurnToPage:), [NSNumber numberWithInt:pageNumber]);
}
// Request controller from delegate
- (UIViewController *) controllerAtPage: (int) aPageNumber
{
if (bookDelegate &&
[bookDelegate respondsToSelector:#selector(viewControllerForPage:)])
{
UIViewController *controller = [bookDelegate viewControllerForPage:aPageNumber];
controller.view.tag = aPageNumber;
return controller;
}
return nil;
}
// Update interface to the given page
- (void) fetchControllersForPage: (uint) requestedPage orientation: (UIInterfaceOrientation) orientation
{
BOOL sideBySide = [self useSideBySide:orientation];
int numberOfPagesNeeded = sideBySide ? 2 : 1;
int currentCount = self.viewControllers.count;
uint leftPage = requestedPage;
if (sideBySide && (leftPage % 2)) leftPage--;
// Only check against current page when count is appropriate
if (currentCount && (currentCount == numberOfPagesNeeded))
{
if (pageNumber == requestedPage) return;
if (pageNumber == leftPage) return;
}
// Decide the prevailing direction by checking the new page against the old
UIPageViewControllerNavigationDirection direction = (requestedPage > pageNumber) ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
[self updatePageTo:requestedPage];
// Update the controllers
NSMutableArray *pageControllers = [NSMutableArray array];
SAFE_ADD(pageControllers, [self controllerAtPage:leftPage]);
if (sideBySide)
SAFE_ADD(pageControllers, [self controllerAtPage:leftPage + 1]);
[self setViewControllers:pageControllers direction: direction animated:YES completion:nil];
}
// Entry point for external move request
- (void) moveToPage: (uint) requestedPage
{
[self fetchControllersForPage:requestedPage orientation:(UIInterfaceOrientation)[UIDevice currentDevice].orientation];
}
#pragma mark Data Source
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
[self updatePageTo:pageNumber + 1];
return [self controllerAtPage:(viewController.view.tag + 1)];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
[self updatePageTo:pageNumber - 1];
return [self controllerAtPage:(viewController.view.tag - 1)];
}
#pragma mark Delegate
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
NSUInteger indexOfCurrentViewController = 0;
if (self.viewControllers.count)
indexOfCurrentViewController = ((UIViewController *)[self.viewControllers objectAtIndex:0]).view.tag;
[self fetchControllersForPage:indexOfCurrentViewController orientation:orientation];
BOOL sideBySide = [self useSideBySide:orientation];
self.doubleSided = sideBySide;
UIPageViewControllerSpineLocation spineLocation = sideBySide ? UIPageViewControllerSpineLocationMid : UIPageViewControllerSpineLocationMin;
return spineLocation;
}
-(void)dealloc{
self.bookDelegate = nil;
}
#pragma mark Class utility routines
// Return a UIViewController that knows how to rotate
+ (id) rotatableViewController
{
UIViewController *vc = [[RotatableVC alloc] init];
return vc;
}
// Return a new book
+ (id) bookWithDelegate: (id) theDelegate
{
BookController *bc = [[BookController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
bc.dataSource = bc;
bc.delegate = bc;
bc.bookDelegate = theDelegate;
return bc;
}
This Class can now be used to control any book you create in any project, and for multiple books in a single project. For each book, create a delegate UIPageViewController with the #interface:
#interface NameOfBookController : UIPageViewController <BookControllerDelegate>
In the .m file of this delegate, include:
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView
{
[super loadView];
CGRect appRect = [[UIScreen mainScreen] applicationFrame];
self.view = [[UIView alloc] initWithFrame: appRect];
self.view.backgroundColor = [UIColor whiteColor];
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
// Establish the page view controller
bookController = [BookController bookWithDelegate:self];
bookController.view.frame = (CGRect){.size = appRect.size};
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
// Add the child controller, and set it to the first page
[self.view addSubview:bookController.view];
[self addChildViewController:bookController];
[bookController didMoveToParentViewController:self];
}
Then add:
- (id) viewControllerForPage: (int) pageNumber
{
// Establish a new controller
UIViewController *controller;
switch (pageNumber) {
case 0:
view1 = [[FirstViewController alloc] init];
view1.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
controller = view1;
//rinse and repeat with each new view controller
break;
case 1:
//etc.
break;
default:
return nil;
break;
}
return controller;
}
For the record, this code is not memory-safe. If using ARC, add in your #autoreleasePool{}; if not, don't forget your retain/release cycle.
I hope this helps!
This article shows how to create an app using UIPageViewController with custom viewcontrollers for each page: http://www.informit.com/articles/article.aspx?p=1760500&seqNum=6

doubts about setters and getters

I have a class, Object2D, with the following interface:
#class Point2D;
#class Vector2D;
#interface Object2D : NSObject
{
Point2D* position;
Vector2D* vector;
ShapeType figure;
CGSize size;
}
#property (assign) CGSize size;
#property ShapeType figure;
- (Point2D*) position;
- (void)setPosition:(Point2D *)pos;
- (Vector2D*) vector;
- (void)setVector:(Vector2D *)vec;
- (id)initWithPosition:(Point2D*)pos vector:(Vector2D*)vec;
- (void)move:(CGRect)bounds;
- (void)bounce:(CGFloat)boundryNormalAngle;
- (void)draw:(CGContextRef)context;
#end
And implementation:
#implementation Object2D
#synthesize size;
#synthesize figure;
- (Point2D*) position
{
return position;
}
- (void)setPosition:(Point2D *)pos
{
if (position != nil) {
[position release];
}
position = pos;
}
- (Vector2D*) vector
{
return vector;
}
- (void)setVector:(Vector2D *)vec
{
if (vector != nil) {
[vec release];
}
vector = vec;
}
...
- (void) dealloc
{
if (position != nil) {
[position release];
}
if (vector != nil) {
[vector release];
}
}
I don't use #synthesize with position and vector because I think, I have to release them before change their values.
I have two question:
If I want to get position value, is this correct? Will I get a reference to position or a new one?
ball = [[Object2D alloc] init];
ball.position = [[Point2D alloc] initWithX:0.0 Y:0.0];
ball.vector = [[Vector2D alloc] initWithX:5.0 Y:4.0];
Point2D* pos2;
pos2 = ball.position; <-- IS THIS CORRECT?
Second question:
Do I need to release previous values before assign new ones to position and vector?
If you use...
#property (nonatomic, retain) Point2D *position;
and the same for vector it will do the retain / release for you. (and #synthesize them obviously!)
So if you do the following....
Point2D *newPosition = [[Point2D alloc] init];
[myObject setPosition:newPosition];
[newPosition release];
OR
[myObject setPosition:[[[Point2D alloc] init] autorelease]];
Then the retains / releases will be handled. You will need to add a dealloc to your Object2D class and do EITHER....
[position release];
OR
[self setPosition:nil];
both will release the object upon cleanup.
I've found an error on my code. Correct setters methods could be:
- (void)setPosition:(Point2D *)pos
{
[pos retain];
[position release];
position = pos;
}
- (void)setVector:(Vector2D *)vec
{
[vec retain];
[vector release];
vector = vec;
}