Skip to content

Commit 5dfbd58

Browse files
authored
Merge pull request #20 from trading-point/michael/sync-upstream
Sync changes with the Point-Free TCA repo
2 parents ccc16e1 + 7fd7bea commit 5dfbd58

File tree

6 files changed

+107
-44
lines changed

6 files changed

+107
-44
lines changed

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ private let readMe = """
1515
closures.
1616
1717
The benefit of doing this is that you can get full test coverage on how a user interacts with \
18-
with alerts and action sheets in your application
18+
alerts and action sheets in your application
1919
"""
2020

2121
struct AlertAndSheetState: Equatable {
@@ -27,8 +27,10 @@ struct AlertAndSheetState: Equatable {
2727
enum AlertAndSheetAction: Equatable {
2828
case actionSheetButtonTapped
2929
case actionSheetCancelTapped
30+
case actionSheetDismissed
3031
case alertButtonTapped
3132
case alertCancelTapped
33+
case alertDismissed
3234
case decrementButtonTapped
3335
case incrementButtonTapped
3436
}
@@ -53,6 +55,9 @@ let alertAndSheetReducer = Reducer<
5355
return .none
5456

5557
case .actionSheetCancelTapped:
58+
return .none
59+
60+
case .actionSheetDismissed:
5661
state.actionSheet = nil
5762
return .none
5863

@@ -66,21 +71,24 @@ let alertAndSheetReducer = Reducer<
6671
return .none
6772

6873
case .alertCancelTapped:
74+
return .none
75+
76+
case .alertDismissed:
6977
state.alert = nil
7078
return .none
7179

7280
case .decrementButtonTapped:
73-
state.actionSheet = nil
81+
state.alert = .init(title: "Decremented!")
7482
state.count -= 1
7583
return .none
7684

7785
case .incrementButtonTapped:
78-
state.actionSheet = nil
79-
state.alert = nil
86+
state.alert = .init(title: "Incremented!")
8087
state.count += 1
8188
return .none
8289
}
8390
}
91+
.debug()
8492

