Recently I noticed how my ViewModels aren't deallocating and they end up as a memory leaks. I found something similar in this thread but this is also happening without using @Observation. Check the source code below:
class CellViewModel: Identifiable {
let id = UUID()
var color: Color = Color.red
init() { print("init") }
deinit { print("deinit") }
}
struct CellView: View {
let viewModel: CellViewModel
var body: some View {
ZStack {
Color(viewModel.color)
Text(viewModel.id.uuidString)
}
}
}
@main
struct LeakApp: App {
@State var list = [CellViewModel]()
var body: some Scene {
WindowGroup {
Button("Add") {
list.append(CellViewModel())
}
Button("Remove") {
list = list.dropLast()
}
ScrollView {
LazyVStack {
ForEach(list) { model in
CellView(viewModel: model)
}
}
}
}
}
}
When I tap the Add button twice in the console I will see "init" message twice. So far so good. But then I click the Remove button twice and I don't see any "deinit" messages.
I used the Debug Memory Graph in Xcode and it showed me that two CellViewModel objects are in the memory and they are owned by the CellView and some other objects that I don't know where are they coming from (I assume from SwiftUI internally).
I tried using VStack instead of LazyVStack and that did worked a bit better but still not 100% "deinits" were in the Console.
I tried using weak var
struct CellView: View {
weak var viewModel: CellViewModel?
....
}
but this also helped only partially.
The only way to fully fix this is to have a separate class that holds the list of items and to use weak var viewModel: CellViewModel?. Something like this:
class CellViewModel: Identifiable {
let id = UUID()
var color: Color = Color.red
init() { print("init") }
deinit { print("deinit") }
}
struct CellView: View {
var viewModel: CellViewModel?
var body: some View {
ZStack {
if let viewModel = viewModel {
Color(viewModel.color)
Text(viewModel.id.uuidString)
}
}
}
}
@Observable
class ListViewModel {
var list = [CellViewModel]()
func insert() {
list.append(CellViewModel())
}
func drop() {
list = list.dropLast()
}
}
@main
struct LeakApp: App {
@State var viewModel = ListViewModel()
var body: some Scene {
WindowGroup {
Button("Add") {
viewModel.insert()
}
Button("Remove") {
viewModel.drop()
}
ScrollView {
LazyVStack {
ForEach(viewModel.list) { model in
CellView(viewModel: model)
}
}
}
}
}
}
But this won't work if I want to use @Bindable such as
@Bindable var viewModel: CellViewModel?
I don't understand why SwiftUI doesn't want to release the objects?
Observation
RSS for tagMake responsive apps that update the presentation when underlying data changes.
Posts under Observation tag
36 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
Hello, following is the issue: I have a @Observable view model which has an array of @Observable Items.
Tapping an item leads to a detail like view to submit a value to the item. This work thru bindings. However I have the need to replace the contents of the array entirely with a fresh version loaded from the network. It will contain the "same" objects with the same id but some values might have changed. So replacing the entire array seems to not update the UI because the IDs are the same as before. Also this seems to break the bindings because when replacing the array, editing no longer updates the UI.
How to test the behavior:
Launch the app in simulator.
Add some values to the items by tapping on an item and then on add.
Notice how changes are updated.
Tap the blue button to sync fresh data to the array. (Not replacing the actual array)
Confirm everything is still working
Replace the array with the red button.
Editing and UI updates are broken from now on.
What is the proper way to handle this scenario?
Project: https://github.com/ChristianSchuster/DTS_DataReplaceExample.git
I have an app with the following model:
@Model class TaskList {
@Attribute(.unique)
var name: String
// Relationships
var parentList: TaskList?
@Relationship(deleteRule: .cascade, inverse: \TaskList.parentList)
var taskLists: [TaskList]?
init(name: String, parentTaskList: TaskList? = nil) {
self.name = name
self.parentList = parentTaskList
self.taskLists = []
}
}
If I run the following test, I get the expected results - Parent has it's taskLists array updated to include the Child list created. I don't explicitly add the child to the parent array - the parentList relationship property on the child causes SwiftData to automatically perform the append into the parent array:
@Test("TaskList with children with independent saves are in the database")
func test_savingRootTaskIndependentOfChildren_SavesAllTaskLists() async throws {
let modelContext = TestHelperUtility.createModelContext(useInMemory: false)
let parentList = TaskList(name: "Parent")
modelContext.insert(parentList)
try modelContext.save()
let childList = TaskList(name: "Child")
childList.parentList = parentList
modelContext.insert(childList)
try modelContext.save()
let fetchedResults = try modelContext.fetch(FetchDescriptor<TaskList>())
let fetchedParent = fetchedResults.first(where: { $0.name == "Parent"})
let fetchedChild = fetchedResults.first(where: { $0.name == "Child" })
#expect(fetchedResults.count == 2)
#expect(fetchedParent?.taskLists.count == 1)
#expect(fetchedChild?.parentList?.name == "Parent")
#expect(fetchedChild?.parentList?.taskLists.count == 1)
}
I have a subsequent test that deletes the child and shows the parent array being updated accordingly.
With this context in mind, I'm not seeing these relationship updates being observed within SwiftUI. This is an app that reproduces the issue. In this example, I am trying to move "Finance" from under the "Work" parent and into the "Home" list.
I have a List that loops through a @Query var taskList: [TaskList] array. It creates a series of children views and passes the current TaskList element down into the view as a binding.
When I perform the operation below the "Finance" element is removed from the "Work" item's taskLists array automatically and the view updates to show the removal within the List. In addition to that, the "Home" item also shows "Finance" within it's taskLists array - showing me that SwiftData is acting how it is supposed to - removed the record from one array and added it to the other.
The View does not reflect this however. While the view does update and show "Finance" being removed from the "Work" list, it does not show the item being added to the "Home" list. If I kill the app and relaunch I can then see the "Finance" list within the "Home" list. From looking at the data in the debugger and in the database, I've confirmed that SwiftData is working as intended. SwiftUI however does not seem to observe the change.
ToolbarItem {
Button("Save") {
list.name = viewModel.name
list.parentList = viewModel.parentTaskList
try! modelContext.save()
dismiss()
}
}
To troubleshoot this, I modified the above code so that I explicitly add the "Finance" list to the "Home" items taskLists array.
ToolbarItem {
Button("Save") {
list.name = viewModel.name
list.parentList = viewModel.parentTaskList
if let newParent = viewModel.parentTaskList {
// MARK: Bug - This resolves relationship not being reflected in the View
newParent.taskLists?.append(list)
}
try! modelContext.save()
dismiss()
}
}
Why does my explicit append call solve for this? My original approach (not manually updating the arrays) works fine in every unit/integration test I run but I can't get SwiftUI to observe the array changes.
Even more strange is that when I look at viewModel.parentTaskList.taskLists in this context, I can see that the list item already exists in it. So my code effectively tries to add it a second time, which SwiftData is smart enough to prevent from happening. When I do this though, SwiftUI observes a change in the array and the UI reflects the desired state.
In addition to this, if I replace my custom list rows with an OutlineGroup this issue doesn't manifest itself. SwiftUI stays updated to match SwiftData when I remove my explicit array addition.
I don't understand why my views, which is passing the TaskList all the way down the stack via Bindable is not updating while an OutlineGroup does.
I have a complete reproducible ContentView file that demonstrates this as a Gist. I tried to provide the source here but it was to much for the post.
One other anecdote. When I navigate to the TaskListEditorScreen and open the TaskListPickerScreen I get the following series of errors:
error: the replacement path doesn't exist: "/var/folders/07/3px_03md30v9n105yh3rqzvw0000gn/T/swift-generated-sources/@_swiftmacro_09SwiftDataA22UIChangeDetectionIssue20TaskListPickerScreenV9taskLists33_A40669FFFCF66BB4EEA5302BB5ED59CELL5QueryfMa.swift"
I saw another post regarding these and I'm wondering if my issue is related to this.
So my question is, do I need to handle observation of SwiftData models containing arrays differently in my custom views? Why do bindings not observe changes made by SwiftData but they observe changes made explicitly by me?
Im building an recipe app for the social media of my mother. i already have the functionality for the users, when a user gets created an empty array gets initiated at the database named favoriteRecipes, which stores the id of his favorite recipes to show in a view.
This is my AuthViewModel which is relevant for the user stuff:
import Firebase
import FirebaseAuth
import FirebaseFirestore
protocol AuthenticationFormProtocol {
var formIsValid: Bool { get }
}
@MainActor
class AuthViewModel : ObservableObject {
@Published var userSession: FirebaseAuth.User?
@Published var currentUser: User?
@Published var currentUserId: String?
init() {
self.userSession = Auth.auth().currentUser
Task {
await fetchUser()
}
}
func signIn(withEmail email: String, password: String) async throws {
do {
let result = try await Auth.auth().signIn(withEmail: email, password: password)
self.userSession = result.user
await fetchUser() // fetch user sonst profileview blank
} catch {
print("DEBUG: Failed to log in with error \(error.localizedDescription)")
}
}
func createUser(withEmail email: String, password: String, fullName: String) async throws {
do {
let result = try await Auth.auth().createUser(withEmail: email, password: password)
self.userSession = result.user
let user = User(id: result.user.uid, fullName: fullName, email: email)
let encodedUser = try Firestore.Encoder().encode(user)
try await Firestore.firestore().collection("users").document(result.user.uid).setData(encodedUser)
await fetchUser()
} catch {
print("Debug: Failed to create user with error \(error.localizedDescription)")
}
}
func signOut() {
do {
try Auth.auth().signOut() // sign out user on backend
self.userSession = nil // wipe out user session and take back to login screen
self.currentUser = nil // wipe out current user data model
} catch {
print("DEBUG: Failed to sign out with error \(error.localizedDescription)")
}
}
func deleteAcocount() {
let user = Auth.auth().currentUser
user?.delete { error in
if let error = error {
print("DEBUG: Error deleting user: \(error.localizedDescription)")
} else {
self.userSession = nil
self.currentUser = nil
}
}
}
func fetchUser() async {
guard let uid = Auth.auth().currentUser?.uid else { return }
currentUserId = uid
let userRef = Firestore.firestore().collection("users").document(uid)
do {
let snapshot = try await userRef.getDocument()
if snapshot.exists {
self.currentUser = try? snapshot.data(as: User.self)
print("DEBUG: current user is \(String(describing: self.currentUser))")
} else {
// Benutzer existiert nicht mehr in Firebase, daher setzen wir die userSession auf nil
self.userSession = nil
self.currentUser = nil
}
} catch {
print("DEBUG: Fehler beim Laden des Benutzers: \(error.localizedDescription)")
}
}
}
This is the code to fetch the favorite recipes, i use the id of the user to access the collection and get the favoriteRecipes out of the array:
import SwiftUI
@MainActor
class FavoriteRecipeViewModel: ObservableObject {
@Published var favoriteRecipes: [Recipe] = []
@EnvironmentObject var viewModel: AuthViewModel
private var db = Firestore.firestore()
init() {
Task {
await fetchFavoriteRecipes()
}
}
func fetchFavoriteRecipes() async{
let userRef = db.collection("users").document(viewModel.userSession?.uid ?? "")
do {
let snapshot = try await userRef.collection("favoriteRecipes").getDocuments()
let favoriteIDs = snapshot.documents.map { $0.documentID }
let favoriteRecipes = try await fetchRecipes(recipeIDs: favoriteIDs)
} catch {
print("DEBUG: Failed to load favorite recipes for user: \(error.localizedDescription)")
}
}
func fetchRecipes(recipeIDs: [String]) async throws -> [Recipe] {
var recipes: [Recipe] = []
for id in recipeIDs {
let snapshot = try await db.collection("recipes").document(id).getDocument()
if let recipe = try? snapshot.data(as: Recipe.self) {
recipes.append(recipe)
}
}
return recipes
}
}
Now the Problem occurs at the build of the project, i get the error
SwiftUICore/EnvironmentObject.swift:92: Fatal error: No ObservableObject of type AuthViewModel found. A View.environmentObject(_:) for AuthViewModel may be missing as an ancestor of this view.
I already passed the ViewModel instances as EnvironmentObject in the App Struct.
import SwiftUI
import FirebaseCore
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
@main
struct NimetAndSonApp: App {
@StateObject var viewModel = AuthViewModel()
@StateObject var recipeViewModel = RecipeViewModel()
@StateObject var favoriteRecipeViewModel = FavoriteRecipeViewModel()
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
.environmentObject(recipeViewModel)
.environmentObject(favoriteRecipeViewModel)
}
}
}
I have a problem with the following code, I am not being notified of changes to the progress property of my Job object, which is @Observable... This is a command-line Mac application (the same code works fine in a SwiftUI application).
I must have missed something?
do {
let job = AsyncJob()
withObservationTracking {
let progress = job.progress
} onChange: {
print("Current progress: \(job.progress)")
}
let _ = try await job.run()
print("Done...")
} catch {
print(error)
}
I Try this without any success:
@main
struct MyApp {
static func main() async throws {
// my code here
}
}
In WWDC 2023 there was a good summary of how to handle the iOS 17 Observation capability.
But despite the clear graphics, it was still ambiguous (for me.)
I want to inject a class (view-model) so that it can be used in the complete view heirarchy, and used in bindings to allow bi-directional communication.
As far as I can tell there are 2 ways of declaring the VM (alternatives 1 and 2 in my code), and 2 ways of consuming the VM in a view (alternatives 3 and 4 in my code). Using the flow-diagram I can't determine which is best.
Here's the crux of my #Observable problem.
import SwiftUI
// MARK: - Model
struct MyMod {
var title = "Hello, World!"
}
// MARK: - MVV
@Observable
class MyMVV {
var model: MyMod
init() {
self.model = MyMod()
}
}
// MARK: - App
@main
struct MyApp: App {
@Bindable var myGlobalMVV = MyMVV() // Alternative 1
// @State var myGlobalMVV = MyMVV() // Alternative 2
var body: some Scene {
WindowGroup {
ContentView()
.environment(myGlobalMVV) // inject
}
}
}
struct ContentView: View {
var body: some View {
ContentDeepHierarchyView()
}
}
struct ContentDeepHierarchyView: View {
@Environment(MyMVV.self) var myGlobalMVV // digest
var body: some View {
@Bindable var myLocalMVV = myGlobalMVV // Alternative 3
TextField("The new title", text: $myLocalMVV.model.title) // Alternative 3
TextField("The new title", text: Bindable(myGlobalMVV).model.title) // Alternative 4
}
Opinions?
According to this old thread
the answer is no. But I never understood why.
In the old world. It was always required that you make changes to @Published properties on the main thread. In fact compiler would complain.
In the main world, can you just update that in the background thread? And then SwiftUI take cares of refreshing the views on the main thread? So I guess that begs that question, why did it used to require it for @Published?
Furthermore, I have recently gotten new crashes when update is done from background but I can't be sure it's related:
For example
I have the following, and the crash is as follows:
@Observable
class PlanViewModel {
var stagingPlan: Plan?
func savePlan() async {
//some code here....
stagingPlan = nil //crash
}
}
Is this issue potentially related to main thread? Should I do that assignment forcefully on main thread?
call stack 1
call stack 2
call stack 3
I dont know how to troubleshoot this further as xcode doesnt provide me any info other than that one red line
In SwiftUI's ViewModel class that are @Observable, is it necessary to annotate private fields as @ObservationIgnored?
I'm not sure if adding @ObservationIgnored to these fields will get performance gains, since there are no SwiftUI structs referencing these fields because they're private. However, I'd like to know what's the recommended approach here?
While this might not seem obvious for the example below, however, sometimes I have private fields that are changing pretty frequently. For these frequently changed fields, I think the performance gains will be larger.
Example:
@Observable
class UserProfileViewModel {
var userName: String?
var userPhoneNumber: String?
private var isFetchingData = false
}
vs
@Observable
class UserProfileViewModel {
var userName: String?
var userPhoneNumber: String?
@ObservationIgnored private var isFetchingData = false
}
I'm trying to create an equivalent to TabView, but with the difference that the 2nd View slides in over the top of the primary view.
Maybe there's a more elegant way of coding this (suggestions appreciated), but I've almost succeeded using the dragGesture. When a user swipes right to left the observed variable showTab2 is set to true, and the 2nd tab glides in over the top of tab 1 and displays 🥳.
The only problem is, that when a user happens to start the swipe over a button, the observed status (showTab2) does change as expected but the main view does not catch this change and does not display tab2. And that despite the showTab2 being an @Observable.
Any clues what I've missed? Or how to capture that the start of a swipe gesture starts over the top of a button and should be ignored.
According to the code in SwipeTabView this screenshot 👆 should never occur.
Here's the code:
@Observable
class myclass {
var showTab2 = false
}
struct SwipeTabView: View {
@State var myClass = myclass()
@State var dragAmount: CGSize = CGSize.zero
var body: some View {
VStack {
ZStack {
GeometryReader { geometryProxy in
VStack {
tab(tabID: 1, selectedTab: myClass.showTab2)
.zIndex(/*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/)
.background(.black)
.transition(.identity)
.swipeable(stateOfViewAdded: $myClass.showTab2, dragAmount: $dragAmount, geometryProxy: geometryProxy, insertion: true)
}
if myClass.showTab2 || dragAmount.width != 0 {
tab(tabID: 2, selectedTab: myClass.showTab2)
.zIndex(2.0)
.drawingGroup()
.transition(.move(edge: .trailing))
.offset(x: dragAmount.width )
.swipeable(stateOfViewAdded: $myClass.showTab2, dragAmount: $dragAmount, geometryProxy: geometryProxy, insertion: false)
}
}
}
}
}
}
extension View {
func swipeable(stateOfViewAdded: Binding<Bool>,
dragAmount: Binding<CGSize>,
geometryProxy: GeometryProxy,
insertion: Bool) -> some View {
self.gesture(
DragGesture()
.onChanged { gesture in
// inserting must be minus, but removing must be positive - hence the multiplication.
if gesture.translation.width * (insertion ? 1 : -1 ) < 0 {
if insertion {
dragAmount.wrappedValue.width = geometryProxy.size.width + gesture.translation.width
} else {
dragAmount.wrappedValue.width = gesture.translation.width
}
}
}
.onEnded { gesture in
if abs(gesture.translation.width) > 100.0 && gesture.translation.width * (insertion ? 1 : -1 ) < 0 {
withAnimation(.easeOut.speed(Double(gesture.velocity.width))) {
stateOfViewAdded.wrappedValue = insertion
}
} else {
withAnimation(.easeOut.speed(Double(gesture.velocity.width))) {
stateOfViewAdded.wrappedValue = !insertion
}
}
withAnimation(.smooth) {
dragAmount.wrappedValue = CGSize.zero
}
}
)
}
}
struct tab: View {
var tabID: Int
var selectedTab: Bool
var body: some View {
ZStack {
Color(tabID == 1 ? .yellow : .orange)
VStack {
Text("Tab \(tabID) ").foregroundColor(.black)
Button(action: {
print("Tab2 should display - \(selectedTab.description)")
}, label: {
ZStack {
circle
label
}
})
Text("Tab2 should display - \(selectedTab.description)")
}
}
}
var circle: some View {
Circle()
.frame(width: 100, height: 100)
.foregroundColor(.red)
}
var label: some View {
Text("\(tabID == 1 ? ">>" : "<<")").font(.title).foregroundColor(.black)
}
}
With the new @Observable macro, it looks like every time the struct of a view is reinitialized, any observable class marked as @State in the struct also gets reinitialized. Moreover, the result of the reinitialization immediately gets discarded.
This is in contrast to @StateObject and ObservableObject, where the class would only be initialized at the first creation of the view. The initialization method of the class would never be called again between view updates.
Is this a bug or an expected behavior? This redundant reinitialization causes performance issues when the init method of the observable class does anything slightly heavyweight.
Feedback ID: FB13697724
So any time I create a class that's both @Observable and Codable, e.g.
@Observable class GameLocationManager : Codable {
I get a warning in the macro expansion code:
@ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten.
I've been ignoring them for now, but there are at least a half a dozen of them now in my (relatively small) codebase, and I'd like to find a solution (ideally one that doesn't require me to write init(decoder:) for every @Observable class in my project...), especially since I'm not sure what the actual consequences of ignoring this might be.
Hello, I can't seem to set any breakpoint in didSet for all properties inside Observable.
Is this a bug?
XcodeVersion 15.2
(15C500b)
Thanks!
According to docs, .focusedObject() usage should be moved to .focusedValue() when migrating to @Observable, but there is no .focusedSceneValue() overload that accepts Observable like with .focusedValue(). So how are we supposed migrate .focusedSceneObject() to @Observable?
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
When I update a variable inside my model that is marked @Transient, my view does not update with this change. Is this normal? If I update a non-transient variable inside the model at the same time that I update the transient one, then both changes are propagated to my view.
Here is an example of the model:
@Model public class WaterData {
public var target: Double = 3000
@Transient public var samples: [HKQuantitySample] = []
}
Updating samples only does not propagate to my view.
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!