How to get a CAEmitterLayer from Canvas - ios

I am trying to follow the tutorial about iOS particle systems here: http://www.raywenderlich.com/6063/uikit-particle-systems-in-ios-5-tutorial
I am having trouble casting the self.canvas.layer in C4Workspace.m to a CAEmitterLayer. The code compiles just fine but fails at runtime.
I tried this:
particlesystem = (CAEmitterLayer *)self.canvas.layer;
But I receive this error every time.
-[C4Layer setEmitterPosition:]: unrecognized selector sent to instance 0xa183830
It seems that I am not casting or exposing methods properly. How might I do this?

You cannot simply cast one layer as another. In order for a view to have a non-standard layer, you need to subclass it and define the +layerClass method:
#implementation MyViewSubclass
+ (Class)layerClass {
return [CAEmitterLayer class];
}
...
Unfortunately for your case, the view you're working with has already set up a custom layer, C4Layer, which can be seen on GitHub. This layer is doing a lot and you don't want to try replacing it.
What you can do is insert your own sublayer into your canvas:
CAEmitterLayer *myLayer = [CAEmitterLayer layer];
myLayer.frame = self.canvas.bounds;
[self.canvas.layer addSublayer:myLayer];
This emitter layer will now overlay your layer and you can add any effects you want. If you want the emitter below other layers, you can use insertSublayer:myLayer atIndex:0.

Related

How to animate drawing in Swift, but also change a UIImageView's scale?

I'd like to animate a drawing sequence. My code draws a spiral into a UIImageView.image. The sequence changes the image contents, but also changes the scale of the surrounding UIImageView. The code is parameterized for the number of revolutions of the spiral:
func drawSpiral(rotations:Double) {
let scale = scaleFactor(rotations) // do some math to figure the best scale
UIGraphicsBeginImageContextWithOptions(mainImageView.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: scale, y: scale) // some animation prohibits changes!
// ... drawing happens here
myUIImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}
For example, I'd like to animate from drawSpiral(2.0) to drawSpiral(2.75) in 20 increments, over a duration of 1.0 seconds.
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values? How? Is there a better animation approach?
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values
Animation is merely a succession of timed intermediate values being thrown at something. It is perfectly reasonable to ask that they be thrown at your code so that you can do whatever you like with them. Here's how.
You'll need a special layer:
class MyLayer : CALayer {
#objc var spirality : CGFloat = 0
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(spirality) {
return true
}
return super.needsDisplay(forKey:key)
}
override func draw(in con: CGContext) {
print(self.spirality) // in real life, this is our signal to draw!
}
}
The layer must actually be in the interface, though it can be impossible for the user to see:
let lay = MyLayer()
lay.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.view.layer.addSublayer(lay)
Subsequently, we can initialize the spirality of the layer:
lay.spirality = 2.0
lay.setNeedsDisplay() // prints: 2.0
Now when we want to "animate" the spirality, this is what we do:
let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.spirality))
ba.fromValue = lay.spirality
ba.toValue = 2.75
ba.duration = 1
lay.add(ba, forKey:nil)
CATransaction.setDisableActions(true)
lay.spirality = 2.75
The console shows the arrival of a succession of intermediate values over the course of 1 second!
2.03143266495317
2.04482554644346
2.05783333256841
2.0708108600229
2.08361491002142
2.0966724678874
2.10976020619273
2.12260236591101
2.13551922515035
2.14842618256807
2.16123360767961
2.17421661689878
2.18713565543294
2.200748950243
2.21360073238611
2.2268518730998
2.23987507075071
2.25273013859987
2.26560932397842
2.27846492826939
2.29135236144066
2.30436328798532
2.31764804571867
2.33049770444632
2.34330793470144
2.35606706887484
2.36881992220879
2.38163591921329
2.39440815150738
2.40716737508774
2.42003352940083
2.43287514150143
2.44590276479721
2.45875595510006
2.47169743478298
2.48451870679855
2.49806520342827
2.51120449602604
2.52407149970531
2.53691896796227
2.54965999722481
2.56257836520672
2.57552136480808
2.58910304307938
2.60209316015244
2.6151298135519
2.62802086770535
2.64094598591328
2.6540260463953
2.6669240295887
2.6798157542944
2.69264766573906
2.70616912841797
2.71896715462208
2.73285858333111
2.74564798176289
2.75
2.75
2.75
Those are exactly the numbers that would be thrown at an animatable property, such as when you change a view's frame origin x from 2 to 2.75 in a 1-second duration animation. But now the numbers are coming to you as numbers, and so you can now do anything you like with that series of numbers. If you want to call your method with each new value as it arrives, go right ahead.
Personally, in more complicated animations I would use lottie the animation itself is built with Adobe After Effect and exported as a JSON file which you will manage using the lottie library this approach will save you time and effort when you port your app to another platform like Android as they also have an Android Lottie which means the complicated process of creating the animation is only done once.
Lottie Files has some examples animations as well for you to look.
#Matt provided the answer and gets the checkmark. I'll recap some points for emphasis:
UIView animation is great for commonly animated properties, but if
you need to vary a property not on UIView's animatable list, you can't use it. You must
create a new CALayer and add a CABasicAnimation(keyPath:) to it.
I tried but was unable to get my CABasicAnimations to fire by adding them to the default UIView.layer. I needed to create a custom CALayer
sublayer to the UIView.layer - something like
myView.layer.addSublayer(myLayer)
Leave the custom sublayer installed and re-add the CABasicAnimation to that sublayer when (and only when) you want to animate drawing.
In the custom CALayer object, be sure to override class func needsDisplay(forKey key: String) -> Bool with your key property (as #Matt's example shows), and also override func draw(in cxt: CGContext) to do your drawing. Be sure to decorate your key property with #objc. And reference the key property within the drawing code.
A "gotcha" to avoid: in the UIView object, be sure to null out the usual draw method (override func draw(_ rect: CGRect) { }) to avoid conflict between animated and non-animated drawing on the separate layers. For coordinating animated and non-animated content in the same UIView, it's good (necessary?) to do all your drawing from your custom layer.
When doing that, use myLayer.setNeedsDisplay() to update the non-animated content within the custom layer; use myLayer.add(myBasicAnimation, forKey:nil) to trigger animated drawing within the custom layer.
As I said above, #Matt answered - but these items seemed worth emphasizing.

Obtain Bezier Path of CALayer

CALayer objects have a property accessibilityPath which as stated is supposedly
Returns the path of the element in screen coordinates.
Of course as expected this does not return the path of the layer.
Is there a way to access the physical path of a given CALayer that has already been created? For instance, how would you grab the path of a UIButton's layer property once the button has been initialized?
EDIT
For reference, I am trying to detect whether a rotated button contains a point. The reason for the difficulty here is due to the fact that the buttons are drawn in a curved view...
My initial approach was to create bezier slices then pass them as a property to the button to check if the path contains the point. For whatever reason, there seems to be an ugly offset from the path and the button.
They are both added to the same view and use the same coordinates / values to determine their frame, but the registered path seems to be offset to the left from the actual drawn shape from the path. Below is an image of the shapes I have drawn. The green outline is where the path is drawn (and displayed....) where the red is approximately the area which registers as inside the path...
I'm having a hard time understanding how the registered area is different.
If anyone has any ideas on why this offset would be occurring would be most appreciated.
UPDATE
Here is a snippet of me adding the shapes. self in this case is simply a UIView added to a controller. it's frame is the full size of the controller which is `{0, height_of_device - controllerHeight, width_of_device, controllerHeight}
UIBezierPath *slicePath = UIBezierPath.new;
[slicePath moveToPoint:self.archedCenterRef];
[slicePath addArcWithCenter:self.archedCenterRef radius:outerShapeDiameter/2 startAngle:shapeStartAngle endAngle:shapeEndAngle clockwise:clockwise];
[slicePath addArcWithCenter:self.archedCenterRef radius:(outerShapeDiameter/2 - self.rowHeight) startAngle:shapeEndAngle endAngle:shapeStartAngle clockwise:!clockwise];
[slicePath closePath];
CAShapeLayer *sliceShape = CAShapeLayer.new;
sliceShape.path = slicePath.CGPath;
sliceShape.fillColor = [UIColor colorWithWhite:0 alpha:.4].CGColor;
[self.layer addSublayer:sliceShape];
...
...
button.hitTestPath = slicePath;
In a separate method in my button subclass to detect if it contains the point or not: (self here is the button of course)
...
if ([self.hitTestPath containsPoint:touchPosition]) {
if (key.alpha > 0 && !key.isHidden) return YES;
else return NO;
}
else return NO;
You completely missunderstood the property, this is for assistive technology, from the docs:
Excerpt:
"The default value of this property is nil. If no path is set, the accessibility frame rectangle is used to highlight the element.
When you specify a value for this property, the assistive technology uses the path object you specify (in addition to the accessibility frame, and not in place of it) to highlight the element."
You can only get the path from a CAShapeLayer, alls other CALayers don't need to be drawn with a path at all.
Update to your update:
I think the offset is due to a missing
UIView convert(_ point: CGPoint, to view: UIView?)
The point needs to be converted to the buttons coordinate systems.

UIKit Dynamics and CALayers decoupled from UIView

Is it possible to not use UIViews when playing with UIKit Dynamics, and instead bind the physics properties, anchors, forces etc to CALayers all within one UIView?
According to this page by Ash Farrow:
UIDynamicItem is a protocol that defines a centre, a bounds, and a
transform (only two-dimensional transforms are used). UIView conforms
to this protocol, and is the most common use of UIDynamicBehaviour.
You can also use UICollectionViewLayoutAttributes with
UIDynamicBehaviours, but we’re not going to cover that today.
Which seems to indicate that anything can be made to conform to the protocol UIDynamicItem, but I can't find anything about others playing around with making CALayers.
Can it be done more directly than this approach:
https://www.invasivecode.com/weblog/uikit-dynamics-layer-constraint/
As the linked article notes, CALayer has conflicting types for these methods, so it can't directly conform to UIDynamicItem. What you'd need would be some kind of adapter layer that converted the CALayer methods into the types and meanings needed by UIDynamicItem.
Luckily, UIKit offers exactly such an adapter layer. It's called UIView. Just put each layer in a view and nest the views.
I had long thought of views as very heavyweight, particularly coming from the OS X world. So I always thought "if you want this to be fast, you'd better do it with a layer." But it turns out that views are actually quite thin and there isn't a lot of overhead vs a layer in most cases, particularly for the numbers you're talking about in iOS. See the TearOff demo for an example that does UIKItDynamics with many dozens of views without trouble.
If you really do need layers, then yes, you can definitely still do it. Just create the adapter yourself. It just needs to hold a CALayer and conform to UIKitDynamics. It doesn't have to do anything else. Something like this should work:
public class DynamicLayerItem : NSObject, UIDynamicItem {
let layer: CALayer
init(layer: CALayer) { self.layer = layer}
public var center: CGPoint { get {
return layer.position
}
set {
layer.position = center
}
}
public var bounds: CGRect { return layer.bounds }
public var transform: CGAffineTransform {
get {
return layer.affineTransform()
}
set {
layer.transform = CATransform3DMakeAffineTransform(newValue)
}
}
}
You'd just animate this object, and that causes the layer to animate equivalently.

UIDynamicItem update transform manually

I know that external change to center, bounds and transform will be ignored after UIDynamicItems init.
But I need to manually change the transform of UIView that in UIDynamicAnimator system.
Every time I change the transform, it will be covered at once.
So any idea? Thanks.
Any time you change one of the animated properties, you need to call [dynamicAnimator updateItemUsingCurrentState:item] to let the dynamic animator know you did it. It'll update it's internal representation to match the current state.
EDIT: I see from your code below that you're trying to modify the scale. UIDynamicAnimator only supports rotation and position, not scale (or any other type of affine transform). It unfortunately takes over transform in order to implement just rotation. I consider this a bug in UIDynamicAnimator (but then I find much of the implementation of UIKit Dynamics to classify as "bugs").
What you can do is modify your bounds (before calling updateItem...) and redraw yourself. If you need the performance of an affine transform, you have a few options:
Move your actual drawing logic into a CALayer or subview and modify its scale (updating your bounds to match if you need collision behaviors to still work).
Instead of attaching your view to the behavior, attach a proxy object (just implement <UIDyanamicItem> on an NSObject) that passes the transform changes to you. You can then combine the requested transform with your own transform.
You can also use the .action property of the UIDynamicBehavior to set your desired transform at every tick of the animation.
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
attachment.damping = 0.8f;
attachment.frequency = 0.8f;
attachment.action = ^{
CGAffineTransform currentTransform = item.transform;
item.transform = CGAffineTransformScale(currentTransform, 1.2, 1.2)
};
You would need to add logic within the action block to determine when the scale should be changed, and by how much, otherwise your view will always be at 120%.
Another way to fix this (I think we should call it bug) is to override the transform property of the UIView used. Something like this:
override var transform: CGAffineTransform {
set (value) {
}
get {
return super.transform
}
}
var ownTransform: CGAffineTransform. {
didSet {
super.transform = ownTransform
}
}
It's a kind of a hack, but it works fine, if you don't use rotation in UIKitDynamics.

iOS CALayer and TapGestureRecognizer

I'm developing an app on iOS 6.1 for iPad.
I've a problem with the CALayer and a TapGestureRecognizer.
I've 7 CALayers forming a rainbow (every layer is a colour).
Every layer is built using a CAShapeLayer generate from a CGMutablePathRef. Everything works fine. All the layers are drawn on screen and I can see a beautiful rainbow.
Now I want to detect the tap above a single color/layer. I try this way:
- (void)tap:(UITapGestureRecognizer*)recognizer
{
//I've had the tapGestureRecognizer to rainbowView (that is an UIView) in viewDidLoad
CGLayer* tappedLayer = [rainbowView.layer.presentationlayer hitTest:[recognizer locationInView:rainbowView];
if (tappedLayer == purpleLayer) //for example
NSLog(#"PURPLE!");
}
I don't understand why this code won't work! I've already red other topics in here: all suggest the hitTest: method for solving problems like this. But in my case I can't obtain the desired result.
Can anyone help me? Thanks!!
EDIT:
Here's the code for the creation of paths and layers:
- (void)viewDidLoad
{
//Other layers
...
...
//Purple Arc
purplePath = CGPathCreateMutable();
CGPathMoveToPoint(purplePath, NULL, 150, 400);
CGPathAddCurveToPoint(purplePath, NULL, 150, 162, 550, 162, 550, 400);
purpletrack = [CAShapeLayer layer];
purpletrack.path = purplePath;
purpletrack.strokeColor = [UIColor colorWithRed:134.0/255.0f green:50.0/255.0f blue:140.0/255.0f alpha:1.0].CGColor;
purpletrack.fillColor = nil;
purpletrack.lineWidth = 25.0;
[rainbowView.layer insertSublayer:purpletrack above:bluetrack];
}
This was my first approach to the problem. And the touch didn't work.
I also tried to create a RainbowView class where the rainbow was drawing in drawRect method using UIBezierPaths.
Then I follow the "Doing Hit-Detection on a Path" section in http://developer.apple.com/library/ios/#documentation/2ddrawing/conceptual/drawingprintingios/BezierPaths/BezierPaths.html
In this case the problem was the path variable passed to the method. I try to compare the UIBezierPath passed with the paths in RainbowView but with no results.
I could try to create curves instead of paths. In this case maybe there isn't a fill part of figure and the touching area is limited to the stroke. But then how can I recognize the touch on a curve?
I'm so confused about all of these stuff!!! :D
The problem you are facing is that you are checking agains the frame/bounds of the layer when hit testing and not agains the path of the shape layer.
If your paths are filled you should instead use CGPathContainsPoint() to determine if the tap was inside the path. If your paths aren't filled but instead stroked I refer you to Ole Begemann's article about CGPath Hit Testing.
To make your code cleaner you could do the hit testing in your own subclass. Also, unless the layer is animating when hit testing it makes no sense using the presentationLayer.

Resources