diff --git a/DesignSystemApp/Assets.xcassets/AccentColor.colorset/Contents.json b/DesignSystemApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/DesignSystemApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DesignSystemApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/DesignSystemApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/DesignSystemApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DesignSystemApp/Assets.xcassets/Contents.json b/DesignSystemApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DesignSystemApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DesignSystemApp/ContentView.swift b/DesignSystemApp/ContentView.swift new file mode 100644 index 0000000..0b939af --- /dev/null +++ b/DesignSystemApp/ContentView.swift @@ -0,0 +1,26 @@ +// +// ContentView.swift +// DesignSystemApp +// +// Created by 홍승현 on 3/24/24. +// + +import DesignSystem +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Button("Toast!") { + Toast.shared.present(title: "Airpods Pro", symbol: "airpodspro", isUserInteractionEnabled: true) + } + } + .padding() + } +} + +#Preview { + RootView { + ContentView() + } +} diff --git a/DesignSystemApp/DesignSystemApp.swift b/DesignSystemApp/DesignSystemApp.swift new file mode 100644 index 0000000..66b96c7 --- /dev/null +++ b/DesignSystemApp/DesignSystemApp.swift @@ -0,0 +1,20 @@ +// +// DesignSystemApp.swift +// DesignSystemApp +// +// Created by 홍승현 on 3/24/24. +// + +import DesignSystem +import SwiftUI + +@main +struct DesignSystemApp: App { + var body: some Scene { + WindowGroup { + RootView { + ContentView() + } + } + } +} diff --git a/DesignSystemApp/Preview Content/Preview Assets.xcassets/Contents.json b/DesignSystemApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DesignSystemApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PyeonHaeng-iOS.xcodeproj/project.pbxproj b/PyeonHaeng-iOS.xcodeproj/project.pbxproj index 609a226..bd6151b 100644 --- a/PyeonHaeng-iOS.xcodeproj/project.pbxproj +++ b/PyeonHaeng-iOS.xcodeproj/project.pbxproj @@ -43,6 +43,11 @@ BAA4D9AF2B5A1795005999F8 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA4D9AE2B5A1795005999F8 /* SplashView.swift */; }; BAA4D9B12B5A1796005999F8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BAA4D9B02B5A1796005999F8 /* Assets.xcassets */; }; BAA4D9B42B5A1796005999F8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BAA4D9B32B5A1796005999F8 /* Preview Assets.xcassets */; }; + BAAF1D292BAFF1910001EA36 /* DesignSystemApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAF1D282BAFF1910001EA36 /* DesignSystemApp.swift */; }; + BAAF1D2B2BAFF1910001EA36 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAF1D2A2BAFF1910001EA36 /* ContentView.swift */; }; + BAAF1D2D2BAFF1920001EA36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BAAF1D2C2BAFF1920001EA36 /* Assets.xcassets */; }; + BAAF1D302BAFF1920001EA36 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BAAF1D2F2BAFF1920001EA36 /* Preview Assets.xcassets */; }; + BAAF1D362BAFFA340001EA36 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = BAAF1D352BAFFA340001EA36 /* DesignSystem */; }; BAB569612B639F3000D1E0F9 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = BAB569602B639F3000D1E0F9 /* DesignSystem */; }; BAB5CF252B6B7C5A008B24BF /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB5CF242B6B7C5A008B24BF /* Services.swift */; }; BAB5CF272B6B7CF3008B24BF /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB5CF262B6B7CF3008B24BF /* HomeViewModel.swift */; }; @@ -122,6 +127,11 @@ BAA4D9AE2B5A1795005999F8 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; BAA4D9B02B5A1796005999F8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; BAA4D9B32B5A1796005999F8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + BAAF1D262BAFF1910001EA36 /* DesignSystemApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DesignSystemApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BAAF1D282BAFF1910001EA36 /* DesignSystemApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignSystemApp.swift; sourceTree = ""; }; + BAAF1D2A2BAFF1910001EA36 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + BAAF1D2C2BAFF1920001EA36 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BAAF1D2F2BAFF1920001EA36 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; BAB5CF242B6B7C5A008B24BF /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = ""; }; BAB5CF262B6B7CF3008B24BF /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; BAB720332B9325F200C2CA1A /* PromotionSelectBottomSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionSelectBottomSheetView.swift; sourceTree = ""; }; @@ -175,6 +185,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BAAF1D232BAFF1910001EA36 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAF1D362BAFFA340001EA36 /* DesignSystem in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -379,6 +397,7 @@ BABFEA6F2B6399C30084C0EC /* Shared */, BA0564032B6219D4003D6DC7 /* APIService */, BA0564022B62179A003D6DC7 /* Core */, + BAAF1D272BAFF1910001EA36 /* DesignSystemApp */, BAA4D9AA2B5A1795005999F8 /* Products */, BAA4D9AB2B5A1795005999F8 /* PyeonHaeng-iOS */, BA28F17A2B6155450052855E /* PyeonHaeng-iOSTests */, @@ -392,6 +411,7 @@ children = ( BAA4D9A92B5A1795005999F8 /* PyeonHaeng-iOS.app */, BA28F1792B6155450052855E /* PyeonHaeng-iOSTests.xctest */, + BAAF1D262BAFF1910001EA36 /* DesignSystemApp.app */, ); name = Products; sourceTree = ""; @@ -434,6 +454,25 @@ path = View; sourceTree = ""; }; + BAAF1D272BAFF1910001EA36 /* DesignSystemApp */ = { + isa = PBXGroup; + children = ( + BAAF1D282BAFF1910001EA36 /* DesignSystemApp.swift */, + BAAF1D2A2BAFF1910001EA36 /* ContentView.swift */, + BAAF1D2C2BAFF1920001EA36 /* Assets.xcassets */, + BAAF1D2E2BAFF1920001EA36 /* Preview Content */, + ); + path = DesignSystemApp; + sourceTree = ""; + }; + BAAF1D2E2BAFF1920001EA36 /* Preview Content */ = { + isa = PBXGroup; + children = ( + BAAF1D2F2BAFF1920001EA36 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; BAB8C3AB2BAC7A09003DF3CC /* LeaveReview */ = { isa = PBXGroup; children = ( @@ -514,6 +553,26 @@ productReference = BAA4D9A92B5A1795005999F8 /* PyeonHaeng-iOS.app */; productType = "com.apple.product-type.application"; }; + BAAF1D252BAFF1910001EA36 /* DesignSystemApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = BAAF1D342BAFF1920001EA36 /* Build configuration list for PBXNativeTarget "DesignSystemApp" */; + buildPhases = ( + BAAF1D222BAFF1910001EA36 /* Sources */, + BAAF1D232BAFF1910001EA36 /* Frameworks */, + BAAF1D242BAFF1910001EA36 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DesignSystemApp; + packageProductDependencies = ( + BAAF1D352BAFFA340001EA36 /* DesignSystem */, + ); + productName = DesignSystemApp; + productReference = BAAF1D262BAFF1910001EA36 /* DesignSystemApp.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -521,7 +580,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 1530; LastUpgradeCheck = 1520; TargetAttributes = { BA28F1782B6155450052855E = { @@ -531,6 +590,9 @@ BAA4D9A82B5A1795005999F8 = { CreatedOnToolsVersion = 15.2; }; + BAAF1D252BAFF1910001EA36 = { + CreatedOnToolsVersion = 15.3; + }; }; }; buildConfigurationList = BAA4D9A42B5A1795005999F8 /* Build configuration list for PBXProject "PyeonHaeng-iOS" */; @@ -550,6 +612,7 @@ targets = ( BAA4D9A82B5A1795005999F8 /* PyeonHaeng-iOS */, BA28F1782B6155450052855E /* PyeonHaeng-iOSTests */, + BAAF1D252BAFF1910001EA36 /* DesignSystemApp */, ); }; /* End PBXProject section */ @@ -583,6 +646,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BAAF1D242BAFF1910001EA36 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAF1D302BAFF1920001EA36 /* Preview Assets.xcassets in Resources */, + BAAF1D2D2BAFF1920001EA36 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -680,6 +752,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BAAF1D222BAFF1910001EA36 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAF1D2B2BAFF1910001EA36 /* ContentView.swift in Sources */, + BAAF1D292BAFF1910001EA36 /* DesignSystemApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1036,6 +1117,96 @@ }; name = Release; }; + BAAF1D312BAFF1920001EA36 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"DesignSystemApp/Preview Content\""; + DEVELOPMENT_TEAM = 2ZQR76M3UH; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pyeonhaeng.DesignSystemApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BAAF1D322BAFF1920001EA36 /* Staging */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"DesignSystemApp/Preview Content\""; + DEVELOPMENT_TEAM = 2ZQR76M3UH; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pyeonhaeng.DesignSystemApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Staging; + }; + BAAF1D332BAFF1920001EA36 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"DesignSystemApp/Preview Content\""; + DEVELOPMENT_TEAM = 2ZQR76M3UH; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pyeonhaeng.DesignSystemApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1069,6 +1240,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + BAAF1D342BAFF1920001EA36 /* Build configuration list for PBXNativeTarget "DesignSystemApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BAAF1D312BAFF1920001EA36 /* Debug */, + BAAF1D322BAFF1920001EA36 /* Staging */, + BAAF1D332BAFF1920001EA36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1100,6 +1281,10 @@ isa = XCSwiftPackageProductDependency; productName = NoticeAPISupport; }; + BAAF1D352BAFFA340001EA36 /* DesignSystem */ = { + isa = XCSwiftPackageProductDependency; + productName = DesignSystem; + }; BAB569602B639F3000D1E0F9 /* DesignSystem */ = { isa = XCSwiftPackageProductDependency; productName = DesignSystem; diff --git a/Shared/Sources/DesignSystem/Components/Toast/Toast.swift b/Shared/Sources/DesignSystem/Components/Toast/Toast.swift new file mode 100644 index 0000000..1fa4a2c --- /dev/null +++ b/Shared/Sources/DesignSystem/Components/Toast/Toast.swift @@ -0,0 +1,35 @@ +// +// Toast.swift +// +// +// Created by 홍승현 on 3/24/24. +// + +import SwiftUI + +@Observable +public final class Toast { + /// Since the class conforms to the Observable protocol, + /// we can use this singleton object as a state object to receive UI updates on the overlay window root controller + public static let shared = Toast() + var toasts: [ToastItem] = [] + + /// 토스트를 띄웁니다. + /// - Parameters: + /// - title: 토스트 제목 + /// - symbol: 토스트에 들어갈 `SF Symbol` 이미지 문자열 값 + /// - tint: 토스트의 틴트 색상 + /// - isUserInteractionEnabled: 사용자의 상호작용 여부, 스스로 지우거나 못하도록 설정할 수 있습니다. + /// - duration: 토스트 유지 시간 + public func present( + title: String, + symbol: String?, + tint: Color = .gray900, + isUserInteractionEnabled: Bool = false, + duration: ToastTime = .medium + ) { + withAnimation(.snappy) { + toasts.append(.init(title: title, symbol: symbol, tint: tint, isUseInteractionEnabled: isUserInteractionEnabled, duration: duration)) + } + } +} diff --git a/Shared/Sources/DesignSystem/Components/Toast/ToastItem.swift b/Shared/Sources/DesignSystem/Components/Toast/ToastItem.swift new file mode 100644 index 0000000..cdacd0a --- /dev/null +++ b/Shared/Sources/DesignSystem/Components/Toast/ToastItem.swift @@ -0,0 +1,49 @@ +// +// ToastItem.swift +// +// +// Created by 홍승현 on 3/24/24. +// + +import SwiftUI + +// MARK: - ToastItem + +struct ToastItem: Identifiable { + let id: UUID = .init() + + // MARK: Custom Properties + + /// 토스트 제목 + let title: String + + /// 토스트 심볼 이미지 문자열 + let symbol: String? + + /// 토스트 틴트 색상 + let tint: Color + + /// 사용자 상호작용 여부 + let isUserInteractionEnabled: Bool + + // MARK: Timing + + /// 토스트 유지 시간 + let duration: ToastTime + + init(title: String, symbol: String?, tint: Color, isUseInteractionEnabled: Bool, duration: ToastTime) { + self.title = title + self.symbol = symbol + self.tint = tint + isUserInteractionEnabled = isUseInteractionEnabled + self.duration = duration + } +} + +// MARK: - ToastTime + +public enum ToastTime: CGFloat { + case short = 1.0 + case medium = 2.0 + case long = 3.5 +} diff --git a/Shared/Sources/DesignSystem/Components/Toast/View/RootView.swift b/Shared/Sources/DesignSystem/Components/Toast/View/RootView.swift new file mode 100644 index 0000000..19c0c8d --- /dev/null +++ b/Shared/Sources/DesignSystem/Components/Toast/View/RootView.swift @@ -0,0 +1,56 @@ +// +// RootView.swift +// +// +// Created by 홍승현 on 3/24/24. +// + +import SwiftUI + +// MARK: - RootView + +public struct RootView: View { + @ViewBuilder public var content: Content + + @inlinable public init(@ViewBuilder content: @escaping () -> Content) { + self.content = content() + } + + // MARK: View Properties + + @State private var overlayWindow: UIWindow? + + public var body: some View { + content + .onAppear { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + overlayWindow == nil + else { + return + } + + let window = PassthroughWindow(windowScene: windowScene) + window.backgroundColor = .clear + window.isUserInteractionEnabled = true + window.isHidden = false + + // view controller part + + let rootController = UIHostingController(rootView: ToastGroup()) + rootController.view.frame = windowScene.screen.bounds + rootController.view.backgroundColor = .clear + window.rootViewController = rootController + + overlayWindow = window + } + } +} + +// MARK: - PassthroughWindow + +private class PassthroughWindow: UIWindow { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let view = super.hitTest(point, with: event) else { return nil } + return rootViewController?.view == view ? nil : view + } +} diff --git a/Shared/Sources/DesignSystem/Components/Toast/View/ToastGroup.swift b/Shared/Sources/DesignSystem/Components/Toast/View/ToastGroup.swift new file mode 100644 index 0000000..9bf76c5 --- /dev/null +++ b/Shared/Sources/DesignSystem/Components/Toast/View/ToastGroup.swift @@ -0,0 +1,43 @@ +// +// ToastGroup.swift +// +// +// Created by 홍승현 on 3/24/24. +// + +import SwiftUI + +struct ToastGroup: View { + let model = Toast.shared + + var body: some View { + GeometryReader { proxy in + let size = proxy.size + let safeArea = proxy.safeAreaInsets + + ZStack { + ForEach(model.toasts) { toast in + ToastView(size: size, item: toast) + .scaleEffect(scale(toast)) + .offset(y: offsetY(toast)) + .zIndex(Double(model.toasts.firstIndex(where: { $0.id == toast.id }) ?? 0)) + } + } + .padding(.bottom, safeArea.top == .zero ? 15 : 10) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .offset(y: -100) + } + } + + private func offsetY(_ item: ToastItem) -> CGFloat { + let index = model.toasts.firstIndex(where: { $0.id == item.id }) ?? 0 + let totalCount = model.toasts.count - 1 + return totalCount - index >= 2 ? -20 : CGFloat(totalCount - index) * -10 + } + + private func scale(_ item: ToastItem) -> CGFloat { + let index = model.toasts.firstIndex(where: { $0.id == item.id }) ?? 0 + let totalCount = model.toasts.count - 1 + return 1.0 - (totalCount - index >= 2 ? 0.2 : CGFloat(totalCount - index) * 0.1) + } +} diff --git a/Shared/Sources/DesignSystem/Components/Toast/View/ToastView.swift b/Shared/Sources/DesignSystem/Components/Toast/View/ToastView.swift new file mode 100644 index 0000000..459b5a8 --- /dev/null +++ b/Shared/Sources/DesignSystem/Components/Toast/View/ToastView.swift @@ -0,0 +1,79 @@ +// +// ToastView.swift +// +// +// Created by 홍승현 on 3/24/24. +// + +import SwiftUI + +struct ToastView: View { + private let size: CGSize + private let item: ToastItem + + init(size: CGSize, item: ToastItem) { + self.size = size + self.item = item + } + + // MARK: View Properties + + @State private var delayTask: DispatchWorkItem? + + var body: some View { + HStack(spacing: 0) { + if let symbol = item.symbol { + Image(systemName: symbol) + .font(.title3) + .padding(.trailing, 10) + } + + Text(item.title) + .lineLimit(1) + } + .foregroundStyle(item.tint) + .padding(.horizontal, 15) + .padding(.vertical, 8) + .background( + .background + .shadow(.drop(color: .primary.opacity(0.06), radius: 5, x: 5, y: 5)) + .shadow(.drop(color: .primary.opacity(0.06), radius: 5, x: -5, y: -5)), + in: .capsule + ) + .contentShape(.capsule) + .gesture( + DragGesture(minimumDistance: 0) + .onEnded { value in + guard item.isUserInteractionEnabled else { return } + let endY = value.translation.height + let velocityY = value.velocity.height + + if (endY + velocityY) > 100 { + // Removing Toast + removeToast() + } + } + ) + .onAppear { + guard delayTask == nil else { return } + delayTask = .init { + removeToast() + } + + if let delayTask { + DispatchQueue.main.asyncAfter(deadline: .now() + item.duration.rawValue, execute: delayTask) + } + } + // Limiting Size + .frame(maxWidth: size.width * 0.7) + .transition(.offset(y: 250)) + } + + private func removeToast() { + delayTask?.cancel() + + withAnimation(.snappy) { + Toast.shared.toasts.removeAll(where: { $0.id == item.id }) + } + } +}