Skip to main content

essential swift in one hour

·3177 words·15 mins

Learn essential Swift in one hour #

https://www.hackingwithswift.com/articles/242/learn-essential-swift-in-one-hour

This is a summary of the first 14 days of 100 days of swift

Constants and Variables #

Constants let and variables var.

  • Prefer consts over variables

Dictionaries #

Dictionaries (like hashes)

  • remembers orders
  • creates duplicates
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var employee = [
    "name": "Taylor",
    "job": "Singer"
]
// overwrite the value
employee["name"] = "Ed"
// add a new dictionary item
employee["surname"] = "Sherrin"
print(employee)
//["job": "Singer", "name": "Ed", "surname": "Sherrin"]

Sets #

Sets are similar to arrays, except you can’t add duplicate items

  • doesn’t remember order
  • doesn’t create duplicates
  • don’t store in a particular order
  • can use contains() -> is effectively instant
  • add new elements using insert()
1
2
3
4
5
var numbers = Set([1, 1, 3, 5, 7])
// add a new item
numbers.insert(9)
print(numbers)
// [7, 3, 9, 5, 1]

Enums #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum Weekday {
    case monday, tuesday, wednesday, thursday, friday
}
var day = Weekday.monday
day = .friday

// enum type annotation
enum UIStyle {
    case light, dark, system
}

var style: UIStyle = .light

Type annotations #

Sample type annotations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let player: String = "Roy"
var luckyNumber: Int = 13
let pi: Double = 3.141
var isEnabled: Bool = true
var albums: Array<String> = ["Red", "Fearless"]
var user: Dictionary<String, String> = ["id": "@twostraws"]
var books: Set<String> = Set(["The Bluest Eye", "Foundation"])
// Alternate syntax for arrays and dicts
var albums: [String] = ["Red", "Fearless"]
var user: [String: String] = ["id": "@twostraws"]
// empty arrays can be written either of these ways
var teams: [String] = [String]()
var clues = [String]()

Switch statement #

Must be exhaustive ….

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum Weather {
    case sun, rain, wind
}

let forecast = Weather.sun

switch forecast {
case .sun:
    print("A nice day.")
case .rain:
    print("Pack an umbrella.")
default:
    print("Should be okay.")
}

Loops #

Loops are as expected,

  • continue to skip the current loop iteration
  • break to exit the loop and skip all remaining iterations
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for os in platforms {
    print("Swift works on \(os).")
}
for i in 1...12 {
    print("5 x \(i) is \(5 * i)")
}
// if you don't need the iterator
for _ in 1...5 { }
while count > 0 {
    print("\(count)…")
    count -= 1
}

Functions #

Functions have the following characteristics:

  • parameters inside params
  • -> return type
  • if function has only a single line of code you can remove the return
1
2
3
func rollDice(diceSides: Int) -> Int {
  Int.random(in: 1...diceSides)
}

Return multiple values from functions #

Return a tuple

1
2
3
4
5
6
7
8
9
func getUser() -> (firstName: String, lastName: String) {
    (firstName: "Taylor", lastName: "Swift")
}

let user = getUser()
print("Name: \(user.firstName) \(user.lastName)")
//if you don't need all the values, you can use `_` to ignore some
let (firstName, _) = getUser()
print("Name: \(firstName)")

Unnamed params for functions #

Pass in a value without a name

1
2
3
4
5
6
func isUppercase(_ string: String) -> Bool {
    string == string.uppercased()
}

let string = "HELLO, WORLD"
let result = isUppercase(string)

Alias params for functions #

Alias a params for the caller and callee

