100 Days of Swift UI

As part of 100 Days of Swift UI I've committed to an hour a day of learning the language with Paul Hudson and then logging what I've learnt.

Even though I'm an experienced JavaScript developer and I'd expect quite a lot of the concepts will hold, I'm still going to log every day's content.

Day 1 - Simple Data Types

vars and lets

Variables are created with the var keyword, constants with let. Swift is a type-safe language, so you can only reassign variable values of their initial type.

Ints and Doubles

Integers can use _ as a thousand separator, but this is only for readability. Doubles are for fractional values, and can't be mixed with Ints.

Explicit Types

Every variable and constant is given a type automatically at the point of creation (type inference), but you can also explicitly state the type at the point of creation:

let name: String = "Kevin"
let year: Int = 2021
let pi: Double = 3.1416
let amLearning: Bool = true

Strings

Multiline strings use triple-quotes on the opening and closing lines, though those opening and closing line-breaks won't be included in the string:

var a = """
Multiline string
uses these backticks
"""

String interpolation looks like this:

var score = 85
var a = "Your score was \(score)"

Day 2 - Collections

Arrays

var numbers = [1, 2, 3]
var numbers: [Int] = [1, 2, 3]
numbers[0] // 1

// Create empty array
var numbers = Array<Int>()
var numbers = [Int]()

Dictionaries

var pet = [ "name": "Moo", "owner": "Kevin" ]
var pet: [String, String] = [ "key": "value" ]

// Get values
pet["name"] // "Moo"
pet["friend"] // nil
pet["friend", default: "Otto"] // "Otto"

// Create empty dictionary
var pet = Dictionary<String, String>()
var pet = [String: String]()

Tuples

let name = (first: "Kevin", last: "Lewis")
name.0 // "Kevin"
name.last // "Lewis"

// Can't add or remove items, or change their type

Sets

let colors = Set(["red", "green", "blue"])

// Create empty set
var colors = Set<String>()

Enums

enum Activity {
    case running
    case cooking
    case cleaning
}

// Associated values
enum Activity {
    case running(destination: String)
    case cooking(meal: String)
    case singing(volume: Int)
}

let song = Activity.singing(volume: 9)

// Raw values
enum Planet: Int {
    case mercury = 1
    case venus // implicitly set as 2, can be made explicit
    case earth // implicitly set as 3, can be made explicit
}

Day 3 - Operators & Conditions

  • You can add arrays to concatenate them - [1, 2] + [3, 4] returns [1, 2, 3, 4].
  • Unlike JS, you cannot add an Int to a String.

If Statements

If statements look like this (conditions aren't in brackets):

if condition {
} else if condition {
} else {
}

Switch Blocks

Switch blocks look the same as JS except there are no brackets around the input and no need to break after each case:

switch weather {
case "rain":
    print("Bring an umbrella")
case "snow":
    print("Wrap up warm")
case "sunny":
    print("Wear sunscreen")
default:
    print("Enjoy your day!")
}
  • In switch blocks you can use the fallthrough keyword where break would go in JS to continue execution on to the next case.Range operators:

Range Operators

let score = 85

switch score {
case 0...49:
    print("You failed badly. Score is 0-49.")
case 50..<85:
    print("You did OK. Score is 50-84.")
default:
    print("You did great!")
}

Basic operators, comparisons, &&, ``||, ternary operators look the same as JS.

Day 4 - Loops

For and While Loops

for number in 1...10 {
    print("Number is \(number)")
}

while condition {
    print("condition is met")
}

while condition {
    // do stuff
    break
}

Nested Loops

// Breaking out of nested loop by naming outer loop
outerLoop: for i in 1...10 {
    for j in 1...10 {
        let product = i * j
        if product == 50 {
            break outerLoop
        }
    }
}

// Skip iteration with continue
for i in 1...10 {
    for j in 1...10 {
        let product = i * j
        if product == 50 {
            continue
        }
    }
}

Day 5 - Functions

func functionName(param1: Type) {
  print(param1)
}

functionName(param1: "value")

Returning data

If we want to return multiple values, tuples are a good data structure

func square(number: Int) -> Int {
  return number * number
}

Internal & External Parameter Names

func myFunction(externalName internalName: Type) {
  print(internalName)
}

myFunction(externalName: "Hello")

// Set no external parameter name

func myFunction(_ internalName: Type) {
  print(internalName)
}

myFunction("Hello")

Default Parameters

func a(value: String = "default") { }
a() // "default"
a("provided") // "provided"

Allowing n parameters (variadic)

// params are internally converted to an array
func myFunc(values: Int...) {
  for value in values {
    print(value)
  }
}

myFunc(values: "Mercury", "Venus", "Earth", "Mars")

Throwing functions

Prepend the return type with throws

enum ApiError: Error {
  case badCreds
  case rateLimitExceeded
  case serverError
}

func checkCreds(_ creds: String) throw -> String {
  if creds != "correctCreds" {
    throw ApiError.badCreds
  }
  return true
}

Using throwing functions

do {
  try PotentiallyFailingFunction()
  print("All good")
} catch {
  print("Error occurred")
}

inout Parameters

inout parameters can be used to alter external parameters in-place. Without them, parameters are treated as constants only within the scope of the function call. As well as defining the function param as inout, we must prepend the variable with &.

func doubleValue(_ number: inout Int) {
  number *= 2
}

var num = 10
doubleInPlace(&num)

Day 6 - Closures 1

// Creates a function without a name, assigns to variable
let phrase = {
  print("phrase here")
}

driving()

Accepting Parameters

let phrase = { (text: String) in
  print(text)
}

phrase("Hello world")

Returning Values

let phrase = { (text: String) -> String in
  return "Phrase is \(text)"
}

phrase("Hello world")

Closures as Parameters

// () => Void
// Accepts no params, returns Void (nothing)
func travel(action: () -> Void) {
    print("I'm getting ready to go.")
    action()
    print("I arrived!")
}

travel(actions: driving)

Syntantic Sugar

If the last param in a function is a closure, there is some special syntax known as trailing closure syntax.

func travel(action: () -> Void) {
    print("I'm getting ready to go.")
    action()
    print("I arrived!")
}

// As the last (and only) param is a closure, we can call it like this:

travel {
  print("I'm driving in my car")
}

// Another example

func animate(duration: Double, animations: () -> Void) {
    print("Starting a \(duration) second animation…")
    animations()
}

animate(duration: 3) {
    print("Fade out the image")
}

Trailing closures work best when their meaning is directly attached to the name of the function.

Day 7 - Closures 2

Closures as Parameters When They Accept Parameters

func travel(action: (String) -> Void) {
  print("I'm getting ready to go.")
  action("London")
  print("I arrived!")
}

// Now our closure code requires a string to be passed
travel { (place: String) in
  print("I'm going to \(place)")
}

Closures as Parameters When They Return Parameters

func travel(action: (String) -> String) {
  print("I'm getting ready to go.")
  let description = action("London")
  print(description)
  print("I arrived!")
}

travel { (place: String) -> String in
  return "I'm going to \(place) in my car"
}

Shorthand Parameter Names

func travel(action: (String) -> String) {
  print("I'm getting ready to go.")
  let description = action("London")
  print(description)
  print("I arrived!")
}

// Instead of
travel { (place: String) -> String in
  return "I'm going to \(place) in my car"
}

// You can write
travel { place -> String in
  return "I'm going to \(place) in my car"
}

// Or as we know the closure param must be a string
travel { place in
  return "I'm going to \(place) in my car"
}

// Or as there's only one line
travel { place in
  "I'm going to \(place) in my car"
}

// Or if you don't care about naming the parameter
travel {
  "I'm going to \($0) in my car"
}

Day 8 - Structs 1

Computed Properties

struct Dog {
  var name: String
  var age: Double
  var isPuppy: Bool {
    if age < 1 {
      return true
    } else {
      return false
    }
  }
}

const myPet = Dog(name: "Moo", age: 5)
print(myPet.name) // "Moo"
print(myPet.isPuppy) // false

Property Observers

struct Progress {
  var task: String
  var amount: Int {
    didSet {
      print("\(task) is now \(amount)% complete")
    }
  }
}

var progress = Progress(task: "Loading data", amount: 0)
progress.amount = 50 // triggers print
progress.amount = 100 // triggers print

Struct Methods

struct City {
  var population: Int
  func collectTaxes() => Int {
    return population * 1000
  }
}

var london = City(population: 9000000)
var taxation = london.collectTaxes()
print(taxation)

Mutating Methods

By default, structs are constants and so are their member properties. You can specifically do this by prepending a func keyword with mutating:

struct Game {
  var score: Int
  mutating func resetScore() {
    score = 0
  }
}

let game1 = Game(10)
game1.resetScore() // won't work - instance is constant

var game2 = Game(10)
game2.resetScore() // works

String Methods

let string = "Do or do not, there is no try."

print(string.count)  // 30
print(string.hasPrefix("Do")) // true
print(string.uppercased()) // DO OR DO NOT, THERE IS NO TRY.

Array Methods

var toys = ["Woody"]

print(toys.count) // 1
toys.append("Buzz")
toys.firstIndex(of: "Buzz") // 1
print(toys.sorted()) // ["Buzz", "Woody"]
toys.remove(at: 0) // "Woody"

Day 9 - Structs 2

Initializers

Initializers are basically constructors for structs. The default initializer is called a memberwise initializer and requires you to provide a value for each property. You can override this by creating an init() method (without the func keyword):

struct User {
  var username: String

  init() {
    username = "Joe Bloggs"
    print("Now you don't need to pass any parameters")
  }
}

var user = User()
user.username = "Jane Bloggs"

self

self is the Swift version of this in JavaScript. Inside of a struct you can refer to self.property.

Lazy Properties

Lazy properties are only created when they are first accessed. You can prepend property definitions in a struct with the lazy keyword to achieve this:

struct Person {
  var name: String
  lazy var familyTree = FamilyTree()

  init(name: String) {
      self.name = name
  }
}

var ed = Person(name: "Ed")
// ed.familyTree has not been created yet, but when called it is created
print(ed.familyFree) // now it exists

Static Properties

As expected, static properties exist on the struct-level.

struct Student {
  static var classSize = 0
  var name: String
  init(name: String) {
    self.name = name // retain default behavior
    Student.classSize += 1
  }
}

var kev = Student(name: "Kev")
print(kev.name) // "Kev"
print(Student.classSize) // 1

Private Properties

Also in the realm of familiarity are private properties by prepending private to the property definitions.

Day 10 - Classes

Initializers

Classes do not get a memberwise initializer like structs - you must create your own init():

class Dog {
  var name: String
  var breed: String

  init(name: String, breed: String) {
      self.name = name
      self.breed = breed
  }
}

let moo = Dog(name: "Moo", breed: "Jack Russell")

Inheritance

Parent class is called a "super class" to the "child class" or "sub class".

class Poodle: Dog {
  // Gets same properties and initializers as Dog by default
}

You can provide it's own initializer, and even call the superclass' initializer too.

class Poodle: Dog {
  init(name: String) {
    super.init(name: name, breed: "Poodle")
  }
}

Overriding

If you want to override a superclass function, you must prepend it with the word override:

class Dog {
  func makeNoise() {
    print("Woof!")
  }
}

class Poodle: Dog {
  override func makeNoise() {
      print("Yip!")
  }
}

Disallowing Inheritance

final class Dog {
  // The final keyword will do it
}

Deinitializers

These are called when a class instance is destroyed

class Dog {
  deinit {
    print("Being destroyed")
  }
}

Mutability

Unlike structs, variables inside of classes can be changed without the mutating keyword.

Day 11 - Protocols & Extensions

Protocols

Protocols create blueprints for how our funcs, structs, and classes share requirements such as methods and properties. A protocol can then be adopted by a class, struct, or class.

protocol Identifiable {
  var id: String { get set }
}

// We can be confident that thing exists as it is made mandatory by the protocol.
func displayID(thing: Identifiable) {
  print("My ID is \(thing.id)")
}

Protocol Inheritance

You can inherit from multiple protocols at the same time.

protocol Payable {
  func calculateWages() -> Int
}

protocol NeedsTraining {
  func study()
}

protocol HasVacation {
  func takeVacation(days: Int)
}

protocol Employee: Payable, NeedsTraining, HasVacation { }

Extensions

Extensions are like when you add prototype properties in JavaScript to add functionality to an existing type. They can be methods or computed properties.

extension Int {
  func squared() -> Int {
      return self * self
  }
}

let number = 8
let numberSquared = number.squared()

Protocol Extensions

Same deal as normal extensions. For example, both arrays and sets conform to a protocol called Collection, so we can add functionality by extending it.

let pythons = ["Eric", "Graham", "John", "Michael", "Terry", "Terry"]
let beatles = Set(["John", "Paul", "George", "Ringo"])

extension Collection {
  func summarize() {
      print("There are \(count) of us:")
      for name in self {
          print(name)
      }
  }
}

pythons.summarize()
beatles.summarize()

Protocol-Oriented Programming (POP)

Protocol extensions can provide default implementations for our own protocol methods.

protocol Identifiable {
  var id: String { get set }
  func identify()
}

extension Identifiable {
  func identify() {
      print("My ID is \(id).")
  }
}

struct User: Identifiable {
  var id: String
}

let twostraws = User(id: "twostraws")
twostraws.identify()

Some might say that the only real difference between the two is that in protocol-oriented programming (POP) we prefer to build functionality by composing protocols (“this new struct conforms to protocols X, Y, and Z”), whereas in object-oriented programming (OOP) we prefer to build functionality through class inheritance. However, even that is dubious because OOP developers also usually prefer composing functionality to inheriting it – it’s just easier to maintain.

In fact, the only important difference between the two is one of mindset: POP developers lean heavily on the protocol extension functionality of Swift to build types that get a lot of their behavior from protocols. This makes it easier to share functionality across many types, which in turn lets us build bigger, more powerful software without having to write so much code.

Day 12 - Optionals

Optional types can hold nil as well as a value of their specified type

var age: Int? = nil
age = 42

Unwrapping

Unwrapping optionals allows you to handle cases where variables hold nil. For example, you can do val.count if a value is a string, but not if it's nil. This check will be unsafe and Swift won't allow it.

if let

if let unwrapped = name {
  print("\(unwrapped.count) letters")
} else {
  print("Missing name.")
}

guard let

Unwrapped optional remains usable after the guard code. You can handle these problems at the start of your functions - kind of similar to if you did if(err) return err in JS.

func greet(_ name: String?) {
  guard let unwrapped = name else {
    print("You didn't provide a name!")
    return
  }

  print("Hello, \(unwrapped)!")
}

Force Unwrapping

Also known as a crash operator.

If you know with 100% confidence that an optional is not nil at the point of using it you can store it in a new variable and append a ! to the value.

let str = "5"
let num = Int(str)!

Implicitly Unwrapped Optionals

Sometime a variable starts life an nil but will 100% have a value by the time you are using it. You can use the following syntax at the time of creating a variable and then you don't need to unwrap them before use.

let age: Int! = nil

Nil Coalescing

Unwraps an optional and returns value if it exists, or uses a default. Like val || 'fallback' in JS.

func username(for id: Int) -> String? {
  if id == 1 {
    return "Taylor Swift"
  } else {
    return nil
  }
}

let user = username(for: 15) ?? "Anonymous"

Optional Chaining

Exactly the same as ES6:

let value = a.b?.c

Optional Try

Given this prior code:

enum PasswordError: Error {
  case obvious
}

func checkPassword(_ password: String) throws -> Bool {
  if password == "password" {
    throw PasswordError.obvious
  }
  return true
}

do {
  try checkPassword("password")
  print("That password is good!")
} catch {
  print("You can't use that password.")
}

We can write it like this:

if let result = try? checkPassword("password") {
  print("Result was \(result)")
} else {
  print("D'oh.")
}

Or, if we're confident that at a point in time the function will not fail:

try! checkPassword("sekrit")
print("OK!")

Failable Initializers

You can create initializers that may not work in your own structs and classes with init?:

struct Person {
  var id: String

  init?(id: String) {
    if id.count == 9 {
      self.id = id
    } else {
      return nil
    }
  }
}

Typecasting

Sometimes Swift will try and be helpful and use type inference. If you want to explicitly check the type of a value you can use as?:

let pets = [Fish(), Dog(), Fish(), Dog()]
for pet in pets {
  if let dog = pet as? Dog {
    dog.makeNoise()
  }
}

Day 13: Consolidation 1.1

  • Variables
    • var for values that change
    • let for constants
  • Basic types (strings, integers, floats, doubles, boolean)
    • Preferable to assign a value over explicit type annotation
  • Operators
  • Collections
    • Arrays (single level, same type)
    • Dictionaries (key value pairs, can be different types)
  • Conditionals
    • If
    • Switch
    • Range
  • Loops
    • For
      • Continue skips this iteration only
    • While
    • Nested loops
      • Break outerLoop
  • Switch (can combine with ranges)

Day 14: Consolidation 1.2

  • Functions
    • Internal & external parameter names
    • Remove requirement to name parameter by setting external name to _
    • Variadic parameters allow n number of parameters
    • Specify chance function with error with throw and calling in a do catch block
    • inout parameters allow manipulation of external parameters in-place
  • Optionals
    • Allow value nil as well as specified type
    • Need 'unwrapping' when used
    • You can force or implicitly unwrap when you know the value won't be nil by the time of use
  • Enums
    • Enums increase the chance for correct and readable code by storing a small number of values in a set
    • You can specify an input as an enum value, and only those values will be valid
  • Structs & Classes
    • Complex data types comprising multiple properties
    • Can contain initializers to set up initial state
    • Can be extended with extension
    • Can conform to protocols
  • Structs
    • Computed properties have dynamic values based on other data in the instance
    • Property observers can run when values change in the instance
  • Classes
    • Classes can inherit from other classes
    • Inherited properties can be overridden
    • You can create a final class which cannot be inherited
    • Classes can be deinitialized
    • You can check which type an instance is using typecasting (as?)

Day 15: Consolidation 1.3

Closures (still fuzzy - going to skip this now until we're applying it in future classes)

Day 16 - WeSplit Project, Part 1

Xcode App Boilerplate

When creating a new project some files get created:

  • ContentView.swift contains initial UI for the app
  • Assets.xcassets is for assets
  • info.plist contains metadata about the app
import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
  • struct ContentView: View create a struct which conforms to the View protocol. View is provided by SwiftUI and is a basic protocol which must be adopted anything you want something displayed on the screen.
  • Views must have one computed property called body, which has a type of some view.
  • some keyword means the same type of view must be returned - more on this later.
  • The whole ContentView_previews struct allows Xcode to display the preview alongside code - this won't be shipped.

Creating forms

Forms are scrolling lists of static controls like text or images. They may also include user-interactive elements.

var body: some View {
    Form {
        Text("Hello World")
    }
}

// Forms can only have 10 children
// If you want more use a Group
// Group does not alter visuals
var body: some View {
    Form {
        Group {
            Text("Hello World")
        }
        Group {
            Text("Hello World")
        }
    }
}

// Section does alter layout slightly
var body: some View {
    Form {
        Section {
            Text("Hello World")
        }
        Group {
            Text("Hello World")
        }
    }
}

Big title

var body: some View {
    NavigationView {
        Form {
            Text("Hello World")
        }
        .navigationBarTitle("SwiftUI")
    }
}

Small title

var body: some View {
    NavigationView {
        Form {
            Text("Hello World")
        }
        .navigationBarTitle("SwiftUI", displayMode: .inline)
    }
}

@State

Swift does not let us make computed properties mutating. To achieve state manipulation in SwiftUI we use a 'property wrapper' @State

struct ContentView: View {
    @State var tapCount = 0

    var body: some View {
        Button("Tap Count: \(tapCount)") {
            self.tapCount += 1
        }
    }
}

Input Data Binding

The $ is what creates two-way data binding.

struct ContentView: View {
    @State private var name = ""

    var body: some View {
        Form {
            TextField("Enter your name", text: $name)
            Text("Your name is \(name)")
        }
    }
}

View Loops with ForEach

struct ContentView: View {
let students = ["Harry", "Hermione", "Ron"]
@State private var selectedStudent = 0

var body: some View {
    VStack {
        Picker("Select your student", selection: $selectedStudent) {
            ForEach(0 ..< students.count) {
                Text(self.students[$0])
            }
        }
        Text("You chose: Student # \(students[selectedStudent])")
    }
}
}

Day 17 - WeSplit Project, Part 2

Reading Text From User Input

  • SwiftUI must use strings to store text field values. You cannot bind them to Ints, Doubles, etc.
  • You can add a keyboard modifier to show just numbers or numbers as a period, but know this isn't ironclad - users can still enter other characters.
TextField("Amount", text: $checkAmount).keyboardType(.numberPad)
TextField("Amount", text: $checkAmount).keyboardType(.decimalPad)

Pickers in Forms

Declarative user interface design - what we want over how we want it to be done. SwiftUI make the choice about what type of UI is most appropriate given the device and context.

var body: some View {
    NavigationView {
        Form {
            Section {
                TextField("Amount", text: $checkAmount).keyboardType(.decimalPad)

                Picker("Number of people", selection: $numberOfPeople) {
                    ForEach(2 ..< 100) {
                        Text("\($0) people")
                    }
                }
            }
            Section {
                Text("$\(checkAmount)")
            }
        }
        .navigationTitle("WeSplit")
    }
}

Pickers outside of forms will give the big spinner. SwiftUI decides here that it should be collapsed and be entered in a new view. Because of this, we need the whole form to add a navigation view.

Segmented Controls

You can use the pickerStyle modifier to change how they look. Here's SegemntedPickerStyle():

Section {
  Picker("Tip percentage", selection: $tipPercentage) {
      ForEach(0 ..< tipPercentages.count) {
          Text("\(self.tipPercentages[$0])%")
      }
  }
  .pickerStyle(SegmentedPickerStyle())
}

Section Headers

Section(header: Text("Tip amount"))

Text Format Specifier

Text("$\(totalPerPerson, specifier: "%.2f")")

WeSplit Code

Complete Code
struct ContentView: View {
@State private var checkAmount = ""
@State private var numberOfPeople = 2
@State private var tipPercentage = 2
let tipPercentages = [10, 15, 20, 25, 0]

// Computed property
var totalPerPerson: Double {
    let peopleCount = Double(numberOfPeople + 2) // offset
    let tipSelection = Double(tipPercentages[tipPercentage])
    let orderAmount = Double(checkAmount) ?? 0 // nil coalescing
    let overallTip = orderAmount / 100 * tipSelection
    let grandTotal = orderAmount + overallTip
    let amountPerPerson = grandTotal / peopleCount
    return amountPerPerson
}

var body: some View {
    NavigationView {
        Form {
            Section {
                TextField("Amount", text: $checkAmount)
                    .keyboardType(.decimalPad)

                Picker("Number of people", selection: $numberOfPeople) {
                    ForEach(2 ..< 100) {
                        Text("\($0) people")
                    }
                }
            }
            Section(header: Text("Tip amount")) {
                Picker("Tip percentage", selection: $tipPercentage) {
                    ForEach(0 ..< tipPercentages.count) {
                        Text("\(self.tipPercentages[$0])%")
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
            }
            Section {
                Text("$\(totalPerPerson, specifier: "%.2f")")
            }
        }
        .navigationTitle("WeSplit")
        }
    }
}

Day 18 - WeSplit Review

Day 19 - Unit Conversion Challenge

Complete Code
struct ContentView: View {
@State private var startUnit = 0
@State private var endUnit = 0
@State private var value = ""
let units = ["Seconds", "Minutes", "Hours", "Days"]

var result: Double {
    let startValue = Double(value) ?? 0
    var startValueSecs = startValue
    switch(startUnit) {
        case 1: startValueSecs = startValue * 60
        case 2: startValueSecs = startValue * 3600
        case 3: startValueSecs = startValue * 86400
        default: startValueSecs = startValue
    }

    switch(endUnit) {
        case 0: return startValueSecs
        case 1: return startValueSecs / 60
        case 2: return startValueSecs / 3600
        case 3: return startValueSecs / 86400
        default: return 0
    }
}

var body: some View {
    NavigationView {
        Form {
            Section(header: Text("Initial Value")) {
                TextField("Value", text: $value).keyboardType(.decimalPad)
                Picker("Starting Unit", selection: $startUnit) {
                    ForEach(0 ..< units.count ) {
                        Text(self.units[$0])
                    }
                }.pickerStyle(SegmentedPickerStyle())
            }
            Section(header: Text("Show value in")) {
                Picker("Ending Unit", selection: $endUnit) {
                    ForEach(0 ..< units.count ) {
                        Text(self.units[$0])
                    }
                }.pickerStyle(SegmentedPickerStyle())
            }
            Section {
                Text("\(self.result, specifier: "%.2f") \(units[endUnit])")
            }
        }
        .navigationTitle("WeConvert")
        }
    }
}

Day 20 - Guess The Flag, Part 1

some View only allows one child. It can be a form, a section, or a stack.

Stacks

  • Still max 10 children.
  • VStack, HStack, and ZStack.
  • Spacing between the views can be done by providing a parameter when creating the stack:
VStack(spacing: 20) {
  Text("Hello World")
  Text("This is inside a stack")
}
  • The alignment property can be used to align text.
  • A Spacer() view act like a flex: 1 and utilize the rest of the space.

Challenge: Create a 3x3 grid.

Code snippet
struct ContentView: View {
    var body: some View {
        VStack {
            HStack {
                Text("1")
                Text("2")
                Text("3")
            }
            HStack {
                Text("4")
                Text("5")
                Text("6")
            }
            HStack {
                Text("7")
                Text("8")
                Text("9")
            }
        }
    }
}

Colors

Colors are views in their own right, which is why it can be used like shapes and text. It automatically takes up all the space available, but you can also use the frame() modifier to ask for specific sizes:

Background color on one element

ZStack {
    Text("Your content")
        .background(Color.red)
}

Background color on whole stack

ZStack {
    Color.red
    Text("Your content")
}

Set frame size

Color.red.frame(width: 200, height: 200)

Semantic colors

Color.primary // default color of text in SwiftUI based on theme
Color.secondary

Custom colors

Color(red: 1, green: 0.8, blue: 0)

Color in safe area (by notch)

ZStack {
    Color.red.edgesIgnoringSafeArea(.all)
    Text("Your content")
}

Gradients

  • SwiftUI provides three types of gradients to use - linear, radial, and angular (conic).
  • They are also views which can be drawn in our UI.
  • A gradient is made up of an array of colors, size & direction info, and the gradient type.
LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .top, endPoint: .bottom)

RadialGradient(gradient: Gradient(colors: [.blue, .black]), center: .center, startRadius: 20, endRadius: 200)

AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red]), center: .center)

