Skip to content

Commit eb7a1c5

Browse files
Merge pull request #73 from componentskit/alert-with-item
Add new alert API
2 parents 3d7951f + e43d064 commit eb7a1c5

File tree

5 files changed

+202
-104
lines changed

5 files changed

+202
-104
lines changed
Lines changed: 171 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,92 @@
11
import SwiftUI
22

3+
struct AlertContent: View {
4+
@Binding var isPresented: Bool
5+
let model: AlertVM
6+
let primaryAction: (() -> Void)?
7+
let secondaryAction: (() -> Void)?
8+
9+
var body: some View {
10+
SUCenterModal(
11+
isVisible: self.$isPresented,
12+
model: self.model.modalVM,
13+
header: {
14+
if self.model.message.isNotNil,
15+
let text = self.model.title {
16+
self.title(text)
17+
}
18+
},
19+
body: {
20+
if let text = self.model.message {
21+
self.message(text)
22+
} else if let text = self.model.title {
23+
self.title(text)
24+
}
25+
},
26+
footer: {
27+
switch AlertButtonsOrientationCalculator.preferredOrientation(model: model) {
28+
case .horizontal:
29+
HStack(spacing: AlertVM.buttonsSpacing) {
30+
self.button(
31+
model: self.model.secondaryButtonVM,
32+
action: self.secondaryAction
33+
)
34+
self.button(
35+
model: self.model.primaryButtonVM,
36+
action: self.primaryAction
37+
)
38+
}
39+
case .vertical:
40+
VStack(spacing: AlertVM.buttonsSpacing) {
41+
self.button(
42+
model: self.model.primaryButtonVM,
43+
action: self.primaryAction
44+
)
45+
self.button(
46+
model: self.model.secondaryButtonVM,
47+
action: self.secondaryAction
48+
)
49+
}
50+
}
51+
}
52+
)
53+
}
54+
55+
// MARK: - Helpers
56+
57+
func title(_ text: String) -> some View {
58+
Text(text)
59+
.font(UniversalFont.mdHeadline.font)
60+
.foregroundStyle(UniversalColor.foreground.color)
61+
.multilineTextAlignment(.center)
62+
.frame(maxWidth: .infinity)
63+
}
64+
65+
func message(_ text: String) -> some View {
66+
Text(text)
67+
.font(UniversalFont.mdBody.font)
68+
.foregroundStyle(UniversalColor.secondaryForeground.color)
69+
.multilineTextAlignment(.center)
70+
.frame(maxWidth: .infinity)
71+
}
72+
73+
func button(
74+
model: ButtonVM?,
75+
action: (() -> Void)?
76+
) -> some View {
77+
Group {
78+
if let model {
79+
SUButton(model: model) {
80+
action?()
81+
self.isPresented = false
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
// MARK: - Presentation Helpers
89+
390
extension View {
491
/// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons.
592
///
@@ -53,95 +140,95 @@ extension View {
53140
transitionDuration: model.transition.value,
54141
onDismiss: onDismiss,
55142
content: {
56-
SUCenterModal(
57-
isVisible: isPresented,
58-
model: model.modalVM,
59-
header: {
60-
if model.message.isNotNil,
61-
let title = model.title {
62-
AlertTitle(text: title)
63-
}
64-
},
65-
body: {
66-
if let message = model.message {
67-
AlertMessage(text: message)
68-
} else if let title = model.title {
69-
AlertTitle(text: title)
70-
}
71-
},
72-
footer: {
73-
switch AlertButtonsOrientationCalculator.preferredOrientation(model: model) {
74-
case .horizontal:
75-
HStack(spacing: AlertVM.buttonsSpacing) {
76-
AlertButton(
77-
isAlertPresented: isPresented,
78-
model: model.secondaryButtonVM,
79-
action: secondaryAction
80-
)
81-
AlertButton(
82-
isAlertPresented: isPresented,
83-
model: model.primaryButtonVM,
84-
action: primaryAction
85-
)
86-
}
87-
case .vertical:
88-
VStack(spacing: AlertVM.buttonsSpacing) {
89-
AlertButton(
90-
isAlertPresented: isPresented,
91-
model: model.primaryButtonVM,
92-
action: primaryAction
93-
)
94-
AlertButton(
95-
isAlertPresented: isPresented,
96-
model: model.secondaryButtonVM,
97-
action: secondaryAction
98-
)
99-
}
100-
}
101-
}
143+
AlertContent(
144+
isPresented: isPresented,
145+
model: model,
146+
primaryAction: primaryAction,
147+
secondaryAction: secondaryAction
102148
)
103149
}
104150
)
105151
}
106-
}
107-
108-
// MARK: - Helpers
109-
110-
private struct AlertTitle: View {
111-
let text: String
112-
113-
var body: some View {
114-
Text(self.text)
115-
.font(UniversalFont.mdHeadline.font)
116-
.foregroundStyle(UniversalColor.foreground.color)
117-
.multilineTextAlignment(.center)
118-
.frame(maxWidth: .infinity)
119-
}
120-
}
121152

122-
private struct AlertMessage: View {
123-
let text: String
124-
125-
var body: some View {
126-
Text(self.text)
127-
.font(UniversalFont.mdBody.font)
128-
.foregroundStyle(UniversalColor.secondaryForeground.color)
129-
.multilineTextAlignment(.center)
130-
.frame(maxWidth: .infinity)
131-
}
132-
}
133-
134-
private struct AlertButton: View {
135-
@Binding var isAlertPresented: Bool
136-
let model: ButtonVM?
137-
let action: (() -> Void)?
138-
139-
var body: some View {
140-
if let model {
141-
SUButton(model: model) {
142-
self.action?()
143-
self.isAlertPresented = false
153+
/// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons.
154+
///
155+
/// All actions in an alert dismiss the alert after the action runs. If no actions are present, a standard “OK” action is included.
156+
///
157+
/// - Parameters:
158+
/// - isPresented: A binding that determines whether the alert is presented.
159+
/// - item: A binding to an optional `Item` that determines whether the alert is presented.
160+
/// When `item` is `nil`, the alert is hidden.
161+
/// - primaryAction: An optional closure executed when the primary button is tapped.
162+
/// - secondaryAction: An optional closure executed when the secondary button is tapped.
163+
/// - onDismiss: An optional closure executed when the alert is dismissed.
164+
///
165+
/// - Example:
166+
/// ```swift
167+
/// struct ContentView: View {
168+
/// struct AlertData: Identifiable {
169+
/// var id: String {
170+
/// return text
171+
/// }
172+
/// let text: String
173+
/// }
174+
///
175+
/// @State private var selectedItem: AlertData?
176+
/// private let items: [AlertData] = [
177+
/// AlertData(text: "data 1"),
178+
/// AlertData(text: "data 2")
179+
/// ]
180+
///
181+
/// var body: some View {
182+
/// List(items) { item in
183+
/// Button("Show Alert") {
184+
/// selectedItem = item
185+
/// }
186+
/// }
187+
/// .suAlert(
188+
/// item: $selectedItem,
189+
/// model: { data in
190+
/// return AlertVM {
191+
/// $0.title = "Data Preview"
192+
/// $0.message = data.text
193+
/// }
194+
/// },
195+
/// onDismiss: {
196+
/// print("Alert dismissed")
197+
/// }
198+
/// )
199+
/// }
200+
/// }
201+
/// ```
202+
public func suAlert<Item: Identifiable>(
203+
item: Binding<Item?>,
204+
model: @escaping (Item) -> AlertVM,
205+
primaryAction: ((Item) -> Void)? = nil,
206+
secondaryAction: ((Item) -> Void)? = nil,
207+
onDismiss: (() -> Void)? = nil
208+
) -> some View {
209+
return self.modal(
210+
item: item,
211+
transitionDuration: { model($0).transition.value },
212+
onDismiss: onDismiss,
213+
content: { unwrappedItem in
214+
AlertContent(
215+
isPresented: .init(
216+
get: {
217+
return item.wrappedValue.isNotNil
218+
},
219+
set: { isPresented in
220+
if isPresented {
221+
item.wrappedValue = unwrappedItem
222+
} else {
223+
item.wrappedValue = nil
224+
}
225+
}
226+
),
227+
model: model(unwrappedItem),
228+
primaryAction: { primaryAction?(unwrappedItem) },
229+
secondaryAction: { secondaryAction?(unwrappedItem) }
230+
)
144231
}
145-
}
232+
)
146233
}
147234
}

Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ struct ModalPresentationModifier<Modal: View>: ViewModifier {
2323

2424
func body(content: Content) -> some View {
2525
content
26-
.onChange(of: self.isContentVisible) { newValue in
27-
if newValue {
26+
.onAppear {
27+
if self.isContentVisible {
28+
self.isPresented = true
29+
}
30+
}
31+
.onChange(of: self.isContentVisible) { isVisible in
32+
if isVisible {
2833
self.isPresented = true
2934
} else {
3035
DispatchQueue.main.asyncAfter(deadline: .now() + self.transitionDuration) {

Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ struct ModalPresentationWithItemModifier<Modal: View, Item: Identifiable>: ViewM
66

77
@ViewBuilder var content: (Item) -> Modal
88

9-
let transitionDuration: TimeInterval
9+
let transitionDuration: (Item) -> TimeInterval
1010
let onDismiss: (() -> Void)?
1111

1212
init(
1313
item: Binding<Item?>,
14-
transitionDuration: TimeInterval,
14+
transitionDuration: @escaping (Item) -> TimeInterval,
1515
onDismiss: (() -> Void)?,
1616
@ViewBuilder content: @escaping (Item) -> Modal
1717
) {
@@ -23,11 +23,17 @@ struct ModalPresentationWithItemModifier<Modal: View, Item: Identifiable>: ViewM
2323

2424
func body(content: Content) -> some View {
2525
content
26-
.onChange(of: self.visibleItem.isNotNil) { newValue in
27-
if newValue {
26+
.onAppear {
27+
self.presentedItem = self.visibleItem
28+
}
29+
.onChange(of: self.visibleItem.isNotNil) { isVisible in
30+
if isVisible {
2831
self.presentedItem = self.visibleItem
2932
} else {
30-
DispatchQueue.main.asyncAfter(deadline: .now() + self.transitionDuration) {
33+
let duration = self.presentedItem.map { item in
34+
self.transitionDuration(item)
35+
} ?? 0.3
36+
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
3137
self.presentedItem = self.visibleItem
3238
}
3339
}
@@ -49,7 +55,7 @@ struct ModalPresentationWithItemModifier<Modal: View, Item: Identifiable>: ViewM
4955
extension View {
5056
func modal<Modal: View, Item: Identifiable>(
5157
item: Binding<Item?>,
52-
transitionDuration: TimeInterval,
58+
transitionDuration: @escaping (Item) -> TimeInterval,
5359
onDismiss: (() -> Void)? = nil,
5460
@ViewBuilder content: @escaping (Item) -> Modal
5561
) -> some View {

Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ extension View {
195195
/// }
196196
/// .bottomModal(
197197
/// item: $selectedItem,
198-
/// model: BottomModalVM(),
198+
/// model: { _ in BottomModalVM() },
199199
/// onDismiss: {
200200
/// print("Modal dismissed")
201201
/// },
@@ -218,15 +218,15 @@ extension View {
218218
/// ```
219219
public func bottomModal<Item: Identifiable, Header: View, Body: View, Footer: View>(
220220
item: Binding<Item?>,
221-
model: BottomModalVM = .init(),
221+
model: @escaping (Item) -> BottomModalVM = { _ in .init() },
222222
onDismiss: (() -> Void)? = nil,
223223
@ViewBuilder header: @escaping (Item) -> Header,
224224
@ViewBuilder body: @escaping (Item) -> Body,
225225
@ViewBuilder footer: @escaping (Item) -> Footer
226226
) -> some View {
227227
return self.modal(
228228
item: item,
229-
transitionDuration: model.transition.value,
229+
transitionDuration: { model($0).transition.value },
230230
onDismiss: onDismiss,
231231
content: { unwrappedItem in
232232
SUBottomModal(
@@ -242,7 +242,7 @@ extension View {
242242
}
243243
}
244244
),
245-
model: model,
245+
model: model(unwrappedItem),
246246
header: { header(unwrappedItem) },
247247
body: { body(unwrappedItem) },
248248
footer: { footer(unwrappedItem) }
@@ -289,7 +289,7 @@ extension View {
289289
/// }
290290
/// .bottomModal(
291291
/// item: $selectedItem,
292-
/// model: BottomModalVM(),
292+
/// model: { _ in BottomModalVM() },
293293
/// onDismiss: {
294294
/// print("Modal dismissed")
295295
/// },
@@ -302,7 +302,7 @@ extension View {
302302
/// ```
303303
public func bottomModal<Item: Identifiable, Body: View>(
304304
item: Binding<Item?>,
305-
model: BottomModalVM = .init(),
305+
model: @escaping (Item) -> BottomModalVM = { _ in .init() },
306306
onDismiss: (() -> Void)? = nil,
307307
@ViewBuilder body: @escaping (Item) -> Body
308308
) -> some View {

0 commit comments

Comments
 (0)