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()
It will print:
Romina: gets home
Fausto: Meow!
Lupi: Woof!
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
}
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")
]
)
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!")
}
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!")
}
}
Note here that:
- 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. - 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 anArray
, 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!
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. Aninit
method is automatically generated. -
It can't inherit from another
class
orstruct
.
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
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
}
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
}
}
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
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.
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")
}
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
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 a
struct` 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 returnstrue
in case the element should be included into the result array, orfalse
otherwise. -
sorted
is a function that let us sort an array. It works similarly to thefilter
function, we will get two elements and we'll returntrue
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, unlikemap
,filter
andsorted
.
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)"
}
}
`
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
andLandmark
objects for a certain user.
Use your creativity and try to get the exercise as complete as possible.
Top comments (0)