Skip to content

Commit bb34f69

Browse files
authored
Require main actor isolation in store collection (#3333)
* Require main actor isolation in store collection * `preconditionIsolated` is not available in iOS <14 * Update Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift
1 parent 5e3f420 commit bb34f69

File tree

1 file changed

+33
-14
lines changed

1 file changed

+33
-14
lines changed

Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift

+33-14
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@
9090
private let store: Store<IdentifiedArray<ID, State>, IdentifiedAction<ID, Action>>
9191
private let data: IdentifiedArray<ID, State>
9292

93+
#if swift(<5.10)
94+
@MainActor(unsafe)
95+
#else
96+
@preconcurrency@MainActor
97+
#endif
9398
fileprivate init(_ store: Store<IdentifiedArray<ID, State>, IdentifiedAction<ID, Action>>) {
9499
self.store = store
95100
self.data = store.withState { $0 }
@@ -98,21 +103,35 @@
98103
public var startIndex: Int { self.data.startIndex }
99104
public var endIndex: Int { self.data.endIndex }
100105
public subscript(position: Int) -> Store<State, Action> {
101-
guard self.data.indices.contains(position)
102-
else {
103-
return Store()
104-
}
105-
let id = self.data.ids[position]
106-
var element = self.data[position]
107-
return self.store.scope(
108-
id: self.store.id(state: \.[id:id]!, action: \.[id:id]),
109-
state: ToState {
110-
element = $0[id: id] ?? element
111-
return element
112-
},
113-
action: { .element(id: id, action: $0) },
114-
isInvalid: { !$0.ids.contains(id) }
106+
precondition(
107+
Thread.isMainThread,
108+
#"""
109+
Store collections must be interacted with on the main actor.
110+
111+
When passing a scoped store to a 'ForEach' in a lazy view (for example, 'LazyVStack'), it \
112+
must be eagerly transformed into a collection to avoid access off the main actor:
113+
114+
Array(store.scope(state: \.elements, action: \.elements))
115+
"""#
115116
)
117+
return MainActor._assumeIsolated { [uncheckedSelf = UncheckedSendable(self)] in
118+
let `self` = uncheckedSelf.wrappedValue
119+
guard self.data.indices.contains(position)
120+
else {
121+
return Store()
122+
}
123+
let id = self.data.ids[position]
124+
var element = self.data[position]
125+
return self.store.scope(
126+
id: self.store.id(state: \.[id:id]!, action: \.[id:id]),
127+
state: ToState {
128+
element = $0[id: id] ?? element
129+
return element
130+
},
131+
action: { .element(id: id, action: $0) },
132+
isInvalid: { !$0.ids.contains(id) }
133+
)
134+
}
116135
}
117136
}
118137
#endif

0 commit comments

Comments
 (0)