I’m trying to create a property wrapper that that can manage shared state across any context, which can get notified if changes happen from somewhere else.
I'm using mutex, and getting and setting values works great. However, I can't find a way to create an observer pattern that the property wrappers can use.
The problem is that I can’t trigger a notification from a different thread/context, and have that notification get called on the correct thread of the parent object that the property wrapper is used within.
I would like the property wrapper to work from anywhere: a SwiftUI view, an actor, or from a class that is created in the background. The notification preferably would get called synchronously if triggered from the same thread or actor, or otherwise asynchronously. I don’t have to worry about race conditions from the notification because the state only needs to reach eventuall consistency.
Here's the simplified pseudo code of what I'm trying to accomplish:
// A single source of truth storage container.
final class MemoryShared<Value>: Sendable {
let state = Mutex<Value>(0)
func withLock(_ action: (inout Value) -> Void) {
state.withLock(action)
notifyObservers()
}
func get() -> Value
func notifyObservers()
func addObserver()
}
// Some shared state used across the app
static let globalCount = MemoryShared<Int>(0)
// A property wrapper to access the shared state and receive changes
@propertyWrapper
struct SharedState<Value> {
public var wrappedValue: T {
get { state.get() }
nonmutating set { // Can't set directly }
}
var publisher: Publisher {}
init(state: MemoryShared) {
// ...
}
}
// I'd like to use it in multiple places:
@Observable
class MyObservable {
@SharedState(globalCount)
var count: Int
}
actor MyBackgroundActor {
@SharedState(globalCount)
var count: Int
}
@MainActor
struct MyView: View {
@SharedState(globalCount)
var count: Int
}
What I’ve Tried
All of the examples below are using the property wrapper within a @MainActor class. However the same issue happens no matter what context I use the wrapper in: The notification callback is never called on the context the property wrapper was created with.
I’ve tried using @isolated(any) to capture the context of the wrapper and save it to be called within the state in with unchecked sendable, which doesn’t work:
final class MemoryShared<Value: Sendable>: Sendable {
// Stores the callback for later.
public func subscribe(callback: @escaping @isolated(any) (Value) -> Void) -> Subscription
}
@propertyWrapper
struct SharedState<Value> {
init(state: MemoryShared<Value>) {
MainActor.assertIsolated() // Works!
state.subscribe {
MainActor.assertIsolated() // Fails
self.publisher.send()
}
}
}
I’ve tried capturing the isolation within a task with AsyncStream. This actually compiles with no sendable issues, but still fails:
@propertyWrapper
struct SharedState<Value> {
init(isolation: isolated (any Actor)? = #isolation, state: MemoryShared<Value>) {
let (taskStream, continuation) = AsyncStream<Value>.makeStream()
// The shared state sends new values to the continuation.
subscription = state.subscribe(continuation: continuation)
MainActor.assertIsolated() // Works!
let task = Task {
_ = isolation
for await value in taskStream {
_ = isolation
MainActor.assertIsolated() // Fails
}
}
}
}
I’ve tried using multiple combine subjects and publishers:
final class MemoryShared<Value: Sendable>: Sendable {
let subject: PassthroughSubject<T, Never> // ...
var publisher: Publisher {} // ...
}
@propertyWrapper
final class SharedState<Value> {
var localSubject: Subject
init(state: MemoryShared<Value>) {
MainActor.assertIsolated() // Works!
handle = localSubject.sink {
MainActor.assertIsolated() // Fails
}
stateHandle = state.publisher.subscribe(localSubject)
}
}
I’ve also tried:
Using NotificationCenter
Making the property wrapper a class
Using NSKeyValueObserving
Using a box class that is stored within the wrapper.
Using @_inheritActorContext.
All of these don’t work, because the event is never called from the thread the property wrapper resides in.
Is it possible at all to create an observation system that notifies the observer from the same context as where the observer was created?
Any help would be greatly appreciated!
Observation
RSS for tagMake responsive apps that update the presentation when underlying data changes.
Posts under Observation tag
28 Posts
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi,
Previously, we would conform model objects to the ObservableObject protocol and use the @StateObject property wrapper when storing them to an owned binding in a View.
Now, if I understand correctly, it is recommended that we use the new @Observable macro/protocol in place of ObservableObject and use the @State property wrapper rather than @StateObject. This is my understanding from documentation articles such as Migrating from the Observable Object protocol to the Observable macro.
However, the StateObject property wrapper has an initialiser which takes an autoclosure parameter:
extension StateObject {
public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
}
This is an extremely important initialiser for state objects that are expensive to allocate. As far as I can tell, the @State property wrapper lacks an equivalent initialiser.
What is the recommended migration strategy for objects which made use of this on StateObject?
Thanks
I've encountered an issue where using @Observable in SwiftUI causes extra initializations and deinitializations when a reference type is included as a property inside a struct. Specifically, when I include a reference type (a simple class Empty {}) inside a struct (Test), DetailsViewModel is initialized and deinitialized twice instead of once. If I remove the reference type, the behavior is correct.
This issue does not occur when using @StateObject instead of @Observable. Additionally, I've submitted a feedback report: FB16631081.
Steps to Reproduce
Run the provided SwiftUI sample code (tested on iOS 18.2 & iOS 18.3 using Xcode 16.2).
Observe the console logs when navigating to DetailsView.
Comment out var empty = Empty() in the Test struct.
Run again and compare console logs.
Change @Observable in DetailsViewModel to @StateObject and observe that the issue no longer occurs.
Expected Behavior
The DetailsViewModel should initialize once and deinitialize once, regardless of whether Test contains a reference type.
Actual Behavior
With var empty = Empty() present, DetailsViewModel initializes and deinitializes twice. However, if the reference type is removed, or when using @StateObject, the behavior is correct (one initialization, one deinitialization).
Code Sample
import SwiftUI
enum Route {
case details
}
@MainActor
@Observable
final class NavigationManager {
var path = NavigationPath()
}
struct ContentView: View {
@State private var navigationManager = NavigationManager()
var body: some View {
NavigationStack(path: $navigationManager.path) {
HomeView()
.environment(navigationManager)
}
}
}
final class Empty { }
struct Test {
var empty = Empty() // Comment this out to make it work
}
struct HomeView: View {
private let test = Test()
@Environment(NavigationManager.self) private var navigationManager
var body: some View {
Form {
Button("Go To Details View") {
navigationManager.path.append(Route.details)
}
}
.navigationTitle("Home View")
.navigationDestination(for: Route.self) { route in
switch route {
case .details:
DetailsView()
.environment(navigationManager)
}
}
}
}
@MainActor
@Observable
final class DetailsViewModel {
var fullScreenItem: Item?
init() {
print("DetailsViewModel Init")
}
deinit {
print("DetailsViewModel Deinit")
}
}
struct Item: Identifiable {
let id = UUID()
let value: Int
}
struct DetailsView: View {
@State private var viewModel = DetailsViewModel()
@Environment(NavigationManager.self) private var navigationManager
var body: some View {
ZStack {
Color.green
Button("Show Full Screen Cover") {
viewModel.fullScreenItem = .init(value: 4)
}
}
.navigationTitle("Details View")
.fullScreenCover(item: $viewModel.fullScreenItem) { item in
NavigationStack {
FullScreenView(item: item)
.navigationTitle("Full Screen Item: \(item.value)")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
withAnimation(completionCriteria: .logicallyComplete) {
viewModel.fullScreenItem = nil
} completion: {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
navigationManager.path.removeLast()
}
}
}
}
}
}
}
}
}
struct FullScreenView: View {
@Environment(\.dismiss) var dismiss
let item: Item
var body: some View {
ZStack {
Color.red
Text("Full Screen View \(item.value)")
.navigationTitle("Full Screen View")
}
}
}
Console Output
With var empty = Empty() in Test
DetailsViewModel Init
DetailsViewModel Init
DetailsViewModel Deinit
DetailsViewModel Deinit
Without var empty = Empty() in Test
DetailsViewModel Init
DetailsViewModel Deinit
Using @StateObject Instead of @Observable
DetailsViewModel Init
DetailsViewModel Deinit
Additional Notes
This issue occurs only when using @Observable. Switching to @StateObject prevents it. This behavior suggests a possible issue with how SwiftUI handles reference-type properties inside structs when using @Observable.
Using a struct-only approach (removing Empty class) avoids the issue, but that’s not always a practical solution.
Questions for Discussion
Is this expected behavior with @Observable?
Could this be an unintended side effect of SwiftUI’s state management?
Are there any recommended workarounds apart from switching to @StateObject?
Would love to hear if anyone else has run into this or if Apple has provided any guidance!
Hi, folks.
I know that in the new observation, class property changes can be automatically notified to SwiftUI, which is very convenient. But in the new observation framework, how to monitor the property changes of different model classes? For example, class1 has an instance of class2, and I need to notify class1 to perform some actions and make some changes when some properties of class2 are changed. How to do it in observation? In the past, I could use combined methods to write the second part of the code for monitoring. However, using the combined framework in observation is a bit confusing. I know this method can be withObservationTracking(_:onChange:) but it needs to be registered continuously.
If Observation is not possible, do I need to change my design structure?
Thanks.
// Observation
@Observable class Sample1 {
var count: Int = 0
var name = "Sample1"
}
@Observable class Sample2 {
var count: Int = 0
var name = "Sample2"
var sample1: Sample1?
init (sample1 : Sample1) {
self.sample1 = sample1
}
func render() {
withObservationTracking {
print("Accessing Sample1.count: \(sample1?.count ?? 0)")
} onChange: { [weak self] in
print("Sample1.count changed! Re-rendering Sample2.")
self?.handleSample1CountChange()
}
}
private func handleSample1CountChange() {
print("Handling count change in Sample2...")
self.count = sample1?.count ?? 0
}
}
// ObservableObject
class Sample1: ObservableObject {
@Published var count: Int = 0
var name = "Sample1"
}
class Sample2: ObservableObject {
@Published var count: Int = 0
var name = "Sample1"
var sample1: Sample1?
private var cancellables = Set<AnyCancellable>()
init (sample1 : Sample1) {
self.sample1 = sample1
setupSubscribers()
}
private func setupSubscribers() {
sample1?.$count
.receive(on: DispatchQueue.main)
.sink { [weak self] count in
guard let self = self else { return }
// Update key theory data
self.count = count
self.doSomeThing()
}
.store(in: &cancellables)
}
private func doSomeThing() {
print("Count changes, need do some thing")
}
}
When I run my app with XCode on my iPhone, and then moved into the background, I'm getting a EXC_BREAKPOINT exception after a few minutes, seemingly when iOS attempts to call my app with a BGAppRefreshTask:
Thread 23 Queue: com.apple.BGTaskScheduler (com.mycompany.MyApp.RefreshTask) (serial)
0 _dispatch_assert_queue_fail
12 _pthread_wqthread
Enqueued from com.apple.duet.activityscheduler.client.xpcqueue (Thread 23)
0 dispatch_async
20 start_wqthread
I can't quite understand the reason from this crash. In the background task, I'm attempting to update live activities. In the process, it might encounter code that calls MainActor and manipulate @Observable objects. Might that be the reason?
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
Swift
Background Tasks
Observation
I must admit my knowledge of swift is limited, and I cannot wrap my head around this problem.
I've defined this protocol, so I can use different auth providers in my app.
protocol AuthRepository {
associatedtype AuthData
associatedtype AuthResponseData
associatedtype RegistrationData
associatedtype RegistrationResponseData
func login(with data: AuthData) async throws -> AuthResponseData?
func register(with data: RegistrationData) async throws -> RegistrationResponseData?
}
and an implementation for my server
struct MyServerAuthData {
let email: String
let password: String
}
struct MyServerAuthResponseData {
let token: String
}
struct MyServerRegistrationData {
let email: String
let password: String
let name: String
}
actor AuthRepositoryImpl: AuthRepository {
func login(with data: MyServerAuthData) async throws -> MyServerAuthResponseData? {
...
}
func register(with data: MyServerRegistrationData) async throws -> Void? {
...
}
}
To use across the app, I've created this ViewModel
@MainActor
final class AuthViewModel<T: AuthRepository>: ObservableObject {
private let repository: T
init(repository: T) {
self.repository = repository
}
func login(data: T.AuthData) async throws -> T.AuthResponseData? {
try await repository.login(with: data)
}
func register(with data: T.RegistrationData) async throws {
try await repository.register(with: data)
}
}
defined in the app as
@main
struct MyApp: App {
@StateObject var authViewModel = AuthViewModel(repository: AuthRepositoryImpl())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.authViewModel)
}
}
}
and consumed as
@EnvironmentObject private var authViewModel: AuthViewModel<AuthRepositoryImpl>
But with this code, the whole concept of having a generic implementation for the auth repository is useless, because changing the AuthRepostory will need to search and replace AuthViewModel<AuthRepositoryImpl> across all the app.
I've experienced this directly creating a MockAuthImpl to use with #Preview, and the preview crashed because it defines AuthViewModel(repository: MockAuthImpl()) but the view expects AuthViewModel.
There is a better way to do that?
Hi
I am developing an app for counting money. One view is for entering the intended destinations specified by a donor. Each destination has a row with a TextField that has an OnChange to keep it all numeric. The model that holds the data is a class called AllocationItems, which I recently changed from having the protocol ObservableObject to having the macro @Observable.
It mostly works the same, except that before with each key stroke the total would be updated, but now with the macro it only gets updated when I exit the current TextField by clicking on another TextField
How do I get it to update the total with each keystroke?
The code that shows the total is this:
Text(allocations.totalAllocated.fmtCurrency)
where allocations is an instance of AllocationItems
with this definition:
var totalAllocated: NSDecimalNumber { items.reduce(.zero) { $0 + $1.amountValue } }
I hope someone knows why this has changed and can suggest a simple fix.
When I switched to observable, I noticed a strange behavior of the ViewModel. The ViewModel is created 3x times. And my question is:
How to properly initialize the ViewModel via state?
Below is a minimal example with log output:
ViewModel INIT : EBBB2C41
ViewModel INIT : D8E490DA
ViewModel INIT : 54407300
ViewModel DEINIT: D8E490DA
@Observable
final class ViewModel {
@ObservationIgnored let idd: UUID
init() {
idd = UUID()
print("ViewModel INIT : \(idd.uuidString.prefix(8))")
}
deinit {
print("ViewModel DEINIT: \(idd.uuidString.prefix(8))")
}
}
struct SimpleView: View {
@Environment(ViewModel.self) private var viewModel
var body: some View {
@Bindable var viewModel = viewModel
Text("SimpleView: \(viewModel.idd.uuidString.prefix(8))")
}
}
struct ContentView: View {
@State private var viewModel = ViewModel()
var body: some View {
SimpleView()
.environment(mainViewModel)
}
}