8593
struct AlertAndSheetView: View {
8694
let store: Store<AlertAndSheetState, AlertAndSheetAction>
@@ -94,13 +102,13 @@ struct AlertAndSheetView: View {
94102
Button("Alert") { viewStore.send(.alertButtonTapped) }
95103
.alert(
96104
self.store.scope(state: { $0.alert }),
97-
dismiss: .alertCancelTapped
105+
dismiss: .alertDismissed
98106
)
99107

100108
Button("Action sheet") { viewStore.send(.actionSheetButtonTapped) }
101109
.actionSheet(
102110
self.store.scope(state: { $0.actionSheet }),
103-
dismiss: .actionSheetCancelTapped
111+
dismiss: .actionSheetDismissed
104112
)
105113
}
106114
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ struct SharedStateProfileView: View {
221221
This tab shows state from the previous tab, and it is capable of reseting all of the \
222222
state back to 0.
223223
224-
This shows that it is possible to for each screen to model its state in the way that \
225-
makes the most sense for it, while still allowing the state and mutations to be shared \
224+
This shows that it is possible for each screen to model its state in the way that makes \
225+
the most sense for it, while still allowing the state and mutations to be shared \
226226
across independent screens.
227227
""",
228228
.caption

Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ class AlertsAndActionSheetsTests: XCTestCase {
2323
)
2424
},
2525
.send(.incrementButtonTapped) {
26-
$0.alert = nil
26+
$0.alert = .init(title: "Incremented!")
2727
$0.count = 1
28+
},
29+
.send(.alertDismissed) {
30+
$0.alert = nil
2831
}
2932
)
3033
}
@@ -49,8 +52,11 @@ class AlertsAndActionSheetsTests: XCTestCase {
4952
)
5053
},
5154
.send(.incrementButtonTapped) {
52-
$0.actionSheet = nil
55+
$0.alert = .init(title: "Incremented!")
5356
$0.count = 1
57+
},
58+
.send(.actionSheetDismissed) {
59+
$0.actionSheet = nil
5460
}
5561
)
5662
}

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Using ReactiveSwift, which doesn't use Combine's type model, `Effect<Output, Fai
1818

1919
# The Composable Architecture
2020

21-
The Composable Architecture is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).
21+
The Composable Architecture (TCA, for short) is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).
2222

2323
* [What is the Composable Architecture?](#what-is-the-composable-architecture)
2424
* [Learn more](#learn-more)
@@ -28,6 +28,7 @@ The Composable Architecture is a library for building applications in a consiste
2828
* [FAQ](#faq)
2929
* [Requirements](#requirements)
3030
* [Installation](#installation)
31+
* [Documentation](#documentation)
3132
* [Help](#help)
3233
* [Credits and thanks](#credits-and-thanks)
3334
* [Other libraries](#other-libraries)
@@ -154,7 +155,7 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, e
154155
state.numberFactAlert = fact
155156
return .none
156157

157-
case let .numberFactResponse(.failure):
158+
case .numberFactResponse(.failure):
158159
state.numberFactAlert = "Could not load a number fact :("
159160
return .none
160161
}
@@ -377,7 +378,7 @@ If you are interested in contributing a wrapper library for a framework that we
377378
* How does the Composable Architecture compare to Elm, Redux, and others?
378379
<details>
379380
<summary>Expand to see answer</summary>
380-
The Composable Architecture (TCA) is built on a foundation of ideas popularized by Elm and Redux, but made to feel at home in the Swift language and on Apple's platforms.
381+
The Composable Architecture (TCA) is built on a foundation of ideas popularized by the Elm Architecture (TEA) and Redux, but made to feel at home in the Swift language and on Apple's platforms.
381382

382383
In some ways TCA is a little more opinionated than the other libraries. For example, Redux is not prescriptive with how one executes side effects, but TCA requires all side effects to be modeled in the `Effect` type and returned from the reducer.
383384

@@ -430,6 +431,10 @@ You can add ComposableArchitecture to an Xcode project by adding it as a package
430431
- If you have a single application target that needs access to the library, then add **ComposableArchitecture** directly to your application.
431432
- If you want to use this library from multiple targets you must create a shared framework that depends on **ComposableArchitecture** and then depend on that framework in all of your targets. For an example of this, check out the [Tic-Tac-Toe](./Examples/TicTacToe) demo application, which splits lots of features into modules and consumes the static library in this fashion using the **TicTacToeCommon** framework.
432433

434+
## Documentation
435+
436+
The latest documentation for the Composable Architecture APIs is available [here](https://trading-point.github.io/reactiveswift-composable-architecture/).
437+
433438
## Help
434439

435440
If you want to discuss the Composable Architecture or have a question about how to use it to solve a particular problem, ask around on [its Swift forum](https://forums.swift.org/c/related-projects/swift-composable-architecture).

Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import SwiftUI
105105
@available(tvOS 13, *)
106106
@available(watchOS 6, *)
107107
public struct ActionSheetState<Action> {
108+
public let id = UUID()
108109
public var buttons: [Button]
109110
public var message: String?
110111
public var title: String
@@ -127,24 +128,50 @@ public struct ActionSheetState<Action> {
127128
@available(macOS 10.15, *)
128129
@available(tvOS 13, *)
129130
@available(watchOS 6, *)
130-
extension ActionSheetState: Equatable where Action: Equatable {}
131+
extension ActionSheetState: CustomDebugOutputConvertible {
132+
public var debugOutput: String {
133+
let fields = (
134+
title: self.title,
135+
message: self.message,
136+
buttons: self.buttons
137+
)
138+
return "\(Self.self)\(ComposableArchitecture.debugOutput(fields))"
139+
}
140+
}
131141

132142
@available(iOS 13, *)
133143
@available(macCatalyst 13, *)
134144
@available(macOS 10.15, *)
135145
@available(tvOS 13, *)
136146
@available(watchOS 6, *)
137-
extension ActionSheetState: Hashable where Action: Hashable {}
147+
extension ActionSheetState: Equatable where Action: Equatable {
148+
public static func == (lhs: Self, rhs: Self) -> Bool {
149+
lhs.title == rhs.title
150+
&& lhs.message == rhs.message
151+
&& lhs.buttons == rhs.buttons
152+
}
153+
}
138154

139155
@available(iOS 13, *)
140156
@available(macCatalyst 13, *)
141157
@available(macOS 10.15, *)
142158
@available(tvOS 13, *)
143159
@available(watchOS 6, *)
144-
extension ActionSheetState: Identifiable where Action: Hashable {
145-
public var id: Self { self }
160+
extension ActionSheetState: Hashable where Action: Hashable {
161+
public func hash(into hasher: inout Hasher) {
162+
hasher.combine(self.title)
163+
hasher.combine(self.message)
164+
hasher.combine(self.buttons)
165+
}
146166
}
147167

168+
@available(iOS 13, *)
169+
@available(macCatalyst 13, *)
170+
@available(macOS 10.15, *)
171+
@available(tvOS 13, *)
172+
@available(watchOS 6, *)
173+
extension ActionSheetState: Identifiable {}
174+
148175
@available(iOS 13, *)
149176
@available(macCatalyst 13, *)
150177
@available(macOS 10.15, *)
@@ -157,7 +184,8 @@ extension View {
157184
/// - Parameters:
158185
/// - store: A store that describes if the action sheet is shown or dismissed.
159186
/// - dismissal: An action to send when the action sheet is dismissed through non-user actions,
160-
/// such as when an action sheet is automatically dismissed by the system.
187+
/// such as when an action sheet is automatically dismissed by the system. Use this action to
188+
/// `nil` out the associated action sheet state.
161189
@available(iOS 13, *)
162190
@available(macCatalyst 13, *)
163191
@available(macOS, unavailable)
@@ -168,16 +196,11 @@ extension View {
168196
dismiss: Action
169197
) -> some View {
170198

171-
let viewStore = ViewStore(store, removeDuplicates: { ($0 == nil) != ($1 == nil) })
172-
return self.actionSheet(
173-
isPresented: Binding(
174-
get: { viewStore.state != nil },
175-
set: {
176-
guard !$0 else { return }
177-
viewStore.send(dismiss)
178-
}),
179-
content: { viewStore.state?.toSwiftUI(send: viewStore.send) ?? ActionSheet(title: Text("")) }
180-
)
199+
WithViewStore(store, removeDuplicates: { $0?.id == $1?.id }) { viewStore in
200+
self.actionSheet(item: viewStore.binding(send: dismiss)) { state in
201+
state.toSwiftUI(send: viewStore.send)
202+
}
203+
}
181204
}
182205
}
183206

Sources/ComposableArchitecture/SwiftUI/Alert.swift

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import SwiftUI
9191
/// )
9292
///
9393
public struct AlertState<Action> {
94+
public let id = UUID()
9495
public var message: String?
9596
public var primaryButton: Button?
9697
public var secondaryButton: Button?
@@ -169,34 +170,54 @@ extension View {
169170
/// - Parameters:
170171
/// - store: A store that describes if the alert is shown or dismissed.
171172
/// - dismissal: An action to send when the alert is dismissed through non-user actions, such
172-
/// as when an alert is automatically dismissed by the system.
173+
/// as when an alert is automatically dismissed by the system. Use this action to `nil` out
174+
/// the associated alert state.
173175
public func alert<Action>(
174176
_ store: Store<AlertState<Action>?, Action>,
175177
dismiss: Action
176178
) -> some View {
177179

178-
let viewStore = ViewStore(store, removeDuplicates: { ($0 == nil) != ($1 == nil) })
179-
return self.alert(
180-
isPresented: Binding(
181-
get: { viewStore.state != nil },
182-
set: {
183-
guard !$0 else { return }
184-
viewStore.send(dismiss)
185-
}),
186-
content: { viewStore.state?.toSwiftUI(send: viewStore.send) ?? Alert(title: Text("")) }
180+
WithViewStore(store, removeDuplicates: { $0?.id == $1?.id }) { viewStore in
181+
self.alert(item: viewStore.binding(send: dismiss)) { state in
182+
state.toSwiftUI(send: viewStore.send)
183+
}
184+
}
185+
}
186+
}
187+
188+
extension AlertState: CustomDebugOutputConvertible {
189+
public var debugOutput: String {
190+
let fields = (
191+
title: self.title,
192+
message: self.message,
193+
primaryButton: self.primaryButton,
194+
secondaryButton: self.secondaryButton
187195
)
196+
return "\(Self.self)\(ComposableArchitecture.debugOutput(fields))"
188197
}
189198
}
190199

191-
extension AlertState: Equatable where Action: Equatable {}
192-
extension AlertState: Hashable where Action: Hashable {}
200+
extension AlertState: Equatable where Action: Equatable {
201+
public static func == (lhs: Self, rhs: Self) -> Bool {
202+
lhs.title == rhs.title
203+
&& lhs.message == rhs.message
204+
&& lhs.primaryButton == rhs.primaryButton
205+
&& lhs.secondaryButton == rhs.secondaryButton
206+
}
207+
}
208+
extension AlertState: Hashable where Action: Hashable {
209+
public func hash(into hasher: inout Hasher) {
210+
hasher.combine(self.title)
211+
hasher.combine(self.message)
212+
hasher.combine(self.primaryButton)
213+
hasher.combine(self.secondaryButton)
214+
}
215+
}
216+
extension AlertState: Identifiable {}
217+
193218
extension AlertState.Button: Equatable where Action: Equatable {}
194219
extension AlertState.Button: Hashable where Action: Hashable {}
195220

196-
extension AlertState: Identifiable where Action: Hashable {
197-
public var id: Self { self }
198-
}
199-
200221
extension AlertState.Button {
201222
@available(iOS 13, *)
202223
@available(macCatalyst 13, *)

0 commit comments

Comments
 (0)