Update note: Christine Abernathy updated this for iOS 12, Xcode 10 and Swift 4.2. Ray Wenderlich wrote the original.

In this tutorial, you’ll create with and use them to finish a pattern recognition game called Recall. Along the way, you’ll reinforce concepts like path-based drawing.

If you’re brand new to Core Graphics, it would be a good idea to review some of our more entry level tutorials on the topic. Consider working through our Lines, Rectangles, and Gradients and Arcs and Paths tutorials to get a better understanding of foundations you’ll build upon here. Paid subscribers can also check out our Beginning Core Graphics video series for a good overview of Core Graphics.

Getting Started

Start by clicking the Download Materials button at the top or bottom of this tutorial. Build and run the starter app. You’ll see something like this:

Recall takes inspiration from a game in the Left vs Right brain app. The goal in the game is to choose the popular direction for objects in view. A new set of objects displays once you make a choice. You have five attempts before the game ends.

Recall groups pattern objects in four quadrants. Each quadrant in the starter app has a label. The text represents the direction, and the background color represents the fill color.

The game is rather underwhelming right now.

Your task is to use your newly discovered Core Graphics chops to turn this into the finished app below:

Take a look at the project in Xcode. These are the main files:

  • GameViewController.swift: Controls the gameplay and displays the game view.
  • ResultViewController.swift: Displays the final score and a button to restart the game.
  • PatternView.swift: Displays the pattern view for one of the quadrants in the game view.

You will enhance PatternView to display the desired pattern.

For starters, you’ll prototype your new and improved PatternView in Playgrounds. This allows you to iterate much faster while learning the ins and outs of Core Graphics patterns. Once you’re done, you’ll transfer the relevant code over to the Recall starter project.

In Xcode, go to File ▸ New ▸ Playground…. Select the Single View template. Click Next, name the playground PatternView.playground and click Create. Your playground contains a view controller with a single view.

Select Editor ▸ Run Playground to execute the playground. Click Show the Assistant Editor to display your starter view. It displays the iconic “Hello World!” result:

As you go through the next sections, you’ll replace the starter view with your pattern view. Ready to get started?

Anatomy of a Pattern

In previous Core Graphics tutorials, you’ve seen how to define and paint paths like this:

The example above shows you filling out the path on the left with a color. Keep in mind that you can also stroke a path with a color.

Using Core Graphics, you can also stroke or fill a path with a pattern. The example below shows a colored pattern filling out a path:

You set up a pattern by doing the following:

  1. Write a method that draws an individual pattern cell.
  2. Create a pattern with parameters that include how to draw and place an individual cell.
  3. Define the color information that your pattern will use.
  4. Paint the desired path with the pattern you created.

Now, check out a slightly different pattern cell with extra padding. The thin black border shows the bounds of the cell:

To draw within the bounds of the cell, write the draw method for your pattern cell. Core Graphics clips anything outside of the bounds. Core Graphics also expects you to draw the pattern cell exactly the same way each time.

Your draw method can apply color when setting up your pattern cell. This is a colored pattern. An uncolored or masking pattern is one where you apply the fill color outside of the draw method. This gives you the flexibility to set up your pattern colors where it makes sense.

Core Graphics calls your draw method repeatedly to set up your pattern. The pattern creation parameters define what the pattern looks like. The example below shows a basic repeated pattern with the cells lining up up right next to each other:

You can specify the spacing between the pattern cells when you configure the pattern:

You can also apply transformations to change the pattern’s appearance. The pictures below show the pattern drawn inside a space represented by the fuzzy border:

The first shows an unchanged pattern. In the second, you see a translated pattern. The third shows the pattern rotated. Again, the black border around the pattern cells highlights its bounds.

You have a lot of options available when configuring a pattern. You’ll start putting all this together in the next section.

Creating your First Pattern

Add the following code before the view controller class in PatternView.playground:


class PatternView: UIView {
  override func draw(_ rect: CGRect) {
    // 1
    let context = UIGraphicsGetCurrentContext()!
    // 2
    UIColor.orange.setFill()
    // 3
    context.fill(rect)
  }
}

This represents the custom view for your pattern. Here, you override draw(_:) to do the following:

  1. Get the view’s graphics context.
  2. Set the current fill color for the context.
  3. Fill the entire context with the current fill color.

Think of the graphics context as a canvas you can draw on. The context contains information such as the color that will fill in or stroke a path. You can draw out paths in your canvas before painting them in with a context’s color information.

