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.
Related
I have a Canvas UIView as below. (Following https://youtu.be/E2NTCmEsdSE)
class Canvas: UIView {
override func draw(_ rect: CGRect){
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
// Other codes
context.setLineWidth(10)
context.setLineCap(.round)
context.strokePath()
}
// Other codes
}
Looks like I can only setLineWidth and setLineCap inside the draw function. That means whenever a draw happens, they will get set again and again.
I wonder, is there a way for me to set the drawing attribute once per launch and not to set over and over again?
AFAIK UIKit uses contexts to draw almost everything, changing attributes globally would have a huge impact.
What you could do is to extend CGContext to either set the attributes you want or perform the stroke with the attributes you want.
extension CGContext {
func applyAppStyle() {
context.setLineWidth(10)
context.setLineCap(.round)
}
func strokeWithAppStyle() {
context.setLineWidth(10)
context.setLineCap(.round)
context.strokePath()
}
}
As you've been told, you can certainly move the multiple lines of code off to a utility function if you don't like having them march down the page inside the draw method.
Another possibility is to construct, in advance, in code, a UIImage (by drawing into its context), and then just draw that into the view context in the draw method. Surprisingly, though, that is less efficient than what you are doing, because it means storing all the pixels and then copying them all into the context at draw time.
The important thing to understand is that your code is correct, as it stands. What you are doing is what you do. Your code is not inefficient or slow; it may seem verbose, but that's just the way things are. Drawing commands are highly optimized; drawing is, after all, the very essence of app display.
Keep in mind that the context does not necessarily persist, and that calls to draw are not particularly frequent (indeed, it is not uncommon for a view to be told to draw just once in the lifetime of the app).
So just obey the rules: when you are told to draw, do the complete drawing.
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.
I've created a animatable Core Graphics drawing using PaintCode. It's basically a circle meter (not unlike the Apple Watch rings), which basically fills up as a timer counts down. The meterLevel controls the fill level of the circle meter, from 0 to 100. Basically, if a timer is set to 10 seconds, I set the meterLevel every 1 seconds to 90, 80, 70, etc...
This works good, however the animation is only drawn ever 1 second, and looks quite choppy. Instead, I'd like it be a smooth continuous filling meter.
Looking around it seemed like subclassing CALayer and creating an implicit animation for the meterLevel property might be the way to go. So here is what I have:
import UIKit
class MeterControlView: UIView
{
var meterLevel: Int = 0 {
didSet {
self.layer.setValue(meterLevel, forKey: "meterLevel")
}
}
var meterText: String = "00:00:00" {
didSet {
self.layer.setValue(meterText, forKey: "meterText")
}
}
override class func layerClass() -> AnyClass {
return MeterControlLayer.self
}
override func drawRect(rect: CGRect) {
// Do nothing
}
}
class MeterControlLayer: CALayer
{
#NSManaged
var meterLevel: Int
var meterText: String = "00:00:00"
override class func needsDisplayForKey(key: String) -> Bool {
if (key == "meterLevel") {
return true
}
return super.needsDisplayForKey(key)
}
override func actionForKey(key: String) -> CAAction? {
if (key == "meterLevel") {
let anim: CABasicAnimation = CABasicAnimation.init(keyPath: key)
anim.fromValue = self.presentationLayer()?.meterLevel
anim.duration = 1.0
return anim
} else {
return super.actionForKey(key)
}
}
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
UIGraphicsPushContext(ctx)
XntervalStyleKit.drawMeterControl(frame: self.bounds, meterTime: meterText, meterLevelValue: CGFloat(meterLevel))
UIGraphicsPopContext()
}
}
This unfortunately, doesn't work exactly the way I would expect. The animation is still a bit choppy, though closer to what I want.
My question is more general though, is this the right way to go about accomplishing what I want to do? I couldn't figure out the right way to set the layer properties meterLevel and meterText, without using setValueForKey:. Is that the right way to do this?
Animation/Graphics is definitely new to me. I'm an embedded C software guy trying to get into iOS development as a hobby.
The animation is still a bit choppy, though closer to what I want.
Given this, it seems as if Core Animation is actually drawing your layer every frame (or trying to, anyway).
Unfortunately, once you perform custom layer drawing each frame, your performance becomes main thread-bound: that is, normally, for properties that Core Animation can natively animate (such as bounds), the animation itself is rendered in the render server, which operates out-of-process from your application and has its own, high-priority render thread. The main thread of your application is free to do whatever it wants during these types of animation without any interruption to the animation.
drawInContext(_:), however, is called on the main thread of your application. If you put a log statement or breakpoint there, is it called many times over the course of your animation duration? If so, then the layer is properly animating. Your drawing operations within this function may be what's holding up the animation.
Try setting drawsAsynchronously to true on your layer. This defers drawing commands to a background thread, and it can improve performance sometimes. (Note that most, if not all, UIGraphics and Core Graphics functions are thread safe as of iOS 4, so background drawing is safe.)
Additionally, depending on how complex your animation is, you may want to draw several intermediate representations in advance (in the background and in parallel, if possible). Store these somewhere in memory if they aren't too large so you can simply display some of these bitmaps in your layer instead of drawing them just-in-time.
I wrote a UIView subclass called ConcentricProgressRingView which does something similar to what you're trying to do.
https://github.com/lionheart/ConcentricProgressRingView
Here's an example of what it looks like:
Usage
At the top of your UIViewController, import the module:
import ConcentricProgressRingView
Then, in your viewDidLoad:
let rings = [
ProgressRing(color: UIColor(.RGB(232, 11, 45)), backgroundColor: UIColor(.RGB(34, 3, 11))),
ProgressRing(color: UIColor(.RGB(137, 242, 0)), backgroundColor: UIColor(.RGB(22, 33, 0))),
ProgressRing(color: UIColor(.RGB(0, 200, 222)), backgroundColor: UIColor(.RGB(0, 30, 28)))
]
let progressRingView = try! ConcentricProgressRingView(center: view.center, radius: radius, margin: margin, rings: rings, defaultColor: UIColor.clearColor(), defaultWidth: 18)
view.addSubview(progressRingView)
Once you've instantiated your ConcentricProgressRingView instance, animate a specific ring to a percentage with setProgress.
ring.arcs[1].setProgress(0.5, duration: 2)
How it works
I'm not exactly sure what I'm doing differently from your example code in the question, but I'm creating a CABasicAnimation and setting a few parameters on it to tweak the animation behavior. The code is open source, so check it out. Hope this helps!
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.
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.