CALayer Tutorial: Getting Started

来源:互联网 发布:本地连接 未识别的网络 编辑:程序博客网 时间:2024/04/29 19:04

As you probably know, everything you see in an iOS app is a view. There’s button views, table views, slider views, and even parent views that contain other views.

But what you might not know is that each view in iOS is backed by another class called a layer – a CALayer to be specific.

In this article, you’ll learn what a CALayer is, and how it works. You’ll also see 10 examples of using CALayers for cool effects, like shapes, gradients, and even particle systems.

This article assumes you’re familiar with the basics of iOS app development and Swift, including constructing your UI with storyboards.

Note: If you’re not quite there, no worries. You’ll be happy to know we have quite a few tutorials and books on the subject, such as Learn to Code iOS Apps with Swift andThe iOS Apprentice.

Getting Started

The easiest way to understand what layers are is to see them in action. So let’s start by creating a simple project from scratch to play around with layers.

Ready to write some code? Good! Fire up Xcode and:

  1. Choose File\New\Project… from the menu.
  2. Select iOS\Application\Single View Application from the dialog.
  3. Click Next, enter CALayerPlayground for the Product Name and enter an organization name and identifier of your choosing.
  4. Select Swift for Language and Universal for Devices.
  5. Uncheck Core Data is unchecked, then click Next.
  6. Find a nice home for your project (I keep projects in a folder within my user directory called Source), and clickCreate.

    Okay, now you have the files set up and the next order of business is to create a view:

  7. In the Project navigator, select Main.storyboard.
  8. Select View\Assistant Editor\Show Assistant Editor from the menu, and View\Utilities\Show Object Library if it’s not already displayed.
  9. Also, select Editor\Canvas\Show Bounds Rectangles, so that you can see the bounds outline of the view you’re about to add to your scene.
  10. From the Object library, drag a View onto your View Controller Scene. With it selected, go to the Size inspector (View\Utilities\Show Size Inspector) and set x and y to 150 and width and height to 300.
  11. With that view still selected, click the Align button in the auto layout toolbar (bottom-right of the storyboard) and check Horizontal Center in Container and Vertical Center in Container, leave both their values set to 0, and click Add 2 Constraints.
  12. Click the Pin button, check Width and Height, make sure both values are set to 300, and click Add 2 Constraints.

Finally, control-drag from the view you just created to the scene’s ViewController.swift file, right above theviewDidLoad() method. In the popup that appears, give your outlet the name viewForLayer. Your Xcode should now look similar to this:

Xcode-CreateOutlet

Click Connect to create the outlet.

Replace the contents of ViewController.swift with the following:

import UIKit class ViewController: UIViewController {   @IBOutlet weak var viewForLayer: UIView!   var l: CALayer {    return viewForLayer.layer  }   override func viewDidLoad() {    super.viewDidLoad()    setUpLayer()  }   func setUpLayer() {    l.backgroundColor = UIColor.blueColor().CGColor    l.borderWidth = 100.0    l.borderColor = UIColor.redColor().CGColor    l.shadowOpacity = 0.7    l.shadowRadius = 10.0  } }

As mentioned earlier, every view in iOS has a layer associated with it, and you can retrieve that layer withyourView.layer. The first thing this code does is create a computed property called “l” (that’s a lower case L) to access the viewForLayer‘s layer, which saves some keystrokes as you write the subsequent code.

The code also calls setUpLayer to set a few properties on the layer – a shadow, a blue background color, and a huge red border. You’ll learn more about setUpLayer() in a moment, but first, build and run to the iOS Simulator (I chose iPhone 6) and check out your customized layer:

CALayerPlayground-1

Pretty cool effect with just a few lines of code, eh? And again – since every view is backed by a layer, you can do this kind of thing for any view in your app. Let’s take a closer look.

Basic CALayer Properties

CALayer has several properties that let you customize its appearance. Think back to what you’ve already done:

  • Changed the layer’s background color from its default of no background color to blue
  • Gave it a border by changing border width from the default 0 to 100
  • Changed its color from the default black to red.
  • Lastly, you gave it a shadow by changing its shadow opacity from default zero (transparent) to 0.7; this alone would cause a shadow to display, and you took it a step further by increasing its shadow radius from its default value of 3 to 10.

These are just a few of the properties you can set on CALayer. Let’s try two more. Add these lines to the bottom ofsetUpLayer():

l.contents = UIImage(named: "star")?.CGImagel.contentsGravity = kCAGravityCenter

The contents property on a CALayer allows you to set the layer’s content to an image, so you set it to an image named “star” here. For this to work, you’ll need to add that image to your project, so download this star image I madeand add it to your project.

Build and run and take a moment to appreciate this stunning piece of art:

CALayerPlayground-2

Notice how the star is centered – this is because you set the contentsGravity property to kCAGravityCenter. As you might expect, you can also change the gravity to top, top-right, right, bottom-right, bottom, bottom-left, left and top-left.

Changing the Layer’s Appearance

Just for fun, let’s add some gesture recognizers to manipulate the appearance of this layer. In Xcode, drag a tap gesture recognizer onto the viewForLayer object. For reference, here’s what adding the tap gesture recognizer should look like:

Xcode-AddTapGestureRecognizer

Note: If you’re not familiar with gesture recognizers, check out Using UIGestureRecognizer with Swift.

Repeat this to add a pinch gesture recognizer onto the viewForLayer as well.

Then control-drag from each gesture recognizer item on your storyboard scene dock into ViewController.swift, one after the other, and place them between setUpLayer() and the closing curly brace for the class itself.

In the popup, change the connection to Action and name the tap recognizer action tapGestureRecognized and the pinch recognizer pinchGestureRecognized. For example:

Xcode-CreateAction

Change tapGestureRecognized(_:) to look like this:

@IBAction func tapGestureRecognized(sender: UITapGestureRecognizer) {  l.shadowOpacity = l.shadowOpacity == 0.7 ? 0.0 : 0.7}

This tells the viewForLayer layer to toggle its layer’s shadow opacity between 0.7 and 0 when the view recognizes a tap.