1
2
3
4
5
6
7
func printTimesTables(for number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(for: 5)

Default values for functions #

Pass in a default value if there is none passed in.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func greet(_ person: String, formal: Bool = false) {
    if formal {
        print("Welcome, \(person)!")
    } else {
        print("Hi, \(person)!")
    }
}
// now we can call `greet()` in two ways
greet("Tim", formal: true)
greet("Taylor")

Handle errors in functions #

Begin by defining errors

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
enum PasswordError: Error {
    case short, obvious
}
// uses `throws`
func checkPassword(_ password: String) throws -> String {
    if password.count < 5 {
        throw PasswordError.short
    }

    if password == "12345" {
        throw PasswordError.obvious
    }

    if password.count < 10 {
        return "OK"
    } else {
        return "Good"
    }
}

Try / Catch blocks #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let string = "12345"

do {
    let result = try checkPassword(string)
    print("Rating: \(result)")
} catch PasswordError.obvious {
    print("I have the same combination on my luggage!")
} catch {
    print("There was an error.")
}

Closures #

A closure is a chunk of code that we can pass around and call when we want. A simple closure looks like this:

1
2
3
4
let sayHello(){
  print("Hi there!")
}
sayHello()

If you want to pass parameters to the closure, you must add them inside the brackets and with the keyword in to denote the end of the method signature

1
2
3
4
let sayHello = { (name: String) -> String in
  "Hi \(name)!"
}
sayHello("Andrew")

Builtin Closures are used extensively. For example, an array method called filter(), if the closure returns true, get returned in a new array, we could filter an array to include only names beginning with T:

1
2
3
4
5
let team = ["Gloria", "Suzanne", "Tiffany", "Tasha"]

let onlyT = team.filter({ (name: String) -> Bool in
    return name.hasPrefix("T")
})

Trailing closures and shorthand syntax #

This example (from above), can be refactored:

1
2
3
let onlyT = team.filter({ (name: String) -> Bool in
    return name.hasPrefix("T")
})

As there is only a single line, we can omit the return:

1
2
3
let onlyT = team.filter({ (name: String) -> Bool in
    name.hasPrefix("T")
})

filter() must be given a function that accepts one item from and array and returns true. Because the function must behave like that, we can omit the type:

1
2
3
let onlyT = team.filter({ name in
  name.hasPrefix("T")
})

Trailing Closure Syntax. If the closure expression is the method’s only argument and you provide that expression as a trailing closure, you can omit the ()

1
2
3
let onlyT = team.filter { name in
    name.hasPrefix("T")
}

Finally, sift provides short parameter names for us (suggest only use for a single parameter)

1
2
3
let onlyT = team.filter {
  $0.hasPrefix("T")
}

Structs #

Structs let us create our own custom data types.

  • it also silently generates memberwise initializer based on the properties
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct Album {
    let title: String
    let artist: String
    var isReleased = true

    func printSummary() {
        print("\(title) by \(artist)")
    }
    // if you want to be able to change a property from a method
    mutating func removeFromSale() {
        isReleased = false
    }
}

let red = Album(title: "Red", artist: "Taylor Swift")
print(red.title)
red.printSummary()

Computed properties #

A computed property is calculated every time it’s accessed.

1
2
3
4
5
6
7
8
9
struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}

If we want to be able to right to vactionRemaining, we need to provide the getter and settter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var vacationRemaining: Int {
    get {
        vacationAllocated - vacationTaken
    }

    set {
        // new value is provided by Swift and stores what the user passed in
        vacationAllocated = vacationTaken + newValue
    }
}

Property observers #

These are callbacks, didSet runs when the property just changed and willSet runs before the property changed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            // if you don't set the parameter, swift provides this as newValue
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 896

Custom initializers #

Swift automatically generates memberwise initializers for you, however you can override this explicitly.

  • note no func
  • note no return value
1
2
3
4
5
6
7
8
9
struct Player {
    let name: String
    let number: Int

    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}

Access Control #

Four access controls are the most common but there are more.

  • Use private for “don’t let anything outside the struct use this.”
  • Use private(set) for “anything outside the struct can read this, but don’t let them change it.”
  • Use fileprivate for “don’t let anything outside the current file use this.”
  • Use public for “let anyone, anywhere use this.”
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct BankAccount {
    // reading is fine, but writing can't be done outside the struct
    private(set) var funds = 0

    mutating func deposit(amount: Int) {
        funds += amount
    }

    mutating func withdraw(amount: Int) -> Bool {
        if funds > amount {
            funds -= amount
            return true
        } else {
            return false
        }
    }
}

Static properties and methods #

Swift allow you to add static methods and properties directly to the struct rather than an instance

1
2
3
4
struct AppData {
    static let version = "1.3 beta 2"
    static let settings = "settings.json"
}

Classes #

Classes also allow us to create custom data types, however vary from structs in 5 key ways.

Inheritance #

You can inherit classes from a base class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Employee {
    let hours: Int

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

    func printSummary() {
        print("I work \(hours) hours a day.")
    }
}

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

let novall = Developer(hours: 8)
novall.work()
novall.printSummary()

in order to change a method’s functionality, you need to override it.

1
2
3
override func printSummary() {
    print("I spend \(hours) hours a day searching Stack Overflow.")
}

Initializers #

