Skip to content

Commit 413c24c

Browse files
authored
Merge branch 'main' into core
2 parents c3338b5 + a952dde commit 413c24c

File tree

14 files changed

+385
-103
lines changed

14 files changed

+385
-103
lines changed

.github/workflows/ci.yml

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,51 @@ concurrency:
1414
cancel-in-progress: true
1515

1616
jobs:
17+
xcodebuild-latest:
18+
name: xcodebuild (16)
19+
runs-on: macos-15
20+
strategy:
21+
matrix:
22+
command: ['']
23+
platform: [IOS, MACOS]
24+
xcode: ['16.0']
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: Select Xcode ${{ matrix.xcode }}
28+
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
29+
- name: List available devices
30+
run: xcrun simctl list devices available
31+
- name: Cache derived data
32+
uses: actions/cache@v3
33+
with:
34+
path: |
35+
~/.derivedData
36+
key: |
37+
deriveddata-xcodebuild-${{ matrix.platform }}-${{ matrix.xcode }}-${{ matrix.command }}-${{ hashFiles('**/Sources/**/*.swift', '**/Tests/**/*.swift') }}
38+
restore-keys: |
39+
deriveddata-xcodebuild-${{ matrix.platform }}-${{ matrix.xcode }}-${{ matrix.command }}-
40+
- name: Set IgnoreFileSystemDeviceInodeChanges flag
41+
run: defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES
42+
- name: Update mtime for incremental builds
43+
uses: chetan/git-restore-mtime-action@v2
44+
- name: Debug
45+
run: make XCODEBUILD_ARGUMENT="${{ matrix.command }}" CONFIG=Debug PLATFORM="${{ matrix.platform }}" WORKSPACE=.github/package.xcworkspace xcodebuild
46+
1747
xcodebuild:
18-
name: xcodebuild
48+
name: xcodebuild (15)
1949
runs-on: macos-14
2050
strategy:
2151
matrix:
2252
command: [test, '']
2353
platform: [IOS, MAC_CATALYST, MACOS, TVOS, VISIONOS, WATCHOS]
24-
xcode: [15.2, 15.4, '16.0']
54+
xcode: [15.2, 15.4]
2555
exclude:
2656
- {xcode: 15.2, command: test}
2757
- {xcode: 15.4, command: ''}
2858
- {xcode: 15.2, platform: MAC_CATALYST}
2959
- {xcode: 15.2, platform: TVOS}
3060
- {xcode: 15.2, platform: VISIONOS}
3161
- {xcode: 15.2, platform: WATCHOS}
32-
- {xcode: '16.0', command: ''}
33-
- {xcode: '16.0', platform: MAC_CATALYST}
34-
- {xcode: '16.0', platform: TVOS}
35-
- {xcode: '16.0', platform: VISIONOS}
36-
- {xcode: '16.0', platform: WATCHOS}
3762
include:
3863
- {xcode: 15.2, skip_release: 1}
3964
steps:
@@ -80,7 +105,7 @@ jobs:
80105

81106
examples:
82107
name: Examples
83-
runs-on: macos-14
108+
runs-on: macos-15
84109
steps:
85110
- uses: actions/checkout@v4
86111
- name: Cache derived data
@@ -113,7 +138,7 @@ jobs:
113138
run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="Todos" xcodebuild-raw
114139
- name: VoiceMemos
115140
run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="VoiceMemos" xcodebuild-raw
116-
141+
117142
check-macro-compatibility:
118143
name: Check Macro Compatibility
119144
runs-on: macos-latest

Makefile

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS,Watch)
1212
PLATFORM = IOS
1313
DESTINATION = platform="$(PLATFORM_$(PLATFORM))"
1414

15+
PLATFORM_ID = $(shell echo "$(DESTINATION)" | sed -E "s/.+,id=(.+)/\1/")
16+
1517
SCHEME = ComposableArchitecture
1618

1719
WORKSPACE = ComposableArchitecture.xcworkspace
@@ -36,11 +38,17 @@ endif
3638

3739
TEST_RUNNER_CI = $(CI)
3840

39-
xcodebuild:
41+
warm-simulator:
42+
@test "$(PLATFORM_ID)" != "" \
43+
&& xcrun simctl boot $(PLATFORM_ID) \
44+
&& open -a Simulator --args -CurrentDeviceUDID $(PLATFORM_ID) \
45+
|| exit 0
46+
47+
xcodebuild: warm-simulator
4048
$(XCODEBUILD)
4149

4250
# Workaround for debugging Swift Testing tests: https://github.com/cpisciotta/xcbeautify/issues/313
43-
xcodebuild-raw:
51+
xcodebuild-raw: warm-simulator
4452
$(XCODEBUILD_COMMAND)
4553

4654
build-for-library-evolution:
@@ -58,7 +66,7 @@ format:
5866
-not -path '*/.*' -print0 \
5967
| xargs -0 swift format --ignore-unparsable-files --in-place
6068

