DEV Community

Fernando Martín Ortiz
Fernando Martín Ortiz

Posted on

Introduction to Swift - Part 2

Note: This article is part of the course Introduction to iOS using UIKit I've given many times in the past. The course was originally in Spanish, and I decided to release it in English so more people can read it and hopefully it will help them.

Object oriented programming

In every program we've got two well defined concepts: Data and behavior.
A tuple with five fields that define an address is data.
A function that takes that tuple and returns a String with its description is behavior.

Everything we've done so far in the first part of Swift introduction has been defining data on one side and the behavior that acts on that data in another, separated side, which has worked well for us so far. However, the most frequent way of organizing code in modern languages, such as Swift, is integrating data and behavior in cohesive structures such as the ones we'll see today, when we'll review Object oriented programming (OOP).

This will bring us some advantages. Especially, the ability to abstract. A class will have behavior that can be used from the outside. It means, we'll know what that class can do. However, the how is private to that class. That's known as encapsulation.

Class

A class is one of those structures that integrate data and behavior as a unit. Data inside a class is called attributes. Behavior inside a class is modeled as a set of functions known as methods.
In Swift, a class is defined using the keyword class followed by its name. Attributes will be variables, and methods will be functions.
Let's see an example of a class hierarchy and then we'll explain in detail what just has happened:

class Animal {
    var name: String
    var walks: [String]

    var animalType: String { return "" }

    init(name: String) {
        self.name = name
        self.walks = []
    }

    func emitSound() {}

    func walk(to place: String) {
        self.walks.append(place)
    }
}

class Dog: Animal {
    override var animalType: String { return "Dog" }
    override func emitSound() {
        print("\(name): Woof!")
    }
}

class Cat: Animal {
    override var animalType: String { return "Cat" }
    override func emitSound() {
        print("\(name): Meow!")
    }
}

class Person {
    private var name: String
    private var pets: [Animal]

    init(name: String, pets: [Animal]) {
        self.name = name
        self.pets = pets
    }

    func getHome() {
        print("\(name): gets home")
        for pet in pets {
            pet.emitSound()
        }
    }
}

let romina = Person(
    name: "Romina",
    pets: [
        Cat(name: "Fausto"),
        Dog(name: "Lupi")
    ]
)

romina.getHome()
Enter fullscreen mode Exit fullscreen mode

It will print:

Romina: gets home
Fausto: Meow!
Lupi: Woof!
Enter fullscreen mode Exit fullscreen mode

Inheritance

Ok, what just has happened here? First, we've defined a base class, called Animal. It's called base class because it's the class that will define the base of the class hierarchy, it will define the common set of data and behavior for the other classes in the hierarchy.

What can an animal do? (behavior). In this case, it can go for a walk, and it can emite a sound. What sound? We don't know that. The sound an animal emits will be define in its subclasses. That's called inheritance. An animal can either be a Cat or a Dog, in this example. So that, a Cat can say Meow, and define its type as "Cat", but it inherits all the behavior defined in its base class.

What do we know of an animal? (data). In this case, its name and the walks it has taken.

And animalType? It's a computed variable, or getter. A getter is essentially a function that returns a value. From the outside of a class, it's seen and it's used a variable that we can't change its value. It takes part of the behavior of a class.

Encapsulation

A class only exposes part of its data and behavior that we can use from its outside. What is internal to that class is defined with the keyword private. What is private can only be accessed from inside that class. What isn't marked as private is accessible from anywhere else in the app and it's known as the public interface for that class.

Init

The init method is an especial method of a class, that has the responsibility of define how an object of that class will be created. In this example, the Person class will have an init defined like this:

 init(name: String, pets: [Animal]) {
     self.name = name
     self.pets = pets
 }
Enter fullscreen mode Exit fullscreen mode

self in this case refers to the same object on which we're working.