Inside MyViewController, replace the code in loadView() related to label with the following:


let patternView = PatternView()
patternView.frame = CGRect(x: 10, y: 10, width: 200, height: 200)
view.addSubview(patternView)

This creates an instance of the pattern view, sets its frame and adds it to view.

Hit Shift+Command+Return to run the playground. The previous label is gone and replaced with an orange subview:

Coloring is just the beginning of the journey. You know there’s more where that came from!

Add the following property at the top of PatternView:


let drawPattern: CGPatternDrawPatternCallback = { _, context in
  context.addArc(
    center: CGPoint(x: 20, y: 20), radius: 10.0,
    startAngle: 0, endAngle: CGFloat(2.0 * .pi),
    clockwise: false)
  context.setFillColor(UIColor.black.cgColor)
  context.fillPath()
}

The code above draws a circular path in the graphics context and fills it with black color.

This represents your pattern cell’s drawing method, which is of the type CGPatternDrawPatternCallback. The method accepts a pointer to private data associated with the pattern. You’re not using private data, so an unnamed parameter is used. The method also accepts the graphics context used in drawing your pattern cell.

Add the following code to the end of draw(_:):


var callbacks = CGPatternCallbacks(
  version: 0, drawPattern: drawPattern, releaseInfo: nil)

You provide a callback to drawPattern and releaseInfo accepts a callback invoked when the system releases the pattern. You would typically set up a release callback function if you’re using private data in the pattern. Since you don’t use private data in your draw method, you pass nil to this callback.

Add the following right after your callbacks assignment:


let pattern = CGPattern(
  info: nil,
  bounds: CGRect(x: 0, y: 0, width: 20, height: 20),
  matrix: .identity,
  xStep: 50,
  yStep: 50,
  tiling: .constantSpacing,
  isColored: true,
  callbacks: &callbacks)

This creates a pattern object. In the code above, you pass in the following parameters:

  • info: Pointer to any private data you want to use in your pattern callbacks. You pass in nil here since you’re not using any.
  • bounds: Pattern cell’s bounding box.
  • matrix: Matrix that represents the transform to apply. You pass in the identity matrix as you’re not applying any transforms.
  • xStep: Horizontal spacing between pattern cells.
  • yStep: Vertical spacing between pattern cells.
  • tiling: Change to account for differences between user space units and device pixels.
  • isColored: Whether the pattern cell draw method applies color. You’re setting this to true since your draw method sets a color.
  • callbacks: Pointer to the structure that holds the pattern callbacks.

Add the following code right after your pattern assignment:


var alpha : CGFloat = 1.0
context.setFillPattern(pattern!, colorComponents: &alpha)
context.fill(rect)

The code above sets the fill pattern for the graphics context. For colored patterns, you must also pass in an alpha value to specify the pattern opacity. The pattern draw method provides the color. Finally, the code paints the view’s frame area with the pattern.

Run the playground by hitting Shift+Command+Return. Your pattern isn’t showing up. Strange. What’s going on?

You need to provide Core Graphics with information about your pattern color space so it knows how to handle pattern colors.

Add the following above the alpha declaration:


// 1
let patternSpace = CGColorSpace(patternBaseSpace: nil)!
// 2
context.setFillColorSpace(patternSpace)

Here’s what the code does:

  1. Creates a pattern color space. The base space parameter should be nil for colored patterns. This delegates the coloring to your pattern cell draw method.
  2. Set the fill color space to your defined pattern color space.

Run the playground. Yes! You should now see a circular black pattern:

How about getting more comfortable with configuring patterns?

Configuring a Pattern

In your playground, change the spacing parameters that set up pattern as follows:


xStep: 30,
yStep: 30,

Run the playground. Note that the circular dots appear to be much closer to each other:

This makes sense as you’ve shrunk the step size between pattern cells.

Now, change the spacing parameters as follows:


xStep: 20,
yStep: 20,

Run the playground. Your circles have turned into quarters:

To understand why, note that your draw method returns a circle with a radius of 10 centered at (20, 20). The pattern’s horizontal and vertical displacement is 20. The cell’s bounding box is 20✕20 at origin (0,0). This results in a repeating quarter circle that starts at the lower right edge.

Change the drawPattern code that draws the circle as follows:


context.addArc(
      center: CGPoint(x: 10, y: 10), radius: 10.0,
      startAngle: 0, endAngle: CGFloat(2.0 * .pi),
      clockwise: false)