Buttons

Button("Tap me!") {
    // This is a closure
    print("Button was tapped")
}

Show more complex buttons while having an actions closure:

Button(action: {
    print("Button was tapped")
}) {
    Text("Tap me!")
}

Button(action: {
    print("Edit button was tapped")
}) {
    Image(systemName: "pencil")
}

Button(action: {
    print("Edit button was tapped")
}) {
    HStack(spacing: 10) {
        Image(systemName: "pencil")
        Text("Edit")
    }
}

Images

As shown above, you can use Image() to handle images.

Loading Project Images

// Reads name to screenreader
Image("pencil")

Loading Decorative Images

// Won't read name to screenreader
Image(decorative: "pencil")

Loading SF Symbols

Image(systemName: "pencil")

Alert Messages

Alert(title: Text("Hello SwiftUI!"), message: Text("This is some detail message"), dismissButton: .default(Text("OK")))

To display an alert:

struct ContentView: View {
    @State private var showingAlert = false

    var body: some View {
        Button("Show Alert") {
            self.showingAlert = true
        }
        // Show when showingAlert is true
        .alert(isPresented: $showingAlert) {
            Alert(title: Text("Hello SwiftUI!"), message: Text("This is some detail message"), dismissButton: .default(Text("OK")))
            // When dismissed, sets showingAlert back to false
        }
    }
}

