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?
Observation
RSS for tagMake responsive apps that update the presentation when underlying data changes.
Posts under Observation tag
44 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
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)
}
}
How can one observe changes in the SwiftData DB?
I'm aware that this is possible via Queries, but I need to fetch the data in background, so a query is not an option.
I'm aware of the ModelContext.didSave / .willSave notifications, but these don't work with iOS 17.
-> How can I observe changes to models of a certain type? I don't want to observe the whole database.
Hey everyone,
I’m relatively new to SwiftUI and iOS development, having started earlier this year, and I’m working on a Notes app using SwiftData. I’ve run into some issues when dealing with nested views.
In my app, I’m using a SwiftData @Query in a parent view and then passing the model objects down the view tree. However, I’m frequently encountering errors such as “No container found” or similar. To work around this, I’ve tried having the child views perform their own SwiftData @Query, with the parent view passing a string identifier to the child views. This approach has helped somewhat, but I’m still not entirely sure how to properly manage UI updates.
Additionally, I’ve noticed that turning on iCloud syncing seems to have made the problem worse. The errors have become more frequent, and it’s unclear to me how to handle these situations effectively.
Could someone explain the best practices for handling SwiftData queries in nested SwiftUI views, especially with iCloud syncing enabled? Any guidance or feedback would be greatly appreciated.
Thank you!
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.
DESCRIPTION OF PROBLEM
I have changed my app to the @Observable-Macro.
When using an iPhone (on simulator and on real device) the navigation from a player to the player detail view and back breaks. In the attached video on my GitHub you can see me tapping on both players in the team, but the navigation ist not showing the detail view.
What is the reason? Is my usage/understanding of @Observable wrong? Is it wrong to have the selectedPlayer within the PlayerController which is @Observable? And why does it sometimes work and sometimes not?
The project can be found here: GitHub Project
STEPS TO REPRODUCE
Start the App, add one or two demo teams, tap on a team and add two or more demo players.
tap a player and then go back, tap the player again and back again. After a while (number of taps is always different), the navigation breaks. See my video attached.
PLATFORM AND VERSION
iOS
Development environment: Xcode 15.4, macOS 14.5 (23F79)
Run-time configuration: iOS 17.5,
Hey,
I have a setup in my app that I am currently building, that uses @Observable router objects that hold the app's entire navigation state, so that I can easily set it globally and let SwiftUI show the appropriate views accordingly. Each view gets passed in such a router object and there is a global "app" router that the app's root view can access as an entry point:
@Observable @MainActor
final class AppRouter {
static let shared = AppRouter() // Entry point used by the app's root view
var isShowingSheet = false // Navigation state
let sheetRouter = SheetRouter() // Router that's passed to the sheet view. This router could contain other routers for sheets it will show, and so on
}
@Observable @MainActor
final class SheetRouter { // Example of a "nested" router for a sheet view
var path = NavigationPath()
var isShowingOtherSheet = false
func reset() {
path = .init()
isShowingOtherSheet = false
}
}
To open a sheet, I have a button like this:
@Bindable var appRouter = AppRouter.shared
// ...
Button("Present") {
appRouter.sheetRouter.reset() // Reset sheet router
appRouter.isShowingSheet = true // show sheet
}
This seems to work perfectly fine. However, this produces tons of "error" logs in the console, whenever I open the sheet for a second time:
Mutating observable property \SheetRouter.path after view is torn down has no effect.
Mutating observable property \SheetRouter.path after view is torn down has no effect.
Mutating observable property \SheetRouter.path after view is torn down has no effect.
Mutating observable property \SheetRouter.path after view is torn down has no effect.
Mutating observable property \SheetRouter.isShowingOtherSheet after view is torn down has no effect.
These errors appear when calling the reset() of the sheet view's router before opening the sheet. That method simply resets all navigation states in a router back to their defaults. It's as if the sheetRouter is still connected to the dismissed view from the previous sheet, causing a mutation to trigger these error logs.
Am I misusing SwiftUI here or is that a bug? It's also worth mentioning that these error logs do not appear on iOS 17. Only on iOS 18. So it might be a bug but I just want to make sure my usage of these router objects is okay and not a misuse of the SwiftUI API that the runtime previously simply did not catch/notice. Then I'd have to rewrite my entire navigation logic.
I do have an example project to demonstrate the issue. You can find it here: https://github.com/SwiftedMind/PresentationBugDemo.
I have also filed a feedback for this: FB14162780
STEPS TO REPRODUCE
Open the example project.
Open the sheet in the ContentView twice by tapping "Present"
Close that sheet
Open it again.
Then the console will show these error logs from above.
I'd appreciate any help with this.
Cheers
Hello,
I have a SwiftUI view with the following state variable:
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
@State private var client: Client? = nil
@State private var project: Project? = nil
@State private var service: Service? = nil
@State private var billable: Bool = false
Client, Project, and Service are all SwiftData models. I have some view content that binds to these values, including Pickers for the client/project/service and a DatePicker for the Dates.
I have an onAppear listener:
.onAppear {
switch state.mode {
case .editing(let tt):
Task {
await MainActor.run {
startDate = tt.startDate
endDate = tt.endDate
client = tt.client
project = tt.project
service = tt.service
billable = tt.billable
}
}
default:
return
}
}
This works as expected. However, if I remove the Task & MainActor.run, the values do not fully update. The DatePickers show the current date, the Pickers show a new value but tapping on them shows a nil default value.
What is also extremely strange is that if tt.billable is true, then the view does update as expected.
I am using Xcode 15.4 on iOS simulator 17.5. Any help would be appreciated.
import SwiftUI
import SwiftData
class DateManagerStore : ObservableObject {
@Query private var myData: [myData]
@Published var myDataToString = ""
func hopitalDataQuery() {
if let lastMyData = myData {
self.myDataToString = String(lastMyData.sorted(by: {$0.visitedDate > $1.visitedDate}).last)
}
}
}
struct MainView: View {
@EnvironmentObject var dateManagerStore : DateManagerStore
var body: some View {
VStack{
Text("\(dateManagerStore.myDataToString)")
}
.onAppear(perform: {
dateManagerStore.hopitalDataQuery()
})
}
}
I thought it would be good to manage SwiftData values used within multiple views in one place.
I wanted to use Query data in the DateManagerStore class declared as ObservableObject through onApper of the MainView.
However, when printing the myData variable within hopitalDataQuery() of the DateManagerStore class, empty data was output.
I tried to use @Query defined inside the DateManagerStore class in various ways, but none of the methods allowed me to put a value into the @Query variable 'myData'.
There is no error in Xcode itself, but no data is coming in.
I can't find any related information anywhere, so I ask if it's officially not possible.
I am exploring on managing state in SwiftUI app with purpose built Views due to the advantages for managing dependency with Environment.
This is the minimal example I came up with:
@MainActor
struct AsyncStateModifier<T: Equatable>: View {
let input: T
let action: (T) async -> Void
@Environment var queue: AsyncActionQueue
var body: some View {
return EmptyView()
.onChange(of: input, initial: true) { old, new in
queue.process(action: action, with: input)
}
}
}
The drawback of this approach is initial: true allows the onChange callback to fire when view appears and since EmptyView doesn't appear the action is never executed initially.
When replacing EmptyView with Rectangle().hidden() this can be achieved, but I wanted to avoid having any impact on view hierarchy and EmptyView is suitable for that. Is there any alternative approach to make something like this possible?
I am little confused about when to use State / StateObject / ObservedObject.
What I have researched and what I understand:
@State --> for value types
@StateObject --> for reference types
@ObservedObject --> child objects who needs reference to above two (the parent object should have @State/@StateObject and the object should conform to Observable)
I am clear about Environment object.
i'm having trouble modifying an optional environment object.
i'm using the .environment modifier to pass along an optional object to other views. to access it in other views, i have to get it through an @Environment property wrapper. but i can't modify it even if i redeclare it in the body as @Bindable. here's an example code:
@main
struct MyApp: App {
@State private var mySession: MySession?
var body: some Scene {
HomeScreen()
.environment(mySession)
}
}
now for the HomeScreen:
struct HomeScreen: View {
@Environment(MySession.self) private var mySession: MySession?
var body: some View {
@Bindable var mySession = mySession
Button {
mySession = MySession()
} label: {
Text("Create Session")
}
}
}
an error shows up in the @Bindable declaration saying init(wrappedValue:)' is unavailable: The wrapped value must be an object that conforms to Observable. but MySession is declared as @Observable. in fact it works just fine if i don't make the environment optional, but i have to setup MySession in the root of the app, which goes against the app flow.
If I annotate a class with @Observable I get this error in @Query:
Expansion of macro 'Query()' produced an unexpected 'init' accessor
If I remove @Observable the error goes away.
Elsewhere I have .environment referencing the class. With @Observable this complains that the class needs to be @Observable.
I am mystified. Does anyone have a suggestion?
Originally asked on Swift Forums: https://forums.swift.org/t/using-bindable-with-a-observable-type/70993
I'm using SwiftUI environments in my app to hold a preferences object which is an @Observable object
But I want to be able to inject different instances of the preferences object for previews vs the production code so I've abstracted my production object in to a Preferences protocol and updated my Environment key's type to:
protocol Preferences { }
@Observable
final class MyPreferencesObject: Preferences { }
@Observable
final class MyPreviewsObject: Preferences { }
// Environment key
private struct PreferencesKey: EnvironmentKey {
static let defaultValue : Preferences & Observable = MyPreferencesObject()
}
extension EnvironmentValues {
var preferences: Preferences & Observable {
get { self[PreferencesKey.self] }
set { self[PreferencesKey.self] = newValue }
}
}
The compiler is happy with this until I go to use @Bindable in my code where the compiler explodes with a generic error,
eg:
@Environment(\.preferences) private var preferences
// ... code
@Bindable var preferences = preferences
If I change the environment object back to a conforming type eg:
@Observable
final class MyPreferencesObject() { }
private struct PreferencesKey: EnvironmentKey {
static let defaultValue : MyPreferencesObject = MyPreferencesObject()
}
extension EnvironmentValues {
var preferences: MyPreferencesObject {
get { self[PreferencesKey.self] }
set { self[PreferencesKey.self] = newValue }
}
}
Then @Bindable is happy again and things compile.
Is this a known issue/limitation? Or am I missing something here?