If you have come from a background using UIKit, you will have worked with ViewControllers and have seen several methods from the ViewController class that were called at certain times during the view loading. An example is viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
// Custom setup code
print("View has loaded.")
}
We override the method, and call super.viewDidLoad(), and then have time to do our own thing such as bringing in CoreLocation and setting the delegate, or whatever else you needed. There were several of these methods:
viewDidLoad() – Called when the view controller is loaded into memory
viewWillAppear() – Called when the view is about to appear on the screen
viewDidAppear() – Called right after the view appears on screen
viewWillDisappear() – Called just prior to the view being removed from the screen
viewDidDisappear() – Called when the view has disappeared from the screen
Depending on what setup or cleanup was needed, these were the places to perform that work.
SwiftUI has simplified this and now has .onAppear and .onDisappear that can be used. Because of @State, @Binding, @ObservedObject, and so on, we now have state-driven rendering that handle views updating as needed. When changes are detected, the view will load what it needs.
.onAppear
This relaces the need for viewWillAppear and viewDidAppear. You can add this to any view as follows:
struct ContentView: View {
var body: some View {
Text("Some text")
.onAppear {
print("onAppear")
// do something!!!
}
}
}
The Text view in this case has the .onAppear method applied. It gets called and completes before the first rendered frame appears. This highlights an issue that if anything takes time to process, it will hold up the entire view and not just the Text sub view. An example would be something like this:
import SwiftUI
struct ContentView: View {
@State private var message = "Waiting..."
var body: some View {
VStack(spacing: 20) {
Text("Hello, World!")
Text(message)
.onAppear {
// Simulate blocking CPU work (100 million iterations)
var total = 0
for i in 0..<100_000_000 {
total += i
}
message = "Finished heavy task"
}
}
.padding()
}
}
Nothing would appear on the screen until 100 million iterations have completed. You wouldn’t see the “Waiting…” message while it performs the loop because it completes before the first rendered frame.
In short, be careful what you do in .onAppear. If something takes time then its best wrapping it in a Task and using async/await so that the rest of the view loads.
This can be done the following way:
struct ContentView: View {
@State private var message = "Waiting..."
var body: some View {
VStack(spacing: 20) {
Text("Hello, World!")
Text(message)
.onAppear {
Task {
// Simulate blocking CPU work (100 million iterations)
var total = 0
for i in 0..<100_000_000 {
total += i
}
message = "Finished heavy task"
}
}
}
.padding()
}
}
By wrapping it in Task, the view is able to load, and when the task completes, the message @State is updated and the view is refreshed. Alternatively, you can use .task { … }.
.onDisappear
The SwiftUI .onDisappear method replaces the need for viewWillDisappear() and viewDidDisappear() from UIKit. Just like heavy work can block loading of the entire view when done in .onAppear, you can also block removal of the view when using .onDisappear, so make sure you use Task when needed. Here is an example of .onDisappear delaying the view from being removed:
struct ContentView: View {
@State private var showingDetail = true
@State private var status = "Waiting..."
var body: some View {
VStack(spacing: 20) {
Text("Parent View")
Text("Status: \(status)")
if showingDetail {
BlockingDetailView(onFinish: {
status = "Heavy task finished"
})
}
Button(showingDetail ? "Hide Detail" : "Show Detail") {
showingDetail.toggle()
}
}
.padding()
}
}
struct BlockingDetailView: View {
let onFinish: () -> Void
var body: some View {
VStack {
Text("Detail View")
}
.onDisappear {
print("Start blocking task")
performHeavyTask()
print("Finished blocking task")
onFinish()
}
}
func performHeavyTask() {
var total = 0
for i in 0..<100_000_000 {
total += i
}
}
}
The performHeavyTask causes the view to not disappear until the heavy task completes. To correct this, we can make the following changes:
.onDisappear {
Task {
await runHeavyTaskAsync()
onFinish()
}
}
Await the heavy task method.
func runHeavyTaskAsync() async {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
// Simulate blocking task
var total = 0
for i in 0..<100_000_000 {
total += i
}
continuation.resume()
}
}
}
Make sure the heavy task method runs asynchronously.
When running this new version of the code, the view will immediately dismiss while the Task does its thing. Because onFinish is also included in the task, this is called on completion and updates the parent view:
BlockingDetailView(onFinish: {
status = "Heavy task finished"
})
.onAppear and .onDisappear are extremely helpful when needing to do something when a view is getting ready to appear or getting ready to disappear. Just be mindful of longer running tasks and use Task and async/await where needed.
Leave a Reply
You must be logged in to post a comment.