Day 21 - Guess The Flag, Part 2

Adding Assets

Click on Assets.xcassets and drag files in.

Random Number

Int.random(in: 0...2)

Rendering Mode

Image(image).renderingMode(.original) tells SwiftUI to render original pixels rather than trying to recolor them in a button.

Randomize Array

Array.shuffled()

Font Modifiers

Text("Hello World").font(.largeTitle).fontWeight(.black)

Image Modifiers

  • There are 4 built-in shapes in SwiftUI - rectangle, rounded rectangle, circle and capsule. You can apply them with .clipShape(SHAPE)
  • Borders can be added with .stroke(COLOR, lineWidth: Int)
  • Shadows can be added with .shadow(color: COLOR, radius: Int)

To stack them:

Image(image)
  .renderingMode(.original)
  .clipShape(RoundedRectangle(cornerRadius: 10))
  .overlay(RoundedRectangle(cornerRadius: 10)
  .stroke(Color.black, lineWidth: 1))
  .shadow(color: .black, radius: 2)

Challenges

  • Track & show score in alert
  • Show score in UI
  • When wrong, show correct answer in alert
Full code example
struct ContentView: View {
    @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()
    @State private var correctAnswer = Int.random(in: 0...2)
    @State private var showingScore = false
    @State private var scoreTitle = ""
    @State private var scoreMessage = ""
    @State private var score = 0


