From 1e543c2c92f25e466270c170679861da577f4573 Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Tue, 11 Feb 2025 14:18:30 +0200 Subject: [PATCH 1/6] feat: Add AlertToast [ignore-commit-lint] --- .../HorizonUI.AlertToast.Storybook.swift | 65 +++++++++ .../HorizonUI.AlertToast.Style.swift | 51 +++++++ .../AlertToast/HorizonUI.AlertToast.swift | 130 ++++++++++++++++++ .../Sources/HorizonUI/Sources/Storybook.swift | 6 + 4 files changed, 252 insertions(+) create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift new file mode 100644 index 0000000000..748fd054a5 --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift @@ -0,0 +1,65 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +public extension HorizonUI.AlertToast { + struct Storybook: View { + public var body: some View { + ScrollView { + HorizonUI.AlertToast( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .info, + isShowCancelButton: false + ) + + HorizonUI.AlertToast( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .success, + isShowCancelButton: true) { print("Cancel Tapped") } + + HorizonUI.AlertToast( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .warning, + isShowCancelButton: true, + buttons: .solid(title: "Button")) { + print("Cancel Tapped") + } onTapSolidButton: { + print("on Tap Solid Button") + } + + HorizonUI.AlertToast( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .error, + isShowCancelButton: false, + buttons: .group(defaultTitle: "NO", solidTitle: "Yes"), onTapDefaultButton: { + print("on Tap Default Button") + }, onTapSolidButton: { + print("on Tap Solid Button") + }) + } + .padding() + .navigationTitle("Alert Toast") + } + } +} + +#Preview { + HorizonUI.AlertToast.Storybook() +} + diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift new file mode 100644 index 0000000000..8efcd5eb3f --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift @@ -0,0 +1,51 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +public extension HorizonUI.AlertToast { + enum Style { + case info + case success + case warning + case error + + var color: Color { + switch self { + case .info: HorizonUI.colors.surface.attention + case .success: HorizonUI.colors.surface.success + case .warning: HorizonUI.colors.surface.warning + case .error: HorizonUI.colors.surface.error + } + } + + var image: Image { + switch self { + case .info: HorizonUI.icons.info + case .success: HorizonUI.icons.check + case .warning: HorizonUI.icons.warning + case .error: HorizonUI.icons.error + } + } + } + + enum Buttons { + case solid(title: String) + case group(defaultTitle: String, solidTitle: String) + } +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift new file mode 100644 index 0000000000..820aa919e5 --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift @@ -0,0 +1,130 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +public extension HorizonUI { + struct AlertToast: View { + // MARK: - Properties + + private let cornerRadius = CornerRadius.level3 + + // MARK: - Dependencies + + private let text: String + private let style: AlertToast.Style + private let isShowCancelButton: Bool + private let buttons: AlertToast.Buttons? + private let onTapCancel: (() -> Void)? + private let onTapDefaultButton: (() -> Void)? + private let onTapSolidButton: (() -> Void)? + + init( + text: String, + style: AlertToast.Style, + isShowCancelButton: Bool = true, + buttons: AlertToast.Buttons? = nil, + onTapCancel: (() -> Void)? = nil, + onTapDefaultButton: (() -> Void)? = nil, + onTapSolidButton: (() -> Void)? = nil + ) { + self.text = text + self.style = style + self.isShowCancelButton = isShowCancelButton + self.buttons = buttons + self.onTapCancel = onTapCancel + self.onTapDefaultButton = onTapDefaultButton + self.onTapSolidButton = onTapSolidButton + } + + public var body: some View { + HStack(alignment: .top, spacing: .zero) { + alertIcon + VStack(alignment: .leading, spacing: .zero) { + textView + .padding(.huiSpaces.primitives.mediumSmall) + groupButtons + .padding(.bottom, .huiSpaces.primitives.mediumSmall) + } + trailingButtons + .padding(.top,.huiSpaces.primitives.mediumSmall) + } + + .frame(minHeight: 64) + .huiBorder(level: .level2, color: style.color, radius: cornerRadius.attributes.radius) + .huiCornerRadius(level: cornerRadius) + .fixedSize(horizontal: false, vertical: true) + } + + private var alertIcon: some View { + Rectangle() + .fill(style.color) + .frame(width: 50) + .overlay { + style.image + .foregroundStyle(Color.huiColors.icon.surfaceColored) + } + } + + private var textView: some View { + Text(text) + .foregroundStyle(Color.huiColors.text.body) + .huiTypography(.p1) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var trailingButtons: some View { + HStack(spacing: .huiSpaces.primitives.mediumSmall) { + if case .solid(title: let title) = buttons { + HorizonUI.PrimaryButton(title, type: .black) { + onTapSolidButton?() + } + } + if isShowCancelButton { + HorizonUI.IconButton( HorizonUI.icons.close, type: .white) { + onTapCancel?() + } + .padding(.trailing, .huiSpaces.primitives.mediumSmall) + } + } + } + + @ViewBuilder + private var groupButtons: some View { + if case let .group(defaultTitle, solidTitle) = buttons { + HStack { + HorizonUI.PrimaryButton(defaultTitle, type: .white) { + onTapDefaultButton?() + } + + HorizonUI.PrimaryButton(solidTitle, type: .black) { + onTapSolidButton?() + } + } + } + } + } +} + +#Preview { + HorizonUI.AlertToast( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .warning + ) + .padding(5) +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift index f46b59b956..6106abce31 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift @@ -128,6 +128,12 @@ public struct Storybook: View { } label: { Text("Intro Block").tint(Color.black) } + + NavigationLink { + HorizonUI.AlertToast.Storybook() + } label: { + Text("Alert Toast").tint(Color.black) + } } } .listStyle(.sidebar) From 8c0e18302992fde33caa19352bd2bb8e21c4f418 Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Tue, 11 Feb 2025 22:23:45 +0200 Subject: [PATCH 2/6] feat: make toast dynamic show and hide --- .../View/AlertToast/AlertToastModifier.swift | 52 +++++++++++ .../AlertToastStorybookViewModel.swift | 87 +++++++++++++++++++ .../View/AlertToast/AlertToastViewModel.swift | 53 +++++++++++ .../HorizonUI.AlertToast.Storybook.swift | 65 -------------- .../HorizonUI.AlertToast.Style.swift | 48 ++++++++++ .../AlertToast/HorizonUI.AlertToast.swift | 51 ++++------- .../Sources/HorizonUI/Sources/Storybook.swift | 6 -- 7 files changed, 256 insertions(+), 106 deletions(-) create mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift create mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift create mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift delete mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift new file mode 100644 index 0000000000..d8ec515497 --- /dev/null +++ b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift @@ -0,0 +1,52 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI +import HorizonUI + +struct AlertToastModifier: ViewModifier { + @ObservedObject var viewModel: AlertToastViewModel + + func body(content: Content) -> some View { + content.overlay(alignment: viewModel.model?.direction.alignment ?? .bottom) { + ZStack(alignment: .top) { + if let model = viewModel.model { + HorizonUI.AlertToast(model: model) { + viewModel.dismiss() + } + .transition( + .move(edge: viewModel.model?.direction.edge ?? .bottom) + .combined(with: .opacity) + ) + } + } + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: viewModel.model?.direction.alignment ?? .bottom + ) + .animation(.easeInOut(duration: 0.25), value: viewModel.isShowToast) + } + } +} + +extension View { + func alertToast(viewModel: AlertToastViewModel) -> some View { + modifier(AlertToastModifier(viewModel: viewModel)) + } +} diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift new file mode 100644 index 0000000000..bfea1d4b38 --- /dev/null +++ b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift @@ -0,0 +1,87 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import Foundation +import Observation +import HorizonUI + +@Observable +final class AlertToastStorybookViewModel { + private(set) var alertToastViewModel = AlertToastViewModel() + + func showErrorToast() { + let model = HorizonUI.AlertToast.Model( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .error, + isShowCancelButton: true, + direction: .bottom, + dismissAfter: 2, + buttons: nil + ) + + alertToastViewModel.show(alertModel: model) + } + + func showSuccessToast() { + var model = HorizonUI.AlertToast.Model( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .success, + isShowCancelButton: true, + direction: .top, + buttons: .solid(title: "Yes Bro") + ) + + model.onTapSolidButton = { + print("onTapSolidButton") + } + + alertToastViewModel.show(alertModel: model) + } + + func showWarningToast() { + var model = HorizonUI.AlertToast.Model( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .warning, + isShowCancelButton: false, + direction: .bottom, + dismissAfter: 5, + buttons: .group(defaultTitle: "NO", solidTitle: "Thanks") + ) + + model.onTapDefaultButton = { + print("onTapDefaultButton") + } + + model.onTapSolidButton = { + print("onTapSolidButton") + } + + alertToastViewModel.show(alertModel: model) + } + + func showInfoToast() { + let model = HorizonUI.AlertToast.Model( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum.", + style: .info, + isShowCancelButton: true, + direction: .top + ) + + alertToastViewModel.show(alertModel: model) + } +} diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift new file mode 100644 index 0000000000..249906865b --- /dev/null +++ b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift @@ -0,0 +1,53 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import HorizonUI +import Foundation +import CombineSchedulers +import Combine + +final class AlertToastViewModel: ObservableObject { + // MARK: - Output + + @Published private(set) var model: HorizonUI.AlertToast.Model? + @Published private(set) var isShowToast = false + + private var scheduledDisappearance: Cancellable? + private let scheduler: AnySchedulerOf + + init(scheduler: AnySchedulerOf = .main) { + self.scheduler = scheduler + } + + func show(alertModel: HorizonUI.AlertToast.Model) { + model = alertModel + isShowToast = true + scheduledDisappearance = scheduler.schedule( + after: scheduler.now.advanced(by: .init(floatLiteral: alertModel.dismissAfter)), + interval: .zero + ) { [weak self] in + self?.dismiss() + } + } + + func dismiss() { + model = nil + isShowToast = false + scheduledDisappearance = nil + } +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift deleted file mode 100644 index 748fd054a5..0000000000 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// This file is part of Canvas. -// Copyright (C) 2025-present Instructure, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -import SwiftUI - -public extension HorizonUI.AlertToast { - struct Storybook: View { - public var body: some View { - ScrollView { - HorizonUI.AlertToast( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .info, - isShowCancelButton: false - ) - - HorizonUI.AlertToast( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .success, - isShowCancelButton: true) { print("Cancel Tapped") } - - HorizonUI.AlertToast( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .warning, - isShowCancelButton: true, - buttons: .solid(title: "Button")) { - print("Cancel Tapped") - } onTapSolidButton: { - print("on Tap Solid Button") - } - - HorizonUI.AlertToast( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .error, - isShowCancelButton: false, - buttons: .group(defaultTitle: "NO", solidTitle: "Yes"), onTapDefaultButton: { - print("on Tap Default Button") - }, onTapSolidButton: { - print("on Tap Solid Button") - }) - } - .padding() - .navigationTitle("Alert Toast") - } - } -} - -#Preview { - HorizonUI.AlertToast.Storybook() -} - diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift index 8efcd5eb3f..bb3cde8862 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift @@ -49,3 +49,51 @@ public extension HorizonUI.AlertToast { case group(defaultTitle: String, solidTitle: String) } } + +public extension HorizonUI.AlertToast { + struct Model { + let text: String + let style: HorizonUI.AlertToast.Style + let isShowCancelButton: Bool + let buttons: HorizonUI.AlertToast.Buttons? + public let direction: Direction + public let dismissAfter: Double + public var onTapDefaultButton: (() -> Void)? + public var onTapSolidButton: (() -> Void)? + + public init( + text: String, + style: HorizonUI.AlertToast.Style, + isShowCancelButton: Bool = true, + direction: Direction = .bottom, + dismissAfter: Double = 2.0, + buttons: HorizonUI.AlertToast.Buttons? = nil + ) { + self.text = text + self.style = style + self.isShowCancelButton = isShowCancelButton + self.direction = direction + self.dismissAfter = dismissAfter + self.buttons = buttons + } + } + + enum Direction { + case top + case bottom + + public var alignment: Alignment { + switch self { + case .top: return .top + case .bottom: return .bottom + } + } + + public var edge: Edge { + switch self { + case .top: return .top + case .bottom: return .bottom + } + } + } +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift index 820aa919e5..6b187730ce 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift @@ -26,30 +26,15 @@ public extension HorizonUI { // MARK: - Dependencies - private let text: String - private let style: AlertToast.Style - private let isShowCancelButton: Bool - private let buttons: AlertToast.Buttons? + private let model: AlertToast.Model private let onTapCancel: (() -> Void)? - private let onTapDefaultButton: (() -> Void)? - private let onTapSolidButton: (() -> Void)? - init( - text: String, - style: AlertToast.Style, - isShowCancelButton: Bool = true, - buttons: AlertToast.Buttons? = nil, - onTapCancel: (() -> Void)? = nil, - onTapDefaultButton: (() -> Void)? = nil, - onTapSolidButton: (() -> Void)? = nil + public init( + model: AlertToast.Model, + onTapCancel: (() -> Void)? = nil ) { - self.text = text - self.style = style - self.isShowCancelButton = isShowCancelButton - self.buttons = buttons + self.model = model self.onTapCancel = onTapCancel - self.onTapDefaultButton = onTapDefaultButton - self.onTapSolidButton = onTapSolidButton } public var body: some View { @@ -64,25 +49,24 @@ public extension HorizonUI { trailingButtons .padding(.top,.huiSpaces.primitives.mediumSmall) } - .frame(minHeight: 64) - .huiBorder(level: .level2, color: style.color, radius: cornerRadius.attributes.radius) + .huiBorder(level: .level2, color: model.style.color, radius: cornerRadius.attributes.radius) .huiCornerRadius(level: cornerRadius) .fixedSize(horizontal: false, vertical: true) } private var alertIcon: some View { Rectangle() - .fill(style.color) + .fill(model.style.color) .frame(width: 50) .overlay { - style.image + model.style.image .foregroundStyle(Color.huiColors.icon.surfaceColored) } } private var textView: some View { - Text(text) + Text(model.text) .foregroundStyle(Color.huiColors.text.body) .huiTypography(.p1) .frame(maxWidth: .infinity, alignment: .leading) @@ -90,12 +74,12 @@ public extension HorizonUI { private var trailingButtons: some View { HStack(spacing: .huiSpaces.primitives.mediumSmall) { - if case .solid(title: let title) = buttons { + if case .solid(title: let title) = model.buttons { HorizonUI.PrimaryButton(title, type: .black) { - onTapSolidButton?() + model.onTapSolidButton?() } } - if isShowCancelButton { + if model.isShowCancelButton { HorizonUI.IconButton( HorizonUI.icons.close, type: .white) { onTapCancel?() } @@ -106,14 +90,14 @@ public extension HorizonUI { @ViewBuilder private var groupButtons: some View { - if case let .group(defaultTitle, solidTitle) = buttons { + if case let .group(defaultTitle, solidTitle) = model.buttons { HStack { HorizonUI.PrimaryButton(defaultTitle, type: .white) { - onTapDefaultButton?() + model.onTapDefaultButton?() } HorizonUI.PrimaryButton(solidTitle, type: .black) { - onTapSolidButton?() + model.onTapSolidButton?() } } } @@ -122,9 +106,6 @@ public extension HorizonUI { } #Preview { - HorizonUI.AlertToast( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .warning - ) + HorizonUI.AlertToast(model: .init(text: "Alert Toast", style: .info)) .padding(5) } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift index 6106abce31..f46b59b956 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift @@ -128,12 +128,6 @@ public struct Storybook: View { } label: { Text("Intro Block").tint(Color.black) } - - NavigationLink { - HorizonUI.AlertToast.Storybook() - } label: { - Text("Alert Toast").tint(Color.black) - } } } .listStyle(.sidebar) From 8e3e8bc4263b89387c9ff3331b7ee44aea894149 Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Wed, 12 Feb 2025 13:35:08 +0200 Subject: [PATCH 3/6] Add dynamic show hide toast --- .../View/AlertToast/AlertToastStorybook.swift | 56 +++++++++++++++++++ .../Features/HorizonTabBarController.swift | 9 +-- 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift new file mode 100644 index 0000000000..ca856fb5ca --- /dev/null +++ b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift @@ -0,0 +1,56 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +struct AlertToastStorybook: View { + let viewModel = AlertToastStorybookViewModel() + var body: some View { + VStack { + Button { + viewModel.showInfoToast() + } label: { + Text(verbatim: "Show Info Alert") + } + + Button { + viewModel.showErrorToast() + } label: { + Text(verbatim: "Show Error Alert") + } + + Button { + viewModel.showSuccessToast() + } label: { + Text(verbatim: "Show Success Alert") + } + Button { + viewModel.showWarningToast() + } label: { + Text(verbatim: "Show Warning Alert") + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .alertToast(viewModel: viewModel.alertToastViewModel) + .padding(16) + } +} + +#Preview { + AlertToastStorybook() +} diff --git a/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift b/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift index bd0d69c05f..c6c021f75d 100644 --- a/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift +++ b/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift @@ -102,10 +102,11 @@ final class HorizonTabBarController: UITabBarController, UITabBarControllerDeleg } private func careerTab() -> UIViewController { - let vc = CoreNavigationController( - rootViewController: CoreHostingController(Storybook()) - ) - vc.navigationBar.prefersLargeTitles = true +// let vc = CoreNavigationController( +// rootViewController: ) +// ) + let vc = CoreHostingController(AlertToastStorybook()) +// vc.navigationBar.prefersLargeTitles = false vc.tabBarItem.title = String(localized: "Skillspace", bundle: .horizon) vc.tabBarItem.image = getHorizonImage(name: "hub") vc.tabBarItem.selectedImage = getHorizonImage(name: "hub_filled") From b46182d880b61198ad451216e446a08c7c250500 Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Wed, 12 Feb 2025 16:15:17 +0200 Subject: [PATCH 4/6] Addressed code review --- .../View/AlertToast/AlertToastModifier.swift | 52 ----------- .../View/AlertToast/AlertToastStorybook.swift | 56 ------------ .../AlertToastStorybookViewModel.swift | 87 ------------------- .../View/AlertToast/AlertToastViewModel.swift | 53 ----------- .../Features/HorizonTabBarController.swift | 9 +- ...onUI.AlertToast.Storybook.ViewModel .swift | 80 +++++++++++++++++ .../HorizonUI.AlertToast.Storybook.swift | 71 +++++++++++++++ .../HorizonUI.AlertToast.Style.swift | 2 +- .../AlertToast/HorizonUI.AlertToast.swift | 29 ++++--- .../HorizonUI.AlertToastModifier.swift | 64 ++++++++++++++ .../Sources/HorizonUI/Sources/Storybook.swift | 6 ++ 11 files changed, 241 insertions(+), 268 deletions(-) delete mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift delete mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift delete mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift delete mode 100644 Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToastModifier.swift diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift deleted file mode 100644 index d8ec515497..0000000000 --- a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastModifier.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// This file is part of Canvas. -// Copyright (C) 2025-present Instructure, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -import SwiftUI -import HorizonUI - -struct AlertToastModifier: ViewModifier { - @ObservedObject var viewModel: AlertToastViewModel - - func body(content: Content) -> some View { - content.overlay(alignment: viewModel.model?.direction.alignment ?? .bottom) { - ZStack(alignment: .top) { - if let model = viewModel.model { - HorizonUI.AlertToast(model: model) { - viewModel.dismiss() - } - .transition( - .move(edge: viewModel.model?.direction.edge ?? .bottom) - .combined(with: .opacity) - ) - } - } - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: viewModel.model?.direction.alignment ?? .bottom - ) - .animation(.easeInOut(duration: 0.25), value: viewModel.isShowToast) - } - } -} - -extension View { - func alertToast(viewModel: AlertToastViewModel) -> some View { - modifier(AlertToastModifier(viewModel: viewModel)) - } -} diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift deleted file mode 100644 index ca856fb5ca..0000000000 --- a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybook.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// This file is part of Canvas. -// Copyright (C) 2025-present Instructure, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -import SwiftUI - -struct AlertToastStorybook: View { - let viewModel = AlertToastStorybookViewModel() - var body: some View { - VStack { - Button { - viewModel.showInfoToast() - } label: { - Text(verbatim: "Show Info Alert") - } - - Button { - viewModel.showErrorToast() - } label: { - Text(verbatim: "Show Error Alert") - } - - Button { - viewModel.showSuccessToast() - } label: { - Text(verbatim: "Show Success Alert") - } - Button { - viewModel.showWarningToast() - } label: { - Text(verbatim: "Show Warning Alert") - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .alertToast(viewModel: viewModel.alertToastViewModel) - .padding(16) - } -} - -#Preview { - AlertToastStorybook() -} diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift deleted file mode 100644 index bfea1d4b38..0000000000 --- a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastStorybookViewModel.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// This file is part of Canvas. -// Copyright (C) 2025-present Instructure, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -import Foundation -import Observation -import HorizonUI - -@Observable -final class AlertToastStorybookViewModel { - private(set) var alertToastViewModel = AlertToastViewModel() - - func showErrorToast() { - let model = HorizonUI.AlertToast.Model( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .error, - isShowCancelButton: true, - direction: .bottom, - dismissAfter: 2, - buttons: nil - ) - - alertToastViewModel.show(alertModel: model) - } - - func showSuccessToast() { - var model = HorizonUI.AlertToast.Model( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .success, - isShowCancelButton: true, - direction: .top, - buttons: .solid(title: "Yes Bro") - ) - - model.onTapSolidButton = { - print("onTapSolidButton") - } - - alertToastViewModel.show(alertModel: model) - } - - func showWarningToast() { - var model = HorizonUI.AlertToast.Model( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", - style: .warning, - isShowCancelButton: false, - direction: .bottom, - dismissAfter: 5, - buttons: .group(defaultTitle: "NO", solidTitle: "Thanks") - ) - - model.onTapDefaultButton = { - print("onTapDefaultButton") - } - - model.onTapSolidButton = { - print("onTapSolidButton") - } - - alertToastViewModel.show(alertModel: model) - } - - func showInfoToast() { - let model = HorizonUI.AlertToast.Model( - text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum.", - style: .info, - isShowCancelButton: true, - direction: .top - ) - - alertToastViewModel.show(alertModel: model) - } -} diff --git a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift b/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift deleted file mode 100644 index 249906865b..0000000000 --- a/Horizon/Horizon/Sources/Common/View/AlertToast/AlertToastViewModel.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// This file is part of Canvas. -// Copyright (C) 2025-present Instructure, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -import HorizonUI -import Foundation -import CombineSchedulers -import Combine - -final class AlertToastViewModel: ObservableObject { - // MARK: - Output - - @Published private(set) var model: HorizonUI.AlertToast.Model? - @Published private(set) var isShowToast = false - - private var scheduledDisappearance: Cancellable? - private let scheduler: AnySchedulerOf - - init(scheduler: AnySchedulerOf = .main) { - self.scheduler = scheduler - } - - func show(alertModel: HorizonUI.AlertToast.Model) { - model = alertModel - isShowToast = true - scheduledDisappearance = scheduler.schedule( - after: scheduler.now.advanced(by: .init(floatLiteral: alertModel.dismissAfter)), - interval: .zero - ) { [weak self] in - self?.dismiss() - } - } - - func dismiss() { - model = nil - isShowToast = false - scheduledDisappearance = nil - } -} diff --git a/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift b/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift index c6c021f75d..bd0d69c05f 100644 --- a/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift +++ b/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift @@ -102,11 +102,10 @@ final class HorizonTabBarController: UITabBarController, UITabBarControllerDeleg } private func careerTab() -> UIViewController { -// let vc = CoreNavigationController( -// rootViewController: ) -// ) - let vc = CoreHostingController(AlertToastStorybook()) -// vc.navigationBar.prefersLargeTitles = false + let vc = CoreNavigationController( + rootViewController: CoreHostingController(Storybook()) + ) + vc.navigationBar.prefersLargeTitles = true vc.tabBarItem.title = String(localized: "Skillspace", bundle: .horizon) vc.tabBarItem.image = getHorizonImage(name: "hub") vc.tabBarItem.selectedImage = getHorizonImage(name: "hub_filled") diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift new file mode 100644 index 0000000000..88cbd7b727 --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift @@ -0,0 +1,80 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import Foundation +import Observation + +extension HorizonUI.AlertToast { + @Observable + final class AlertToastStorybookViewModel { + var toastViewModel = HorizonUI.AlertToast.ViewModel(text: "", style: .info) + + func showErrorToast() { + toastViewModel = HorizonUI.AlertToast.ViewModel( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .error, + isShowCancelButton: true, + direction: .bottom, + dismissAfter: 2, + buttons: nil + ) + } + + func showSuccessToast() { + toastViewModel = HorizonUI.AlertToast.ViewModel( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .success, + isShowCancelButton: true, + direction: .top, + buttons: .solid(title: "Yes Bro") + ) + + toastViewModel.onTapSolidButton = { + print("onTapSolidButton") + } + } + + func showWarningToast() { + toastViewModel = HorizonUI.AlertToast.ViewModel( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", + style: .warning, + isShowCancelButton: false, + direction: .bottom, + dismissAfter: 15, + buttons: .group(defaultTitle: "NO", solidTitle: "Thanks") + ) + + toastViewModel.onTapDefaultButton = { + print("onTapDefaultButton") + } + + toastViewModel.onTapSolidButton = { + print("onTapSolidButton") + } + } + + func showInfoToast() { + toastViewModel = HorizonUI.AlertToast.ViewModel( + text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum.", + style: .info, + isShowCancelButton: true, + direction: .top + ) + } + } +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift new file mode 100644 index 0000000000..973f9e8b6e --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift @@ -0,0 +1,71 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +public extension HorizonUI.AlertToast { + struct Storybook: View { + let viewModel = AlertToastStorybookViewModel() + @State var isShowToast: Bool = false + + public var body: some View { + VStack { + Button { + viewModel.showInfoToast() + isShowToast = true + } label: { + Text(verbatim: "Show Info Alert") + } + + Button { + viewModel.showErrorToast() + isShowToast = true + } label: { + Text(verbatim: "Show Error Alert") + } + + Button { + viewModel.showSuccessToast() + isShowToast = true + } label: { + Text(verbatim: "Show Success Alert") + } + Button { + viewModel.showWarningToast() + isShowToast = true + } label: { + Text(verbatim: "Show Warning Alert") + } + + Button { + isShowToast = false + } label: { + Text(verbatim: "Dismiss") + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .alertToast(viewModel: viewModel.toastViewModel, isShowToast: $isShowToast) + .padding(16) + .navigationTitle("Alert Toast") + } + } +} + +#Preview { + HorizonUI.AlertToast.Storybook() +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift index bb3cde8862..3c2ed7f046 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift @@ -51,7 +51,7 @@ public extension HorizonUI.AlertToast { } public extension HorizonUI.AlertToast { - struct Model { + struct ViewModel { let text: String let style: HorizonUI.AlertToast.Style let isShowCancelButton: Bool diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift index 6b187730ce..c4d3c48975 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift @@ -26,14 +26,14 @@ public extension HorizonUI { // MARK: - Dependencies - private let model: AlertToast.Model + private let viewModel: AlertToast.ViewModel private let onTapCancel: (() -> Void)? public init( - model: AlertToast.Model, + viewModel: AlertToast.ViewModel, onTapCancel: (() -> Void)? = nil ) { - self.model = model + self.viewModel = viewModel self.onTapCancel = onTapCancel } @@ -50,23 +50,23 @@ public extension HorizonUI { .padding(.top,.huiSpaces.primitives.mediumSmall) } .frame(minHeight: 64) - .huiBorder(level: .level2, color: model.style.color, radius: cornerRadius.attributes.radius) + .huiBorder(level: .level2, color: viewModel.style.color, radius: cornerRadius.attributes.radius) .huiCornerRadius(level: cornerRadius) .fixedSize(horizontal: false, vertical: true) } private var alertIcon: some View { Rectangle() - .fill(model.style.color) + .fill(viewModel.style.color) .frame(width: 50) .overlay { - model.style.image + viewModel.style.image .foregroundStyle(Color.huiColors.icon.surfaceColored) } } private var textView: some View { - Text(model.text) + Text(viewModel.text) .foregroundStyle(Color.huiColors.text.body) .huiTypography(.p1) .frame(maxWidth: .infinity, alignment: .leading) @@ -74,12 +74,12 @@ public extension HorizonUI { private var trailingButtons: some View { HStack(spacing: .huiSpaces.primitives.mediumSmall) { - if case .solid(title: let title) = model.buttons { + if case .solid(title: let title) = viewModel.buttons { HorizonUI.PrimaryButton(title, type: .black) { - model.onTapSolidButton?() + viewModel.onTapSolidButton?() } } - if model.isShowCancelButton { + if viewModel.isShowCancelButton { HorizonUI.IconButton( HorizonUI.icons.close, type: .white) { onTapCancel?() } @@ -90,14 +90,14 @@ public extension HorizonUI { @ViewBuilder private var groupButtons: some View { - if case let .group(defaultTitle, solidTitle) = model.buttons { + if case let .group(defaultTitle, solidTitle) = viewModel.buttons { HStack { HorizonUI.PrimaryButton(defaultTitle, type: .white) { - model.onTapDefaultButton?() + viewModel.onTapDefaultButton?() } HorizonUI.PrimaryButton(solidTitle, type: .black) { - model.onTapSolidButton?() + viewModel.onTapSolidButton?() } } } @@ -106,6 +106,7 @@ public extension HorizonUI { } #Preview { - HorizonUI.AlertToast(model: .init(text: "Alert Toast", style: .info)) + HorizonUI.AlertToast(viewModel: .init(text: "Alert Toast", style: .info)) .padding(5) } + diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToastModifier.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToastModifier.swift new file mode 100644 index 0000000000..7b03ed9f48 --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToastModifier.swift @@ -0,0 +1,64 @@ +// +// This file is part of Canvas. +// Copyright (C) 2025-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +extension HorizonUI.AlertToast { + struct AlertToastModifier: ViewModifier { + let viewModel: HorizonUI.AlertToast.ViewModel + @Binding var isShowToast: Bool + + func body(content: Content) -> some View { + content.overlay(alignment: viewModel.direction.alignment) { + ZStack(alignment: .top) { + if isShowToast { + HorizonUI.AlertToast(viewModel: viewModel) { + isShowToast = false + } + .transition( + .move(edge: viewModel.direction.edge) + .combined(with: .opacity) + ) + .onAppear { dismissToast() } + } + } + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: viewModel.direction.alignment + ) + .animation(.easeInOut(duration: 0.25), value: isShowToast) + } + } + + private func dismissToast() { + DispatchQueue.main.asyncAfter(deadline: .now() + viewModel.dismissAfter) { + isShowToast = false + } + } + } +} + +extension View { + public func alertToast( + viewModel: HorizonUI.AlertToast.ViewModel, + isShowToast: Binding + ) -> some View { + modifier(HorizonUI.AlertToast.AlertToastModifier(viewModel: viewModel, isShowToast: isShowToast)) + } +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift index f46b59b956..6106abce31 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift @@ -128,6 +128,12 @@ public struct Storybook: View { } label: { Text("Intro Block").tint(Color.black) } + + NavigationLink { + HorizonUI.AlertToast.Storybook() + } label: { + Text("Alert Toast").tint(Color.black) + } } } .listStyle(.sidebar) From 655bd0aa17415a162530de1eda90f425433eff89 Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Wed, 12 Feb 2025 17:47:13 +0200 Subject: [PATCH 5/6] Addressed code reivew --- ...orizonUI.Toast.Storybook.ViewModel .swift} | 36 ++++---- .../HorizonUI.Toast.Storybook.swift} | 18 ++-- .../HorizonUI.Toast.Style.swift} | 54 +---------- .../HorizonUI.Toast.swift} | 92 +++++++++++++++---- .../HorizonUI.ToastViewModifier.swift} | 26 +++--- .../Sources/HorizonUI/Sources/Storybook.swift | 2 +- 6 files changed, 118 insertions(+), 110 deletions(-) rename packages/HorizonUI/Sources/HorizonUI/Sources/Components/{AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift => Toast/HorizonUI.Toast.Storybook.ViewModel .swift} (74%) rename packages/HorizonUI/Sources/HorizonUI/Sources/Components/{AlertToast/HorizonUI.AlertToast.Storybook.swift => Toast/HorizonUI.Toast.Storybook.swift} (82%) rename packages/HorizonUI/Sources/HorizonUI/Sources/Components/{AlertToast/HorizonUI.AlertToast.Style.swift => Toast/HorizonUI.Toast.Style.swift} (50%) rename packages/HorizonUI/Sources/HorizonUI/Sources/Components/{AlertToast/HorizonUI.AlertToast.swift => Toast/HorizonUI.Toast.swift} (55%) rename packages/HorizonUI/Sources/HorizonUI/Sources/Components/{AlertToast/HorizonUI.AlertToastModifier.swift => Toast/HorizonUI.ToastViewModifier.swift} (74%) diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.ViewModel .swift similarity index 74% rename from packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift rename to packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.ViewModel .swift index 2a31f2c349..2ba940efd6 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.ViewModel .swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.ViewModel .swift @@ -19,13 +19,13 @@ import Foundation import Observation -extension HorizonUI.AlertToast { +extension HorizonUI.Toast { @Observable final class StorybookViewModel { - var toastViewModel = HorizonUI.AlertToast.ViewModel(text: "", style: .info) + var toastViewModel = HorizonUI.Toast.ViewModel(text: "", style: .info) func showErrorToast() { - toastViewModel = HorizonUI.AlertToast.ViewModel( + toastViewModel = HorizonUI.Toast.ViewModel( text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", style: .error, isShowCancelButton: true, @@ -36,40 +36,38 @@ extension HorizonUI.AlertToast { } func showSuccessToast() { - toastViewModel = HorizonUI.AlertToast.ViewModel( + let solidButton = HorizonUI.Toast.ButtonAttribute(title: "Yes Now") { + print("onTapSolidButton") + } + toastViewModel = HorizonUI.Toast.ViewModel( text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", style: .success, isShowCancelButton: true, direction: .top, - buttons: .solid(title: "Yes Bro") + buttons: .solid(button: solidButton) ) - toastViewModel.onTapSolidButton = { - print("onTapSolidButton") - } } func showWarningToast() { - toastViewModel = HorizonUI.AlertToast.ViewModel( + let defaultButton = HorizonUI.Toast.ButtonAttribute(title: "no") { + print("defaultButton") + } + let solidButton = HorizonUI.Toast.ButtonAttribute(title: "Yes Now") { + print("onTapSolidButton") + } + toastViewModel = HorizonUI.Toast.ViewModel( text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", style: .warning, isShowCancelButton: false, direction: .bottom, dismissAfter: 15, - buttons: .group(defaultTitle: "NO", solidTitle: "Thanks") + buttons: .group(defaultButton: defaultButton, solidButton: solidButton) ) - - toastViewModel.onTapDefaultButton = { - print("onTapDefaultButton") - } - - toastViewModel.onTapSolidButton = { - print("onTapSolidButton") - } } func showInfoToast() { - toastViewModel = HorizonUI.AlertToast.ViewModel( + toastViewModel = HorizonUI.Toast.ViewModel( text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.Nunc ut lacus ac libero ultrices vestibulum.", style: .info, isShowCancelButton: true, diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.swift similarity index 82% rename from packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift rename to packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.swift index 2db452aae7..7379d1ff29 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.swift @@ -18,48 +18,48 @@ import SwiftUI -public extension HorizonUI.AlertToast { +public extension HorizonUI.Toast { struct Storybook: View { let viewModel = StorybookViewModel() - @State var isShowToast: Bool = false + @State var isPresented: Bool = false public var body: some View { VStack { Button { viewModel.showInfoToast() - isShowToast = true + isPresented = true } label: { Text(verbatim: "Show Info Alert") } Button { viewModel.showErrorToast() - isShowToast = true + isPresented = true } label: { Text(verbatim: "Show Error Alert") } Button { viewModel.showSuccessToast() - isShowToast = true + isPresented = true } label: { Text(verbatim: "Show Success Alert") } Button { viewModel.showWarningToast() - isShowToast = true + isPresented = true } label: { Text(verbatim: "Show Warning Alert") } Button { - isShowToast = false + isPresented = false } label: { Text(verbatim: "Dismiss") } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .alertToast(viewModel: viewModel.toastViewModel, isShowToast: $isShowToast) + .huiToast(viewModel: viewModel.toastViewModel, isPresented: $isPresented) .padding(16) .navigationTitle("Alert Toast") } @@ -67,5 +67,5 @@ public extension HorizonUI.AlertToast { } #Preview { - HorizonUI.AlertToast.Storybook() + HorizonUI.Toast.Storybook() } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Style.swift similarity index 50% rename from packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift rename to packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Style.swift index 3c2ed7f046..341e441b97 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.Style.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Style.swift @@ -18,7 +18,7 @@ import SwiftUI -public extension HorizonUI.AlertToast { +public extension HorizonUI.Toast { enum Style { case info case success @@ -45,55 +45,7 @@ public extension HorizonUI.AlertToast { } enum Buttons { - case solid(title: String) - case group(defaultTitle: String, solidTitle: String) - } -} - -public extension HorizonUI.AlertToast { - struct ViewModel { - let text: String - let style: HorizonUI.AlertToast.Style - let isShowCancelButton: Bool - let buttons: HorizonUI.AlertToast.Buttons? - public let direction: Direction - public let dismissAfter: Double - public var onTapDefaultButton: (() -> Void)? - public var onTapSolidButton: (() -> Void)? - - public init( - text: String, - style: HorizonUI.AlertToast.Style, - isShowCancelButton: Bool = true, - direction: Direction = .bottom, - dismissAfter: Double = 2.0, - buttons: HorizonUI.AlertToast.Buttons? = nil - ) { - self.text = text - self.style = style - self.isShowCancelButton = isShowCancelButton - self.direction = direction - self.dismissAfter = dismissAfter - self.buttons = buttons - } - } - - enum Direction { - case top - case bottom - - public var alignment: Alignment { - switch self { - case .top: return .top - case .bottom: return .bottom - } - } - - public var edge: Edge { - switch self { - case .top: return .top - case .bottom: return .bottom - } - } + case solid(button: ButtonAttribute) + case group(defaultButton: ButtonAttribute, solidButton: ButtonAttribute) } } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.swift similarity index 55% rename from packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift rename to packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.swift index c4d3c48975..47c5c1c553 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToast.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.swift @@ -19,22 +19,22 @@ import SwiftUI public extension HorizonUI { - struct AlertToast: View { + struct Toast: View { // MARK: - Properties private let cornerRadius = CornerRadius.level3 // MARK: - Dependencies - private let viewModel: AlertToast.ViewModel - private let onTapCancel: (() -> Void)? + private let viewModel: Toast.ViewModel + private let onTapDismiss: (() -> Void)? public init( - viewModel: AlertToast.ViewModel, - onTapCancel: (() -> Void)? = nil + viewModel: Toast.ViewModel, + onTapDismiss: (() -> Void)? = nil ) { self.viewModel = viewModel - self.onTapCancel = onTapCancel + self.onTapDismiss = onTapDismiss } public var body: some View { @@ -74,14 +74,14 @@ public extension HorizonUI { private var trailingButtons: some View { HStack(spacing: .huiSpaces.primitives.mediumSmall) { - if case .solid(title: let title) = viewModel.buttons { - HorizonUI.PrimaryButton(title, type: .black) { - viewModel.onTapSolidButton?() + if case .solid(button: let buttonAttribute) = viewModel.buttons { + HorizonUI.PrimaryButton(buttonAttribute.title, type: .black) { + buttonAttribute.action() } } if viewModel.isShowCancelButton { HorizonUI.IconButton( HorizonUI.icons.close, type: .white) { - onTapCancel?() + onTapDismiss?() } .padding(.trailing, .huiSpaces.primitives.mediumSmall) } @@ -90,14 +90,14 @@ public extension HorizonUI { @ViewBuilder private var groupButtons: some View { - if case let .group(defaultTitle, solidTitle) = viewModel.buttons { + if case let .group(defaultTitleAttribute, solidTitleAttribute) = viewModel.buttons { HStack { - HorizonUI.PrimaryButton(defaultTitle, type: .white) { - viewModel.onTapDefaultButton?() + HorizonUI.PrimaryButton(defaultTitleAttribute.title, type: .white) { + defaultTitleAttribute.action() } - HorizonUI.PrimaryButton(solidTitle, type: .black) { - viewModel.onTapSolidButton?() + HorizonUI.PrimaryButton(solidTitleAttribute.title, type: .black) { + solidTitleAttribute.action() } } } @@ -105,8 +105,66 @@ public extension HorizonUI { } } +public extension HorizonUI.Toast { + struct ButtonAttribute { + let title: String + let action: () -> Void + + public init( + title: String, + action: @escaping () -> Void + ) { + self.title = title + self.action = action + } + } + + struct ViewModel { + let text: String + let style: HorizonUI.Toast.Style + let isShowCancelButton: Bool + let buttons: HorizonUI.Toast.Buttons? + public let direction: Direction + public let dismissAfter: Double + + public init( + text: String, + style: HorizonUI.Toast.Style, + isShowCancelButton: Bool = true, + direction: Direction = .bottom, + dismissAfter: Double = 2.0, + buttons: HorizonUI.Toast.Buttons? = nil + ) { + self.text = text + self.style = style + self.isShowCancelButton = isShowCancelButton + self.direction = direction + self.dismissAfter = dismissAfter + self.buttons = buttons + } + } + + enum Direction { + case top + case bottom + + public var alignment: Alignment { + switch self { + case .top: return .top + case .bottom: return .bottom + } + } + + public var edge: Edge { + switch self { + case .top: return .top + case .bottom: return .bottom + } + } + } +} + #Preview { - HorizonUI.AlertToast(viewModel: .init(text: "Alert Toast", style: .info)) + HorizonUI.Toast(viewModel: .init(text: "Alert Toast", style: .info)) .padding(5) } - diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToastModifier.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.ToastViewModifier.swift similarity index 74% rename from packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToastModifier.swift rename to packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.ToastViewModifier.swift index 7b03ed9f48..1f42678acd 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/AlertToast/HorizonUI.AlertToastModifier.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.ToastViewModifier.swift @@ -18,17 +18,17 @@ import SwiftUI -extension HorizonUI.AlertToast { - struct AlertToastModifier: ViewModifier { - let viewModel: HorizonUI.AlertToast.ViewModel - @Binding var isShowToast: Bool +extension HorizonUI.Toast { + struct ToastViewModifier: ViewModifier { + let viewModel: HorizonUI.Toast.ViewModel + @Binding var isPresented: Bool func body(content: Content) -> some View { content.overlay(alignment: viewModel.direction.alignment) { ZStack(alignment: .top) { - if isShowToast { - HorizonUI.AlertToast(viewModel: viewModel) { - isShowToast = false + if isPresented { + HorizonUI.Toast(viewModel: viewModel) { + isPresented = false } .transition( .move(edge: viewModel.direction.edge) @@ -42,23 +42,23 @@ extension HorizonUI.AlertToast { maxHeight: .infinity, alignment: viewModel.direction.alignment ) - .animation(.easeInOut(duration: 0.25), value: isShowToast) + .animation(.easeInOut(duration: 0.25), value: isPresented) } } private func dismissToast() { DispatchQueue.main.asyncAfter(deadline: .now() + viewModel.dismissAfter) { - isShowToast = false + isPresented = false } } } } extension View { - public func alertToast( - viewModel: HorizonUI.AlertToast.ViewModel, - isShowToast: Binding + public func huiToast( + viewModel: HorizonUI.Toast.ViewModel, + isPresented: Binding ) -> some View { - modifier(HorizonUI.AlertToast.AlertToastModifier(viewModel: viewModel, isShowToast: isShowToast)) + modifier(HorizonUI.Toast.ToastViewModifier(viewModel: viewModel, isPresented: isPresented)) } } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift index c7cb3b7bf5..b534f17c64 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift @@ -134,7 +134,7 @@ public struct Storybook: View { Text("File Upload Sheet").tint(Color.black) } NavigationLink { - HorizonUI.AlertToast.Storybook() + HorizonUI.Toast.Storybook() } label: { Text("Alert Toast").tint(Color.black) } From 508954e2b4e8d4fba3d2bb67d1e73c6f8beaf1c8 Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Thu, 13 Feb 2025 11:33:11 +0200 Subject: [PATCH 6/6] Addressed code review --- ...HorizonUI.Toast.Storybook.ViewModel .swift | 20 +++--- .../Toast/HorizonUI.Toast.Style.swift | 4 +- .../Components/Toast/HorizonUI.Toast.swift | 64 ++++++++++--------- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.ViewModel .swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.ViewModel .swift index 2ba940efd6..9fdf545823 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.ViewModel .swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Storybook.ViewModel .swift @@ -30,31 +30,30 @@ extension HorizonUI.Toast { style: .error, isShowCancelButton: true, direction: .bottom, - dismissAfter: 2, - buttons: nil + dismissAfter: 2 ) } func showSuccessToast() { - let solidButton = HorizonUI.Toast.ButtonAttribute(title: "Yes Now") { - print("onTapSolidButton") + let confirmButton = HorizonUI.Toast.ButtonAttribute(title: "Yes Now") { + print("confirmButton") } toastViewModel = HorizonUI.Toast.ViewModel( text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", style: .success, isShowCancelButton: true, direction: .top, - buttons: .solid(button: solidButton) + confirmActionButton: confirmButton ) } func showWarningToast() { - let defaultButton = HorizonUI.Toast.ButtonAttribute(title: "no") { - print("defaultButton") + let cancelButton = HorizonUI.Toast.ButtonAttribute(title: "no") { + print("cancelButton") } - let solidButton = HorizonUI.Toast.ButtonAttribute(title: "Yes Now") { - print("onTapSolidButton") + let confirmButton = HorizonUI.Toast.ButtonAttribute(title: "Yes Now") { + print("confirmButton") } toastViewModel = HorizonUI.Toast.ViewModel( text: "Nunc ut lacus ac libero ultrices vestibulum. Integer elementum.", @@ -62,7 +61,8 @@ extension HorizonUI.Toast { isShowCancelButton: false, direction: .bottom, dismissAfter: 15, - buttons: .group(defaultButton: defaultButton, solidButton: solidButton) + confirmActionButton: confirmButton, + cancelActionButton: cancelButton ) } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Style.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Style.swift index 341e441b97..d7e547c2f2 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Style.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.Style.swift @@ -45,7 +45,7 @@ public extension HorizonUI.Toast { } enum Buttons { - case solid(button: ButtonAttribute) - case group(defaultButton: ButtonAttribute, solidButton: ButtonAttribute) + case single(confirmButton: ButtonAttribute) + case double(cancelButton: ButtonAttribute, confirmButton: ButtonAttribute) } } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.swift index 47c5c1c553..751e8ebd2c 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Toast/HorizonUI.Toast.swift @@ -21,14 +21,14 @@ import SwiftUI public extension HorizonUI { struct Toast: View { // MARK: - Properties - + private let cornerRadius = CornerRadius.level3 - + // MARK: - Dependencies - + private let viewModel: Toast.ViewModel private let onTapDismiss: (() -> Void)? - + public init( viewModel: Toast.ViewModel, onTapDismiss: (() -> Void)? = nil @@ -36,7 +36,7 @@ public extension HorizonUI { self.viewModel = viewModel self.onTapDismiss = onTapDismiss } - + public var body: some View { HStack(alignment: .top, spacing: .zero) { alertIcon @@ -54,7 +54,7 @@ public extension HorizonUI { .huiCornerRadius(level: cornerRadius) .fixedSize(horizontal: false, vertical: true) } - + private var alertIcon: some View { Rectangle() .fill(viewModel.style.color) @@ -64,19 +64,19 @@ public extension HorizonUI { .foregroundStyle(Color.huiColors.icon.surfaceColored) } } - + private var textView: some View { Text(viewModel.text) .foregroundStyle(Color.huiColors.text.body) .huiTypography(.p1) .frame(maxWidth: .infinity, alignment: .leading) } - + private var trailingButtons: some View { HStack(spacing: .huiSpaces.primitives.mediumSmall) { - if case .solid(button: let buttonAttribute) = viewModel.buttons { - HorizonUI.PrimaryButton(buttonAttribute.title, type: .black) { - buttonAttribute.action() + if case .single(confirmButton: let confirmButton) = viewModel.buttons { + HorizonUI.PrimaryButton(confirmButton.title, type: .black) { + confirmButton.action() } } if viewModel.isShowCancelButton { @@ -87,17 +87,17 @@ public extension HorizonUI { } } } - + @ViewBuilder private var groupButtons: some View { - if case let .group(defaultTitleAttribute, solidTitleAttribute) = viewModel.buttons { + if case let .double(cancelButton: cancelButton, confirmButton: confirmButton) = viewModel.buttons { HStack { - HorizonUI.PrimaryButton(defaultTitleAttribute.title, type: .white) { - defaultTitleAttribute.action() + HorizonUI.PrimaryButton(cancelButton.title, type: .white) { + cancelButton.action() } - - HorizonUI.PrimaryButton(solidTitleAttribute.title, type: .black) { - solidTitleAttribute.action() + + HorizonUI.PrimaryButton(confirmButton.title, type: .black) { + confirmButton.action() } } } @@ -109,7 +109,7 @@ public extension HorizonUI.Toast { struct ButtonAttribute { let title: String let action: () -> Void - + public init( title: String, action: @escaping () -> Void @@ -118,43 +118,49 @@ public extension HorizonUI.Toast { self.action = action } } - + struct ViewModel { let text: String let style: HorizonUI.Toast.Style let isShowCancelButton: Bool let buttons: HorizonUI.Toast.Buttons? - public let direction: Direction - public let dismissAfter: Double - + let direction: Direction + let dismissAfter: Double public init( text: String, style: HorizonUI.Toast.Style, isShowCancelButton: Bool = true, direction: Direction = .bottom, dismissAfter: Double = 2.0, - buttons: HorizonUI.Toast.Buttons? = nil + confirmActionButton: ButtonAttribute? = nil, + cancelActionButton: ButtonAttribute? = nil ) { self.text = text self.style = style self.isShowCancelButton = isShowCancelButton self.direction = direction self.dismissAfter = dismissAfter - self.buttons = buttons + if let confirmActionButton, let cancelActionButton { + buttons = .double(cancelButton: cancelActionButton, confirmButton: confirmActionButton) + } else if let confirmActionButton { + buttons = .single(confirmButton: confirmActionButton) + } else { + self.buttons = nil + } } } - + enum Direction { case top case bottom - + public var alignment: Alignment { switch self { case .top: return .top case .bottom: return .bottom } } - + public var edge: Edge { switch self { case .top: return .top @@ -166,5 +172,5 @@ public extension HorizonUI.Toast { #Preview { HorizonUI.Toast(viewModel: .init(text: "Alert Toast", style: .info)) - .padding(5) + .padding(5) }