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:
struct
; methods are implemented separately in impl
blocks.class
and works mostly like Swiftclass
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++ projectsObject-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 struct
s, 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:
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:
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:
String
Int
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
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:
/// 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:
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 mutatingsearch(byTitle title: String) -> [Book]
— this must return an array as potentially many books could matchsearch(byAuthor name: String) -> [Book]
— same as abovesearch(byISBN isbn: Int) -> Book
— ISBN are unique to a given book, so this returns just one objectshowDetails(forBook book: Book)
— this method prints the details in nicely-formatted wayshowAll()
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]
]
formattedPrint()
— prints the details of the book in a nice formatCatalogue
struct's showDetails
method to use the Book
object's formattedPrint
method