How Draw A Trinagle In A Layer Swift
Update (12/26/2019): A previous version of this post incorrectly stated implict animations happen in layers which back
UIViews. Really, layers which backUIViewsouthward don't have animations unless you implement the view'southwardaction(for:forKey:)method. More information is bachelor in this github repo.
Recently, I've been learning more than about Metal - I'm withal working through the basics, just I've written a couple of posts and tweets about it, and I'm about halfway through Metal By Instance. Up until at present I've mostly been coding sample projects, just I recently had the opportunity to epitome a Metallic implementation integrated into our production app at work.
One thing I hadn't considered until I had to implement Metal in a real app was animations - when another role of our interface animated, we wanted to scale the Metal content along with an animation. This post volition dive into how to suit the CALayer animation system to work with a custom Metallic view, and forth the mode nosotros'll see a situation which Swift can't handle without hacks. (😱)
Our goal volition exist to build a triangle that animates to a new scale when a button is pressed:
Full sample lawmaking is available for this project at NGCAMetalLayerAnimationExample.
Getting started
Our sample project volition follow the architecture adapted from the Xcode default Metallic projection (what Xcode gives you later New Project > Cross Platform Game):
- We'll have a
Rendererwith adraw(drawable:)method which is responsible for issuing Metallic draw calls with thedrawableequally a target. The renderer will accept atriangleScaleproperty to determine what transform to the triangle's vertices. - We'll take a custom view with a
CAMetalLayersubclass, and we'll override the layer'southwarddisplay()to calldraw, passing the drawable fromnextDrawable.
Note: we need a
CAMetalLayerinstead of anMTKViewhither, for reasons I'll get into near the terminate of this mail.
Our custom layer looks like this:
course CustomCAMetalLayer : CAMetalLayer { individual var renderer: Renderer! override init () { super . init () self . device = MTLCreateSystemDefaultDevice ()! self . renderer = Renderer ( device : self . device !) self . setNeedsDisplay () } /// Block the electric current thread until this layer has a drawable ready. private func blockRequestingNextDrawable () -> CAMetalDrawable { var drawable: CAMetalDrawable? = nil while (drawable == nil ) { drawable = self . nextDrawable () } return drawable! } override func display () { let drawable = cocky . blockRequestingNextDrawable () self . renderer . draw ( drawable : drawable) } } grade MetalView : UIView { override class var layerClass: AnyClass { render CustomCAMetalLayer.self } } Nosotros fix our renderer in init, wait for a drawable to get available, and utilize our renderer to draw the triangle. The renderer lawmaking is more than verbose since Metal requires quite a lot of lawmaking to prepare a command queue, vertex buffers, etc, but y'all can view the code here if you like - information technology creates a buffer of 3 vertices for the points of the triangle, and the fragment shader shades them bluish.
Hello World
CALayer animations
In gild to implement our triangle's scale-up animation, we need to talk more than near CoreAnimation.
CoreAnimation is the framework that powers CALayer (used to implement UIView). Changes in a CALayer (like setting a new position or groundwork colour) are initiated past changing the layer's properties.
Many layer properties (you tin can see a list here) are animatable, which ways that when they're inverse, CoreAnimation will interpolate between the old value and the new value to breathing the change. If you've ever tried to alter the background colour of a CALayer directly (using backgroundColor), you might find that the change isn't instant - there's a subtle fade. Animatable CALayer properties all accept default, "implicit" animations.
Annotation: layers which dorsum
UIViewdue south don't have these implicit animations past default sinceUIViews don't provide an action through their layer delegate method. More on this later.
Nosotros'll add our triangleScale every bit a custom layer property - we want this property to animate betwixt 1 and two to double the size of the triangle.
class CustomCAMetalLayer : CAMetalLayer { // ... var triangleScale: CGFloat override init () { self . triangleScale = one // ... } override func brandish () { self . renderer . triangleScale = Float (triangleScale) // ... } } Our button callback sets the new value:
@objc individual func didTapButton () { layer. triangleScale = ii } And when we run the app and click the button, we get...
...zippo!
Customizing layer animations
Turns out, custom CALayer backdrop don't come completely for gratuitous. I haven't been able to find an official Apple tree reference on this, simply at that place are several articles online about how to implement them - I'd recommend this objc.io article and this talk from Rob Napier, simply I'll try to summarize hither.
In society for CoreAnimation to manage our custom layer properties, they accept to exist Objc @dynamic properties. Declaring an @dynamic property in Objc tells the compiler that the property implementation will be managed dynamically - in this case, since we're a subclass of CALayer, CoreAnimation will manage this property for us, and we don't have to declare getters or setters, or handle initializing the property. (Learn more well-nigh @dynamic in the Objective-C reference.)
The dynamic nature of how CALayer handles holding animations is why we need to use a CAMetalLayer instead of an MTKView - if we were to declare these properties on a view subclass instead of a layer subclass, they wouldn't be automatically handled in the aforementioned way.
There'south another contraction though - Swift doesn't support managing dynamic properties in the same way. There's a dynamic keyword in Swift, but that specifies that function calls should use Objc-style dynamic method dispatch, not that belongings implementations should exist managed dynamically.
@dynamic is a mechanism in Objc that but doesn't exist in Swift. Luckily, there'south a way to go around it - @NSManaged.
Dynamic properties in Swift
The @NSManaged holding modifier is used for Core Information, but its effect is the aforementioned as Objc'southward @dynamic - information technology defers implementation of the property getters and setters. If nosotros declare our layer holding as @NSManaged, CALayer will exist able to manage its getters and setters! Our new lawmaking looks similar:
class CustomCAMetalLayer : CAMetalLayer { // ... @NSManaged var triangleScale: CGFloat override init () { // ... self . triangleScale = 1 } override func display () { self . renderer . triangleScale = Float (triangleScale) // ... } } Now that we've alleged our holding dynamically, the last footstep is to tell CoreAnimation that we want the layer to exist redisplayed when the property changes. We do this by overriding needsDisplay(forKey:).
override class func needsDisplay ( forKey central : String ) -> Bool { if key == "triangleScale" { return true } return super . needsDisplay ( forKey : key) } Nosotros have a triangle that changes calibration! The only thing left is to really implement the animation.
Using presentation layers
In CoreAnimation, each layer is really equanimous of a "model" layer, which represents the current prepare of non-interpolated properties, and a "presentation" layer, which represents the layer's current state equally information technology appears on screen. Copies of the model and presentation layers are accessible through CALayer'due south model() and presentation() methods.
As an example: permit's say we added an blitheness for our triangleScale, to take its value from 1 to 2.
The first matter we take to do is make sure our CALayer subclass knows how to exist instantiated equally a presentation copy - CoreAnimation will call init(layer:) for this.
override init ( layer : Any ) { super . init ( layer : layer) guard allow layer = layer as? CustomCAMetalLayer else { return } self . renderer = layer. renderer } And then we add an explicit animation:
permit animation = CABasicAnimation () animation. keyPath = "triangleScale" animation. fromValue = 1 animation. toValue = 2 animation. duration = 0.25 self . metalView . layer . add (blitheness, forKey : "some-primal" ) And a print in our layer's brandish():
let modelScale = cocky . model (). triangleScale guard let presentationScale = self . presentation ()?. triangleScale else { return } print ( "model: \( modelScale ) , presentation: \( presentationScale ) " ) Nosotros'll go the post-obit output:
model: 1.0, presentation: 1.0053806598298252 model: 1.0, presentation: one.1318122893571854 model: one.0, presentation: ane.2192756980657578 model: 1.0, presentation: 1.2894128262996674 model: 1.0, presentation: i.363368809223175 model: 1.0, presentation: 1.4336175322532654 model: i.0, presentation: 1.5070415139198303 model: i.0, presentation: 1.577137529850006 model: 1.0, presentation: 1.649360716342926 model: one.0, presentation: ane.7197566032409668 model: ane.0, presentation: 1.7918232679367065 model: 1.0, presentation: i.8647624254226685 model: 1.0, presentation: 1.9373475313186646 model: 1.0, presentation: 1.0 model: 1.0, presentation: i.0 The model layer never changes, and the presentation layer's triangleScale is automatically interpolated from 1 to 2 - but changes back to 1 later the animation ends, since CoreAnimation starts using the model value again when the animation ends. Before we see the triangle growing, we'll take to fix this interpolation issue.
Custom animatable properties
I noted before that with CoreAnimation, animatable layer properties have default animations that apply when you lot change the belongings. Nosotros can define our own blitheness past returning a value from the action(forKey:) class method:
override func action ( forKey primal : String ) -> CAAction? { if key == "triangleScale" { let blitheness = CABasicAnimation ( keyPath : key) animation. fromValue = self . presentation ()?. triangleScale return blitheness } render super . action ( forKey : fundamental) } Notation: specifying the fromValue hither is important, only other animation backdrop (duration, timing function, etc) will be inherited from the current
CATransaction.
If we specify an activeness, we don't need to define an animation anymore - CoreAnimation will automatically add the animation we divers when the layer's property changes. Adding the animation is now:
guard let layer = self . metalView . layer as? CustomCAMetalLayer else { render } layer. triangleScale = 2 Our model layer stays at ii while the presentation layer's value is interpolated all the mode at that place.
model: 2.0, presentation: 1.0 model: ii.0, presentation: 1.0715526789426804 model: 2.0, presentation: 1.1949102729558945 model: 2.0, presentation: i.27173313498497 model: 2.0, presentation: one.3447068929672241 model: 2.0, presentation: 1.4154288470745087 model: 2.0, presentation: 1.4875067472457886 model: 2.0, presentation: 1.5570306777954102 model: 2.0, presentation: 1.628233015537262 model: ii.0, presentation: i.7017306685447693 model: two.0, presentation: ane.771639108657837 model: 2.0, presentation: i.8431110382080078 model: ii.0, presentation: one.9160330295562744 model: 2.0, presentation: 1.9892664551734924 model: two.0, presentation: 2.0 The last step
We've got our presentation layer prepare to interpolate the triangle'southward scale, so the last step is to pass information technology to the renderer. We'll change our layer'due south display function to use the presentation value:
override func display () { guard allow effectiveScale = cocky . presentation ()?. triangleScale else { return } self . renderer . triangleScale = Float (effectiveScale) let drawable = self . blockRequestingNextDrawable () self . renderer . draw ( drawable : drawable) } Specify a few parameters for the animation:
CATransaction. begin () CATransaction. setAnimationDuration ( two ) CATransaction. setAnimationTimingFunction (. init ( proper noun : . easeInEaseOut )) layer. triangleScale = 2 CATransaction. commit () And tada! 🎉
More than complicated animations
Using CALayer properties to implement our animations ways that we can hook into the entire CoreAnimation ecosystem - our triangleScale is now equally as animatable as every other CALayer property. Ane do good is that we become keyframe animations with no extra piece of work! Allow's make the triangle leap around a bit:
let layer = self . metalView . layer as! CustomCAMetalLayer let blitheness = CAKeyframeAnimation () animation. keyPath = "triangleScale" animation. values = [ layer. triangleScale , -1 , 2 , -ii , layer. triangleScale ] animation. keyTimes = [ 0 , 0.two , 0.five , 0.viii , i ] blitheness. timingFunctions = [ . init ( name : . easeInEaseOut ), . init ( name : . easeInEaseOut ), . init ( name : . easeInEaseOut ), . init ( name : . easeInEaseOut ) ] animation. elapsing = 8 self . metalView . layer . add together (animation, forKey : "expandScale" )
Conclusion and farther reading
CoreAnimation was ever one of the more than confusing parts of iOS development to me, only hopefully this post has helped to pull back the covers a fleck on how the system works - going through these examples has certainly helped me understand how to make Metal play nicely with other parts of the iOS ecosystem.
There'south a lot of keen Apple and non-Apple tree content written about custom CALayer animations not related to Metallic that I'd recommend checking out:
- Animative Custom Layer Properties
- Cocoaheads June 2012: Animating Custom Layer Properties
- Avant-garde Graphics with CoreAnimation
- Custom CALayer
- Advanced Animation Tricks
Source: https://noahgilmore.com/blog/coreanimation-metal/
Posted by: johnsonroon1987.blogspot.com

0 Response to "How Draw A Trinagle In A Layer Swift"
Post a Comment