diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift index babbd227..72b919b2 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift @@ -3,6 +3,7 @@ import SwiftUI import UIKit struct AlertPreview: View { + @State var isAlertPresented: Bool = false @State private var model = AlertVM { $0.title = Self.alertTitle $0.message = AlertMessage.short.rawValue @@ -21,6 +22,15 @@ struct AlertPreview: View { } .preview } + PreviewWrapper(title: "SwiftUI") { + SUButton(model: .init { $0.title = "Show Alert" }) { + self.isAlertPresented = true + } + .suAlert( + isPresented: self.$isAlertPresented, + model: self.model + ) + } Form { Section("Title") { Toggle("Has Title", isOn: .init( diff --git a/Sources/ComponentsKit/Components/Alert/Helpers/AlertButtonsOrientationCalculator.swift b/Sources/ComponentsKit/Components/Alert/Helpers/AlertButtonsOrientationCalculator.swift new file mode 100644 index 00000000..3312394f --- /dev/null +++ b/Sources/ComponentsKit/Components/Alert/Helpers/AlertButtonsOrientationCalculator.swift @@ -0,0 +1,47 @@ +import UIKit + +struct AlertButtonsOrientationCalculator { + enum Orientation { + case vertical + case horizontal + } + + private static let primaryButton = UKButton() + private static let secondaryButton = UKButton() + + private init() {} + + static func preferredOrientation(model: AlertVM) -> Orientation { + guard let primaryButtonVM = model.primaryButtonVM, + let secondaryButtonVM = model.secondaryButtonVM else { + return .vertical + } + + self.primaryButton.model = primaryButtonVM.updating { + $0.isFullWidth = false + } + self.secondaryButton.model = secondaryButtonVM.updating { + $0.isFullWidth = false + } + + let primaryButtonWidth = self.primaryButton.intrinsicContentSize.width + let secondaryButtonWidth = self.secondaryButton.intrinsicContentSize.width + + // Since the `maxWidth` of the alert is always less than the width of the + // screen, we can assume that the width of the container is equal to this + // `maxWidth` value. + let containerWidth = model.modalVM.size.maxWidth + let availableButtonsWidth = containerWidth + - AlertVM.buttonsSpacing + - model.contentPaddings.leading + - model.contentPaddings.trailing + let availableButtonWidth = availableButtonsWidth / 2 + + if primaryButtonWidth <= availableButtonWidth, + secondaryButtonWidth <= availableButtonWidth { + return .horizontal + } else { + return .vertical + } + } +} diff --git a/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift b/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift index 63a61fb8..1f1fcc41 100644 --- a/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift +++ b/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift @@ -85,6 +85,7 @@ extension AlertVM { $0.color = model.color $0.cornerRadius = model.cornerRadius $0.style = model.style + $0.isFullWidth = true } } } @@ -96,5 +97,6 @@ extension AlertVM { $0.title = "OK" $0.color = .primary $0.style = .filled + $0.isFullWidth = true } } diff --git a/Sources/ComponentsKit/Components/Alert/SUAlert.swift b/Sources/ComponentsKit/Components/Alert/SUAlert.swift new file mode 100644 index 00000000..517612c9 --- /dev/null +++ b/Sources/ComponentsKit/Components/Alert/SUAlert.swift @@ -0,0 +1,147 @@ +import SwiftUI + +extension View { + /// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons. + /// + /// All actions in an alert dismiss the alert after the action runs. If no actions are present, a standard “OK” action is included. + /// + /// - Parameters: + /// - isPresented: A binding that determines whether the alert is presented. + /// - model: A model that defines the appearance properties for an alert. + /// - primaryAction: An optional closure executed when the primary button is tapped. + /// - secondaryAction: An optional closure executed when the secondary button is tapped. + /// - onDismiss: An optional closure executed when the alert is dismissed. + /// + /// - Example: + /// ```swift + /// SomeView() + /// .suAlert( + /// isPresented: $isAlertPresented, + /// model: .init { alertVM in + /// alertVM.title = "My Alert" + /// alertVM.message = "This is an alert." + /// alertVM.primaryButton = .init { buttonVM in + /// buttonVM.title = "OK" + /// buttonVM.color = .primary + /// buttonVM.style = .filled + /// } + /// alertVM.secondaryButton = .init { buttonVM in + /// buttonVM.title = "Cancel" + /// buttonVM.style = .light + /// } + /// }, + /// primaryAction: { + /// NSLog("Primary button tapped") + /// }, + /// secondaryAction: { + /// NSLog("Secondary button tapped") + /// }, + /// onDismiss: { + /// print("Alert dismissed") + /// } + /// ) + /// ``` + public func suAlert( + isPresented: Binding, + model: AlertVM, + primaryAction: (() -> Void)? = nil, + secondaryAction: (() -> Void)? = nil, + onDismiss: (() -> Void)? = nil + ) -> some View { + return self.modal( + isVisible: isPresented, + transitionDuration: model.transition.value, + onDismiss: onDismiss, + content: { + SUCenterModal( + isVisible: isPresented, + model: model.modalVM, + header: { + if model.message.isNotNil, + let title = model.title { + AlertTitle(text: title) + } + }, + body: { + if let message = model.message { + AlertMessage(text: message) + } else if let title = model.title { + AlertTitle(text: title) + } + }, + footer: { + switch AlertButtonsOrientationCalculator.preferredOrientation(model: model) { + case .horizontal: + HStack(spacing: AlertVM.buttonsSpacing) { + AlertButton( + isAlertPresented: isPresented, + model: model.secondaryButtonVM, + action: secondaryAction + ) + AlertButton( + isAlertPresented: isPresented, + model: model.primaryButtonVM, + action: primaryAction + ) + } + case .vertical: + VStack(spacing: AlertVM.buttonsSpacing) { + AlertButton( + isAlertPresented: isPresented, + model: model.primaryButtonVM, + action: primaryAction + ) + AlertButton( + isAlertPresented: isPresented, + model: model.secondaryButtonVM, + action: secondaryAction + ) + } + } + } + ) + } + ) + } +} + +// MARK: - Helpers + +private struct AlertTitle: View { + let text: String + + var body: some View { + Text(self.text) + .font(UniversalFont.mdHeadline.font) + .foregroundStyle(UniversalColor.foreground.color) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } +} + +private struct AlertMessage: View { + let text: String + + var body: some View { + Text(self.text) + .font(UniversalFont.mdBody.font) + .foregroundStyle(UniversalColor.secondaryForeground.color) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } +} + +private struct AlertButton: View { + @Binding var isAlertPresented: Bool + let model: ButtonVM? + let action: (() -> Void)? + + var body: some View { + if let model { + SUButton(model: model) { + self.action?() + self.isAlertPresented = false + } + } + } +} diff --git a/Sources/ComponentsKit/Components/Alert/UKAlertController.swift b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift index 3bec03d7..beae4539 100644 --- a/Sources/ComponentsKit/Components/Alert/UKAlertController.swift +++ b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift @@ -138,30 +138,12 @@ public class UKAlertController: UKCenterModalController { super.updateViewConstraints() if self.buttonsStackView.arrangedSubviews.count == 2 { - self.buttonsStackView.axis = .vertical - let primaryButtonWidth = self.primaryButton.intrinsicContentSize.width - let secondaryButtonWidth = self.secondaryButton.intrinsicContentSize.width - - // Since the `maxWidth` of the alert is always less than the width of the - // screen, we can assume that the width of the container is equal to this - // `maxWidth` value. - let containerWidth = self.model.size.maxWidth - let availableButtonsWidth = containerWidth - - AlertVM.buttonsSpacing - - self.model.contentPaddings.leading - - self.model.contentPaddings.trailing - let availableButtonWidth = availableButtonsWidth / 2 - - if primaryButtonWidth <= availableButtonWidth, - secondaryButtonWidth <= availableButtonWidth { + switch AlertButtonsOrientationCalculator.preferredOrientation(model: self.alertVM) { + case .horizontal: self.buttonsStackView.removeArrangedSubview(self.secondaryButton) self.buttonsStackView.insertArrangedSubview(self.secondaryButton, at: 0) - self.buttonsStackView.axis = .horizontal - } else { - self.buttonsStackView.removeArrangedSubview(self.secondaryButton) - self.buttonsStackView.insertArrangedSubview(self.secondaryButton, at: 1) - + case .vertical: self.buttonsStackView.axis = .vertical } } else { diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift index 6d5280a5..dd7c65d2 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift @@ -54,7 +54,7 @@ struct SUCenterModal: View { extension View { /// A SwiftUI view modifier that presents a center-aligned modal. /// - /// This modifier allows you to attach a cetner modal to any SwiftUI view, providing a structured way to display modals + /// This modifier allows you to attach a center modal to any SwiftUI view, providing a structured way to display modals /// with a header, body, and footer, all styled and laid out according to the provided `CenterModalVM` model. /// /// - Parameters: