Understanding SwiftUI View lifecycle
iOS SwiftUI View Estimated reading time: 5 minutesSwiftUI brings for us, developers, the whole new ecosystem for creating complex and responsible UI.
Thus the entry point for this approach is quite low and u can start producing acceptable UI after the first 5 min, it’s always better to dive a bit and understand how everything works under the hood. Such knowledge will improve your future work and our developer’s skills. Even more - without understanding how it works, u can’t develop something really interesting and stunning.
Talking about SwiftUI, the good start point may be understanding View’s lifecycle - how everything is combined and how every part and components of this ecosystem are related one to each other.
Lifecycle
We may draw a small scheme that visualize the full Lifecycle of a View as follow:
As u can see - it’s not very complex. But under this simplicity, there is much more. Every action has its mechanism(s) that improve and optimize it.
Let’s review a bit what’s going on when we create and use a SwiftUI’s View.
Initialization
View is a protocol that requires from us only body definition, we also have one more requirement - Type that conform to View protocol should be a value type, thus struct.
This requirement is not something that u can observe during a compiling time, instead of in runtime u will receive fatalError:
Fatal error: views must be value types: <ViewFromClass>: file SwiftUI, line 0
So View may be only a value type - struct. This means that initialization is quite simple and yet powerful. In most cases compiler create an initializer for us.
During initialization, we also often can create inner @State variables (should be private) or pass @State through @Binding. Other properties may be observed using various ways such as @StateObject, @ObservedValue or @SceneStorage etc.
So during initialization, we simply create and configure View’s states and “store” variables in SwiftUI ViewGraph.
ViewGraphconstructed by private frameworkAttributeGraph.framework(/System/Library/PrivateFrameworks/AttributeGraph.framework)
Result of this initialization - very complex type with Generics, or simply some View. some specially designed for hiding actual type. Why? That’s because of a few main reasons:
- type-level abstraction
- protection by hiding implementation details (that’s almost the same as the point above)
- simplification
some- is opaque type that was introduces here. This type simply hide return value’s type information and only refere to conformed protocol.
someis opaque type, so all limitation and possibilities are also in place:
PAT’s (protocol associated types) can’t be used for opaque type- these types are identifiable
- can be composed with generic placeholders
To check the actual type of View, we may create a very simple example:
struct ContentView: View {
@State private var counter = 0
var body: some View {
VStack {
Button(action: {
self.counter += 1
}, label: {
Text("Some Text")
})
if counter > 0 {
Text("Counter \(counter)")
}
}
.frame(height: 50)
}
}And if we use Mirror (aka print(Mirror(reflecting: self).subjectType)) we can get:
ModifiedContent<VStack<TupleView<(Button<Text>, Optional<Text>)>>, _FrameLayout>Quite complex generic type, that can be hidden above some View.
check out any complex
Viewthat u use in a real project - u will observe a huge name ofType.
Change
As we already know, View’s can be redrawn whenever something is changed. This performed very efficiently because of the used mechanism - SwiftUI checks what exactly was changed and redraw only this part (this is also known as diff).
AnyViewremoves this efficiency because this is type-eraser, soSwiftUIcan’t compare an unknown type with an unknown type, instead, the whole view will be redrawn. So useAnyViewwisely.
Another essential component of any View - various @propertyWrappers.
We can’t (actually can but with a lot of efforts) interact within View and show to user any update without special variables that can change and hold their state independently from View (thus view is a struct and any change will recreate/mutate it). Thanks to @propertyWrappers, we have a template with boiler part code for various purposes needed during the life of View.
I wrote an overview about available
@propertyWrappersinSwiftUI. You can check it here.
Anyway, these values are initialized within the view and changed outside of view. The only thing that should be done by View - is properly reacting to them. And it does. This is done by design. So we should think only about logic now, not about sync the data and view.
The update/redraw View flow may be as follow:
- Find State of View using
Field Descriptor - Inject
ViewGraphintoState - Render
View.body Stateis changedStatenotify theViewGraphto update view- Re-render
View.body
if u interested into how
@Statemight work in details - check this post
Events
Configuration of View’s states is also simplified - we have few callbacks. In additional, if we need some event handling configuration we may use another dataFlow mechanisms such as onChange(of:perform:) or some other View Modifiers.
check this official doc for more about data flow
If be clear, there are 2 callback that can be used
That’s it. The name tells us their purpose by itself.
var body: some View {
VStack {
EmptyView()
}
.onAppear {
// action
}
.onDisappear {
// action
}
}other input events
In total, we may sum-up all events and update a bit the scheme from the very beginning of the post:
Summary
Such an elegant design of View’s lifecycle reduces required effort, amount of code and so bugs.
With a declarative
Swiftsyntax that’s easy to read and natural to write,SwiftUIworks seamlessly with newXcodedesign tools to keep your code and design perfectly in sync (Apple). source
Share on: