Core Graphics Tutorial Part 1: Getting Started

Imagine you’ve finished your app and it works just fine, but the interface lacks style. You could draw several sizes of all your custom control images in Photoshop and hope that Apple doesn’t come out with a @4x retina screen… or, you could think ahead and use Core Graphics to create one image in code that scales crisply for any device size.

Core Graphics is Apple’s vector drawing framework – it’s a big, powerful API and there’s a lot to learn. But never fear – this three-part series will ease you into it by starting out simple, and by the end you’ll be able to create stunning graphics ready to use in your apps.

This is a brand new series, with a modern approach to teaching Core Graphics. The series also covers cool features like @IBDesignable and @IBInspectable that make learning Core Graphics fun and easy.

So grab your favorite beverage, it’s time to begin!

Introducing Flo – One glass at a time

You’ll be creating a complete app to track your drinking habits.

Specifically, it makes it easy to track how much water you drink. “They” tell us that drinking eight glasses of water a day is healthy, but it’s easy to lose track after a few glasses. This is where Flo comes in; every time you polish off a refreshing glass of water, tap the counter. You’ll also see a graph of your previous seven days’ consumption.

In the first part of this series, you’ll create three controls using UIKit’s drawing methods.

Then in part two, you’ll have a deeper look at Core Graphics contexts and draw the graph.

In part three, you’ll create a patterned background and award yourself a homemade Core Graphics medal. :]

Getting Started

Your first task is to create your very own Flo app. There is no download to get you going, because you’ll learn more if you build it from the ground up.

Create a new project (File\New\Project…), select the template iOS\Application\Single View App and click Next.

Fill out the project options. Set the Product Name to Flo, the Language to Swift, and click Next.

On the final screen, uncheck Create Git repository and click Create.

You now have a starter project with a storyboard and a view controller.

Custom Drawing on Views

There are three steps for custom drawings:

  1. Create a UIView subclass.
  2. Override draw(_:) and add some Core Graphics drawing code.
  3. There is no step 3 – that’s it! :]

You’ll try this out by making a custom-drawn plus button, like this:

Create a new file (File\New\File…), choose iOS\Source\Cocoa Touch Class, click Next. In this screen, name the new class PushButton, make it a subclass of UIButton, and ensure the language is Swift. Click Next and then Create.

UIButton is a subclass of UIView, so all methods in UIView, such as draw(_:), are also available in UIButton.

In Main.storyboard, drag a UIButton into the view controller’s view, and select the button in the Document Outline.

In the Identity Inspector, change the class to use your own PushButton.

Auto Layout Constraints

Now you’ll set up the Auto Layout constraints (text instructions follow):

  1. With the button selected, Control-drag from the center of the button slightly left (still within the button), and choose Width from the popup menu.
  2. Similarly, with the button selected, control-drag from the center of the button slightly up (still within the button), and choose Height from the popup menu.
  3. Control-drag left from inside the button to outside the button, and choose Center Vertically in Safe Area.
  4. Finally control-drag up from inside the button to outside the button and choose Center Horizontally in Safe Area.

This will create the four required Auto Layout constraints; you can now see them in the Size Inspector:

Click Edit on the Align center Y constraint, and set its constant to be 100. This will shift the vertical position of the button from the center to 100 points below the center. Change the Width and Heightconstraint constants to be equal to 100 too. The final constraints should look like this:

In the Attributes Inspector, remove the default title “Button”.

You can build and run at this point if you’d like, but right now you’ll just see a blank screen. It’s time to fix that up!

Drawing the Button

Recall the button you’re trying to make is circular:

To draw a shape in Core Graphics, you define a path that tells Core Graphics the line to trace (like two straight lines for the plus) or the line to fill (like the circle which should be filled here). If you’re familiar with Illustrator or the vector shapes in Photoshop, then you’ll easily understand paths.

There are three fundamentals to know about paths:

  • path can be stroked and filled.
  • stroke outlines the path in the current stroke color.
  • fill will fill up a closed path with the current fill color.

One easy way to create a Core Graphics path is through a handy class called UIBezierPath. This lets you easily create paths with a user-friendly API, whether you want to create paths based on lines, curves, rectangles, or a series of connected points.

Try using UIBezierPath to create a path, and then fill it with a green color. To do this, open PushButton.swift and add this method:

override func draw(_ rect: CGRect) {
  let path = UIBezierPath(ovalIn: rect)
  UIColor.green.setFill()
  path.fill()
}

First, you create an oval-shaped UIBezierPath that is the size of the rectangle passed to it. In this case, it’ll be the size of the 100×100 button you defined in the storyboard, so the “oval” will actually be a circle.

Paths themselves don’t draw anything. You can define paths without an available drawing context. To draw the path, you set a fill color on the current context (more on this below), and then fill the path.

Build and run the application, and you’ll see the green circle.

So far, you’ve discovered how easy it is to make custom-shaped views. You’ve done this by creating a UIButton subclass, overriding draw(_:) and adding the UIButton to your storyboard.

Behind the Scenes in Core Graphics

Each UIView has a graphics context, and all drawing for the view renders into this context before being transferred to the device’s hardware.

iOS updates the context by calling draw(_:) whenever the view needs to be updated. This happens when:

  • The view is new to the screen.
  • Other views on top of it are moved.
  • The view’s hidden property is changed.
  • Your app explicitly calls the setNeedsDisplay() or setNeedsDisplayInRect() methods on the view.

Note: Any drawing done in draw(_:) goes into the view’s graphics context. Be aware that if you start drawing outside of draw(_:), as you’ll do in the final part of this tutorial, you’ll have to create your own graphics context.

You haven’t used Core Graphics yet in this tutorial because UIKit has wrappers around many of the Core Graphics functions. A UIBezierPath, for example, is a wrapper for a CGMutablePath, which is the lower-level Core Graphics API.

Note: Never call draw(_:) directly. If your view is not being updated, then call setNeedsDisplay() on the view.

setNeedsDisplay() does not itself call draw(_:), but it flags the view as ‘dirty’, triggering a redraw using draw(_:) on the next screen update cycle. Even if you call setNeedsDisplay() five times in the same method you’ll only ever actually call draw(_:) once.

@IBDesignable – Interactive Drawing

Creating code to draw a path and then running the app to see what it looks like can be about as exciting as watching paint dry, but you’ve got options. Live Rendering allows views to draw themselves more accurately in a storyboard, by running their draw(_:) methods. What’s more, the storyboard will immediately update to changes in draw(_:). All you need is a single attribute!

Still in PushButton.swift, just before the class declaration, add:

@IBDesignable

This is all that is needed to enable Live Rendering. Go back to Main.storyboard and notice that now, your button is shown as a green circle, just like when you build and run.

Now set up your screen so that you have the storyboard and the code side-by-side.

Do this by selecting PushButton.swift to show the code, then at the top right, click the Assistant Editor — the icon that looks like two intertwined rings. The storyboard should then show on the right-hand pane. If it doesn’t, you’ll have to choose the storyboard in the breadcrumb trail at the top of the pane:

Close the document outline at the left of the storyboard to free up some room. Do this either by dragging the edge of the document outline pane or clicking the button at the bottom of the storyboard:

When you’re all done, your screen should look like this:

In PushButton‘s draw(_:), change

UIColor.green.setFill()

to

UIColor.blue.setFill()

and you’ll (nearly) immediately see the change in the storyboard. Pretty cool!

Now you’ll create the lines for the plus sign.

Drawing Into the Context

Core Graphics uses a “painter’s model”. When you draw into a context, it’s almost like making a painting. You lay down a path and fill it, and then lay down another path on top and fill it. You can’t change the pixels that have been laid down, but you can “paint” over them.

This image from Apple’s documentation describes how this works. Just as it is when you’re painting on a canvas, the order in which you draw is critical.

Your plus sign is going on top of the blue circle, so first you code the blue circle and then the plus sign.

You could draw two rectangles for the plus sign, but it’s easier to draw a path and then stroke it with the desired thickness.

Add this struct and these constants inside of PushButton:

private struct Constants {
  static let plusLineWidth: CGFloat = 3.0
  static let plusButtonScale: CGFloat = 0.6
  static let halfPointShift: CGFloat = 0.5
}
  
private var halfWidth: CGFloat {
  return bounds.width / 2
}
  
private var halfHeight: CGFloat {
  return bounds.height / 2
}

Now add this code at the end of the draw(_:) method to draw the horizontal dash of the plus sign:

//set up the width and height variables
//for the horizontal stroke
let plusWidth: CGFloat = min(bounds.width, bounds.height) * Constants.plusButtonScale
let halfPlusWidth = plusWidth / 2

//create the path
let plusPath = UIBezierPath()

//set the path's line width to the height of the stroke
plusPath.lineWidth = Constants.plusLineWidth

//move the initial point of the path
//to the start of the horizontal stroke
plusPath.move(to: CGPoint(
  x: halfWidth - halfPlusWidth,
  y: halfHeight))

//add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(
  x: halfWidth + halfPlusWidth,
  y: halfHeight))

//set the stroke color
UIColor.white.setStroke()

//draw the stroke
plusPath.stroke()

In this block, you set up a UIBezierPath, give it a start position (left side of the circle) and draw to the end position (right side of the circle). Then you stroke the path outline in white. At this point, you should see this in the Storyboard:

In your storyboard, you’ll now have a blue circle with a dash in the middle of it:

Note: Remember that a path simply consists of points. Here’s an easy way to grasp the concept: when creating the path imagine that you have a pen in hand. Put two dots on a page, then place the pen at the starting point, and then draw a line to the next point by drawing a line.That’s essentially what you do with the above code by using move(to:) and addLine(to:).

Now run the application on either an iPad 2 or an iPhone 6 Plus simulator, and you’ll notice the dash is not as crisp as it should be. It has a pale blue line encircling it.

Points and Pixels

Back in the days of the very first iPhones, points and pixels occupied the same space and were the same size, making them essentially the same thing. When retina iPhones came into existence, suddenly there were four times the pixels on the screen for the same number of points.

Similarly, the iPhone 6 Plus has once again increased the amount of pixels for the same points.

Note: The following is conceptual – the actual hardware pixels may differ. For example, after rendering 3x, the iPhone 6 Plus downsamples to display the full image on the screen. To learn more about iPhone 6 Plus downsampling, check out this great post.

Here’s a grid of 12×12 pixels, where points are shown in gray and white. The first (iPad 2) is a direct mapping of points to pixels. The second (iPhone 6) is a 2x retina screen, where there are 4 pixels to a point, and the third (iPhone 6 Plus) is a 3x retina screen, where there are 9 pixels to a point.

The line you’ve just drawn is 3 points high. Lines stroke from the center of the path, so 1.5 points will draw on either side of the center line of the path.

This picture shows drawing a 3-point line on each of the devices. You can see that the iPad 2 and the iPhone 6 Plus result in the line being drawn across half a pixel — which of course can’t be done. So, iOS anti-aliases the half-filled pixels with a color half way between the two colors, and the line looks fuzzy.

In reality, the iPhone 6 Plus has so many pixels, that you probably won’t notice the fuzziness, although you should check this for your own app on the device. But if you’re developing for non-retina screens like the iPad 2 or iPad mini, you should do anything you can to avoid anti-aliasing.

If you have oddly sized straight lines, you’ll need to position them at plus or minus 0.5 points to prevent anti-aliasing. If you look at the diagrams above, you’ll see that a half point on the iPad 2 will move the line up half a pixel, on the iPhone 6, up one whole pixel, and on the iPhone 6 Plus, up one and a half pixels.

In draw(_:), replace the move(to:) and addLine(to:) code lines with:

//move the initial point of the path
//to the start of the horizontal stroke
plusPath.move(to: CGPoint(
  x: halfWidth - halfPlusWidth + Constants.halfPointShift,
  y: halfHeight + Constants.halfPointShift))
    
//add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(
  x: halfWidth + halfPlusWidth + Constants.halfPointShift,
  y: halfHeight + Constants.halfPointShift))

iOS will now render the lines sharply on all three devices because you’re now shifting the path by half a point.

Note: For pixel perfect lines, you can draw and fill a UIBezierPath(rect:) instead of a line, and use the view’s contentScaleFactor to calculate the width and height of the rectangle. Unlike strokes that draw outwards from the center of the path, fills only draw inside the path.

Add the vertical stroke of the plus just after the previous two lines of code, and before setting the stroke color in draw(_:). I bet you can figure out how to do this on your own, since you’ve already drawn a horizontal stroke:

Solution Inside: Solution Hide
//Vertical Line
 
plusPath.move(to: CGPoint(
  x: halfWidth + Constants.halfPointShift,
  y: halfHeight - halfPlusWidth + Constants.halfPointShift))
      
plusPath.addLine(to: CGPoint(
  x: halfWidth + Constants.halfPointShift,
  y: halfHeight + halfPlusWidth + Constants.halfPointShift))

This is basically the same code you used to draw the horizontal line on your button.

You should now see the live rendering of the plus button in your storyboard. This completes the drawing for the plus button.

@IBInspectable – Custom Storyboard Properties

So you know that frantic moment when you tap a button more than needed, just to make sure it registers? Well, you need to provide a way for the user to reverse such overzealous tapping — you need a minus button.

A minus button is identical to the plus button except that it has no vertical bar and sports a different color. You’ll use the same PushButton class for the minus button, and declare what sort of button it is and its color when you add it to your storyboard.

@IBInspectable is an attribute you can add to a property that makes it readable by Interface Builder. This means that you will be able to configure the color for the button in your storyboard instead of in code.

