diff --git a/Examples/SyncUps/SyncUps/AppFeature.swift b/Examples/SyncUps/SyncUps/AppFeature.swift index 5b57e9de647a..17d9360ef14a 100644 --- a/Examples/SyncUps/SyncUps/AppFeature.swift +++ b/Examples/SyncUps/SyncUps/AppFeature.swift @@ -71,18 +71,13 @@ struct AppView: View { } #Preview { - AppView( - store: Store( - initialState: AppFeature.State( - syncUpsList: SyncUpsList.State( - syncUps: [ - .mock, - .productMock, - .engineeringMock, - ] - ) - ) - ) { + @Shared(.syncUps) var syncUps: IdentifiedArrayOf = [ + .mock, + .productMock, + .engineeringMock + ] + return AppView( + store: Store(initialState: AppFeature.State()) { AppFeature() } ) diff --git a/Examples/SyncUps/SyncUps/Meeting.swift b/Examples/SyncUps/SyncUps/Meeting.swift index c278bfcfbbc2..d6ac16babb62 100644 --- a/Examples/SyncUps/SyncUps/Meeting.swift +++ b/Examples/SyncUps/SyncUps/Meeting.swift @@ -7,19 +7,19 @@ struct MeetingView: View { var body: some View { Form { Section { - ForEach(self.syncUp.attendees) { attendee in + ForEach(syncUp.attendees) { attendee in Text(attendee.name) } } header: { Text("Attendees") } Section { - Text(self.meeting.transcript) + Text(meeting.transcript) } header: { Text("Transcript") } } - .navigationTitle(Text(self.meeting.date, style: .date)) + .navigationTitle(Text(meeting.date, style: .date)) } } diff --git a/Examples/SyncUps/SyncUps/Models.swift b/Examples/SyncUps/SyncUps/Models.swift index a88a7465a87e..dc8bfa60ab13 100644 --- a/Examples/SyncUps/SyncUps/Models.swift +++ b/Examples/SyncUps/SyncUps/Models.swift @@ -11,7 +11,7 @@ struct SyncUp: Equatable, Identifiable, Codable { var title = "" var durationPerAttendee: Duration { - self.duration / self.attendees.count + duration / attendees.count } } @@ -56,9 +56,9 @@ enum Theme: String, CaseIterable, Equatable, Identifiable, Codable { } } - var mainColor: Color { Color(self.rawValue) } + var mainColor: Color { Color(rawValue) } - var name: String { self.rawValue.capitalized } + var name: String { rawValue.capitalized } } extension SyncUp { diff --git a/Examples/SyncUps/SyncUps/RecordMeeting.swift b/Examples/SyncUps/SyncUps/RecordMeeting.swift index 312afc16dd0f..c2765744ef4d 100644 --- a/Examples/SyncUps/SyncUps/RecordMeeting.swift +++ b/Examples/SyncUps/SyncUps/RecordMeeting.swift @@ -13,7 +13,7 @@ struct RecordMeeting { var transcript = "" var durationRemaining: Duration { - self.syncUp.duration - .seconds(self.secondsElapsed) + syncUp.duration - .seconds(secondsElapsed) } } @@ -41,11 +41,11 @@ struct RecordMeeting { Reduce { state, action in switch action { case .alert(.presented(.confirmDiscard)): - return .run { _ in await self.dismiss() } + return .run { _ in await dismiss() } case .alert(.presented(.confirmSave)): state.syncUp.insert(transcript: state.transcript) - return .run { _ in await self.dismiss() } + return .run { _ in await dismiss() } case .alert: return .none @@ -68,18 +68,18 @@ struct RecordMeeting { case .onTask: return .run { send in let authorization = - await self.speechClient.authorizationStatus() == .notDetermined - ? self.speechClient.requestAuthorization() - : self.speechClient.authorizationStatus() + await speechClient.authorizationStatus() == .notDetermined + ? speechClient.requestAuthorization() + : speechClient.authorizationStatus() await withTaskGroup(of: Void.self) { group in if authorization == .authorized { group.addTask { - await self.startSpeechRecognition(send: send) + await startSpeechRecognition(send: send) } } group.addTask { - await self.startTimer(send: send) + await startTimer(send: send) } } } @@ -94,7 +94,7 @@ struct RecordMeeting { if state.secondsElapsed.isMultiple(of: secondsPerAttendee) { if state.secondsElapsed == state.syncUp.duration.components.seconds { state.syncUp.insert(transcript: state.transcript) - return .run { _ in await self.dismiss() } + return .run { _ in await dismiss() } } state.speakerIndex += 1 } @@ -118,7 +118,7 @@ struct RecordMeeting { private func startSpeechRecognition(send: Send) async { do { - let speechTask = await self.speechClient.startTask(SFSpeechAudioBufferRecognitionRequest()) + let speechTask = await speechClient.startTask(SFSpeechAudioBufferRecognitionRequest()) for try await result in speechTask { await send(.speechResult(result)) } @@ -128,7 +128,7 @@ struct RecordMeeting { } private func startTimer(send: Send) async { - for await _ in self.clock.timer(interval: .seconds(1)) { + for await _ in clock.timer(interval: .seconds(1)) { await send(.timerTick) } } @@ -138,7 +138,7 @@ extension SyncUp { fileprivate mutating func insert(transcript: String) { @Dependency(\.date.now) var now @Dependency(\.uuid) var uuid - self.meetings.insert( + meetings.insert( Meeting( id: Meeting.ID(uuid()), date: now, @@ -239,14 +239,14 @@ struct MeetingHeaderView: View { var body: some View { VStack { - ProgressView(value: self.progress) - .progressViewStyle(MeetingProgressViewStyle(theme: self.theme)) + ProgressView(value: progress) + .progressViewStyle(MeetingProgressViewStyle(theme: theme)) HStack { VStack(alignment: .leading) { Text("Time Elapsed") .font(.caption) Label( - Duration.seconds(self.secondsElapsed).formatted(.units()), + Duration.seconds(secondsElapsed).formatted(.units()), systemImage: "hourglass.bottomhalf.fill" ) } @@ -254,7 +254,7 @@ struct MeetingHeaderView: View { VStack(alignment: .trailing) { Text("Time Remaining") .font(.caption) - Label(self.durationRemaining.formatted(.units()), systemImage: "hourglass.tophalf.fill") + Label(durationRemaining.formatted(.units()), systemImage: "hourglass.tophalf.fill") .font(.body.monospacedDigit()) .labelStyle(.trailingIcon) } @@ -264,12 +264,12 @@ struct MeetingHeaderView: View { } private var totalDuration: Duration { - .seconds(self.secondsElapsed) + self.durationRemaining + .seconds(secondsElapsed) + durationRemaining } private var progress: Double { - guard self.totalDuration > .seconds(0) else { return 0 } - return Double(self.secondsElapsed) / Double(self.totalDuration.components.seconds) + guard totalDuration > .seconds(0) else { return 0 } + return Double(secondsElapsed) / Double(totalDuration.components.seconds) } } @@ -279,11 +279,11 @@ struct MeetingProgressViewStyle: ProgressViewStyle { func makeBody(configuration: Configuration) -> some View { ZStack { RoundedRectangle(cornerRadius: 10) - .fill(self.theme.accentColor) + .fill(theme.accentColor) .frame(height: 20) ProgressView(configuration) - .tint(self.theme.mainColor) + .tint(theme.mainColor) .frame(height: 12) .padding(.horizontal) } @@ -300,8 +300,8 @@ struct MeetingTimerView: View { .overlay { VStack { Group { - if self.speakerIndex < self.syncUp.attendees.count { - Text(self.syncUp.attendees[self.speakerIndex].name) + if speakerIndex < syncUp.attendees.count { + Text(syncUp.attendees[speakerIndex].name) } else { Text("Someone") } @@ -312,14 +312,14 @@ struct MeetingTimerView: View { .font(.largeTitle) .padding(.top) } - .foregroundStyle(self.syncUp.theme.accentColor) + .foregroundStyle(syncUp.theme.accentColor) } .overlay { - ForEach(Array(self.syncUp.attendees.enumerated()), id: \.element.id) { index, attendee in - if index < self.speakerIndex + 1 { - SpeakerArc(totalSpeakers: self.syncUp.attendees.count, speakerIndex: index) + ForEach(Array(syncUp.attendees.enumerated()), id: \.element.id) { index, attendee in + if index < speakerIndex + 1 { + SpeakerArc(totalSpeakers: syncUp.attendees.count, speakerIndex: index) .rotation(Angle(degrees: -90)) - .stroke(self.syncUp.theme.mainColor, lineWidth: 12) + .stroke(syncUp.theme.mainColor, lineWidth: 12) } } } @@ -339,21 +339,21 @@ struct SpeakerArc: Shape { path.addArc( center: center, radius: radius, - startAngle: self.startAngle, - endAngle: self.endAngle, + startAngle: startAngle, + endAngle: endAngle, clockwise: false ) } } private var degreesPerSpeaker: Double { - 360 / Double(self.totalSpeakers) + 360 / Double(totalSpeakers) } private var startAngle: Angle { - Angle(degrees: self.degreesPerSpeaker * Double(self.speakerIndex) + 1) + Angle(degrees: degreesPerSpeaker * Double(speakerIndex) + 1) } private var endAngle: Angle { - Angle(degrees: self.startAngle.degrees + self.degreesPerSpeaker - 1) + Angle(degrees: startAngle.degrees + degreesPerSpeaker - 1) } } @@ -365,13 +365,13 @@ struct MeetingFooterView: View { var body: some View { VStack { HStack { - if self.speakerIndex < self.syncUp.attendees.count - 1 { - Text("Speaker \(self.speakerIndex + 1) of \(self.syncUp.attendees.count)") + if speakerIndex < syncUp.attendees.count - 1 { + Text("Speaker \(speakerIndex + 1) of \(syncUp.attendees.count)") } else { Text("No more speakers.") } Spacer() - Button(action: self.nextButtonTapped) { + Button(action: nextButtonTapped) { Image(systemName: "forward.fill") } } diff --git a/Examples/SyncUps/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUps/SyncUpDetail.swift index 35de892a7ce5..48397c31be5f 100644 --- a/Examples/SyncUps/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUps/SyncUpDetail.swift @@ -65,13 +65,13 @@ struct SyncUpDetail { case .confirmDeletion: @Shared(.syncUps) var syncUps: IdentifiedArrayOf = [] syncUps.remove(id: state.syncUp.id) - return .run { _ in await self.dismiss() } + return .run { _ in await dismiss() } case .continueWithoutRecording: return .send(.delegate(.startMeeting)) case .openSettings: - return .run { _ in await self.openSettings() } + return .run { _ in await openSettings() } } case .destination: @@ -89,7 +89,7 @@ struct SyncUpDetail { return .none case .startMeetingButtonTapped: - switch self.authorizationStatus() { + switch authorizationStatus() { case .notDetermined, .authorized: return .send(.delegate(.startMeeting)) diff --git a/Examples/SyncUps/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUps/SyncUpForm.swift index 8a09b0a10e68..4e5e25e8d2ab 100644 --- a/Examples/SyncUps/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUps/SyncUpForm.swift @@ -37,7 +37,7 @@ struct SyncUpForm { Reduce { state, action in switch action { case .addAttendeeButtonTapped: - let attendee = Attendee(id: Attendee.ID(self.uuid())) + let attendee = Attendee(id: Attendee.ID(uuid())) state.syncUp.attendees.append(attendee) state.focus = .attendee(attendee.id) return .none @@ -48,7 +48,7 @@ struct SyncUpForm { case let .deleteAttendees(atOffsets: indices): state.syncUp.attendees.remove(atOffsets: indices) if state.syncUp.attendees.isEmpty { - state.syncUp.attendees.append(Attendee(id: Attendee.ID(self.uuid()))) + state.syncUp.attendees.append(Attendee(id: Attendee.ID(uuid()))) } guard let firstIndex = indices.first else { return .none } @@ -104,7 +104,7 @@ struct ThemePicker: View { @Binding var selection: Theme var body: some View { - Picker("Theme", selection: self.$selection) { + Picker("Theme", selection: $selection) { ForEach(Theme.allCases) { theme in ZStack { RoundedRectangle(cornerRadius: 4) @@ -122,7 +122,7 @@ struct ThemePicker: View { extension Duration { fileprivate var minutes: Double { - get { Double(self.components.seconds / 60) } + get { Double(components.seconds / 60) } set { self = .seconds(newValue * 60) } } } diff --git a/Examples/SyncUps/SyncUps/SyncUpsList.swift b/Examples/SyncUps/SyncUps/SyncUpsList.swift index ae72ec826be9..2bf5895d4253 100644 --- a/Examples/SyncUps/SyncUps/SyncUpsList.swift +++ b/Examples/SyncUps/SyncUps/SyncUpsList.swift @@ -36,7 +36,7 @@ struct SyncUpsList { case .addSyncUpButtonTapped: state.destination = .add( SyncUpForm.State( - syncUp: SyncUp(id: SyncUp.ID(self.uuid())) + syncUp: SyncUp(id: SyncUp.ID(uuid())) ) ) return .none @@ -51,7 +51,7 @@ struct SyncUpsList { if syncUp.attendees.isEmpty { syncUp.attendees.append( editState.syncUp.attendees.first - ?? Attendee(id: Attendee.ID(self.uuid())) + ?? Attendee(id: Attendee.ID(uuid())) ) } state.syncUps.append(syncUp) @@ -123,19 +123,19 @@ struct CardView: View { var body: some View { VStack(alignment: .leading) { - Text(self.syncUp.title) + Text(syncUp.title) .font(.headline) Spacer() HStack { - Label("\(self.syncUp.attendees.count)", systemImage: "person.3") + Label("\(syncUp.attendees.count)", systemImage: "person.3") Spacer() - Label(self.syncUp.duration.formatted(.units()), systemImage: "clock") + Label(syncUp.duration.formatted(.units()), systemImage: "clock") .labelStyle(.trailingIcon) } .font(.caption) } .padding() - .foregroundColor(self.syncUp.theme.accentColor) + .foregroundColor(syncUp.theme.accentColor) } } @@ -152,23 +152,18 @@ extension LabelStyle where Self == TrailingIconLabelStyle { static var trailingIcon: Self { Self() } } -struct SyncUpsList_Previews: PreviewProvider { - static var previews: some View { - NavigationStack { - SyncUpsListView( - store: Store( - initialState: SyncUpsList.State( - syncUps: [ - .mock, - .productMock, - .engineeringMock, - ] - ) - ) { - SyncUpsList() - } - ) - } +#Preview("List") { + @Shared(.syncUps) var syncUps: IdentifiedArrayOf = [ + .mock, + .productMock, + .engineeringMock + ] + return NavigationStack { + SyncUpsListView( + store: Store(initialState: SyncUpsList.State()) { + SyncUpsList() + } + ) } } diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift index a8b4de4ae229..53574d3492dc 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/DebugReducer.swift @@ -80,24 +80,20 @@ public struct _PrintChangesReducer: Reducer { ) -> Effect { #if DEBUG if let printer = self.printer { - return withSharedChangeTracking { + return withSharedChangeTracking { changeTracker in let oldState = state let effects = self.base.reduce(into: &state, action: action) - @Dependency(SharedChangeTrackerKey.self) var changeTracker - guard let changeTracker - else { return effects } return withEscapedDependencies { continuation in effects.merge( with: .publisher { [newState = state, queue = printer.queue] in Deferred> { queue.async { continuation.yield { - let wasAsserting = changeTracker.isAsserting - changeTracker.isAsserting = true - defer { changeTracker.isAsserting = wasAsserting } - printer.printChange( - receivedAction: action, oldState: oldState, newState: newState - ) + changeTracker.assert { + printer.printChange( + receivedAction: action, oldState: oldState, newState: newState + ) + } } } return Empty() diff --git a/Sources/ComposableArchitecture/SharedState/Shared.swift b/Sources/ComposableArchitecture/SharedState/Shared.swift index a1ebaf6ef8da..b4248830543c 100644 --- a/Sources/ComposableArchitecture/SharedState/Shared.swift +++ b/Sources/ComposableArchitecture/SharedState/Shared.swift @@ -19,7 +19,7 @@ public struct Shared { public var wrappedValue: Value { get { @Dependency(SharedChangeTrackerKey.self) var changeTracker - if changeTracker?.isAsserting == true { + if changeTracker != nil { return self.snapshot ?? self.currentValue } else { return self.currentValue @@ -27,10 +27,17 @@ public struct Shared { } nonmutating set { @Dependency(SharedChangeTrackerKey.self) var changeTracker - if changeTracker?.isAsserting == true { + if changeTracker != nil { self.snapshot = newValue } else { - changeTracker?.track(self.reference) + @Dependency(SharedChangeTrackersKey.self) + var changeTrackers: LockIsolated> + + changeTrackers.withValue { changeTrackers in + for changeTracker in changeTrackers { + changeTracker.track(self.reference) + } + } self.currentValue = newValue } } @@ -119,28 +126,25 @@ public struct Shared { file: StaticString = #file, line: UInt = #line ) rethrows where Value: Equatable { - @Dependency(SharedChangeTrackerKey.self) var changeTracker - guard let changeTracker + @Dependency(SharedChangeTrackersKey.self) var changeTrackers + guard + let changeTracker = changeTrackers.value + .first(where: { $0.changes[ObjectIdentifier(self.reference)] != nil }) else { - XCTFail( - "Use 'withSharedChangeTracking' to track changes to assert against.", - file: file, - line: line - ) - return - } - let wasAsserting = changeTracker.isAsserting - changeTracker.isAsserting = true - defer { changeTracker.isAsserting = wasAsserting } - guard var snapshot = self.snapshot, snapshot != self.currentValue else { XCTFail("Expected changes, but none occurred.", file: file, line: line) return } - try updateValueToExpectedResult(&snapshot) - self.snapshot = snapshot - // TODO: Finesse error more than `XCTAssertNoDifference` - XCTAssertNoDifference(self.currentValue, self.snapshot, file: file, line: line) - self.snapshot = nil + try changeTracker.assert { + guard var snapshot = self.snapshot, snapshot != self.currentValue else { + XCTFail("Expected changes, but none occurred.", file: file, line: line) + return + } + try updateValueToExpectedResult(&snapshot) + self.snapshot = snapshot + // TODO: Finesse error more than `XCTAssertNoDifference` + XCTAssertNoDifference(self.currentValue, self.snapshot, file: file, line: line) + self.snapshot = nil + } } private var currentValue: Value { @@ -196,7 +200,7 @@ extension Shared: @unchecked Sendable where Value: Sendable {} extension Shared: Equatable where Value: Equatable { public static func == (lhs: Shared, rhs: Shared) -> Bool { @Dependency(SharedChangeTrackerKey.self) var changeTracker - if changeTracker?.isAsserting == true, lhs.reference === rhs.reference { + if changeTracker != nil, lhs.reference === rhs.reference { if let lhsReference = lhs.reference as? any Equatable { func open(_ lhsReference: T) -> Bool { lhsReference == rhs.reference as? T diff --git a/Sources/ComposableArchitecture/SharedState/SharedChangeTracking.swift b/Sources/ComposableArchitecture/SharedState/SharedChangeTracking.swift index e64a494a0b51..88122e0341d1 100644 --- a/Sources/ComposableArchitecture/SharedState/SharedChangeTracking.swift +++ b/Sources/ComposableArchitecture/SharedState/SharedChangeTracking.swift @@ -1,25 +1,21 @@ import CustomDump import Dependencies -public func withSharedChangeTracking( - _ apply: () throws -> T +func withSharedChangeTracking( + _ apply: (SharedChangeTracker) throws -> T ) rethrows -> T { - try withDependencies { - $0[SharedChangeTrackerKey.self] = $0[SharedChangeTrackerKey.self]?.copy() - ?? SharedChangeTracker() - } operation: { - try apply() + let changeTracker = SharedChangeTracker() + return try changeTracker.track { + try apply(changeTracker) } } -public func withSharedChangeTracking( - _ apply: () async throws -> T +func withSharedChangeTracking( + _ apply: (SharedChangeTracker) async throws -> T ) async rethrows -> T { - try await withDependencies { - $0[SharedChangeTrackerKey.self] = $0[SharedChangeTrackerKey.self]?.copy() - ?? SharedChangeTracker() - } operation: { - try await apply() + let changeTracker = SharedChangeTracker() + return try await changeTracker.track { + try await apply(changeTracker) } } @@ -58,23 +54,22 @@ struct AnyChange: Change { } } -@_spi(Internals) public final class SharedChangeTracker { - var changes: [ObjectIdentifier: Any] = [:] - @_spi(Internals) public var isAsserting = false - @_spi(Internals) public var hasChanges: Bool { !self.changes.isEmpty } - @_spi(Internals) public init() {} - @_spi(Internals) public func resetChanges() { self.changes.removeAll() } - @_spi(Internals) public func assertUnchanged() { +final class SharedChangeTracker: Sendable { + let changes: LockIsolated<[ObjectIdentifier: Any]> = LockIsolated([:]) + var hasChanges: Bool { !self.changes.isEmpty } + init() {} + func resetChanges() { self.changes.withValue { $0.removeAll() } } + func assertUnchanged() { for change in self.changes.values { if let change = change as? any Change { change.assertUnchanged() } } - self.changes.removeAll() + self.resetChanges() } func track(_ reference: some Reference) { if !self.changes.keys.contains(ObjectIdentifier(reference)) { - self.changes[ObjectIdentifier(reference)] = AnyChange(reference) + self.changes.withValue { $0[ObjectIdentifier(reference)] = AnyChange(reference) } } } subscript(_ reference: some Reference) -> AnyChange? { @@ -82,17 +77,49 @@ struct AnyChange: Change { _modify { var change = self.changes[ObjectIdentifier(reference)] as? AnyChange yield &change - self.changes[ObjectIdentifier(reference)] = change + self.changes.withValue { [change] in $0[ObjectIdentifier(reference)] = change } + } + } + func track(_ operation: () throws -> R) rethrows -> R { + @Dependency(SharedChangeTrackersKey.self) + var sharedChangeTrackers: LockIsolated> + sharedChangeTrackers.withValue { _ = $0.insert(self) } + defer { sharedChangeTrackers.withValue { _ = $0.remove(self) } } + return try operation() + } + func track(_ operation: () async throws -> R) async rethrows -> R { + @Dependency(SharedChangeTrackersKey.self) + var sharedChangeTrackers: LockIsolated> + sharedChangeTrackers.withValue { _ = $0.insert(self) } + defer { sharedChangeTrackers.withValue { _ = $0.remove(self) } } + return try await operation() + } + func assert(_ operation: () throws -> R) rethrows -> R { + try withDependencies { + $0[SharedChangeTrackerKey.self] = self + } operation: { + try operation() } } - func copy() -> SharedChangeTracker { - let changeTracker = SharedChangeTracker() - changeTracker.changes = self.changes - return changeTracker +} + +extension SharedChangeTracker: Hashable { + static func == (lhs: SharedChangeTracker, rhs: SharedChangeTracker) -> Bool { + lhs === rhs + } + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +enum SharedChangeTrackersKey: DependencyKey { + static var liveValue: LockIsolated> { LockIsolated([]) } + static var testValue: LockIsolated> { + LockIsolated([SharedChangeTracker()]) } } -@_spi(Internals) public enum SharedChangeTrackerKey: DependencyKey { - @_spi(Internals) public static var liveValue: SharedChangeTracker? { nil } - @_spi(Internals) public static var testValue: SharedChangeTracker? { SharedChangeTracker() } +enum SharedChangeTrackerKey: DependencyKey { + static var liveValue: SharedChangeTracker? { nil } + static var testValue: SharedChangeTracker? { nil } } diff --git a/Sources/ComposableArchitecture/TestStore.swift b/Sources/ComposableArchitecture/TestStore.swift index 6258643666d8..9d072b6bd02a 100644 --- a/Sources/ComposableArchitecture/TestStore.swift +++ b/Sources/ComposableArchitecture/TestStore.swift @@ -490,6 +490,11 @@ public final class TestStore { private let file: StaticString private var line: UInt let reducer: TestReducer + private let sharedChangeTracker = { + @Dependency(SharedChangeTrackersKey.self) + var sharedChangeTrackers: LockIsolated> + return sharedChangeTrackers.first! + }() private let store: Store.TestAction> /// Creates a test store with an initial state and a reducer powering its runtime. @@ -658,7 +663,7 @@ public final class TestStore { line: effect.action.line ) } - self.dependencies[SharedChangeTrackerKey.self]?.assertUnchanged() + self.sharedChangeTracker.assertUnchanged() } /// Overrides the store's dependencies for a given operation. @@ -957,19 +962,12 @@ extension TestStore where State: Equatable { file: StaticString, line: UInt ) throws { - let changeTracker = self.reducer.dependencies[SharedChangeTrackerKey.self] - try Dependencies.withDependencies { - $0[SharedChangeTrackerKey.self] = changeTracker - } operation: { - let wasAsserting = changeTracker?.isAsserting - changeTracker?.isAsserting = true - defer { changeTracker?.isAsserting = wasAsserting ?? false } - + try self.sharedChangeTracker.assert { let skipUnnecessaryModifyFailure = skipUnnecessaryModifyFailure - || changeTracker?.hasChanges == true + || self.sharedChangeTracker.hasChanges == true if self.exhaustivity != .on { - changeTracker?.resetChanges() + self.sharedChangeTracker.resetChanges() } let current = expected @@ -996,6 +994,7 @@ extension TestStore where State: Equatable { if let updateStateToExpectedResult { try Dependencies.withDependencies { $0 = self.reducer.dependencies + $0[SharedChangeTrackerKey.self] = self.sharedChangeTracker } operation: { try updateStateToExpectedResult(&expectedWhenGivenPreviousState) } @@ -1013,6 +1012,7 @@ extension TestStore where State: Equatable { if let updateStateToExpectedResult { try Dependencies.withDependencies { $0 = self.reducer.dependencies + $0[SharedChangeTrackerKey.self] = self.sharedChangeTracker } operation: { try updateStateToExpectedResult(&expectedWhenGivenActualState) } @@ -1032,6 +1032,7 @@ extension TestStore where State: Equatable { do { try Dependencies.withDependencies { $0 = self.reducer.dependencies + $0[SharedChangeTrackerKey.self] = self.sharedChangeTracker } operation: { try updateStateToExpectedResult(&expectedWhenGivenPreviousState) } @@ -1104,7 +1105,7 @@ extension TestStore where State: Equatable { line: line ) } - self.reducer.dependencies[SharedChangeTrackerKey.self]?.resetChanges() + self.sharedChangeTracker.resetChanges() } } }