The view, you say? Well, yes. You could override CALayer’s hitTest(_:) to do the same thing, and actually you’ll see that approach later in this article. But here’s the logic behind the above approach: hit testing is all a layer can do because it cannot react to recognized gestures. That’s why you set up the tap gesture recognizer on the view.

Now change pinchGestureRecognized(_:) to look like this:

@IBAction func pinchGestureRecognized(sender: UIPinchGestureRecognizer) {  let offset: CGFloat = sender.scale < 1 ? 5.0 : -5.0  let oldFrame = l.frame  let oldOrigin = oldFrame.origin  let newOrigin = CGPoint(x: oldOrigin.x + offset, y: oldOrigin.y + offset)  let newSize = CGSize(width: oldFrame.width + (offset * -2.0), height: oldFrame.height + (offset * -2.0))  let newFrame = CGRect(origin: newOrigin, size: newSize)  if newFrame.width >= 100.0 && newFrame.width <= 300.0 {    l.borderWidth -= offset    l.cornerRadius += (offset / 2.0)    l.frame = newFrame  }}

Here you’re creating a positive or negative offset based on the user’s pinch, and then adjusting the size of the layer’s frame, width of its border and the border’s corner radius.

A layer’s corner radius is 0 by default, meaning it’s a standard rectangle with 90-degree corners. Increasing the radius creates rounded corners. Want to turn a square layer into a circle? Set its corner radius to half of its width.

Note that adjusting the corner radius doesn’t clip the layer’s contents (the star image) unless the layer’smasksToBounds property is set to true.

Build and run, and try tapping on and pinching your view in and out:

CALayerPlayground-3

Hey, with a little more polish you could have yourself a pretty nifty avatar maker! :]

The Great CALayer Tour

CaLayerTour

CALayer has more than just a few properties and methods to tinker with, as well as several subclasses that have unique properties and methods.

What better way to get an overview of all this great API than by taking a guided tour, raywenderlich.com-style?

For the rest of this article, you will need the following:

  • The Layer Player App
  • The Layer Player Source Code

This is a handy app that includes examples of 10 different types of CALayers, which you’ll learn about in this article. Here’s a preview of the 10 examples:

CALayerTourScenes

As we go through each example below, I recommend you play around with it in the CALayer app, and optionally look at the source code provided. You don’t need to actually code anything for the rest of this article, so just sit back, read and relax :]

These should be some great examples for you as you add different types of CALayers to your own projects. We hope you enjoy!

Example #1: CALayer

You’ve already seen an example of using CALayer, and setting a few of the properties.

There are a few things I didn’t mention about CALayers yet:

  • Layers can have sublayers. Just like views can have subviews, layers can have sublayers. You can use this for some cool effects!
  • Layer properties are animated. When you change the property of a layer, it is animated over time by default. You can also customize this animation behavior to your own timing.
  • Layers are lightweight. Layers are lighter-weight than views, and therefore they help you achieve better performance.
  • Layers have tons of useful properties. You’ve seen a few already, but let’s take a look at a few more!

As you’ve seen earlier, layers have tons of useful properties. Let’s take a tour of the full list of CALayer properties – some you haven’t seen yet, and are quite handy!

// 1let layer = CALayer()layer.frame = someView.bounds // 2layer.contents = UIImage(named: "star")?.CGImagelayer.contentsGravity = kCAGravityCenter // 3layer.magnificationFilter = kCAFilterLinearlayer.geometryFlipped = false // 4layer.backgroundColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0).CGColorlayer.opacity = 1.0layer.hidden = falselayer.masksToBounds = false // 5layer.cornerRadius = 100.0layer.borderWidth = 12.0layer.borderColor = UIColor.whiteColor().CGColor // 6layer.shadowOpacity = 0.75layer.shadowOffset = CGSize(width: 0, height: 3)layer.shadowRadius = 3.0someView.layer.addSublayer(layer)

In the above code:

  1. Creates a CALayer instance and sets it to the bounds of someView.
  2. Sets an image as the layer’s contents and centers it within the layer. Notice that the underlying Quartz image data (CGImage) is assigned.
  3. Use this filter when enlarging the image via contentsGravity, which can be used to change both size (resize, resize aspect, and resize aspect fill) and position (center, top, top-right, right, etc.).

    The previous changes are not animated, and if geometryFlipped is not set to true, the positional geometry and shadow will be upside-down. Continuing on:

  4. You set the background color to Ray’s favorite shade of green :] and the made the layer opaque and visible. At the same time, you tell the layer not mask its contents, which means that if its size is smaller than its contents (the star image), the image will not be clipped.
  5. The layer’s corner radius is set to half the width of the layer to create visuals of a circle a border; notice that layer colors are assigned as the Quartz color references (CGColor).
  6. Creates a shadow and sets shouldRasterize to true (discussed below), and then adds the layer to the view hierarchy.

Here’s the result:

CALayer

CALayer has two additional properties that can improve performance: shouldRasterize and drawsAsynchronously.

shouldRasterize is false by default, and when set to true it can improve performance because a layer’s contents only need to be rendered once. It’s perfect for objects that are animated around the screen but don’t change in appearance.

drawsAsynchronously is sort of the opposite of shouldRasterize. It’s also false by default. Set it to true to improve performance when a layer’s contents must be repeatedly redrawn, such as when you work with an emitter layer that continuously renders animated particles. (See the CAEmitterLayer example later.)

A Word of Caution: Consider the implications before setting either shouldRasterize or drawsAsynchronouslyto true for a given layer. Compare the performance between true and false so you know if activating these features actually improves performance. When misused, performance is likely to take a nosedive.

Now shift your attention briefly to Layer Player. It includes controls to manipulate many of CALayer’s properties:

CALayer_5.5

Play around with the various controls – it’s a great way to get a feel of what you can do with CALayer!

Note: Layers are not part of the responder chain so they won’t directly react to touches or gestures like views can, as you saw in the CALayerPlayground example.

However, you can hit test them, as you’ll see in the example code for CATransformLayer. You can also add custom animations to layers, which you’ll see when we get to CAReplicatorLayer.

Example #2: CAScrollLayer

