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 Appllication 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, and be 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

SceneFile @main struct MyScene: App { var body: some Scene { #if os(macOS) WindowGroup { Text("Mac") } #else WindowGroup { Text("iOS") } #endif } }

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

Some Basic UI Components

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 } ) 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

Navigation struct ContentView: View { var body: some View { NavigationView { NavigationLink(destination: Text("Second View")) { Text("Hello, World!") }.navigationBarTitle("Navigation") } } }

Slide 62

Slide 62 text

Output

Slide 63

Slide 63 text

ForEach • ForEach in SwiftUI is a view • Can return it directly from your view body. • Provide it an array of items • You may 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 64

Slide 64 text

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

Slide 65

Slide 65 text

Example: Using Trailing Closure struct ContentView: View { var body: some View { VStack() { ForEach((1..<10)) { Text("\($0)") } } } } init(Range, content: (Int) -> Content)

Slide 66

Slide 66 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 67

Slide 67 text

ForEach: Solution 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 68

Slide 68 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 69

Slide 69 text

ForEach: Solution struct MyColor: Hashable { var color : 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() .background($0.color) .cornerRadius(20) } } } } Creates id that is the same if properties are same

Slide 70

Slide 70 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 71

Slide 71 text

And now this works also... struct ContentView: View { let colors: [Color] = [.red, .green, .blue] var body: some View { VStack() { ForEach(colors, id: \.self) { Text("\($0.description)") .padding() .background($0) .cornerRadius(20) } } } }

Slide 72

Slide 72 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 73

Slide 73 text

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

Slide 74

Slide 74 text

Navigation with List struct ContentView: View { let colors = ["red", "green", "blue"] var body: some View { NavigationStack { List { ForEach(colors, id: \.self) { color in Text("\(color)") } }.navigationBarTitle("Colors") } } } Notice that this is given to the inner component!

Slide 75

Slide 75 text

Toggle and NavigationStack struct ContentView: View { @State private var toggle = true let colors = ["red", "green", "blue"] let cars = ["audi", "bmw", "mercedes"] var body: some View { VStack { NavigationView { if(toggle) { List { ForEach(colors, id: \.self) { color in Text("\(color)") } }.navigationBarTitle("Colors") } else { List { ForEach(cars, id: \.self) { car in Text("\(car)") } }.navigationBarTitle("Cars") } } Toggle(isOn: $toggle) { Text("Colors") }.padding() } } }

Slide 76

Slide 76 text

With Toggle

Slide 77

Slide 77 text

Sections struct ContentView: View { let premiumCars = ["mercedes", "bmw", "audi"] let cars = ["skoda", "ford", "kia"] var body: some View { VStack { NavigationStack { List { Section(header: Text("Premium")) { ForEach(premiumCars, id: \.self) { car in Text("\(car)") } } Section(header: Text("Other")) { ForEach(cars, id: \.self) { car in Text("\(car)") } } }.navigationBarTitle("Cars") } } } }

Slide 78

Slide 78 text

More about Navigation

Slide 79

Slide 79 text

NavigationView vs NavigationStack • If your app has a minimum deployment target of iOS 16, iPadOS 16, macOS 13, tvOS 16, or watchOS 9, or later, transition away from using NavigationView. In its place, use NavigationStack and NavigationSplitView instances.

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

struct ContentView: View { var body: some View { NavigationStack { VStack { Spacer() Text("First View") Spacer() NavigationLink(destination: Text("Second View")) { Text("Move to Second View") }.padding() } .navigationBarTitle("Navigation") } } }

Slide 83

Slide 83 text

NavigationLink

Slide 84

Slide 84 text

Passing Data A -> B struct SecondView: View { var name = "" var body: some View { Text("you gave: \(name)") } } struct ContentView: View { @State var name = "" var body: some View { NavigationStack { VStack { Spacer() TextField("Enter name", text: $name) .padding() .textFieldStyle(RoundedBorderTextFieldStyle()) Spacer() NavigationLink(destination: SecondView(name: name)) { Text("To the Second View") }.padding() } .navigationBarTitle("Navigation") } } }

Slide 85

Slide 85 text