At the top of the PushButton class, add these two properties:

@IBInspectable var fillColor: UIColor = UIColor.green
@IBInspectable var isAddButton: Bool = true

Change the fill color code at the top of draw(_:) from

UIColor.blue.setFill()

to:

fillColor.setFill()	

The button will turn green in your storyboard view.

Surround the vertical line code in draw(_:) with an if statement:

//Vertical Line

if isAddButton {
  //vertical line code move(to:) and addLine(to:)
}
//existing code
//set the stroke color
UIColor.white.setStroke()
plusPath.stroke()

This makes it so you only draw the vertical line if isAddButton is set – this way the button can be either a plus or a minus button.

The completed PushButton looks like this:

import UIKit

@IBDesignable
class PushButton: UIButton {
  
  private struct Constants {
    static let plusLineWidth: CGFloat = 3.0
    static let plusButtonScale: CGFloat = 0.6
    static let halfPointShift: CGFloat = 0.5
  }
  
  private var halfWidth: CGFloat {
    return bounds.width / 2
  }
  
  private var halfHeight: CGFloat {
    return bounds.height / 2
  }
  
  @IBInspectable var fillColor: UIColor = UIColor.green
  @IBInspectable var isAddButton: Bool = true
  
  override func draw(_ rect: CGRect) {
    let path = UIBezierPath(ovalIn: rect)
    fillColor.setFill()
    path.fill()
    
    //set up the width and height variables
    //for the horizontal stroke
    let plusWidth: CGFloat = min(bounds.width, bounds.height) * Constants.plusButtonScale
    let halfPlusWidth = plusWidth / 2
    
    //create the path
    let plusPath = UIBezierPath()
    
    //set the path's line width to the height of the stroke
    plusPath.lineWidth = Constants.plusLineWidth
    
    //move the initial point of the path
    //to the start of the horizontal stroke
    plusPath.move(to: CGPoint(
            x: halfWidth - halfPlusWidth + Constants.halfPointShift,
            y: halfHeight + Constants.halfPointShift))
        
    //add a point to the path at the end of the stroke
    plusPath.addLine(to: CGPoint(
            x: halfWidth + halfPlusWidth + Constants.halfPointShift,
            y: halfHeight + Constants.halfPointShift))

    if isAddButton {
      //move the initial point of the path
      //to the start of the horizontal stroke
      plusPath.move(to: CGPoint(
        x: halfWidth - halfPlusWidth + Constants.halfPointShift,
        y: halfHeight + Constants.halfPointShift))
      
      //add a point to the path at the end of the stroke
      plusPath.addLine(to: CGPoint(
        x: halfWidth + halfPlusWidth + Constants.halfPointShift,
        y: halfHeight + Constants.halfPointShift))
    }
    
    //set the stroke color
    UIColor.white.setStroke()
    plusPath.stroke()
  }
}

In your storyboard, select the push button view. The two properties you declared with @IBInspectableappear at the top of the Attributes Inspector:

Change Fill Color to RGB(87, 218, 213), and change the Is Add Button to off. Change the color by going to Fill Color\Other…\Color Sliders and entering the values in each input field next to the colors, so it looks like this:

The changes will take place immediately in the storyboard:

Pretty cool, eh? Now change Is Add Button back to on to return the button to a plus button.

A Second Button

Add a new UIButton to the storyboard and select it. Change its class to PushButton as you did it with previous one:

The green plus button will be drawn under your old plus button.

In the Attributes Inspector, change Fill Color to RGB(238, 77, 77) and change Is Add Button to off.

Remove the default title Button.

Add the Auto Layout constraints for the new view similarly to how you did before:

  • With the button selected, Control-drag from the center of the button slightly to the left (still within the button), and choose Width from the popup menu.
  • Similarly, with the button selected, Control-drag from the center of the button slightly up (still within the button), and choose Height from the popup menu.
  • Control-drag left from inside the button to outside the button and choose Center Horizontally in Safe Area.
  • Control-drag up from the bottom button to the top button, and choose Vertical Spacing.

After you add the constraints, edit their constant values in the Size Inspector to match these:

Build and run the application. You now have a reusable customizable view that you can add to any app. It’s also crisp and sharp on any size device. Here it is on the iPhone 4S.

Arcs with UIBezierPath

The next customized view you’ll create is this one:

This looks like a filled shape, but the arc is actually just a fat stroked path. The outlines are another stroked path consisting of two arcs.

Create a new file, File\New\File…, choose Cocoa Touch Class, and name the new class CounterView. Make it a subclass of UIView, and ensure the language is Swift. Click Next, and then click Create.

Replace the code with:

import UIKit

@IBDesignable class CounterView: UIView {
  
  private struct Constants {
    static let numberOfGlasses = 8
    static let lineWidth: CGFloat = 5.0
    static let arcWidth: CGFloat = 76
    
    static var halfOfLineWidth: CGFloat {
      return lineWidth / 2
    }
  }
  
  @IBInspectable var counter: Int = 5
  @IBInspectable var outlineColor: UIColor = UIColor.blue
  @IBInspectable var counterColor: UIColor = UIColor.orange
  
  override func draw(_ rect: CGRect) {
    
  }
}

Here you also create a struct with constants. Those constants will be used when drawing, the odd one out – numberOfGlasses – is the target number of glasses to drink per day. When this figure is reached, the counter will be at its maximum.

You also create three @IBInspectable properties that you can update in the storyboard. The variable counter keeps track of the number of glasses consumed, and it’s an @IBDesignable property as it is useful to have the ability to change it in the storyboard, especially for testing the counter view.

Go to Main.storyboard and add a UIView above the plus PushButton. Add the Auto Layout constraints for the new view similarly to how you did before:

  1. With the view selected, Control-drag from the center of the button slightly left (still within the view), and choose Width from the popup menu.
  2. Similarly, with the view selected, Control-drag from the center of the button slightly up (still within the view), and choose Height from the popup menu.
  3. Control-drag left from inside the view to outside the view and choose Center Horizontally in Safe Area.
  4. Control-drag down from the view to the top button, and choose Vertical Spacing.

Edit the constraint constants in the Size Inspector to look like this:

In the Identity Inspector, change the class of the UIView to CounterView. Any drawing that you code in draw(_:) will now show up in the view (but you’ve not added any yet!).

Impromptu Math Lesson

We interrupt this tutorial for a brief, and hopefully un-terrifying look back at high school level math. As Douglas Adams would say – Don’t Panic! :]

Drawing in the context is based on this unit circle. A unit circle is a circle with a radius of 1.0.

The red arrow shows where your arc will start and end, drawing in a clockwise direction. You’ll draw an arc from the position 3π / 4 radians — that’s the equivalent of 135º, clockwise to π / 4 radians – that’s 45º.

Radians are generally used in programming instead of degrees, and it’s useful to be able to think in radians so that you don’t have to convert to degrees every time you want to work with circles. Later on you’ll need to figure out the arc length, which is when radians will come into play.

An arc’s length in a unit circle (where the radius is 1.0) is the same as the angle’s measurement in radians. For example, looking at the diagram above, the length of the arc from 0º to 90º is π/2. To calculate the length of the arc in a real situation, take the unit circle arc length and multiply it by the actual radius.

To calculate the length of the red arrow above, you would simply need to calculate the number of radians it spans:

2π – end of arrow (3π/4) + point of arrow (π/4) = 3π/2

In degrees that would be:

360º – 135º + 45º = 270º

Back to Drawing Arcs

In CounterView.swift, add this code to draw(_:) to draw the arc:

// 1
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)

// 2
let radius: CGFloat = max(bounds.width, bounds.height)

// 3
let startAngle: CGFloat = 3 * .pi / 4
let endAngle: CGFloat = .pi / 4

// 4
let path = UIBezierPath(arcCenter: center,
                           radius: radius/2 - Constants.arcWidth/2,
                       startAngle: startAngle,
                         endAngle: endAngle,
                        clockwise: true)

// 5
path.lineWidth = Constants.arcWidth
counterColor.setStroke()
path.stroke()

The following explains what each section does:

  1. Define the center point of the view where you’ll rotate the arc around.
  2. Calculate the radius based on the max dimension of the view.
  3. Define the start and end angles for the arc.
  4. Create a path based on the center point, radius, and angles you just defined.
  5. Set the line width and color before finally stroking the path.

Imagine drawing this with a compass — you’d put the point of the compass in the center, open the arm to the radius you need, load it with a thick pen and spin it to draw your arc.

In this code, center is the point of the compass, radius is the width that the compass is open (minus half the width of the pen) and the arc width is the width of the pen.

Note: When you’re drawing arcs, this is generally all you need to know, but if you want to dive further into drawing arcs, then Ray’s (older) Core Graphics Tutorial on Arcs and Paths will help.

In the storyboard and when you run your application, this is what you’ll see:

Outlining the Arc

When the user indicates they’ve enjoyed a glass of water, an outline on the counter shows the progress towards the goal of eight glasses.

This outline will consist of two arcs, one outer and one inner, and two lines connecting them.

In CounterView.swift , add this code to the end of draw(_:):

//Draw the outline

//1 - first calculate the difference between the two angles
//ensuring it is positive
let angleDifference: CGFloat = 2 * .pi - startAngle + endAngle
//then calculate the arc for each single glass
let arcLengthPerGlass = angleDifference / CGFloat(Constants.numberOfGlasses)
//then multiply out by the actual glasses drunk
let outlineEndAngle = arcLengthPerGlass * CGFloat(counter) + startAngle

//2 - draw the outer arc
let outlinePath = UIBezierPath(arcCenter: center,
                                  radius: bounds.width/2 - Constants.halfOfLineWidth,
                              startAngle: startAngle,
                                endAngle: outlineEndAngle,
                               clockwise: true)

//3 - draw the inner arc
outlinePath.addArc(withCenter: center,
                       radius: bounds.width/2 - Constants.arcWidth + Constants.halfOfLineWidth,
                   startAngle: outlineEndAngle,
                     endAngle: startAngle,
                    clockwise: false)
    
//4 - close the path
outlinePath.close()
    
outlineColor.setStroke()
outlinePath.lineWidth = Constants.lineWidth
outlinePath.stroke()

A few things to go through here:

  1. outlineEndAngle is the angle where the arc should end, calculated using the current counter value.
  2. outlinePath is the outer arc. The radius is given to UIBezierPath() to calculate the actual length of the arc, as this arc is not a unit circle.
  3. Adds an inner arc to the first arc. This has the same angles but draws in reverse (clockwise is set to false). Also, this draws a line between the inner and outer arc automatically.
  4. Closing the path automatically draws a line at the other end of the arc.

With the counter property in CounterView.swift set to 5, your CounterView should now look like this in the storyboard:

Open Main.storyboard, select the CounterView and in the Attributes Inspector, change the Counterproperty to check out your drawing code. You’ll find that it is completely interactive. Try adjusting the counter to be more than eight and less than zero. You’ll fix that up later on.

Change the Counter Color to RGB(87, 218, 213), and change the Outline Color to RGB(34, 110, 100).

Making it All Work

Congrats! You have the controls; all you have to do is wire them up so the plus button increments the counter and the minus button decrements the counter.

In Main.storyboard, drag a UILabel to the center of the Counter View, and make sure it is a subview of the Counter View. It will look like this in the document outline:

Add constraints to center the label both vertically and horizontally. In the end, the label should have constraints that look like this:

In the Attributes Inspector, change Alignment to centerfont size to 36 and the default label title to 8.

Go to ViewController.swift and add these properties to the top of the class:

//Counter outlets
@IBOutlet weak var counterView: CounterView!
@IBOutlet weak var counterLabel: UILabel!

Still in ViewController.swift, add this method to the end of the class:

@IBAction func pushButtonPressed(_ button: PushButton) {
  if button.isAddButton {
    counterView.counter += 1
  } else {
    if counterView.counter > 0 {
      counterView.counter -= 1
    }
  }
  counterLabel.text = String(counterView.counter)
}

Here you increment or decrement the counter depending on the button’s isAddButton property, make sure the counter doesn’t drop below zero — nobody can drink negative water. :] You also update the counter value in the label.

Also add this code to the end of viewDidLoad() to ensure that the initial value of the counterLabel will be updated:

  
counterLabel.text = String(counterView.counter)  

In Main.storyboard, connect the CounterView outlet and UILabel outlet. Connect the method to the Touch Up Inside event of the two PushButtons.

Run the application and see if your buttons update the counter label. They should.

But wait, why isn’t the counter view updating?

Think way back to the beginning of this tutorial, and how you only call draw(_:) when other views on top of it are moved, or its hidden property is changed, or the view is new to the screen, or your app calls the setNeedsDisplay() or setNeedsDisplayInRect() methods on the view.

However, the Counter View needs to be updated whenever the counter property is updated, otherwise the user will think your app is busted.

Go to CounterView.swift and change the counter property declaration to:

@IBInspectable var counter: Int = 5 {
  didSet {
    if counter <=  Constants.numberOfGlasses {
      //the view needs to be refreshed
      setNeedsDisplay()
    }
  }
}

This code makes it so that the view refreshes only when the counter is less than or equal to the user’s targeted glasses, as the outline only goes up to 8.

Run your app again. Everything should now be working properly.

Where to Go From Here?

You’ve covered basic drawing in this tutorial, and you should now be able to change the shape of views in your UIs. But wait – there’s more! In Part 2 of this tutorial , you’ll explore Core Graphics contexts in more depth and create a graph of your water consumption over time.

Advertisements
Core Graphics Tutorial Part 1: Getting Started

Core Animation Swift Tutorial – Animatable Properties

This tutorial series requires a basic understanding of UIView hierarchy.

When you first hear about Core Animation, you might think it is all about animation. However, animation is only a part of this framework. Core Animation is an infrastructure for compositing and manipulating visual content onscreen. It uses the GPU to accelerate the rendering of on-screen objects. It divides the content onscreen into individual objects called layers, and arranges them in a tree hierarchy (known as the layer tree). You are familiar with the UIView tree hierarchy, which is built upon the layer tree. In other words, Core Animation accounts for everything that you see onscreen.

In the first part of this tutorial series, you are going to learn the basics of CALayer, how to use its properties to create neat visual effects easily, and at the end of this tutorial, you will learn the most important subclass of CALayer – CAShapeLayer.

What Is CALayer?

CALayers are rectangular objects that can contain visual content. They are stored into a tree hierarchy, and each manages the position of its children sublayers.

Sound familiar? You may say, “It’s like UIView!”

That’s true, but it’s not just a coincidence. Every UIView has a layer property known as the backing layer, which is an instance of CALayerUIView just ensures the corresponding back layers are correctly connected in the layer tree when subviews are added or removed from the view. It is the backing layer that is responsible for display and animation of the view. The only major feature that the backing layer does not handle is user interaction.

For the purposes of going through this tutorial, I recommend creating an empty single-view iPhone application.

Exploring CALayer

Creating a CALayer instance is very straightforward.

1
let redLayer = CALayer()

We can set its framebackgroundColor, and add it to a superlayer just like we do with UIView.

1
2
3
redLayer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
redLayer.backgroundColor = UIColor.red.cgColor
layer.addSublayer(redLayer)

Add this code to a function called setup in the file ViewController.swift, and call the method from viewDidLoad. You should have something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import UIKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setup()
    }
    func setup() {
        let redLayer = CALayer()
        
        redLayer.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
        redLayer.backgroundColor = UIColor.red.cgColor
        self.view.layer.addSublayer(redLayer)
    }
}

Notice that the backgroundColor property of CALayer is a CGColor instead of UIColor.

You can also set an image as the content via the contents property. contents is also known as backing image.

We’ll use an image of a butterfly:

Note that you’ll have to drag your image named ButterflySmall.jpg in to your Xcode project hierarchy in order for the UIImage() command to find the picture.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func setup() {
    let redLayer = CALayer()
    
    redLayer.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
    redLayer.backgroundColor = UIColor.red.cgColor
    self.view.layer.addSublayer(redLayer)
    
    
    let imageLayer = CALayer()
    let image = UIImage(named: "ButterflySmall.jpg")!
    imageLayer.contents = image.cgImage
    
    imageLayer.frame = CGRect(x: 0, y: 100, width: image.size.width, height: image.size.height)
    imageLayer.contentsGravity = kCAGravityResizeAspect
    imageLayer.contentsScale = UIScreen.main.scale
    
    self.view.layer.addSublayer(imageLayer)
}

contentsGravity is a constant that specifies how the layer’s contents are positioned or scaled within its bounds.

contentsScale defines a ratio mapping between the size of the layer (measured in points) and the size of the bitmap used to present the layer’s content (known as backing image, measured in pixels). The default value is 1.0. Normally we set the value as the scale of the image, as shown above. However, when working with image that are generated programmatically, or image that missing @2x/@3x suffix, you will remember to manually set the layer’s contentsScaleto match the screen scale. Otherwise, you will get a pixelated image on your device.

Corners and Border

CALayer has a property called cornerRadius, which applies rounded corners to the layer’s background and border. When the masksToBounds property is set to true, the layer’s backing image and sublayers are clipped to this curve.

On our redLayer, let’s apply some rounded corners, and make the border visible.

1
2
3
4
5
6
7
8
9
10
11
12
13
func setup() {
    let redLayer = CALayer()
    
    redLayer.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
    redLayer.backgroundColor = UIColor.red.cgColor
    
    // Round corners
    redLayer.cornerRadius = 25
    
    // Set border
    redLayer.borderColor = UIColor.black.cgColor
    redLayer.borderWidth = 10
    ...

borderWidthborderColor defines the width and color of a layer’s border.

Drop Shadow

There are four properties that you could configure the drop shadow for a layer, shadowOpacityshadowColorshadowOffset and shadowRadius shadowRadius controls the blurriness of the shadow. A larger value could create a softer shadow that looks more natural.

Let’s add a shadow to our redLayer as well.

1
2
3
4
redLayer.shadowColor = UIColor.black.cgColor
redLayer.shadowOpacity = 0.8
redLayer.shadowOffset = CGSize(width: 2, height: 2)
redLayer.shadowRadius = 3

Unlike the layer border, the layer’s shadow derives from the exact shape of its contents, not just the bounds and cornerRadius. However, if you know what the shape of your shadow would be in advance, you could specify it via shadowPath (an instance of CGPath). You could improve performance by doing this.

Animating Layers

Now that we’ve covered a few of the types of properties that are present in Core Animation, let’s quickly walk through creating some actual animations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Create a blank animation using the keyPath "cornerRadius", the property we want to animate
let animation = CABasicAnimation(keyPath: "cornerRadius")
// Set the starting value
animation.fromValue = redLayer.cornerRadius
// Set the completion value
animation.toValue = 0
// Set the duration of animation
animation.duration = 5
// How may times should the animation repeat?
animation.repeatCount = 10
// Finally, add the animation to the layer
redLayer.addAnimation(animation, forKey: "cornerRadius")

Here we create a new CABasicAnimation for the cornerRadius property. Run your app and take a look, cool right?

You could just as easily make this animation apply to any other animatable property of CALayer. Try swapping the “cornerRadius” keyPath value in the CABasicAnimation() constructor with the value “borderWidth”. What happens? What about a value of “shadowRadius”?

Use keyPath “strokeEnd” to fill arc with animation.

1
2
3
4
// Set the starting value
animation.fromValue = 0

// Set the completion value
animation.toValue = 1

From this tutorial you can see how the basics of Core Animation work. In the next part we’ll move on to learn about more animatable properties, and how to work with masks to do really nifty effects.

Core Animation Swift Tutorial – Animatable Properties

A Beginner’s Guide to Bezier Paths and Shape Layers

There are always times that you need to create custom shaped views programmatically, and if you don’t know how, then problems start to arise.

These problems can be avoided however, by using the UIBezierPath class, which according to the documentation, is what you need to create vector-based paths. In simple words, with this class you can define custom paths that describe any shape, and use those paths to achieve any custom result you want. With it, it’s possible to create from simple shapes like rectangles, squares, ovals and circles to highly complex shapes by adding lines to a path, both straight and curved, between sets of points.

A bezier path that is created by the above class cannot stand on its own. It needs a Core Graphics context where it can be rendered to. There are three ways to get a context like that:

  1. To use a CGContext context.
  2. To subclass the UIView class which you want to draw the custom shape to, and use its draw(_:) method provided by default. The context needed is provided automatically then.
  3. To create special layers called CAShapeLayer objects.

From all three options, we’ll meet the last two in this tutorial, so we don’t miss our scope with the Core Graphics programming details.

The CAShapeLayer class just mentioned is also a topic of this post, and we’ll see a few things about it. This class inherits from the CALayer, and an object of it is used by the default layer that every view has. Most of the times a shape layer object is added as an additional layer on top of the default one, but it can be also used as a mask. We’ll discuss about both cases later. In addition, we’ll discuss about some of the most important properties of a shape layer object, and what their meaning is.

My goal in this tutorial is to give you practical guidelines on how to create bezier paths and how to use shape layer objects along with them. We’ll meet a series of small, but straight to the point examples, so when you’re through this tutorial you’ll be familiar with the basics of both concepts, and you’ll enable yourself to start thinking about more complex cases after that. If you already possess some knowledge about bezier paths and shape layers, then this tutorial might add something new to that knowledge. If you’re a newcomer to this stuff, then go ahead. You’ll learn some really interesting parts of the iOS development.

Preparing the Project

Let’s get started by creating a new project on Xcode. Unlike most of the other tutorials of mine, this time there’s not a starter project for you to download, but that’s not going to be a problem. In just a few steps we’ll have a project made and configured that way so we can start working on it. So, launch Xcode, and create a new single view application as shown next:

Next, give the project a name. I named it PathsNLayers, but choose another name if you want. Provide the team and organisation information, and make sure it runs on iPhone devices.

Finally, choose a place on your disk and save it. Once you’re ready, go to the File > New > File… menu, and select the Cocoa Touch Class source:

Make that new file a subclass of the UIView class, and give the name DemoView.

Proceed until the new file is created and added to your project. You should be able to see it in the Project Navigator pane.

Now open the ViewController.swift file, and add the following method:

Finally, open the DemoView.swift file, and add the next code inside the class body:

The ground that we’ll step on is now ready, so let’s go straight ahead to create our first bezier path.

Creating Paths and Shapes

They say that the best way to learn swimming is to just jump into the water, and I believe that the same principle applies when working with bezier paths too. So, with no much talking we’ll start seeing specific examples that will make clear how UIBezierPath objects can be created, configured and used. Actually, we’ll create a number of paths, where each one will result to a different kind of shape. Get ready, take a deep breath, and open the DemoView.swift file. We’ll make all the implementation that follows to this class.

Rectangular Shapes

We’ll begin with something simple, however quite instructive:

The above creates a rectangle shape that will surround our view, but it still has no effect as we haven’t provided a context for the path where it’ll be rendered to. We’ll do that later.

First, let’s have a detailed look at what all these lines do:

  • At first, we initialise a UIBezierPath object. There are various initialisers that this class provides and accept different number and kind of parameters, but for now that simple one is all we need. With the path object prepared, the first thing we always must do is to define a point that the shape will start to be drawn from. That point is expressed as a CGPoint value, and the move(to:) method tells to the path what that point is. In our sample code, the coordinates (0.0, 0.0) mean the top-left corner of our view.
  • The next thing we want, is to create a line between the starting point and another one that we must specify. We provide that point, and at the same time we instruct the system to draw a line by using the addLine(to:) method. Like the previous one, this method accepts a CGPoint value as an argument too, which obviously is the second point that we want to connect using the line to the first one. That line starts from the top-left corner and ends to the bottom-right corner of the view.
  • The following two commands are at the same logic like the previous one, and they connect points using lines. With the fourth line of code, we add a line to our path that connects the bottom-left to the bottom-right side corners of the view. The fifth line of code draws a new line from the bottom-right to the top-right.
  • So far, we’ve drawn three out of four sides of the rectangle. As it would be reasonably expected, we should add one more line to finish our shape. However, that’s not necessary to do; by calling the close() method (last line in the code snippet) we declare the end of the path, and the system will automatically draw the missing line for us.

Easy, right? All we did was to connect the dots and create lines from point to point. Now, the above method must be used, and the place to do that is the draw(_:) method of the DemoViewclass, which will provide a context for the path:

Before we test what we’ve done so far, let’s make it more interesting by setting a fill and a stroke colour. The fill colour covers the entire shape with the colour we choose, while the stroke paints the border of the shape. Adding a few more lines to the above:

All these are really straightforward, so we can run and see the results of our bezier path. Here’s what you should see by running the app now:

Congratulations, that was your first bezier path! However, it’s a little bit pointless to draw a shape just to outline the exact frame of the view, as you can achieve the same result by setting the background colour of the view and adding a border to the layer, which is easier. But it doesn’t matter; what we did here was the best kickstart for us! Now we know the basics, and we can move on to see more advanced cases.

Triangles

As you might guess, creating any shape that is made just by adding lines to the path is a matter of specifying the proper points. You can create from single shapes to polygons, as long as the given points to the path are the proper ones. In this spirit, let’s create this time a triangle shape:

The starting point here is the top-center point of the view. The above code is similar to the previous one in the createRectangle() method, we’ve only changed the points and number of lines created. The result is this:

Don’t forget to call the above method in draw(_:):

Try to play with the starting point and the added lines. Different parameters lead to different results.

Ovals and Circles

Creating oval shapes is easy, and all we really need is to use a different initialiser method for our path, other than the simple UIBezierPath(). Look at the following simple example, where we’re using a different initializer and we’re providing our view’s bounds as the frame for the shape:

The above results to the following:

By changing the frame, we can affect the resulting path and draw that way any size of oval shapes. Apparently, you don’t always have to use the view’s bounds as the parameter value; you can provide a frame that it’s just a part of the view.

An interesting case is how we turn an oval shape into a circle. Here it is:

Notice that the width and height of the new oval shape become equal (in this case, equal to the view’s height), and this is what makes the shape a circle. Also, note that in this example, the center of our circle matches to the center of the view.

Rectangles with Rounded Corners

One of the most basic things that all iOS developers know is how to apply rounded corners to a view using a command similar to the next one:

That’s the easiest way to round the corners, but the above unfortunately won’t have any effect if you create a bezier path on top of the view. There is, however, a solution to that. All you need is to create your path as a rounded rectangle, like it’s shown below:

The above initialiser creates a new rectangular path, and it rounds the corners according to the value you specify in the second parameter. If we test the app, here’s what we get:

What we just saw is useful, but not as interesting as when we want to round just some of the corners of the rectangle. It’s often required to create custom views with one or more, but not all, the corners rounded, and bezier paths are here to make that task easy. Let’s use an example to see how we can round only the top-left and bottom-right corners of the view:

That new initialiser we see here takes three parameters:

  1. The frame of the rectangular shape that will be created.
  2. The corners that will be rounded. For one corner only, there’s no need to use the array notation, but for more, it’s required as shown above. The values given in this parameter are properties of the UIRectCorner structure, and you can find more information here.
  3. The corner radius value. This parameter expects a CGSize value, but in truth, just the width is taken into account; the height is just disregarded.

By running our demo app now again, here’s what we see:

Not bad, as we only had to initialise an object and make part of the corners rounded!

Creating Arcs

For many developers, creating arc bezier paths always seems more complicated than all the other previous case we met so far. And yes, it is more complicated, unless you understand how it works! Trust me, it’s easy.

Let’s see an example first, so we can go through everything step by step. Initially, watch the following screenshot:

The code that creates that arc shape is this:

Let’s see the parameters list one by one:

  1. arcCenter: An arc is always a part of a circle, so if you visualize the missing part of the circle and draw it entirely in your mind, you can see its center. That center of the imaginary circle is also the center of the arc. In the above example we want our imaginary circle to be included in the frame of our view, so the center of it should also be the center of the view. That parameter value is always a CGPoint value.
  2. radius: The radius of the circle. As the diameter of our imaginary circle is equal to the view’s height, then the half of it is the radius.
  3. startAngle: It describes the “starting point” for the arc you’re drawing. We’ll see a few more details both for this and for the endAngle parameter right next, but for now just think of it as the point on the perimeter of the imaginary circle where the arc starts from. The expected value is the angle expressed in radians, and not in degrees. I’ll explain more in a while, and especially about what you’re seeing in the above code.
  4. endAngle: The “ending point” of the arc that it’s being drawn. Similarly to what I just said about the startAngle, think of it as the point on the perimeter of the imaginary circle where the arc ends.
  5. clockwise: It’s a boolean value that indicates whether the arc should be drawn in a clockwise direction, or the opposite. The proper combination of this and the previous two parameters leads to the proper arc you want to be drawn.

Let’s talk now a little bit more about the startAngle and endAngle values. To properly understand how they express the starting and ending points respectively of an arc, remember the maths at your school age, and the circle of degrees from the books.

As you recall from your youth, and you also see at the above image, a circle starts at 0 degrees, and follows a counter-clockwise direction until it makes a 360 degrees round and form a complete shape. Based on what we see above, it’s not difficult to draw an arc; you start at any degree A, go to degree B, and finish. You agree, right? Me too, but here’s the secret that will make arcs easy for you: Forget about all that!

In iOS many things are upside down comparing to what we know from the usual math. Take for example the coordinate system: When moving towards bottom in iOS (vertical axis) the Y value is increased, while it gets decreased in a Cartesian coordinate system (math). The same happens with the degrees circle you see above. You have to consider that for iOS a circle starts at 0 degrees (right side like above), but it’s moving in a clockwise direction until it draws a 360 degrees circle:

Note: The above image is courtesy of tillnagel.com

Watching that image, and considering that for our example we started the arc at 180 degrees, finished at 0 degrees, and followed a clockwise direction, it becomes clear how the arc shown in the last screenshot was created.

To make it even more clear, let’s try to create the following arc:

There are two ways to achieve that:

  • Either we start from 90 degrees and go to 270 degrees in a clockwise direction according to the degrees circle shown above,
  • Or we start from 270 degrees going to 90, and we follow a counter-clockwise direction.

Let’s take the second case, and let’s move in a CCW direction:

One last example, to make sure that we fully get it:

That results to:

And a few words now about the start and end angle values. We all know that an angle is expressed either in degrees, or in radians. The two respective parameters in the above initializer expect radian values, but for us as humans it’s easier to think in degrees. For that reason, I provide you with the following CGFloat extension that performs the conversion from degrees to radians, and that was used it in the last three examples:

With this extension, you can easily have the respective radians value from any CGFloat number that expresses an angle in degrees, similarly to what you’ve seen in the start and end angle parameter values in the previous examples.

Bezier Paths and CAShapeLayer

As we’ve seen in the previous examples, the draw(_:) method in an UIView subclass provides a context for the bezier path that will be drawn. However, it’s not always necessary to override that method to perform custom drawing, and actually, you should avoid it if possible, as it has impact to the performance. A good alternative is using CAShapeLayer objects, which are rendered much faster and you have additional flexibility by using them.

As said in the beginning of this post, a CAShapeLayer is a CALayer subclass, and by creating and using such objects we actually add extra layers to a view. It has several properties that can be set for customising the final outcome of a view, and most of them are animatable, meaning that their values can be changed using animated manners (for example, CABasicAnimation). Animations consist of a big chapter in layers, and we won’t see them in this post; maybe it’ll consist of another tutorial at some point.

When creating a CAShapeLayer object, it’s necessary to specify its shape, or in other words its path. The most easy way to set that path is by creating a bezier path first, and then assigning it to the shape layer object. As this is the most usual approach, we’ll use it in the following examples.

Except for specifying the path, there are other properties that can be set, and usually should be set, too. For example, the fill colour, stroke colour, line width, and position are just some of them, and we’ll see them all in action. Also, there are two ways that a shape layer object can be used in the layer of a view: As a sublayer, or as a mask. We’ll see their difference in a while. Lastly, multiple shape layers can be created and added as sublayers to a layer. However, as the top most layers cover those under them, you should carefully size and position them.

A Simple CAShapeLayer Object

Let’s create our first CAShapeLayer object, but before doing that, make sure to comment out the entire draw(_:) method that we were using previously in the DemoView.swift file. The simplest thing we could do would be this:

First off, we call the createRectangle() method implemented in the previous part so we draw a rectangular bezier path. Remember that the path specified in this method is kept to the path property in our class.

Next, we create the shape layer object, which always is as easy as shown above. First we initialise the object, and then we assign the bezier path to its path property. Note though that this property expects a CGPath value, while our path is a UIBezierPath object, so we use the Core Graphics representation of it by just accessing the cgPath property.

Lastly, we add it as a sublayer to the view’s layer. For your information, even though this is the most usual way to add a sublayer, in case you have many of them you could use any of the next methods that the CALayer class provides:

Read more about them herehere and here respectively.

Now, let’s call this method and let’s run the app to see the results (note that we change the background colour of the view from “clear” to dark gray):

You see, if we don’t specify a fill colour for the shape layer, black will be used as the default one. Let’s set one now, and also let’s play with a couple more properties: The stroke colour and the line width:

Notice that both the fillColor and strokeColor properties are expecting for a CGColor value, that’s why we’re “converting” our UIColor objects to CGColor by accessing the cgColor properties of them. By using the lineWidth property we can specify the thickness of the border line.

Mask or Sublayer?

A shape layer can be used in two different ways: As a mask to the default layer of the view, or to be added as a sublayer to it (like what we did above). But what’s the real difference between these two ways, and how should we decide which one we need to use?

Going by the example once again, let’s find that out with the following new method:

Once again, we’re using a method that we created previously (createTriangle()) so we can just stay into the point. That method draws a path that creates a triangular path, and we’re initializing a new shape layer object to use it. We specify its fill colour, and then we add it as a sublayer to the default layer of the view. Nothing difficult, and nothing new here.

Let’s call this new method, and then let’s see the (pretty much expected) results:

Obviously, our new shape layer is a nice yellow triangle that sits on top of the view, and the part of the view that is not covered by the fill colour remains dark gray.

Let’s perform the following changes now to our method:

Comment out the addSublayer(_:) method, and use the mask property instead. Then run, and watch:

This time there’s just the triangular shape, which is not yellow anymore even though we’ve set the fill colour, but even further the part of the view that is not covered by the path of the shape layer is missing!

Well, this is how the mask works. Any part of the view that is not a part of the path is clipped, and the view actually takes the shape of the shape layer that is applied as a mask. Furthermore, properties like the fill colour have no effect when the new layer is a mask, so if we want to change the colour of the triangle we need to update the background colour of the view:

In short, if you want to give your view a custom shape, then use the mask property, otherwise add the new shape layer you create as a sublayer and play with the view’s and layer’s
properties after that to achieve your final purpose.

Not Just One SubLayer

I’ve already said that you can definitely add more than one shape layers as sublayers to a view’s layer, so let’s see a few more things about that. In this part, we’ll create two sublayers and we’ll change a few properties of them, so we display them properly on the view. For starters, let’s create two paths and two shapes:

The above code creates two paths that represent triangular shapes, where the first has the “nose” up, and the second has its “nose” down. This time, we don’t use the entire width and height of the view for paths and shapes, just the half of them. Once both paths are specified, are assigned to the two shape layer objects, which in turn are added as sublayers to the layer of the view.

Calling the above new method:

… and then running it, it gives us this:

As you see, the second shape layer overlaps the first one, and that doesn’t seem right. So, why don’t we just move it?

That’s better! The position property that we just used is really important, as it allows us to “move” shape layers around. And what if we want to center both layers? Just replace the above line with the next two:

These new lines lead to this:

Most of the times, the position property will be good enough to place a shape layer to a different position. However, there will be also times where you’ll be wanting to change the layer’s frame. In that case, and keep that in mind, you shouldn’t access the frame property, but the bounds property of the sublayer.

According to the documentation, the bounds property expresses the origin and size of the sublayer in its own coordinate system. To understand what that means and see how you change the origin or size of the bounds rectangle, let’s make a few changes to our example:

With the above addition we changed the default size of the sublayer, and it’s obviously exceeding the view’s visible space. The drawn path’s size is not affected by that, however if we change the origin the path will move:

Notice something important: Unlike to the normal view’s coordinates where we move a view to the right by increasing the X value of the origin point, and to the bottom by increasing the Y value, here it’s the opposite that happens. We move the path to the right and bottom by providing negative values. Positive values would move the origin of the path to the left and up respectively. However, most of the times you’ll be needing just to reposition your layer, and probably rarely you’ll have to deal with the bounds.

Combining Paths

When we started discussing about paths in this post, we went through a series of initializers that the UIBezierPath class provides, and we we found out that several types of shapes can be created just by using those initializers. However, we never saw how the various path types are combined together, and now that we’ve talked about shape layers it’s the best time to do so.
Similarly to the addLine(to:) method that we’ve known already and that creates a line between two points, there are other methods that create other kind of shapes, such as arcs and curves.

Let’s take a look at the following example:

Call the above in the init(frame:) method:

Then run it and watch at the shape that it produces:

First of all, notice in the code that we use the shape layer as a mask and not as a sublayer. I’m doing that on purpose, just because I want to highlight the final shape. However, the important part is the path creation. As you see, we have a combination of several lines, of an arc and of a curve. We start drawing from the top-left corner (point 0.0, 0.0), and continue in a clockwise direction. Watch the given points carefully to see how the path is drawn step by step.

The new thing above is the addCurve method, which creates the curve we see on the right side of the shape. A curved line is something like that:

Note: The image above is originally published on https://developer.apple.com/reference/uikit/uibezierpath/1624357-addcurve.

The above method that adds a line like that expects three arguments: The final point (endpoint) of the curve, and the two control points that actually define the curvature of the line. Try to change the values of these control points to see how the curve behaves when you modify it. The (painful) truth, though, is that a lot of mathematics lie behind curves, and you can have your first taste here.

Regarding the arc that we create early in the path, there’s nothing new or difficult to mention about. Just remember what we’ve said about drawing arcs, and you’ll see that we followed the recipe “by the book”. All the rest, are just known stuff.

Bonus Material: Using CATextLayer

One of those classes-tools that is less-known, or used at least, is the CATextLayer class. It’s purpose is to allow us to create a layer (like CAShapeLayer) that displays some text. And even though this class has nothing to do with bezier paths or shapes, I really wanted to present it in this post, so I’m providing it here as an additional stuff to read. Think of this part as a small parenthesis in the post.

Every developer displays text in the interface using UILabel objects at 99% of the times (talking about text that cannot be modified). But sometimes using a label might not work, or it becomes messy if there are multiple sublayers added to the layer of the view that contains the label. In that case, the CATextLayer class is what we really need. The additional advantage of this is that we can actually add text layers on top of any view, so we’re not depending just on labels.

Creating and using a CATextlayer is just a matter of configuring a bunch of properties according to the way you want text to be shown. The next code snippet creates such a text layer, which is added as a sublayer to our view’s default layer. See the code, see the results, and then play as much as you want so you get acquainted with it.

A few things to notice:

  • We set the text that we want to show using the string property of the textLayerobject.
  • Even though we specify a font by providing a name and size, it’s also necessary to use the fontSize property to specify the font size. It seems that the size given in the font initialisation is not taken into account.
  • Text alignment can be set using the textAlignmentMode property. There are specific values you can set, and by using the kCAAlignmentCenter we align text on the center. More values [here](https://developer.apple.com/reference/quartzcore/catextlayer/horizontal_alignment_modes).
  • There’s no option to set the number of lines here, like it happens in a UILabel object. Instead, we can use the “\n” character to break the text in multiple lines according to our preferences. Also, keep in mind to properly set the layer’s frame so text fits properly in it.
  • Quite important: Always use the contentScale property, as this will make the text be drawn properly on the screen taking its scale value into account. If not used, the result will be pixelated.

Call that method now:

And here’s what we get back:

For more information, take a look at the official documentation.

A Beginner’s Guide to Bezier Paths and Shape Layers

Development Tutorial for iPhone X

What’s Different?

First, a quick rundown on what’s different about the iPhone X screen:

  • Screen size is 375 x 812 points, so the aspect ratio is 9:19.5 instead of 9:16. That’s 145 pts more than the iPhone 6/7/8’s 4.7″ screen but the status bar uses 44 pts, and the home indicator almost doubles the height of the toolbar.
  • Screen resolution is 3x: 1125 x 2436 pixels.
  • Screen design must take into account the sensor housing, home indicator and rounded corners.

  • In portrait, the status and navigation bars occupy 88 pts, or 140 pts for large titles. The toolbar is 83 pts instead of 44 pts. In landscape, the toolbar is 53 pts, and layout margins are 64 pts instead of 20 pts.
  • In portrait, the status bar is taller — 44 pts, not 20 pts — and uses space not used by the app, either side of the sensor housing. And it doesn’t change size to indicate phone, location-tracking, and other background tasks.

Geoff Hackworth’s Medium article has super-helpful diagrams of the new screen anatomy, courtesy of his Adaptivity app.

Getting Started

Download the starter package, and unzip it.

First, see for yourself what happens when you load a 9:16 image into an iPhone X screen. Open AspectRatioSample, then open Main.storyboard. Set View as to iPhone X, and select the image view. In the Attributes Inspector, switch Content Mode between Aspect Fit and Aspect Fill:

The 8Plus image were created by stacking two image views, building and running in the iPhone 8 Plus simulator, then taking a screenshot. So the image’s aspect ratio is 9:16.

The image view’s constraints are set to fill the safe area, so its aspect ratio is 9:19.5.

In Aspect Fit, the black view background shows above and below the image (letter-boxing). Aspect Fill covers the view, but crops the sides of the image.

In landscape orientation, Aspect Fit would pillar-box the image (show the background view on the sides), and Aspect Fill would crop the top and bottom.

Assuming you don’t want to create different images just for iPhone X, and you want to cover the view, then you’re going to get cropping.

Rule: Compose your images so you don’t lose important visual information when Aspect Fill crops them.

Designing a New App

Close AspectRatioSample, and open NewCandySearch. Build and run on the iPhone X simulator:

This is a master-detail app with a list of candies. The detail view shows an image of the selected candy.

I’ll wait while you get your favorite snack! ;]

Scroll the table view, to see that it makes no attempt to avoid the home indicator. This is perfectly OK for vertically scrollable views and background images.

Rules:

  • Avoid the sensor housing and home indicator, except for background image and vertically scrollable views.
  • Avoid placing controls where the home indicator overlaps, or corners crop.
  • Don’t hide or draw attention to sensor housing, corners or home indicator.

Use Auto Layout

Open Main.storyboard, select either view controller, and show the File Inspector:

New projects created in Xcode 9 default to using Auto Layout and Safe Area Layout Guides. This is the simplest way to reduce the work needed for iPhone X design.

Rules:

  • Use safe area layout guides.
  • Use margin layout guides.
  • Center content or inset it symmetrically.

Use Standard UI Elements

Now you’re going to add a search bar with scope bar. And you might as well opt for the new large title, too.

Select Master Scene/Navigation Bar, show the Attributes Inspector, and check the box for Prefers Large Titles:

While you’re here, select the table view’s cell, and set its background color to light gray:

Next, open MasterViewController.swift: it already has most of the search controller code. You just have to replace the TODO comment in setupSearchController() with this:

// In iOS 11, integrate search controller into navigation bar
if #available(iOS 11.0, *) {
  self.navigationItem.searchController = searchController
  // Search bar is always visible
  self.navigationItem.hidesSearchBarWhenScrolling = false
} else {
  tableView.tableHeaderView = searchController.searchBar
}

If the device is running iOS 11, you set the navigation item’s searchController property; otherwise, you put the search bar in the table view’s table header view.

Build and run on the iPhone X simulator. Admire the large title, then rotate the simulator to landscape, and tap the search field to show the scope bar:

The search field, cancel button and scope bar are all nicely inset from the rounded corners and sensor housing. The cell background color extends all the way across, and the table view scrolls under the home indicator. You get all these behaviors free, just for using standard UI elements.

Recommendation: Use standard UI elements.
Note: If you rotate back to portrait while the scope bar is showing, it collapses messily onto the search field. Fortunately, you can still tap the cancel button to get rid of the scope bar. This seems to be an iOS 11 bug: the same thing happens when the search bar is in the table header view, but doesn’t happen in the same app running in iOS 10.

Build and run the app on the iPhone 8 simulator, to see that it looks fine there, too:

Note: The iPhone X is compact width in landscape orientation, so it behaves like the iPhone 8, not the 8 Plus.

Here are some other recommendations:

Status Bar

  • The iPhone X status bar is higher, so dynamically position content based on the device type, instead of assuming a fixed 20-pt height.
  • Always show the status bar unless hiding it adds real value to your app.

3x Screen Resolution

  • Use PDF for flat vector artwork; provide @3x and @2x of rasterized artwork.
  • An app doesn’t use 3x if it doesn’t have a LaunchScreen.storyboard.

Home Indicator Special Cases

  • If your app uses the swipe-up-from-bottom gesture, turn on edge protect with preferredScreenEdgesDeferringSystemGestures(): the user must swipe up twice to access the home indicator.
  • If your app includes passive viewing experiences, turn on auto-hiding with prefersHomeIndicatorAutoHidden(): the home indicator fades out if the user hasn’t touched the screen for a few seconds, and reappears when the user touches the screen.

iPhone X Simulator vs Device

  • Use the simulator to check portrait and landscape layout.
  • Use an iPhone X device to test wide color imagery, Metal, front-facing camera.

Other Stuff

  • Don’t reference Touch ID on iPhone X. Don’t reference Face ID on devices that support Touch ID.
  • Don’t duplicate system-provided keyboard features like Emoji/Globe and Dictation buttons.

Updating an Existing App

What if you want to update an existing app for iPhone X? First, update its assets, including background images, to PDF or add @3x images. Then make sure it has a LaunchScreen.storyboard, and turn on Safe Area. Safe area layout guides change top, bottom and edge constraints, so check these, and fix them if necessary.

Check for any layout that depends on a 20-pt status bar or 44-pt tool bar, and modify it to allow for different heights. Or, if your app hides the status bar, consider unhiding it for iPhone X.

Next, set View as to iPhone X, and check every layout configuration. Move controls away from the edges, corners, sensor housing and home indicator.

Consider integrating search view controllers into the navigation bar.

Build and run the app on the iPhone X simulator, and check every layout configuration. In landscape, check that table view cell and section header content is inset, but the background extends to the edges.

Here’s a simple example to work through. Download the (original) finished sample app from UISearchController Tutorial. This app was built with Xcode 9 beta, before Apple announced the iPhone X, so Tom Elliott couldn’t test it on the iPhone X simulator.

Build and run it on the iPhone X simulator. In portrait, it looks like NewCandySearch, plus the navigation bar has a candy-green background color and a fancy title image instead of a large title:

But there’s a line between the navigation bar and the search bar, because the search bar is in the table header view. It gets worse: tap in the search field to show the scope bar:

The search bar replaces the navigation bar, removing the background color from the status bar. The search bar is still candy-green, so it just doesn’t look right.

To see more problems, cancel the scope bar, then rotate to landscape:

The title image is slightly clipped, and the sensor housing cuts a bit off the lower left corner of the search bar. But the table view isn’t customized, so its cell contents are well clear of the sensor housing.

Again, tap in the search field to show the scope bar:

Now the rounded corners clip the search bar.

Note: You probably noticed that showing the scope bar hides the first table view cell Chocolate Bar. Although this didn’t happen in NewCandySearch, it’s not a reason to move the search bar into the navigation bar. This is an iOS 11 bug that Tom Elliott reported to Apple on August 2, 2017. At the time of writing this tutorial, its status was still Open.

Turning on Safe Area

Open Main.storyboard, select one of the view controllers, and show the File Inspector:

This app doesn’t use safe area layout guides! So check that checkbox, then check the constraints now refer to Safe Area:

Build and run, and see how it looks in landscape, with the scope bar:

It’s even worse! The table header view extends far beyond its superview, although the table footer view is fine. This is possibly another iOS 11 bug, which might be fixed by the time you read this tutorial.

Even if Safe Area prevented the corner clipping, it’s not a good look for the status bar to lose its background color when the search bar replaces the navigation bar. You can fix that by moving the search bar into the navigation bar, so that’s what you’ll do next.

Integrating the Search Bar

Well, this is easy — just copy the NewCandySearch code that sets the navigation bar’s searchController into CandySearch. Open MasterViewController.swift in both projects, and copy these lines from NewCandySearch:

// replace tableHeaderView assignment with this
if #available(iOS 11.0, *) {
  self.navigationItem.searchController = searchController
  // Search bar is always visible
  self.navigationItem.hidesSearchBarWhenScrolling = false
} else {
  tableView.tableHeaderView = searchController.searchBar
}

In MasterViewController.swift of CandySearch, paste these lines in viewDidLoad(), and comment out this line:

tableView.tableHeaderView = searchController.searchBar

Now open AppDelegate.swift, and find these lines:

UISearchBar.appearance().barTintColor = .candyGreen
UISearchBar.appearance().tintColor = .white
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = .candyGreen

Delete or comment out the first line: the search bar will get its tint color from the navigation bar.

Build and run, and tap in the search field to show the scope bar:

So that’s fixed the status bar background color, but now the text field is also green, making it hard to see the search field prompt text and icon. The insertion bar is also green, but you can fix that — change the third appearance setting in AppDelegate.swift to:

UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = .white

Below this, add the following line:

UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).backgroundColor = .white

This should make the text field background white but, at the time of writing this tutorial, it doesn’t work, which suggests another iOS 11 bug. As an interim fix, you could change the color of the search field prompt text and icons. For example, to make the prompt text white, add the following code to AppDelegate.swift in application(_:didFinishLaunchingWithOptions:):

let placeholderAttributes: [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.foregroundColor.rawValue): UIColor.white, NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue): UIFont.systemFont(ofSize: UIFont.systemFontSize)]
let attributedPlaceholder: NSAttributedString = NSAttributedString(string: "Search", attributes: placeholderAttributes)
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).attributedPlaceholder = attributedPlaceholder

To make the search icon and clear button white, add the following code to viewDidLoad() in MasterViewController.swift:

let textField = searchController.searchBar.value(forKey: "searchField") as! UITextField
let glassIconView = textField.leftView as! UIImageView
glassIconView.image = glassIconView.image?.withRenderingMode(.alwaysTemplate)
glassIconView.tintColor = .white
let clearButton = textField.value(forKey: "clearButton") as! UIButton
clearButton.setImage(clearButton.imageView?.image?.withRenderingMode(.alwaysTemplate), for: .normal)
clearButton.tintColor = .white

Again, this doesn’t work at the time of writing this tutorial, but it might be fixed by the time you read this.

Development Tutorial for iPhone X

Upgrading To Swift 4? Replacing fileprivate with private

You may not have noticed but there was a small but important change to the private access level in Swift 4. After a quick recap of the five levels I cover what has changed in Swift 4, what do you need to know when migrating from Swift 3 and what is the point of fileprivate now?

The Five Access Levels of Swift 4

To recap Swift 4 has the same five access levels as Swift 3. In order from most open to most restricted:

  • open you can access open classes and class members from any source file in the defining module or any module that imports that module. You can subclass an open class or override an open class member both within their defining module and any module that imports that module.
  • public allows the same access as open – any source file in any module – but has more restrictive subclassing and overriding. You can only subclass a public class within the same module. A public class member can only be overriden by subclasses in the same module. This is important if you are writing a framework. If you want a user of that framework to be able to subclass a class or override a method you must make it open.
  • internal allows use from any source file in the defining module but not from outside that module. This is the default access level.
  • fileprivate allows use only within the defining source file.
  • private allows use only from the enclosing declaration and new in Swift 4, to any extensions of that declaration in the same source file.

Notes:

  • Remember that your application target is its own module and that internal access is the default. Your classes, structs, enums, properties and methods are all accessibile within the application module by default unless you choose to restrict access.
  • As with Swift 3 you can add final to any access level, except open, to prevent subclassing or overriding of a class method or property.

How Did private Access Change?

In Swift 3 if you wanted an extension to have access to a private property or method you had to use fileprivate. Consider this Swift 3 code for a view controller with a private property:

class RootViewController: UIViewController {
  private var started = false
}

If I have an extension of the class in the same file, using Swift 3, it cannot access the private property:

extension RootViewController {
  func doSomething {
    if started { ... } // Error - no private access (Swift 3)
  }
}

Since extensions are a common Swift technique this led to the widespread use of fileprivate when splitting a type between a base declaration and one or more extensions:

// RootViewController.swift
class RootViewController: UIViewController {
  fileprivate var started = false
}

extension RootViewController {
  func doSomething {
    if started { ... } // fileprivate access allowed
  }
}

I think it is fair to say this is now considered unnecessarily complicated. Swift 4 limits the need for fileprivate by widening the scope of private access to include extensions. So this now works fine in Swift 4:

// RootViewController.swift
class RootViewController: UIViewController {
  private var started = false
}

extension RootViewController {
  func doSomething {
    if started { }     // private access allowed (Swift 4)
  }
}

Note that the extension still has to be in the same source file for private access.

// RootViewController+Extension.swift
extension RootViewController {
  func doExtra {
    if started { }     // no private access from here
  }
}

So When Do I Use fileprivate?

So what is the point of fileprivate now? Well you should need it a whole lot less with Swift 4. One example, suppose I have a Widget class with private and fileprivate methods:

// Widget.swift
class Widget {   
  private func privateMethod() { ... }
  fileprivate func fileprivateMethod() { ... }
}

Neither method is visible outside of the source file but what about a subclass in the same source file? It is not an extension of the Widget class so it cannot access the private method but it can access the fileprivate method:

