Chapters

Hide chapters

SwiftUI Apprentice

Third Edition · iOS 18 · Swift 5.9 · Xcode 16.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

14. Gestures
Written by Caroline Begbie

Heads up... You’re accessing parts of this content for free, with some sections shown as zgmyjvwot text.

Heads up... You’re accessing parts of this content for free, with some sections shown as mwgolmdib text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Gestures are the main interface between you and your app. You’ve already used the built-in gestures for tapping and swiping, but SwiftUI also provides various gesture types for customization.

When users are new to Apple devices, once they’ve spent a few minutes with iPhone, it becomes second nature to tap, pinch two fingers to zoom or make the element larger, or rotate an element with two fingers. Your app should use these standard gestures. In this chapter, you’ll explore how to drag, magnify and rotate elements with the built-in gesture recognizers.

Standing Tall
Back of the napkin design

In the single card view, you’ll drag around and resize photo and text elements. That’s an opportunity to create a view or a view modifier which takes in any view content and allows the user to drag the view around the screen or pinch to scale and rotate the view. Throughout this chapter, you’ll work towards creating a resizable, reusable view modifier. You’ll be able to use this in any of your future apps.

Creating the Resizable View

To start with, the resizable view will simply show a colored rectangle but, later on, you’ll change it to show any view content.

➤ Open the starter project, which is the same as the previous chapter’s challenge project.

➤ In the Views folder, create a new SwiftUI View file named ResizableView.swift. Replace ResizableView with this code:

struct ResizableView: View {
  // 1
  private let content = RoundedRectangle(cornerRadius: 30.0)
  private let color = Color.red

  var body: some View {
    // 2
    content
      .frame(width: 250, height: 180)
      .foregroundStyle(color)
  }
}

Going through the code:

  1. Create a RoundedRectangle view property. You choose private access here as, for now, no other view should be able to reference these properties. Later on, you’ll change the access to pass in any view.
  2. Use content as the required View in body and apply modifiers to it.

➤ Preview the view, and you’ll see your red rectangle with rounded corners.

Preview rounded rectangle
Preview rounded rectangle

Creating Transforms

Skills you’ll learn in this section: transformation

Heads up... You’re accessing parts of this content for free, with some sections shown as jlzajjfod text.

Heads up... You’re accessing parts of this content for free, with some sections shown as ntvosftem text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
import SwiftUI

struct Transform {
  var size = CGSize(width: 250, height: 180)
  var rotation: Angle = .zero
  var offset: CGSize = .zero
}
@State private var transform = Transform()
.frame(
  width: transform.size.width,
  height: transform.size.height)

Heads up... You’re accessing parts of this content for free, with some sections shown as kwmymsbyh text.

Heads up... You’re accessing parts of this content for free, with some sections shown as rwpybrdim text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Rounded rectangle with transform sizing
Duuxxep kubfaxkki razz tximbtivn samuqw

Creating a Drag Gesture

Skills you’ll learn in this section: drag gesture; operator overloading

var dragGesture: some Gesture {
  DragGesture()
    .onChanged { value in
      transform.offset = value.translation
    }
}
qumyw uzphot (3, 7) heaghl
Utgwaq elf ldummjiniiq

Heads up... You’re accessing parts of this content for free, with some sections shown as jsrenvjar text.

Heads up... You’re accessing parts of this content for free, with some sections shown as tbmudmpyw text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.offset(transform.offset)
.gesture(dragGesture)
Dragging the view
Dmovneym mra beor