CAScrollLayer displays a portion of a scrollable layer. It’s fairly basic and cannot directly respond to user touches or even check the bounds of the scrollable layer, so it does cool things like preventing scrolling beyond the bounds ad infinitum! :]

UIScrollView doesn’t use a CAScrollLayer to do its work, instead it directly changes its layer’s bounds.

What you can do with a CAScrollLayer is set its scrolling mode to horizontal and/or vertical, and you can programmatically tell it to scroll to a specific point or rect:

// In ScrollingView.swiftimport UIKit class ScrollingView: UIView {  // 1  override class func layerClass() -> AnyClass {    return CAScrollLayer.self  }} // In CAScrollLayerViewController.swiftimport UIKit class CAScrollLayerViewController: UIViewController {  @IBOutlet weak var scrollingView: ScrollingView!   // 2  var scrollingViewLayer: CAScrollLayer {    return scrollingView.layer as CAScrollLayer  }   override func viewDidLoad() {    super.viewDidLoad()    // 3    scrollingViewLayer.scrollMode = kCAScrollBoth  }   @IBAction func tapRecognized(sender: UITapGestureRecognizer) {    // 4    var newPoint = CGPoint(x: 250, y: 250)    UIView.animateWithDuration(0.3, delay: 0, options: .CurveEaseInOut, animations: {      [unowned self] in      self.scrollingViewLayer.scrollToPoint(newPoint)      }, completion: nil)  } }

In the above code:

  1. A custom UIView subclass is used to override layerClass() to return CAScrollLayer; this is an alternative to creating a new layer and adding it as a sublayer, such as what was done in the CALayer example.
  2. A computed property is used to streamline working with the scrolling view layer of the custom UIView.
  3. Scrolling is set to both horizontal and vertical.
  4. When a tap is recognized, a new point is created and the scrolling layer scrolls to that point inside a UIViewanimation. Note: scrollToPoint(_:) and scrollToRect(_:) do not animate automatically.

Case study: An instance of ScrollingView that houses an image view with an image that’s larger than the scrolling view’s bounds. When you run the above code and tap on the view, this would be the result:

CAScrollLayer

Layer Player includes multiple controls to lock scrolling horizontally and vertically.

Here are some rules of thumb for when to use (or not to use) CAScrollLayer:

  • If you want something lightweight and only need to programmatically scroll: Consider using CAScrollLayer.
  • If you want the user to be able to scroll: You’re probably better off with UIScrollView. To learn more, check out our 18-part video tutorial series on this.
  • If you are scrolling a very large image: Consider using CATiledLayer (more below).

Example #3: CATextLayer

CATextLayer provides simple but fast rendering of plain text or attributed strings. Unlike UILabel, a CATextLayer cannot have an assigned UIFont, only a CTFontRef or CGFontRef.

With a block of code like this, it’s possible to manipulate font, font size, color, alignment, wrapping and truncation, as well as animate changes:

// 1let textLayer = CATextLayer()textLayer.frame = someView.bounds // 2var string = ""for _ in 1...20 {  string += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce auctor arcu quis velit congue dictum. "} textLayer.string = string // 3let fontName: CFStringRef = "Noteworthy-Light"textLayer.font = CTFontCreateWithName(fontName, fontSize, nil) // 4textLayer.foregroundColor = UIColor.darkGrayColor().CGColortextLayer.wrapped = truetextLayer.alignmentMode = kCAAlignmentLefttextLayer.contentsScale = UIScreen.mainScreen().scalesomeView.layer.addSublayer(textLayer)

Explanation of the above code:

  1. Creates a CATextLayer instance and sets its to someView‘s bounds.
  2. Creates a string of repeated text and assigns it to the text layer.
  3. Creates a font and assigns it to the text layer.
  4. Sets the text layer to wrap and left-align, (You have the option of setting it to natural, right, center and justified.), matches its contentsScale to the screen, and then adds the layer to the view hierarchy.

All layer classes, not just CATextLayer, render at a scale factor of 1 by default. When attached to views, layers automatically have their contentsScale set to the appropriate scale factor for the current screen. You need to set thecontentsScale explicitly for layers you create manually, or else their scale factor will be 1 and you’ll have pixilation on retina displays.

If added to a square-shaped someView, the created text layer would look like this:

CATextLayer

Truncation is a setting you can play with, and it’s nice when you’d like to represent clipped text with an ellipsis. Truncation defaults to none and can be set to start, end and middle:

CATextLayer-MiddleTruncation.png

CATextLayer-StartTruncation.png

CATextLayer-EndTruncation

Layer Player has controls to change many of CATextLayer’s properties:

CATextLayer_5.5

Example #4: AVPlayerLayer

AVPlayerLayer adds a sweet layer goodness to AVFoundation. It holds an AVPlayer to play AV media files (AVPlayerItems). Here’s an example of creating an AVPlayerLayer:

