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.
Observation
RSS for tagMake responsive apps that update the presentation when underlying data changes.
Posts under Observation tag
55 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
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?
in the SampleTrips, for the pure SwiftData version, when I add a living accommodation from the trip detail view, the trip detail view won’t update until I go back to the trip list view and enter again.
why didn’t the trip detail view update?
I have tested the pure CoreData version in sample code, it worked well.
is this a bug or a feature of SwiftData?
At the moment, using Bindable for an object stored in Environment works in a cumbersome way:
struct ContentView: View {
@Environment(Model.self) var model
var body: some View {
@Bindable var model = model
VStack {
Text(model.someField.uppercased())
TextField("", text: $model.someField)
someSubView
}
.padding()
}
@ViewBuilder
var someSubView: some View {
@Bindable var model = model
TextField("", text: $model.someField)
}
}
A new @Bindable needs to be instantiated for each computed property in the view, which creates boilerplate I would like to avoid.
I made a new property wrapper which functions the same as the EnvironmentObject wrapper, but for Observable:
@propertyWrapper
struct EnvironmentObservable<Value: AnyObject & Observable>: DynamicProperty {
@Environment var wrappedValue: Value
public init(_ objectType: Value.Type) {
_wrappedValue = .init(objectType)
}
public init() {
_wrappedValue = .init(Value.self)
}
private var store: Bindable<Value>!
var projectedValue: Bindable<Value> {
store
}
mutating func update() {
store = Bindable(wrappedValue)
}
}
Example:
struct ContentView: View {
@EnvironmentObservable var model: Model
var body: some View {
VStack {
Text(model.someField.uppercased())
SubView(value: $model.someField)
someSubView
}
.padding()
}
var someSubView: some View {
TextField("", text: $model.someField)
}
}
I was wondering if there would be any downsides to using this method? In my testings it seems to behave the same, but I'm not sure if using this could have a performance impact.
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.
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?
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 get this error while migrating from ObservableObject to @Observable.
Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context
My original code:
struct SomeView: View {
@StateObject private var viewModel = ViewModel()
}
After migration:
@MainActor @Observable class BaseViewModel {
}
@MainActor class ViewModel: BaseViewModel {
}
struct SomeView: View {
@State private var viewModel = ViewModel()
}
As discussed here. It seems like @StateObject is adding @MainActor compliance to my View under the hood because it's wrappedValue and projectedValue properties are marked as @MainActor, while on @State they are not.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@frozen @propertyWrapper public struct StateObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
...
@MainActor public var wrappedValue: ObjectType { get }
....
@MainActor public var projectedValue: ObservedObject<ObjectType>.Wrapper { get }
}
One solution for this is to mark my View explicitly as @MainActor struct ViewModel: View but it have it side effects, for example code like:
Button(action: resendButtonAction) {
Text(resendButtonAttributedTitle())
}
Will result a warning
Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'
While could be easily solved by using instead
Button(action: { resendButtonAction() } ) {
Text(resendButtonAttributedTitle())
}
I still feel like marking the whole View explicitly as @MainActor is not a good practice.
Adding fake @StateObject property to my view also do the trick, but it's a hack (the same for @FetchRequest).
Can anyone think of a more robust solution for this?
I've just started tinkering with @Observable and have run into a question... What is the best practice for observing an Observable object outside of SwiftUI? For example:
@Observable
class CountingService {
public var count: Int = 0
}
@Observable
class ObservableViewModel {
public var service: CountingService
init(service: CountingService) {
self.service = service
// how to bind to value changes on service?
}
// suggestion I've seen that doesn't smell right
func checkCount() {
_ = withObservationTracking {
service.count
} onChange: {
DispatchQueue.main.async {
print("count: \(service.count)")
checkCount()
}
}
}
}
Historically using ObservableObject I'd have used Combine to monitor changes to service. That doesn't seem possible with @Observable and I don't know that I've come across an accepted / elegant solution? Perhaps there isn't one? There's no particular reason that CountingService has to be @Observable -- it's just nice and clean.
Any suggestions would be appreciated!
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.
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.
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?
Hello everyone, I hope you are well. I have a question about .environment. I have an observable viewModel which has some functions and publishing value. I'm observing this viewModel in only 2 views but I'm using viewModel functions in every view. Should I use it (.environment). if I should use it, should I use this environment macro (@Environment(ViewModel.Self) var vm) for only functions in view? Thank you so much.
I am targeting iOS17 and using @Observable.
I have an array of items, and I want to present a sheet when the array is greater than zero.
My @Observable looks like this:
@Observable class BStoreOO {
var items: [Int] = []
var showBS: Bool { items.isEmpty }
func updateItems(newItems: [Int]) {
self.items = newItems
}
}
When this array gets added to, from another view, I would like to present a sheet/popover.
Inside my ContentView I have a @State `... that uses that @Observable
struct ContentView: View {
@State var bStore = BStoreOO()
Further down in my View, how should I toggle this view on the basis of the observed array not being empty? I have tried a number of ways. Some error, some don't, but none present the sheet!
For example:
.popover(isPresented: bStore.showBS) {
PopoverContent()
}
Gives the error "Cannot convert value of type 'Bool' to expected argument type 'Binding'"
If I add a State like this: @State private var isShowingBS = false
and then add this:
.onChange(of: bStore.items) {
self.isShowingBS = self.bStore.items.count > 0
}
I don't get errors but nothing is presented.
What is the correct way to bind the presentation of the sheet to whether the observed items array is empty or not?
Is it possible to do something with @Observable class to make it constantly monitored and updatable?
Using SwiftUI, the timecode (seconds notation) has been referenced using ObservableObject as follows. In this case, the timecode values were reflected in Text in real time without any problem.
struct ContentView: View {
.
.
.
var body: some View {
NavigationStack {
// Time Code Text
Text(String(format:"%02d:%02d:%02d", sMinute, sSecond, sMsec))
.font(Font(UIFont.monospacedDigitSystemFont(ofSize: 30.0, weight: .regular)))
class ViewModel: ObservableObject {
// Time Code "%02d:%02d:%02d"
@Published var sMinute = 0
@Published var sSecond = 0
@Published var sMsec = 0
When I changed it to @Observable class as follows, the timecode operation is stopped and the values are updated only when the operation is finished.
@Observable class ViewModel {
// Time Code "%02d:%02d:%02d"
var sMinute = 0
var sSecond = 0
var sMsec = 0
Is it possible to do something with the @Observable class that would allow it to be constantly monitored and updated in real time?
Or should we change it back?
If we have a history of changing to @Observable in relation to other functions, and we have no choice but to revert back, is this where we need to make a change that would keep it functional separately?
Using SwiftData I have one relatively short list of @Model class objects that are referenced in several views. I thought it would be handy to query once and share via the environment but my Table in other views does not update when data changes as it does when I repeat the Query in each view. Is this a bad idea or perhaps I implemented it improperly?
@Observable
class AllAccounts: ObservableObject {
var accounts: [Account] = []
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// an instance of the AllAccounts class, will save Query results to all.accounts
@StateObject var all = AllAccounts()
// one query for all accounts once in app, store in the environment`
@Query private var allAccounts: [Account]
.onAppear {
all.accounts = allAccounts
}
.environment(all)
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!
Hey there i try to adept the Observable macro for my SwiftUI Project.
I wanna get data from HealthKit and transform it to FHIR.
Here i need one class for managing this process which will be shared through my project in order to access these data.
The following Codesnippet raises an error:
import Observation
import HealthKit
import FHIR
@Observable
class HealthKitManager{
let healthStore = HKHealthStore()
init(){
}
func requestAuthorization(){
let authTypes: Set = [HKQuantityType(.stepCount),
HKQuantityType(.heartRate)
]
guard HKHealthStore.isHealthDataAvailable() else {
print("health data not available!")
return
}
healthStore.requestAuthorization(toShare: nil, read: authTypes) {
success, error in
if success {
print("OK Fetching")
} else {
print("\(String(describing: error))")
}
}
}
}
So the FHIR module contains a Models module which define different models.
In the FHIR world there is also a concept called observation... now the problems begin.
open class Observation : Models.DomainResource {
...
}
When i try import Observation for the Observable macro and also import FHIR in order to create Observations, XCode rises an error: Observable' is not a member type of class 'Models.Observation'
It's also said that Observation is declared in the FHIR.Models.Observation class which is true, but it seems that this raises the Problem with the @Observable macro.
It's also said that Type 'Observation' has no member 'ObservationRegistrar'
Which comes from the Observable Macro i think.
When i don't import FHIR then everything works fine, but then my use case is broken because i need these standard in order to get my use case running.
All in all i need big help and be thankfull for possible solutions!
I am using the Observable macro and when I use @Environment property wrapper to instance my model the preview stop working. Sample code below
my model Library
import SwiftUI
import Observation
@Observable class Library {
// ...
}
Now in my main app I created an instance of Library and add that instance to the environment
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(library)
}
}
}
Now if I want to retrieve the Library instance from any view using the @Environment property wrapper the preview stop working completely
struct LibraryView: View {
@Environment(Library.self) private var library
var body: some View {
List(library.books) { book in
BookView(book: book)
}
}
}
#Preview {
LibraryView()
}
Check the 2 screenshots below
Any idea why this is happening? Is there any workaround? I am working with Xcode Version 15.2. Thanks in advance for any kind of help!