Our app has an architecture based on ViewModels.
Currently, we are working on migrating from the ObservableObject protocol to the Observable macro (iOS 17+).
The official docs about this are available here: https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro
Our ViewModels that were previously annotated with @StateObject now use just @State, as recommended in the official docs.
Some of our screens (a screen is a SwiftUI view with a corresponding ViewModel) are presented modally. We expect that after dismissing a SwiftUI view that was presented modally, its corresponding ViewModel, which is owned by this view (via the @State modifier), will be deinitialized. However, it seems there is a memory leak, as the ViewModel is not deinitialized after a modal view is dismissed.
Here's a simple code where ModalView is presented modally (through the .sheet modifier), and ModalViewModel, which is a @State of ModalView, is never deinitialized.
import SwiftUI
import Observation
@Observable
final class ModalViewModel {
init() {
print("Simple ViewModel Inited")
}
deinit {
print("Simple ViewModel Deinited") // never called
}
}
struct ModalView: View {
@State var viewModel: ModalViewModel = ModalViewModel()
let closeButtonClosure: () -> Void
var body: some View {
ZStack {
Color.yellow
.ignoresSafeArea()
Button("Close") {
closeButtonClosure()
}
}
}
}
struct ContentView: View {
@State var presentSheet: Bool = false
var body: some View {
Button("Present sheet modally") {
self.presentSheet = true
}
.sheet(isPresented: $presentSheet) {
ModalView {
self.presentSheet = false
}
}
}
}
#Preview {
ContentView()
}
Is this a bug in the iOS 17 beta version or intended behavior? Is it possible to build a relationship between the View and ViewModel in a way where the ViewModel will be deinitialized after the View is dismissed?
Thank you in advance for the help.
Observation
RSS for tagMake responsive apps that update the presentation when underlying data changes.
Posts under Observation tag
45 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
Since Xcode 15 beta 5, making a class with the @Observable macro no longer requires all properties to have an initialization value, as seen in the video. Just put an init that collects the properties and everything works correctly.
@Observable
final class Score: Identifiable {
let id: Int
var title: String
var composer: String
var year: Int
var length: Int
var cover: String
var tracks: [String]
init(id: Int, title: String, composer: String, year: Int, length: Int, cover: String, tracks: [String]) {
self.id = id
self.title = title
self.composer = composer
self.year = year
self.length = length
self.cover = cover
self.tracks = tracks
}
}
But there is a problem: the @Observable macro makes each property to integrate the @ObservationTracked macro that seems not to conform the types to Equatable, and in addition, to Hashable.
Obviously, being a feature of each property, it is not useful to conform the class in a forced way with the static func == or with the hash(into:Hasher) function that conforms both protocols.
That any class we want to be @Observable does not conform to Hashable, prevents any instance with the new pattern to be usable within a NavigationStack using the data driven navigation bindings and the navigationDestination(for:) modifier.
I understand that no one has found a solution to this. If you have found it it would be great if you could share it but mainly I am making this post to invoke the mighty developers at Apple to fix this bug. Thank you very much.
P.S. - I also posted a Feedback (FB12535713), but no one replies. At least that I see.
Hi. The binding in a ForEach or List view doesn't work anymore when using the @Observable macro to create the observable object. For example, the following are the modifications I introduced to the Apple's example called "Migrating from the Observable Object Protocol to the Observable Macro" https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro
struct LibraryView: View {
@Environment(Library.self) private var library
var body: some View {
List($library.books) { $book in
BookView(book: book)
}
}
}
All I did was to add the $ to turn the reference to library.books into a binding but I got the error "Cannot find '$library' in scope"
Is this a bug or the procedure to use binding in lists changed?
Thanks
Previously, it was recommended to use the @MainActor annotation for ObservableObject implementation.
@MainActor
final class MyModel: ObservableObject {
let session: URLSession
@Published var someText = ""
init(session: URLSession) {
self.session = session
}
}
We could use this as either a @StateObject or @ObservedObject:
struct MyView: View {
@StateObject let model = MyModel(session: .shared)
}
By moving to Observation, I need to the @Observable macro, remove the @Published property wrappers and Switch @StateObject to @State:
@MainActor
@Observable
final class MyModel {
let session: URLSession
var someText = ""
init(session: URLSession) {
self.session = session
}
}
But switching from @StateObject to @State triggers me an error due to a call to main-actor isolated initialiser in a synchronous nonisolated context.
This was not the case with @StateObject of @ObservedObject.
To suppress the warning I could :
mark the initializer as nonisolated but it is not actually what I want
Mark the View with @MainActor but this sounds odd
Both solutions does not sound nice to my eye.
Did I miss something here?
I'm in the process of migrating to the Observation framework but it seems like it is not compatible with didSet. I cannot find information about if this is just not supported or a new approach needs to be implemented?
import Observation
@Observable class MySettings {
var windowSize: CGSize = .zero
var isInFullscreen = false
var scalingMode: ScalingMode = .scaled {
didSet {
...
}
}
...
}
This code triggers this error:
Instance member 'scalingMode' cannot be used on type 'MySettings'; did you mean to use a value of this type instead?
Anyone knows what needs to be done? Thanks!