// Widget.swift
class SpecialWidget: Widget {
  func start() {
//  privateMethod()      // No access to private method
    fileprivateMethod()  // fileprivate access allowed
  }
}

I’m Upgrading From Swift 3 – What Should I Do?

The short answer is probably not much. You can replace fileprivate with private if you were using it for extension access – but you are not forced to. Your Swift 3 code will still work with one possible exception.

If you are unlucky enough to have used the same name for private methods in a base declaration and one or more extensions in the same source file you will get an error with Swift 4:

<code>struct Person {
  private func helperMethod() { ... }
}

extension Person {
  private func helperMethod() { ... }
  // Invalid redeclaration
}
</code>

The increased scope of private in Swift 4 means that you will have to rename the duplicate method names.

Upgrading To Swift 4? Replacing fileprivate with private

Google Material Design Tutorial for iOS

Using Google Material Design Components for iOS, you will beautify the app with a flexible header, standard material colors, typography, sliding tabs, and cards with ink.

Getting Started

Download the starter project for News Ink, and take a look around to familiarize yourself.

You may notice that the project is using CocoaPods. In your Terminal, navigate to the project’s root folder and run pod install.

Before you start working with the app, you’ll need to obtain a free newsapi.org key by signing up at https://newsapi.org/register.

Once you’ve got your key, open NewsClient.swift and insert your key in the Constants struct like so:

static let apiKey = "REPLACE_WITH_NEWSAPIORG_KEY"

Then build and run.

There’s nothing terribly interesting yet: just a basic list of articles with photo and basic information. You can tap on an item in the list to go to a web view of the full article, but that’s about it.

Before diving into some code, it’s worth learning a little about Material Design.

Material Design

Google introduced Material Design in 2014, and it’s quickly become the UI/UX standard across all of Google’s web and mobile products. The Google Material Design Guidelines is a great place to start, and I’d recommend having a quick read through before you go any further.

But why is Material Design a good idea, and more importantly, why would you want to use it for an iOS app? After all, Apple has its own UI/UX guidelines in the form of the Human Interface Guidelines.

The answer lies in how we use the devices around us. From mobile phones, to tablets, to desktop PCs, to the television; our daily lives are now a journey from one screen to the next. A single interface design that feels the same across all screens and devices makes for a smooth user experience and greatly reduces the cognitive load of jumping from one device to the next.

An example of dos and don’ts from Google’s Material Design Guidelines.

Using a metaphor that humans are already familiar with — material, in this case, paper — makes approaching each new screen somewhat easier. Moreover, when the design guidelines are extremely opinionated, specific, and supported by actual UI components at the platform level, apps built using those design guidelines easily fall in line with each other.

There’s nothing in the Material specification about only applying to Google’s platforms. All of the benefits of a unified design system are as relevant on iOS as they are on any other platform. If you compare Apple’s Human Interface Guidelines to Google’s Material Design Guidelines, you’ll notice that the Material spec is much deeper and more opinionated. In contrast, Apple’s guidelines are not nearly as prescriptive, particularly when it comes to visual aspects such as typography, color and layouts.

Google is so committed to making Material Design a cross platform standard that it’s created a Platform Adaptation guide that walks you through implementing Material in a way that feels at home on any platform.

That was a lot of info up front! Rest assured, none of it was… immaterial. Now you’re going to have some fun working with the Google Material Components for iOS.

Material Design in Practice on iOS

When you’re done with this section, your app will open with a large header, including a full-bleed photo background and large word mark text. As you scroll, the photo will move and fade out, while the word mark label shrinks until the entire header magically morphs into a more traditional navigation bar.

To start, there’s no navigation bar, title, or anything else to tell the user which app they’re using. You’ll fix that that by introducing an app bar with flexible header, hero image, and fluid scroll effects.

Adding an App Bar

The first, and probably coolest Material Design component you’ll add is an App Bar. In this case, you’ll get a lot of bang for your buck, since the App Bar combines three components in one: Flexible Header, Header Stack View, and Navigation Bar. Each of these components is powerful on its own, but as you will see, when combined, you get something really special.

Open HeroHeaderView.swift. To keep things clean, you’re going to build a UIView subclass that contains all the subviews that make up the flexible header, as well as the logic for how those subviews change in relation to the scroll position.

First add the following struct inside the HeroHeaderView class:

struct Constants {
  static let statusBarHeight: CGFloat = UIApplication.shared.statusBarFrame.height
  static let minHeight: CGFloat = 44 + statusBarHeight
  static let maxHeight: CGFloat = 400.0
}

Here you add a number of constants that will be useful as you build out the header view.

statusBarHeight represents the height of the status bar and minHeight and maxHeight represent the minimum (fully collapsed) and maximum (fully expanded) height of the header.

Now add the following properties to HomeHeaderView:

// MARK: Properties

let imageView: UIImageView = {
  let imageView = UIImageView(image: #imageLiteral(resourceName: "img-hero"))
  imageView.contentMode = .scaleAspectFill
  imageView.clipsToBounds = true
  return imageView
}()

let titleLabel: UILabel = {
  let label = UILabel()
  label.text = NSLocalizedString("News Ink", comment: "")
  label.textAlignment = .center
  label.textColor = .white
  label.shadowOffset = CGSize(width: 1, height: 1)
  label.shadowColor = .darkGray
  return label
}()

Nothing too complicated here; you add a UIImageView to house the header’s background and a UILabel that represents the app title word mark.

Next, add the following code to initialize HomeHeaderView, add the subviews, and specify the layout:

// MARK: Init

// 1
init() {
  super.init(frame: .zero)
  autoresizingMask = [.flexibleWidth, .flexibleHeight]
  clipsToBounds = true
  configureView()
}

// 2
required init?(coder aDecoder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

// MARK: View

// 3
func configureView() {
  backgroundColor = .darkGray
  addSubview(imageView)
  addSubview(titleLabel)
}

// 4
override func layoutSubviews() {
  super.layoutSubviews()
  imageView.frame = bounds
  titleLabel.frame = CGRect(
    x: 0,
    y: Constants.statusBarHeight,
    width: frame.width,
    height: frame.height - Constants.statusBarHeight)
}

There’s a bit more going on here:

  1. Here you add some basic initialization code that sets a resizing mask, configures clipping mode, then calls the configureView method to, well, configure the view. The MDCAppBar and its cohorts don’t support Auto Layout, so for this section of the tutorial, it’s frame math or bust.
  2. This view is only intended for use via code, so here you prevent it from being loaded via XIB or Storyboards.
  3. To configure the view, you set the background color to .darkGray. As the view collapses, the background image will become transparent, leaving this dark gray color to serve as the navigation bar color. You also added the background image and label as subviews.
  4. The layout code here does two things. First, it assures that the background image fills the frame of the header view. Second, it also fills the label to the header frame, but accounts for the status bar height so that the label is vertically centered between the lower edge of the status bar and the bottom edge of the header frame.

Now that you have the basic header view with subviews in place, it’s time to configure the App Bar and use your header view as the content.

Open ArticlesViewController.swift and import the Material Components by adding the following import statement at the top of the file, below the existing imports:

import MaterialComponents

Now add the following property declarations above the existing properties:

let appBar = MDCAppBar()
let heroHeaderView = HeroHeaderView()

You have a property for the App Bar (an instance of MDCAppBar) and one for the HeroHeaderView you created in previous steps.

Next, add the following method to the ArticlesViewController extension marked as // MARK: UI Configuration:

func configureAppBar() {
  // 1
  self.addChildViewController(appBar.headerViewController)

   // 2
  appBar.navigationBar.backgroundColor = .clear
  appBar.navigationBar.title = nil

   // 3
  let headerView = appBar.headerViewController.headerView
  headerView.backgroundColor = .clear
  headerView.maximumHeight = HeroHeaderView.Constants.maxHeight
  headerView.minimumHeight = HeroHeaderView.Constants.minHeight

   // 4
  heroHeaderView.frame = headerView.bounds
  headerView.insertSubview(heroHeaderView, at: 0)

   // 5
  headerView.trackingScrollView = self.collectionView

   // 6
  appBar.addSubviewsToParent()
}

There’s quite a lot going on here, so let’s break it down:

  1. To start, you add the app bar’s header view controller as a child view controller of the ArticlesViewController. This is required so that the header view controller can receive standard UIViewController events.
  2. Next, you configure the background color of the app bar to be clear, since you’ll be relying on the hero header view subclass to provide the color. You also set the titleView property to nil because the hero header view also provides a custom title.
  3. Now you configure the app bar’s flexible header view, first by setting it’s background to .clear, again because your hero header view subclass will handle the background. Then you set the min and max heights to the values you defined in the HeroHeaderView.Constants struct. When the collection view is at scroll position zero (e.g. the top), the app bar will be at max height. As you scroll the content, the app bar will collapse until it reaches min height, where it will stay until the collection view is scrolled back towards the top.
  4. Here you set up the initial frame of the hero header view to match the app bar’s header view, then insert it as the bottom-most subview of the header view. This effectively sets the hero header view as the primary content of the app bar’s flexible header view.
  5. Next, you set the header view’s trackingScrollView to the collection view. The flexible header needs to know which UIScrollView subclass to use for tracking scroll events so that it can adjust its size, position, and adjust its subviews as the user scrolls.
  6. Finally, you call addSubviewsToParent on the app bar as required by MDCAppBar in order to add a few of its views to your view controller’s view.

Now invoke configureAppBar() by adding it to viewDidLoad(), right after calling super.viewDidLoad():

override func viewDidLoad() {
  super.viewDidLoad()
  configureAppBar()
  configureCollectionView()
  refreshContent()
}

Build and run, and you should see the following:

Sweet, the header is there! But there are a few problems.

Flexible header height

First, the title logo’s font is small, and as a result, looks awful. Try scrolling the collection view, and you’ll also notice that the flexible header doesn’t seem so flexible yet.

Both of these problems are tied to the fact that there is still some configuration needed to fully wire up the app bar to the collection view’s scroll events.

It turns out that simply setting the flexible header’s trackingScrollView is not enough. You also have to explicitly inform it of scroll events by passing them via the UIScrollViewDelegate methods.

Add the following to the same UI Configuration extension on ArticlesViewController, below where you added configureAppBar():

// MARK: UIScrollViewDelegate

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
  let headerView = appBar.headerViewController.headerView
  if scrollView == headerView.trackingScrollView {
    headerView.trackingScrollDidScroll()
  }
}

override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  let headerView = appBar.headerViewController.headerView
  if scrollView == headerView.trackingScrollView {
    headerView.trackingScrollDidEndDecelerating()
  }
}

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  let headerView = appBar.headerViewController.headerView
  if scrollView == headerView.trackingScrollView {
    headerView.trackingScrollDidEndDraggingWillDecelerate(decelerate)
  }
}

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, 
                                        targetContentOffset: UnsafeMutablePointer<CGPoint>) {
  let headerView = appBar.headerViewController.headerView
  if scrollView == headerView.trackingScrollView {
    headerView.trackingScrollWillEndDragging(withVelocity: velocity, 
                                             targetContentOffset: targetContentOffset)
  }
}

In each of these methods, you check if the scroll view is the one you care about (e.g. the header view’s trackingScrollView), and if it is, pass along the event.

Build and run, and you should now see that the header’s height has become flexible.

Adding more effectst

Now that the flexible header is appropriately tied to the collection view’s scrolling, it’s time to have your HeroHeaderView respond to header scroll position changes in order to create some neat effects.

Open HeroHeaderView.swift once more, and add the following method to HeroHeaderView:

func update(withScrollPhasePercentage scrollPhasePercentage: CGFloat) {
  // 1
  let imageAlpha = min(scrollPhasePercentage.scaled(from: 0...0.8, to: 0...1), 1.0)
  imageView.alpha = imageAlpha
  
  // 2
  let fontSize = scrollPhasePercentage.scaled(from: 0...1, to: 22.0...60.0)
  let font = UIFont(name: "CourierNewPS-BoldMT", size: fontSize)
  titleLabel.font = font
}

This is a short, but very important method.

To start, the method takes a scrollPhase value as its only parameter. The scroll phase is a number from 0.0 to 1.0, where 0.0 is when the flexible header is at minimum height, and 1.0 represents, you guessed it, the header at maximum height.

Through the use of a scaled utility extension in the starter project, the scroll phase is mapped to values appropriate for each of the two header components:

  1. By mapping 0...0.8 to 0...1, the alpha of the background goes from 0 when the header is completely collapsed, to 1.0 once the phase hits 0.8 as it is expanded. This prevents the image from fading away as soon as the user starts scrolling the content.
  2. You map the font size range for the title logo as 22.0...60.0. This means that the title logo will start at font size 60.0 when the header is fully expanded, then shrink as it is collapsed.

To connect the method you just added, open ArticlesViewController.swift once more and add the following extension:

// MARK: MDCFlexibleHeaderViewLayoutDelegate
extension ArticlesViewController: MDCFlexibleHeaderViewLayoutDelegate {

  public func flexibleHeaderViewController(_ flexibleHeaderViewController: MDCFlexibleHeaderViewController, 
    flexibleHeaderViewFrameDidChange flexibleHeaderView: MDCFlexibleHeaderView) {
    heroHeaderView.update(withScrollPhasePercentage: flexibleHeaderView.scrollPhasePercentage)
  }
}

This passes the header scroll phase event straight to your hero header view by invoking the method you just added to HeroHeaderView.

Last but not least, add the following line to configureAppBar() in order to wire up the header layout delegate:

appBar.headerViewController.layoutDelegate = self

Build and run, and you should see the following:

