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
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
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?
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)
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!
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?
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?
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.
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.