CGContextDrawImage without Transaction animation - iphone

How do I draw a layer without a transaction animation? For example, when I set the contents of a layer using CATransaction it works well:
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
myLayer.contents = (id)[[imagesTop objectAtIndex:Number]CGImage];
[CATransaction commit];
but I need to change contents from the delegate method [myLayer setNeedsDisplay]. Here is the code:
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
CGContextDrawImage(context, rect, Image.CGImage]);
}

CALayer implements setNeedsDisplay method:
Calling this method will cause the receiver to recache its content. This will result in the layer receiving a drawInContext: which may result in the delegate receiving either a displayLayer: or drawLayer:inContext: message.
... and displayIfNeeded:
When this message is received the layer will invoke display if it has been marked as requiring display.
If you wrap [myLayer displayIfNeeded] in a CATransaction, you can turn off implicit animation.

You can subclass CALayer and override actionForKey:,
- (id <CAAction>) actionForKey: (NSString *) key
{
if ([key isEqualToString: #"contents"])
return nil;
return [super actionForKey: key];
}
This code disables the built-in animation for the contents property.
Alternatively, you can achieve the same effect by implementing the layer's delegate method
- (id <CAAction>) actionForLayer: (CALayer *) layer forKey: (NSString *) key
{
if (layer == myLayer) {
if ([key isEqualToString: #"contents"])
return nil;
return [[layer class] defaultActionForKey: key];
}
return [layer actionForKey: key];
}

Related

CALayer on CATiledLayer, drawLayer:inContext never called

i have a UIView which display a PDF page using CATiledLayer, now i want to add another CALayer on TiledLayer to draw some annots, Please see the code below.
+ (Class)layerClass
{
return [TiledLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
TiledLayer *tiledLayer = (id)self.layer;
tiledLayer.backgroundColor = [UIColor whiteColor].CGColor;
NSAssert([tiledLayer isKindOfClass:[TiledLayer class]], #"self.layer must be CATiledLayer");
drawingLayer = [CALayer layer];
drawingLayer.frame = frame;
//its crashing if set the delegate, if not drawLayer is never called.
[drawingLayer setDelegate:self];
[self.layer addSublayer:self.drawingLayer];
}
}
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
if(layer == [self layer])
{
[self drawPDFPage];
[drawingLayer setNeedsDisplay];
}
else if(layer == drawingLayer)
{
//this one is never called.
[self drawSomethinghere];
}
}
UIView uses the existence of -drawRect: to determine if it should allow its CALayer to be invalidated, which would then lead to the layer creating a backing store and -drawLayer:inContext: being called.
Implementing an empty -drawRect: method allows UIKit to continue to implement this logic, while doing the real drawing work inside of -drawLayer:inContext:.
Just add to you class implementation:
-(void)drawRect:(CGRect)r
{
}
And your drawLayer method should work.

Is there a way to get notified when my UIImageView.image property changes?

Is there a way to set an observer on a UIImageView.image property, so I can get notified of when the property has been changed? Perhaps with NSNotification? How would I go about doing this?
I have a large number of UIImageViews, so I'll need to know which one the change occurred on as well.
How do I do this? Thanks.
This is called Key-Value Observing. Any object that is Key-Value Coding compliant can be observed, and this includes objects with properties. Have a read of this programming guide on how KVO works and how to use it. Here is a short example (disclaimer: it might not work)
- (id) init
{
self = [super init];
if (!self) return nil;
// imageView is a UIImageView
[imageView addObserver:self
forKeyPath:#"image"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
return self;
}
- (void) observeValueForKeyPath:(NSString *)path ofObject:(id) object change:(NSDictionary *) change context:(void *)context
{
// this method is used for all observations, so you need to make sure
// you are responding to the right one.
if (object == imageView && [path isEqualToString:#"image"])
{
UIImage *newImage = [change objectForKey:NSKeyValueChangeNewKey];
UIImage *oldImage = [change objectForKey:NSKeyValueChangeOldKey];
// oldImage is the image *before* the property changed
// newImage is the image *after* the property changed
}
}
In Swift, use KVO. E.g.
imageView.observe(\.image, options: [.new]) { [weak self] (object, change) in
// do something with image
}
more discussions How i detect changes image in a UIImageView in Swift iOS

iPhone - Catching the end of a grouped animation

I animate 2 views, each one with its CAAnimationGroup that contains 2 CAAnimations. They are launched at the same time (if computing time is negligible), and have the same duration.
How may I do to know when both grouped animation is finished.
I put the - (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag delegate method, but... What may I test ? Sounds simple, but I don't see the way of doing this.
You can use two variables to track whether the animations have completed:
BOOL firstAnimFinished;
BOOL secondAnimFinished;
then in your animationDidStop delegate you check which animation is calling the method and set the flags appropriately. The catch is that you'll need to add a key to identify the animations when they call the delegate (the animations you created will not be the ones calling the delegate, which is a subject for another question/rant). For example:
// when you create the animations
[firstAnmim setValue: #"FirstAnim" ForKey: #"Name"];
[secondAnmim setValue: #"SecondAnim" ForKey: #"Name"];
// Your delegate
- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)flag {
NSString* name = [theAnimation valueForKey: #"Name"];
if ([name isEqualToString: #"FirstAnim"]) {
firstAnimFinished = YES;
} else if ([name isEqualToString: #"SecondAnim"]) {
secondAnimFinished = YES;
}
if (firstAnimFinished && secondAnimFinished) {
// ALL DONE...
}
}

Why do CALayer's move slower than UIView's?

I have a UIView subclass that moved around 'event's when the user touches them, using the following overridden method:
// In a custom UIView...
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint point = [[touches anyObject] locationInView:self];
UIView *eventView = [self.ringOfEvents hitTest:point withEvent:event];
for (EventView *e in self.events) {
if (e == eventView) {
event.position = point;
}
}
}
Why is it that when I make EventView a CALayer instead of UIView, the movement slows down to a creep? I can post that code too, but it is so similar that I don't think it is necessary.
I would think that abstracting to a lower level would speed up event handling, but I must be missing something.
By the way, either if *eventView is a subclass of UIView or CALayer, the position property is as follows:
- (void)setPosition:(CGPoint)pos {
self.layer.position = pos;
}
- (CGPoint)position {
return self.layer.position;
}
Not sure why I get a huge decrease in latency when using UIView as apposed to CALayer..
Most CALayer properties are changed with animation by default, so decrease in latency is probably caused by that.
You may want to disable animations when layer position is changed. Possible solutions are discussed for example in here and here
This is due to implicit animations.
I've implemented a category method which removes implicit animation for givven keys and can be used like this
[view.layer setNullAsActionForKeys:#[#"bounds", #"position"]];
Implementation
#implementation CALayer (Extensions)
- (void)setNullAsActionForKeys:(NSArray *)keys
{
NSMutableDictionary *dict = [self.actions mutableCopy];
if(dict == nil)
{
dict = [NSMutableDictionary dictionaryWithCapacity:[keys count]];
}
for(NSString *key in keys)
{
[dict setObject:[NSNull null] forKey:key];
}
self.actions = dict;
}
#end

MapView - Have Annotations Appear One at a Time

I'm currently adding annotations to my map through a loop... but the annotations are only appearing on my map in groups. Also, on load, only about 4 annotations are actually displayed on the map... but as I move the map a little, all of the annotations that should be there, suddenly appear.
How can I get all of the annotations to load in the right place, one at a time?
Thanks in advance!
Here is the code I'm using to add annotations:
NSString *incident;
for (incident in weekFeed) {
NSString *finalCoordinates = [[NSString alloc] initWithFormat:#"%#", [incident valueForKey:#"coordinates"]];
NSArray *coordinatesArray = [finalCoordinates componentsSeparatedByString:#","];
latcoord = (#"%#", [coordinatesArray objectAtIndex:0]);
longcoord = (#"%#", [coordinatesArray objectAtIndex:1]);
// Final Logs
NSLog(#"Coordinates in NSString: [%#] - [%#]", latcoord, longcoord);
CLLocationCoordinate2D coord;
coord.latitude = [latcoord doubleValue];
coord.longitude = [longcoord doubleValue];
DisplayMap *ann = [[DisplayMap alloc] init];
ann.title = [NSString stringWithFormat: #"%#", [incident valueForKey:#"incident_type"]];
ann.subtitle = [NSString stringWithFormat: #"%#", [incident valueForKey:#"note"]];
ann.coordinate = coord;
[mapView addAnnotation:ann];
[ann release];
}
// Custom Map Markers
-(MKAnnotationView *)mapView:(MKMapView *)map viewForAnnotation:(id <MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil; //return nil to use default blue dot view
static NSString *AnnotationViewID = #"annotationViewID";
MKAnnotationView *annotationView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:AnnotationViewID];
if (annotationView == nil) {
annotationView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:AnnotationViewID] autorelease];
}
annotationView.canShowCallout = YES;
if ([annotationView.annotation.title isEqualToString:#"one"]) {
UIImage *pinImage = [UIImage imageNamed:#"marker_1.png"];
[annotationView setImage:pinImage];
}
if ([annotationView.annotation.title isEqualToString:#"two"]) {
UIImage *pinImage = [UIImage imageNamed:#"marker_2.png"];
[annotationView setImage:pinImage];
}
annotationView.annotation = annotation;
return annotationView;
}
- (void) mapView:(MKMapView *)mapV didAddAnnotationViews:(NSArray *)views {
CGRect visibleRect = [mapV annotationVisibleRect];
for (MKAnnotationView *view in views) {
CGRect endFrame = view.frame;
CGRect startFrame = endFrame; startFrame.origin.y = visibleRect.origin.y - startFrame.size.height;
view.frame = startFrame;
[UIView beginAnimations:#"drop" context:NULL];
[UIView setAnimationDuration:0.4];
view.frame = endFrame;
[UIView commitAnimations];
}
}
Adam,
This solution is a bit messy as I had to munge up one of my current projects to test, but hopefully this will work for you.
First an explanation, it's critical to separate data from UI presentation. The [MKMapView addAnnotation(s)] are just a data update to MKMapView and have no direct impact on animation or timing.
The delegate method mapView:didAddAnnotationViews: is where all of the custom presentation behavior should be defined. In your description you didn't want these to appear all at once, so you need to sequence your animations instead of performing them simultaneously.
One method is to add all of the annotations at once and then just add them with increasing animation delays, however new annotations that get added for whatever reason will begin their animations at zero again.
The method below sets up an animation queue self.pendingViewsForAnimation (NSMutableArray) to hold annotation views as they are added and then chains the animation sequentially.
I've replaced the frame animation with alpha to focus on the animation problem to separate it from the issue of some items not appearing. More on this after the code...
// Interface
// ...
// Add property or iVar for pendingViewsForAnimation; you must init/dealloc the array
#property (retain) NSMutableArray* pendingViewsForAnimation;
// Implementation
// ...
- (void)processPendingViewsForAnimation
{
static BOOL runningAnimations = NO;
// Nothing to animate, exit
if ([self.pendingViewsForAnimation count]==0) return;
// Already animating, exit
if (runningAnimations)
return;
// We're animating
runningAnimations = YES;
MKAnnotationView* view = [self.pendingViewsForAnimation lastObject];
[UIView animateWithDuration:0.4 animations:^(void) {
view.alpha = 1;
NSLog(#"Show Annotation[%d] %#",[self.pendingViewsForAnimation count],view);
} completion:^(BOOL finished) {
[self.pendingViewsForAnimation removeObject:view];
runningAnimations = NO;
[self processPendingViewsForAnimation];
}];
}
// This just demonstrates the animation logic, I've removed the "frame" animation for now
// to focus our attention on just the animation.
- (void) mapView:(MKMapView *)mapV didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView *view in views) {
view.alpha = 0;
[self.pendingViewsForAnimation addObject:view];
}
[self processPendingViewsForAnimation];
}
Regarding your second issue, items are not always appearing until you move the map. I don't see any obvious errors in your code, but here are some things I would do to isolate the problem:
Temporarily remove your mapView:didAddAnnotationViews:, mapView:annotationForView: and any other custom behaviors to see if default behavior works.
Verify that you have a valid Annotation at the addAnnotation: call and that the coordinates are visible (use [mapView visibleMapRect], MKMapRectContainsPoint(), and MKMapPointForCoordinate().
If it is still not functioning, look at where you are calling the add annotations code from. I try to avoid making annotation calls during map movement by using performSelector:withObject:afterDelay. You can precede this with an [NSObject cancelPreviousPerformRequestsWithTarget:selector:object:] to create a slight delay prior to loading annotations in case the map is being moved a long distance with multiple swipes.
One last point, to achieve the pin-drop effect you're looking for, you probably want to offset by a fixed distance from the original object instead of depending on annotationVisibleRect. Your current implementation will result in pins moving at different speeds depending on their distance from the edge. Items at the top will slowly move into place while items at the bottom will fly rapidly into place. Apple's default animation always drops from the same height. An example is here: How can I create a custom "pin-drop" animation using MKAnnotationView?
Hope this helps.
Update:
To demonstrate this code in action I've attached a link to a modified version of Apple's Seismic demo with the following changes:
Changed Earthquake.h/m to be an MKAnnotation object
Added SeismicMapViewController.h/m with above code
Updated RootViewController.h/m to open the map view as a modal page
See: http://dl.dropbox.com/u/36171337/SeismicXMLWithMapDelay.zip
You will need to pause updating after you add each annotation and allow the map to have time to refresh.