banner



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 back UIViewsouthward don't have animations unless you implement the view'southward action(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:

Blue triangle animating from 1x to 2x scale when a button is clicked

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):

  1. We'll have a Renderer with a draw(drawable:) method which is responsible for issuing Metallic draw calls with the drawable equally a target. The renderer will accept a triangleScale property to determine what transform to the triangle's vertices.
  2. We'll take a custom view with a CAMetalLayer subclass, and we'll override the layer'southward display() to call draw, passing the drawable from nextDrawable.

Note: we need a CAMetalLayer instead of an MTKView hither, 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.

Blue triangle displaying on the screen

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 since UIViews 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...

Blue triangle displaying with cursor clicking button, but nothing happens with the triangle after button click

...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.

Blue triangle changing scale (not animated) when the button is clicked.

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! 🎉

Blue triangle animating from 1x to 2x scale when the button is clicked

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"              )                              

Blue triangle animating between multiple different scales with a keyframe animation

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

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel