Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
2 changes: 2 additions & 0 deletions Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ extension AlertVM {
$0.color = model.color
$0.cornerRadius = model.cornerRadius
$0.style = model.style
$0.isFullWidth = true
}
}
}
Expand All @@ -96,5 +97,6 @@ extension AlertVM {
$0.title = "OK"
$0.color = .primary
$0.style = .filled
$0.isFullWidth = true
}
}
147 changes: 147 additions & 0 deletions Sources/ComponentsKit/Components/Alert/SUAlert.swift
Original file line number Diff line number Diff line change
@@ -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<Bool>,
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
}
}
}
}
24 changes: 3 additions & 21 deletions Sources/ComponentsKit/Components/Alert/UKAlertController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct SUCenterModal<Header: View, Body: View, Footer: View>: 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:
Expand Down
Loading