Skip to content

Commit e9a2c53

Browse files
mluisbrownmbrandonw
andcommitted
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 066213b commit e9a2c53

File tree

4 files changed

+91
-38
lines changed

4 files changed

+91
-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: 33 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,37 +128,59 @@ 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

148168
@available(iOS 13, *)
149169
@available(macCatalyst 13, *)
150170
@available(macOS 10.15, *)
151171
@available(tvOS 13, *)
152172
@available(watchOS 6, *)
173+
extension ActionSheetState: Identifiable {}
174+
153175
extension View {
154176
/// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it
155177
/// becomes `nil`.
156178
///
157179
/// - Parameters:
158180
/// - store: A store that describes if the action sheet is shown or dismissed.
159181
/// - 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.
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.
161184
@available(iOS 13, *)
162185
@available(macCatalyst 13, *)
163186
@available(macOS, unavailable)
@@ -168,16 +191,11 @@ extension View {
168191
dismiss: Action
169192
) -> some View {
170193

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-
)
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+
}
181199
}
182200
}
183201

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)