Chapters

Hide chapters

SwiftUI by Tutorials

Second Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Building Blocks of SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

13. Drawing & Custom Graphics
Written by Bill Morefield

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

As you begin to develop more complex apps, you’ll find that you need more flexibility or flash than the built-in controls of SwiftUI offer. Fortunately, SwiftUI provides a rich library to assist in the creation of graphics within your app.

Graphics convey information to the user efficiently and understandably; for instance, you can augment text that takes time to read with graphics that summarizes the same information.

In this chapter, you’ll explore the use of graphics in SwiftUI by creating several award graphics for the airport app.

Creating shapes

Open the starter project for this chapter; build and run the project in Xcode and you’ll see an early, in-progress app for a small airport, showing flight boards for arrivals and departures. These function as the in-app equivalent to the large-screen displays that show flights arriving and leaving from the airport.

You’ll also see a page to display a user’s award badges. In this chapter, you’ll create three initial awards. The first badge you’ll create is awarded the first time someone comes to the airport, and will look like this when you’re done:

First up, create a new SwiftUI View named FirstVisitAward.swift. Then, open the new file and, if the preview doesn’t show, select Editor ▸ Editor and Canvas to show it. The preview view will make the iterative process of creating drawings and animations much easier.

Open AirportAwards.swift and replace the view code with the following to add it to the view:

VStack {
  ScrollView {
    FirstVisitAward()
      .frame(width: 250, height: 250)
    Text("First Visit")
  }
}
.navigationBarTitle("Your Awards")

One of the basic drawing structures in SwiftUI is the Shape, which is a set of simple primitives you use to build up more complex drawings.

First, you’ll need to add a rectangle shape to the SwiftUI view. To do this, replace the default Text from the view template’s body with the following code:

Rectangle()

The preview is a little underwhelming since all you have is a black rectangle that fills the screen. By default, a shape in SwiftUI fills the entirety of its container, but you can specify a smaller container for the preview.

Add the following line below Rectangle() in the view to set its size:

.frame(width: 200, height: 200)

You will now see a black square, 200 points on each side. in the middle of the view.

This view demonstrates a few defaults that apply when drawing in SwiftUI. If you don’t make an explicit fill or stroke call, the shape will fill with the current foreground color, which is Color.primary. You’ll get one color for Color.primary when your app runs in light mode, and a different color for that same variable when running in dark mode. Although that looks good in light mode, it’s a good idea to always consider how your drawings will appear under dark mode.

Below the frame method in the preview, add the following line:

.environment(\.colorScheme, .dark)

In dark mode, Color.primary is white, so now you should see a white square against the black background on the Canvas. But you won’t. The square turns white, but because of a bug still present in Xcode 11.4, the background doesn’t change color. As a workaround, you need to wrap the preview inside a NavigationView. Change the code for the preview so you can preview both light and dark mode as follows:

struct FirstVisitAward_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      FirstVisitAward()
        .environment(\.colorScheme, .light)

      NavigationView {
        FirstVisitAward()
          .environment(\.colorScheme, .dark)
      }
    }
  }
}

Then, remove .environment(\.colorScheme, .dark) from the body again. You should now see two previews; one with light mode and one with dark mode.

It’s easy to change the color of the fill. Back in your view, add the following between the Rectangle() and frame(width:height:) lines:

.fill(Color.blue)

Build

Providing a color overrides the default: The square fills with blue in both light and dark modes. Note that order matters here, as you must call fill before the frame. You could also use the border(_:width:) to outline the shape instead of filling it.

Using gradients

A solid color fill works well for many cases, but for this badge, you’ll use a gradient fill instead to provide a smooth transition between two or more colors.

.fill(
  LinearGradient(gradient: .init(colors: [Color.green, Color.blue]),
                 startPoint: .bottomLeading,
                 endPoint: .topTrailing
))

Rotating shapes

You could repeat the code to draw the square three times, and rotate two of the shapes. However, SwiftUI provides a more general way to do this — the ForEach() method.

