From c04f44042d1344d92d354fde99872250e7670329 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 10 Mar 2025 15:38:34 +0100 Subject: [PATCH 1/4] fix: modal not being presented on app launch --- .../Modal/SwiftUI/Helpers/ModalPresentationModifier.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift index 3eb02ad4..04be9a05 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift @@ -23,6 +23,11 @@ struct ModalPresentationModifier: ViewModifier { func body(content: Content) -> some View { content + .onAppear { + if self.isContentVisible { + self.isPresented = true + } + } .onChange(of: self.isContentVisible) { newValue in if newValue { self.isPresented = true From 18f53ba6b6f9f172794ee998cb3e0d2ac943b2b5 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 10 Mar 2025 17:46:48 +0100 Subject: [PATCH 2/4] change api of modals: calculate model with item --- .../Helpers/ModalPresentationModifier.swift | 4 ++-- .../ModalPresentationWithItemModifier.swift | 18 ++++++++++++------ .../Modal/SwiftUI/SUBottomModal.swift | 12 ++++++------ .../Modal/SwiftUI/SUCenterModal.swift | 12 ++++++------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift index 04be9a05..9b0eebc7 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift @@ -28,8 +28,8 @@ struct ModalPresentationModifier: ViewModifier { self.isPresented = true } } - .onChange(of: self.isContentVisible) { newValue in - if newValue { + .onChange(of: self.isContentVisible) { isVisible in + if isVisible { self.isPresented = true } else { DispatchQueue.main.asyncAfter(deadline: .now() + self.transitionDuration) { diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift index 31e7cdc8..0ad9a283 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift @@ -6,12 +6,12 @@ struct ModalPresentationWithItemModifier: ViewM @ViewBuilder var content: (Item) -> Modal - let transitionDuration: TimeInterval + let transitionDuration: (Item) -> TimeInterval let onDismiss: (() -> Void)? init( item: Binding, - transitionDuration: TimeInterval, + transitionDuration: @escaping (Item) -> TimeInterval, onDismiss: (() -> Void)?, @ViewBuilder content: @escaping (Item) -> Modal ) { @@ -23,11 +23,17 @@ struct ModalPresentationWithItemModifier: 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 } } @@ -49,7 +55,7 @@ struct ModalPresentationWithItemModifier: ViewM extension View { func modal( item: Binding, - transitionDuration: TimeInterval, + transitionDuration: @escaping (Item) -> TimeInterval, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Item) -> Modal ) -> some View { diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift index 000a1466..8323bc39 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift @@ -195,7 +195,7 @@ extension View { /// } /// .bottomModal( /// item: $selectedItem, - /// model: BottomModalVM(), + /// model: { _ in BottomModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -218,7 +218,7 @@ extension View { /// ``` public func bottomModal( item: Binding, - model: BottomModalVM = .init(), + model: @escaping (Item) -> BottomModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder header: @escaping (Item) -> Header, @ViewBuilder body: @escaping (Item) -> Body, @@ -226,7 +226,7 @@ extension View { ) -> some View { return self.modal( item: item, - transitionDuration: model.transition.value, + transitionDuration: { model($0).transition.value }, onDismiss: onDismiss, content: { unwrappedItem in SUBottomModal( @@ -242,7 +242,7 @@ extension View { } } ), - model: model, + model: model(unwrappedItem), header: { header(unwrappedItem) }, body: { body(unwrappedItem) }, footer: { footer(unwrappedItem) } @@ -289,7 +289,7 @@ extension View { /// } /// .bottomModal( /// item: $selectedItem, - /// model: BottomModalVM(), + /// model: { _ in BottomModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -302,7 +302,7 @@ extension View { /// ``` public func bottomModal( item: Binding, - model: BottomModalVM = .init(), + model: @escaping (Item) -> BottomModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder body: @escaping (Item) -> Body ) -> some View { diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift index dd7c65d2..610a4933 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift @@ -157,7 +157,7 @@ extension View { /// } /// .centerModal( /// item: $selectedItem, - /// model: CenterModalVM(), + /// model: { _ in CenterModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -180,7 +180,7 @@ extension View { /// ``` public func centerModal( item: Binding, - model: CenterModalVM = .init(), + model: @escaping (Item) -> CenterModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder header: @escaping (Item) -> Header, @ViewBuilder body: @escaping (Item) -> Body, @@ -188,7 +188,7 @@ extension View { ) -> some View { return self.modal( item: item, - transitionDuration: model.transition.value, + transitionDuration: { model($0).transition.value }, onDismiss: onDismiss, content: { unwrappedItem in SUCenterModal( @@ -204,7 +204,7 @@ extension View { } } ), - model: model, + model: model(unwrappedItem), header: { header(unwrappedItem) }, body: { body(unwrappedItem) }, footer: { footer(unwrappedItem) } @@ -251,7 +251,7 @@ extension View { /// } /// .centerModal( /// item: $selectedItem, - /// model: CenterModalVM(), + /// model: { _ in CenterModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -264,7 +264,7 @@ extension View { /// ``` public func centerModal( item: Binding, - model: CenterModalVM = .init(), + model: @escaping (Item) -> CenterModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder body: @escaping (Item) -> Body ) -> some View { From dc8e9b333e938db1c7c87a1a68b11f9e77d28922 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 10 Mar 2025 18:22:08 +0100 Subject: [PATCH 3/4] added new api to present `suAlert` with an item --- .../Components/Alert/SUAlert.swift | 296 +++++++++++++----- 1 file changed, 212 insertions(+), 84 deletions(-) diff --git a/Sources/ComponentsKit/Components/Alert/SUAlert.swift b/Sources/ComponentsKit/Components/Alert/SUAlert.swift index 517612c9..645510b5 100644 --- a/Sources/ComponentsKit/Components/Alert/SUAlert.swift +++ b/Sources/ComponentsKit/Components/Alert/SUAlert.swift @@ -1,5 +1,133 @@ 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: - 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 + } + } + } +} + +// MARK: - Presentation Helpers + extension View { /// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons. /// @@ -53,95 +181,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: Binding, + 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) } + ) } - } + ) } } From e43d0641afac60c2d0e773ce715e67ad87bfaa29 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 11 Mar 2025 12:23:11 +0100 Subject: [PATCH 4/4] remove unused helpers --- .../Components/Alert/SUAlert.swift | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/Sources/ComponentsKit/Components/Alert/SUAlert.swift b/Sources/ComponentsKit/Components/Alert/SUAlert.swift index 645510b5..741a8ba6 100644 --- a/Sources/ComponentsKit/Components/Alert/SUAlert.swift +++ b/Sources/ComponentsKit/Components/Alert/SUAlert.swift @@ -85,47 +85,6 @@ struct AlertContent: View { } } -// 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 - } - } - } -} - // MARK: - Presentation Helpers extension View {