From d3d1a9837e268d02a646ead185fa1c720fe2c005 Mon Sep 17 00:00:00 2001 From: Jeong Chong In Date: Mon, 25 Dec 2023 20:12:00 +0900 Subject: [PATCH] =?UTF-8?q?[#249]=20Toast=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20Haptic=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YDS-Essential/Source/HapticManager.swift | 29 ++++++ .../SwiftUI/Components/ToastPageView.swift | 13 ++- YDS-SwiftUI/Source/Component/YDSToast.swift | 97 +++++++++++++------ YDS.xcodeproj/project.pbxproj | 4 + 4 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 YDS-Essential/Source/HapticManager.swift diff --git a/YDS-Essential/Source/HapticManager.swift b/YDS-Essential/Source/HapticManager.swift new file mode 100644 index 00000000..43ddd159 --- /dev/null +++ b/YDS-Essential/Source/HapticManager.swift @@ -0,0 +1,29 @@ +// +// HapticManager.swift +// YDS +// +// Created by 정종인 on 12/25/23. +// + +import UIKit + +public class HapticManager { + public static let instance = HapticManager() + private init() {} + + public func notification(type: UINotificationFeedbackGenerator.FeedbackType) { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(type) + } + + public func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) { + let generator = UIImpactFeedbackGenerator(style: style) + generator.impactOccurred() + } +} + +/* + Haptic Manager 사용 방법 + HapticManager.instance.notification(type: .success) + HapticManager.instance.impact(style: .soft) + */ diff --git a/YDS-Storybook/SwiftUI/Components/ToastPageView.swift b/YDS-Storybook/SwiftUI/Components/ToastPageView.swift index 5e7d2b3f..ab88863b 100644 --- a/YDS-Storybook/SwiftUI/Components/ToastPageView.swift +++ b/YDS-Storybook/SwiftUI/Components/ToastPageView.swift @@ -13,7 +13,7 @@ struct ToastPageView: View { @State var text: String? = "Toast" @State var durationIndex: Int = 0 - @State var isShowing: Bool = false + @State var hapticIndex: Int = 0 var body: some View { StorybookPageView( @@ -26,21 +26,26 @@ struct ToastPageView: View { ), Option.enum( description: "duration", - cases: YDSToast.ToastDuration.allCases, + cases: YDSToastModel.ToastDuration.allCases, selectedIndex: $durationIndex + ), + Option.enum( + description: "haptic", + cases: YDSToastModel.HapticType.allCases, + selectedIndex: $hapticIndex ) ] ) .navigationTitle(title) .overlay(alignment: .bottom) { Button(action: { // 버튼은 추후 YDSButton 추가 이후에 수정 예정 - isShowing = true + YDSToast(text ?? "", duration: .allCases[durationIndex], haptic: .allCases[hapticIndex]) }, label: { Text("토스트 생성!") }) .padding() } - .ydsToast($text, isShowing: $isShowing) + .registerYDSToast() } } diff --git a/YDS-SwiftUI/Source/Component/YDSToast.swift b/YDS-SwiftUI/Source/Component/YDSToast.swift index 244deca9..9fca803d 100644 --- a/YDS-SwiftUI/Source/Component/YDSToast.swift +++ b/YDS-SwiftUI/Source/Component/YDSToast.swift @@ -8,10 +8,17 @@ import SwiftUI import UIKit import YDS_Essential +import Combine +/* + YDSToast 사용 방법 + 1. 최상단 뷰에 .registerYDSToast() Modifier를 붙여준다. + 2. toast를 띄우고 싶을 때 YDSToast()를 불러준다. + */ -public struct YDSToast: Equatable { +public struct YDSToastModel: Equatable { let text: String let duration: ToastDuration + let haptic: HapticType public enum ToastDuration: CaseIterable { case short case long @@ -25,6 +32,30 @@ public struct YDSToast: Equatable { } } } + public enum HapticType: CaseIterable { + case success + case failed + case none + } + var playHaptic: () -> Void { + switch haptic { + case .success: { + HapticManager.instance.notification(type: .success) + } + case .failed: { + HapticManager.instance.notification(type: .warning) + } + case .none: {} + } + } +} + +public func YDSToast( + _ text: String, + duration: YDSToastModel.ToastDuration = .short, + haptic: YDSToastModel.HapticType = .none +) { + YDSToastHelper.shared.enqueueToast(YDSToastModel(text: text, duration: duration, haptic: haptic)) } public struct YDSToastView: View { @@ -44,43 +75,53 @@ public struct YDSToastView: View { } } -public struct YDSToastModifier: ViewModifier { - @Binding public var isShowing: Bool - @Binding public var toast: YDSToast - public func body(content: Content) -> some View { +private class YDSToastHelper: ObservableObject { + static let shared = YDSToastHelper() + @Published var isShowing: Bool = false + @Published var showingToast: YDSToastModel? + private let subject = PassthroughSubject() + private var cancellables = Set() + init() { + initSubscription() + } + private func initSubscription() { + subject + .receive(on: RunLoop.main) + .sink { [weak self] toast in + self?.isShowing = false + self?.isShowing = true + self?.showingToast = toast + toast.playHaptic() + DispatchQueue.main.asyncAfter(deadline: .now() + toast.duration.value) { + self?.isShowing = false + self?.showingToast = nil + } + } + .store(in: &cancellables) + } + func enqueueToast(_ toast: YDSToastModel) { + subject.send(toast) + } +} + +struct YDSToastModifier: ViewModifier { + @StateObject private var toastHelper = YDSToastHelper.shared + func body(content: Content) -> some View { content .overlay(alignment: .bottom) { - if isShowing { + if toastHelper.isShowing, let toast = toastHelper.showingToast { YDSToastView(toast.text) - .opacity(isShowing ? 1.0 : 0.0) - } - } - .onChange(of: isShowing) { value in - if value { - DispatchQueue.main.asyncAfter(deadline: .now() + toast.duration.value) { - isShowing = false - } + .opacity(toastHelper.isShowing ? 1.0 : 0.0) } } - .animation(.easeInOut, value: isShowing) + .animation(.easeInOut, value: toastHelper.isShowing) } } extension View { - public func ydsToast( - _ text: Binding, - duration: YDSToast.ToastDuration = .short, - isShowing: Binding - ) -> some View { + public func registerYDSToast() -> some View { self.modifier( - YDSToastModifier( - isShowing: isShowing, - toast: .init(get: { - YDSToast(text: text.wrappedValue ?? "", duration: duration) - }, set: { ydsToast in - text.wrappedValue = ydsToast.text - }) - ) + YDSToastModifier() ) } } diff --git a/YDS.xcodeproj/project.pbxproj b/YDS.xcodeproj/project.pbxproj index dfea9781..fe857e67 100644 --- a/YDS.xcodeproj/project.pbxproj +++ b/YDS.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ 5A10B1B62A8F5C8500139E89 /* SwiftUIYDSIconArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A10B1B52A8F5C8500139E89 /* SwiftUIYDSIconArray.swift */; }; 5A10B1B82A8F5FFF00139E89 /* IconPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A10B1B72A8F5FFF00139E89 /* IconPageView.swift */; }; 6CA05E822A90846B00B07920 /* SwiftUIYDSColorArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA05E812A90846B00B07920 /* SwiftUIYDSColorArray.swift */; }; + 6F124C632B399953004187E6 /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F124C612B399753004187E6 /* HapticManager.swift */; }; 6F2D527B2A79446000BAF200 /* ColorPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2D527A2A79446000BAF200 /* ColorPageView.swift */; }; 6F2D527D2A7944D800BAF200 /* PageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2D527C2A7944D800BAF200 /* PageListView.swift */; }; 6F3C1EEE2A7A8573003D0D06 /* YDS_Essential.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F3C1EED2A7A8573003D0D06 /* YDS_Essential.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -329,6 +330,7 @@ 5A10B1B52A8F5C8500139E89 /* SwiftUIYDSIconArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIYDSIconArray.swift; sourceTree = ""; }; 5A10B1B72A8F5FFF00139E89 /* IconPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconPageView.swift; sourceTree = ""; }; 6CA05E812A90846B00B07920 /* SwiftUIYDSColorArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIYDSColorArray.swift; sourceTree = ""; }; + 6F124C612B399753004187E6 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; 6F2D527A2A79446000BAF200 /* ColorPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPageView.swift; sourceTree = ""; }; 6F2D527C2A7944D800BAF200 /* PageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListView.swift; sourceTree = ""; }; 6F3C1EEB2A7A8572003D0D06 /* YDS_Essential.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = YDS_Essential.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -776,6 +778,7 @@ 6F3C1F132A7A9616003D0D06 /* Source */ = { isa = PBXGroup; children = ( + 6F124C612B399753004187E6 /* HapticManager.swift */, 6F95FE5B2A768CAF00B398CF /* YDSBundle.swift */, 6F3C1EFF2A7A8843003D0D06 /* Rule */, 6F3C1EFE2A7A8839003D0D06 /* Foundation */, @@ -1343,6 +1346,7 @@ buildActionMask = 2147483647; files = ( 6F3C1F122A7A9428003D0D06 /* YDSConstant.swift in Sources */, + 6F124C632B399953004187E6 /* HapticManager.swift in Sources */, 6F3C1F0C2A7A8EC6003D0D06 /* YDSBundle.swift in Sources */, 6F3C1F112A7A9428003D0D06 /* YDSAnimation.swift in Sources */, 6F3C1F102A7A9428003D0D06 /* YDSScreenSize.swift in Sources */,