SwiftUI Stanford Course Assigned Reading Notes (Part 2)

August 31, 2020

Book Notes

SwiftUI

These notes are continued from Part 1 found here.

Functions

func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

print(greet(person: "Olivia"))
// Prints "Hello, Olivia!"

Functions can have multiple input parameters, which are written within the function’s parentheses, separated by commas.

Functions are not required to define a return type. That’s the -> String part in the above code block. Functions without a defined return type return a special value of type Void. This is simply an empty tuple, which is written as (). Return values can be ignored, but a function that says it will return a value must always do so.

If the tuple type to be returned from a function has the potential to have “no value” for the entire tuple, you can use an optional tuple return type to reflect the fact that the entire tuple can be nil.

An optional tuple type such as (Int, Int)? is different from a tuple that contains optional types such as (Int?, Int?). With an optional tuple type, the entire tuple is optional, not just each individual value within the tuple.

If the entire body of the function is a single expression, the function implicitly returns that expression.

You write an argument label before the parameter name, separated by a space:

func someFunction(argumentLabel parameterName: Int) {
   // do something
}

If you don’t want an argument label for a parameter, write an underscore (_) instead of an explicit argument label for that parameter.

func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
    // do something
}
someFunction(1, secondParameterName: 2)

You can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
    // do something
}

someFunction(parameterWithoutDefault: 4)
// parameterWithDefault is 12

You can define a constant or variable to be of a function type and assign an appropriate function to that variable:

var mathFunction: (Int, Int) -> Int = addTwoInts

This can be read as: “Define a variable called mathFunction, which has a type of ‘a function that takes two Int values, and returns an Int value.’ Set this new variable to refer to the function called addTwoInts.”

Closures

Closures take one of three forms:

For characters in strings, “greater than” means “appears later in the alphabet than”:

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
{ (parameters) -> return type in
    statements
}

Structures & Classes

Whenever you define a new structure or class, you define a new Swift type. Give types UpperCamelCase names (such as SomeStructure and SomeClass here) to match the capitalization of standard Swift types (such as StringInt, and Bool). Give properties and methods lowerCamelCase names (such as frameRate and incrementCount) to differentiate them from type names.

The code below creates a new instance of the class or structure, with any properties initialized to their default values. 

// A struct
struct Resolution {
    var width = 0
    var height = 0
}

// Creating a new instance of that struct
let someResolution = Resolution()

You can access the properties of an instance using dot syntax.

print("The width of someResolution is \(someResolution.width)")

You can drill down into subproperties, such as the width property in the resolution property of a VideoMode:

print("The width of someVideoMode is \(someVideoMode.resolution.width)")

You can also use dot syntax to assign a new value to a variable property:

someVideoMode.resolution.width = 1280

All structures and enumerations are value types in Swift.

Unlike value types (struct), reference types (class) are not copied when they are assigned to a variable or constant, or when they are passed to a function.

// Example of a struct
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
cinema.width = 2048
// When you check the value of hd.width, it will still be 1920

// Example of a class
let tenEighty = VideoMode()
tenEighty.frameRate = 25.0
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
// Now when you check the value of tenEighty.frameRate, it will be 30.0

This example also shows how reference types can be harder to reason about. If tenEighty and alsoTenEighty were far apart in your program’s code, it could be difficult to find all the ways that the video mode is changed. Wherever you use tenEighty, you also have to think about the code that uses alsoTenEighty, and vice versa. In contrast, value types are easier to reason about because all of the code that interacts with the same value is close together in your source files.

tenEighty and alsoTenEighty are declared as constants, rather than variables. However, you can still change tenEighty.frameRate and alsoTenEighty.frameRate because the values of the tenEighty and alsoTenEighty constants themselves don’t actually change.

Because classes are reference types, it’s possible for multiple constants and variables to refer to the same single instance of a class behind the scenes. (The same isn’t true for structures and enumerations, because they are always copied when they are assigned to a constant or variable, or passed to a function.) This is where the === operator comes into play. Use === to find out whether two constants or variables refer to exactly the same instance of a class.

if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// Prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."

Properties

lazy stored property is a property whose initial value is not calculated until the first time it is used. You indicate a lazy stored property by writing the lazy modifier before its declaration.

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
}

Global variables are variables that are defined outside of any function, method, closure, or type context. Local variables are variables that are defined within a function, method, or closure context.

Methods

Methods are functions that are associated with a particular type.

Static variables are those variables whose values are shared among all the instance or object of a class. When we define any variable as static, it gets attached to a class rather than an object.

Instance Methods

Instance methods are functions that belong to instances of a particular class, structure, or enumeration. Instance methods have exactly the same syntax as functions.

Every instance of a type has an implicit property called self, which is exactly equivalent to the instance itself. You use the self property to refer to the current instance within its own instance methods. In practice, you don’t need to write self in your code very often. If you don’t explicitly write self, Swift assumes that you are referring to a property or method of the current instance whenever you use a known property or method name within a method. This assumption is demonstrated by the use of count (rather than self.count) inside the three instance methods for Counter.

// Counter Class
class Counter {
    var count = 0
    func increment() {
        count += 1
    }
}

// increment() can also be written like this:
func increment() {
    self.count += 1
}

Type Methods

Type methods are methods that are called on the type itself. You indicate type methods by writing the static keyword before the method’s func keyword. Classes can use the class keyword instead, to allow subclasses to override the superclass’s implementation of that method.

The example below defines a structure called LevelTracker, which tracks a player’s progress through the different levels or stages of a game. It is a single-player game, but can store information for multiple players on a single device. All of the game’s levels (apart from level one) are locked when the game is first played. Every time a player finishes a level, that level is unlocked for all players on the device. The LevelTracker structure uses type properties and methods to keep track of which levels of the game have been unlocked. It also tracks the current level for an individual player.

