Skip to content

Commit da205c7

Browse files
authored
Deprecate Store.scope(state:) for view store observe (#2097)
* Deprecate `Store.scope(state:)` for view store `observe` Explicit scoping is most appropriate for transforming domains, which almost always requires an action transform. In the rare case it doesn't, we should prefer an explicit `{ $0 }`. Scoping for the view has been deprecated for awhile for the `observe` parameter when creating view stores, so let's lead folks that direction. * wip * wip
1 parent 89e4f0c commit da205c7

29 files changed

+120
-94
lines changed

Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ struct AlertAndConfirmationDialogView: View {
112112
}
113113
.navigationTitle("Alerts & Dialogs")
114114
.alert(
115-
self.store.scope(state: \.alert),
115+
self.store.scope(state: \.alert, action: { $0 }),
116116
dismiss: .alertDismissed
117117
)
118118
.confirmationDialog(
119-
self.store.scope(state: \.confirmationDialog),
119+
self.store.scope(state: \.confirmationDialog, action: { $0 }),
120120
dismiss: .confirmationDialogDismissed
121121
)
122122
}

Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ struct AnimationsView: View {
139139
Button("Reset") { viewStore.send(.resetButtonTapped) }
140140
.padding([.horizontal, .bottom])
141141
}
142-
.alert(self.store.scope(state: \.alert), dismiss: .alertDismissed)
142+
.alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed)
143143
.navigationBarTitleDisplayMode(.inline)
144144
}
145145
}

Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ struct SharedStateCounterView: View {
218218
}
219219
.padding(.top)
220220
.navigationTitle("Shared State Demo")
221-
.alert(self.store.scope(state: \.alert), dismiss: .alertDismissed)
221+
.alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed)
222222
}
223223
}
224224
}

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ struct WebSocketView: View {
180180
Text("Received messages")
181181
}
182182
}
183-
.alert(self.store.scope(state: \.alert), dismiss: .alertDismissed)
183+
.alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed)
184184
.navigationTitle("Web Socket")
185185
}
186186
}

Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ struct FavoriteButton<ID: Hashable & Sendable>: View {
7777
Image(systemName: "heart")
7878
.symbolVariant(viewStore.isFavorite ? .fill : .none)
7979
}
80-
.alert(self.store.scope(state: \.alert), dismiss: .alertDismissed)
80+
.alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed)
8181
}
8282
}
8383
}

Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ struct SpeechRecognitionView: View {
141141
}
142142
}
143143
.padding()
144-
.alert(self.store.scope(state: \.alert), dismiss: .authorizationStateAlertDismissed)
144+
.alert(
145+
self.store.scope(state: \.alert, action: { $0 }),
146+
dismiss: .authorizationStateAlertDismissed
147+
)
145148
}
146149
}
147150
}

Examples/TicTacToe/tic-tac-toe/Sources/GameUIKit/GameViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public final class GameViewController: UIViewController {
2929

3030
public init(store: StoreOf<Game>) {
3131
self.store = store
32-
self.viewStore = ViewStore(store.scope(state: ViewState.init))
32+
self.viewStore = ViewStore(store, observe: ViewState.init)
3333
super.init(nibName: nil, bundle: nil)
3434
}
3535

Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public struct LoginView: View {
9595
.disabled(viewStore.isLoginButtonDisabled)
9696
}
9797
.disabled(viewStore.isFormDisabled)
98-
.alert(self.store.scope(state: \.alert), dismiss: .alertDismissed)
98+
.alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed)
9999
}
100100
.navigationTitle("Login")
101101
}

Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class LoginViewController: UIViewController {
4040

4141
public init(store: StoreOf<Login>) {
4242
self.store = store
43-
self.viewStore = ViewStore(store.scope(state: ViewState.init, action: Login.Action.init))
43+
self.viewStore = ViewStore(store, observe: ViewState.init, send: Login.Action.init)
4444
super.init(nibName: nil, bundle: nil)
4545
}
4646

Examples/TicTacToe/tic-tac-toe/Sources/NewGameUIKit/NewGameViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public class NewGameViewController: UIViewController {
3333

3434
public init(store: StoreOf<NewGame>) {
3535
self.store = store
36-
self.viewStore = ViewStore(store.scope(state: ViewState.init, action: NewGame.Action.init))
36+
self.viewStore = ViewStore(store, observe: ViewState.init, send: NewGame.Action.init)
3737
super.init(nibName: nil, bundle: nil)
3838
}
3939

Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public struct TwoFactorView: View {
6666
}
6767
}
6868
}
69-
.alert(self.store.scope(state: \.alert), dismiss: .alertDismissed)
69+
.alert(self.store.scope(state: \.alert, action: { $0 }), dismiss: .alertDismissed)
7070
.disabled(viewStore.isFormDisabled)
7171
.navigationTitle("Confirmation Code")
7272
}

Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorUIKit/TwoFactorViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public final class TwoFactorViewController: UIViewController {
3030

3131
public init(store: StoreOf<TwoFactor>) {
3232
self.store = store
33-
self.viewStore = ViewStore(store.scope(state: ViewState.init, action: TwoFactor.Action.init))
33+
self.viewStore = ViewStore(store, observe: ViewState.init, send: TwoFactor.Action.init)
3434
super.init(nibName: nil, bundle: nil)
3535
}
3636

Examples/Todos/Todos/Todos.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ struct AppView: View {
111111

112112
init(store: StoreOf<Todos>) {
113113
self.store = store
114-
self.viewStore = ViewStore(self.store.scope(state: ViewState.init(state:)))
114+
self.viewStore = ViewStore(self.store, observe: ViewState.init(state:))
115115
}
116116

117117
struct ViewState: Equatable {

Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ struct VoiceMemosView: View {
165165
.background(Color.init(white: 0.95))
166166
}
167167
.alert(
168-
self.store.scope(state: \.alert),
168+
self.store.scope(state: \.alert, action: { $0 }),
169169
dismiss: .alertDismissed
170170
)
171171
.navigationTitle("Voice memos")

Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ struct AppView: View {
184184
TabView {
185185
ActivityView(
186186
store: self.store
187-
.scope(state: \.activity, action: AppAction.activity
187+
.scope(state: \.activity, action: AppAction.activity)
188188
)
189189
.badge("\(viewStore.unreadActivityCount)")
190190

Sources/ComposableArchitecture/Internal/Deprecations.swift

+18-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ import XCTestDynamicOverlay
55

66
// MARK: - Deprecated after 0.52.0
77

8+
extension Store {
9+
@available(
10+
*,
11+
deprecated,
12+
message: """
13+
'Store.scope' requires an explicit 'action' transform and is intended to be used to transform a store of a parent domain into a store of a child domain.
14+
15+
When transforming store state into view state, use the 'observe' parameter when constructing a view store.
16+
"""
17+
)
18+
public func scope<ChildState>(
19+
state toChildState: @escaping (State) -> ChildState
20+
) -> Store<ChildState, Action> {
21+
self.scope(state: toChildState, action: { $0 })
22+
}
23+
}
24+
825
extension EffectPublisher {
926
@available(
1027
*,
@@ -856,7 +873,7 @@ extension ForEachStore {
856873
{
857874
let data = store.state.value
858875
self.data = data
859-
self.content = WithViewStore(store.scope(state: { $0.map { $0[keyPath: id] } })) { viewStore in
876+
self.content = WithViewStore(store, observe: { $0.map { $0[keyPath: id] } }) { viewStore in
860877
ForEach(Array(viewStore.state.enumerated()), id: \.element) { index, _ in
861878
content(
862879
store.scope(

Sources/ComposableArchitecture/Store.swift

+1-13
Original file line numberDiff line numberDiff line change
@@ -316,18 +316,6 @@ public final class Store<State, Action> {
316316
#endif
317317
}
318318

319-
/// Scopes the store to one that exposes child state.
320-
///
321-
/// A version of ``scope(state:action:)`` that leaves the action type unchanged.
322-
///
323-
/// - Parameter toChildState: A function that transforms `State` into `ChildState`.
324-
/// - Returns: A new store with its domain (state and action) transformed.
325-
public func scope<ChildState>(
326-
state toChildState: @escaping (State) -> ChildState
327-
) -> Store<ChildState, Action> {
328-
self.scope(state: toChildState, action: { $0 })
329-
}
330-
331319
func filter(
332320
_ isSent: @escaping (State, Action) -> Bool
333321
) -> Store<State, Action> {
@@ -485,7 +473,7 @@ public final class Store<State, Action> {
485473

486474
/// Returns a "stateless" store by erasing state to `Void`.
487475
public var stateless: Store<Void, Action> {
488-
self.scope(state: { _ in () })
476+
self.scope(state: { _ in () }, action: { $0 })
489477
}
490478

491479
/// Returns an "actionless" store by erasing action to `Never`.

Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift

+15-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SwiftUI
99
///
1010
/// ```swift
1111
/// IfLetStore(
12-
/// store.scope(state: \SearchState.results, action: SearchAction.results),
12+
/// store.scope(state: \SearchState.results, action: SearchAction.results)
1313
/// ) {
1414
/// SearchResultsView(store: $0)
1515
/// } else: {
@@ -59,10 +59,13 @@ public struct IfLetStore<State, Action, Content: View>: View {
5959
first: ifContent(
6060
store
6161
.filter { state, _ in state == nil ? !BindingLocal.isActive : true }
62-
.scope {
63-
state = $0 ?? state
64-
return state
65-
}
62+
.scope(
63+
state: {
64+
state = $0 ?? state
65+
return state
66+
},
67+
action: { $0 }
68+
)
6669
)
6770
)
6871
} else {
@@ -88,10 +91,13 @@ public struct IfLetStore<State, Action, Content: View>: View {
8891
return ifContent(
8992
store
9093
.filter { state, _ in state == nil ? !BindingLocal.isActive : true }
91-
.scope {
92-
state = $0 ?? state
93-
return state
94-
}
94+
.scope(
95+
state: {
96+
state = $0 ?? state
97+
return state
98+
},
99+
action: { $0 }
100+
)
95101
)
96102
} else {
97103
return nil

Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ public struct WithViewStore<ViewState, ViewAction, Content: View>: View {
339339
line: UInt = #line
340340
) {
341341
self.init(
342-
store: store.scope(state: toViewState),
342+
store: store.scope(state: toViewState, action: { $0 }),
343343
removeDuplicates: isDuplicate,
344344
content: content,
345345
file: file,
@@ -591,7 +591,7 @@ extension WithViewStore where ViewState: Equatable, Content: View {
591591
line: UInt = #line
592592
) {
593593
self.init(
594-
store: store.scope(state: toViewState),
594+
store: store.scope(state: toViewState, action: { $0 }),
595595
removeDuplicates: ==,
596596
content: content,
597597
file: file,

Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift

+7-4
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,13 @@ extension Store {
5454
.sink { state in
5555
if var state = state {
5656
unwrap(
57-
self.scope {
58-
state = $0 ?? state
59-
return state
60-
}
57+
self.scope(
58+
state: {
59+
state = $0 ?? state
60+
return state
61+
},
62+
action: { $0 }
63+
)
6164
)
6265
} else {
6366
`else`()

Tests/ComposableArchitectureTests/EffectDebounceTests.swift

+2-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ final class EffectDebounceTests: BaseTCATestCase {
1010
let mainQueue = DispatchQueue.test
1111
var values: [Int] = []
1212

13-
// NB: Explicit @MainActor is needed for Swift 5.5.2
14-
@MainActor func runDebouncedEffect(value: Int) {
13+
func runDebouncedEffect(value: Int) {
1514
struct CancelToken: Hashable {}
1615
Just(value)
1716
.eraseToEffect()
@@ -57,8 +56,7 @@ final class EffectDebounceTests: BaseTCATestCase {
5756
var values: [Int] = []
5857
var effectRuns = 0
5958

60-
// NB: Explicit @MainActor is needed for Swift 5.5.2
61-
@MainActor func runDebouncedEffect(value: Int) {
59+
func runDebouncedEffect(value: Int) {
6260
struct CancelToken: Hashable {}
6361

6462
Deferred { () -> Just<Int> in

Tests/ComposableArchitectureTests/EffectRunTests.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final class EffectRunTests: BaseTCATestCase {
3030
return .run { _ in
3131
struct Failure: Error {}
3232
throw Failure()
33-
} catch: { @Sendable _, send in // NB: Explicit '@Sendable' required in 5.5.2
33+
} catch: { _, send in
3434
await send(.response)
3535
}
3636
case .response:
@@ -109,7 +109,7 @@ final class EffectRunTests: BaseTCATestCase {
109109
Task.cancel(id: CancelID.responseA)
110110
try Task.checkCancellation()
111111
await send(.responseA)
112-
} catch: { @Sendable _, send in // NB: Explicit '@Sendable' required in 5.5.2
112+
} catch: { _, send in
113113
await send(.responseB)
114114
}
115115
.cancellable(id: CancelID.responseA)

Tests/ComposableArchitectureTests/EffectTaskTests.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class EffectTaskTests: BaseTCATestCase {
2929
return .task {
3030
struct Failure: Error {}
3131
throw Failure()
32-
} catch: { @Sendable _ in // NB: Explicit '@Sendable' required in 5.5.2
32+
} catch: { _ in
3333
.response
3434
}
3535
case .response:
@@ -106,7 +106,7 @@ final class EffectTaskTests: BaseTCATestCase {
106106
Task.cancel(id: CancelID.responseA)
107107
try Task.checkCancellation()
108108
return .responseA
109-
} catch: { @Sendable _ in // NB: Explicit '@Sendable' required in 5.5.2
109+
} catch: { _ in
110110
.responseB
111111
}
112112
.cancellable(id: CancelID.responseA)

Tests/ComposableArchitectureTests/EffectThrottleTests.swift

+4-10
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ final class EffectThrottleTests: BaseTCATestCase {
1414
var values: [Int] = []
1515
var effectRuns = 0
1616

17-
// NB: Explicit @MainActor is needed for Swift 5.5.2
18-
@MainActor func runThrottledEffect(value: Int) {
19-
17+
func runThrottledEffect(value: Int) {
2018
Deferred { () -> Just<Int> in
2119
effectRuns += 1
2220
return Just(value)
@@ -71,9 +69,7 @@ final class EffectThrottleTests: BaseTCATestCase {
7169
var values: [Int] = []
7270
var effectRuns = 0
7371

74-
// NB: Explicit @MainActor is needed for Swift 5.5.2
75-
@MainActor func runThrottledEffect(value: Int) {
76-
72+
func runThrottledEffect(value: Int) {
7773
Deferred { () -> Just<Int> in
7874
effectRuns += 1
7975
return Just(value)
@@ -140,8 +136,7 @@ final class EffectThrottleTests: BaseTCATestCase {
140136
var values: [Int] = []
141137
var effectRuns = 0
142138

143-
// NB: Explicit @MainActor is needed for Swift 5.5.2
144-
@MainActor func runThrottledEffect(value: Int) {
139+
func runThrottledEffect(value: Int) {
145140

146141
Deferred { () -> Just<Int> in
147142
effectRuns += 1
@@ -188,8 +183,7 @@ final class EffectThrottleTests: BaseTCATestCase {
188183
var values: [Int] = []
189184
var effectRuns = 0
190185

191-
// NB: Explicit @MainActor is needed for Swift 5.5.2
192-
@MainActor func runThrottledEffect(value: Int) {
186+
func runThrottledEffect(value: Int) {
193187
Deferred { () -> Just<Int> in
194188
effectRuns += 1
195189
return Just(value)

0 commit comments

Comments
 (0)