    var body: some View {
        ZStack {
            LinearGradient(gradient: Gradient(colors: [.blue, .black]), startPoint: .top, endPoint: .bottom)
                .edgesIgnoringSafeArea(.all)
            VStack(spacing: 30) {
                VStack {
                    Text("Tap \(countries[correctAnswer])")
                        .foregroundColor(.white)
                        .font(.largeTitle)
                        .fontWeight(.black)
                }
                ForEach(0 ..< 3) { number in
                    Button(action: {
                        self.flagTapped(number)
                    }) {
                        Image(self.countries[number])
                            .renderingMode(.original)
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                            .overlay(RoundedRectangle(cornerRadius: 10)
                            .stroke(Color.black, lineWidth: 1))
                            .shadow(color: .black, radius: 2)
                    }
                }
                Spacer()
                Text("Current Score: \(score)").foregroundColor(.white)
            }
        }
        .alert(isPresented: $showingScore) {
            Alert(title: Text(scoreTitle), message: Text(scoreMessage), dismissButton: .default(Text("Continue")) {
                self.askQuestion()
            })
        }
    }

    func flagTapped(_ number: Int) {
        if number == correctAnswer {
            score += 1
            scoreTitle = "Correct"
            scoreMessage = "Your score is \(score)"
        } else {
            score -= 1
            scoreTitle = "Incorrect"
            scoreMessage = "That's \(countries[number]). Your score is \(score)"
        }
        showingScore = true
    }