// 1
ZStack {
  // 2
  ForEach(0..<3) { i in
    Rectangle()
      .fill(
        LinearGradient(gradient: .init(colors: [Color.green,
                                                Color.blue]),
                       startPoint: .bottomLeading,
                       endPoint: .topTrailing)
      )
      .frame(width: 200, height: 200)
      // 3
      .rotationEffect(.degrees(Double(i) * 60.0))
    }
}

Adding images

Mixing prebuilt images with your drawings can save a lot of time and work. The airplane image for this award is from the new set of SF Symbols in iOS 13. Add the following code after the ForEach loop:

Image(systemName: "airplane")

Group {
  FirstVisitAward()
    .environment(\.colorScheme, .light)
    .frame(width: 200, height: 200)

  FirstVisitAward()
    .environment(\.colorScheme, .dark)
    .frame(width: 200, height: 200)  
}
.resizable()
.rotationEffect(.degrees(-90))
.opacity(0.5)

Scaling drawings in views

The badge looks pretty good right now, but there’s a subtle bug you might not have noticed. To see it, you’ll need to add the award to a view.

// 1
GeometryReader { geometry in
  ZStack {
    ForEach(0..<3) { i in
      Rectangle()
        .fill(
          LinearGradient(
            gradient: .init(colors: [Color.green, Color.blue]),
            startPoint: .bottomLeading,
            endPoint: .topTrailing)
          )
      // 2
      .frame(width: geometry.size.width * 0.7,
             height: geometry.size.width * 0.7)
      .rotationEffect(.degrees(Double(i) * 60.0))
    }
    Image(systemName: "airplane")
      .resizable().rotationEffect(.degrees(-90))
      .opacity(0.5)
      // 3
      .scaleEffect(0.7)
  }
}

Other shapes

You only used one shape for this first award, but SwiftUI provides several more shapes:

Drawing lines with paths

Sometimes you want to define your own shape, and not use the built-in ones. For this, you use Paths, which allow you to define a shape by combining individual segments. These segments make up the outline of a two-dimensional shape. You’ll create your next award using paths.

OverNightParkAward()
  .frame(width: 250, height: 250)
Text("Left Car Overnight")

The simplest element that you can add to a path is the line. This award uses lines to draw a road.


![bordered width=30%](images/overnight-parking-award.png)

Go back to **OverNightParkAward.swift**. First up, update the new view’s `body` to:

```swift
Path { path in
  path.move(to: CGPoint(x: 120, y: 20))
  path.addLine(to: .init(x: 180, y: 180))
  path.addLine(to: .init(x: 20, y: 180))
  path.addLine(to: .init(x: 80, y: 20))
}

.frame(width: 200, height: 200)
GeometryReader { geometry in
  Path { path in
    let size = min(geometry.size.width, geometry.size.height)
    let nearLine = size * 0.1
    let farLine = size * 0.9

    path.move(to: CGPoint(x: size/2 + nearLine, y: nearLine))
    path.addLine(to: .init(x: farLine, y: farLine))
    path.addLine(to: .init(x: nearLine, y: farLine))
    path.addLine(to: .init(x: size/2 - nearLine, y: nearLine))
  }
}
.fill(Color.init(red: 0.4, green: 0.4, blue: 0.4))

Drawing dashed lines

Next, you’ll add a dashed white line down the middle of the road. First, wrap the current Path inside a ZStack like so:

GeometryReader { geometry in
  ZStack {
    // Your current Path view
    // ...
  }
}
Path { path in
  let size = min(geometry.size.width, geometry.size.height)
  let nearLine = size * 0.1
  let farLine = size * 0.9
  let middle = size / 2

  path.move(to: .init(x: middle, y: farLine))
  path.addLine(to: .init(x: middle, y: nearLine))
}
.stroke(Color.white,
        style: .init(lineWidth: 3.0,
        dash: [geometry.size.height / 20,
              geometry.size.height / 30],
        dashPhase: 0))

Image(systemName: "car.fill")
  .resizable()
  .foregroundColor(Color.blue)
  .scaleEffect(0.20)
  .offset(x: -geometry.size.width / 7.25)


Build and run the app, go to Awards and you should see your two stylish awards.

![bordered width=30%](images/second-awards.png)

## Drawing arcs and curves

Paths offer more flexibility than drawing lines. You’ll find a wide range of options, including shapes that are better suited for drawing curved objects. You’ll create the next award using arcs and quadratic curves.

![width=30%](images/food-award.png)

As with the previous awards, start by creating a new **SwiftUI View** and name it **AirportMealAward.swift**.

Now, open **AirportAwards.swift** and add the following to the end of the view to add it to the collection of awards:

```swift
AirportMealAward()
  .frame(width: 250, height: 250)