As you scroll, the header should collapse, fading the background image and shrinking the title logo. The flexible header even applies its own effects to stretch its content if you pull down when the collection view is at the top most content offset.

Next up, you’ll add a Material-style scrolling tab bar to let you choose from different news sources.

Adding a Tab Bar

Being able to see a single list of news articles from CNN is already making this app feel pretty useful, but wouldn’t it be even better if you could choose from a bunch of different news sources? Material Design includes just the right component for presenting such a list: the tab bar.

“But wait!” you cry, “iOS already has its own tab bar component!”

Indeed it does, but in Material Design the tab bar can function both as a bottom-style bar with icons and titles (much like the iOS tab bar), or as part of a flexible header, where tabs appear as a horizontally scrolling list of titles.

The second mode is more suited to a list where you might not know the number of values until runtime, and the titles are dynamic to the extent that you wouldn’t be able to provide a unique icon for each. It sounds like this fits the bill perfectly for your news sources navigation.

Open ArticlesViewController.swift and add the following property for the tab bar:

let tabBar = MDCTabBar()

You’re going to add the tab bar as the app bar’s “bottom bar”, which means it will stick to the bottom of the flexible header so that it’s always visible, regardless whether the header is expanded or collapsed. To do this, add the following method right below configureAppBar():

func configureTabBar() {
  // 1
  tabBar.itemAppearance = .titles
  // 2
  tabBar.items = NewsSource.allValues.enumerated().map { index, source in
    return UITabBarItem(title: source.title, image: nil, tag: index)
  }
  // 3
  tabBar.selectedItem = tabBar.items[0]
  // 4
  tabBar.delegate = self
  // 5
  appBar.headerStackView.bottomBar = tabBar
}

This doesn’t look too complicated:

  1. First, you set the item appearance to .titles. This causes the tab bar items to only show titles, without icons.
  2. Here you map all of the news sources, represented by the NewsSource enum, into instances of UITabBarItem. Just as in a UITabBar, this is how the individual tabs are defined. You set the tab on the tab bar item as the index of the news source in the list. This is so that later, when you handle the tab bar selection, you’ll know which news source to select for a given tab.
  3. Next, you set the selected item to the first item in the list. This will set the first news source as the selected news source when the app first starts.
  4. You simply set the tab bar’s delegate to self. You’ll implement this delegate in the next section.
  5. Finally, set the tab bar as the header stack view’s bottom bar to make it “stick” to the bottom of the flexible header.

At this point the tab bar can be configured, but you need to actually call this method first. Find viewDidLoad() and call this new method right below configureAppBar():

configureTabBar()

The bar is now configured, but it still won’t do much because you haven’t implemented the delegate methods yet. Implement its delegate by adding the following extension:

// MARK: MDCTabBarDelegate
extension ArticlesViewController: MDCTabBarDelegate {

  func tabBar(_ tabBar: MDCTabBar, didSelect item: UITabBarItem) {
    refreshContent()
  }
}

This code refreshes the content every time the selected tab changes. This won’t do much unless you update refreshContent() to take the selected tab into account.

Change refreshContent() to look like the following:

func refreshContent() {
  guard inProgressTask == nil else {
    inProgressTask?.cancel()
    inProgressTask = nil
    return
  }

  guard let selectedItem = tabBar.selectedItem else {
    return
  }

  let source = NewsSource.allValues[selectedItem.tag]

  inProgressTask = apiClient.articles(forSource: source) { [weak self] (articles, error) in
    self?.inProgressTask = nil
    if let articles = articles {
      self?.articles = articles
      self?.collectionView?.reloadData()
    } else {
      self?.showError()
    }
  }
}

The above code looks similar to that in the starter project — with one key difference. Instead of hard-coding the news source to .cnn, you obtain the selected tab bar item via tabBar.selectedItem. You then grab the corresponding news source enum via the tab bar item’s tag — remember, you set it to the news source index above. Finally, you pass that news source to the API client method that fetches the articles.

You’re almost there! There’s one more thing to do before achieving tab bar nirvana.

When you configured the app bar, you set the absolute minimum and maximum heights. Without changing anything, you haven’t provided any extra room for the tab bar when the app bar is in the collapsed state. Build and run right now, and you’ll see something like the following when you scroll down into the content:

This would look much snazzier if the app bar allotted space for both the title and the tab bar.

Open HeroHeaderView.swift and change the Constants enum to the following:

struct Constants {
  static let statusBarHeight: CGFloat = UIApplication.shared.statusBarFrame.height
  static let tabBarHeight: CGFloat = 48.0
  static let minHeight: CGFloat = 44 + statusBarHeight + tabBarHeight
  static let maxHeight: CGFloat = 400.0
}

Here you add a new constant for tabBarHeight and then add it to the minHeight constant. This will make sure there is enough room for both the title and the tab bar when in the collapsed state.

Finally, there’s one last problem to contend with. Since you added a new component to the flexible header, the title will no longer look centered vertically. You can resolve this by changing layoutSubviews() in HeroHeaderView.swift to the following:

override func layoutSubviews() {
  super.layoutSubviews()
  imageView.frame = bounds
  titleLabel.frame = CGRect(
    x: 0,
    y: Constants.statusBarHeight,
    width: frame.width,
    height: frame.height - Constants.statusBarHeight - Constants.tabBarHeight)
}

The only difference is that you’re now subtracting Constants.tabBarHeight when calculating the title label’s height.

This centers the title label vertically between the status bar at the top and the tab bar at the bottom. It’ll look much nicer and will prevent one of those pesky UX designers from throwing a brick through your window while you sleep.

Build and run, and you can now choose from a number of news sources, all while expanding or collapsing the header to your heart’s content.

Now that you’ve done a number on the header and navigation, it’s time to give the content a magnificent material makeover.

Adding Article Cards

One of the core tenets of Material Design is the idea of using material as a metaphor. Cards are an excellent implementation of this metaphor, and are used to group content, indicate hierarchy or structure, and denote interactivity, all through the use of varying levels of elevation and movement.

The individual news items in your app are rather dull. But you’re about to change that and turn each news item into a card with a ripple touch effect.

Open ArticleCell.swift and add the familiar import statement to pull in Material Components:

import MaterialComponents

To give the cell a shadow, add the following code to the bottom of ArticleCell:

override class var layerClass: AnyClass {
  return MDCShadowLayer.self
}

var shadowLayer: MDCShadowLayer? {
  return self.layer as? MDCShadowLayer
}

Here you override the UIView class var layerClass in order to force the view’s backing layer to be of type MDCShadowLayer.

This layer lets you set a shadow elevation and will then render a nice-looking shadow. You then expose a convenience variable named shadowLayer so it’s easier to access the shadow layer for configuration purposes.

Now that the shadow layer is in place, add the following code to awakeFromNib():

// 1
shadowLayer?.elevation = MDCShadowElevationCardResting

 // 2
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

 // 3
clipsToBounds = false
imageView.clipsToBounds = true

Taking each commented section in turn:

  1. First, you set the shadow layer’s elevation to MDCShadowElevationCardResting. This is the standard elevation for a card in the “resting” state. There are other elevations that correspond to various types of components and interactions.
  2. Next, you configure the rasterization mode for the view’s layer in order to improve scrolling performance.
  3. Finally, you set clipsToBounds to false on the cell so the shadow can escape the bounds of the cell, and set the clipsToBounds to true for the image view. Because you’re using the .scaleAspectFill mode, this will ensure the image content stays confined to the view.

Build and run once again. You should now see a classy shadow surrounding each piece of content, giving it a very defined card look.

Your app is now looking decidedly more Material. Those cards almost scream “please tap me”, but alas, when you do so, nothing happens to indicate your tap before you’re ushered away to the article detail.

Ripple effect on tap

Material Design has a universal method of indicating interactivity, through the use of an “ink” component that causes a very subtle ripple to occur whenever something is tapped or clicked on.

Let’s pour some ink onto these cards. Add a variable for an MDCInkTouchController to ArticleCell like so:

var inkTouchController: MDCInkTouchController?

The ink touch controller manages an underlying ink view and deals with handling input. The only other thing to do is initialize the ink touch controller and add it to the view.

Add the following code to awakeFromNib():

inkTouchController = MDCInkTouchController(view: self)
inkTouchController?.addInkView()

The ink touch controller maintains a weak reference to the view, so don’t worry about causing a retain cycle here.

Build and run, then tap on a card to see ink in action.

And that’s it! You’ve have yourself a fully armed and operational Material Design news app.

Google Material Design Tutorial for iOS

Custom UIViewController Transitions

iOS delivers some nice view controller transitions — push, pop, cover vertically — for free but it’s great fun to make your own. Custom UIViewController transitions can significantly enhance your users’ experiences and set your app apart from the rest of the pack. If you’ve avoided making your own custom transitions because the process seems too daunting, you’ll find that it’s not nearly as difficult as you might expect.

In this tutorial, you’ll add some custom UIViewController transitions to a small guessing game app. By the time you’ve finished, you’ll have learned:

  • How the transitioning API is structured.
  • How to present and dismiss view controllers using custom transitions.
  • How to build interactive transitions.

Getting Started

Download the starter project. Build and run the project; you’ll see the following guessing game:

The app presents several cards in a page view controller. Each card shows a description of a pet and tapping a card reveals which pet it describes.

Your job is to guess the pet! Is it a cat, dog or fish? Play with the app and see how well you do.

The navigation logic is already in place but the app currently feels quite bland. You’re going to spice it up with custom transitions.

Exploring the Transitioning API

The transitioning API is a collection of protocols. This allows you to make the best implementation choice for your app: use existing objects or create purpose-built objects to manage your transitions. By the end of this section, you’ll understand the responsibilities of each protocol and the connections between them. The diagram below shows you the main components of the API:

The Pieces of the Puzzle

Although the diagram looks complex, it will feel quite straightforward once you understand how the various parts work together.

Transitioning Delegate

Every view controller can have a transitioningDelegate, an object that conforms to UIViewControllerTransitioningDelegate.

Whenever you present or dismiss a view controller, UIKit asks its transitioning delegate for an animation controller to use. To replace a default animation with your own custom animation, you must implement a transitioning delegate and have it return the appropriate animation controller.

Animation Controller

The animation controller returned by the transitioning delegate is an object that implements UIViewControllerAnimatedTransitioning. It does the “heavy lifting” of implementing the animated transition.

Transitioning Context

The transitioning context object implements UIViewControllerContextTransitioning and plays a vital role in the transitioning process: it encapsulates information about the views and view controllers involved in the transition.

As you can see in the diagram, you don’t implement this protocol yourself. UIKit creates and configures the transitioning context for you and passes it to your animation controller each time a transition occurs.

The Transitioning Process

Here are the steps involved in a presentation transition:

  1. You trigger the transition, either programmatically or via a segue.
  2. UIKit asks the “to” view controller (the view controller to be shown) for its transitioning delegate. If it doesn’t have one, UIKIt uses the standard, built-in transition.
  3. UIKit then asks the transitioning delegate for an animation controller via animationController(forPresented:presenting:source:). If this returns nil, the transition will use the default animation.
  4. UIKit constructs the transitioning context.
  5. UIKit asks the animation controller for the duration of its animation by calling transitionDuration(using:).
  6. UIKit invokes animateTransition(using:) on the the animation controller to perform the animation for the transition.
  7. Finally, the animation controller calls completeTransition(_:) on the transitioning context to indicate that the animation is complete.

The steps for a dismissing transition are nearly identical. In this case, UIKit asks the “from” view controller (the one being dismissed) for its transitioning delegate. The transitioning delegate vends the appropriate animation controller via animationController(forDismissed:).

Creating a Custom Presentation Transition

Time to put your new-found knowledge into practice! Your goal is to implement the following animation:

  • When the user taps a card, it flips to reveal the second view scaled down to the size of the card.
  • Following the flip, the view scales to fill the whole screen.

Creating the Animator

You’ll start by creating the animation controller.

From the menu, select File\New\File…, choose iOS\Source\Cocoa Touch Class, and click Next. Name the file FlipPresentAnimationController, make it a subclass of NSObject and set the language to Swift. Click Next and set the Group to Animation Controllers. Click Create.

Animation controllers must conform to UIViewControllerAnimatedTransitioning. Open FlipPresentAnimationController.swift and update the class declaration accordingly.

class FlipPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

}

Xcode will raise an error complaining that FlipPresentAnimationController does not conform to UIViewControllerAnimatedTransitioning. Click Fix to add the necessary stub routines.

You’re going to use the frame of the tapped card as a starting point for the animation. Inside the body of the class, add the following code to store this information.

private let originFrame: CGRect

init(originFrame: CGRect) {
  self.originFrame = originFrame
}

Next, you must fill in the code for the two stubs you added. Update transitionDuration(using:) as follows:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
  return 2.0
}

As the name suggests, this method specifies the duration of your transition. Setting it to two seconds will prove useful during development as it leaves enough time to observe the animation.

Add the following to animateTransition(using:):

// 1
guard let fromVC = transitionContext.viewController(forKey: .from),
  let toVC = transitionContext.viewController(forKey: .to),
  let snapshot = toVC.view.snapshotView(afterScreenUpdates: true)
  else {
    return
}

// 2
let containerView = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: toVC)

// 3
snapshot.frame = originFrame
snapshot.layer.cornerRadius = CardViewController.cardCornerRadius
snapshot.layer.masksToBounds = true

