Adapting to SwiftUI View Lifecycle
For an iOS developer working with UIKit and View Controllers, lifecycle events are an important part of code for the implementation of any kind of view really.
For an iOS developer working with UIKit and View Controllers, lifecycle events are an important part of code for the implementation of any kind of view really.
View controllers and their views are created and disposed of when the user navigates through your application. These user-initiated actions and other actions called by the system make the controllers perform predefined methods based on the event occurring. This sequence of events is the view lifecycle, from creation to deletion.
Lifecycle events are called automatically by the view controller as it moves between different states and are used for managing the subviews and other services used within the scene.
Some of the events can be used for additional setup actions as the controller and views are being created or after they have been loaded, while others are used for cleanup and teardown actions as the view is dismissed.
You need to add at least one lifecycle event override to every view controller, and you usually need more for a variety of purposes. This depends on the complexity of your application, both visually and service-wise.
The events used are:
- init
- loadView
- viewDidLoad
- viewWillAppear
- viewWillLayoutSubviews
- viewDidLayoutSubviews
- viewDidAppear
- didReceiveMemoryWarning
- viewWillDisappear
- viewDidDisappear
- deinit
How is this different with SwiftUI?
Like many things in SwiftUI, you also have to unlearn these lifecycle events when transitioning away from UIKit. Fortunately it’s much less complicated and declutters your code quite a bit.
Because of the declarative and reactive nature of SwiftUI, you can’t really use UIKit’s lifecycle events anymore. In their place, SwiftUI provides a few identifiable “events” that can be used for similar actions. OnAppear and onDisappear are instance methods used in views and they are equivalent to UIKit’s viewDidAppear() and viewDidDisappear().
These methods are quite straightforward and should be quick to learn when transitioning from UIKit.
As SwiftUI does not use View Controllers but just Views, these methods are linked to the specific view or subview and are called wherever the view is shown or removed from the stack. Example code below:
VStack {
Text("View")
}.onAppear {
print("VStack appeared!")
}.onDisappear {
print("VStack disappeared!")
}
SwiftUI, together with the Combine framework, provides a declarative way of binding the views to the underlying viewmodel with property wrappers. Keeping that in mind, they can also be used as triggers to determine which subviews are displayed. Changing such states can be seen as a lifecycle event as it can be surrounded by runnable code.
In addition to these, view initialization can also be counted as a lifecycle event. It happens separately before view properties are available and can include runnable code.
Although I don’t have any first-hand experience of translating a UIViewController to a SwiftUI View, I’m sure it would require a lot of rewriting to adapt to the new ways. As UIKit events cannot be used, some of the business logic may have to be refactored and the designs adjusted too.
Get to know the UIViewControllerRepresentable protocol
If translating a View Controller would bring too many changes or you want to use the old lifecycle events for other reasons, you may want to do the conversion with SwiftUI’s UIViewControllerRepresentable protocol. It allows you to manage your View Controller object in the SwiftUI interface without having to alter the controller itself. It also provides all the methods needed for creating, configuring and updating the View Controller as well as coordinating changes to other SwiftUI views. Example of a simple UIViewController wrapper:
struct ExampleView: UIViewControllerRepresentable {
typealias UIViewControllerType = ExampleUIViewController
class Coordinator {
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIViewController(context: Context) -> UIViewControllerType {
return ExampleUIViewController()
}
func updateUIViewController(_ uiViewController:
UIViewControllerType, context: Context) {
}
static func dismantleUIViewController(_ uiViewController:
UIViewControllerType, coordinator: Coordinator) {
}
}
In the example above, you can see the functions that are part of a represented UIViewControllers lifecycle. The events happen in the following order:
- makeCoordinator()
- makeUIViewController()
- updateUIViewController()
- dismantleUIViewController()
Another good thing to know is that you can wrap a single UIView inside SwiftUI using the UIViewRepresentable protocol that has similar lifecycle events. This is useful when you want to integrate a UIKit view that doesn’t yet have an equivalent in SwiftUI.
Conclusion
Reactive programming in general has always been confusing to me as a iOS developer, but the way SwiftUI and Combine work has made it surprisingly clear to me. While I’m still learning, I don’t miss the traditional lifecycle events too often anymore. I enjoy the readability of SwiftUI code and am happy that I don’t have to clutter it with some of the methods required in UIKit.
OnAppear and onDisappear methods together with state changes provide the most use when implementing view lifecycle events. And while SwiftUI is still young and lacking in many areas, any type of UIKit views and View Controllers can be integrated seamlessly to your SwiftUI app when needed.
Further reading and other great resources
Medium – The Simple Life(cycle) of a SwiftUI View
Medium – Lifecycle Methods in SwiftUI
Yet Another Swift Blog – Using UIView and UIViewController in SwiftUI
Hacking with Swift – SwiftUI by Example
Fucking SwiftUI – Cheat Sheet
Illustration: Joel Pöllänen