Skip to content

Commit 7f482ac

Browse files
Alert bug exploration (#249)
* wip * Fix * revert * Fix tests * Better debug output * organize * alphabetize * Fix mac Co-authored-by: Brandon Williams <[email protected]>
1 parent 8308541 commit 7f482ac

File tree

4 files changed

+96
-38
lines changed

4 files changed

+96
-38
lines changed

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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/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
}

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,32 +128,59 @@ public struct ActionSheetState<Action> {
127128
@available(macOS, unavailable)
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, unavailable)
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, unavailable)
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, unavailable)
171+
@available(tvOS 13, *)
172+
@available(watchOS 6, *)
173+
extension ActionSheetState: Identifiable {}
174+
148175
extension View {
149176
/// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it
150177
/// becomes `nil`.
151178
///
152179
/// - Parameters:
153180
/// - store: A store that describes if the action sheet is shown or dismissed.
154181
/// - dismissal: An action to send when the action sheet is dismissed through non-user actions,
155-
/// such as when an action sheet is automatically dismissed by the system.
182+
/// such as when an action sheet is automatically dismissed by the system. Use this action to
183+
/// `nil` out the associated action sheet state.
156184
@available(iOS 13, *)
157185
@available(macCatalyst 13, *)
158186
@available(macOS, unavailable)
@@ -163,16 +191,11 @@ extension View {
163191
dismiss: Action
164192
) -> some View {
165193

166-
let viewStore = ViewStore(store, removeDuplicates: { ($0 == nil) != ($1 == nil) })
167-
return self.actionSheet(
168-
isPresented: Binding(
169-
get: { viewStore.state != nil },
170-
set: {
171-
guard !$0 else { return }
172-
viewStore.send(dismiss)
173-
}),
174-
content: { viewStore.state?.toSwiftUI(send: viewStore.send) ?? ActionSheet(title: Text("")) }
175-
)
194+
WithViewStore(store, removeDuplicates: { $0?.id == $1?.id }) { viewStore in
195+
self.actionSheet(item: viewStore.binding(send: dismiss)) { state in
196+
state.toSwiftUI(send: viewStore.send)
197+
}
198+
}
176199
}
177200
}
178201

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?
@@ -164,34 +165,54 @@ extension View {
164165
/// - Parameters:
165166
/// - store: A store that describes if the alert is shown or dismissed.
166167
/// - dismissal: An action to send when the alert is dismissed through non-user actions, such
167-
/// as when an alert is automatically dismissed by the system.
168+
/// as when an alert is automatically dismissed by the system. Use this action to `nil` out
169+
/// the associated alert state.
168170
public func alert<Action>(
169171
_ store: Store<AlertState<Action>?, Action>,
170172
dismiss: Action
171173
) -> some View {
172174

173-
let viewStore = ViewStore(store, removeDuplicates: { ($0 == nil) != ($1 == nil) })
174-
return self.alert(
175-
isPresented: Binding(
176-
get: { viewStore.state != nil },
177-
set: {
178-
guard !$0 else { return }
179-
viewStore.send(dismiss)
180-
}),
181-
content: { viewStore.state?.toSwiftUI(send: viewStore.send) ?? Alert(title: Text("")) }
175+
WithViewStore(store, removeDuplicates: { $0?.id == $1?.id }) { viewStore in
176+
self.alert(item: viewStore.binding(send: dismiss)) { state in
177+
state.toSwiftUI(send: viewStore.send)
178+
}
179+
}
180+
}
181+
}
182+
183+
extension AlertState: CustomDebugOutputConvertible {
184+
public var debugOutput: String {
185+
let fields = (
186+
title: self.title,
187+
message: self.message,
188+
primaryButton: self.primaryButton,
189+
secondaryButton: self.secondaryButton
182190
)
191+
return "\(Self.self)\(ComposableArchitecture.debugOutput(fields))"
183192
}
184193
}
185194

186-
extension AlertState: Equatable where Action: Equatable {}
187-
extension AlertState: Hashable where Action: Hashable {}
195+
extension AlertState: Equatable where Action: Equatable {
196+
public static func == (lhs: Self, rhs: Self) -> Bool {
197+
lhs.title == rhs.title
198+
&& lhs.message == rhs.message
199+
&& lhs.primaryButton == rhs.primaryButton
200+
&& lhs.secondaryButton == rhs.secondaryButton
201+
}
202+
}
203+
extension AlertState: Hashable where Action: Hashable {
204+
public func hash(into hasher: inout Hasher) {
205+
hasher.combine(self.title)
206+
hasher.combine(self.message)
207+
hasher.combine(self.primaryButton)
208+
hasher.combine(self.secondaryButton)
209+
}
210+
}
211+
extension AlertState: Identifiable {}
212+
188213
extension AlertState.Button: Equatable where Action: Equatable {}
189214
extension AlertState.Button: Hashable where Action: Hashable {}
190215

191-
extension AlertState: Identifiable where Action: Hashable {
192-
public var id: Self { self }
193-
}
194-
195216
extension AlertState.Button {
196217
func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button {
197218
let action = { if let action = self.action { send(action) } }

0 commit comments

Comments
 (0)