-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathActionSheet.swift
217 lines (207 loc) · 6.96 KB
/
ActionSheet.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import SwiftUI
/// A data type that describes the state of an action sheet that can be shown to the user. The
/// `Action` generic is the type of actions that can be sent from tapping on a button in the sheet.
///
/// This type can be used in your application's state in order to control the presentation or
/// dismissal of action sheets. It is preferable to use this API instead of the default SwiftUI API
/// for action sheets because SwiftUI uses 2-way bindings in order to control the showing and
/// dismissal of sheets, and that does not play nicely with the Composable Architecture. The library
/// requires that all state mutations happen by sending an action so that a reducer can handle that
/// logic, which greatly simplifies how data flows through your application, and gives you instant
/// testability on all parts of your application.
///
/// To use this API, you model all the action sheet actions in your domain's action enum:
///
/// enum AppAction: Equatable {
/// case cancelTapped
/// case deleteTapped
/// case favoriteTapped
/// case infoTapped
///
/// // Your other actions
/// }
///
/// And you model the state for showing the action sheet in your domain's state, and it can start
/// off in a `nil` state:
///
/// struct AppState: Equatable {
/// var actionSheet: ActionSheetState<AppAction>?
///
/// // Your other state
/// }
///
/// Then, in the reducer you can construct an `ActionSheetState` value to represent the action
/// sheet you want to show to the user:
///
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in
/// switch action
/// case .cancelTapped:
/// state.actionSheet = nil
/// return .none
///
/// case .deleteTapped:
/// state.actionSheet = nil
/// // Do deletion logic...
///
/// case .favoriteTapped:
/// state.actionSheet = nil
/// // Do favoriting logic
///
/// case .infoTapped:
/// state.actionSheet = .init(
/// title: "What would you like to do?",
/// buttons: [
/// .default("Favorite", send: .favoriteTapped),
/// .destructive("Delete", send: .deleteTapped),
/// .cancel(),
/// ]
/// )
/// return .none
/// }
/// }
///
/// And then, in your view you can use the `.actionSheet(_:send:dismiss:)` method on `View` in order
/// to present the action sheet in a way that works best with the Composable Architecture:
///
/// Button("Info") { viewStore.send(.infoTapped) }
/// .actionSheet(
/// self.store.scope(state: \.actionSheet),
/// dismiss: .cancelTapped
/// )
///
/// This makes your reducer in complete control of when the action sheet is shown or dismissed, and
/// makes it so that any choice made in the action sheet is automatically fed back into the reducer
/// so that you can handle its logic.
///
/// Even better, you can instantly write tests that your action sheet behavior works as expected:
///
/// let store = TestStore(
/// initialState: AppState(),
/// reducer: appReducer,
/// environment: .mock
/// )
///
/// store.assert(
/// .send(.infoTapped) {
/// $0.actionSheet = .init(
/// title: "What would you like to do?",
/// buttons: [
/// .default("Favorite", send: .favoriteTapped),
/// .destructive("Delete", send: .deleteTapped),
/// .cancel(),
/// ]
/// )
/// },
/// .send(.favoriteTapped) {
/// $0.actionSheet = nil
/// // Also verify that favoriting logic executed correctly
/// }
/// )
///
@available(iOS 13, *)
@available(macCatalyst 13, *)
@available(macOS, unavailable)
@available(tvOS 13, *)
@available(watchOS 6, *)
public struct ActionSheetState<Action> {
public let id = UUID()
public var buttons: [Button]
public var message: LocalizedStringKey?
public var title: LocalizedStringKey
public init(
title: LocalizedStringKey,
message: LocalizedStringKey? = nil,
buttons: [Button]
) {
self.buttons = buttons
self.message = message
self.title = title
}
public typealias Button = AlertState<Action>.Button
}
@available(iOS 13, *)
@available(macCatalyst 13, *)
@available(macOS, unavailable)
@available(tvOS 13, *)
@available(watchOS 6, *)
extension ActionSheetState: CustomDebugOutputConvertible {
public var debugOutput: String {
let fields = (
title: self.title,
message: self.message,
buttons: self.buttons
)
return "\(Self.self)\(ComposableArchitecture.debugOutput(fields))"
}
}
@available(iOS 13, *)
@available(macCatalyst 13, *)
@available(macOS, unavailable)
@available(tvOS 13, *)
@available(watchOS 6, *)
extension ActionSheetState: Equatable where Action: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.title.formatted() == rhs.title.formatted()
&& lhs.message?.formatted() == rhs.message?.formatted()
&& lhs.buttons == rhs.buttons
}
}
@available(iOS 13, *)
@available(macCatalyst 13, *)
@available(macOS, unavailable)
@available(tvOS 13, *)
@available(watchOS 6, *)
extension ActionSheetState: Hashable where Action: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.title.formatted())
hasher.combine(self.message?.formatted())
hasher.combine(self.buttons)
}
}
@available(iOS 13, *)
@available(macCatalyst 13, *)
@available(macOS, unavailable)
@available(tvOS 13, *)
@available(watchOS 6, *)
extension ActionSheetState: Identifiable {}
extension View {
/// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it
/// becomes `nil`.
///
/// - Parameters:
/// - store: A store that describes if the action sheet is shown or dismissed.
/// - dismissal: An action to send when the action sheet is dismissed through non-user actions,
/// such as when an action sheet is automatically dismissed by the system. Use this action to
/// `nil` out the associated action sheet state.
@available(iOS 13, *)
@available(macCatalyst 13, *)
@available(macOS, unavailable)
@available(tvOS 13, *)
@available(watchOS 6, *)
public func actionSheet<Action>(
_ store: Store<ActionSheetState<Action>?, Action>,
dismiss: Action
) -> some View {
WithViewStore(store, removeDuplicates: { $0?.id == $1?.id }) { viewStore in
self.actionSheet(item: viewStore.binding(send: dismiss)) { state in
state.toSwiftUI(send: viewStore.send)
}
}
}
}
@available(iOS 13, *)
@available(macCatalyst 13, *)
@available(macOS, unavailable)
@available(tvOS 13, *)
@available(watchOS 6, *)
extension ActionSheetState {
fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet {
SwiftUI.ActionSheet(
title: Text(self.title),
message: self.message.map { Text($0) },
buttons: self.buttons.map {
$0.toSwiftUI(send: send)
}
)
}
}