Let’s play with swift- part 3

Building Your Own Types(structures, classes, enumerations and protocols)

Rejaul Hasan
18 min readAug 2, 2021

This is not a language learning article. In daily life coding I often forget so many topics and tricks which are useful to use in live project. So this lockdow I think to brush-up some of my skills and this time make my own document. So that, I can get back to it easily. I planned to brush-up swift, c++, basic data structures and algorithms, some advance DS and ALGO, important design patterns and testing. So, now started with swift. I use Swift Apprentice and apple provided book. Mostly you can say it’s a short note of Swift Apprentice book. They explain very well and I love to keep my note as they explain. As a learner, I always recommend to start learning from a book or read the official documentation, if you have nice amount of time. Reading random blog do not give you depth knowledge about a topic. Yes, we can learn from those and then just go through a book when you have time. You might not choose best book on the topic of you first time but it is still good to build habit to read book. later, you can easily go through another book to know more in depth. This time you can finish it fast, as you already know many things. It is my personal opinion, so do what you love to. You can find Part-1 and Part-2 from those link. Let’s start.

Struct

struct Location {
let x: Int
let y: Int
}
let storeLocation = Location(x: 2, y: 4)

Initializers enforce that all properties are set before you start using them. This is one of the key safety features of Swift.

Structures as values

struct Location{
let x: Double
let y: Double
}
struct DeliveryArea{
let center:Location
let radius: Double
}
var area1 = DeliveryArea(center: Location(x: 2, y: 4), radius:
2.5)
var area2 = area1
print(area1.radius) // 2.5
print(area2.radius) // 2.5
area1.radius = 4
print(area1.radius) // 4.0
print(area2.radius) // 2.5

Struct always work as a value type and have value semantics like most of our build in data type work(int, double, float). As Struct, Int, Double, String and many other data type are value type so there something similar in there properties. If you search for definition of Int you will find this

public struct Int : FixedWidthInteger, SignedInteger {// ...}

So they all are struct in their definition.

struct test{
let x:Int = 10
var y: Int = 20
}

let’s have a look about let and var. Those thing are small and easy but forget most of the time.

let ob = test()
ob.y = 5 //give error because ob is a constant though y is a variable.
var ob1 = test()
ob1.x = 2 // Not execute because x is a constant.
ob1.y = 4 // execute properly because ob1 and y both are variable.

Properties

Name type can have 2 types of properties. First one is stored properties and the second one is computed properties. Stored properties are the most used one.

struct TV {
var height: Double
var width: Double
var diagonal: Int {
let result = (height * height +
width * width).squareRoot().rounded()
return Int(result)
}
}

Computed properties don’t store any values; they return values based on calculations.

Computed properties getter and setter

computed properties is a read only property and Getter come by default when you declare a computed property. It compute and then return the value, does not store value into the property like store property does. We can use set for assigning new datas into any other variables which previously declared. Let’s have an example.

struct height {    
var kilometre: Int = 0
var meter: Int
{
get {
return kilometre / 1000
}
set(newMeter) {
kilometre = newMeter*1000
}
}
}

Here when we set meter we also calculate kilometre and assign it into respective variable.

Type properties

In the previous section, you learned how to associate stored and computed properties with instances of a particular type. So those properties are associate with each instant you declare of that type. However, the type itself may also need properties that are common across all instances. These properties are called type properties.

struct Level {
static var highestLevel = 1
let id: Int = 0
var boss: String = ""
var unlocked: Bool = true
}

Here, highestLevel is a property on Level itself rather than on the instances. That means you don’t access this property on an instance:

let level3 = Level()
let highestLevel = level3.highestLevel //not compiled as highestLevel is a property of Level not it's instant
let highestLevel = Level.highestLevel

You define type properties with the static keyword. For computed type properties for class types, you can use the class keyword instead to allow subclasses to override the superclass’s implementation. The example below shows the syntax for stored and computed type properties.

class Data{
static var test = ""
class var overrideableComputedTypeProperty: Int {
return 10
}
}
class SubClass: Data{
override class var overrideableComputedTypeProperty: Int{
return 100
}
}

overrideableComputedTypeProperty not a class variable, it’s a class computed property. The below code is not supported. It provides compile time error.

class Data{
class var o = "it works"
}
//Class stored properties not supported in classes; did you mean 'static'?

Note from apple book: Unlike stored instance properties, you must always give stored type properties a default value. This is because the type itself doesn’t have an initializer that can assign a value to a stored type property at initialization time.

Stored type properties are lazily initialized on their first access. They’re guaranteed to be initialized only once, even when accessed by multiple threads simultaneously, and they don’t need to be marked with the lazy modifier.

Property observers

A willSet observer is called when a property is about to be changed while a didSet observer is called after a property has been changed.

struct Level {
static var highestLevel = 1
let id: Int
var boss: String
var unlocked: Bool {
didSet {
if unlocked && id > Self.highestLevel {
Self.highestLevel = id
}
}
}
}

willSet and didSet observers are not called when a property is set during initialization; they only get called when you assign a new value to a fully initialized instance. That means property observers are only useful for variable properties since constant properties are only set during initialization. Select between var and let accordingly to match your needs.

struct LightBulb {
static let maxCurrent = 40
var current = 0 {
didSet {
if current > LightBulb.maxCurrent {
print("""
Current is too high,
falling back to previous setting.
""")
current = oldValue
}
}
}
}

In this example, if the current flowing into the bulb exceeds the maximum value, it will revert to its last successful value.

Lazy stored property

struct Circle {
lazy var pi = {
((4.0 * atan(1.0 / 5.0)) - atan(1.0 / 239.0)) * 4.0
}()
var radius = 0.0
var circumference: Double {
mutating get {
pi * radius * 2
}
}
init(radius: Double) {
self.radius = radius
}
}

The calculation of pi waits patiently until you need it. Only when you ask for the circumference property is pi calculated and assigned a value.

var circle = Circle(radius: 5) //pi has not been run
let circumference = circle.circumference // 31.42 - also, pi now has a value

The lazy property must be a variable, defined with var, instead of a constant defined with let. When you first initialize the structure, the property effectively has no value. Then when some part of your code requests the property, its value will be calculated. So even though the value only changes once, you still use var.

Methods

Introducing mutating methods

struct test{
var data: Int = 3
func change() {
data = 1 //error
}
}
var obj = test()
obj.change()

Is it compile? The answer is no. Structs have two kind of methods. plain and mutating methods. Plain method implies immutable (or non-mutating). This separation exists only to support immutable semantics. An object in immutable mode shouldn’t change its state at all.

Then, immutable methods must guarantee this semantic immutability. Which means it shouldn’t change any internal value. So compiler disallows any state changes of itself in a immutable method. In contrast, mutating methods are free to modify states. The default initialisation of a method make it immutable. So we can not change any properties inside it. Below code work properly.

struct test{
var data: Int = 3
mutating func change(){
data+=1
}
}
var obj = test()
obj.change()

But the below code again do not work. Because we declare the instance in a constant variable which is immutable and we call a mutable function by using this instance.

struct test{
var data: Int = 3
mutating func change(){
data+=1
}
}
let obj = test()
obj.change() // error

Type methods

Like type properties, you can use type methods to access data across all instances. You call type methods on the type itself, instead of on an instance. To define a type method, you prefix it with the static modifier.

struct Math {
static func factorial(of number: Int) -> Int {
(1...number).reduce(1, *)
}
}
Math.factorial(of: 6) // 720

Adding to an existing structure with extensions

struct Math {
static func factorial(of number: Int) -> Int {
(1...number).reduce(1, *)
}
}
extension Math{
print("whatever you love to do")
}

The above code extent the Math structure and add extra functionality. You can open an existing structure and add methods, initializers and computed properties to it by using an extension.

Classes

Classes are reference types, so a variable of a class type doesn’t store an actual instance — it stores a reference to a location in memory that stores the instance.

class SimplePerson {
let name: String
init(name: String) {
self.name = name
}
}
var var1 = SimplePerson(name: "John")

Here variable var1 store a reference or memory location where it can find the class properties.

The heap vs. the stack

Both the heap and the stack have essential roles in the execution of any program. A general understanding of what they are and how they work will help you visualize the functional differences between a class and a structure:

  • The system uses the stack to store anything on the immediate thread of execution; it’s tightly managed and optimized by the CPU. When a function creates a variable, the stack stores that variable and then destroys it when the function exits. Since the stack is so strictly organized, it’s very efficient, and thus quite fast.
  • The system uses the heap to store instances of reference types. The heap is generally a large pool of memory from which the system can request and dynamically allocate blocks of memory. Lifetime is flexible and dynamic.

The heap doesn’t automatically destroy its data like the stack does; additional work is required to do that. This makes creating and removing data on the heap a slower process, compared to on the stack.

In Swift, the === operator lets you check if the identity of one object is equal to the identity of another. So obj1 === obj2 will return true only if they pointing the same memory location/reference.

Methods and mutability in class

As you’ve read before, instances of classes are mutable objects whereas instances of structures are immutable values. So, we do not need to add mutating keyword with the method inside the class.

class Student {
var firstName: String
var lastName: String
var grades: [Grade] = []
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
func recordGrade(_ grade: Grade) {
grades.append(grade)
}
}
let jane = Student(firstName: "Jane", lastName: "Appleseed")
let history = Grade(letter: "B", points: 9.0, credits: 3.0)
var math = Grade(letter: "A", points: 16.0, credits: 4.0)
jane.recordGrade(history)
jane.recordGrade(math)

may have had you wondering how you were able to modify jane even though it was defined as a constant. When you define a constant, the value of the constant cannot be changed. If you recall back to the discussion of value types vs reference types, it’s important to remember that, with reference types, the value is a reference. Here assume, jane = 0x005672 the memory address. This value is a reference and because jane is declared as a constant, this reference is constant. If you were to attempt to assign another student to jane, you would get a compiler error.

Extending a class using an extension

extension Student {
var fullName: String {
"\(firstName) \(lastName)"
}
}

Speed between class & structure

Speed considerations are a thing, as structs rely on the faster stack while classes rely on the slower heap. If you’ll have many more instances (hundreds and greater), or if these instances will only exist in memory for a short time — lean towards using a struct. If your instance will have a longer lifecycle in memory, or if you’ll create relatively few instances, then class instances on the heap shouldn’t create much overhead.

Minimalist approach

Another approach is to use only what you need. If your data will never change or you need a simple data store, then use structures. If you need to update your data and you need it to contain logic to update its own state, then use classes. Often, it’s best to begin with a struct. If you need the added capabilities of a class sometime later, then you just convert the struct to a class.

Advance Class

Inheritance

class Person {
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
class Student: Person {
var grades: [Grade] = []
func recordGrade(_ grade: Grade) {
grades.append(grade)
}
}

A class that inherits from another class is known as a subclass or a derived class, and the class here Person from which it inherits is known as a superclass or a base class.

class test{
let value = 0
}
class data{
let dataValue = 0
}
class now:test,data{ // provide error
let now = 0
}

A Swift class can inherit from only one other class, a concept known as single inheritance.

Runtime hierarchy checks

The first call send object as a BandMember always. Because it’s immediate parent class is BandMember . So to pass as a Student we have to cast it manually. By using as we can check or cast the object into our needed class or type.

Inheritance, methods and overrides

class Test{
func method()->Int{ 10 }
}
class SubClassOfTest:Test{
override func method()->Int{
super.method()+20
}
}
let obj = SubClassOfTest()
let value = obj.method()

it’s override a function also call super method. So the value will be 30 here because test class method return 10 and we add 20 then in the method function of SubClassOfTest.

Preventing inheritance

final class FinalStudent: Person {}
class FinalStudentAthlete: FinalStudent {} // Build error!

By marking the FinalStudent class final, you tell the compiler to prevent any classes from inheriting from FinalStudent. This can remind you — or others on your team! — that a class wasn’t designed to have subclasses.

Additionally, you can mark individual methods as final, if you want to allow a class to have subclasses, but protect individual methods from being overridden.

class AnotherStudent: Person {
final func recordGrade(_ grade: Grade) {}
}
class AnotherStudentAthlete: AnotherStudent {
override func recordGrade(_ grade: Grade) {} // Build error!
}

There are benefits to initially marking any new class you write as final. This tells the compiler it doesn’t need to look for any more subclasses, which can shorten compile time, and it also requires you to be very explicit when deciding to subclass a class previously marked final.

Inheritance and class initialization

class Test{ 
let a:Int
init(a:Int) {
self.a = a
}
func method()->Int{ 10 }
}
class SubClassOfTest:Test{
let b:Int
init(b:Int) {
self.b = b
}
override func method()->Int{
super.method()+20
}
}
let
obj = SubClassOfTest(b: 10)

makes an error. Here we inherit Test class so we have to call Test class init method also. Otherwise the superclass won’t be able to provide initial states for all its stored properties. So, it give compile time error.

Two-phase initialization

class Test{let a:Intinit(a:Int) {self.a = a}func method()->Int{ 10 }}class SubClassOfTest:Test{let b:Intinit(b:Int) {super.init(a: 20) // error:- before store b we call super class property. which is also consider this class property.self.b = b }override func method()->Int{super.method()+20}}let obj = SubClassOfTest(b: 10)

Because of Swift’s requirement that all stored properties have initial values, initializers in subclasses must adhere to Swift’s convention of two-phase initialization.

  • Phase one: Initialize all of the stored properties in the class instance, from the bottom to the top of the class hierarchy. You can’t use properties and methods until phase one is complete.
  • Phase two: You can now use properties and methods, as well as initializations that require the use of self.

Required and convenience initializers

if we declare an initialize as required then all of it’s subclass must have to implement it. Notice that the subclass SubclassOfTest also initialize the parent init as required not override because it tell the next sub class that you also have to initialize it.

You might want to mark an initializer as convenience if you only use that initializer as an easy way to initialize an object, but you still want it to leverage one of your designated initializers. A non-convenience initializer is called a designated initializer and is subject to the rules of two-phase initialization. All initializers you’ve written in previous examples were in fact designated initializers.

Here’s a summary of the compiler rules for using designated and convenience initializers:

  1. A designated initializer must call a designated initializer from its immediate superclass.
  2. A convenience initializer must call another initializer from the same class.
  3. A convenience initializer must ultimately call a designated initializer.

The class lifecycle(Automatic reference counting or ARC)

var someone = Person(firstName: "Johnny", lastName: "Appleseed")
// Person object has a reference count of 1 (someone variable)
var anotherSomeone: Person? = someone
// Reference count 2 (someone, anotherSomeone)
var lotsOfPeople = [someone, someone, anotherSomeone, someone]
// Reference count 6 (someone, anotherSomeone, 4 references in
lotsOfPeople)
anotherSomeone = nil
// Reference count 5 (someone, 4 references in lotsOfPeople)
lotsOfPeople = []
// Reference count 1 (someone)
someone = Person(firstName: "Johnny", lastName: "Appleseed")
// Reference count 0 for the original Person object!
// Variable someone now references a new object

Deinitialization

class Person {
// original code
deinit {
print("the obj is removed from memory!")
}
}

A deinitializer is a special method on classes that runs when an object’s reference count reaches zero, but before Swift removes the object from memory.

Retain cycles and weak references

class Student: Person {
var partner: Student?
// original code
deinit {
print("\(firstName) is being deallocated!")
}
}
var alice: Student? = Student(firstName: "Alice",
lastName: "Appleseed")
var bob: Student? = Student(firstName: "Bob",
lastName: "Appleseed")
alice?.partner = bob
bob?.partner = alice
alice = nil
bob = nil

If you run this in your playground, you’ll notice that you don’t see the message Alice/Bob is being deallocated!, and Swift doesn’t call deinit. Why is that?

Alice and Bob each have a reference to each other, so the reference count never reaches zero! To make things worse, by assigning nil to alice and bob, there are no more references to the initial objects. This is a classic case of a retain cycle, which leads to a software bug known as a memory leak.

With a memory leak, memory isn’t freed up even though its practical lifecycle has ended. Retain cycles are the most common cause of memory leaks. Fortunately, there’s a way that the Student object can reference another Student without being prone to retain cycles, and that’s by making the reference weak:

class Student: Person {
weak var partner: Student?
// original code
}

Enumerations

enum Month {
case january, february, march, april, may, june, july, august,september, october, november, december
}
func semester(for month: Month) -> String {
switch month {
case .august, .september, .october, .november, .december:
return "Autumn"
case .january, .february, .march, .april, .may:
return "Spring"
case .june, .july:
return "Summer"
}
}

do the same work with enum computed property.

enum Month {case january, february, march, april, may, june, july, august,september, october, november, decembervar semester:String{
switch self {
case .august, .september, .october, .november, .december:
return "Autumn"
case .january, .february, .march, .april, .may:
return "Spring"
case .june, .july:
return "Summer"
}
}
}
let month = Month.april
let semister = month.semester // Spring

Raw values

enum Month: Int {
case january = 1, february = 2, march = 3, april = 4, may = 5,
june = 6, july = 7, august = 8, september = 9,
october = 10, november = 11, december = 12
}
OR
//the compiler will automatically increment the values if you provide the first one and leave out the rest
enum Month: Int {
case january = 1, february, march, april, may, june, july,august, september, october, november, december
}

//Here the case name itself are the row value.
enum Icon: String {
case music
case sports
case weather
var filename: String {
"\(rawValue).png"
}
}
let icon = Icon.weather
icon.filename // weather.png

Associated values

An enumeration can have raw values or associated values, but not both.

enum WithdrawalResult {
case success(newBalance: Int)
case error(message: String)
}
class Account{
var balance: Int
init(balance: Int) {
self.balance = balance
}
func withdwar(money:Int) -> WithdrawalResult {
if balance >= money {
balance -= money
return .success(newBalance: balance)
}else{
return .error(message: "Asking more money you have your account")
}
}
}
func test(){
let myAccount = Account(balance: 100)
let message = myAccount.withdwar(money: 50)
switch message{
case .success(newBalance: let newBalance):
print("you have $\(newBalance) left in your account")
case .error(message: let message):
print(message)
}
}
test()

ItIterating through all cases

When you conform to the CaseIterable protocol, your enumeration gains a class method called allCases that lets you loop through each case in the order that it was declared.

enum Pet: CaseIterable {
case cat, dog, bird, turtle, fish, hamster
}
for pet in Pet.allCases { print(pet) }

Enumerations without any cases

struct Math {
static func factorial(of number: Int) -> Int {
(1...number).reduce(1, *)
}
}
let factorial = Math.factorial(of: 6)

you could create an instance of Math.The math instance doesn’t serve any purpose since it is completely empty; it doesn’t have any stored properties. In cases like this, the better design is actually to transform Math from a structure to an enumeration.

enum Math {
static func factorial(of number: Int) -> Int {
(1...number).reduce(1, *)
}
}
let factorial = Math.factorial(of: 6)

init method in enumeration

In order to create an instance of an enumeration though, you have to assign a member value as the state. If there are no member values, then you won’t be able to create an instance

enum WithdrawalResult {
case success(newBalance: Int)
case error(message: String)
init?(value: Int) {
switch value {
case 1:
self = .success(newBalance: 10)
case 2:
self = .error(message: "some error")
default:
return nil
}
}
}

Optional

var age: Int?
age = 17
age = nil
switch age { // none printed
case .none:
print("none")
case .some(let value):
print(value)
}

Optionals are really enumerations with two cases:

  1. none means there’s no value.
  2. some means there is a value, which is attached to the enumeration case as an associated value.

protocols

protocol Vehicle {
func accelerate()
func stop()
}
// Inheritance protocol
protocol WheeledVehicle: Vehicle {
var numberOfWheels: Int { get }
var wheelSize: Double { get set }
}
class Bike: WheeledVehicle {
let numberOfWheels = 2 //get property used as constant stored property
var wheelSize = 16.0
var peddling = false
var brakesApplied = false
func accelerate() {
peddling = true
brakesApplied = false
}
func stop() {
peddling = false
brakesApplied = true
}
}

Protocols don’t care how you implement their requirements, as long as you implement them. Your choices for implementing a get requirement are:

  • A constant stored property.
  • A variable stored property.
  • A read-only computed property.
  • A read-write computed property.

Your choices for implementing both a get and a set property are limited to a variable stored property or a read-write computed property.

Associated types in protocols

You can also add an associated type as a protocol member. When using associatedtype in a protocol, you’re simply stating there is a type used in this protocol, without specifying what type this should be. It’s up to the protocol adopter to decide what the exact type should be.

This lets you give arbitrary names to types without specifying exactly which type it will eventually be:

protocol WeightCalculatable {
associatedtype WeightType
var weight: WeightType { get }
}
class HeavyThing: WeightCalculatable {
// This heavy thing only needs integer accuracy
typealias WeightType = Int
var weight: Int { 100 }
}
class LightThing: WeightCalculatable {
// This light thing needs decimal places
typealias WeightType = Double
var weight: Double { 0.0025 }
}

Implementing multiple protocols

protocol Wheeled {
var numberOfWheels: Int { get }
var wheelSize: Double { get set }
}
class Bike: Vehicle, Wheeled {
// Implement both Vehicle and Wheeled
}

Requiring reference semantics

Protocols can be adopted by both value types (structs and enums) and reference types (classes), so you might wonder if protocols have reference or value semantics.

The truth is… it depends! If you have an instance of a class or struct assigned to a variable of a protocol type, it will express value or reference semantics that match the type it was defined as.

To illustrate, take the simple example of a Named protocol below, implemented as a struct and a class:

protocol Named {
var name: String { get set }
}
class ClassyName: Named {
var name: String
init(name: String) {
self.name = name
}
}
struct StructyName: Named {
var name: String
}
var named: Named = ClassyName(name: "Classy")
var copy = named
named.name = "Still Classy"
named.name // Still Classy
copy.name // Still Classy
//Struct behave like value type.
named = StructyName(name: "Structy")
copy = named
named.name = "Still Structy?"
named.name // Still Structy?
copy.name // Structy

If you’re designing a protocol to be adopted exclusively by classes, it’s best to request that Swift uses reference semantics when using this protocol as a type

protocol Named: AnyObject {
var name: String { get set }
}

By using the AnyObject constraint above, you indicate that only classes may adopt this protocol. This makes it clear that Swift should use reference semantics.

Protocols in the Standard Library

We can confirm those protocol in our class and use them our way. Below example:

class Record {
var wins: Int
var losses: Int
init(wins: Int, losses: Int) {
self.wins = wins
self.losses = losses
}
}
//Equatable is a library protocol
extension Record: Equatable {
static func ==(lhs: Record, rhs: Record) -> Bool {
lhs.wins == rhs.wins && lhs.losses == rhs.losses
}
}
let recordA = Record(wins: 10, losses: 5)
let recordB = Record(wins: 10, losses: 5)
recordA == recordB // work perfectly

Other useful protocols

Hashable

class Student {
let email: String
let firstName: String
let lastName: String
init(email: String, firstName: String, lastName: String) {
self.email = email
self.firstName = firstName
self.lastName = lastName
}
}
extension Student:Hashable{
static func == (lhs: Student, rhs: Student) -> Bool {
lhs.email == rhs.email && lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName
}
func hash(into hasher: inout Hasher) {//emails and names used for hashing
hasher.combine(email)
hasher.combine(firstName)
hasher.combine(lastName)
}
}

let
testStudent = Student(email: "ok", firstName: "rx", lastName: "ry")
let test = [testStudent:"is it working"]
print(test.first!)

CustomStringConvertible

extension Student: CustomStringConvertible {
var description: String {
"\(firstName) \(lastName)"
}
}
print(testStudent) //rx ry

There is much more pre define protocols provide with standard libraries which we can use our project. Here we do not talk about the optional properties in protocols. We will discuss about those letter part.

--

--

Rejaul Hasan

I work as a Sr. software engineer for iOS platform. Available to discuss about any good opportunities or projects.