61-
.PHONY: build-for-library-evolution format xcodebuild
69+
.PHONY: build-for-library-evolution format warm-simulator xcodebuild xcodebuild-raw
6270

6371
define udid_for
6472
$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')

Sources/ComposableArchitecture/Internal/Create.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,23 @@
2121
// THE SOFTWARE.
2222

2323
@preconcurrency import Combine
24-
import Darwin
24+
import Foundation
2525

2626
final class DemandBuffer<S: Subscriber>: @unchecked Sendable {
2727
private var buffer = [S.Input]()
2828
private let subscriber: S
2929
private var completion: Subscribers.Completion<S.Failure>?
3030
private var demandState = Demand()
31-
private let lock: os_unfair_lock_t
31+
private let lock = NSRecursiveLock()
3232

3333
init(subscriber: S) {
3434
self.subscriber = subscriber
35-
self.lock = os_unfair_lock_t.allocate(capacity: 1)
36-
self.lock.initialize(to: os_unfair_lock())
37-
}
38-
39-
deinit {
40-
self.lock.deinitialize(count: 1)
41-
self.lock.deallocate()
4235
}
4336

4437
func buffer(value: S.Input) -> Subscribers.Demand {
38+
lock.lock()
39+
defer { lock.unlock() }
40+
4541
precondition(
4642
self.completion == nil, "How could a completed publisher sent values?! Beats me 🤷‍♂️")
4743

@@ -55,6 +51,9 @@ final class DemandBuffer<S: Subscriber>: @unchecked Sendable {
5551
}
5652

5753
func complete(completion: Subscribers.Completion<S.Failure>) {
54+
lock.lock()
55+
defer { lock.unlock() }
56+
5857
precondition(
5958
self.completion == nil, "Completion have already occurred, which is quite awkward 🥺")
6059

Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ final class CurrentValueRelay<Output>: Publisher, @unchecked Sendable {
3333
}
3434

3535
func send(_ value: Output) {
36-
self.lock.sync {
36+
let subscriptions = self.lock.sync {
3737
self.currentValue = value
38+
return self.subscriptions
3839
}
39-
for subscription in self.lock.sync({ self.subscriptions }) {
40+
for subscription in subscriptions {
4041
subscription.receive(value)
4142
}
4243
}
@@ -79,23 +80,27 @@ extension CurrentValueRelay {
7980
}
8081

8182
func receive(_ value: Output) {
82-
guard let downstream else { return }
83+
self.lock.lock()
84+
85+
guard let downstream else {
86+
self.lock.unlock()
87+
return
88+
}
8389

8490
switch self.demand {
8591
case .unlimited:
92+
self.lock.unlock()
8693
// NB: Adding to unlimited demand has no effect and can be ignored.
8794
_ = downstream.receive(value)
8895

8996
case .none:
90-
self.lock.sync {
91-
self.receivedLastValue = false
92-
}
97+
self.receivedLastValue = false
98+
self.lock.unlock()
9399

94100
default:
95-
self.lock.sync {
96-
self.receivedLastValue = true
97-
self.demand -= 1
98-
}
101+
self.receivedLastValue = true
102+
self.demand -= 1
103+
self.lock.unlock()
99104
let moreDemand = downstream.receive(value)
100105
self.lock.sync {
101106
self.demand += moreDemand
@@ -106,14 +111,18 @@ extension CurrentValueRelay {
106111
func request(_ demand: Subscribers.Demand) {
107112
precondition(demand > 0, "Demand must be greater than zero")
108113

109-
guard let downstream else { return }
110-
111114
self.lock.lock()
115+
116+
guard let downstream else {
117+
self.lock.unlock()
118+
return
119+
}
120+
112121
self.demand += demand
113122

114123
guard
115124
!self.receivedLastValue,
116-
let value = self.upstream?.currentValue
125+
let value = self.upstream?.value
117126
else {
118127
self.lock.unlock()
119128
return

Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,69 @@
2222
public typealias _SendableCaseKeyPath<Root, Value> = CaseKeyPath<Root, Value>
2323
#endif
2424

25-
@_transparent
26-
func sendableKeyPath(
27-
_ keyPath: AnyKeyPath
28-
) -> _SendableAnyKeyPath {
29-
#if compiler(>=6)
30-
unsafeBitCast(keyPath, to: _SendableAnyKeyPath.self)
31-
#else
32-
keyPath
33-
#endif
25+
// NB: Dynamic member lookup does not currently support sendable key paths and even breaks
26+
// autocomplete.
27+
//
28+
// * https://github.com/swiftlang/swift/issues/77035
29+
// * https://github.com/swiftlang/swift/issues/77105
30+
extension _AppendKeyPath {
31+
@_transparent
32+
func unsafeSendable() -> _SendableAnyKeyPath
33+
where Self == AnyKeyPath {
34+
#if compiler(>=6)
35+
unsafeBitCast(self, to: _SendableAnyKeyPath.self)
36+
#else
37+
self
38+
#endif
39+
}
40+
41+
@_transparent
42+
func unsafeSendable<Root>() -> _SendablePartialKeyPath<Root>
43+
where Self == PartialKeyPath<Root> {
44+
#if compiler(>=6)
45+
unsafeBitCast(self, to: _SendablePartialKeyPath<Root>.self)
46+
#else
47+
self
48+
#endif
49+
}
50+
51+
@_transparent
52+
func unsafeSendable<Root, Value>() -> _SendableKeyPath<Root, Value>
53+
where Self == KeyPath<Root, Value> {
54+
#if compiler(>=6)
55+
unsafeBitCast(self, to: _SendableKeyPath<Root, Value>.self)
56+
#else
57+
self
58+
#endif
59+
}
60+
61+
@_transparent
62+
func unsafeSendable<Root, Value>() -> _SendableWritableKeyPath<Root, Value>
63+
where Self == WritableKeyPath<Root, Value> {
64+
#if compiler(>=6)
65+
unsafeBitCast(self, to: _SendableWritableKeyPath<Root, Value>.self)
66+
#else
67+
self
68+
#endif
69+
}
70+
71+
@_transparent
72+
func unsafeSendable<Root, Value>() -> _SendableReferenceWritableKeyPath<Root, Value>
73+
where Self == ReferenceWritableKeyPath<Root, Value> {
74+
#if compiler(>=6)
75+
unsafeBitCast(self, to: _SendableReferenceWritableKeyPath<Root, Value>.self)
76+
#else
77+
self
78+
#endif
79+
}
80+
81+
@_transparent
82+
func unsafeSendable<Root, Value>() -> _SendableCaseKeyPath<Root, Value>
83+
where Self == CaseKeyPath<Root, Value> {
84+
#if compiler(>=6)
85+
unsafeBitCast(self, to: _SendableCaseKeyPath<Root, Value>.self)
86+
#else
87+
self
88+
#endif
89+
}
3490
}

Sources/ComposableArchitecture/Observation/Binding+Observation.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,17 @@ extension BindableAction where State: ObservableState {
159159

160160
extension Store where State: ObservableState, Action: BindableAction, Action.State == State {
161161
public subscript<Value: Equatable & Sendable>(
162-
dynamicMember keyPath: _SendableWritableKeyPath<State, Value>
162+
dynamicMember keyPath: WritableKeyPath<State, Value>
163163
) -> Value {
164164
get { self.state[keyPath: keyPath] }
165165
set {
166166
BindingLocal.$isActive.withValue(true) {
167167
self.send(
168-
.set(keyPath, newValue, isInvalidated: { [weak self] in self?.core.isInvalid ?? true })
168+
.set(
169+
keyPath.unsafeSendable(),
170+
newValue,
171+
isInvalidated: { [weak self] in self?.core.isInvalid ?? true }
172+
)
169173
)
170174
}
171175
}
@@ -199,14 +203,18 @@ where
199203
Action.ViewAction.State == State
200204
{
201205
public subscript<Value: Equatable & Sendable>(
202-
dynamicMember keyPath: _SendableWritableKeyPath<State, Value>
206+
dynamicMember keyPath: WritableKeyPath<State, Value>
203207
) -> Value {
204208
get { self.state[keyPath: keyPath] }
205209
set {
206210
BindingLocal.$isActive.withValue(true) {
207211
self.send(
208212
.view(
209-
.set(keyPath, newValue, isInvalidated: { [weak self] in self?.core.isInvalid ?? true })
213+
.set(
214+
keyPath.unsafeSendable(),
215+
newValue,
216+
isInvalidated: { [weak self] in self?.core.isInvalid ?? true }
217+
)
210218
)
211219
)
212220
}

Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,11 @@ extension PresentationAction: CasePathable {
293293
}
294294

295295
public subscript<AppendedAction>(
296-
dynamicMember keyPath: _SendableCaseKeyPath<Action, AppendedAction>
296+
dynamicMember keyPath: CaseKeyPath<Action, AppendedAction>
297297
) -> AnyCasePath<PresentationAction, AppendedAction>
298298
where Action: CasePathable {
299-
AnyCasePath<PresentationAction, AppendedAction>(
299+
let keyPath = keyPath.unsafeSendable()
300+
return AnyCasePath<PresentationAction, AppendedAction>(
300301
embed: { .presented(keyPath($0)) },
301302
extract: {
302303
guard case let .presented(action) = $0 else { return nil }
@@ -307,10 +308,11 @@ extension PresentationAction: CasePathable {
307308

308309
@_disfavoredOverload
309310
public subscript<AppendedAction>(
310-
dynamicMember keyPath: _SendableCaseKeyPath<Action, AppendedAction>
311+
dynamicMember keyPath: CaseKeyPath<Action, AppendedAction>
311312
) -> AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>
312313
where Action: CasePathable {
313-
AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>(
314+
let keyPath = keyPath.unsafeSendable()
315+
return AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>(
314316
embed: {
315317
switch $0 {
316318
case .dismiss:

0 commit comments

Comments
 (0)