Three key points

  • Swift won’t generate memberwise initializers for a class
  • if a child class has custom initializers, it must call the parent’s initializer after setting itself up
  • If a subclass doesn’t have an initializer, it automatically inherits from the parent class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Vehicle {
    let isElectric: Bool

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

class Car: Vehicle {
    let isConvertible: Bool

    init(isElectric: Bool, isConvertible: Bool) {
        self.isConvertible = isConvertible
        super.init(isElectric: isElectric)
    }
}

Copy by reference #

All copies of a class instance share their data, changes to one will automatically change the data in others. In comparison, struct copies don’t share their data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Singer {
    var name = "Adele"
}

var singer1 = Singer()
var singer2 = singer1
singer2.name = "Justin"
print(singer1.name)
// Justin -> not Adele
print(singer2.name)
// Justin

Deininitializer #

Classes can have a deinitializer that calls when the last reference to an object is destroyed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class User {
    let id: Int

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

    deinit {
        print("User \(id): I'm dead!")
    }
}

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

Variable properties #

The final difference is that classes allow us to change variable properties when the class itself is constant, as a result classes don’t need the mutating keyword with methods that change their data.

1
2
3
4
5
6
7
class User {
    var name = "Paul"
}

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

Protocols #

Protocols are interfaces. They define functionality and swift ensures conformance. The protocol only defines the methods, not the implementation. Once you have a protocol, you can make a data types conform to it by implementing the required functionality (all protocols must be implemented)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
protocol Vehicle {
    func estimateTime(for distance: Int) -> Int
    func travel(distance: Int)
}
struct Car: Vehicle {
    func estimateTime(for distance: Int) -> Int {
        distance / 50
    }

    func travel(distance: Int) {
        print("I'm driving \(distance)km.")
    }
}

This allows us to write a function that accepts any kind of type that conforms to Vehicle because Swift knows it implemnents both estimateTime() and travel().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func commute(distance: Int, using vehicle: Vehicle) {
    if vehicle.estimateTime(for: distance) > 100 {
        print("Too slow!")
    } else {
        vehicle.travel(distance: distance)
    }
}

let car = Car()
commute(distance: 100, using: car)

Protocols can also require properties. You can define as a get that might be a constant or computed property and one marked with get set that might be a variable or computed property with a getter and setter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
protocol Vehicle {
    var name: String { get }
    var currentPassengers: Int { get set }
    func estimateTime(for distance: Int) -> Int
    func travel(distance: Int)
}
// all conforming types must implement the two properties,
// like this for `Car`
let name = "Car"
var currentPassengers = 1
Tip You can conform to as many protocols as you need, just by listing them separated by commas

Extensions #

Extensions let us add functionality to any type, both user defined and built-in!

For example, swift strings have a method to trim whitespace and new lines, but it’s quite long so you could turn it into an extension.

  • Use ed or ing to return a copy of the data
  • Use the verb to return a mutated version of the data
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var quote = "   The truth is rarely pure and never simple   "

// return a copy of the data
extension String {
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}
let trimmed = quote.trimmed()

// return a mutate form of the data
extension String {
    mutating func trim() {
        self = self.trimmed()
    }
}
quote.trim()

Extensions can also add a computed properties to types like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
extension String {
    var lines: [String] {
        self.components(separatedBy: .newlines)
    }
}
let lyrics = """
But I keep cruising
Can't stop, won't stop moving
"""

print(lyrics.lines.count)
// 2

Protocol Extensions #

Protocol extensions extend a whole protocol to add computed properties and method implementations, so any types conforming to that protocol get them.

For example, Array, Dictionary and Set all conform to the Collection protocol so we can add a computed property to all three of them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
extension Collection {
    var isNotEmpty: Bool {
        isEmpty == false
    }
}
let guests = ["Mario", "Luigi", "Peach"]

if guests.isNotEmpty {
    print("Guest count: \(guests.count)")
}

This means that we can list required methods in a protocol and then add default implementations inside a protocol extension. All conforming types then get to use those default implementations or provide their own.

Optionals #

Optionals represent the absence of data, like a nil. Think about this code:

1
2
3
4
5
let opposites = [
    "Mario": "Wario",
    "Luigi": "Waluigi"
]
let peachOpposite = opposites["Peach"]

peachOpposite doesn’t exist, so this can’t be a regular string. Swift’s solution is called optionals. An optional string might have a string or a nil, any kind of data can be optional including Int, Double and Bool as well as enums, structs, and classes.

Unwrapping optionals with if #

Swift won’t let us use the optional data directly as it might be empty, we have to unwrap the optional to use it. There are multiple ways to do this, but the most common looks like this:

1
2
3
if let marioOpposite = opposites["Mario"] {
  print("Mario's opposite is \(marioOpposite)")
}

Unwrapping optionals with guard (unless) #

Swift has another way of unwrapping using guard.

  • if let runs the code inside the braces if the optional had a value;
  • guard let runs the code if the optional doesn’t have a value (ala unless)
1
2
3
4
5
6
7
8
func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")
        return
    }

    print("\(number) x \(number) is \(number * number)")
}

If you use a guard to check a function’s inputs are valid, Swift requires you to use return if the check fails. If successful, you can use the let after the guard clause finishes.

Tip You can use a guard with any condition, including ones that don’t unwrap optionals.

Unwrapping optionals using Nil coalescing #

Swift has a third way of unwrapping optionals called the nil coalescing operator. It unwraps an optional and provides a default value if the optional is empty.

1
2
let tvShows = ["Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"

The nil coalescing operator is useful in many places optionals are created, for example, creating an Int from a string returns an optional Int? because the conversion might have failed so we can provide a default value:

1
2
3
let input = ""
let number = Int(input) ?? 0
print(number)

Optional chaining #

Optional chaining reads optionals inside optionals, like this:

1
2
3
let names = ["Arya", "Bran", "Robb", "Sansa"]
let chosen = names.randomElement()?.uppercased()
print("Next in line: \(chosen ?? "No one")")

If there’s a random element returned, uppercase it.

Optional try? #

When calling a function that might throw errors, we can use try? to convert its result into an optional containing a value on success or nil otherwise.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum UserError: Error {
    case badID, networkFailed
}

func getUser(id: Int) throws -> String {
    throw UserError.networkFailed
}

if let user = try? getUser(id: 23) {
    // getUser always throws an error so we never get here.
    print("User: \(user)")
}