Here’s what this does:

  1. Extract a reference to both the view controller being replaced and the one being presented. Make a snapshot of what the screen will look like after the transition.
  2. UIKit encapsulates the entire transition inside a container view to simplify managing both the view hierarchy and the animations. Get a reference to the container view and determine what the final frame of the new view will be.
  3. Configure the snapshot’s frame and drawing so that it exactly matches and covers the card in the “from” view.

Continue adding to the body of animateTransition(using:).

// 1
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
toVC.view.isHidden = true

// 2
AnimationHelper.perspectiveTransform(for: containerView)
snapshot.layer.transform = AnimationHelper.yRotation(.pi / 2)
// 3
let duration = transitionDuration(using: transitionContext)

The container view, as created by UIKit, contains only the “from” view. You must add any other views that will participate in the transition. It’s important to remember that addSubview(_:) puts the new view in front of all others in the view hierarchy so the order in which you add subviews matters.

  1. Add the new “to” view to the view hierarchy and hide it. Place the snapshot in front of it.
  2. Set up the beginning state of the animation by rotating the snapshot 90˚ around its y-axis. This causes it to be edge-on to the viewer and, therefore, not visible when the animation begins.
  3. Get the duration of the animation.
Note: AnimationHelper is a small utility class responsible for adding perspective and rotation transforms to your views. Feel free to have a look at the implementation. If you’re curious about the magic of perspectiveTransform(for:), try commenting out the call after you finish the tutorial.

You now have everything set up; time to animate! Complete the method by adding the following.

// 1
UIView.animateKeyframes(
  withDuration: duration,
  delay: 0,
  options: .calculationModeCubic,
  animations: {
    // 2
    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1/3) {
      fromVC.view.layer.transform = AnimationHelper.yRotation(-.pi / 2)
    }
    
    // 3
    UIView.addKeyframe(withRelativeStartTime: 1/3, relativeDuration: 1/3) {
      snapshot.layer.transform = AnimationHelper.yRotation(0.0)
    }
    
    // 4
    UIView.addKeyframe(withRelativeStartTime: 2/3, relativeDuration: 1/3) {
      snapshot.frame = finalFrame
      snapshot.layer.cornerRadius = 0
    }
},
  // 5
  completion: { _ in
    toVC.view.isHidden = false
    snapshot.removeFromSuperview()
    fromVC.view.layer.transform = CATransform3DIdentity
    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})

Here’s the play-by-play of your animation:

  1. You use a standard UIView keyframe animation. The duration of the animation must exactly match the length of the transition.
  2. Start by rotating the “from” view 90˚ around its y-axis to hide it from view.
  3. Next, reveal the snapshot by rotating it back from its edge-on state that you set up above.
  4. Set the frame of the snapshot to fill the screen.
  5. The snapshot now exactly matches the “to” view so it’s finally safe to reveal the real “to” view. Remove the snapshot from the view hierarchy since it’s no longer needed. Next, restore the “from” view to its original state; otherwise, it would be hidden when transitioning back. Calling completeTransition(_:) informs UIKit that the animation is complete. It will ensure the final state is consistent and remove the “from” view from the container.

Your animation controller is now ready to use!

Wiring Up the Animator

UIKit expects a transitioning delegate to vend the animation controller for a transition. To do this, you must first provide an object which conforms to UIViewControllerTransitioningDelegate. In this example, CardViewController will act as the transitioning delegate.

Open CardViewController.swift and add the following extension at the bottom of the file.

extension CardViewController: UIViewControllerTransitioningDelegate {
  func animationController(forPresented presented: UIViewController,
                           presenting: UIViewController,
                           source: UIViewController)
    -> UIViewControllerAnimatedTransitioning? {
    return FlipPresentAnimationController(originFrame: cardView.frame)
  }
}

Here you return an instance of your custom animation controller, initialized with the frame of the current card.

The final step is to mark CardViewController as the transitioning delegate. View controllers have a transitioningDelegate property, which UIKit will query to see if it should use a custom transition.

Add the following to the end of prepare(for:sender:) just below the card assignment:

destinationViewController.transitioningDelegate = self

It’s important to note that it is the view controller being presented that is asked for a transitioning delegate, not the view controller doing the presenting!

Build and run your project. Tap on a card and you should see the following:

And there you have it — your first custom transition!

Dismissing the View Controller

You have a great presentation transition but that’s only half the job. You’re still using the default dismissal transition. Time to fix that!

From the menu, select File\New\File…, choose iOS\Source\Cocoa Touch Class, and click Next. Name the file FlipDismissAnimationController, make it a subclass of NSObject and set the language to Swift. Click Next and set the Group to Animation Controllers. Click Create.

Replace the class definition with the following.

class FlipDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
  
  private let destinationFrame: CGRect
  
  init(destinationFrame: CGRect) {
    self.destinationFrame = destinationFrame
  }
  
  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.6
  }
  
  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    
  }
}

This animation controller’s job is to reverse the presenting animation so that the UI feels symmetric. To do this it must:

  • Shrink the displayed view to the size of the card; destinationFrame holds this value.
  • Flip the view around to reveal the original card.

Add the following lines to animateTransition(using:).

// 1
guard let fromVC = transitionContext.viewController(forKey: .from),
  let toVC = transitionContext.viewController(forKey: .to),
  let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false)
  else {
    return
}

snapshot.layer.cornerRadius = CardViewController.cardCornerRadius
snapshot.layer.masksToBounds = true

// 2
let containerView = transitionContext.containerView
containerView.insertSubview(toVC.view, at: 0)
containerView.addSubview(snapshot)
fromVC.view.isHidden = true

// 3
AnimationHelper.perspectiveTransform(for: containerView)
toVC.view.layer.transform = AnimationHelper.yRotation(-.pi / 2)
let duration = transitionDuration(using: transitionContext)

This should all look familiar. Here are the important differences:

  1. This time it’s the “from” view you must manipulate so you take a snapshot of that.
  2. Again, the ordering of layers is important. From back to front, they must be in the order: “to” view, “from” view, snapshot view. While it may not seem important in this particular transition, it is vital in others, particularly if the transition can be cancelled.
  3. Rotate the “to” view to be edge-on so that it isn’t immediately revealed when you rotate the snapshot.

All that’s needed now is the actual animation itself. Add the following code to the end of animateTransition(using:).

UIView.animateKeyframes(
  withDuration: duration,
  delay: 0,
  options: .calculationModeCubic,
  animations: {
    // 1
    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1/3) {
      snapshot.frame = self.destinationFrame
    }
    
    UIView.addKeyframe(withRelativeStartTime: 1/3, relativeDuration: 1/3) {
      snapshot.layer.transform = AnimationHelper.yRotation(.pi / 2)
    }
    
    UIView.addKeyframe(withRelativeStartTime: 2/3, relativeDuration: 1/3) {
      toVC.view.layer.transform = AnimationHelper.yRotation(0.0)
    }
},
  // 2
  completion: { _ in
    fromVC.view.isHidden = false
    snapshot.removeFromSuperview()
    if transitionContext.transitionWasCancelled {
      toVC.view.removeFromSuperview()
    }
    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})

This is exactly the inverse of the presenting animation.

  1. First, scale the snapshot view down, then hide it by rotating it 90˚. Next, reveal the “to” view by rotating it back from its edge-on position.
  2. Clean up your changes to the view hierarchy by removing the snapshot and restoring the state of the “from” view. If the transition was cancelled — it isn’t yet possible for this transition, but you will make it possible shortly — it’s important to remove everything you added to the view hierarchy before declaring the transition complete.

Remember that it’s up to the transitioning delegate to vend this animation controller when the pet picture is dismissed. Open CardViewController.swift and add the following method to the UIViewControllerTransitioningDelegate extension.

func animationController(forDismissed dismissed: UIViewController)
  -> UIViewControllerAnimatedTransitioning? {
  guard let _ = dismissed as? RevealViewController else {
    return nil
  }
  return FlipDismissAnimationController(destinationFrame: cardView.frame)
}

This ensures that the view controller being dismissed is of the expected type and then creates the animation controller giving it the correct frame for the card it will reveal.

It’s no longer necessary to have the presentation animation run slowly. Open FlipPresentAnimationController.swift and change the duration from 2.0 to 0.6 so that it matches your new dismissal animation.

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
  return 0.6
}

Build and run. Play with the app to see your fancy new animated transitions.

Making It Interactive

Your custom animations look really sharp. But, you can improve your app even further by adding user interaction to the dismissal transition. The Settings app in iOS has a great example of an interactive transition animation:

Your task in this section is to navigate back to the card’s face-down state with a swipe from the left edge of the screen. The progress of the transition will follow the user’s finger.

How Interactive Transitions Work

An interaction controller responds either to touch events or programmatic input by speeding up, slowing down, or even reversing the progress of a transition. In order to enable interactive transitions, the transitioning delegate must provide an interaction controller. This can be any object that implements UIViewControllerInteractiveTransitioning.

You’ve already made the transition animation. The interaction controller will manage this animation in response to gestures rather than letting it play like a video. Apple provides the ready-made UIPercentDrivenInteractiveTransition class, which is a concrete interaction controller implementation. You’ll use this class to make your transition interactive.

From the menu, select File\New\File…, choose iOS\Source\Cocoa Touch Class, and click Next. Name the file SwipeInteractionController, make it a subclass of UIPercentDrivenInteractiveTransition and set the language to Swift. Click Next and set the Group to Interaction Controllers. Click Create.

Add the following to the class.

var interactionInProgress = false

private var shouldCompleteTransition = false
private weak var viewController: UIViewController!

init(viewController: UIViewController) {
  super.init()
  self.viewController = viewController
  prepareGestureRecognizer(in: viewController.view)
}

These declarations are fairly straightforward.

  • interactionInProgress, as the name suggests, indicates whether an interaction is already happening.
  • shouldCompleteTransition will be used internally to control the transition. You’ll see how shortly.
  • viewController is a reference to the view controller to which this interaction controller is attached.

Next, set up the gesture recognizer by adding the following method to the class.

private func prepareGestureRecognizer(in view: UIView) {
  let gesture = UIScreenEdgePanGestureRecognizer(target: self,
                                                 action: #selector(handleGesture(_:)))
  gesture.edges = .left
  view.addGestureRecognizer(gesture)
}

The gesture recognizer is configured to trigger when the user swipes from the left edge of the screen and is added to the view.

The final piece of the interaction controller is handleGesture(_:). Add that to the class now.

@objc func handleGesture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
  // 1
  let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
  var progress = (translation.x / 200)
  progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
  
  switch gestureRecognizer.state {
  
  // 2
  case .began:
    interactionInProgress = true
    viewController.dismiss(animated: true, completion: nil)
    
  // 3
  case .changed:
    shouldCompleteTransition = progress > 0.5
    update(progress)
    
  // 4
  case .cancelled:
    interactionInProgress = false
    cancel()
    
  // 5
  case .ended:
    interactionInProgress = false
    if shouldCompleteTransition {
      finish()
    } else {
      cancel()
    }
  default:
    break
  }
}

Here’s the play-by-play:

  1. You start by declaring local variables to track the progress of the swipe. You fetch the translation in the view and calculate the progress. A swipe of 200 or more points will be considered enough to complete the transition.
  2. When the gesture starts, you set interactionInProgress to true and trigger the dismissal of the view controller.
  3. While the gesture is moving, you continuously call update(_:). This is a method on UIPercentDrivenInteractiveTransition which moves the transition along by the percentage amount you pass in.
  4. If the gesture is cancelled, you update interactionInProgress and roll back the transition.
  5. Once the gesture has ended, you use the current progress of the transition to decide whether to cancel() it or finish() it for the user.

Now, you must add the plumbing to actually create your SwipeInteractionController. Open RevealViewController.swift and add the following property.

var swipeInteractionController: SwipeInteractionController?

Next, add the following to the end of viewDidLoad().

swipeInteractionController = SwipeInteractionController(viewController: self)

When the picture view of the pet card is displayed, an interaction controller is created and connected to it.

Open FlipDismissAnimationController.swift and add the following property after the declaration for destinationFrame.

let interactionController: SwipeInteractionController?

Replace init(destinationFrame:) with:

init(destinationFrame: CGRect, interactionController: SwipeInteractionController?) {
  self.destinationFrame = destinationFrame
  self.interactionController = interactionController
}

The animation controller needs a reference to the interaction controller so it can partner with it.

Open CardViewController.swift and replace animationController(forDismissed:) with:

func animationController(forDismissed dismissed: UIViewController)
  -> UIViewControllerAnimatedTransitioning? {
  guard let revealVC = dismissed as? RevealViewController else {
    return nil
  }
  return FlipDismissAnimationController(destinationFrame: cardView.frame,
                                        interactionController: revealVC.swipeInteractionController)
}

This simply updates the creation of FlipDismissAnimationController to match the new initializer.

Finally, UIKit queries the transitioning delegate for an interaction controller by calling interactionControllerForDismissal(using:). Add the following method at the end of the UIViewControllerTransitioningDelegate extension.

func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning)
  -> UIViewControllerInteractiveTransitioning? {
  guard let animator = animator as? FlipDismissAnimationController,
    let interactionController = animator.interactionController,
    interactionController.interactionInProgress
    else {
      return nil
  }
  return interactionController
}

This checks first that the animation controller involved is a FlipDismissAnimationController. If so, it gets a reference to the associated interaction controller and verifies that a user interaction is in progress. If any of these conditions are not met, it returns nil so that the transition will proceed without interactivity. Otherwise, it hands the interaction controller back to UIKit so that it can manage the transition.

Build and run. Tap a card, then swipe from the left edge of the screen to see the final result.

Congratulations! You’ve created a interesting and engaging interactive transition!

Custom UIViewController Transitions