struct LevelTracker {
    static var highestUnlockedLevel = 1
    var currentLevel = 1

    static func unlock(_ level: Int) {
        if level > highestUnlockedLevel { highestUnlockedLevel = level }
    }

    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }

    @discardableResult
    mutating func advance(to level: Int) -> Bool {
        if LevelTracker.isUnlocked(level) {
            currentLevel = level
            return true
        } else {
            return false
        }
    }
}

The LevelTracker structure keeps track of the highest level that any player has unlocked. This value is stored in a type property called highestUnlockedLevel.

LevelTracker also defines two type functions to work with the highestUnlockedLevel property. The first is a type function called unlock(_:), which updates the value of highestUnlockedLevel whenever a new level is unlocked. The second is a convenience type function called isUnlocked(_:), which returns true if a particular level number is already unlocked. (Note that these type methods can access the highestUnlockedLevel type property without your needing to write it as LevelTracker.highestUnlockedLevel.)

In addition to its type property and type methods, LevelTracker tracks an individual player’s progress through the game. It uses an instance property called currentLevel to track the level that a player is currently playing.

To help manage the currentLevel property, LevelTracker defines an instance method called advance(to:). Before updating currentLevel, this method checks whether the requested new level is already unlocked. The advance(to:) method returns a Boolean value to indicate whether or not it was actually able to set currentLevel. Because it’s not necessarily a mistake for code that calls the advance(to:) method to ignore the return value, this function is marked with the @discardableResult attribute. For more information about this attribute, see Attributes.

The LevelTracker structure is used with the Player class, shown below, to track and update the progress of an individual player:

class Player {
    var tracker = LevelTracker()
    let playerName: String
    func complete(level: Int) {
        LevelTracker.unlock(level + 1)
        tracker.advance(to: level + 1)
    }
    init(name: String) {
        playerName = name
    }
}

The Player class creates a new instance of LevelTracker to track that player’s progress. It also provides a method called complete(level:), which is called whenever a player completes a particular level. This method unlocks the next level for all players and updates the player’s progress to move them to the next level. (The Boolean return value of advance(to:) is ignored, because the level is known to have been unlocked by the call to LevelTracker.unlock(_:) on the previous line.)

You can create an instance of the Player class for a new player, and see what happens when the player completes level one:

var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// Prints "highest unlocked level is now 2"

If you create a second player, whom you try to move to a level that is not yet unlocked by any player in the game, the attempt to set the player’s current level fails:

player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
    print("player is now on level 6")
} else {
    print("level 6 has not yet been unlocked")
}
// Prints "level 6 has not yet been unlocked"

Initialization

Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This process involves setting an initial value for each stored property on that instance and performing any other setup or initialization that is required before the new instance is ready for use.

You implement this initialization process by defining initializers, which are like special methods that can be called to create a new instance of a particular type. Their primary role is to ensure that new instances of a type are correctly initialized before they are used for the first time.

In its simplest form, an initializer is like an instance method with no parameters, written using the init keyword:

init() {
    // perform some initialization here
}

The example below defines a new structure called Fahrenheit to store temperatures expressed in the Fahrenheit scale. The Fahrenheit structure has one stored property, temperature, which is of type Double:

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

This example defines a class called ShoppingListItem, which encapsulates the name, quantity, and purchase state of an item in a shopping list:

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

Because all properties of the ShoppingListItem class have default values, and because it is a base class with no superclass, ShoppingListItem automatically gains a default initializer implementation that creates a new instance with all of its properties set to their default values. (The name property is an optional String property, and so it automatically receives a default value of nil, even though this value is not written in the code.) The example above uses the default initializer for the ShoppingListItem class to create a new instance of the class with initializer syntax, written as ShoppingListItem(), and assigns this new instance to a variable called item.

Memberwise Initializers

The example below defines a structure called Size with two properties called width and height. Both properties are inferred to be of type Double by assigning a default value of 0.0. The Size structure automatically receives an init(width:height:) memberwise initializer, which you can use to initialize a new Size instance:

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

When you call a memberwise initializer, you can omit values for any properties that have default values. In the example above, the Size structure has a default value for both its height and width properties. You can omit either property or both properties, and the initializer uses the default value for anything you omit—for example:

let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0"

let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// Prints "0.0 0.0"

Swift API Guidelines

Fundamentals

Begin with a summary that describes the entity being declared. Often, an API can be completely understood from its declaration and its summary. Focus on the summary; it’s the most important part. Many excellent documentation comments consist of nothing more than a great summary. Use a single sentence fragment if possible, ending with a period. Do not use a complete sentence.

/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection

Optionally, continue with one or more paragraphs and bullet items. Paragraphs are separated by blank lines and use complete sentences.

Naming

More words may be needed to clarify intent or disambiguate meaning, but those that are redundant with information the reader already possesses should be omitted. In particular, omit words that merely repeat type information.

func removeElement(_ member: Element) {} // wrong 
func remove(_ member: Element) {} // correct

Name variables, parameters, and associated types according to their roles, rather than their type constraints.

var string = "Hello" // wrong
var greeting = "Hello" // correct

Prefer method and function names that make use sites form grammatical English phrases. The following are all good examples.

x.insert(y, at: z)          “x, insert y at z”
x.subViews(havingColor: y)  “x's subviews having color y”
x.capitalizingNouns()       “x, capitalizing nouns”

Begin names of factory methods with “make”, e.g. x.makeIterator().

Name functions and methods according to their side-effects

Conventions

Argument Labels

func move(from start: Point, to end: Point)
x.move(from: x, to: y) 

@joekotlan on Twitter