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
255 changes: 171 additions & 84 deletions Sources/ComponentsKit/Components/Alert/SUAlert.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,92 @@
import SwiftUI

struct AlertContent: View {
@Binding var isPresented: Bool
let model: AlertVM
let primaryAction: (() -> Void)?
let secondaryAction: (() -> Void)?

var body: some View {
SUCenterModal(
isVisible: self.$isPresented,
model: self.model.modalVM,
header: {
if self.model.message.isNotNil,
let text = self.model.title {
self.title(text)
}
},
body: {
if let text = self.model.message {
self.message(text)
} else if let text = self.model.title {
self.title(text)
}
},
footer: {
switch AlertButtonsOrientationCalculator.preferredOrientation(model: model) {
case .horizontal:
HStack(spacing: AlertVM.buttonsSpacing) {
self.button(
model: self.model.secondaryButtonVM,
action: self.secondaryAction
)
self.button(
model: self.model.primaryButtonVM,
action: self.primaryAction
)
}
case .vertical:
VStack(spacing: AlertVM.buttonsSpacing) {
self.button(
model: self.model.primaryButtonVM,
action: self.primaryAction
)
self.button(
model: self.model.secondaryButtonVM,
action: self.secondaryAction
)
}
}
}
)
}

// MARK: - Helpers

func title(_ text: String) -> some View {
Text(text)
.font(UniversalFont.mdHeadline.font)
.foregroundStyle(UniversalColor.foreground.color)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}

func message(_ text: String) -> some View {
Text(text)
.font(UniversalFont.mdBody.font)
.foregroundStyle(UniversalColor.secondaryForeground.color)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}

func button(
model: ButtonVM?,
action: (() -> Void)?
) -> some View {
Group {
if let model {
SUButton(model: model) {
action?()
self.isPresented = false
}
}
}
}
}

// MARK: - Presentation Helpers

extension View {
/// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons.
///
Expand Down Expand Up @@ -53,95 +140,95 @@ extension View {
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
)
}
}
}
AlertContent(
isPresented: isPresented,
model: model,
primaryAction: primaryAction,
secondaryAction: 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
/// 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.
/// - item: A binding to an optional `Item` that determines whether the alert is presented.
/// When `item` is `nil`, the alert is hidden.
/// - 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
/// struct ContentView: View {
/// struct AlertData: Identifiable {
/// var id: String {
/// return text
/// }
/// let text: String
/// }
///
/// @State private var selectedItem: AlertData?
/// private let items: [AlertData] = [
/// AlertData(text: "data 1"),
/// AlertData(text: "data 2")
/// ]
///
/// var body: some View {
/// List(items) { item in
/// Button("Show Alert") {
/// selectedItem = item
/// }
/// }
/// .suAlert(
/// item: $selectedItem,
/// model: { data in
/// return AlertVM {
/// $0.title = "Data Preview"
/// $0.message = data.text
/// }
/// },
/// onDismiss: {
/// print("Alert dismissed")
/// }
/// )
/// }
/// }
/// ```
public func suAlert<Item: Identifiable>(
item: Binding<Item?>,
model: @escaping (Item) -> AlertVM,
primaryAction: ((Item) -> Void)? = nil,
secondaryAction: ((Item) -> Void)? = nil,
onDismiss: (() -> Void)? = nil
) -> some View {
return self.modal(
item: item,
transitionDuration: { model($0).transition.value },
onDismiss: onDismiss,
content: { unwrappedItem in
AlertContent(
isPresented: .init(
get: {
return item.wrappedValue.isNotNil
},
set: { isPresented in
if isPresented {
item.wrappedValue = unwrappedItem
} else {
item.wrappedValue = nil
}
}
),
model: model(unwrappedItem),
primaryAction: { primaryAction?(unwrappedItem) },
secondaryAction: { secondaryAction?(unwrappedItem) }
)
}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ struct ModalPresentationModifier<Modal: View>: ViewModifier {

func body(content: Content) -> some View {
content
.onChange(of: self.isContentVisible) { newValue in
if newValue {
.onAppear {
if self.isContentVisible {
self.isPresented = true
}
}
.onChange(of: self.isContentVisible) { isVisible in
if isVisible {
self.isPresented = true
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + self.transitionDuration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ struct ModalPresentationWithItemModifier<Modal: View, Item: Identifiable>: ViewM

@ViewBuilder var content: (Item) -> Modal

let transitionDuration: TimeInterval
let transitionDuration: (Item) -> TimeInterval
let onDismiss: (() -> Void)?

init(
item: Binding<Item?>,
transitionDuration: TimeInterval,
transitionDuration: @escaping (Item) -> TimeInterval,
onDismiss: (() -> Void)?,
@ViewBuilder content: @escaping (Item) -> Modal
) {
Expand All @@ -23,11 +23,17 @@ struct ModalPresentationWithItemModifier<Modal: View, Item: Identifiable>: ViewM

func body(content: Content) -> some View {
content
.onChange(of: self.visibleItem.isNotNil) { newValue in
if newValue {
.onAppear {
self.presentedItem = self.visibleItem
}
.onChange(of: self.visibleItem.isNotNil) { isVisible in
if isVisible {
self.presentedItem = self.visibleItem
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + self.transitionDuration) {
let duration = self.presentedItem.map { item in
self.transitionDuration(item)
} ?? 0.3
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
self.presentedItem = self.visibleItem
}
}
Expand All @@ -49,7 +55,7 @@ struct ModalPresentationWithItemModifier<Modal: View, Item: Identifiable>: ViewM
extension View {
func modal<Modal: View, Item: Identifiable>(
item: Binding<Item?>,
transitionDuration: TimeInterval,
transitionDuration: @escaping (Item) -> TimeInterval,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping (Item) -> Modal
) -> some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ extension View {
/// }
/// .bottomModal(
/// item: $selectedItem,
/// model: BottomModalVM(),
/// model: { _ in BottomModalVM() },
/// onDismiss: {
/// print("Modal dismissed")
/// },
Expand All @@ -218,15 +218,15 @@ extension View {
/// ```
public func bottomModal<Item: Identifiable, Header: View, Body: View, Footer: View>(
item: Binding<Item?>,
model: BottomModalVM = .init(),
model: @escaping (Item) -> BottomModalVM = { _ in .init() },
onDismiss: (() -> Void)? = nil,
@ViewBuilder header: @escaping (Item) -> Header,
@ViewBuilder body: @escaping (Item) -> Body,
@ViewBuilder footer: @escaping (Item) -> Footer
) -> some View {
return self.modal(
item: item,
transitionDuration: model.transition.value,
transitionDuration: { model($0).transition.value },
onDismiss: onDismiss,
content: { unwrappedItem in
SUBottomModal(
Expand All @@ -242,7 +242,7 @@ extension View {
}
}
),
model: model,
model: model(unwrappedItem),
header: { header(unwrappedItem) },
body: { body(unwrappedItem) },
footer: { footer(unwrappedItem) }
Expand Down Expand Up @@ -289,7 +289,7 @@ extension View {
/// }
/// .bottomModal(
/// item: $selectedItem,
/// model: BottomModalVM(),
/// model: { _ in BottomModalVM() },
/// onDismiss: {
/// print("Modal dismissed")
/// },
Expand All @@ -302,7 +302,7 @@ extension View {
/// ```
public func bottomModal<Item: Identifiable, Body: View>(
item: Binding<Item?>,
model: BottomModalVM = .init(),
model: @escaping (Item) -> BottomModalVM = { _ in .init() },
onDismiss: (() -> Void)? = nil,
@ViewBuilder body: @escaping (Item) -> Body
) -> some View {
Expand Down
Loading