At the start of Year 12, we began by writing simple programs that just consisted of a series of statements, one after the other. These programs were not very flexible or reusable! The main issue was that the code

By the end of the programming unit, we had then moved to placing sections of code inside functions. These could unctions could be called repeatedly and passed different parameter values to make them more flexible. This made for a massive leap in the sophistication, including the flexibility and reusability, of our programming.

We are about to make another such leap and explore a different way of designing our programs.

Before we go on, here's some tips for users of other programming languages this year:

  • Rust supports objects through struct; methods are implemented separately in impl blocks.
  • Java and C# support objects through class and works mostly like Swift
  • C++ supports objects through a class declaration, with attributes and function signatures, in a header file (.h); separately, the implementation for the functions are given in an implementation file (.cpp) — try to follow this structure as it is considered a convention for C++ projects

Object-oriented programming (OOP) refers to a program being composed of several objects interacting with each other. Just like we could define functions and re-use them, we can create use objects containing data and behaviour and re-use them.

In Swift, objects are created from structs, the blueprints for what data to store — then we create multiple objects from those blueprints, each containing different data.

Objects consist of the data that will be stored in them. This makes them like a bit dictionary, storing related data together. What makes them more powerful than dictionaries is that objects also contain methods that operate on the instance variables. A method is like a function — except it belongs to the object.

Let's say you wanted to store data on supermarket stock, namely:

  • the item names
  • the number of items in stock
  • the prices

You could store this data in three separate arrays as shown below.

var items = [
    "Bread",
    "Milk",
    "Butter"
]

var stock = [
    100,
    40,
    30
]

var prices = [
    1.49,
    3.49,
    4.99
]

Question: What problems do you think such a structure could encounter?

Answer: If a new item must be added or an item must be removed, it must be removed from three separate variables. This is not flexible. After all, what happens if a new item is added to one list but not to the others?

var items = [
    "Bread",
    "Milk",
    "Cheese",  // New item!
    "Butter"
]

var stock = [
    100,
    40,
    30
]

var prices = [
    1.49,
    3.49,
    4.99
]
🍞 🥛 🧀 🧈
1.49 3.49 4.99 ??? ???

To solve this, you could combine the arrays into an array of dictionaries. With each item's data together (all the info for bread is in one dictionary, etc.), it's easier to ensure the data is valid — adding cheese would be its own separate dictionary, its own separate stock, its own separate price…

var items: [[String: Any]] = [
    [
        "name": "Bread",
        "stock": 100,
        "price": 1.49
    ], [
        "name": "Milk",
        "stock": 40,
        "price": 3.49
    ], [
        "name": "Butter",
        "stock": 30,
        "price": 4.99
    ]
]

However, this means using the Any type (as the dictionary values can be either a String, an Int, or a Double). Thus, any time you access the dictionary values, you need to cast them to ensure the correct data type, adding the additional step of unwrapping optional values due to casting.

Using Any in Swift is somewhat equivalent to Python's lack of type enforcement. It generally means you aren't clear on what kind of data you're dealing with. This lack of clarity is a key indicator that your code may not be flexible or robust, so you should avoid it where possible. If you aren't sure, ask your kaiako.

Nevertheless, let's continue with the Any-typed values; it's valid Swift (although not great). We can write a 'sell' function that accepts the dictionary as an argument, the item to be sold, and the quantity to sell. Let's see what that looks like:

/// Sell some of the named item.
func sell(from items: inout [[String: Any]], name: String, quantity: Int) {
    // Find the item. If not found, end early.
    guard let item = items.first, let itemName = item["name"] as? String { else {
        print("No such item.")
        return
    }

    // Make sure there's enough stock.
    guard let stockValue = item["stock"], let stock = stockValue as? Int, quantity <= stock else {
        // ⬆️ Look at this condition! Even if you don't understand it all, it's SOOOOOO long!
        item["stock"] = stock - quantity
    }

    print("Sold \(quantity)x \(name)")
}

# Sell some butter.
sell(items, "Butter", 3)

As you can see, we need to handle:

  • passing in the item array
  • finding the item in the array
  • casting the stock and price values
  • handling optionals

On lines 4 and 10, you can see as String? and as Int?. This is a way to cast a value with an unknown type (i.e. Any) to a known type. As we can't be sure if the value will really be the type we expect (given that all dictionary values are Any), we cast to an optional type (i.e. String?) so that we can safely handle when the value's type is not the one we expect.

As before, using Any in your code is a key indicator that you haven't reasoned about what kind of data your program will use. If you find your code uses as String?, as Int?, etc., rethink your data structures. For example, you could use…

The dictionaries we've seen are a bit of a mess. They solve a problem that arrays couldn't fix but they introduce other problems.

Instead of using dictionaries with values of Any type, we can create a definition for an object that would contain all of the data for an item, just like a dictionary; however, we can also specify the data types involved — this means we won't need to cast types anymore.

To create this definition, Swift uses the struct keyword — we will define what a structure looks like.

/// A supermarket item.
struct Item {
    let name: String
    var stock: Int
    var price: Double
}

In the example, we state that:

  • the name is a String
  • the stock is an Int
  • the price is a Double

A struct is just the definition of an object. Think of it as a blueprint; we've got the plans for an object but we haven't built one yet. Once we have a definition for the object, we can then create objects based on that definition:

// Create Item objects for each type.
var bread = Item(name: "Bread",
                 stock: 100,
                 price: 1.49)
var milk = Item(name: "Milk",
                 stock: 40,
                 price: 3.49)
var butter = Item(name: "Butter",
                 stock: 30,
                 price: 4.99)

In the example, three objects are created

  • bread
  • milk
  • butter

These three objects are of the type 'Item' using the object definition we gave earlier.

We can create a method, a function that belongs to a struct in order to group data and behaviour together.

Just a reminder for this example:

  • the data is the name, stock, and price
  • the behaviour is the ability to sell an item
/// A supermarket item.
struct Item {
    let name: String
    var stock: Int
    var price: Double

    /// Sell the item.
    mutating func sell(quantity: Int) {
        if quantity <= stock {
            stock = stock - quantity
        }
    }
    // ⬆️ Look at how much easier this method is!
}

// Create Item objects for butter.
var butter = Item(name: "Butter",
                 stock: 30,
                 price: 4.99)

// Sell the butter!
butter.sell(20)

Because the sell function is now a part of the struct, every object we create is able to perform the behaviour. This is called encapsulation; the data and behaviour are encapsulated into one place.

Just for context, previously the function call looked like sell(items, "Butter", 20) — now it's butter.sell(20). Much simpler at the call site as well as in the definition. Using objects can make your code safer, easier to reason about, and more convenient to write!

As with any other type of data, you can store objects in arrays and dictionaries.

// Array of Item objects.
let products: [Item] = [
    Item(name: "Bread", stock: 100, price: 1.49),
    Item(name: "Milk", stock: 40, price: 3.49),
    Item(name: "Butter", stock: 30, price: 4.99)
]

// Searching for an item with a specific name using .first()
if let bread = products.first { item in
    return item.name == "Bread"
}

// Searching for products in a price range using .filter()
let expensiveProducts = products.filter { item in
    return item.price >= 3.00
}

Objects can also contain collections.

struct Item {
    let name: String
    var stock: Int
    var price: Double
    var allergenWarnings: [String]?
}

let products: [Item] = [
    Item(name: "White bread", stock: 35, price: 3.49, allergenWarnings: ["Gluten"]),
    Item(name: "Milk bread", stock: 20, price: 4.99, allergenWarnings: ["Dairy", "Gluten"]),
    Item(name: "Literally just a rock", stock: 1, price: 0.49, allergenWarnings: nil)
]

// Find products with gluten inside.
let glutenProducts = products.filter { item in
    return item.allergenWarnings.contains("Gluten")
}

let products = [Item…, Item…, Item…]

products.forEach { item in
    print("NAME: \(item.name) - PRICE: $\(String(format: "%.2f", item.price))")
}

let productsArrayOfDicts: [[String: Any]] = [
    ["name": "Bread", "stock": 100, "price": 1.49],
    ["name": "Milk", "stock": 40, "price": 3.49],
    ["name": "Butter", "stock": 30, "price": 4.99]
]

// Use map to convert the dictionaries to objects.
let products = productsArrayOfDicts.compactMap { dict in
    // Ensure all dicts have the expected keys and values.
    guard let name = dict["name"] as? String, let stock = dict["stock"] as? Int, let price = dict["price"] as? Double else {
        // Handle failed casts by exiting.
        exit(0)
    }

    // If the keys and values are usable, create an object.
    return Item(name: name, stock: stock, price: price)
}

Create a program that allows you to add and lookup details of books in a library catalogue.

The program will let you:

  • add a book to the catalogue
  • show the details of a book
  • search for books by title, author, or ISBN
  • display the titles and authors of all library books

Create the following two structs:

  • Book
  • Catalogue

In the Catalogue structs, add the following methods:

  • add(title:String, author: String, isbn: Int) — this method must be mutating
  • search(byTitle title: String) -> [Book] — this must return an array as potentially many books could match
  • search(byAuthor name: String) -> [Book] — same as above
  • search(byISBN isbn: Int) -> Book — ISBN are unique to a given book, so this returns just one object
  • showDetails(forBook book: Book) — this method prints the details in nicely-formatted way
  • showAll()

To get you started, here is some dummy data you can use. It is stored as an array of arrays:

// Name, Author, ISBN number
let booksArray: [[Any]] = [
    ["Huwi's First Egg", "Kat Mereweather", 9780995124615],
    ["The Kuia and the Spider", "Patricia Grace", 9780140503876],
    ["Cousins", "Patricia Grace", 9781742539690],
    ["Where the Wild Things Are", "Maurice Sendak", 9780099408390],
    ["Under the Mountain", "Maurice Gee", 9780143305019],
    ["Under the Mountain", "Sophie Cook", 9781409069386],
    ["Electric City", "Patricia Grace", 9781742539713],
    ["How Māui Found His Father and the Magic Jawbone", "Peter Gossage", 9780143505198]
]

  • Ensure your program does not allow multiple ISBN that are the same.
  • Adjust your program to allow deleting books from the catalogue.
    • Remember to make your collection mutable (a variable) and use a mutating function!
  • In the 'Book' struct, add the following method:
    • formattedPrint() — prints the details of the book in a nice format
  • Adjust your Catalogue struct's showDetails method to use the Book object's formattedPrint method