init doesn't have to be written in order to be used. Consider this example:

 let romina = Persona(
     name: "Romina",
     pets: [
         Cat(name: "Fausto"),
         Dog(name: "Lupi")
     ]
 )
Enter fullscreen mode Exit fullscreen mode

Polymorphism

Functionality defined in a base class can be redefined with the keyword override in the subclasses. So for instance, emitSound doesn't have an actual functionality in the base class. But it does in its subclasses.
Subclasses "override" behavior from the base class. For instance:

 override func emitSound() {
     print("\(name): Meow!")
 }
Enter fullscreen mode Exit fullscreen mode

What's interesting is that we can refer to dog and cats as Animal in the pets Array. Whenever we call pet.emitSound() we aren't sure if the pet is a cat or a dog, that's determined in runtime, where each object will execute the behavior defined by its specific subclass.

Protocol

A class answers the question What is this?

  • What is this? A Cat

  • What is this? A Dog

  • What is this? A Person

A protocol, on the other hand, answers the question What can this do?.
On its simplest form, a protocol (also called interface in other programming languages such as Java), is a set of method signatures under a name. Any class can implement a protocol. If a class implements a protocol, then it must implement all the methods defined on it. Otherwise, the code won't compile.

Let's see an example:

protocol EmitsSound {
    func emitSound()
}

class Animal {}

class Dog: Animal, EmitsSound {
    func emitSound() {
        print("Woof!")
    }
}

class Cat: Animal, EmitsSound {
    func emitSound() {
        print("Meow!")
    }
}

class Ringer: EmitsSound {
    func emitSound() {
        print("Ring!")
    }
}
Enter fullscreen mode Exit fullscreen mode

Note here that:

  1. A class can only inherit from a single class. However, it can implement as many protocols as we need. If a class inherits from another class and also implements protocols, what is written at the right of : must be first the base class, and then the protocols to be implemented.
  2. A Dog, a Cat, and a Ringer are now of the same type. All of them are of the type EmitsSound. And as such, we can use them in an Array, for instance:
class SoundEffects {
    let sounds: [EmitsSound]

    init(sounds: [EmitsSound]) {
        self.sounds = sounds
    }

    func play() {
        for sound in sounds {
            sound.emitSound()
        }
    }
}

let getHomeSounds = SoundEffects(sounds: [Ringer(), Cat(), Dog()])
getHomeSounds.play()
// Ring!
// Meow!
// Woof!
Enter fullscreen mode Exit fullscreen mode

As we've implemented EmitsSound for all our objects, regardless of their class, we can now use them to emit sounds. In this example, we use this to implement sound effects. In practice, this same principle is used a lot. Especially when all we need from other object is what it can do, regardless of what type it really is.

Struct

A struct is very similar to a class. Let's see some differences:

A class:

  • Has attributes and methods.
  • The attributes and methods it has may be private.
  • It can implement protocols.
  • It must implement a init method to initialize its attributes when it's instantiated.
  • It can inherit from other classes.

A struct:

  • Has attributes and methods.
  • The attributes and methods it has may be private.
  • It can implement protocols.
  • It is not necessary that it implements an init method to initialize its attributes in general. An init method is automatically generated.
  • It can't inherit from another class or struct.

A class also can't inherit from a struct.

protocol HasDescription {
    func getDescription() -> String
}

struct Address: HasDescription {
    let street: String
    let number: String
    let apartment: String
    let floor: String
    let city: City

    func getDescription() -> String {
        return "\(street) \(number) - Apt. \(floor)-\(apartment) - \(city.getDescription())"
    }
}

struct City: HasDescription {
    let name: String
    let state: State

    func getDescription() -> String {
        return "\(name), \(state.getDescription())"
    }
}

struct State: HasDescription {
    let name: String
    let country: String

    func getDescription() -> String {
        return "\(name), \(country)"
    }
}

