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 aString
.
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 wherebreak
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
- For
- 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 ado 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
- Allow value
- 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 appAssets.xcassets
is for assetsinfo.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 ofsome 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")
}
}
}
Navigation Bar
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
, andZStack
.- 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 aflex: 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
- Each turn of the game the app will randomly pick either rock, paper, or scissors.
- Each turn the app will either prompt the player to win or lose.
- The player must then tap the correct move to win or lose the game.
- If they are correct they score a point; otherwise they lose a point.
- 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
- Open the CreateML option from the XCode menu.
- In this case, we'll use a tabular regression model.
- Select training data.
- Choose target field that we're trying to compute.
- Pick the features that will be considered when predicting values.
- Pick an algorithm, or let it choose for you.
- Click 'train'
- Evaluation tab will give the answer. We can see that the Root Mean Square Error is about 180, which is 3 minutes.
- 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