You’ve changed the center point to be (10, 10) rather than (20, 20).

Run the playground. You’re back to whole circles due to the shift in the circle’s center:

The pattern cell bounds also match up perfectly with the circle, resulting in each cell abutting the other.

You can transform your pattern in many interesting ways. Inside draw(_:), replace pattern with the following:


// 1
let transform = CGAffineTransform(translationX: 5, y: 5)
// 2
let pattern = CGPattern(
      info: nil,
      bounds: CGRect(x: 0, y: 0, width: 20, height: 20),
      matrix: transform,
      xStep: 20,
      yStep: 20,
      tiling: .constantSpacing,
      isColored: true,
      callbacks: &callbacks)

You’ve modified the CGPattern by passing it a transformation matrix. Here’s a closer look:

  1. Create an affine transformation matrix that represents a translation.
  2. Configure the pattern to use this transformation by passing it in the matrix parameter.

Run the playground. Note how the pattern shifted to the right and downwards to match the translation you defined:

Besides translating your pattern, you can also scale and rotate the pattern cells. You’ll see how to rotate the pattern later on when you build the pattern for your app.

Here’s how you can fill and stroke a colored pattern. In drawPattern replace:


context.setFillColor(UIColor.black.cgColor)
context.fillPath()  

With the following code:


context.setFillColor(UIColor.yellow.cgColor)
context.setStrokeColor(UIColor.darkGray.cgColor)
context.drawPath(using: .fillStroke)

Here, you change the fill color to yellow and set the stroke color. You then call drawPath(using:) with the option that fills and strokes the path.

Run your playground and check that the pattern now shows your new fill color and stroke:

Thus far, you’ve worked with colored patterns and defined the colors in the pattern draw method. In the completed app, you’ll have to create a pattern with different colors. You probably realize that writing a draw method for each color isn’t the way to go. This is where masking patterns come into play.

Masking Patterns

Masking patterns define their color information outside the pattern cell draw method. This allows you to change up the pattern color to suit your needs.

Here’s an example of a masking pattern that has no color associated with it:

With the pattern in place, you can now apply color. The first example below shows a blue color applied to the mask, and the second displays an orange color:

Now you’ll change the pattern you’ve been working with to a masking pattern.

Replace the following code from drawPattern:


context.setFillColor(UIColor.yellow.cgColor)
context.setStrokeColor(UIColor.darkGray.cgColor)
context.drawPath(using: .fillStroke)

With the following:


context.fillPath()

This reverts the code back to filling the path.

Replace the pattern assignment with the following:


let pattern = CGPattern(
      info: nil,
      bounds: CGRect(x: 0, y: 0, width: 20, height: 20),
      matrix: transform,
      xStep: 25,
      yStep: 25,
      tiling: .constantSpacing,
      isColored: false,
      callbacks: &callbacks)

This sets the isColored parameter to false, changing your pattern to a masking pattern. You’ve also increased the vertical and horizontal spacing to 25. Now, you need to provide the color space information for your pattern.

Replace the patternSpace assignment with the following:


let baseSpace = CGColorSpaceCreateDeviceRGB()
let patternSpace = CGColorSpace(patternBaseSpace: baseSpace)!

Here, you get a reference to a standard device-dependent RGB color space. You then change your pattern color space to this value instead of the previous nil value.

Below that, replace these lines:


var alpha : CGFloat = 1.0
context.setFillPattern(pattern!, colorComponents: &alpha)

With the following:


let fillColor: [CGFloat] = [0.0, 1.0, 1.0, 1.0]
context.setFillPattern(pattern!, colorComponents: fillColor)

This creates a color applied underneath the mask when filling out the pattern.

Run the playground. You pattern’s color updates to reflect the cyan color setting that’s configured outside the draw method:

Time to see how to stroke and fill a masking pattern. It’s like stroking a colored pattern.

Replace the line context.fillPath() in the drawPattern definition with the following:


context.setStrokeColor(UIColor.darkGray.cgColor)
context.drawPath(using: .fillStroke)

Although you set the stroke color inside draw(_:), your pattern color is still set outside the method.

Run the playground to see the stroked pattern:

You’ve now built up experience with different pattern configurations and with masking patterns. You can begin building out the pattern you’ll need for Recall.

Creating the Game Pattern

Add the following code before the view controller presents in the playground:


