100 Days of SwiftUI Learning — Day 12

Krishna
8 min readMay 12, 2022

--

Agenda

  1. Create your own classes
  2. Class inheritance
  3. Add initializers for classes
  4. Copy classes
  5. Create deinitializer for a class
  6. Working with variables inside classes
  7. Summary: Classes
  8. Checkpoint: 7

Create your own classes

Structs lets us create custom data types in swift. There is another way to create custom data types called classes. Let us compare them first.

Common things between classes and structs

  1. You get to create and name them.
  2. You can add properties and methods, including property observers and access control.
  3. You can create custom initializers to configure new instances however you want.

Key differences

  1. You can make one class build upon functionality in another class, gaining all its properties and methods as a starting point. If you want to selectively override some methods, you can do that too.
  2. Because of that first point, Swift won’t automatically generate a memberwise initializer for classes. This means you either need to write your own initializer, or assign default values to all your properties.
  3. When you copy an instance of a class, both copies share the same data — if you change one copy, the other one also changes.
  4. When the final copy of a class instance is destroyed, Swift can optionally run a special function called a deinitializer.
  5. Even if you make a class constant, you can still change its properties as long as they are variables.

Basic class example

class Game {
var score = 0 {
didSet {
print("Score is now \(score)")
}
}
}

var newGame = Game()
newGame.score += 10

For now, it may seem that there is literally no difference between a struct and a class except for the keyword class . We will learn about all 5 differences pointed above in next chapters.

Class Inheritance

Swift lets us create class by basing them on existing classes which is a process known as inheritance.

Swift gives the child class (new class), access to properties and methods from the parent class.

Parent Class

class Employee {
let hours: Int

init(hours: Int) {
self.hours = hours
}
}

Child Class

To create a child class, use a colon after the child class name and then parent class name. (Child Class : Parent Class).

class Developer: Employee {
func work() {
print("I'm writing code for \(hours) hours.")
}
}

class Manager: Employee {
func work() {
print("I'm going to meetings for \(hours) hours.")
}
}

We can access the property hours of class Employee. Each of these child classes inherit from Employee, but each adds their own customization. That is why, work() behaves differently when you create their instances and class the method.

let robert = Developer(hours: 8)
let joseph = Manager(hours: 10)
robert.work()
joseph.work()

Child classes can inherit methods from parent class. For example, add the following method to the parent class Employee.

func printSummary() {
print("I work \(hours) hours a day.")
}
let novall = Developer(hours: 8)
novall.printSummary() // "I work 8 hours a day.
novall.work() // I'm writing code 8 hours.

override

If we want to change the functionality of a method in a child class, you should use the keyword override in the child class. Override does two things.

  1. If you are changing a method, code doesn’t build until you use the keyword override .
  2. If you are using the keyword for a method that doesn’t exist in parent class, code doesn’t build as you are not overriding anything.

It is important to remember that swift handles the use of parameters and return values very smart. Remember, how two functions with different parameters are treated differently by swift, a similar concept is applied in classes as well.

Let me explain it by using an example. If you are creating a method printSummary(place:String) inside a parent class, you don’t have to use the keyword override as you aren’t replacing the parent method. See the below attached screenshot.

Add initializers for classes

Swift will not generate a member wise initializer for you. You either need to write you own initializer or provide default values for all your parameters.

Let us see this by using an example.

Parent Class

class Vehicle {
let isElectric : Bool

init(isElectric:Bool) {
self.isElectric = isElectric
}
}

Child Class

class Car: Vehicle {
let isConvertible: Bool

init(isConvertible: Bool) {
self.isConvertible = isConvertible
}
}

This is going to be a problem because, we haven’t provided a value for isElectric in child class. Rather than trying to store isElectric ourselves we instead need to pass it and ask super class to run its own initializer.

class Car: Vehicle {
let isConvertible: Bool

init(isElectric: Bool, isConvertible: Bool) {
self.isConvertible = isConvertible
super.init(isElectric: isElectric)
}
}
let teslaX = Car(isElectric: true, isConvertible: false)

Copy classes

All copies of a class instance share the same data. That means any changes we make to one copy, applies to all other copies. This happens because classes in swift are reference types in swift, which means all copies of a class refer back to same data.

class User {
var username = "Anonymous"
}
var user1 = User()
var user2 = user1

This property is shared across all copies of the class. Now, let us change the username property value for user2 instance user2.username = "Krishna" . If we try to print the value of username using user1.username it will print “Krishna” and not “Anonymous”.

If you want to create a unique copy of a class instance — sometimes called a deep copy — you need to handle creating a new instance and copy across all your data safely.

In our case that’s straightforward:

class User {
var username = "Anonymous"
func copy() -> User {
let user = User()
user.username = username
return user
}
}

Now we can safely call copy() to get an object with the same starting data, but any future changes won’t impact the original.

