Slide 1

Slide 1 text

iOS Development with SwiftUI Jussi Pohjolainen

Slide 2

Slide 2 text

Different Approaches Swift / Objective-C UIKit + Storyboards / XIB UIKit + Just Code (Snapkit/Purelayout) SwiftUI Swift Learn the language first!

Slide 3

Slide 3 text

UIKit + Storyboards / XIB • Autolayout can be frustrating • Version control can be cumbersome • Debug can be hard • Use several storyboards! Split your app. • Good approach for small apps • Number of developers... 1?

Slide 4

Slide 4 text

UIKit + Code Everything • "Full control of everything" • Use libraries like snapkit or purelayout for easier coding • Good approach if you want control every aspect • But this will require lot of coding!

Slide 5

Slide 5 text

SwiftUI • The future of Apple Devices development • It's cross platform! The UI code should be reusable on all devices. • No backward compatibility (only iOS 13 +) • Hot Reload can be frustrating • Much less documentation available • Good approach to new apps • Way easier than storyboards

Slide 6

Slide 6 text

SwiftUI

Slide 7

Slide 7 text

SwiftUI Project Lifecycle Choices Two lifecycle choices!

Slide 8

Slide 8 text

SwiftUI Project UIKit App Delegate

Slide 9

Slide 9 text

SceneFile import SwiftUI @main struct SwiftUiAppApp: App { var body: some Scene { WindowGroup { ContentView() } } } Starting point of our app App protocol forces body property

Slide 10

Slide 10 text

Lifecycle management • Scene phases • SwiftUI introduces the concept of scene phases which you can use to perform actions at specific points in the lifecycle of your app's UI • Scene? Like Application window • If you need to integrate with UIKit for handling more complex app lifecycle events or legacy codebases, you can use the @UIApplicationDelegateAdaptor property

Slide 11

Slide 11 text

Scene • Each scene has its own lifecycle, meaning • it can be created, enter the foreground • move to the background • Destroyed independently of other scenes. • This is similar to how individual windows in desktop applications can be opened, minimized, or closed independently. • The scene concept is especially powerful on iPadOS, where users can utilize the Split View and Slide Over features to work with multiple instances of the same app • Scenes also facilitate extending an app's UI to external displays.

Slide 12

Slide 12 text

@main struct MyScene: App { var body: some Scene { #if os(macOS) WindowGroup { Text("Mac") } #else WindowGroup { Text("iOS") } #endif } } The @main attribute in Swift is used to designate the entry point of a Swift program or app. It tells the compiler: “This is where the program starts.” In a SwiftUI app, it replaces the traditional main.swift file used in earlier Swift and UIKit-based projects.

Slide 13

Slide 13 text

SwiftUI App Lifecycle

Slide 14

Slide 14 text

Only two files! import SwiftUI @main struct SwiftUIAppLifecycleApp: App { var body: some Scene { WindowGroup { ContentView() } } } import SwiftUI struct ContentView: View { var body: some View { Text("Hello, world!") .padding() } }

Slide 15

Slide 15 text

SwiftUI App Protocol import SwiftUI @main struct SwiftUIAppLifecycleApp: App { var body: some Scene { WindowGroup { ContentView() } } } Starting point of our app

Slide 16

Slide 16 text

SwiftUI App Protocol import SwiftUI @main struct SwiftUIAppLifecycleApp: App { var body: some Scene { WindowGroup { ContentView() } } } App protocol requires that struct has body type of "some Scene"

Slide 17

Slide 17 text

App Delegate import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { NSLog("Ready to go!") return true } } @main struct SwiftUIAppLifecycleApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } }

Slide 18

Slide 18 text

SwiftUI App Protocol import SwiftUI @main struct SwiftUIAppLifecycleApp: App { var body: some Scene { WindowGroup { ContentView() } } } WindowGroup is container scene that wraps SwiftUI Views. Running this on iPad you can create multiple WindowGroup scenes

Slide 19

Slide 19 text

.\variable? struct Address { var city: String } struct Person { var address: Address } let jack = Person(address: Address(city: "New York")) let cityKeyPath : KeyPath = \Person.address.city let city = jack[keyPath: cityKeyPath] !// Accesses 'city' property of 'address' of 'person'

Slide 20

Slide 20 text