extension UIBezierPath {
  // 1
  convenience init(triangleIn rect: CGRect) {
    self.init()
    // 2
    let topOfTriangle = CGPoint(x: rect.width / 2, y: 0)
    let bottomLeftOfTriangle = CGPoint(x: 0, y: rect.height)
    let bottomRightOfTriangle = CGPoint(x: rect.width, y: rect.height)
    // 3
    self.move(to: topOfTriangle)
    self.addLine(to: bottomLeftOfTriangle)
    self.addLine(to: bottomRightOfTriangle)
    // 4
    self.close()
  }
}

Going through the code step by step:

  1. Extend UIBezierPath to create a triangular path.
  2. Specify the three points that make up the triangle.
  3. Draw the triangle, starting from the top. move(to:) begins the path and addLine(to:) adds lines to the path.
  4. Close the path.

Add the following struct to the top of PatternView:


public struct Constants {
  static let patternSize: CGFloat = 30.0
  static let patternRepeatCount = 2
}

These represent the constants you’ll use when setting up your pattern. patternSize defines the pattern cell size. patternRepeatCount defines the number of pattern cells in the pattern view.

Add the following after the Constants definition:


let drawTriangle: CGPatternDrawPatternCallback = { _, context in
  let trianglePath = UIBezierPath(triangleIn:
    CGRect(x: 0, y: 0,
           width: Constants.patternSize,
           height: Constants.patternSize))
  context.addPath(trianglePath.cgPath)
  context.fillPath()
}

This defines a new function for drawing your triangular pattern. Call UIBezierPath(triangleIn:) to return a path representing the triangle. Then, add this path to the context before painting it.

Note that the function doesn’t specify a fill color, so it can be a masking pattern.

In draw(_:), change callbacks to the following:


var callbacks = CGPatternCallbacks(
  version: 0, drawPattern: drawTriangle, releaseInfo: nil)

You’re now using the triangle drawing function.

Delete drawPattern as it’s no longer necessary. One can only go around in circles for so long. :]

Also in draw(_:), replace the code that assigns transform and pattern with the following:


// 1
let patternStepX: CGFloat =
  rect.width / CGFloat(Constants.patternRepeatCount)
let patternStepY: CGFloat =
  rect.height / CGFloat(Constants.patternRepeatCount)
// 2
let patternOffsetX: CGFloat = (patternStepX - Constants.patternSize) / 2.0
let patternOffsetY: CGFloat = (patternStepY - Constants.patternSize) / 2.0
// 3
let transform = CGAffineTransform(translationX: patternOffsetX, y: patternOffsetY)
// 4
let pattern = CGPattern(
  info: nil,
  bounds: CGRect(
    x: 0, 
    y: 0, 
    width: Constants.patternSize, 
    height: Constants.patternSize),
  matrix: transform,
  xStep: patternStepX,
  yStep: patternStepY,
  tiling: .constantSpacing,
  isColored: false,
  callbacks: &callbacks)

Here’s what that code does, step by step:

  1. Calculate the horizontal and vertical step size using the view’s width and height, as well as the number of pattern cells in a view.
  2. Work out the dimensions to horizontally and vertically center a pattern cell within its bounds.
  3. Set up a CGAffineTransform translation based on the centering variables you defined.
  4. Create the pattern object based on your calculated parameters.

Run the playground. You should see two triangles in each direction, centered both vertically and horizontally within their bounds:

You’ll now get your background colors to more closely match the Recall app.

In MyViewController, change the background color setup in loadView() as follows:


view.backgroundColor = .lightGray

Next go to PatternView and change the context fill setup in draw(_:) as follows:


UIColor.white.setFill()

Run the playground. Your main view’s background should now be gray with a white background for your pattern view:

Customizing the Pattern View

Now that you have the basic pattern displaying correctly, you can make changes to control the pattern direction.

Add the following enumeration near the top of PatternView after the Constants definition:


enum PatternDirection: CaseIterable {
  case left
  case top
  case right
  case bottom
}

This represents the different directions the triangle can point. They match the directions in your starter app.

Add the following class properties after the PatternDirection definition:


var fillColor: [CGFloat] = [1.0, 0.0, 0.0, 1.0]
var direction: PatternDirection = .top

This represents the color you’ll apply to the masking pattern and the pattern direction. The class sets a default color of red and default direction of top.

Delete the local fillColor declaration found near the bottom of draw(_:). This will ensure that you use the class property instead.

Replace the transform assignment with the following:


// 1
var transform: CGAffineTransform
// 2
switch direction {
case .top:
  transform = .identity
case .right:
  transform = CGAffineTransform(rotationAngle: CGFloat(0.5 * .pi))
case .bottom:
  transform = CGAffineTransform(rotationAngle: CGFloat(1.0 * .pi))
case .left:
  transform = CGAffineTransform(rotationAngle: CGFloat(1.5 * .pi))
}
// 3
transform = transform.translatedBy(x: patternOffsetX, y: patternOffsetY)

Here’s what just happened:

  1. Declare a CGAffineTransform variable for your pattern transform.
  2. Assign the transform to the identity matrix if the pattern direction is top. Otherwise, the transform is a rotation based on the direction. For example, if the pattern points right, then the rotation is π / 2 radians or 90º clockwise.
  3. Apply a CGAffineTransform translation to center the pattern cell within its bounds.

Run the playground. Your triangles are red based on your default pattern fill color:

Now’s a great time to set up code to control and test the pattern color and direction.

Add the following methods in PatternView after the class property definitions:


init(fillColor: [CGFloat], direction: PatternDirection = .top) {
  self.fillColor = fillColor
  self.direction = direction
  super.init(frame: CGRect.zero)
}
  
required init?(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
}

This sets up an initializer that takes in a fill color and a pattern direction. The direction parameter has a default value.

You’ve also added the required initializer for when a storyboard initializes the view. You’ll need this later on when you transfer your code to the app.

In MyViewController, change the patternView assignment since you’ve changed the initializer:


let patternView = PatternView(
  fillColor: [0.0, 1.0, 0.0, 1.0],
  direction: .right)

Here, you instantiate a pattern view with non-default values.

Run the playground. Your triangles are now green and pointing to the right:

Congratulations! You’re done prototyping your pattern using Playgrounds. It’s time to use this pattern in Recall.

Updating the App Pattern View

Open the Recall starter project. Go to PatternView.swift and copy over the UIBezierPath extension from your playground to the end of the file.

Next, replace the PatternView class in PatternView.swift with the class from your playground.

Note: You can vastly simplify this process by using Xcode’s code folding feature. In the playground, put the cursor just after the opening brace of class PatternView: UIView { and select Editor ▸ Code Folding ▸ Fold from the menu. Triple-click the resulting folded line to select the entire class and press Command-C. In the project, repeat the process to fold and select the class. Press Command-V to replace it.

Build and run the app. You should see something like this:

Something’s not quite right. Your pattern appears to be stuck in default mode. It looks like a new game view isn’t refreshing the pattern views.

Go to GameViewController.swift and add the following to the end of setupPatternView(_:towards:havingColor:):


patternView.setNeedsDisplay()

This prompts the system to redraw the pattern so that it picks up the new pattern info.

Build and run the app. You should now see colors and directions nicely mixed up:

Tap one of the answer buttons and play through the game to check that everything works as expected.

Congratulations on completing Recall! You’ve come a long way from the mind-numbing days of simple paint jobs.

Performance

Core Graphics patterns are really fast. Here are several options you can use to draw patterns:

  1. Using the Core Graphics pattern APIs that you worked through in this tutorial.
  2. Using UIKit wrapper methods such as UIColor(patternImage:).
  3. Drawing the desired pattern in a loop with many Core Graphics calls.

If your pattern is only drawn once, the UIKit wrapper methods is the easiest. Its performance should also be comparable to the lower level Core Graphics calls. An example of this is a background pattern.

Core Graphics can work in a background thread unlike UIKit, which runs on the main thread. Core Graphics patterns are more performant with complicated drawings or dynamic patterns.

Drawing the pattern in a loop is going to be the slowest. A Core Graphics pattern makes the draw call once and caches the results, making it more efficient.

Where to Go From Here?

Access the final project by clicking the Download Materials button at the top or bottom of this tutorial. Use this to check your work if you get stuck.

You should now have a solid understanding of how to use Core Graphics to create patterns. Check out the Patterns and Playgrounds tutorial to learn how you can build patterns using UIKit.

You may also want to read through our Curves and Layers tutorial which focuses on drawing various curves and cloning them using layers.

I hope you enjoyed this tutorial. If you have any comments or questions, please join the forum discussion below!



Source link https://www.raywenderlich.com/232990-core-graphics-tutorial-patterns

LEAVE A REPLY

Please enter your comment!
Please enter your name here