Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Checklists

Section 2: 12 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

47. Swift Playgrounds, Classes & Structs
Written by Joey deVilla

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

Before we move to the next project, we should take a closer look at structs and how they differ from classes.

For most of this book, the objects that you’ve been creating have been instances of class. With the SwiftUI version of Bullseye, you may have wondered why the code that defines the layout and behaviour of SwiftUI screens were instances of struct instead. Why switch from one type of object to another?

There’s a simple one-word answer: Swift. Like a lot of Xcode’s error messages, this answer is technically correct, but vague, confusing and unsatisfying. Before you proceed with the next project, let’s look more closely at structs and classes.

You may be tempted to skip this chapter and simply jump to the next project. Please don’t — just as classes were a key part of the old way of building iOS apps, structs are a key part not just of the new way of building iOS apps, but of Swift programming in general. There’s some valuable information coming up!

Rather than walk you through a dry technical tour of structs and classes and their similarities and differences, let’s play with them using using an Xcode feature appropriately called playgrounds.

Here’s what you’ll see in this chapter:

  • Playgrounds: Think of them as whiteboards where you can try out code ideas without having to build a whole project first.
  • Classes: You’ve been using them for a while, but it never hurts to review what you’ve learned, and you’ll learn a little bit about inheritance while you’re at it.
  • Structs: They’re another way to create objects, and when combined with protocols, they’re pretty powerful.
  • When to use structs and when to use classes: There’s one that you should use more often. I’ll tell you which one, and why.

Playgrounds

A playground is a type of Xcode project that lets you experiment with Swift code and see the results immediately. Think of it as a “scratchpad” where you can try out a new idea before putting it in one of your projects, or as a way of learning about an unfamiliar keyword or feature.

Those of you who have used languages like JavaScript, Python or Ruby with a REPL (Read-Evaluate-Print Loop) where you can type in a single line of code and see immediate results will see that playgrounds are a similar form of tool, yet considerably more powerful. We’ll only scratch the surface of what playgrounds can do in this chapter.

Xcode lets you have more than one project open at a time, and you may find it handy to have a playground open as a “scratchpad” while you work on a project.

Let’s create a playground, which we’ll use to explore classes and structs.

➤ In Xcode’s File menu, select New…, and then Playground. You’ll see a pop-up where you select options for the playground you want to create.

Options for creating a new playground
Options for creating a new playground

I’ve found that the blank macOS playground is the one best suited for experimenting with Swift. That’s because it doesn’t load all the extra material that iOS and tvOS programming require, and it crashes less often.

➤ In the pop-up, select macOS, highlight the Blank playground type, then click Next. You’ll see a Save As: dialog:

Choosing a place to save the playground
Choosing a place to save the playground

➤ Enter a name for the playground (I used Structs and classes; you can use whatever you like). In the Add to: menu, select Don’t add to any project or workspace. Once you’ve done that, click the Create button.

Xcode will create a new playground, which will look like this:

The newly-created Xcode playground
The newly-created Xcode playground

You can see what all the code up to and including a particular line in the playground does by moving the cursor over its line number and pressing the “Play” button that appears. The results will appear in the live view sidebar on the right.

➤ Move the cursor over the number for line 3 — var str = "Hello, playground". A “Run” button will appear and replace the number:

Running a line of code in the playground
Running a line of code in the playground

➤ Click the “Run” button. Every line of code in the playground, up to and including the line for which you clicked the “Run” button will be executed.

You should see the result of line 3 — “Hello, playground” — appear in the live view sidebar:

The result from running a line of code in the playground
The result from running a line of code in the playground

To the right of the result of line 3, you’ll see a rectangular button. This is the “Show result” button. If you move the cursor so that it’s both within the live view sidebar and line 3, a second button shaped like an eye will appear to the left of the “Show result” button. This is the “Quick view” button:

The “Quick look” and “Show result” buttons
The “Quick look” and “Show result” buttons

Clicking the “Show result” button for a line of code in the playground causes the result for that line of code to be constantly displayed below the line:

The line of code, after clicking its “Show result” button
The line of code, after clicking its “Show result” button

Clicking the “Quick look” button for a line of code in the playground causes a pop-up containing the result for that line of code to appear. It disappears as soon as you click anywhere else on the screen:

The line of code, after clicking its “Quick look” button
The line of code, after clicking its “Quick look” button

