Skip to content

Commit 18bf4aa

Browse files
Fix iOS StateObservable data race when updating cached state. (#38)
1 parent 89b8a71 commit 18bf4aa

3 files changed

Lines changed: 31 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## Upcoming
88

9+
### Fixed
10+
- iOS `StateObservable` no longer races on its cached state: the flow/state-flow collection now mutates `lastState` on the main actor instead of the background collection context, preventing a crash (`objc_opt_isKindOfClass` via SKIE `onEnum`) when the value is read during SwiftUI body evaluation
11+
912
### Updates
1013

1114
### Breaking Changes

viewmodel/ios/NullableStateObservable.swift

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,7 @@ public class NullableStateObservable<State>: ObservableObject {
3232

3333
task = Task { [weak self] in
3434
for await newState in stateFlow.dropFirst() {
35-
if let animation = self?.animation?(self?.lastState, newState) {
36-
DispatchQueue.main.async {
37-
withAnimation(animation) {
38-
self?.objectWillChange.send()
39-
}
40-
}
41-
} else {
42-
DispatchQueue.main.async {
43-
self?.objectWillChange.send()
44-
}
45-
}
46-
self?.lastState = newState
35+
await self?.apply(newState)
4736
}
4837
}
4938
}
@@ -55,22 +44,23 @@ public class NullableStateObservable<State>: ObservableObject {
5544

5645
task = Task { [weak self] in
5746
for await newState in flow {
58-
if let animation = self?.animation?(self?.lastState, newState) {
59-
DispatchQueue.main.async {
60-
withAnimation(animation) {
61-
self?.objectWillChange.send()
62-
}
63-
}
64-
} else {
65-
DispatchQueue.main.async {
66-
self?.objectWillChange.send()
67-
}
68-
}
69-
self?.lastState = newState
47+
await self?.apply(newState)
7048
}
7149
}
7250
}
7351

52+
@MainActor
53+
private func apply(_ newState: State?) {
54+
if let animation = animation?(lastState, newState) {
55+
withAnimation(animation) {
56+
objectWillChange.send()
57+
}
58+
} else {
59+
objectWillChange.send()
60+
}
61+
lastState = newState
62+
}
63+
7464
deinit {
7565
task?.cancel()
7666
}

viewmodel/ios/StateObservable.swift

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,7 @@ public class StateObservable<State>: ObservableObject {
3232

3333
task = Task { [weak self] in
3434
for await newState in stateFlow.dropFirst() {
35-
if let lastState = self?.lastState, let animation = self?.animation?(lastState, newState) {
36-
DispatchQueue.main.async {
37-
withAnimation(animation) {
38-
self?.objectWillChange.send()
39-
}
40-
}
41-
} else {
42-
DispatchQueue.main.async {
43-
self?.objectWillChange.send()
44-
}
45-
}
46-
self?.lastState = newState
35+
await self?.apply(newState)
4736
}
4837
}
4938
}
@@ -55,22 +44,23 @@ public class StateObservable<State>: ObservableObject {
5544

5645
task = Task { [weak self] in
5746
for await newState in flow {
58-
if let lastState = self?.lastState, let animation = self?.animation?(lastState, newState) {
59-
DispatchQueue.main.async {
60-
withAnimation(animation) {
61-
self?.objectWillChange.send()
62-
}
63-
}
64-
} else {
65-
DispatchQueue.main.async {
66-
self?.objectWillChange.send()
67-
}
68-
}
69-
self?.lastState = newState
47+
await self?.apply(newState)
7048
}
7149
}
7250
}
7351

52+
@MainActor
53+
private func apply(_ newState: State) {
54+
if let animation = animation?(lastState, newState) {
55+
withAnimation(animation) {
56+
objectWillChange.send()
57+
}
58+
} else {
59+
objectWillChange.send()
60+
}
61+
lastState = newState
62+
}
63+
7464
deinit {
7565
task?.cancel()
7666
}

0 commit comments

Comments
 (0)