let someAddress = Address(
    street: "Some St.",
    number: "18",
    apartment: "A",
    floor: "2",
    city: City(
        name: "Palermo",
        state: State(
            name: "Buenos Aires",
            country: "Argentina"
        )
    )
)

print(someAddress.getDescription()) // Some St. 18 - Apt. 2-A - Palermo, Buenos Aires, Argentina
Enter fullscreen mode Exit fullscreen mode

In practice, the struct objects are used to model small chunks of information. They can serve us to describe an address, credit card data, etc.

Enum

A struct or class let us design based on "and". For example, struct Person has firstName and age, and email as its members.
An enum, on the other hand, allow us design based on "or". For instance, enum State has florida, california or texas as its members.

Unlike enum in languages as C, enum in Swift can get to be really complex. Anyway, we'll use basic functionality on these examples.

Let's see a basic example

class User {
    var name: String
    var credentialType: CredentialType

    init(
        name: String,
        credentialType: CredentialType
    ) {
        self.name = name
        self.credentialType = credentialType
    }
}

// enum CredentialType defines how a user created their account.
enum CredentialType {
    case email
    case facebook
    case apple
    case google
}
Enter fullscreen mode Exit fullscreen mode

With the CredentialType enum, we can say that for instance, a user registered using email, or facebook, or apple, or google. It becomes impossible that a user has registered using more than one of these values, and this code makes it impossible.

Switch using enum

Ok, let's try to use CredentialType for our case. The most typical way of using an enum in our logic, is by using the keyword switch.

func isSocialNetworkUser(_ user: User) -> Bool {
    switch user.credentialType {
    case .email:
        return false
    case .facebook, .apple, .google:
        return true
    }
}
Enter fullscreen mode Exit fullscreen mode

Also note that we could have defined that function inside User. And that we're combining several options inside the same case, but they could have also been written with a case for each option (facebook, apple, google).

Methods and attributes in enum

An enum may have methods and attributes, like a class. The most common pattern for doing that is including a switch self clause inside the method or attribute. Let's see an example:

enum Country {
    case argentina, germany, england, usa

    // We'll create a computed property. A `getter`.
    // Note that this is the same as write
    //
    // func isEuropean() -> Bool { ... }
    //
    // But when we call it, we won't use parenthesis, exactly as if we would be working with a property.
    var isEuropean: Bool {
        // switch self is VERY common in these cases
        switch self {
        // If we're calling this property for cases `germany` or `england`.
        // then we'll return `true`
        case .germany, .england:
            return true
        // However, if we're doing this from any other case of this enum, then
        // we'll return false
        default:
            return false
        }
    }

    // Similar to the previous case but with a String
    var nombre: String {
        switch self {
        case .argentina:
            return "Argentina"
        case .germany:
            return "Germany"
        case .england:
            return "England"
        case .usa:
            return "United States of America"
        }
    }
}

func describe(country: Country) {
    if country.isEuropean {
        print("\(country.name) is European")
    } else {
        print("\(country.name) is NOT European")
    }
}

describe(country: Country.germany) // Germany is European
Enter fullscreen mode Exit fullscreen mode