override func viewDidLoad() {  super.viewDidLoad()  // 1  let playerLayer = AVPlayerLayer()  playerLayer.frame = someView.bounds   // 2  let url = NSBundle.mainBundle().URLForResource("someVideo", withExtension: "m4v")  let player = AVPlayer(URL: url)   // 3  player.actionAtItemEnd = .None  playerLayer.player = player  someView.layer.addSublayer(playerLayer)   // 4  NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerDidReachEndNotificationHandler:", name: "AVPlayerItemDidPlayToEndTimeNotification", object: player.currentItem)} deinit {  NSNotificationCenter.defaultCenter().removeObserver(self)} // 5@IBAction func playButtonTapped(sender: UIButton) {  if playButton.titleLabel?.text == "Play" {    player.play()    playButton.setTitle("Pause", forState: .Normal)  } else {    player.pause()    playButton.setTitle("Play", forState: .Normal)  }   updatePlayButtonTitle()  updateRateSegmentedControl()} // 6func playerDidReachEndNotificationHandler(notification: NSNotification) {  let playerItem = notification.object as AVPlayerItem  playerItem.seekToTime(kCMTimeZero)}

A breakdown of the above code:

  1. Creates a new player layer and sets its frame.
  2. Creates a player with an AV asset.
  3. Tells the player to do nothing when it finishes playing; additional options include pausing or advancing to the next asset, if applicable.
  4. Registers for AVPlayer’s notification when it finishes playing an asset (and remove the controller as an observer in deinit).
  5. When the play button is tapped, it toggles controls to play the AV asset and set the button’s title.

Note this is just a simple example just to get you started. In a real project, it would generally not be advisable to pivot on a button’s title text.

The AVPlayerLayer and its AVPlayer created above would be visually represented by the first frame of the AVPlayerItem instance, like this:

AVPlayerItem

AVPlayerLayer has a couple additional properties:

  • videoGravity sets the resizing behavior of the video display.
  • readyForDisplay checks if the video is ready for display.

AVPlayer, on the other hand, has quite a few additional properties and methods. One to note is rate, which is the playback rate from 0 to 1. Zero means to pause, and 1 means the video plays at regular speed (1x).

However, setting rate also instructs playback to commence at that rate. In other words, calling pause() and settingrate to 0 does the same thing, as calling play() and setting rate to 1.

So what about fast forward, slow motion or playing in reverse? AVPlayerLayer has you covered. Setting rate to anything higher than 1 is equivalent to asking the player to commence playback at that number times regular speed, for instance, setting rate to 2 means double-speed.

As you might assume, setting rate to a negative number instructs playback to commence at that number times regular speed in reverse.

Before playback occurs at any rate other than regular speed (forward), however, the appropriate method is called on the AVPlayerItem instance to verify that it can be played back at that rate:

  • canPlayFastForward() for higher than 1
  • canPlaySlowForward() for between 0 and 1
  • canPlayReverse() for -1
  • canPlaySlowReverse() for between -1 and 0
  • canPlayFastReverse() for lower than -1

Most videos can typically play at various forward speeds, but it’s less typical that they can play in reverse. Layer Player also includes playback controls:

AVLayerPlayer_5.5

Example #5: CAGradientLayer

CAGradientLayer makes it easy to blend two or more colors together, making it especially well suited to backgrounds. To configure it, you assign an array of CGColors, as well as a startPoint and an endPoint to specify where the gradient layer should begin and end.

Bear in mind, startPoint and endPoint are not explicit points. Rather, they are defined in the unit coordinate space and then mapped to the layer’s bounds when drawn. In other words, an x value of 1 means the point is at the right edge of the layer, and a y value of 1 means the point is at the bottom edge of the layer.

CAGradientLayer has a type property, although kCAGradientLayerAxial is the only option, and it transitions through each color in the array linearly.

This means that if you draw a line (A) between startPoint and endPoint, the gradations would occur along an imaginary line (B) that is perpendicular to A, and all points along B would be the same color:

AxialGradientLayerType

Alternatively, you can control the locations property with an array of values between 0 and 1 that specify relative stops where the gradient layer should use the next color in the colors array.

If left unspecified the stop locations default to evenly spaced. If locations is set, though, its count must match colorscount, or else bad things will happen. :[

Here’s an example of how to create a gradient layer:

let gradientLayer = CAGradientLayer()gradientLayer.frame = someView.boundsgradientLayer.colors = [cgColorForRed(209.0, green: 0.0, blue: 0.0),  cgColorForRed(255.0, green: 102.0, blue: 34.0),  cgColorForRed(255.0, green: 218.0, blue: 33.0),  cgColorForRed(51.0, green: 221.0, blue: 0.0),  cgColorForRed(17.0, green: 51.0, blue: 204.0),  cgColorForRed(34.0, green: 0.0, blue: 102.0),  cgColorForRed(51.0, green: 0.0, blue: 68.0)]gradientLayer.startPoint = CGPoint(x: 0, y: 0)gradientLayer.endPoint = CGPoint(x: 0, y: 1)someView.layer.addSublayer(gradientLayer) func cgColorForRed(red: CGFloat, green: CGFloat, blue: CGFloat) -> AnyObject {  return UIColor(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0).CGColor as AnyObject}

In the above code, you create a gradient layer, match its frame to the bounds of someView, assign an array of colors, set start and end points, and add the gradient layer to the view hierarchy. Here’s what it would look like:

CAGradientLayer

So colorful! Next, you’ll program a butterfly that comes fluttering out of the app to tickle your nose. :]

Layer Player provides you controls to change start and end points, colors and locations:

CAGradientLayer_5.5

Example #6: CAReplicatorLayer

CAReplicatorLayer duplicates a layer a specified number of times, which can allow you to create some cool effects.

Each layer copy can have it’s own color and positioning changes, and its drawing can be delayed to give an animation effect to the overall replicator layer. Depth can also be preserved to give the replicator layer a 3D effect. Here’s an example:

// 1let replicatorLayer = CAReplicatorLayer()replicatorLayer.frame = someView.bounds // 2replicatorLayer.instanceCount = 30replicatorLayer.instanceDelay = CFTimeInterval(1 / 30.0)replicatorLayer.preservesDepth = falsereplicatorLayer.instanceColor = UIColor.whiteColor().CGColor // 3replicatorLayer.instanceRedOffset = 0.0replicatorLayer.instanceGreenOffset = -0.5replicatorLayer.instanceBlueOffset = -0.5replicatorLayer.instanceAlphaOffset = 0.0 // 4let angle = Float(M_PI * 2.0) / 30replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)someView.layer.addSublayer(replicatorLayer) // 5let instanceLayer = CALayer()let layerWidth: CGFloat = 10.0let midX = CGRectGetMidX(someView.bounds) - layerWidth / 2.0instanceLayer.frame = CGRect(x: midX, y: 0.0, width: layerWidth, height: layerWidth * 3.0)instanceLayer.backgroundColor = UIColor.whiteColor().CGColorreplicatorLayer.addSublayer(instanceLayer) // 6let fadeAnimation = CABasicAnimation(keyPath: "opacity")fadeAnimation.fromValue = 1.0fadeAnimation.toValue = 0.0fadeAnimation.duration = 1fadeAnimation.repeatCount = Float(Int.max) // 7instanceLayer.opacity = 0.0instanceLayer.addAnimation(fadeAnimation, forKey: "FadeAnimation")

What the above code does:

  1. Creates an instance of CAReplicatorLayer and sets its frame set to someView‘s bounds.
  2. Sets the replicator layer’s number of copies (instanceCount) and drawing delay. Also sets the replicator layer to be 2D (preservesDepth = false) and its instance color to white.
  3. Adds red/green/blue offsets to the color values of each successive replicated instance. Each defaults to 0, and that effectively preserves color value across all instances. However, in this case, the instance color was originally set to white, meaning red, green and blue are 1.0 already. Hence, setting red to 0 and the green and blue offset values to a negative number allows red to be the prominent color. Similarly, the alpha offset is added to the alpha of each successive replicated instance.
  4. Creates a transform to rotate each successive instance around a circle.
  5. Creates an instance layer for the replicator layer to use and sets its frame so the first instance will be drawn at center x and at the top of someView‘s bounds. This block also sets the instance’s color and adds the instance layer to the replicator layer.
  6. Makes a fade animation to animate opacity from 1 (opaque) to 0 (transparent).
  7. Sets the instance layer’s opacity to 0 so that it is transparent until each instance is drawn and its color and alpha values are set.

And here’s what that code would get you:

CAReplicatorLayer

Layer Player includes controls to manipulate most of these properties:

CAReplicatorLayer_5.5

Example #7: CATiledLayer

CATiledLayer asynchronously draws layer content in tiles. This is great for very large images or other sets of content where you are only looking at small bits at a time, because you can start seeing your content without having to load it all into memory at once.

There are a couple of ways to handle the drawing. One is to override UIView and use a CATiledLayer to repeatedly draw tiles to fill up view’s background, like this:

// In ViewController.swiftimport UIKit class ViewController: UIViewController {   // 1  @IBOutlet weak var tiledBackgroundView: TiledBackgroundView! } // In TiledBackgroundView.swiftimport UIKit class TiledBackgroundView: UIView {   let sideLength = CGFloat(50.0)   // 2  override class func layerClass() -> AnyClass {    return CATiledLayer.self  }   // 3  required init(coder aDecoder: NSCoder) {    super.init(coder: aDecoder)    srand48(Int(NSDate().timeIntervalSince1970))    let layer = self.layer as CATiledLayer    let scale = UIScreen.mainScreen().scale    layer.contentsScale = scale    layer.tileSize = CGSize(width: sideLength * scale, height: sideLength * scale)  }   // 4  override func drawRect(rect: CGRect) {    let context = UIGraphicsGetCurrentContext()    var red = CGFloat(drand48())    var green = CGFloat(drand48())    var blue = CGFloat(drand48())    CGContextSetRGBFillColor(context, red, green, blue, 1.0)    CGContextFillRect(context, rect)  } }

Here’s what’s happening in the above code:

  1. tiledBackgroundView is positioned at (150, 150) with width and height of 300.
  2. layerClass() is overridden so the layer for this view is created as an instance of CATiledLayer.
  3. Seeds the rand48() function that will be used to generate random colors in drawRect(). Then scales the contents of the layer (cast as a CATiledLayer) to match the screen’s scale and its tile size set.
  4. Overrides drawRect() to fill the view with tiled layers with random colors.

Ultimately, the above code draws a 6×6 grid of randomly colored square tiles, like this:

CATiledLayer

Layer Player expands upon this usage by also drawing a path on top of the tiled layer background:

CATiledLayer_5.5

The star in the above screenshot becomes blurry as you zoom in on the view:

CATiledLayerZoomedBlurry

This blurriness is the result of levels of detail maintained by the layer. CATiledLayer has two properties,levelsOfDetail and levelsOfDetailBias.

levelsOfDetail, as its name aptly applies, is the number of levels of detail maintained by the layer. It defaults to 1, and each incremental level caches at half the resolution of the previous level. The maximum levelsOfDetail value for a layer is that which its bottom-most level of detail has at least a single pixel.

levelsOfDetailBias, on the other hand, is the number of magnified levels of detail cached by this layer. It defaults to 0, meaning no additional magnified levels will be cached, and each incremental level will be cached at double the preceding level’s resolution.

For example, increasing the levelsOfDetailBias to 5 for the blurry tiled layer above would result in caching levels magnified at 2x, 4x, 8x, 16x and 32x, and the zoomed in layer would look like this:

CATiledLayerZoomed

Pretty cool, eh? But wait, there’s more!

CATiledLayer slices and dices and…sorry, had a flashback to the Ginsu knife infomercial for a second there. :]

