Migrating Ice Cubes to the SwiftUI Observation framework

How to adopt it in a full-featured application

Thomas Ricouard
4 min readSep 18, 2023

If you were wondering if you should migrate your SwiftUI app to the new Observation framework and if it’s even worth it, you’re at the right place.

I recently bumped Ice Cubes, my open source Mastodon client, to iOS 17+ only, as I converted it to the new observation framework data flow.

If you’re unfamiliar with Ice Cubes, please read my previous article about it.

The key takeaways are:

  1. It’s not a complicated exercise. I did it in a few hours.
  2. It fixed bugs instead of creating new ones.
  3. Performance improvements are noticeable and worth it.

Ice Cubes make heavy use of ObservableObject injected as EnvironmentObject. The way the data flow worked previously, any mutation on a @Published property triggers a view update, regardless of its usage in the body of your view. So you have to be very conscious about which EnvironmentObject to retrieve and which property you update within those.

I spent a lot of time on Ice Cubes to ensure I don’t update too many properties at once or too often… But sometimes, it’s not something you can avoid. Not having to worry (too much) about this anymore is incredible and will save a ton of time spent in the profiler to try to understand what is going on.

The big difference between the previous data flow and the new Observation framework is that SwiftUI views only update their body when an observed property is used. It won’t trigger an update just because the object is referenced at the top of your file as @State or @Environment

You can read my pull request for the transition on the Ice Cubes repository here

Two things about SwiftUI performances:

  1. Compiling with Xcode 15 / iOS 17 SDK makes the app much faster out of the box on iOS 16 & 17 devices. It links with a different version of the SwiftUI library, and Apple has improved performances by a significant margin. But it’s noticeable on Ice Cubes. Users are praising how fast it is since the last update (the first update I shipped with Xcode 15 RC).
  2. Migrating the app to the Observation framework made it even faster. And it makes sense. The timeline on Ice Cubes is one of the most complete view hierarchies of the app. It displays statuses with a ton of data to manipulate and various user settings. The row is composed of multiple small views but has a ton of dependencies on different environment values.

When profiling the app, scrolling the timeline triggers way fewer properties and views updates, thanks to Observable being more intelligent about when to update a view when a property changes in the view model, for example.

Don’t forget to use @ObservationIgnored when you don’t want your view to react to a specific property change. If before you didn’t have a @Published property wrapper on it, it’s probably a good candidate to do so. It means it won’t trigger any view refresh, even if you use it within your view body.

@MainActor
@Observable class StatusDetailViewModel {
enum State {
case loading, display(statuses: [Status]), error(error: Error)
}

var state: State = .loading

@ObservationIgnored
var isReplyToPreviousCache: [String: Bool] = [:]
}

Once you convert an ObservableObject to @Observable, the view responsible for holding the object should use @State (instead of @StateObject), but you don’t need any specific annotation when you pass it to one of your subviews.

public struct StatusRowView: View {
@State private var viewModel: StatusRowViewModel

public init(viewModel: StatusRowViewModel) {
_viewModel = .init(initialValue: viewModel)
}
public var body: some View {
HStack(spacing: 0) {
...
StatusRowContentView(viewModel: viewModel)
}
}

struct StatusRowContentView: View {
var viewModel: StatusRowViewModel

var body: some View {
if !viewModel.finalStatus.spoilerText.asRawText.isEmpty {
@Bindable var viewModel = viewModel
StatusRowSpoilerView(status: viewModel.finalStatus, displaySpoiler: $viewModel.displaySpoiler)
}
}

For example, above is one of the subviews of my StatusRowView; the viewModel is referenced as a simple var, and the view will still react to any property change and be used in the body of this view.

Then, if you need to pass a property of this object as a binding to another view, you can make your object Bindable within a view builder.

I don’t have much else to say; the observation framework is a tremendous improvement for SwiftUI. It’s simple and erases a lot of headaches from the previous data flow. “It just works” as Todd Howard would say 🚀

--

--

Thomas Ricouard

📱 🚀 🇫🇷 [Entrepreneur, iOS/Mac & Web dev] | Now @Medium, @Glose 📖| Past @google 🔍 | Co-founded few companies before, a movies 🎥 app and smart browser one.