Create deinitializer for a class

  1. Just like initializers, you don’t use func with deinitializers – they are special.
  2. Deinitializers can never take parameters or return data, and as a result aren’t even written with parentheses.
  3. Your deinitializer will automatically be called when the final copy of a class instance is destroyed. That might mean it was created inside a function that is now finishing, for example.
  4. We never call deinitializers directly; they are handled automatically by the system.
  5. Structs don’t have deinitializers, because you can’t copy them.

Exactly when your deinitializers are called depends on what you’re doing, but really it comes down to a concept called scope. When a value exits scope we mean the context it was created in is going away. In the case of structs that means the data is being destroyed, but in the case of classes it means only one copy of the underlying data is going away — there might still be other copies elsewhere. But when the final copy goes away — when the last constant or variable pointing at a class instance is destroyed — then the underlying data is also destroyed, and the memory it was using is returned back to the system.

To demonstrate this, we could create a class that prints a message when it’s created and destroyed, using an initializer and deinitializer:

class User {
let id: Int
init(id: Int) {
self.id = id
print("User \(id): I'm alive!")
}
deinit {
print("User \(id): I'm dead!")
}
}

Now we can create and destroy instances of that quickly using a loop — if we create a User instance inside the loop, it will be destroyed when the loop iteration finishes:

for i in 1...3 {
let user = User(id: i)
print("User \(user.id): I'm in control!")
}

When that code runs you’ll see it creates and destroys each user individually, with one being destroyed fully before another is even created as the scope of the user is completed.

For example, if we were adding our User instances as they were created, they would only be destroyed when the array is cleared:

var users = [User]()for i in 1...3 {
let user = User(id: i)
print("User \(user.id): I'm in control!")
users.append(user)
}
print("Loop is finished!")
users.removeAll()
print("Array is clear!")

Working with variables insides classes

class User {
var name = "Paul"
}

let user = User()
user.name = "Taylor"
print(user.name)

That creates a constant User instance, but then changes it – it changes the constant value. That’s bad, right?

Except it doesn’t change the constant value at all. Yes, the data inside the class has changed, but the class instance itself – the object we created – has not changed, and in fact can’t be changed because we made it constant.

In contrast, what happens if we made both the user instance and the name property variables? Now we’d be able to change the property, but we’d also be able to change to a wholly new User instance if we wanted. To continue the signpost analogy, it would be like turning the signpost to point at wholly different person.

Try it with this code:

class User {
var name = "Paul"
}
var user = User()
user.name = "Taylor"
user = User()
print(user.name)

That would end up printing “Paul”, because even though we changed name to “Taylor” we then overwrote the whole user object with a new one, resetting it back to “Paul”.

The final variation is having a variable instance and constant properties, which would mean we can create a new User if we want, but once it’s done we can’t change its properties.

So, we end up with four options:

  1. Constant instance, constant property — a signpost that always points to the same user, who always has the same name.
  2. Constant instance, variable property — a signpost that always points to the same user, but their name can change.
  3. Variable instance, constant property — a signpost that can point to different users, but their names never change.
  4. Variable instance, variable property — a signpost that can point to different users, and those users can also change their names.

Summary: Classes

  • Classes have lots of things in common with structs, including the ability to have properties and methods, but there are five key differences between classes and structs.
  • First, classes can inherit from other classes, which means they get access to the properties and methods of their parent class. You can optionally override methods in child classes if you want, or mark a class as being final to stop others subclassing it.
  • Second, Swift doesn’t generate a memberwise initializer for classes, so you need to do it yourself. If a subclass has its own initializer, it must always call the parent class’s initializer at some point.
  • Third, if you create a class instance then take copies of it, all those copies point back to the same instance. This means changing some data in one of the copies changes them all.
  • Fourth, classes can have deinitializers that run when the last copy of one instance is destroyed.
  • Finally, variable properties inside class instances can be changed regardless of whether the instance itself was created as variable.

Checkpoint: 7

Challenge

Make a class hierarchy for animals, starting with Animal at the top, then Dog and Cat as subclasses, then Corgi and Poodle as subclasses of Dog, and Persian and Lion as subclasses of Cat.

But there’s more:

  1. The Animal class should have a legs integer property that tracks how many legs the animal has.
  2. The Dog class should have a speak() method that prints a generic dog barking string, but each of the subclasses should print something slightly different.
  3. The Cat class should have a matching speak() method, again with each subclass printing something different.
  4. The Cat class should have an isTame Boolean property, provided using an initializer.

My Solution

import Cocoaclass Animal {
var legs : Int
init(legs:Int){
self.legs = legs
}
}
class Dog:Animal {
func speak() {
print("The dog is barking")
}
}
class Corgi : Dog {
override func speak() {
print("Corgi barks")
}
}
class Poodle: Dog {
override func speak() {
print("Poodle barks")
}
}
class Cat:Animal {
let isTame : Bool
func speak(){
print("Cat says meow")
}
init(legs: Int,isTame: Bool) {
self.isTame = isTame
super.init(legs: legs)
}
}
class Persian: Cat {
override func speak() {
print("Persian says meow")
}
}
class Lion:Cat {
override func speak() {
print("Lion says meow")
}
}

That is everything for today. See you tomorrow.

--

--