In addition to the live view sidebar, playgrounds also have a debug console at the bottom of the screen. You can used print() statements to output text to the debug console.

Add the following line to the playground:

print("str contains: \(str)")

➤ Move the cursor over the line number of the line you just entered and click the “Run” button. You should see the output of the print() statement in the debug console at the bottom of the playground:

Printing in the playground
Printing in the playground

Now that we’ve covered the basics of playgrounds, let’s experiment with structs and classes.

Classes

Let’s review classes by creating a class that represents cats.

import Cocoa

class CatClass {
  var name: String
  var weight: Double  // In kilograms, just to be
                      // cientific or international
                      // (take your pick)

  init(name: String, weight: Double) {
    self.name = name
    self.weight = weight
  }

  func report() {
    print("\(name) weighs \(weight) kilograms.")
  }

  func fatten() {
    print("Fattening \(name)...")
    weight += 0.5
    report()
  }
}
var classCat1 = CatClass(name: "Anastasia", weight: 2.5)
let classCat2 = CatClass(name: "Bao", weight: 6.3)
classCat1.report()
classCat2.report()
The first result of the new cat class
Ffu codbh mivufn am yvo fij qun xyibg

Constant and variable class instances

classCat1, representing the cat named Anastasia, was declared with the var keyword, which means that it’s a variable. This means that you can assign another instance of CatClass to it.

classCat1 = CatClass(name: "Cuddles", weight: 8.0)
classCat1.report()
classCat2 = CatClass(name: "Dmitry", weight: 4.7)
An error from trying to reassign to a constant
Os igsil whuz svvulf ve tausxowj pe o sizczary

//classCat2 = CatClass(name: "Dmitry", weight: 4.7)

Changing properties of constant and variable class instances

Let’s try changing the properties of both our cats. We’ll start with classCat1, the variable catClass instance. We’ll change its name property directly and use the fatten() method to change its weight.

classCat1.name = "Esmerelda"
classCat1.fatten()
classCat2.name = "Faiza"
classCat2.fatten()

Classes are reference types

When you assign an instance of a class to a variable or constant, that variable or constant doesn’t contain the instance itself. Instead, it contains a reference to the instance. Think of references as being like someone’s email address — it’s not the person, but a way to reach that person.

Declaring a variable class reference
Pimnakexk a dikiuqne bqokp yegaherli

Changing a variable class reference
Gqugyujq u dibiansi ybett hunolafsa

Declaring a constant class reference
Gapzopawt u lekbvotb fbisn putedeqpu

What happens when you assign a class instance variable to another class instance variable?

So far, we’ve been assigning class instances to variables and constants. What happens when you assign a class instance variable to another class instance variable? Let’s find out by creating two new class instances.

var classCat3 = CatClass(name: "Imelda", weight: 6.1)
var classCat4 = CatClass(name: "Jasmine", weight: 2.2)
classCat3.report()
classCat4.report()
Creating two class references and instances
Ngaiyezn qmo gcowg peyivejxak erc udpmekcob

classCat4 = classCat3
classCat3.report()
classCat4.report()
classCat3.name = "Kenji"
classCat3.report()
classCat4.report()
Setting one class reference to be the same as another
Dakzaln eko zwozr jinidilwu to hu xnu remu ey anogxuz

Changing a property of a class instance that has two references
Lkitcapz i qqisarzx ic u vxeph azjnekso sbuq bud tge luyopumpic

classCat4.fatten()
classCat3.report()
classCat4.report()

Classes and inheritance

One of the first things that many books on object-oriented programming will tell you about classes is that they can inherit from other classes. Inheritance lets you build a class using another class, creating a more specialized class in the process.

class RoboCat: CatClass {
  var laserEnergy: Int

  init(name: String, weight: Double, laserEnergy: Int) {
    self.laserEnergy = laserEnergy
    super.init(name: name, weight: weight)
  }

  func fireLaser() {
    if laserEnergy > 0 {
      print("\(name) fires a laser. Pew! Pew!")
      laserEnergy -= 1
    } else {
      print("No energy to fire laser.")
    }
  }