@State private var previousOffset: CGSize = .zero
var dragGesture: some Gesture {
  DragGesture()
    .onChanged { value in
      transform.offset = CGSize(
        width: value.translation.width + previousOffset.width,
        height: value.translation.height + previousOffset.height)
    }
    .onEnded { _ in
      previousOffset = transform.offset
    }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as qxtopbtuc text.

Heads up... You’re accessing parts of this content for free, with some sections shown as pxpagbvoc text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Operator Overloading

Operator overloading is where you redefine what operators such as +, -, * and / do.

import SwiftUI

func + (left: CGSize, right: CGSize) -> CGSize {
  CGSize(
    width: left.width + right.width,
    height: left.height + right.height)
}
var dragGesture: some Gesture {
  DragGesture()
    .onChanged { value in
      transform.offset = value.translation + previousOffset
    }
    .onEnded { _ in
      previousOffset = transform.offset
    }
}

Creating a Rotation Gesture

Skills you’ll learn in this section: rotation gesture; preview on device.

Heads up... You’re accessing parts of this content for free, with some sections shown as drficclaz text.

Heads up... You’re accessing parts of this content for free, with some sections shown as nhpapghib text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
@State private var previousRotation: Angle = .zero
var rotationGesture: some Gesture {
  RotationGesture()
    .onChanged { rotation in
      transform.rotation += rotation - previousRotation
      previousRotation = rotation
    }
    .onEnded { _ in
      previousRotation = .zero
    }
}
.rotationEffect(transform.rotation)
.gesture(dragGesture)
.gesture(rotationGesture)
Two fingers to rotate
Lwo kumnuwr do koxajo

Heads up... You’re accessing parts of this content for free, with some sections shown as wmwexpsuk text.

Heads up... You’re accessing parts of this content for free, with some sections shown as sfwivrtuq text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Rotation
Rabiboov

Heads up... You’re accessing parts of this content for free, with some sections shown as mdpufcbew text.

Heads up... You’re accessing parts of this content for free, with some sections shown as hrgeprxod text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Preview on Device
Kcocuuy ox Ceralo

Creating a Scale Gesture

Skills you’ll learn in this section: magnification gesture; simultaneous gestures

@State private var scale: CGFloat = 1.0
var scaleGesture: some Gesture {
  MagnificationGesture()
    .onChanged { scale in
      self.scale = scale
    }
    .onEnded { scale in
      transform.size.width *= scale
      transform.size.height *= scale
      self.scale = 1.0
    }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as rqxitckej text.

Heads up... You’re accessing parts of this content for free, with some sections shown as xlvyzcmis text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.scaleEffect(scale)

Creating a Simultaneous Gesture

Whereas the drag is a specific gesture with one finger, you can do rotation and scale at the same time with two fingers. To do this, change .gesture(rotationGesture) to:

.gesture(SimultaneousGesture(rotationGesture, scaleGesture))
Completed gestures
Xitpbicaf yurduger

Creating Custom View Modifiers

Skills you’ll learn in this section: creating a ViewModifier; View extension; using a view modifier; advantages of a view modifier

struct ResizableView: ViewModifier {
func body(content: Content) -> some View {

Heads up... You’re accessing parts of this content for free, with some sections shown as stvumqqib text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lhsifcryd text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
private let content = RoundedRectangle(cornerRadius: 30.0)
private let color = Color.red
#Preview {
  RoundedRectangle(cornerRadius: 30)
    .foregroundStyle(.blue)
    .modifier(ResizableView())
}
View Modifier preview
Naan Qakiluuj gfopaey

Using Your Custom View Modifier

In the preview, you used .modifier(ResizableView()). You can improve this by adding a “pass-through” method to View.

Heads up... You’re accessing parts of this content for free, with some sections shown as tsxobmryb text.

Heads up... You’re accessing parts of this content for free, with some sections shown as krsajxneb text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
extension View {
  func resizableView() -> some View {
    modifier(ResizableView())
  }
}
.resizableView()
var content: some View {
  ZStack {
    Capsule()
      .foregroundStyle(.yellow)
      .resizableView()
    Text("Resize Me!")
      .font(.largeTitle)
      .fontWeight(.bold)
      .resizableView()
    Circle()
      .resizableView()
      .offset(CGSize(width: 50, height: 200))
  }
}
content
Resize multiple views
Qapuzo giygupzu guodz

Heads up... You’re accessing parts of this content for free, with some sections shown as krqyltsew text.

Heads up... You’re accessing parts of this content for free, with some sections shown as qgtacwjor text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
Resize the text
Kecajo nbu relr

View Modifier Advantage

One advantage of a view modifier over a custom view is that you can apply one modifier to multiple views. If you want the text and the capsule to be a single group, then you can resize them both at the same time.

Group {
  Capsule()
    .foregroundStyle(.yellow)
  Text("Resize Me!")
    .fontWeight(.bold)
    .font(.system(size: 500))
    .minimumScaleFactor(0.01)
    .lineLimit(1)
}
.resizableView()
Grouped Views
Yqaevur Siolf

Other Gestures

  • Tap gesture

Heads up... You’re accessing parts of this content for free, with some sections shown as sjsavgkif text.

Heads up... You’re accessing parts of this content for free, with some sections shown as zppajtdeg text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Type Properties

Skills you’ll learn in this section: type properties; type methods

var currentTheme = Color.red

Swift Dive: Stored Property vs Type Property

To create a type property, rather than a stored property, you use the static keyword.

public struct CGPoint {
  public var x: CGFloat
  public var y: CGFloat
}

extension CGPoint {
  public static var zero: CGPoint {
    CGPoint(x: 0, y: 0)
  }
}
var point = CGPoint(x: 10, y: 10)
point.x = 20
let pointZero = CGPoint.zero  // pointZero contains (x: 0, y: 0)

Heads up... You’re accessing parts of this content for free, with some sections shown as pkqidmvop text.

Heads up... You’re accessing parts of this content for free, with some sections shown as wtzicbhax text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
zeca t j vuarpE z l lioqyH DNGuufp
Xnya plexognt ncaniki

Creating Global Defaults for Cards

Going back to your hard coded size values, you’ll now create a file that will hold all your global constants.

import SwiftUI

struct Settings {
  static let cardSize =
    CGSize(width: 1300, height: 2000)
  static let thumbnailSize =
    CGSize(width: 150, height: 250)
  static let defaultElementSize =
    CGSize(width: 250, height: 180)
  static let borderColor: Color = .blue
  static let borderWidth: CGFloat = 5
}
let settings1 = Settings()
let settings2 = Settings()
enum Settings {

Heads up... You’re accessing parts of this content for free, with some sections shown as phwugqgow text.

Heads up... You’re accessing parts of this content for free, with some sections shown as dlnobgvig text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
extension Settings {
  static let aNewSetting: Int = 0
}
.frame(
  width: Settings.thumbnailSize.width,
  height: Settings.thumbnailSize.height)
var size = CGSize(
  width: Settings.defaultElementSize.width,
  height: Settings.defaultElementSize.height)

Creating Type Methods

As well as static properties, you can also create static methods. To illustrate this, you’ll extend SwiftUI’s built-in Color type. You’ll probably get fairly tired of the gray list of card thumbnails, so you’ll create a method that will give you random colors each time the view refreshes.

import SwiftUI

extension Color {
  static let colors: [Color] = [
    .green, .red, .blue, .gray, .yellow, .pink, .orange, .purple
  ]
}
static func random() -> Color {
  colors.randomElement() ?? .black
}

Heads up... You’re accessing parts of this content for free, with some sections shown as xtrycpmuz text.

Heads up... You’re accessing parts of this content for free, with some sections shown as cqtaqpjol text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.foregroundStyle(Color.random())
Random color
Ragjab wekaj

Challenge

Challenge: Make new View Modifiers

View modifiers are not just useful for reusing views, but they are also a great way to tidy up. You can combine modifiers into one custom modifier. Or, as with the toolbar modifier in SingleCardView, if a modifier has a lot of code in it, save yourself some code reading fatigue, and separate it into its own file.

Heads up... You’re accessing parts of this content for free, with some sections shown as bwbowdwow text.

Heads up... You’re accessing parts of this content for free, with some sections shown as bqmirljaq text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Key Points

  • Custom gestures let you interact with your app in any way you choose. Make sure the gestures make sense. Pinch to scale is standard across the Apple ecosystem, so even though you can, don’t use MagnificationGesture in non-standard ways.
  • You apply view modifiers to views, resulting in a different version of the view. If the modifier requires a change of state, create a structure that conforms to ViewModifier. If the modifier doesn’t require a change of state, you can make code more readable by adding a method to a View extension and use that method to modify a view.
  • static or type properties and methods exist on the type. Stored properties exist per instance of the type. Self, with the initial capital letter, is the way to refer to the type inside itself. self refers to the instance of the type. Apple uses type properties and methods extensively. For example, Color.yellow is a type property.

Where to Go From Here?

By now you should be able to understand a lot of technical jargon. It’s time to check out Apple’s documentation and articles. Adding Interactivity with Gestures is an article that describes updating state during a gesture. Read this article and check your understanding of the topic so far.

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.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as hskalnjot text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now