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.
Swift overview
Swift is a programming language developed by Apple to be used in application development for Apple platforms, as a replacement for (or in combination to) Objective-C, its previous recommended language. This way, and using Xcode, we can develop apps for iOS, iPad OS, MacOS, WatchOS and TVOS. Since 2015, Swift is also Open Source, being maintained not just by Apple, but by a open development community.
Swift is multi-paradigm, which means we can use it to develop object oriented code (OOP), functional programming, scripting, etc. Swift adapts to our preferred programming style, although the community has agreed on some best practices and a way to develop code following mainly an object oriented approach.
Swift is modern, which means it includes features from other modern languages, as we'll see during this course. What's more, it continues being added more features during the years.
Swift is simple to learn, but it can be as complex and go as low level as we need it to go.
During this course we'll learn the Swift syntax and usage examples. But not in depth. We'll learn the necessary syntax in order to start developing iOS application. However, it's important to note that the language can be much more complex than what we'll learn here. This course isn't intended to be comprehensive, but to bring the needed tools to start.
Variables
Variables are present in most (if not all) programming languages, and are names (also called identifiers), which contain a value.
In Swift, variables are declared using the keyword var
, followed by a =
and the value it contains. For instance:
var name = "Fernando"
var street = "Rivadavia Av."
var catLegsCount = 4
Something that could be curious in these declarations is that we aren't specifying their type. That's because Swift includes a feature called type inference. Type inference let us declare variables without having to be explicit about their types, because Swift is "intelligent" enough to figure out that a 4
is an Int
, and that "Fernando"
is a String
.
We can be explicit
Of course, there are moments where it could be useful to be explicit about the data type of a variable. For instance, if we're talking about a price.
var price1 = 12
We could want to be explicit about the type because the price is a decimal value, and it's not an Int
. In Swift, we can specify the data type using : <DataType>
. For example:
var price2: Double = 2.50
Constants
Let's suppose we have a variable with a value that we know it won't change. For example, a cat has four legs. If we want to define the number of legs a cat has, we can use a constant. In order to so, we'll replace var
by let
, transforming that variable into a constant. A constant is a variable to which we can only associate a value once. If we want to update its value afterwards, we'll get an error.
var leavesInATree = 10
leavesInATree = 22
leavesInATree = 25 // We won't get any errors if we modify this value.
let catLegsCount = 4
// The next line won't compile.
// catLegsCount = 5
While naming a variable, it's important to be very explicit on its meaning. This means: do not abbreviate
lbl
isn't a good variable name. label
is.
chCnt
isn't a good variable name. charactersCount
is.
i
isn't a good variable name. index
is.
Every programming language has it's own "correct" way to write code with. We know it as best practices. In Swift, the best practice is to declare variables as constants, using let
, and use var
only when it's strictly needed. This will save us from potential headaches, because we know that the variable we're using that was declared using let
, hasn't been modified. For complex functions, this can be critical.
Data Types
As we've seen in the last section, although it isn't necessary to be explicit about a variable's data type (because the compiler infers the data type), we can choose to be explicit and specify it anyway. In this section, we'll do it that way.
Swift comes with a set of predefined data types, and let us define our custom types, as we'll see in the following sections.
Numeric Data Types
Swift defines the following main numeric data types:
-
Int
: Integer number. -
Double
: A decimal number. -
Float
: A decimal number, but with less capacity than aDouble
.
let legs: Int = 4
let price: Double = 30.50
let temperature: Float = 20.2
Bool
Bool
is a boolean value. It can either be true
or false
. Those are only two possibilities.
let enabled: Bool = true
let isReady: Bool = false
String
String
is a list of characters. It's used to define texts and words. For instance, a person name or a street name could be defined as String
.
let name: String = "Fernando"
A curious fact about String
values in Swift is that emojis are valid String
values!
Why? Why not? 🤷
let fancyHelloWorld: String = "👋 🌎!"
Sometimes we need to print a value in the console. In Swift, we can do that using the print()
function. For example, a "Hello, World!" program can be written like this:
print("Hello, World!")
If you execute this program using the button in the left bar, a message will appear in the console that is at the bottom of the screen.
Comments
Comments can be single-line or multi-line.
Single-liner comments
They begin with a //
and only occupy one line.
let mensaje = "Hello, world!" // This is a message
Multi-line comments
They begin with a /*
and end with */
.
/* This line
prints the
Hello, World! message */
print(mensaje)
Interpolation
How do we create a String
from other variables? An easy way to do it is by concatenating the parts.
let firstName = "Fernando"
let lastName = "Ortiz"
let fullName = firstName + " " + lastName // Fernando Ortiz
The alternative, and the most common way to do it in Swift, is by using interpolation. A String
interpolation consists in "embedding" variables inside a String
. This is done by entering \(<variable>)
inside the String
. For instance:
let middleName = "Martín"
let experience = 7
let description = "My name is \(firstName) \(middleName) \(lastName) and I have \(experience) years of experience as an iOS developer."
print(description) // "My name is Fernando Martín Ortiz and I have 7 years of experience as an iOS developer."
Note that we're also introducing an Int
in the middle of the String
and we aren't getting problems. That's another advantage of String
interpolation.
Collections
Let's collections in Swift to the data types that contain other data types on them and that we can iterate over them on some way.
Array
Also known as lists or vectors, they are data types that store values in a sequential way. What's interesting about Arrays in Swift is that you can't mix values of different types in an Array
. The simplest way of defining an array is this:
let students = ["Federico", "Micaela", "Aldana", "Oscar"]
Note that we are just using String
values. An Array
with the values ["Fernando", 28, true]
won't be valid, unless the Array
is of type [Any]
. The Any
type is a type that can be of "any" type of value, but there are certain disadvantages on using it. My advice here is that, for now at least, AVOID USING IT.
If we want to be explicit about the type, we can also do it:
let numbers1: [Int] = [1, 21, 129]
let numbers2: Array<Int> = [3, 122]
There are many ways of declaring an empty Array
:
let numbersEmpty1: [Int] = [] // Preferred
let numbersEmpty2: Array<Int> = []
let numbersEmpty3 = [Int]()
let numbersEmpty4 = Array<Int>()
Useful methods in Array
If we want to know the number of elements in an Array
, we use the property count
.
let numbers3 = [12, 31, 231, 23]
print("\(numbers3) has \(number3.count) elements")
Curious fact, String
in Swift are actually sequences of characters, so they also have the count
property defined on them. Actually, most of the things we explain in this section apply also for String
values.
let name = "Fernando"
let nameLength = name.count
print("\(name) has \(nameLength) characters.")
If we want to get the first element in an Array
, we'll use .first!
, why we're using !
will be explained later. For now, let's say we're using !
to affirm that we know with full confidence that the first element in the Array
will be there, because the Array
is not empty. Note that if we do .first!
and the Array
is in fact empty, the program will stop executing and crash.
let names = ["Tamara", "Nicolás", "Francisco"]
let firstName = nombres.first! // "Tamara"
If we want to know if an Array
is empty, we'll use the isEmpty
property, that's of type Bool
.
if numbersEmpty.isEmpty {
print("It's empty!")
}
If we'd like to add an element to an Array, we'll use the function
append`.
swift
var namesToFill: [String] = []
namesToFill.append("Fernando")
namesToFill.append("Martín")
print("namesToFill has \(namesToFill.count) elements") // namesToFill has 2 elements
If we want to access an exact position in an Array
, we'll use what's known as a subscript
, and that's basically a number (the index) between square brackets. Indexes in a Swift Array
start from 0. Note that if we want to get an element in a position that is undefined, the program will stop executing and crash.
swift
let names2 = ["Aldana", "Iván", "Marcos", "Cristian"]
let thirdName = names2[2] // "Marcos"
// This will fail when executed.
// let quintoNombre = nombres2[4]
Dictionaries
A Swift Dictionary
is a container that associate identifiers to values. They are key-value structures where the key is generally a String
and the value can be anything. In contrast to Array
, where the Any
type was generally discouraged, creating Dictionary
objects where the key is String
and the value is Any
is perfectly normal and widely used.
The Dictionary
type is similar to what a Map
is in other programming languages.
swift
Any` as its value type, it can be anything.
let person: [String: Any] = [ // Note that when we define a dictionary with
"firstName": "Richard",
"lastName": "Brown",
"age": 27,
"position": "Project Manager"
]
let family: [String: String] = [ // If we define the dictionary as [String:String], values can only be String.
"mother": "Marge",
"father": "Homer",
"son": "Bart",
"daughter": "Lisa",
"baby": "Maggie",
"dog": "Santa's Little Helper"
]
let emptyDictionary: [String: Any] = [:] // The :
sign must be there, so the compiler won't confuse this with an empty Array
.
`
The advantage of using key-value objects is that we can now access to each value by its key. In the Family example:
swift
let mom = family["mother"]! // "Marge"
The !
sign is necessary because we need to tell the compiler that there is an actual value associated to that key.
For
The for
loop is a very well known structure in general programming languages. In Swift it's a bit different. The for
loop let us execute a code fragment a predefined number of times. In practice, the most common use case for it, is to iterate over a collection, such as an Array
or a Dictionary
.
swift
// We'll use these values for the examples.
let names = ["Ayelén", "Lautaro", "Natalia", "Sergio", "Gerardo"]
let person: [String: Any] = [
"firstName": "Richard",
"lastName": "Brown",
"age": 27,
"position": "Project Manager"
]
for-in - Array
To iterate over an Array
, we can use a for-in
loop. In this case, this will print:
swift
Name: Ayelén
Name: Lautaro
Name: Natalia
Name: Sergio
Name: Gerardo
swift
for name in names {
print("Name: \(name)")
}
for-in - Dictionary
We can also iterate over dictionaries using the for-in
loop. In this case, the syntax will be a bit different, because for each element in a Dictionary
wi'll get a pair (tuple
actually, as we'll see in a later section), with the corresponding key and value. Let's see an example. this will print:
swift
lastName : Gomez
position : Project Manager
firstName : Ricardo
age : 27
Also note that a Dictionary
doesn't preserve the fields order. In this example, lastName
was printed third, regardless I defined it first.
swift
for (key, value) in person {
print("\(key) : \(value)")
}
Ranges
An alternative way of using for
loops is by define a Range
. A Range
defines a value interval.
-
(0 ...< 10)
for instance, defines a range from 0 to 10, not including 10. -
(0 ... 10)
defines a range from 0 to 10, 10 included.
Will print:
Number: 0
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
Number: 6
Number: 7
Number: 8
Number: 9
Number: 10
swift
for number in 0 ... 10 {
print("Number: \(number)")
}
We can use the count
property in Array
, to iterate over it in a more "traditional" way. Note that for index in 0 ..< names.count
is very similar to something like for var index = 0 ; index < names.count ; index++
, which is a syntax that isn't valid in Swift. Furthermore, index++
isn't valid Swift code. An alternative to that is index += 1
, but anyway, the most traditional way of using for
isn't valid in Swift.
swift
for index in 0 ..< names.count {
print("Name: \(names[index])")
}
While
The while
loop is widely used in programming languages. However, if I have to be honest, in my almost 7 years as iOS developer, I don't remember having to ever use it. Most of the times, some sort of for
is enough, and it's better also, because every time we use whie
, we are in the risk of accidentally entering into an infinite loop.
In this section, I'll show you how to use while
. But let me repeat, it's almost unnecessary to use it.
while
is a keyword what let us define a condition and a block of code. The condition will be evaluated. It it's true
, then the block of code will be executed. This process will be repeated indefinitely, until the condition is evaluated as
false`.
This example will print:
Name with while: Ayelén
Name with while: Lautaro
Name with while: Natalia
Name with while: Sergio
Name with while: Gerardo
var index = 0
while index < names.count {
print("Name with while: \(names[index])")
index += 1
}
Functions
Functions are blocks of code that can receive a set of parameters, the input, and can return a value as a result, named output.
In Swift, functions are declared with the keyword func
. The structure to declare a function is the following:
func <functionName> (<input1>: <InputType1>, <input2>: <InputType2>, ..., <inputN>: <InputTypeN>) -> <OutputType> { <body> }
Functions may or may not have an output, as previously said. However, if we declare that the function will return a value, we need to return it using the return
keyword.
All of this can be a bit confusing but it's actually very intuitive. Let's see some examples:
func sum(x: Int, y: Int) -> Int {
return x + y
}
func helloWorld() {
print("Hello, World!")
}
func greet(name: String) {
print("Hi, \(name)!")
}
The sum
function receives two parameters, x
and y
, and returns the sum of both of them (it has an Output).
The helloWorld
function doesn't have an input, neither an output. It only prints "Hello, World!"
in the console.
The greet
function receives an input name
and doesn't return anything.
Calling a function
Functions can be called (or "invoqued") by their name, and the names of their arguments explicitly. This is very important and it's different to other languages.
let sumResult = sum(x: 10, y: 14) // 24
helloWorld()
greet(name: "Fernando")
Legibility
Now, if you're detail-oriented people, you probably noticed that greet(name: "Fernando")
doesn't read so natural, to be honest. We can fix that. Don't you think it would be easier to read greet(to: "Fernando")
? However, that would make things worse, because inside the function we'd have a variable to
, with the value "Fernando"
.
Actually, we aren't forced to choose between one way or the other. Swift differentiates between internal names and external names for function parameters. Let's do a modification here, with a new function sayHello
.
func sayHello(to name: String) {
print("Hello, \(name)!")
}
sayHello(to: "Fernando")
Much better! This new function sayHello
receives a single argument with an internal name name
, visible only inside the function body, and an external name to
, visible from the outside of the function (when we call it). If we don't specify different names for the internal and external ones, both will be the same, such as in the first couple of examples.
Let's fix another thing. The function sum
looks a bit weird. Instead of sum(x: 10, y: 14)
, it would be more natural to say sum(10, 14)
or sum(10, to: 14)
, or something similar. Swift let us define empty external names using the keyword _
. The low dash let us specify that we don't want an external name for that parameter.
Let's try to define a divide function that would look like divide(8, by: 2)
.
func divide(_ x: Double, by y: Double) -> Double {
return x / y
}
divide(8, by: 2) // 4
Preconditions
A precondition is a condition that needs to be true
so that a function is executed. If the precondition is false
, then the function will return a different value or throws an error. In Swift, we express the preconditions using the keyword guard
. We can read it as "Ensure this is true. Otherwise, do this instead".
func describeDivision(of x: Double, by y: Double) -> String {
guard y != 0 else {
return "Dividing by zero is not possible."
}
let result = x / y
return "The result of dividing \(x) by \(y) is \(result)."
}
let description1 = describeDivision(of: 20, by: 5)
print(description1) // The result of dividing 20 by 5 is 4.
let description2 = describeDivision(of: 10, by: 0)
print(description2) // Dividing by zero is not possible.
Best practices
Until here, the theory of functions syntax. The following best practices section is entirely optional. Feel free to skip it or read it at the end of this course. However, I will give you a piece of advice that I think is important.
Why do we write functions? There are many reasons, but it's important to understand that a function is an important tool that we have to build abstractions in our code. An abstraction is in some way, to adapt a concept to natural terms, so we can reason on it in a more intuitive way. Considering we write code to define a solution to a problem in a way that not only the computer will understand (which is the simplest part), but also other people will be able to read it, understand it, and feel confident modifying it and extending it, then abstractions will allow us to improve the readability of our code.
A good function needs to be understandable, clear, readable and modifiable. I'll list, next, a couple of best practices when writing functions.
Short functions
A function shouldn't be longer than 10 or 15 lines of code, in general. If a function is longer than that, it's convenient to split it in smaller functions, and then compose those smaller functions into a bigger function. A single-line function isn't necessarily bad, if it bring clarity to the code.
Readable functions
This is try not only to the function body, but also to the function invocation part. Code inside a function needs to be a well defined sequence of steps. The invocation of that function needs to be read naturally.
Cohesive functions
A function must do one thing, and do it right. This is even more important than writing small functions. If a function is longer than 25 lines or code, but every line of code contribute to the same goal without adding cognitive load to the code reader, then there is no reason to modify it.
Tuples
A tuple is the first custom data type we'll see during this course. Tuples are sets of data with a name and without any additional functionality (unlike classes, for instance). A tuple can be defined using the keyword typealias
, or when it's needed.
typealias FullName = (firstName: String, lastName: String)
let name: FullName = (firstName: "Fernando", lastName: "Ortiz")
print("The first name is \(name.firstName) and the last name is \(name.lastName)") // The first name is Fernando and the last name is Ortiz
The most common use case for tuples is to let a function return more than a single value.
func divide(_ dividend: Int, by divisor: Int) -> (quotient: Int, remainder: Int) {
// We assume we're not dividing by zero
return (
quotient: Int(dividend / divisor), // Integer value without the decimal values
remainder: dividend % divisor // Modulo operator returns the remainder of a division
)
}
let result = divide(13, by: 4)
print("Quotient: \(result.quotient) ;; Remainder: \(result.remainder)") // Quotient: 3 ;; Remainder: 1
Destructuring
Destructuring is a way of split a tuple into its components. It's done by assigning a tuple to a set of variables in parenthesis, and separated by comma:
let (myQuotient, myRemainder) = divide(17, by: 3)
print("Quotient: \(miQuotient) ;; Remainder: \(miRemainder)") // Quotient: 5 ;; Remainder: 2
// We can also ignore any of those values with a low dash.
let (quotient, _) = divide(50, by: 4) // We are ignoring the remainder here
print("The result of dividing 50 by 4 is \(quotient)") // The result of dividing 50 by 4 is 12
Anonymous components
A tuple can also have their components are anonymous values. Even when it's not recommendable, it's possible. In this case, we can access to its components using their index. For example: tuple.0
for the first element, tuple.1
for the second one, etc. The alternative is to use destructuring let (first, second) = tuple
, which is preferrable.
Let's see a last example, generating intervals using a function that will return the lower and the upper extremes in a tuple:
func generateInterval(value: Int, amplitude: Int) -> (Int, Int) {
let lower = valor - amplitud
let upper = valor + amplitud
return (lower, upper)
}
// Using destructuring - Preferrable.
let (lower, upper) = generateInterval(value: 10, amplitude: 2)
print("[\(lower);\(upper)]") // [8;12]
// Accessing by order to the anonymous components
let interval = generateInterval(value: 10, amplitude: 2)
print("[\(interval.0);\(interval.1)]") // [8;12]
Optional data types
Let's suppose we want to describe a person. What attributes does a person have? We could say firstName
, lastName
, identifier
, age
, etc. All of them exist. A person always have a firstName
. A person always have lastName
. All of them are of type String
. String
is a non-optional type.
However, this isn't true for every attribute in a person. What about middleName
? A person may not have middleName
, it's OPTIONAL.
Optional data types in Swift let us design exactly that, values that may not be there, they may be nil
. You might have already noticed that in all examples so far, we have used non-optional values, and only in specific places we have used !
to note that we are sure that the value is not nil
, like in first!
, for Array
.
Every data type have its optional counterpart, that is defined with the ?
suffix. So, for instance:
-
String?
is aString
that might have aString
value ornil
. -
Int?
is anInt
that might benil
. -
Bool?
is aBool
that might benil
. -
[String]?
is anArray
ofString
that might benil
. -
(quotient: Int, remainder: Int)?
is a type composed by twoInt
values. The tuple might benil
. Its components, on the other hand, can't benil
. -
(firstName: String, lastName: String?)
is a tuple composed by twoString
values. The first one (firstName
) can't benil
. The second one might benil
. Thetuple
isn't optional.
typealias PersonData = (firstName: String, middleName: String?, lastName: String, age: Int)
let fernando: PersonData = (
firstName: "Fernando",
middleName: "Martín",
lastName: "Ortiz",
age: 29
)
let nicolas: PersonData = (
firstName: "Nicolás",
middleName: nil, // it's nil, it doesn't exist
lastName: "Duarte",
age: 29
)
Force unwrap optionals
If we're sure that an optional value isn't nil, we can force it to convert it into its non-optional counterpart. To do that, we can add a !
at the end of the variable name, and it will become non-optional.
Let's see some examples. I have to say, however, that this is a VERY BAD PRACTICE. If we add a !
to get the non-optional counterpart of a value that is actually nil
, the app will crash because of a fatal error.
print("Fernando's middle name is \(fernando.middleName)")
// Fernando's middle name is Optional("Martín")
// We have to get the non optional value of Fernando's middle name.
print("Fernando's middle name is \(fernando.middleName!)")
// Fernando's middle name is Martín
if-let
This is one of the "correct" ways of handling optional values. Inside an if
, we can include an assignment from an optional that will be executed just in the case that the value isn't nil
. For example:
if let middleName = fernando.middleName {
// Inside here, middleName isn't optional
print("Fernando's middle name is \(middleName)") // Fernando's middle name is Martín
}
if let middleName = nicolas.middleName {
// This won't be executed
print("Nicolas' middle name is \(middleName)")
} else {
print("Nicolas doesn't have a middle name")
}
guard-let
guard
is the opposite of if
. It will execute its body only in the case that the condition isn't met and it's used to express a precondition. guard
can also be used to unwrap optionals in a safe way, such as if-let
.
func getMiddleName(of person: PersonData) -> String {
guard let middleName = person.middleName else {
return "\(person.firstName) doesn't have middle name"
}
return "\(person.firstName)'s middle name is \(middleName)"
}
print(getMiddleName(for: fernando)) // Fernando's middle name is Martín
print(getMiddleName(of: nicolas)) // Nicolás doesn't have middle name
Implicitly unwrapped optionals
An implicitly unwrapped optional is an optional data type, that we can assign nil
to, obviously, but that when we need to use it, it will be considered non optional for most of the cases.
Of course, the app will crash in case the value is actually nil
and we try to use it as a non-nil value. And, of course, this is a bad practice. However, this is used a lot in iOS, especially in frameworks that are written in Objective-C (Swift's predecessor, in Apple's platforms).
let nonNilMiddleName: String! = "Marcos"
let nilMiddleName: String! = nil
func printMiddleName(_ middleName: String) {
print(middleName)
}
printMiddleName(nonNilMiddleName) // Marcos
// printMiddleName(nilMiddleName) // CRASH!
Optional chaining
Let's suppose we have an optional tuple.
var optionalPerson: PersonData?
optionalPerson = (
firstName: "Nicolás",
middleName: nil,
lastName: "Duarte",
age: 29
)
If in this case, that optionalPerson
is optional, we need to access any of its components, being them attributes or methods (such as in classes, as we'll see later), we can use a feature called Optional chaining.
Optional chaining lets us access members of the optional object using ?.
instead of .
.
let firstName = optionalPerson?.firstName // type: String?, because, although `firstName` in `PersonData` is `String`, we don't know if `optionalPerson` exists, or if it's `nil`.
Exercises
- Define a tuple that describes an address, with fields like
city
,state
,zipCode
,country
, etc. Feel free to experiment and use dictionaries, optional values, and everything we've learned in this lesson. - Inside the address, define some optional components, like
floorNumber
andapartment
. - Create three addresses as constants.
- Write a function that gets an address and prints it as a well formatted
String
. Use interpolation. - Write a function that gets an
Array
of addresses and returns anString
with"floor: \(floor) ; apt: \(apartment)"
ONLY for those addresses with non-nil floor and apartment.
Top comments (0)