    func askQuestion() {
        countries.shuffle()
        correctAnswer = Int.random(in: 0...2)
    }
}

Day 22 - Guess The Flag Review

Day 23 - ViewsAndModifiers, Part 1

  • SwiftUI uses structs for views forces us to isolate state in a clear way, as well as being more performant and not needing to combat inherited attributes.
  • In SwiftUI, there is nothing behind our main view.
  • Modifiers stack in the order they are applied - this is because each modifier creates a new view with the changes applied rather than modifying the existing one.
  • SwiftUI uses 'opaque return types' - by returning some View we can provide anything conforming to the View protocol.
  • Ternary operators in modifiers: .foregroundColor(cond ? .red : .blue)
  • You can add some modifiers to containers (stacks) and these are called environment modifiers.
  • You can create views as properties (variables) and then use them with their variable names.

View Composition

struct CapsuleText: View {
    var text: String

    var body: some View {
        Text(text)
            .font(.largeTitle)
            .padding()
            .foregroundColor(.white)
            .background(Color.blue)
            .clipShape(Capsule())
    }
}

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            CapsuleText(text: "First")
            CapsuleText(text: "Second")
        }
    }
}

Custom Modifiers

struct Title: ViewModifier {
        func body(content: Content) -> some View {
            content
                .font(.largeTitle)
                .foregroundColor(.white)
                .padding()
                .background(Color.blue)
                .clipShape(RoundedRectangle(cornerRadius: 10))
        }
}

extension View {
    func titleStyle() -> some View {
        self.modifier(Title())
    }
}

Text("Hello World").titleStyle()

// Example 2

struct Watermark: ViewModifier {
    var text: String

    func body(content: Content) -> some View {
        ZStack(alignment: .bottomTrailing) {
            content
            Text(text)
                .font(.caption)
                .foregroundColor(.white)
                .padding(5)
                .background(Color.black)
        }
    }
}

extension View {
    func watermarked(with text: String) -> some View {
        self.modifier(Watermark(text: text))
    }
}

Color.blue
    .frame(width: 300, height: 200)
    .watermarked(with: "Hacking with Swift")

Day 24 - ViewsAndModifiers, Part 2

Challenge 1

Create a custom ViewModifier (and accompanying View extension) that makes a view have a large, blue font suitable for prominent titles in a view.

Code
struct LargeTitle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.largeTitle)
            .foregroundColor(.blue)
    }
}

extension View {
    func largeTitle() -> some View {
        self.modifier(LargeTitle())
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .largeTitle()
    }
}

Challenge 2

Go back to project 1 (WeSplit) and use a conditional modifier to change the total amount text view to red if the user selects a 0% tip.

Code
Text("$\(totalPerPerson, specifier: "%.2f")")
    .foregroundColor(self.tipPercentage == 0 ? .red : .black)

Challenge 3

Go back to project 2 (Guess The Flag) and create a FlagImage() view that renders one flag image using the specific set of modifiers we had.