Listening to Scene changes @main struct tuhoaApp: App { @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { ContentView() .onChange(of: scenePhase) { switch scenePhase { case .active: print("App is active") case .inactive: print("App is inactive") case .background: print("App is in the background") default: break } } } } } EnvironmentValues.scenePhase

Slide 21

Slide 21 text

Content View

Slide 22

Slide 22 text

Content View import SwiftUI struct ContentView: View { var body: some View { Text("Hello") } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } Your UI For Live Preview

Slide 23

Slide 23 text

Struct vs Classes? • Notice that the Content View uses struct over classes • Struct is value type • Struct does not have inheritance • Can conform to a protocol • UIKit • Every ui component (class) inherites UIView • UIView is a mammoth class containing several properties • For example UIStackView inherites UIView which contains background color that UIStackView does not use! • SwiftUI • Trivial structs -> speed gain

Slide 24

Slide 24 text

import SwiftUI struct ContentView: View { var body = ... } Conforms to protocol View, This forces to have the var body!

Slide 25

Slide 25 text

import SwiftUI struct ContentView: View { var body = Text("Hello World") } This is now displayed on UI

Slide 26

Slide 26 text

import SwiftUI struct ContentView: View { var body = VStack(content: { Text("moi") Text("Hei") }) } Using closures

Slide 27

Slide 27 text

import SwiftUI struct ContentView: View { var body = VStack { Text("moi") Text("Hei") } } Using trailing lambda

Slide 28

Slide 28 text

VStack init init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, content: () -> Content) You can pass a function that returns Content

Slide 29

Slide 29 text

Content? • Content is "Swift Magic" • It is a generic type that SwiftUI magically determines for you • Notice that VStack has init • struct VStack where Content : View • So when you create VStack it has generics • For example • VStack> • Now VStack type is determined so that it may hold two Text objects

Slide 30

Slide 30 text

Without Closures, without "magic" import SwiftUI func f() -> TupleView<(Text, Text)> { let content : TupleView<(Text, Text)> = TupleView<(Text,Text)>((Text("a"), Text("b"))) return content } struct ContentView: View { var body : HStack> = HStack(content: f) }

Slide 31

Slide 31 text

Using closure struct ContentView: View { var body : HStack> = HStack(content: { let content : TupleView<(Text, Text)> = TupleView<(Text,Text)>((Text("a"), Text("b"))) return content }) }

Slide 32

Slide 32 text

Using trailing closure struct ContentView: View { var body : HStack> = HStack() { let content : TupleView<(Text, Text)> = TupleView<(Text,Text)>((Text("moi"), Text("hei"))) return content } }

Slide 33

Slide 33 text

Omit Type struct ContentView: View { var body : HStack> = HStack() { let content = TupleView<(Text,Text)>((Text("moi"), Text("hei"))) return content } }

Slide 34

Slide 34 text

Default return struct ContentView: View { var body : HStack> = HStack() { TupleView<(Text,Text)>((Text("moi"), Text("hei"))) } }

Slide 35

Slide 35 text

ViewBuilder struct ContentView: View { var body : HStack> = HStack() { ViewBuilder.buildBlock(Text("moi"), Text("hei")) } }

Slide 36

Slide 36 text

ViewBuilder struct ContentView: View { var body : HStack> = HStack { ViewBuilder.buildBlock(Text("moi"), Text("hei")) } }

Slide 37

Slide 37 text