Another curious thing about enum is that when we need to use one of them, like in the case of describe, there is not necessary to specify the type. For instance, Country.germany could have been just .germany. (note: This is not specific of enums, but it's notorious and easy to recognize for these)

describe(country: .argentina) // It's the same to say describe(country: Country.argentina) and it feels more natural.
Enter fullscreen mode Exit fullscreen mode

rawValue

There is an alternative way to define the country name, as we've seen in the previous example, and it's by using a rawValue. Each enum can have a single rawValue for each of its cases, and all of them must be of the same type.

rawValue let us not only get a value associated to each case, but also create a case for the enum based on that associated value.

enum State: String {
    case florida = "Florida"
    case newYork = "New York"
    case california = "California"
}

let state1 = State.florida
print("State 1 is \(state1.rawValue)")
// We use the rawValue to get the value for that state
// this will print "State 1 is Florida"

let state2 = State(rawValue: "California")
// Here, we're doing the inverse process
// instead of getting the rawValue of a state,
// we create the state based on its rawValue
//
// We can make mistakes, so there is no guarantee that it exists
// a state with that name, the type of state2
// is `State?`, an optional State.

if let state = state2 {
    print("We could get state2 and its \(state.rawValue)")
} else {
    print("We couldn't get state2, and it's nil")
}
Enter fullscreen mode Exit fullscreen mode

Associated value

This is the last case we'll see here about enum. I'd like to explain them here because they are something very used in Swift, although it's not inside the scope of what's needed to continue with the course. I mean, it's not necessary to grasp the following lessons. However, knowing this can be very useful in your careers.

Each enum case can have associated values. For example, let's imagine a Cookie enum. Each cookie might be represented using a enum.

enum Cookie {
    // Each case may have any number of associated values. Each one may optionally have
    // an associated name and may be of the type we need.
    case chocChip(dough: DoughType)
    case stuffed(stuff: StuffType, dough: DoughType)
    case fortune

    var cookieDescription: String {
        switch self {
        // We can get associated values using the keyword `let`.
        case .chocChip(let dough):
            return "Chocolate chips cookie with dough of type \(dough.doughDescription)"
        case .stuffed(let stuff, let dough):
            return "Stuffed cookie of \(stuff.stuffDescription), flavor \(dough.doughDescription)"
        case .fortune:
            return "Fortune Cookie"
        }
    }
}

enum StuffType {
    case vanilla, strawberry

    var stuffDescription: String {
        switch self {
        case .vanilla: return "vanilla"
        case .strawberry: return "strawberry"
        }
    }
}

enum DoughType {
    case vanilla, chocolate

    var doughDescription: String {
        switch self {
        case .vanilla: return "vanilla"
        case .chocolate: return "chocolate"
        }
    }
}

struct CookieSet {
    let cookies: [Cookie]

    func describe() {
        print("-")
        print("Cookies:")
        for cookie in cookies {
            print(cookie.cookieDescription)
        }
    }
}

let cookies = CookieSet(cookies: [
    .fortune,
    .stuffed(relleno: .frambuesa, saborDeMasa: .chocolate),
    .stuffed(relleno: .frambuesa, saborDeMasa: .chocolate),
    .stuffed(relleno: .frambuesa, saborDeMasa: .chocolate),
    .chocChip(saborDeMasa: .chocolate),
    .chocChip(saborDeMasa: .vanilla),
    .chocChip(saborDeMasa: .vanilla),
    .fortune,
    .fortune,
    .chocChip(saborDeMasa: .chocolate)
])

cookies.describe()
//    -
//    Cookies:
//    Fortune
//    Stuffed cookie of strawberry, flavor chocolate
//    Stuffed cookie of strawberry, flavor chocolate
//    Stuffed cookie of strawberry, flavor chocolate
//    Chocolate chips cookie with dough of type chocolate
//    Chocolate chips cookie with dough of type vainilla
//    Chocolate chips cookie with dough of type vainilla
//    Fortune
//    Fortune
//    Chocolate chips cookie with dough of type chocolate
Enter fullscreen mode Exit fullscreen mode

Closures

closure are also called anonymous functions. We'll see first the concept of function as a data type.

Functions as a data type

In Swift, Functions are a type of data, such as Int, Double, Bool, a class, struct, or enum. This implies that we could take a function a send it as an argument for another function, or make a function return another function as a result. Or have astruct` where one of its attributes is a function. This is actually a bit weird when we first see it, but it's actually pretty common in modern languages.

To convert a function into its related data type, we need to pay attention to the types of its input and output. So, the sum function:

swift
func sum(x: Int, y: Int) -> Int { ... }

Is of type (Int, Int) -> Int because it receives to Int and returns an Int. Let's see other examples:

swift
func sum(x: Int, y: Int) -> Int { ... } // (Int, Int) -> Int
func describe(_ person: Persona) { ... } // (Persona) -> Void
func printCurrentTime() { ... } // () -> Void
func getCurrentDate() -> String { ... } // () -> String

And, as we said, a Function data type can be used as an argument for other functions:

`swift
struct Person {
let id: Int
let name: String
let role: PersonRole?
let age: Int
}

enum PersonRole {
case developer, projectManager, teacher, doctor
}

func printAdults(_ people: [Person]) {
for person in people {
if person.age >= 18 {
print("(person.id) - (person.name)")
}
}
}

let people = [
Person(id: 1, name: "Franco", role: .teacher, age: 34),
Person(id: 2, name: "Gimena", role: .projectManager, age: 24),
Person(id: 3, name: "Gonzalo", role: .teacher, age: 26),
Person(id: 4, name: "Noelia", role: .developer, age: 29),
Person(id: 5, name: "Pablo", role: nil, age: 15),
Person(id: 6, name: "Lourdes", role: .doctor, age: 29),
]

printAdults(people)
// This will work correctly
//
// However, we don't have a way to provide 'flexibility' to the algorithm. I mean,
// inside the function printAdults we filter by age, and then we print the result.
// If we would like to change the filter criteria, we'd need to write a completely new function.
// Let's convert this function in something more flexible:

// We are "injecting" a function into another function as an argument
func print(_ people: [Person], who matchesCriteria: (Person) -> Bool) {
for person in people {
if matchesCriteria(person) {
print("(person.id) - (person.name)")
}
}
}

func isAdult(_ person: Person) -> Bool {
return person.age >= 18
}

print(people, who: isAdult) // Exactly the same result. We're sending the function as an argument in this case.
`

Anonymous functions

Now it's time, with this introduction we can start talking about anonymous functions. An anonymous function, or closure is a function that lacks a name. It's as simple as it sounds. And the best context for using them is to send them to other functions. For example, in this case, I could have decided that it didn't make sense to define a new function just to determine is a person is an adult.

Let's define it as an anonymous function:

swift
print("Printing adult people using a closure (1):")
imprimir(
people,
who: { (person: Person) -> Bool in
return person.age >= 18
}
)

This is a closure, and there are a couple of different ways to define one, here are more examples: https://fuckingclosuresyntax.com/

For now, let's see the transformation step by step

We have this function:

swift
func isAdult(_ person: Person) -> Bool {
return person.age >= 18
}

Step 1: We remove the func keyword and its name:

swift
(_ person: Person) -> Bool {
return person.age >= 18
}

Step 2: In case its parameters have a different internal and external names, we will only use its internal ones:

swift
(person: Person) -> Bool {
return person.age >= 18
}

Step 3: We move the curly bracket to the beginning of the definition, and in its place, we will put in:

swift
{ (person: Person) -> Bool in
return person.age >= 18
}

Perfect! This is enough to correctly define a closure. We can use this closure as it is right now, but I'll show you some extra steps we can take to shorten this definition even more. Again, this is completely optional:

Extra step 1: We remove the data type, as the compile can infer it based on the context for most situations:

swift
{ (person) in
return person.age >= 18
}

Extra step 2: We remove the parenthesis around the arguments:

swift
{ person in
return person.age >= 18
}

Extra step 3: If the closure has a single sentence, we can remove the return keyword.

swift
{ person in person.age >= 18 }

Extra step 4: Instead of using the arguments names (in this case person), we can refer to the arguments by its order. For instance, instead of person, we can use $0. If we had two arguments, the first of them would be $0 and the second one $1. If we had four, they would be $0, $1, $2, $3, and so forth:

swift
{ $0.age >= 18 }

And that's the minimum expression for this closure

swift
print("Adult people who are teachers")
print(people, who: { $0.age >= 18 && $0.role == .teacher })

If we had a closure as the last argument for a function, we can move it outside the function call. Let's make it clearer with an example:

swift
print("Adult people who are teachers (2)")
print(people) { $0.age >= 18 && $0.role == .teacher } // Exactly the same as the previous example

Map, filter, sorted and forEach

There are some functions inside the Swift standard library that take other functions as their arguments, especially when working with Array.

  • map is a function that let us transform each element of an array into another element by passing it a function that actually performs the transformation.
  • filter is a function that let us filter an array by passing a function that returns true in case the element should be included into the result array, or false otherwise.
  • sorted is a function that let us sort an array. It works similarly to the filter function, we will get two elements and we'll return true in case the first element should be first in the result array and which second.
  • forEach, let us iterate over an array, performing anything based on the origin array. This function won't return a new array, unlike map, filter and sorted.

It's important to note that all these functions (except forEach) return a new array. They don't modify the origin array.

`swift
let adultPeople = people.filter { $0.age >= 18 }
let names = people.map { $0.name }
let peopleSortedByAge = people.sorted { $0.age < $1.age }

// We can also "chain" these functions, because each of them will return a new Array.

print("Chained functions:")

people
.filter { $0.age >= 18 } // We'll only take into account adult people
.sorted { $0.age < $1.age } // We'll then sort them by age
.map { $0.name } // And we'll extract their names
.forEach { print($0) } // Finally, we'll print their names
// Gimena
// Gonzalo
// Noelia
// Lourdes
// Franco
`

Extensions

Extensions let us extend an existent type to add new functionality to it. This functionality may be computed properties or methods.

Let's consider a simple example:

`swift
struct Address {
let street: String
let number: String
let city: String
}

extension Address {
var addressDescription: String {
return "(street) (number) - (city)"
}
}

let address = Address(street: "Rivadavia", number: "185", city: "Palermo")
print(address.addressDescription) // Rivadavia 185 - Palermo
`

And that's exactly the same as this:

`swift
struct Address {
let street: String
let number: String
let city: String

 var addressDescription: String {
     return "\(street) \(number) - \(city)"
 }
Enter fullscreen mode Exit fullscreen mode

}
`

An interesting use case for extension is to extend native types like Int, Double or String.

`swift
extension Int {
func isBigger(than anotherNumber: Int) -> Bool {
return self > anotherNumber
}
}

if 10.isBigger(than: 5) {
print("10 is bigger than 5")
}
`

Typealias

typealias are basically that, alias for types. This means that we can refer to an existing type with a new name. For example, let's suppose we're coding an app that handles users, where each user has an identifier. This user ID is an Int. However, a typealias can be even better, so we are sure we're talking about the identifier of a user.

Remember that a good code is easy to extend and easy to understand.

`swift
typealias UserID = Int

struct User {
let id: UserID
let name: String
}

typealias BuildingID = Int
typealias Address = (street: String, number: String, city: String, state: String, country: String)

struct Building {
let id: BuildingID
let ownerId: UserID
let address: Address
}

let office = Building(
id: 1,
ownerId: 10,
address: (
street: "Rivadavia",
number: "18451 PB Torre 2",
city: "Moron",
state: "Buenos Aires",
country: "Argentina"
)
)
`

Note that we could have done exactly the same without typealias. In general (except for advanced use cases we won't cover during this course), typealias bring clarity to the code, making our intention even more evident and clear to the other developers.

Exercises

We want to develop an application for organizing trips. The idea for the app is that we will have a list of possible destinations. The user can select their favorite destinations and save them, so they could then see those saved destinations in another list.

This exercise consists in developing the data structures needed to support the application use cases. It's required that, least:

  • Use classes, structs or enums to the entities User, Address, Place, Landmark.
  • Allow getting the favorites Place and Landmark objects for a certain user.

Use your creativity and try to get the exercise as complete as possible.

Discussion (0)