Passing Data B -> A struct SecondView: View { @Binding var name : String; var body: some View { TextField("Enter name", text: $name) .padding() .textFieldStyle(RoundedBorderTextFieldStyle()) } } struct ContentView: View { @State var name = "" var body: some View { NavigationStack { VStack { NavigationLink(destination: SecondView(name: $name)) { Text("To the Second View") }.padding() Text("\(name)") } .navigationBarTitle("Navigation") } } } Passing binding to the second view Accepting the binding

Slide 86

Slide 86 text

Several Navigation Links struct ContentView: View { var body: some View { NavigationStack { NavigationLink("Mint", value: 1) NavigationLink("Pink", value: 2) NavigationLink("Teal", value: 3) .navigationDestination(for: Int.self) { value in Text("hello = \(value)") } .navigationTitle("Colors") } } }

Slide 87

Slide 87 text

Using List struct ContentView: View { var body: some View { NavigationStack { List { NavigationLink("Mint", value: 1) NavigationLink("Pink", value: 2) NavigationLink("Teal", value: 3) } .navigationDestination(for: Int.self) { value in Text("hello = \(value)") } .navigationTitle("Colors") } } }

Slide 88

Slide 88 text

And using color.. struct ContentView: View { var body: some View { NavigationStack { List { NavigationLink("Mint", value: Color.mint) NavigationLink("Pink", value: Color.pink) NavigationLink("Teal", value: Color.teal) } .navigationDestination(for: Color.self) { value in Text(value.description) .padding() .background(value) .cornerRadius(8) } .navigationTitle("Colors") } } }

Slide 89

Slide 89 text

Programmically trigger navigation link struct ContentView: View { @State private var isShowingDetailView = false var body: some View { NavigationStack { VStack { Button("Tap to show detail") { self.isShowingDetailView = true } } .navigationBarTitle("Navigation") .navigationDestination(isPresented: $isShowingDetailView) { Text("second view") } } } }

Slide 90

Slide 90 text

Property Wrappers

Slide 91

Slide 91 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 92

Slide 92 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 93

Slide 93 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 94

Slide 94 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 95

Slide 95 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 96

Slide 96 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 97

Slide 97 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 98

Slide 98 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 99

Slide 99 text

Property Wrappers https://swiftuipropertywrappers.com

Slide 100

Slide 100 text

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

Slide 101

Slide 101 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 102

Slide 102 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 103

Slide 103 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 104

Slide 104 text

Lifecycle Methods

Slide 105

Slide 105 text

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

Slide 106

Slide 106 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 107

Slide 107 text

Connecting to Backend (Swift)

Slide 108

Slide 108 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 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()

Slide 110

Slide 110 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 111

Slide 111 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 112

Slide 112 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 113

Slide 113 text

Parsing JSON (Swift)

Slide 114

Slide 114 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 115

Slide 115 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 116

Slide 116 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 117

Slide 117 text

Displaying Result in UI

Slide 118

Slide 118 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)") } }.navigationBarTitle("Location API") } else { ProgressView() } }.onAppear() { httpConnection.connect(url: "https://my-restful-api.onrender.com/locations") } } }

Slide 119

Slide 119 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 120

Slide 120 text

Async + await

Slide 121

Slide 121 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 122

Slide 122 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 123

Slide 123 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 124

Slide 124 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 125

Slide 125 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 126

Slide 126 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 127

Slide 127 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 128

Slide 128 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 129

Slide 129 text

import SwiftUI import Alamofire import Foundation struct User: Decodable, Identifiable { let id: Int let name: String let username: String let email: String } struct ContentView: View { @State private var users: [User] = [] var body: some View { NavigationView { List(users) { user in VStack(alignment: .leading) { Text(user.name) .font(.headline) Text("@\(user.username)") .font(.subheadline) Text(user.email) .font(.footnote) } } .navigationTitle("Users") .task { await fetchUsers() } } } func fetchUsers() async { let urlString = "https://jsonplaceholder.typicode.com/users" do { let response: [User] = try await AF.request(urlString) .serializingDecodable([User].self) .value await MainActor.run { users = response } } catch { print("Failed to fetch users: \(error.localizedDescription)") // Handle error or update the UI accordingly } } }