Code
struct FlagImage: View {
    var country: String
    var body: some View {
        Image(country)
            .renderingMode(.original)
            .clipShape(RoundedRectangle(cornerRadius: 10))
            .overlay(RoundedRectangle(cornerRadius: 10)
            .stroke(Color.black, lineWidth: 1))
            .shadow(color: .black, radius: 2)
    }
}

var body: some View {
    ZStack {
        LinearGradient(gradient: Gradient(colors: [.blue, .black]), startPoint: .top, endPoint: .bottom)
            .edgesIgnoringSafeArea(.all)
        VStack(spacing: 30) {
            VStack {
                Text("Tap \(countries[correctAnswer])")
                    .foregroundColor(.white)
                    .font(.largeTitle)
                    .fontWeight(.black)
            }
            ForEach(0 ..< 3) { number in
                Button(action: {
                    self.flagTapped(number)
                }) {
                    FlagImage(country: self.countries[number])
                }
            }
            Spacer()
            Text("Current Score: \(score)").foregroundColor(.white)
        }
    }
    .alert(isPresented: $showingScore) {
        Alert(title: Text(scoreTitle), message: Text(scoreMessage), dismissButton: .default(Text("Continue")) {
            self.askQuestion()
        })
    }
}```

</details>

Day 25 - Milestone: Projects 1-3

  • NavigationView lets you push new views to the screen
  • Variables editable by your app should be in @State
  • Two-way data binding with forms is acheived with $variableName
  • Stacks can only have 10 children unless rendered with a ForEach
  • We learnt about colors, gradients, and fonts
  • Buttons with actions
  • Alerts
  • some View is an opaque return type
  • Modifiers create new view instances, and therefore order matters
  • Conditional modifiers can be achieved with a ternary operator
  • You can create custom views and view modifiers, using extensions for a better interface

Challenge: Rock Paper Scissors

  1. Each turn of the game the app will randomly pick either rock, paper, or scissors.
  2. Each turn the app will either prompt the player to win or lose.
  3. The player must then tap the correct move to win or lose the game.
  4. If they are correct they score a point; otherwise they lose a point.
  5. The game ends after 10 questions, at which point their score is shown.
Code
struct ContentView: View {
    let possibleMoves = ["Rock", "Paper", "Scissors"]
    @State var currentChoice = Int.random(in: 0...2)
    @State var shouldWin = Bool.random()
    @State var turn = 1
    @State var score = 0
    @State var showAlert = false
    @State var title = ""

    var body: some View {
        VStack(spacing: 30) {
            VStack {
                Text("\(possibleMoves[currentChoice])")
                    .font(.largeTitle)
                    .fontWeight(.black)
                Text("You must \(shouldWin ? "Win" : "Lose")")
            }
            VStack(spacing: 10) {
                ForEach(0 ..< possibleMoves.count) { number in
                    Button(action: {
                        self.makeChoice(number)
                    }) {
                        Text(possibleMoves[number])
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                    }

                }
            }
            Spacer()
            Text("Score: \(score)")
        }
        .alert(isPresented: $showAlert) {
            Alert(title: Text("\(title)"), message: Text("Your score is \(score)"), dismissButton: .default(Text("Continue")) {
                self.nextTurn()
                if(turn < 10) {
                    self.nextTurn()
                } else {
                    self.end()
                }
            })
        }
    }

    func makeChoice(_ number: Int) {
        let win = self.correctPos(currentChoice + 1)
        let lose = self.correctPos(currentChoice - 1)

        if (number == win && shouldWin) || (number == lose && !shouldWin) {
            score += 1
            title = "Correct"
        } else {
            score -= 1
            title = "Wrong"
        }

        showAlert = true
    }

    func correctPos(_ n: Int) -> Int {
        switch n {
            case 0...2:
                return n
            case -1:
                return 2
            case 3:
                return 0
            default:
                return 0
        }
    }

    func nextTurn() {
        currentChoice = Int.random(in: 0...2)
        shouldWin = Bool.random()
        turn += 1
    }

    func end() {
        title = "Game over"
        showAlert = true
    }
}

Day 26 - BetterRest, Part 1

Steppers

Steppers can be bound to any number type. You can optionally provide an in parameter which lets you provide a range, and a step value.

struct ContentView: View {
  @State private var sleepAmount = 8.0
  var body: some View {
    Stepper(value: $sleepAmount, in: 4...12, step: 0.25) {
      Text("\(sleepAmount, specifier: "%g") hours")
    }
  }
}

%g Text Specifier

This specifier removed insignificant zeros from the end of a number.

DatePicker

var body: some View {
  DatePicker("Please enter a date", selection: $wakeUp)
}

Hiding the text label

var body: some View {
  DatePicker("Please enter a date", selection: $wakeUp).labelsHidden()
}

Configuring type of picker

var body: some View {
  DatePicker("Please enter a date", selection: $wakeUp, displayedComponents: .date)
  DatePicker("Please enter a date", selection: $wakeUp, displayedComponents: .hourAndMinute)
}

Select within range

let now = Date()
let tomorrow = Date().addingTimeInterval(86400)
let range = now ... tomorrow

var body: some View {
  DatePicker("Please enter a date", selection: $wakeUp, in: range)
}

One-sided ranges

var body: some View {
  DatePicker("Please enter a date", selection: $wakeUp, in: Date()...)
}

Dates

DateComponents

DateComponents lets you read/write specific parts of a date. 8am today could be represented like this:

var components = DateComponents()
components.hour = 8
components.minute = 0
let date = Calendar.current.date(from: components) ?? Date()

The nil coalescing is used because Calendar.current.date(from:) returns an optional.

If we want to pull out values from a Date():

let components = Calendar.current.dateComponents([.hour, .minute], from: someDate)
let hour = components.hour ?? 0
let minute = components.minute ?? 0

DateFormatter

let formatter = DateFormatter()
formatter.timeStyle = .short
let dateString = formatter.string(from: Date())

DateFormatter also has a dateStyle property.

CreateML

  1. Open the CreateML option from the XCode menu.
  2. In this case, we'll use a tabular regression model.
  3. Select training data.
  4. Choose target field that we're trying to compute.
  5. Pick the features that will be considered when predicting values.
  6. Pick an algorithm, or let it choose for you.
  7. Click 'train'
  8. Evaluation tab will give the answer. We can see that the Root Mean Square Error is about 180, which is 3 minutes.
  9. From output you can save the file or import it into Xcode.

Day 27 - BetterRest, Part 2

You can run functions directly within a button's action closure like so:

.navigationBarItems(trailing:
    Button(action: myFunction) {
        Text("Text to display")
    }
)

Connecting CoreML to SwiftUI

do {
  let prediction = try model.prediction(wake: wake, estimatedSleep: estimatedSleep, coffee: coffee)
  let sleepTime = wakeUp - prediction.actualSleep
  // handle response with DateFormatter and Alert
} catch {
  // handle error
}

Wheel date picker in forms

DatePicker("Time", selection: $wakeUp, displayedComponents: .hourAndMinute)
  .labelsHidden()
  .datePickerStyle(WheelDatePickerStyle())

Day 28: BetterRest, Part 3

Completed project
struct ContentView: View {
@State private var wakeUp = defaultWakeTime
@State private var sleepAmount = 8.0
@State private var coffeeAmount = 1

@State private var alertTitle = ""
@State private var alertMessage = ""
@State private var showingAlert = false

var body: some View {
    NavigationView {
        Form {
            Section {
                Text("What time do you want to wake up?")
                    .font(.headline)
                DatePicker("Time", selection: $wakeUp, displayedComponents: .hourAndMinute)
                    .labelsHidden()
                    .datePickerStyle(WheelDatePickerStyle())

            }
            Section {
                Text("Desired amount of sleep")
                    .font(.headline)
                Stepper(value: $sleepAmount, in: 4...12, step: 0.25) {
                    Text("\(sleepAmount, specifier: "%g") hours")
                }
            }
            Section {
                Text("Daily coffee intake")
                    .font(.headline)
                Picker("Number of people", selection: $coffeeAmount) {
                    ForEach(1 ..< 20) {
                        if($0 == 1) { Text("1 cup") }
                        else { Text("\($0) cups") }
                    }
                }
            }
        }
        .navigationBarTitle("BetterRest")
        .navigationBarItems(trailing:
            Button(action: calculateBedtime) {
                Text("Calculate")
            }
        )
        .alert(isPresented: $showingAlert) {
            Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")))
        }
    }
}

static var defaultWakeTime: Date {
    var components = DateComponents()
    components.hour = 7
    components.minute = 0
    return Calendar.current.date(from: components) ?? Date()
}

func calculateBedtime() {
    let model = SleepCalculator()

    let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
    let hour = (components.hour ?? 0) * 60 * 60
    let minute = (components.minute ?? 0) * 60

    do {
        let wake = Double(hour + minute)
        let estimatedSleep = sleepAmount
        let coffee = Double(sleepAmount)

        let prediction = try model.prediction(wake: wake, estimatedSleep: estimatedSleep, coffee: coffee)
        let sleepTime = wakeUp - prediction.actualSleep

        let formatter = DateFormatter()
        formatter.timeStyle = .short
        alertMessage = formatter.string(from: sleepTime)
        alertTitle = "Your bedtime"
    } catch {
        alertTitle = "Error"
        alertMessage = "There was a problem"
    }

    showingAlert = true
}
}

Day 29: Word Scramble, Part 1

Lists

Scrolling table of data. Like a Form, but it's used for presentation of data over user input.

List {
  Section(header: Text("Section 1")) {
    Text("Static row 1")
    Text("Static row 2")
  }
  Section(header: Text("Section 2")) {
    ForEach(0..<5) {
      Text("Dynamic row \($0)")
    }
  }
  Section(header: Text("Section 3")) {
    Text("Static row 3")
    Text("Static row 4")
  }
}
.listStyle(GroupedListStyle())

Dynamic Rows

Where there are only dynamic rows, you can omit a ForEach.

List(0..<5) {
  Text("Dynamic row \($0)")
}
.listStyle(GroupedListStyle())

Providing Unique Keys

Like looping in Vue, you can provide a unique value to each item in a loop so they can later be uniquely referred to.

let values = ["a", "b", "c"]

List(values, id: \.self) {
  Text($0)
}
.listStyle(GroupedListStyle())

Or with a ForEach

let values = ["a", "b", "c"]

List {
  ForEach(values, id: \.self) {
    Text($0)
  }
}
.listStyle(GroupedListStyle())

Loading Resources

The URL data type can get both remote URLs and locations of files. We read them with Bundle.main.url() which returns the file or nil if it's not found - it's an optional that needs unwrapping. Once we have the location we can get the value wit ha String(contentsOf:) initializer, but this may throw so make sure to try it.

if let fileURL = Bundle.main.url(forResource: "some-file", withExtension: "txt") {
    // we found the file in our bundle!
    if let fileContents = try? String(contentsOf: fileURL) {
        // we loaded the file into a string!
    }
}

String Methods

let input = "a b c"

// Split string into array
let letters = input.components(separatedBy: " ")

// Get random letter - returns an optional as it may be an empty string
let letter = letters.randomElement()

// Remove whitespace and new lines
let trimmed = letter?.trimmingCharacters(in: .whitespacesAndNewlines)

Spelling Errors with ObjC

let word = "swift"
let checker = UITextChecker()

// Create a range od all characters
let range = NSRange(location: 0, length: word.utf16.count)

// Find mispellings
let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")

// If range is empty because there were no errors, value returned is `NSNotFound`
let allGood = misspelledRange.location == NSNotFound