  override func report() {
    print("\(name) weighs \(weight) kilograms and has \(laserEnergy) units of laser energy.")
  }
}
class RoboCat: CatClass {
var laserEnergy: Int
var name: String
var weight: Double  // In kilograms, just to be
                    // be scientific or international
                    // (take your pick)
init(name: String, weight: Double, laserEnergy: Int) {
  self.laserEnergy = laserEnergy
  super.init(name: name, weight: weight)
}
func fireLaser() {
  if laserEnergy > 0 {
    print("\(name) fires a laser. Pew! Pew!")
    laserEnergy -= 1
  } else {
    print("No energy to fire laser.")
  }
}
override func report() {
  print("\(name) weighs \(weight) kilograms and has \(laserEnergy) units of laser energy.")
}
let classCat5 = RoboCat(name: "FELINE SECURITY UNIT", weight: 20.0, laserEnergy: 10)
classCat5.fireLaser()
classCat5.fatten()

Structs

At first glance, Structs look a lot like classes. They also function as “blueprints” that you can use to create objects or instances with properties and methods. You access a struct’s properties and methods in the same way you do so with a class.

struct CatStruct {
  var name: String
  var weight: Double  // In kilograms, just to be
                      // be scientific or international
                      // (take your pick)
}
var structCat1 = CatStruct(name: "Latifah", weight: 3.9)
Declaring a constant class reference
Qebhubiyf a zovcparz lruyb qowadiqvi

Showing structCat1’s properties
Ccogokm qxvatwMen2’t trixabgiap

structCat1.name = "Mongo"
structCat1.weight = 10.0 // Mongo likes candy!
Showing structCat1’s properties
Rzamilb qcreqrJac5’j cmiwohrait

struct CatStruct {
  var name: String
  var weight: Double  // In kilograms, just to be
                      // be scientific or international
                      // (take your pick)

  func report() {
    print("\(name) weighs \(weight) kilograms.")
  }
}
structCat1.report()
struct CatStruct {
  var name: String
  var weight: Double  // In kilograms, just to be
                      // be scientific or international
                      // (take your pick)

  func report() {
    print("\(name) weighs \(weight) kilograms.")
  }

  func fatten() {
    print("Fattening \(name)...")
    weight += 0.5
    report()
  }
}
Xcode displays an error when you add the method
Ktapi wiqryotm iw uxvet frad zio akx jzo kahboh

Mutating struct functions

Let’s first make sure that we haven’t gone crazy. Comment out CatStruct’s fatten() method for now.

struct CatStruct {
  var name: String
  var weight: Double  // In kilograms, just to be
                      // be scientific or international
                      // (take your pick)

  func report() {
    print("\(name) weighs \(weight) kilograms.")
  }

//  func fatten() {
//    print("Fattening \(name)...")
//    weight += 0.5
//    report()
//  }
}
structCat1.weight += 0.5
structCat1.report()
mutating func fatten() {
  print("Fattening \(name)...")
  weight += 0.5
  report()
}
structCat1.fatten()

Structs are value types

One really big difference between structs and classes is that while classes are reference types, structs are value types. When you assign a struct instance to a variable, you’re putting the struct into that variable, not a reference to the struct.

var structCat2 = structCat1
structCat2.report()

structCat2.name = "Naveen"
structCat2.weight = 5.3
structCat1.report()
structCat2.report()
Setting one struct instance to be equal to another
Namfasl oma slyads ijmyarse li vi iliut li igilnag

Value types are always independent
Yupoi cppur ofu abkatr uyposupzayb

let structCat3 = CatStruct(name: "Orson", weight: 5.5)
structCat3.fatten()
Xcode displays an error when you add the method
Mbuwi jablbibq ub okyaq hsos laa ayh tke kovkeq

//let structCat3 = CatStruct(name: "Orson", weight: 5.5)
//structCat3.fatten()

Structs and protocols

Structs don’t support inheritance. They do support protocols (and so do classes and enums), which were described earlier in this book as being like job ads, in that they list the things that a candidate for a certain job should be able to do, but don’t specify how the candidate will perform those tasks.

protocol Pet {
  var name: String { get set }
  var weight: Double { get set }

  func report() -> ()
  mutating func fatten() -> ()
}
var name: String { get set }
var weight: Double { get set }
func report() -> ()
mutating func fatten() -> ()
struct ProtocolCat: Pet {
  var name: String
  var weight: Double

  func report() {
    print("\(name) weighs \(weight) kilograms.")
  }

