Skip to content

Commit 823a8fa

Browse files
Merge pull request #45 from componentskit/sualert
SUAlert
2 parents f7b2047 + c0af4e7 commit 823a8fa

File tree

6 files changed

+210
-22
lines changed

6 files changed

+210
-22
lines changed

Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33
import UIKit
44

55
struct AlertPreview: View {
6+
@State var isAlertPresented: Bool = false
67
@State private var model = AlertVM {
78
$0.title = Self.alertTitle
89
$0.message = AlertMessage.short.rawValue
@@ -21,6 +22,15 @@ struct AlertPreview: View {
2122
}
2223
.preview
2324
}
25+
PreviewWrapper(title: "SwiftUI") {
26+
SUButton(model: .init { $0.title = "Show Alert" }) {
27+
self.isAlertPresented = true
28+
}
29+
.suAlert(
30+
isPresented: self.$isAlertPresented,
31+
model: self.model
32+
)
33+
}
2434
Form {
2535
Section("Title") {
2636
Toggle("Has Title", isOn: .init(
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import UIKit
2+
3+
struct AlertButtonsOrientationCalculator {
4+
enum Orientation {
5+
case vertical
6+
case horizontal
7+
}
8+
9+
private static let primaryButton = UKButton()
10+
private static let secondaryButton = UKButton()
11+
12+
private init() {}
13+
14+
static func preferredOrientation(model: AlertVM) -> Orientation {
15+
guard let primaryButtonVM = model.primaryButtonVM,
16+
let secondaryButtonVM = model.secondaryButtonVM else {
17+
return .vertical
18+
}
19+
20+
self.primaryButton.model = primaryButtonVM.updating {
21+
$0.isFullWidth = false
22+
}
23+
self.secondaryButton.model = secondaryButtonVM.updating {
24+
$0.isFullWidth = false
25+
}
26+
27+
let primaryButtonWidth = self.primaryButton.intrinsicContentSize.width
28+
let secondaryButtonWidth = self.secondaryButton.intrinsicContentSize.width
29+
30+
// Since the `maxWidth` of the alert is always less than the width of the
31+
// screen, we can assume that the width of the container is equal to this
32+
// `maxWidth` value.
33+
let containerWidth = model.modalVM.size.maxWidth
34+
let availableButtonsWidth = containerWidth
35+
- AlertVM.buttonsSpacing
36+
- model.contentPaddings.leading
37+
- model.contentPaddings.trailing
38+
let availableButtonWidth = availableButtonsWidth / 2
39+
40+
if primaryButtonWidth <= availableButtonWidth,
41+
secondaryButtonWidth <= availableButtonWidth {
42+
return .horizontal
43+
} else {
44+
return .vertical
45+
}
46+
}
47+
}

Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ extension AlertVM {
8585
$0.color = model.color
8686
$0.cornerRadius = model.cornerRadius
8787
$0.style = model.style
88+
$0.isFullWidth = true
8889
}
8990
}
9091
}
@@ -96,5 +97,6 @@ extension AlertVM {
9697
$0.title = "OK"
9798
$0.color = .primary
9899
$0.style = .filled
100+
$0.isFullWidth = true
99101
}
100102
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import SwiftUI
2+
3+
extension View {
4+
/// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons.
5+
///
6+
/// All actions in an alert dismiss the alert after the action runs. If no actions are present, a standard “OK” action is included.
7+
///
8+
/// - Parameters:
9+
/// - isPresented: A binding that determines whether the alert is presented.
10+
/// - model: A model that defines the appearance properties for an alert.
11+
/// - primaryAction: An optional closure executed when the primary button is tapped.
12+
/// - secondaryAction: An optional closure executed when the secondary button is tapped.
13+
/// - onDismiss: An optional closure executed when the alert is dismissed.
14+
///
15+
/// - Example:
16+
/// ```swift
17+
/// SomeView()
18+
/// .suAlert(
19+
/// isPresented: $isAlertPresented,
20+
/// model: .init { alertVM in
21+
/// alertVM.title = "My Alert"
22+
/// alertVM.message = "This is an alert."
23+
/// alertVM.primaryButton = .init { buttonVM in
24+
/// buttonVM.title = "OK"
25+
/// buttonVM.color = .primary
26+
/// buttonVM.style = .filled
27+
/// }
28+
/// alertVM.secondaryButton = .init { buttonVM in
29+
/// buttonVM.title = "Cancel"
30+
/// buttonVM.style = .light
31+
/// }
32+
/// },
33+
/// primaryAction: {
34+
/// NSLog("Primary button tapped")
35+
/// },
36+
/// secondaryAction: {
37+
/// NSLog("Secondary button tapped")
38+
/// },
39+
/// onDismiss: {
40+
/// print("Alert dismissed")
41+
/// }
42+
/// )
43+
/// ```
44+
public func suAlert(
45+
isPresented: Binding<Bool>,
46+
model: AlertVM,
47+
primaryAction: (() -> Void)? = nil,
48+
secondaryAction: (() -> Void)? = nil,
49+
onDismiss: (() -> Void)? = nil
50+
) -> some View {
51+
return self.modal(
52+
isVisible: isPresented,
53+
transitionDuration: model.transition.value,
54+
onDismiss: onDismiss,
55+
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+
}
102+
)
103+
}
104+
)
105+
}
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+
}
121+
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
144+
}
145+
}
146+
}
147+
}

Sources/ComponentsKit/Components/Alert/UKAlertController.swift

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -138,30 +138,12 @@ public class UKAlertController: UKCenterModalController {
138138
super.updateViewConstraints()
139139

140140
if self.buttonsStackView.arrangedSubviews.count == 2 {
141-
self.buttonsStackView.axis = .vertical
142-
let primaryButtonWidth = self.primaryButton.intrinsicContentSize.width
143-
let secondaryButtonWidth = self.secondaryButton.intrinsicContentSize.width
144-
145-
// Since the `maxWidth` of the alert is always less than the width of the
146-
// screen, we can assume that the width of the container is equal to this
147-
// `maxWidth` value.
148-
let containerWidth = self.model.size.maxWidth
149-
let availableButtonsWidth = containerWidth
150-
- AlertVM.buttonsSpacing
151-
- self.model.contentPaddings.leading
152-
- self.model.contentPaddings.trailing
153-
let availableButtonWidth = availableButtonsWidth / 2
154-
155-
if primaryButtonWidth <= availableButtonWidth,
156-
secondaryButtonWidth <= availableButtonWidth {
141+
switch AlertButtonsOrientationCalculator.preferredOrientation(model: self.alertVM) {
142+
case .horizontal:
157143
self.buttonsStackView.removeArrangedSubview(self.secondaryButton)
158144
self.buttonsStackView.insertArrangedSubview(self.secondaryButton, at: 0)
159-
160145
self.buttonsStackView.axis = .horizontal
161-
} else {
162-
self.buttonsStackView.removeArrangedSubview(self.secondaryButton)
163-
self.buttonsStackView.insertArrangedSubview(self.secondaryButton, at: 1)
164-
146+
case .vertical:
165147
self.buttonsStackView.axis = .vertical
166148
}
167149
} else {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ struct SUCenterModal<Header: View, Body: View, Footer: View>: View {
5454
extension View {
5555
/// A SwiftUI view modifier that presents a center-aligned modal.
5656
///
57-
/// This modifier allows you to attach a cetner modal to any SwiftUI view, providing a structured way to display modals
57+
/// This modifier allows you to attach a center modal to any SwiftUI view, providing a structured way to display modals
5858
/// with a header, body, and footer, all styled and laid out according to the provided `CenterModalVM` model.
5959
///
6060
/// - Parameters:

0 commit comments

Comments
 (0)