This works also, what?? struct ContentView: View { var body : HStack> = HStack { //TupleView<(Text,Text)>((Text("moi"), Text("hei"))) Text("moi") Text("hei") } }

Slide 38

Slide 38 text

"function builder" feature struct ContentView: View { var body : HStack> = HStack() { //Text("moi") //Text("hei") let view : TupleView<(Text,Text)> = ViewBuilder.buildBlock(Text("moi"), Text("hei")) return view } } By using ViewBuilder it will build the generic type for you

Slide 39

Slide 39 text

Anonymous Function Call var x : Int { return 5 } Invokes anonymous function and return value will be in 5

Slide 40

Slide 40 text

Anonymous Function Calls struct ContentView: View { var body : HStack> { let hstack = HStack { Text("a") Text("b") } return hstack } } Anonymous function Uses function builder

Slide 41

Slide 41 text

Anonymous Function Calls struct ContentView: View { var body : HStack> { HStack { Text("a") Text("b") } } } Default return

Slide 42

Slide 42 text

Anonymous Function Calls struct ContentView: View { var body : HStack)>> { HStack { Text("a") Text("b") Button("click") { print(type(of: self.body)) } } } } We will have to change the type!

Slide 43

Slide 43 text

Using trailing lambda in Constructor struct ContentView: View { var body : some View { HStack { Text("a") Text("b") Button("click") { print(type(of: self.body)) } } } } It will automatically determinate the type! "one specific type that conforms to the View protocol, but we don’t want to say what."

Slide 44

Slide 44 text

State

Slide 45

Slide 45 text

Simple Button struct ContentView: View { var body: some View { Button("Click", action: { print("clicked") }) } }

Slide 46

Slide 46 text

Mutating value struct ContentView: View { var counter = 0 var body: some View { Text("\(counter)") Button("Click", action: { counter = counter + 1 }) } } By default structs are immutable! Error!

Slide 47

Slide 47 text

State • SwiftUI manages the storage of any property you declare as a state • @State private var counter = 0 • When the state value changes, view refreshes • Access state properties only within the view; declare them as private • It is safe to mutate state properties from any thread

Slide 48

Slide 48 text

Fix struct ContentView: View { @State private var counter = 0 var body: some View { Text("\(counter)") Button("Click", action: { counter = counter + 1 }) } }

Slide 49

Slide 49 text

Examples Basic UI

Slide 50

Slide 50 text

Button Example struct ContentView: View { @State private var counter = 0 var body: some View { Text("\(counter)") .fontWeight(.bold) .font(.title) .padding() Button(action: { counter += 1 }, label: { Text("Click Me!") .fontWeight(.bold) .font(.title) .padding() .background(Color.purple) .cornerRadius(40) .foregroundColor(.white) }) } }

Slide 51

Slide 51 text

Trailing Closure struct ContentView: View { @State private var counter = 0 var body: some View { Text("\(counter)") .fontWeight(.bold) .font(.title) .padding() Button(action: { counter += 1 }) { Text("Click Me!") .fontWeight(.bold) .font(.title) .padding() .background(Color.purple) .cornerRadius(40) .foregroundColor(.white) } } }

Slide 52

Slide 52 text

TextField struct ContentView: View { @State private var name = "" var body: some View { let binding : Binding = Binding( get: { self.name }, set: { self.name = $0.lowercased() } ) TextField("Enter username...", text: binding) .padding() Text("Your name is \(name)") .padding() } } State variable here must be wrapped inside of Binding

Slide 53

Slide 53 text

TextField struct ContentView: View { @State private var name = "" var body: some View { TextField("Enter username...", text: $name) .padding() Text("Your name is \(name)") .padding() } } Automatically creates the Binding!

Slide 54

Slide 54 text

SwiftUI Layouts • HStack – horizontal stacking • VStack – vertical stacking • ZStack – views on top of each other • ScrollView – horizontal and vertical scrolling • ..

Slide 55

Slide 55 text

HStack Example struct ContentView: View { @State private var name = "" var body: some View { HStack(alignment: .center) { Text("Username:") .font(.callout) .bold() TextField("Enter username...", text: $name) .textFieldStyle(RoundedBorderTextFieldStyle()) }.padding() } }

Slide 56

Slide 56 text

HStack Example Button(action: { print("Delete tapped!") }) { HStack { Image(systemName: "trash") .font(.title) Text("Delete") .fontWeight(.semibold) .font(.title) } .padding() .foregroundColor(.white) .background(Color.red) .cornerRadius(40) }

Slide 57

Slide 57 text

TabView struct ContentView: View { var body: some View { TabView { Text("The content of the first view") .tabItem { Image(systemName: "phone.fill") Text("First Tab") } Text("The content of the second view") .tabItem { Image(systemName: "tv.fill") Text("Second Tab") } } } }

Slide 58

Slide 58 text

Creating own views struct ContentView: View { var body: some View { TabView { RedView() .tabItem { Image(systemName: "phone.fill") Text("Hello") } BlueView() .tabItem { Image(systemName: "tv.fill") Text("World") } } } }

Slide 59

Slide 59 text

Own Views struct RedView: View { var body: some View { ZStack() { Color.red Text("Hello") .foregroundColor(Color.white) } } } struct BlueView: View { var body: some View { ZStack() { Color.blue Text("World") .foregroundColor(Color.white) } } }

Slide 60

Slide 60 text

Own Views

Slide 61

Slide 61 text

ForEach • ForEach in SwiftUI is a view • Can return it directly from your view body. • Provide it an array of items • You also need to tell SwiftUI how it can identify each of your items uniquely so it knows how to update them when values change.

Slide 62

Slide 62 text

Example struct ContentView: View { var body: some View { VStack() { ForEach((1..<10), content: { value in Text("\(value)") }) } } } Range (not ClosedRange)

Slide 63

Slide 63 text

Example: Using Trailing Closure struct ContentView: View { var body: some View { VStack() { ForEach((1..<10)) { Text("\($0)") } } } }

Slide 64

Slide 64 text

ForEach: Another Example struct ContentView: View { let colors: [Color] = [.red, .green, .blue] var body: some View { VStack() { ForEach(colors) { Text("\($0.description)") .padding() .background($0) .cornerRadius(20) } } } } Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Color' conform to 'Identifiable'

Slide 65

Slide 65 text

ForEach and ID struct MyColor: Identifiable { var color : Color var id = UUID() } struct ContentView: View { let colors: [MyColor] = [MyColor(color: .red), MyColor(color: .blue), MyColor(color: .green)] var body: some View { VStack() { ForEach(colors, id: \.id) { Text("\($0.color.description)") .padding() .background($0.color) .cornerRadius(20) } } } } Forces to have id, UUID() will create unique value.

Slide 66

Slide 66 text

ID can be anything struct MyColor: Identifiable { var color : Color var id : String } struct ContentView: View { let colors: [MyColor] = [MyColor(color: .red, id: "what"), MyColor(color: .blue, id: "is"), MyColor(color: .green, id: "this")] var body: some View { VStack() { ForEach(colors, id: \.id) { Text("\($0.color.description)") .padding() .background($0.color) .cornerRadius(20) } } } } id can be anything, should be unique

Slide 67

Slide 67 text

ForEach and ID struct MyColor: Identifiable { var color : String var id: String { color } } struct ContentView: View { let colors: [MyColor] = [MyColor(color: "red"), MyColor(color: "blue"), MyColor(color: "green")] var body: some View { VStack() { ForEach(colors, id: \.id) { Text("\($0.color.description)") .padding() .cornerRadius(20) } } } } Now id is color string, expects each color to be unique

Slide 68

Slide 68 text

Using \.self struct ContentView: View { let names = ["jack", "tina", "paul"] var body: some View { VStack() { ForEach(names, id: \.self) { Text("\($0)") .padding() .cornerRadius(20) } } } } Using "jack", "tina" and "paul" as ids

Slide 69

Slide 69 text

Picker and ForEach struct ContentView: View { let languages = ["Swift", "Kotlin", "Java"] @State var selectedLanguage : Int = 0 var body: some View { VStack() { Picker("Select your favorite language", selection: $selectedLanguage) { ForEach(0 ..< languages.count, id: \.self) { Text(self.languages[$0]) } } Text("Your favorite language \(languages[selectedLanguage])") } } }

Slide 70

Slide 70 text

List vs ForEach ForEach • Generates views from a collection • No scrolling on its own • Inherits styling from parent view • When generating views inside another container (e.g., VStack, List) List • Displays scrollable, stylized rows • Built-in scrolling (vertical) • Includes system row styles, separators • When creating a table-like list view • Built-in swipe-to-delete support

Slide 71

Slide 71 text

List struct ContentView: View { let colors = ["red", "green", "blue"] var body: some View { List(colors, id: \.self) { color in Text(color) } } }

Slide 72

Slide 72 text

Navigation

Slide 73

Slide 73 text

NavigationView vs NavigationStack • NavigationStack, for versions above iOS16 • NavigationView for older versions

Slide 74

Slide 74 text

Old vs New // Old NavigationView { // This is deprecated. /* content */ } .navigationViewStyle(.stack) // New NavigationStack { /* content */ }

Slide 75

Slide 75 text

Basic Example struct ContentView: View { var body: some View { NavigationStack { Text("Hello, World!") .navigationTitle("Navigation") } } }

Slide 76

Slide 76 text

struct Screen2: View { var body: some View { Text("Screen 2") } } struct ContentView: View { var body: some View { NavigationStack { VStack { Spacer() Text("First View") Spacer() NavigationLink("Move to Second View", value: "screen2") } .navigationDestination(for: String.self) { value in if value == "screen2" { Screen2() } } .navigationTitle("Home") } } }

Slide 77

Slide 77 text

struct Screen2: View { var body: some View { Text("Screen 2") } } struct Screen1: View { var body: some View { Text("Screen 1") } } struct ContentView: View { var body: some View { NavigationStack { VStack { NavigationLink("Screen 1", value: "screen1") NavigationLink("Screen 2", value: "screen2") } .navigationDestination(for: String.self) { value in if value == "screen1" { Screen1() } if value == "screen2" { Screen2() } } .navigationTitle("Home") } } }

Slide 78

Slide 78 text

struct AnotherScreen: View { var message = "" var body: some View { Text(message) } } struct ContentView: View { var body: some View { NavigationStack { VStack { NavigationLink("Screen 1", value: "screen1") NavigationLink("Screen 2", value: "screen2") } .navigationDestination(for: String.self) { value in AnotherScreen(message: value) } .navigationTitle("Home") } } }

Slide 79

Slide 79 text

DetailView and Person itself as Route struct Person: Identifiable, Hashable { let id: Int let name: String } struct PersonDetailView: View { let person: Person var body: some View { Text("Hello, \(person.name)!") .font(.largeTitle) .padding() } } struct ContentView: View { let people: [Person] = [ Person(id: 1, name: "Alice"), Person(id: 2, name: "Bob"), Person(id: 3, name: "Charlie") ] var body: some View { NavigationStack { List(people) { person in NavigationLink(person.name, value: person) } .navigationDestination(for: Person.self) { person in PersonDetailView(person: person) } .navigationTitle("People") } } }

Slide 80

Slide 80 text

Deep linking needs..

Slide 81

Slide 81 text

Enums • An enum (short for enumeration) is a type that defines a set of related values, called cases. You use it when you want to represent a fixed group of possibilities • An enum is one value at a time — either .red, or .green, or .blue, never more than one.

Slide 82

Slide 82 text

enum Direction { case north case south case east case west } var dir1 : Direction = Direction.north var dir2 : Direction = .north

Slide 83

Slide 83 text

Enums with associated valued enum Route { case screen1 case screen2(message: String) } let a: Route = Route.screen1 let b: Route = .screen2(message: "Hello") b holds .screen2 and carries the string "Hello"

Slide 84

Slide 84 text

Enums with associated valued enum Route, Hashable { case screen1 case screen2(message: String) } print(Route.screen1.hashValue) // some Int print(Route.screen2(message: "hello").hashValue) // some other Int var routes = Set() routes.insert(.screen1) routes.insert(.screen2(message: "Hi")) routes.insert(.screen2(message: "Hello")) routes.insert(.screen2(message: "Hi")) // duplicate, won't be added again print(routes) Generates hash function and == function

Slide 85

Slide 85 text

enum Route: Hashable { case screen1 case screen2 } struct Screen2: View { var body: some View { Text("Screen 2") } } struct Screen1: View { var body: some View { Text("Screen 1") } } struct ContentView: View { var body: some View { NavigationStack { VStack { NavigationLink("Screen 1", value: Route.screen1) NavigationLink("Screen 2", value: Route.screen2) } .navigationDestination(for: Route.self) { route in switch route { case .screen1: Screen1() case .screen2: Screen2() } } .navigationTitle("Home") } } }

Slide 86

Slide 86 text

enum Route: Hashable { case screen1 case screen2(message: String) } struct Screen2: View { let message: String var body: some View { Text("Screen 2: \(message)") } } struct Screen1: View { var body: some View { Text("Screen 1") } } struct ContentView: View { var body: some View { NavigationStack { VStack { NavigationLink("Screen 1", value: Route.screen1) NavigationLink("Screen 2", value: Route.screen2(message: "Hello World")) } .navigationDestination(for: Route.self) { route in switch route { case .screen1: Screen1() case .screen2(let message): Screen2(message: message) } } .navigationTitle("Home") } } }

Slide 87

Slide 87 text

enum Route: Hashable { case screen1 case screen2(message: String) } struct Screen2: View { let message: String @Binding var result: String? var body: some View { VStack { Text("Screen 2: \(message)") Button("Return Stuff") { result = "Stuff from Screen2" } } } } struct Screen1: View { var body: some View { Text("Screen 1") } } struct ContentView: View { @State private var messageFromScreen2 : String? = nil var body: some View { NavigationStack { VStack { if let result = messageFromScreen2 { Text("Message: \(result)") .padding() } NavigationLink("Screen 1", value: Route.screen1) NavigationLink("Screen 2", value: Route.screen2(message: "Hello World")) } .navigationDestination(for: Route.self) { route in switch route { case .screen1: Screen1() case .screen2(let message): Screen2(message: message, result: $messageFromScreen2) } } .navigationTitle("Home") } } }

Slide 88

Slide 88 text

Several Navigation Links enum Route: Hashable { case mint case pink case teal } struct ContentView: View { var body: some View { NavigationStack { List { NavigationLink("Mint", value: Route.mint) NavigationLink("Pink", value: Route.pink) NavigationLink("Teal", value: Route.teal) } .navigationDestination(for: Route.self) { route in switch route { case .mint: Text("hello = Mint") case .pink: Text("hello = Pink") case .teal: Text("hello = Teal") } } .navigationTitle("Colors") } } }

Slide 89

Slide 89 text

Property Wrappers

Slide 90

Slide 90 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 91

Slide 91 text

Local State View: Value type struct ContentView: View { @State private var counter = 0 var body: some View { VStack() { Text("\(self.counter)") .padding() Button("click") { counter += 1 } } } }

Slide 92

Slide 92 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 93

Slide 93 text

Passed in from the outside: Value Type struct MyButton: View { @Binding var counter : Int // Binding var body: some View { Button("click") { counter += 1 } } } struct ContentView: View { @State private var counter = 0 var body: some View { VStack() { Text("\(counter)") .padding() MyButton(counter: $counter) } } }

Slide 94

Slide 94 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 95

Slide 95 text

View creates, object type struct ContentView: View { @StateObject private var counterObject = Counter() var body: some View { VStack() { Text("\(counterObject.counter)") .padding() Button("start") { counterObject.start() }.padding() Button("stop") { counterObject.stop() }.padding() } } } class Counter: ObservableObject { @Published var counter = 0 var timer = Timer() func start() { timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in self.counter += 1 } } func stop() { timer.invalidate() self.counter = 0 } } Must be class When changes, refresh happens Class Counter is a singleton, only one instance even if view is recreated

Slide 96

Slide 96 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 97

Slide 97 text

Passing object to child view struct ContentView: View { @StateObject private var counterObject = Counter() var body: some View { VStack() { Text("\(counterObject.counter)") .padding() Buttons(counter: counterObject) } } } struct Buttons : View { @ObservedObject var counter : Counter var body: some View { Button("start") { counter.start() }.padding() Button("stop") { counter.stop() }.padding() } } Object is received as argument

Slide 98

Slide 98 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 99

Slide 99 text

Create the object in "main" import SwiftUI @main struct MyEnvApp: App { var body: some Scene { WindowGroup { ContentView().environmentObject(Counter()) } } }

Slide 100

Slide 100 text

struct Buttons : View { @EnvironmentObject var counterObject: Counter var body: some View { Button("start") { counterObject.start() }.padding() Button("stop") { counterObject.stop() }.padding() } } class Counter: ObservableObject { @Published var counter = 0 var timer = Timer() func start() { timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in self.counter += 1 } } func stop() { timer.invalidate() self.counter = 0 } } struct ContentView: View { @EnvironmentObject var counterObject: Counter var body: some View { VStack() { Text("\(counterObject.counter)") .padding() Buttons() } } }

Slide 101

Slide 101 text

Rules • @State • The view itself creates (and owns) the instance you want to wrap. • You need to respond to changes that occur within the wrapped property. • You're wrapping a value type (struct or enum) • @Binding • You need read- and write access to a property that's owned by a parent view. • The wrapped property is a value type (struct or enum). • You don't own the wrapped property (it's provided by a parent view).

Slide 102

Slide 102 text

Rules • @StateObject • You want to respond to changes or updates in an ObservableObject. • The view you're using @StateObject in creates the instance of the ObservableObject itself. • @ObservedObject • You want to respond to changes or updates in an ObservedObject. • The view does not create the instance of the ObservedObject itself. (if it does, you need a @StateObject) • @EnvironmentObject • You would normally use an @ObservedObject but you would have to pass the ObservableObject through several view's initializers before it reaches the view where it's needed.

Slide 103

Slide 103 text

Lifecycle Methods

Slide 104

Slide 104 text

Lifecycle Methods • In UIKit you have View Controllers with multiple lifecycle methods • In SwiftUI it is a little simpler • .onAppear() • .onDisappear()

Slide 105

Slide 105 text

Example struct ContentView: View { @State private var shouldShowView = true var body: some View { VStack() { Toggle(isOn: $shouldShowView) { Text("Display") }.padding() if(shouldShowView) { VStack() { Text("Hello") Text("World") }.onAppear() { print("appear") }.onDisappear() { print("disappear") } } } } }

Slide 106

Slide 106 text

Connecting to Backend (Swift)

Slide 107

Slide 107 text

URLSession • For simple requests, use URLSession.shared object • When receiving stuff, you will get NSData object • It is asynchronous • The URLSession.shared has method func dataTask(with: URL, completionHandler: (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

Slide 108

Slide 108 text

Example let myURL = URL(string: "https://my-restful-api.onrender.com/locations")! let httpTask = URLSession.shared.dataTask(with: myURL) { (optionalData, response, error) in print("done fetching") } httpTask.resume()

Slide 109

Slide 109 text

Example let myURL = URL(string: " https://my-restful-api.onrender.com/locations")! let httpTask = URLSession.shared.dataTask(with: myURL) { (optionalData, response, error) in print("done fetching") } httpTask.resume() NSData? URLResponse? Error?

Slide 110

Slide 110 text

Example: Using Response let httpTask = URLSession.shared.dataTask(with: myURL) {(optionalData, response, error) in if let httpResponse = response as? HTTPURLResponse { print("statusCode: \(httpResponse.statusCode)") } } HTTPURLResponse is a subclass of URLResponse

Slide 111

Slide 111 text

Example: Data -> String let httpTask = URLSession.shared.dataTask(with: myURL) {(optionalData, response, error) in let data : String = String(data: optionalData!, encoding: .utf8)! print(data) }

Slide 112

Slide 112 text

Parsing JSON (Swift)

Slide 113

Slide 113 text

JSON Parsing • Protocols • Encodable – convert struct to JSON • Decodable – convert JSON into struct • Codable – both • A lot of built types are already Codable, like Int, String, Bool, Dictionaries, Arrays • Arrays => JS Array, Dictionary => JS Object

Slide 114

Slide 114 text

Location struct Location: Decodable { let id: Int let lat: Double let lon: Double } Decodable: bytes (string) -> Object Encodable: Object -> bytes (string) Codable: Decodable and Encodable. These protocols will add methods to the struct that provide implementations for the parsing

Slide 115

Slide 115 text

Example using JSONDecoder let url : URL = URL(string: "https://my-restful-api.onrender.com/locations")! let httpTask = URLSession.shared.dataTask(with: url) { (optionalData : Data?, response: URLResponse?, error: Error?) in let jsonDecoder = JSONDecoder() do { let locations = try jsonDecoder.decode(Array.self, from: optionalData!) print(locations[0].lat) print(locations[0].lon) } catch { print(error) } } // Start the task httpTask.resume()

Slide 116

Slide 116 text

Displaying Result in UI

Slide 117

Slide 117 text

Content View struct ContentView: View { @StateObject var httpConnection = HttpConnection() var body: some View { NavigationStack { if(httpConnection.isFetched) { List { let locations = httpConnection.result! ForEach(locations, id: \.id) { location in Text("\(location.lat) - \(location.lon)") } }.navigationTitle("Location API") } else { ProgressView() } }.onAppear() { httpConnection.connect(url: "https://my-restful-api.onrender.com/locations") } } }

Slide 118

Slide 118 text

HTTP Class struct Location: Decodable { let id: Int let lat: Double let lon: Double } class HttpConnection : ObservableObject { @Published var result : Array? = nil @Published var isFetched = false func connect(url: String) { let myURL = URL(string: url)! let httpTask = URLSession.shared.dataTask(with: myURL) {(optionalData, response, error) in let jsonDecoder = JSONDecoder() DispatchQueue.main.async() { do { self.result = try jsonDecoder.decode(Array.self, from: optionalData!) self.isFetched = true } catch let error { print(error) } } } httpTask.resume() } } Accessing UI thread from worker thread is forbidden

Slide 119

Slide 119 text

Async + await

Slide 120

Slide 120 text

Async + Await • Introduction of async/await: • Introduced in Swift 5.5 as part of the Structured Concurrency model, it offers a more straightforward way to write asynchronous code compared to callbacks and promises. • Asynchronous Functions (async): • An async function is a function that performs an operation asynchronously. You mark a function as async to indicate it can perform work asynchronously and might pause (or "await") its execution to let other work be performed. • Awaiting on Tasks: • The await keyword is used to call async functions.

Slide 121

Slide 121 text

Task • Task in SwiftUI: • A Task in SwiftUI is used to run asynchronous work that can perform operations in parallel to the main UI thread. It is particularly useful for initiating asynchronous operations from the SwiftUI view's lifecycle events or from user interactions. • Automatic Cancellation: • When a SwiftUI view disappears from the screen, any ongoing tasks associated with that view are automatically canceled. This automatic task cancellation helps in managing memory and processing resources more efficiently.

Slide 122

Slide 122 text

Async + await example struct ContentView: View { func fetchData() async -> String { // Simulate a network delay try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds return "Data fetched from network" } @State private var data: String = "Loading..." var body: some View { VStack { Text(data) .padding() } .task { // Execute the fetchData function asynchronously data = await fetchData() } } }

Slide 123

Slide 123 text

try? • try? converts the result of a throwing function into an optional. • It simplifies error handling when you don't need to know the specific error. • The use of try? necessitates further unwrapping of the resulting optional to access the value.

Slide 124

Slide 124 text

Async + await example do { try await Task.sleep(nanoseconds: 2_000_000_000) // Sleeps for 2 seconds } catch { // Handle the cancellation error or other errors print("Task was cancelled or another error occurred.") } // Ignore any errors try? await Task.sleep(nanoseconds: 2_000_000_000)

Slide 125

Slide 125 text

enum DataError: Error { case networkFailure case invalidResponse } func fetchData() throws -> String { // Simulated network request let success = false // Change to true to simulate success if success { return "Fetched data successfully" } else { throw DataError.networkFailure } } // Using try? let result: String? = try? fetchData() if let data = result { print(data) } else { print("Failed to fetch data.") }

Slide 126

Slide 126 text

struct ContentView: View { @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") .padding() Button("Start Counting") { // Reset the count to 0 each time the button is tapped before starting count = 0 Task { await startCounting() } } .padding() } } private func startCounting() async { for i in 1...10 { await MainActor.run { // Update the count on the main thread self.count = i } await sleepWithDelay(seconds: 1) } } func sleepWithDelay(seconds duration: UInt64) async -> Void { do { try await Task.sleep(nanoseconds: duration * 1_000_000_000) // Sleep succeeded without interruption } catch { // Handle the error (e.g., task cancellation) print("Sleep was interrupted: \(error.localizedDescription)") } } }

Slide 127

Slide 127 text

import SwiftUI import Alamofire struct Joke: Decodable { let value: String } struct ContentView: View { @State private var joke = "Tap 'Get Joke' to fetch a random Chuck Norris joke." var body: some View { VStack(spacing: 20) { Text(joke) .padding() Button("Get Joke") { Task { await fetchRandomJoke() } } } } func fetchRandomJoke() async { let urlString = "https://api.chucknorris.io/jokes/random" do { let response: Joke = try await AF.request(urlString) .serializingDecodable(Joke.self) .value // Update the joke state variable on the main thread using Swift's concurrency model await MainActor.run { joke = response.value } } catch { // Handle potential errors, like network issues, on the main thread await MainActor.run { joke = "Failed to fetch joke. Please try again." } } } }

Slide 128

Slide 128 text

struct PeopleResponse: Decodable { let users: [Person] } struct Person: Decodable, Identifiable, Hashable { let id: Int let firstName: String let lastName: String let email: String } struct PersonDetailView: View { let person: Person var body: some View { Text("Hello, \(person.firstName)!") .font(.largeTitle) .padding() } } struct ContentView: View { @State var people: [Person] = [] var body: some View { NavigationStack { List(people) { person in NavigationLink(person.firstName, value: person) } .navigationDestination(for: Person.self) { person in PersonDetailView(person: person) } .navigationTitle("People") }.task { self.people = await fetchUsers() } } func fetchUsers() async -> [Person] { do { let response = try await AF.request("https://dummyjson.com/users") .serializingDecodable(PeopleResponse.self) .value return response.users } catch { print("Failed to fetch users: \(error)") return [] } } }