  mutating func fatten() {
    print("Fattening \(name)...")
    weight += 0.5
    report()
  }
}
var structCat4 = ProtocolCat(name: "Pasquale", weight: 7.7)
structCat4.report()
structCat4.fatten()
struct ProtocolCat: Pet {
  var name: String
  var weight: Double
}

extension Pet {
  func report() {
    print("\(name) weighs \(weight) kilograms.")
  }

  mutating func fatten() {
    print("Fattening \(name)...")
    weight += 0.5
    report()
  }
}
struct Dog: Pet {
  var name: String
  var weight: Double

  func fetch() {
    print("You throw a ball, and \(name) gets it and brings it back to you.")
  }
}
var myDog = Dog(name: "Quincy", weight: 9.4)
myDog.report()
myDog.fatten()
myDog.fetch()
protocol LaserEquipped {
  var laserEnergy: Int { get set }

  mutating func fireLaser() -> ()
}

extension LaserEquipped {
  mutating func fireLaser() {
    if laserEnergy > 0 {
      print("Firing laser. Pew! Pew!")
      laserEnergy -= 1
    } else {
      print("No energy to fire laser.")
    }
  }
}
struct LaserCat: Pet, LaserEquipped {
  var name: String
  var weight: Double
  var laserEnergy: Int
}

var laserKitty = LaserCat(name: "Renoir", weight: 20.0, laserEnergy: 20)
laserKitty.report()
laserKitty.fatten()
laserKitty.fireLaser()

struct LaserDog: Pet, LaserEquipped {
  var name: String
  var weight: Double
  var laserEnergy: Int

  func fetch() {
    print("You throw a ball, and \(name) gets it and brings it back to you.")
  }
}

var laserPuppy = LaserDog(name: "Salieri", weight: 20.0, laserEnergy: 20)
laserPuppy.report()
laserPuppy.fatten()
laserPuppy.fireLaser()
laserPuppy.fetch()
struct Hamster: Pet {
  var name: String
  var weight: Double
  var isOnHamsterWheel: Bool

  func report() {
    let wheelStatus = isOnHamsterWheel ? "on" : "not on"
    print("\(name) weighs \(weight) kilograms, and is \(wheelStatus) its hamster wheel.")
  }
}

var myHamster = Hamster(name: "Tetsuo", weight: 0.1, isOnHamsterWheel: true)
myHamster.report()

When to use structs and when to use classes

Swift has opinions

Sooner or later, when you’re searching the web for information about programming, you’re going to see programming languages and frameworks being described as “opinionated”. It’s actually the creators and maintainers who have opinions about the way programmers should do things, and they’ve designed their languages and frameworks so that there are “right” or “approved” ways of doing things when using them. Programming languages vary in how opinionated they are, and Swift is on the more opinionated side of the spectrum.

superKitty = LaserCat("Uma", 20.0, 20)
superKitty = LaserCat(name: "Uma", weight: 20.0, laserEnergy: 20)

Swift’s opinion: Use structs by default

Just as Swift’s (or more accurately, its creators and maintainers) opinion is that you should use constants by default and variables only when necessary, it’s also the prevailing opinion that you should use structs by default and classes only when necessary.

Structs are easier to reason about

The fact that structs are value types and not reference types makes it easier to think about their logic, which makes them less error-prone as a result. Consider the case where you want to see if two string values are equal. This is easy in Swift, where strings are structs, and therefore value types:

if string1 == string2 {
  // The rest of the code goes here
}
if string1.equals(string2) {
  // The rest of the code goes here
}

Another Swift opinion: Favor protocol-oriented programming over object-oriented programming

In the playground exercise, you built created a laser-equipped robot cat class, RoboCat, by subclassing — that is, inheriting from — the Cat class. You could also make a flying cat class that has properties and methods related to flying by subclassing the Cat class.

Structs provide other benefits

As value types, you put struct instance directly into a constant or variable. There’s a little more work involved in storing a class instance; the constant or variable holding it doesn’t directly store the instance, but the location in memory where the instance can be found. This level of indirection means that the system has to do extra work when accessing class instances. Working with large numbers of class instances can be slower the doing the same work with struct instances — in some cases, many orders of magnitude slower.

So when should you use classes?

Use classes when you need the capabilities that only a class can provide. Here are some of the more likely cases where you’ll need to use classes instead of structs:

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