Text("Ate Meal at Airport")
.frame(width: 200, height: 200)
GeometryReader { geometry in
  ZStack {
    Path { path in
      let size = min(geometry.size.width, geometry.size.height)
      let nearLine = size * 0.1
      let farLine = size * 0.9
      let mid = size / 2
    }
  }
}

Drawing quadratic curves

The name of a quadratic curve comes from its definition by following the line plot of a quadratic math equation. SwiftUI handles the math part (phew!) so you can simply think of a quadratic curve as an elastic line pulled toward a third point, known as the control point. At each end, the curve starts parallel to a line drawn to the control point and curves smoothly between all points.

path.move(to: .init(x: mid, y: nearLine))
path.addQuadCurve(
  to: .init(x: farLine, y: mid),
  control: .init(x: size, y: 0))
path.addQuadCurve(
  to: .init(x: mid, y: farLine),
  control: .init(x: size, y: size))
path.addQuadCurve(
  to: .init(x: nearLine, y: mid),
  control: .init(x: 0, y: size))
path.addQuadCurve(
  to: .init(x: mid, y: nearLine),
  control: .init(x: 0, y: 0))

.fill(
  RadialGradient(
    gradient: .init(colors: [Color.white, Color.yellow]),
    center: .center,
    startRadius: geometry.size.width * 0.05,
    endRadius: geometry.size.width * 0.6)
)

Path { path in
  let size = min(geometry.size.width, geometry.size.height)
  let nearLine = size * 0.1
  let farLine = size * 0.9

  path.addArc(center: .init(x: nearLine, y: nearLine),
              radius: size / 2,
              startAngle: .degrees(90),
              endAngle: .degrees(0),
              clockwise: true)
  path.addArc(center: .init(x: farLine, y: nearLine),
              radius: size / 2,
              startAngle: .degrees(180),
              endAngle: .degrees(90),
              clockwise: true)
  path.addArc(center: .init(x: farLine, y: farLine),
              radius: size / 2,
              startAngle: .degrees(270),
              endAngle: .degrees(180),
              clockwise: true)
  path.addArc(center: .init(x: nearLine, y: farLine),
              radius: size / 2,
              startAngle: .degrees(0),
              endAngle: .degrees(270),
              clockwise: true)
  path.closeSubpath()
}
.stroke(Color.orange, lineWidth: 2)

Fixing performance problems

By default, SwiftUI renders graphics and animations using CoreGraphics. SwiftUI draws each view individually on the screen when needed. The processor and graphics hardware inside modern Apple devices are powerful and can handle many views without the user seeing a slowdown. At some point, however, you can overload the system and see performance drop off to the point a user notices, and your app seems sluggish.

Key points

  • Shapes provide a quick way to draw simple controls. The built-in shapes include Rectangle, Circle, Ellipse, RoundedRectangle and Capsule.
  • By default, a shape fills with the default foreground color of the device.
  • Shapes can be filled with solid colors or with a defined gradient.
  • Gradients can transition in a linear, radial, or angular manner.
  • rotationEffect will rotate a shape around its axis.
  • ZStack will let you combine graphics so they share a common axis. You can mix drawn graphics and images.
  • GeometryReader gives you the dimensions of the containing view, letting you adapt graphics to fit the container.
  • Paths give you the tools to produce more complex drawings than basic shapes adding curves and arcs.
  • You can modify the shapes and fill on paths as you do with shapes.
  • The drawingGroup() can improve performance of graphics-heavy views, but should only be added when performance problems appear as it can slow rendering of simple graphics.

Where to go from here?

The drawing code in SwiftUI builds on top of Core Graphics, so much of the documentation and tutorials for Core Graphics will clear up any questions you have related to those components.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now