But seriously, CATiledLayer has another, dare I say more useful purpose than a Ginsu knife: asynchronously drawing tiles of a very large image, for example, within a scroll view.

You have to provide the tiles and logic to tell the tiled layer which tiles to grab as the user scrolls around, but the performance gain here is remarkable.

Layer Player includes a UIImage extension in a file named UIImage+TileCutter.swift. Fellow tutorial team memberNick Lockwood adapted this code for his Terminal app, which he provided in his excellent book, iOS Core Animation: Advanced Techniques.

Its job is to slice and dice the source image into square tiles of the specified size, named according to the column and row location of each tile; for example, windingRoad_6_2.png for the tile at column 7, row 3 (zero-indexed):

windingRoad

With those tiles in place, a custom UIView subclass can be created to draw those tile layers:

import UIKit class TilingViewForImage: UIView {   // 1  let sideLength = CGFloat(640.0)  let fileName = "windingRoad"  let cachesPath = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0] as String   // 2  override class func layerClass() -> AnyClass {    return CATiledLayer.self  }   // 3  required init(coder aDecoder: NSCoder) {    super.init(coder: aDecoder)    let layer = self.layer as CATiledLayer    layer.tileSize = CGSize(width: sideLength, height: sideLength)  }   // 4  override func drawRect(rect: CGRect) {    let firstColumn = Int(CGRectGetMinX(rect) / sideLength)    let lastColumn = Int(CGRectGetMaxX(rect) / sideLength)    let firstRow = Int(CGRectGetMinY(rect) / sideLength)    let lastRow = Int(CGRectGetMaxY(rect) / sideLength)     for row in firstRow...lastRow {      for column in firstColumn...lastColumn {        if let tile = imageForTileAtColumn(column, row: row) {          let x = sideLength * CGFloat(column)          let y = sideLength * CGFloat(row)          let point = CGPoint(x: x, y: y)          let size = CGSize(width: sideLength, height: sideLength)          var tileRect = CGRect(origin: point, size: size)          tileRect = CGRectIntersection(bounds, tileRect)          tile.drawInRect(tileRect)        }      }    }  }   func imageForTileAtColumn(column: Int, row: Int) -> UIImage? {    let filePath = "\(cachesPath)/\(fileName)_\(column)_\(row)"    return UIImage(contentsOfFile: filePath)  } }

The above code:

  1. Creates properties for length of the tile side, base image filename, and the path to the caches directory where the TileCutter extension saves tiles.
  2. Overrides layerClass() to return CATiledLayer.
  3. Implements init(_:), in the view’s layer, casts it as a tiled layer and sets its tile size. Note that it is not necessary to match contentsScale to the screen scale, because you’re working with the backing layer of the view directly vs. creating a new layer and adding it as a sublayer.
  4. Overrides drawRect() to draw each tile according to its column and row position.

Then a view of that subclass type, sized to the original image’s dimensions can be added to a scroll view:

XcodeTilingViewForImageStoryboard

And voilà, you have buttery smooth scrolling of a large image (5120 x 3200 in this case), thanks to CATiledLayer:

CATiledImageLayer

As you can see in the above animation, though, there is noticeable blockiness when fast-scrolling as individual tiles are drawn. Minimize this behavior by using smaller tiles (the tiles used in the above example were cut to 640 x 640) and by creating a custom CATiledLayer subclass and overriding fadeDuration() to return 0:

class TiledLayer: CATiledLayer {   override class func fadeDuration() -> CFTimeInterval {    return 0.0  } }

Example #8: CAShapeLayer

CAShapeLayer makes use of scalable vector paths to draw, and it’s much faster than using images. Another part of the win here is that you’ll no longer need to provide images at regular, @2x and @3x sizes. w00t!

Additionally, you have a variety of properties at your disposal to customize line thickness, color, dashing, how lines join other lines, if the line intersects itself to form a closed area, and if that area should be filled and with what color. Here’s an example:

import UIKit class ViewController: UIViewController {   @IBOutlet weak var someView: UIView!   // 1  let rwColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0)  let rwPath = UIBezierPath()  let rwLayer = CAShapeLayer()   // 2  func setUpRWPath() {    rwPath.moveToPoint(CGPointMake(0.22, 124.79))    rwPath.addLineToPoint(CGPointMake(0.22, 249.57))    rwPath.addLineToPoint(CGPointMake(124.89, 249.57))    rwPath.addLineToPoint(CGPointMake(249.57, 249.57))    rwPath.addLineToPoint(CGPointMake(249.57, 143.79))    rwPath.addCurveToPoint(CGPointMake(249.37, 38.25), controlPoint1: CGPointMake(249.57, 85.64), controlPoint2: CGPointMake(249.47, 38.15))    rwPath.addCurveToPoint(CGPointMake(206.47, 112.47), controlPoint1: CGPointMake(249.27, 38.35), controlPoint2: CGPointMake(229.94, 71.76))    rwPath.addCurveToPoint(CGPointMake(163.46, 186.84), controlPoint1: CGPointMake(182.99, 153.19), controlPoint2: CGPointMake(163.61, 186.65))    rwPath.addCurveToPoint(CGPointMake(146.17, 156.99), controlPoint1: CGPointMake(163.27, 187.03), controlPoint2: CGPointMake(155.48, 173.59))    rwPath.addCurveToPoint(CGPointMake(128.79, 127.08), controlPoint1: CGPointMake(136.82, 140.43), controlPoint2: CGPointMake(129.03, 126.94))    rwPath.addCurveToPoint(CGPointMake(109.31, 157.77), controlPoint1: CGPointMake(128.59, 127.18), controlPoint2: CGPointMake(119.83, 141.01))    rwPath.addCurveToPoint(CGPointMake(89.83, 187.86), controlPoint1: CGPointMake(98.79, 174.52), controlPoint2: CGPointMake(90.02, 188.06))    rwPath.addCurveToPoint(CGPointMake(56.52, 108.28), controlPoint1: CGPointMake(89.24, 187.23), controlPoint2: CGPointMake(56.56, 109.11))    rwPath.addCurveToPoint(CGPointMake(64.02, 102.25), controlPoint1: CGPointMake(56.47, 107.75), controlPoint2: CGPointMake(59.24, 105.56))    rwPath.addCurveToPoint(CGPointMake(101.42, 67.57), controlPoint1: CGPointMake(81.99, 89.78), controlPoint2: CGPointMake(93.92, 78.72))    rwPath.addCurveToPoint(CGPointMake(108.38, 30.65), controlPoint1: CGPointMake(110.28, 54.47), controlPoint2: CGPointMake(113.01, 39.96))    rwPath.addCurveToPoint(CGPointMake(10.35, 0.41), controlPoint1: CGPointMake(99.66, 13.17), controlPoint2: CGPointMake(64.11, 2.16))    rwPath.addLineToPoint(CGPointMake(0.22, 0.07))    rwPath.addLineToPoint(CGPointMake(0.22, 124.79))    rwPath.closePath()  }   // 3  func setUpRWLayer() {    rwLayer.path = rwPath.CGPath    rwLayer.fillColor = rwColor.CGColor    rwLayer.fillRule = kCAFillRuleNonZero    rwLayer.lineCap = kCALineCapButt    rwLayer.lineDashPattern = nil    rwLayer.lineDashPhase = 0.0    rwLayer.lineJoin = kCALineJoinMiter    rwLayer.lineWidth = 1.0    rwLayer.miterLimit = 10.0    rwLayer.strokeColor = rwColor.CGColor  }   override func viewDidLoad() {    super.viewDidLoad()     // 4    setUpRWPath()    setUpRWLayer()    someView.layer.addSublayer(rwLayer)  } }

Here’s the lowdown on the above code:

  1. Creates color, path, and shape layer objects.
  2. Draws the shape layer’s path. If writing this sort of boilerplate drawing code is not your cup of tea, check outPaintCode; it generates the code for you by letting you draw using intuitive visual controls or import existing vector (SVG) or Photoshop (PSD) files.
  3. Sets up the shape layer. Its path is set to the CGPath of the path drawn in step 2, its fill color to the CGColor of the color created in step 1, and the fill rule is explicitly set to the default value of non-zero.
    • The only other option is even-odd, and for this shape that has no intersecting paths the fill rule makes little difference.
    • The non-zero rule counts left-to-right paths as +1 and right-to-left paths as -1; it adds up all values for paths and if the total is greater than 0, it fills the shape(s) formed by the paths.
    • Essentially, non-zero fills all points inside the shape.
    • The even-odd rule counts the total number of path crossings that form a shape and if the count is odd, that shape is filled. This is definitely a case when a picture is worth a thousand words.

      The number of path crossings in the even-odd diagram that form the pentagon shape is even, so the pentagon is not filled, whereas the number path crossings that form each triangle is odd, so the triangles are filled.
      CAShapeLayerFillRules

    • Calls the path drawing and layer set up code, and then it adds the layer to the view hierarchy.

This code draws the raywenderlich.com logo:

RayWenderlichLogo

And in case you’re curious to know what this drawing looks like in PaintCode:

PaintCodeRayWenderlichLogo

Layer Player includes controls to manipulate many of CAShapeLayer’s properties:

CAShapeLayer_5.5

Note: You may notice that we’re skipping over the next demo in the Layer Player app. This is because CAEAGLLayer is effectively obsoleted by CAMetalLayer, which debuted with iOS 8 alongside the Metal framework. You can find a great tutorial covering CAMetalLayer here.

Example #9: CATransformLayer

CATransformLayer does not flatten its sublayer hierarchy like other layer classes, so it’s handy for drawing 3D structures. It’s actually a container for its sublayers, and each sublayer can have its own transforms and opacity changes, however, it ignores changes to other rendered layer properties such as border width and color.

You cannot directly hit test a transform layer because it doesn’t have a 2D coordinate space to map a touch point to, however, it’s possible to hit test individual sublayers. Here’s an example:

import UIKit class ViewController: UIViewController {   @IBOutlet weak var someView: UIView!   // 1  let sideLength = CGFloat(160.0)  var redColor = UIColor.redColor()  var orangeColor = UIColor.orangeColor()  var yellowColor = UIColor.yellowColor()  var greenColor = UIColor.greenColor()  var blueColor = UIColor.blueColor()  var purpleColor = UIColor.purpleColor()  var transformLayer = CATransformLayer()   // 2  func setUpTransformLayer() {    var layer = sideLayerWithColor(redColor)    transformLayer.addSublayer(layer)     layer = sideLayerWithColor(orangeColor)    var transform = CATransform3DMakeTranslation(sideLength / 2.0, 0.0, sideLength / -2.0)    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)    layer.transform = transform    transformLayer.addSublayer(layer)     layer = sideLayerWithColor(yellowColor)    layer.transform = CATransform3DMakeTranslation(0.0, 0.0, -sideLength)    transformLayer.addSublayer(layer)     layer = sideLayerWithColor(greenColor)    transform = CATransform3DMakeTranslation(sideLength / -2.0, 0.0, sideLength / -2.0)    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)    layer.transform = transform    transformLayer.addSublayer(layer)     layer = sideLayerWithColor(blueColor)    transform = CATransform3DMakeTranslation(0.0, sideLength / -2.0, sideLength / -2.0)    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)    layer.transform = transform    transformLayer.addSublayer(layer)     layer = sideLayerWithColor(purpleColor)    transform = CATransform3DMakeTranslation(0.0, sideLength / 2.0, sideLength / -2.0)    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)    layer.transform = transform    transformLayer.addSublayer(layer)     transformLayer.anchorPointZ = sideLength / -2.0    applyRotationForXOffset(16.0, yOffset: 16.0)  }   // 3  func sideLayerWithColor(color: UIColor) -> CALayer {    let layer = CALayer()    layer.frame = CGRect(origin: CGPointZero, size: CGSize(width: sideLength, height: sideLength))    layer.position = CGPoint(x: CGRectGetMidX(someView.bounds), y: CGRectGetMidY(someView.bounds))    layer.backgroundColor = color.CGColor    return layer  }   func degreesToRadians(degrees: Double) -> CGFloat {    return CGFloat(degrees * M_PI / 180.0)  }   // 4  func applyRotationForXOffset(xOffset: Double, yOffset: Double) {    let totalOffset = sqrt(xOffset * xOffset + yOffset * yOffset)    let totalRotation = CGFloat(totalOffset * M_PI / 180.0)    let xRotationalFactor = CGFloat(totalOffset) / totalRotation    let yRotationalFactor = CGFloat(totalOffset) / totalRotation    let currentTransform = CATransform3DTranslate(transformLayer.sublayerTransform, 0.0, 0.0, 0.0)    let rotationTransform = CATransform3DRotate(transformLayer.sublayerTransform, totalRotation,      xRotationalFactor * currentTransform.m12 - yRotationalFactor * currentTransform.m11,      xRotationalFactor * currentTransform.m22 - yRotationalFactor * currentTransform.m21,      xRotationalFactor * currentTransform.m32 - yRotationalFactor * currentTransform.m31)    transformLayer.sublayerTransform = rotationTransform  }   // 5  override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {    if let location = touches.anyObject()?.locationInView(someView) {      for layer in transformLayer.sublayers {        if let hitLayer = layer.hitTest(location) {          println("Transform layer tapped!")          break        }      }    }  }   override func viewDidLoad() {    super.viewDidLoad()     // 6    setUpTransformLayer()    someView.layer.addSublayer(transformLayer)  } }

The above code does all of this:

  1. Creates properties for side length, colors for each side of the cube, and a transform layer.
  2. Builds the cube by creating, rotating and then adding each side to the transform layer. Then it sets the transform layer’s z axis anchor point, rotates the cube and adds the cube to the view hierarchy.
  3. Creates helper code to create each cube side layer with the specified color and to convert degrees to radians. Why radians? Simply because I find it more intuitive to work with degrees than radians. :]
  4. Applies a rotation based on specified x and y offsets. Notice that the code sets the transform tosublayerTransform, and that applies to the sublayers of the transform layer.
  5. Observes touches and cycles through the sublayers of the transform layer. This section hit tests each one and breaks out as soon as a hit is detected, since there are no benefits to hit testing remaining layers.
  6. Sets up the transform layer and adds it to the view hierarchy.

Note: So what’s with all those currentTransform.m##s? I’m glad you asked, sort of :]. These are CATransform3D properties that represent elements of a matrix that comprises a rectangular array of rows and columns.

To learn more about matrix transformations like those used in this example, check out 3DTransformFun projectby fellow tutorial team member Rich Turton and Enter The Matrix project by Mark Pospesel.

Running the above code with someView being a 250 x 250 view results in this:

CATransformLayer

Now, try something: tap anywhere on the cube and “Transform layer tapped!” will print to the console.

Layer Player includes switches to toggle the opacity of each sublayer, and the TrackBall utility from Bill Dudney,ported to Swift, which makes it easy to apply 3D transforms based on user gestures:

CATransformLayer_5.5

Example #10: CAEmitterLayer

CAEmitterLayer renders animated particles that are instances of CAEmitterCell. Both CAEmitterLayer and CAEmitterCell have properties to change rendering rate, size, shape, color, velocity, lifetime and more. Here’s an example:

import UIKit class ViewController: UIViewController {   // 1  let emitterLayer = CAEmitterLayer()  let emitterCell = CAEmitterCell()   // 2  func setUpEmitterLayer() {    emitterLayer.frame = view.bounds    emitterLayer.seed = UInt32(NSDate().timeIntervalSince1970)    emitterLayer.renderMode = kCAEmitterLayerAdditive    emitterLayer.drawsAsynchronously = true    setEmitterPosition()  }   // 3  func setUpEmitterCell() {    emitterCell.contents = UIImage(named: "smallStar")?.CGImage     emitterCell.velocity = 50.0    emitterCell.velocityRange = 500.0     emitterCell.color = UIColor.blackColor().CGColor    emitterCell.redRange = 1.0    emitterCell.greenRange = 1.0    emitterCell.blueRange = 1.0    emitterCell.alphaRange = 0.0    emitterCell.redSpeed = 0.0    emitterCell.greenSpeed = 0.0    emitterCell.blueSpeed = 0.0    emitterCell.alphaSpeed = -0.5     let zeroDegreesInRadians = degreesToRadians(0.0)    emitterCell.spin = degreesToRadians(130.0)    emitterCell.spinRange = zeroDegreesInRadians    emitterCell.emissionRange = degreesToRadians(360.0)     emitterCell.lifetime = 1.0    emitterCell.birthRate = 250.0    emitterCell.xAcceleration = -800.0    emitterCell.yAcceleration = 1000.0  }   // 4  func setEmitterPosition() {    emitterLayer.emitterPosition = CGPoint(x: CGRectGetMidX(view.bounds), y: CGRectGetMidY(view.bounds))  }   func degreesToRadians(degrees: Double) -> CGFloat {    return CGFloat(degrees * M_PI / 180.0)  }   override func viewDidLoad() {    super.viewDidLoad()     // 5    setUpEmitterLayer()    setUpEmitterCell()    emitterLayer.emitterCells = [emitterCell]    view.layer.addSublayer(emitterLayer)  }   // 6  override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {    setEmitterPosition()  } }

The above code:

  1. Creates an emitter layer and cell.
  2. Sets up the emitter layer by doing the following:
    • Provides a seed for the layer’s random number generator that in turn randomizes certain properties of the layer’s emitter cells, such as velocity. This is further explained in the next comment.
    • Renders emitter cells above the layer’s background color and border in an order specified by renderMode.

      Note: Apple’s documentation currently incorrectly states that values for this property are defined under Emitter Modes. In fact, renderMode’s values are defined under Emitter Render Order. The default value is unordered, and additional options include oldest first, oldest last, back to front and additive.

    • Sets drawsAsynchronously to true, which may improve performance because the emitter layer must continuously redraw its emitter cells.
    • Next, the emitter position is set via a helper method — learn more on comment 4. This is a good case study for how setting drawsAsynchronously to true has a positive effect on performance and smoothness of animation.
  3. There’s a lot of action in this block!
    • It sets up the emitter cell by setting its contents to an image (this image is available in the Layer Player project).
    • Then it specifies an initial velocity and max variance (velocityRange); the emitter layer uses the aforementioned seed to create a random number generator that randomizes values within the range (initial value +/- the range value). This randomization happens for any properties ending in Range.
    • The color is set to black to allow the variance (discussed below) to vary from the default of white, because white results in overly bright particles.
    • A series of color ranges are set next, using the same randomization as for velocityRange, this time to specify the range of variance to each color. Speed values dictate how quickly each color can change over the lifetime of the cell.
    • Next, block three specifies how to distribute the cells around a full circular cone. More detail: It sets the emitter cell’s spinning velocity and emission range. Furthermore, emission range determines how emitter cells are distributed around a cone that is defined by the emissionRange specified in radians.
    • Sets the cell’s lifetime to 1 second. This property’s default value is 0, so if you don’t explicitly set this, your cells never appear! Same goes for birthRate (per second); the default is 0, so this must be set to some positive number in order for cells to appear.
    • Lastly, cell x and y acceleration are set; these values affect the visual angle to which the particles emit.
  4. Makes helper methods to convert degrees to radians and to set the emitter cell position to the midpoint of the view.
  5. Sets up the emitter layer and cell, and then adds that cell to the layer, and the layer to the view hierarchy.
  6. This method, new in iOS 8, provides a way to handle changes to the current trait collection, such as when the device is rotated. Not familiar with trait collections? Check out Section 1 of iOS 8 by Tutorials and you’ll become a master of them :]

Huzzah! I know that’s a lot of information, but you’re tough and smart.

The outcome of running the above code reminds me of those The More You Know commercials:

CAEmitterLayer2

Layer Player includes controls to adjust all of the above-mentioned properties, and several more:

CAEmitterLayer_5.5

1 0