From 3291b3ae850ca8eb62f2402eec19e122c2894f30 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:32:19 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[Feat]=20ControlButton=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contents.json | 12 +++ .../icn_toggle_selected.svg | 4 + .../Contents.json | 12 +++ .../icn_toggle_unselected.svg | 4 + .../Components/Control/ControlButton.swift | 87 +++++++++++++++++++ .../DesignSystem/Image+DesignSystem.swift | 2 + 6 files changed, 121 insertions(+) create mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json create mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg create mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json create mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Control/ControlButton.swift diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json new file mode 100644 index 0000000..e178778 --- /dev/null +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icn_toggle_selected.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg new file mode 100644 index 0000000..2486023 --- /dev/null +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json new file mode 100644 index 0000000..77f3f72 --- /dev/null +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icn_toggle_unselected.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg new file mode 100644 index 0000000..13a74cf --- /dev/null +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/TnT/Projects/DesignSystem/Sources/Components/Control/ControlButton.swift b/TnT/Projects/DesignSystem/Sources/Components/Control/ControlButton.swift new file mode 100644 index 0000000..1e3bc03 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Control/ControlButton.swift @@ -0,0 +1,87 @@ +// +// TControlButton.swift +// DesignSystem +// +// Created by 박민서 on 1/15/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// TnT 앱 내에서 전반적으로 사용되는 커스텀 컨트롤 버튼 컴포넌트입니다. +public struct TControlButton: View { + /// 버틉 탭 액션 + private let tapAction: () -> Void + /// 버튼 스타일 + private let type: Style + /// 버튼 선택 상태 + @Binding private var isSelected: Bool + + /// TControlButton 생성자 + /// - Parameters: + /// - type: 버튼의 스타일. `TControlButton.Style` 사용. + /// - isSelected: 버튼의 선택 상태를 관리하는 바인딩. + /// - action: 버튼이 탭되었을 때 실행할 액션. (기본값: 빈 클로저) + public init( + type: Style, + isSelected: Binding, + action: @escaping () -> Void = {} + ) { + self.type = type + self._isSelected = isSelected + self.tapAction = action + } + + public var body: some View { + Button(action: { + isSelected.toggle() + tapAction() + }, label: { + type.image(isSelected: isSelected) + .resizable() + .scaledToFit() + .frame(width: type.defaultSize.width, height: type.defaultSize.height) + }) + } +} + +public extension TControlButton { + /// TControlButton의 스타일입니다. + enum Style { + case radio + case checkMark + case checkbox + case toggle + case star + case heart + + /// 선택 상태에 따른 이미지 반환 + func image(isSelected: Bool) -> Image { + switch self { + + case .radio: + return Image(isSelected ? .icnRadioButtonSelected : .icnRadioButtonUnselected) + case .checkMark: + return Image(isSelected ? .icnCheckMarkFilled : .icnCheckMarkEmpty) + case .checkbox: + return Image(isSelected ? .icnCheckButtonSelected : .icnCheckButtonUnselected) + case .toggle: + return Image(isSelected ? .icnToggleSelected : .icnToggleUnselected) + case .star: + return Image(isSelected ? .icnStarFilled : .icnStarEmpty) + case .heart: + return Image(isSelected ? .icnHeartFilled : .icnHeartEmpty) + } + } + + /// 스타일에 따른 기본 크기 + var defaultSize: CGSize { + switch self { + case .toggle: + return .init(width: 44, height: 24) + default: + return .init(width: 24, height: 24) + } + } + } +} diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift index 2b1c705..c0bc548 100644 --- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift +++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift @@ -40,6 +40,8 @@ public extension ImageResource { static let icnStarFilled: ImageResource = DesignSystemAsset.icnStarFilled.imageResource static let icnHeartEmpty: ImageResource = DesignSystemAsset.icnHeartEmpty.imageResource static let icnHeartFilled: ImageResource = DesignSystemAsset.icnHeartFilled.imageResource + static let icnToggleUnselected: ImageResource = DesignSystemAsset.icnToggleUnselected.imageResource + static let icnToggleSelected: ImageResource = DesignSystemAsset.icnToggleSelected.imageResource } // MARK: Image From 107bcc312946e1a7e69e7c5e7d2f043ab210728d Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:12:43 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[Feat]=20TPopUp=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/PopUp/TPopUp.swift | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift new file mode 100644 index 0000000..97d2077 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift @@ -0,0 +1,189 @@ +// +// TPopUp.swift +// DesignSystem +// +// Created by 박민서 on 1/15/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// 팝업 관련 네임스페이스 +public struct TPopUp { + /// 팝업 내부 콘텐츠의 기본 패딩 + public static let defaultInnerPadding: CGFloat = 20 + /// 팝업 배경의 기본 불투명도 + /// 0.0 = 완전 투명, 1.0 = 완전 불투명 + public static let defaultBackgroundOpacity: Double = 0.8 + /// 팝업의 기본 배경 색상 + public static let defaultPopUpBackgroundColor: Color = .white + /// 팝업 모서리의 기본 곡률 (Corner Radius) + public static let defaultCornerRadius: CGFloat = 16 + /// 팝업 그림자의 기본 반경 + public static let defaultShadowRadius: CGFloat = 10 + /// 팝업 콘텐츠의 기본 크기 + public static let defaultContentSize: CGSize = .init(width: 297, height: 175) +} + +extension TPopUp { + /// 팝업 컨테이너 (공통 레이아웃) + public struct Modifier: ViewModifier { + + /// 팝업에 표시될 내부 콘텐츠 클로저 + private let innerContent: () -> InnerContent + /// 팝업 표시 여부 + @Binding private var isPresented: Bool + + /// TPopupModifier 초기화 메서드 + /// - Parameters: + /// - isPresented: 팝업 표시 여부를 제어하는 Binding + /// - newContent: 팝업에 표시될 내부 콘텐츠 클로저 + public init( + isPresented: Binding, + newContent: @escaping () -> InnerContent + ) { + self._isPresented = isPresented + self.innerContent = newContent + } + + public func body(content: Content) -> some View { + ZStack { + // 기존 뷰 + content + .zIndex(0) + + if isPresented { + // 반투명 배경 + Color.black.opacity(defaultBackgroundOpacity) + .ignoresSafeArea() + .zIndex(1) + .onTapGesture { + isPresented = false + } + + // 팝업 뷰 + self.innerContent() + .frame(minWidth: TPopUp.defaultContentSize.width, minHeight: TPopUp.defaultContentSize.height) + .padding(defaultInnerPadding) + .background(defaultPopUpBackgroundColor) + .cornerRadius(defaultCornerRadius) + .shadow(radius: 10) + .padding() + .zIndex(2) + } + } + .animation(.bouncy, value: isPresented) + } + } +} + +extension TPopUp { + /// 팝업 Alert컨텐츠 + public struct Alert: View { + /// 팝업 제목 + private let title: String + /// 팝업 메시지 + private let message: String + /// 팝업 버튼 배열 + private let buttons: [TPopUp.ButtonContent] + + /// TPopUpAlert 초기화 메서드 + /// - Parameters: + /// - title: 팝업 제목 + /// - message: 팝업 메시지 + /// - buttons: 팝업 버튼 배열 + public init(title: String, message: String, buttons: [TPopUp.ButtonContent]) { + self.title = title + self.message = message + self.buttons = buttons + } + + public var body: some View { + VStack(spacing: 20) { + // 텍스트 Section + VStack(spacing: 8) { + Text(title) + .typographyStyle(.heading4, with: .neutral900) + .multilineTextAlignment(.center) + .padding(.top, 20) + + Text(message) + .typographyStyle(.body2Medium, with: .neutral500) + .multilineTextAlignment(.center) + } + + // 버튼 Section + HStack { + // TODO: 버튼 컴포넌트 완성시 대체 + ForEach(buttons, id: \.title) { button in + Button(action: button.action) { + Text(button.title) + .typographyStyle(.body1Medium, with: button.style == .primary ? Color.neutral50 : Color.neutral500) + .padding() + .frame(maxWidth: .infinity) + .background(button.style == .primary ? Color.neutral900 : Color.neutral100) + .foregroundColor(.white) + .cornerRadius(8) + } + } + } + } + } + } +} + +// TODO: 버튼 컴포넌트 완성시 대체 +extension TPopUp { + /// 팝업 버튼 스타일 + public struct ButtonContent { + /// 버튼 제목 + let title: String + /// 버튼 스타일 + let style: Style + /// 버튼 클릭 시 동작 + let action: () -> Void + + public enum Style { + case primary + case secondary + } + + /// TPopUpButtonContent 초기화 메서드 + /// - Parameters: + /// - title: 버튼 제목 + /// - style: 버튼 스타일 (기본값: `.primary`) + /// - action: 버튼 클릭 시 동작 + public init( + title: String, + style: Style = .primary, + action: @escaping () -> Void = {} + ) { + self.title = title + self.action = action + self.style = style + } + } +} + +public extension View { + /// 팝업 표시를 위한 View Modifier + /// - Parameters: + /// - isPresented: 팝업 표시 여부를 제어하는 Binding + /// - content: 팝업 내부에 표시할 콘텐츠 클로저 + /// - Returns: 팝업이 추가된 View + func tPopUp( + isPresented: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(TPopUp.Modifier(isPresented: isPresented, newContent: content)) + } + + /// `TPopUp.Alert` 팝업 전용 View Modifier + /// - Parameters: + /// - isPresented: 팝업 표시 여부를 제어하는 Binding + /// - content: 팝업 알림 내용을 구성하는 클로저 + /// - Returns: 팝업이 추가된 View + func tPopUp(isPresented: Binding, content: @escaping () -> TPopUp.Alert) -> some View { + self.modifier(TPopUp.Modifier(isPresented: isPresented, newContent: content)) + } +} From 4c42ede92847a9dea47616e1f482b89bcdc70043 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:12:27 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[Feat]=20TControl=EC=97=90=EC=84=9C=20Toggl?= =?UTF-8?q?e=20=EB=B6=84=EB=A6=AC=20->=20TToggleStyle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ntrolButton.swift => TControlButton.swift} | 19 +++------- .../Components/Control/TToggleStyle.swift | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 15 deletions(-) rename TnT/Projects/DesignSystem/Sources/Components/Control/{ControlButton.swift => TControlButton.swift} (80%) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Control/TToggleStyle.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/Control/ControlButton.swift b/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift similarity index 80% rename from TnT/Projects/DesignSystem/Sources/Components/Control/ControlButton.swift rename to TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift index 1e3bc03..497d411 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Control/ControlButton.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift @@ -10,7 +10,9 @@ import SwiftUI /// TnT 앱 내에서 전반적으로 사용되는 커스텀 컨트롤 버튼 컴포넌트입니다. public struct TControlButton: View { - /// 버틉 탭 액션 + /// 버튼 기본 사이즈 + static private let defaultSize: CGSize = .init(width: 24, height: 24) + /// 버튼 탭 액션 private let tapAction: () -> Void /// 버튼 스타일 private let type: Style @@ -40,7 +42,7 @@ public struct TControlButton: View { type.image(isSelected: isSelected) .resizable() .scaledToFit() - .frame(width: type.defaultSize.width, height: type.defaultSize.height) + .frame(width: TControlButton.defaultSize.width, height: TControlButton.defaultSize.height) }) } } @@ -51,7 +53,6 @@ public extension TControlButton { case radio case checkMark case checkbox - case toggle case star case heart @@ -65,23 +66,11 @@ public extension TControlButton { return Image(isSelected ? .icnCheckMarkFilled : .icnCheckMarkEmpty) case .checkbox: return Image(isSelected ? .icnCheckButtonSelected : .icnCheckButtonUnselected) - case .toggle: - return Image(isSelected ? .icnToggleSelected : .icnToggleUnselected) case .star: return Image(isSelected ? .icnStarFilled : .icnStarEmpty) case .heart: return Image(isSelected ? .icnHeartFilled : .icnHeartEmpty) } } - - /// 스타일에 따른 기본 크기 - var defaultSize: CGSize { - switch self { - case .toggle: - return .init(width: 44, height: 24) - default: - return .init(width: 24, height: 24) - } - } } } diff --git a/TnT/Projects/DesignSystem/Sources/Components/Control/TToggleStyle.swift b/TnT/Projects/DesignSystem/Sources/Components/Control/TToggleStyle.swift new file mode 100644 index 0000000..7acd5a0 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Control/TToggleStyle.swift @@ -0,0 +1,35 @@ +// +// TToggleStyle.swift +// DesignSystem +// +// Created by 박민서 on 1/15/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// TToggleStyle: ViewModifier +/// SwiftUI의 `Toggle`에 TnT 스타일을 적용하기 위한 커스텀 ViewModifier입니다. +/// 기본 크기와 토글 스타일을 설정하여 재사용 가능한 스타일링을 제공합니다. +struct TToggleStyle: ViewModifier { + /// 기본 토글 크기 + static let defaultSize: CGSize = .init(width: 44, height: 24) + + /// `ViewModifier`가 적용된 뷰의 구성 + /// - Parameter content: 스타일이 적용될 뷰 + /// - Returns: TnT 스타일이 적용된 뷰 + func body(content: Content) -> some View { + content + .toggleStyle(SwitchToggleStyle(tint: .red500)) + .labelsHidden() + .frame(width: TToggleStyle.defaultSize.width, height: TToggleStyle.defaultSize.height) + } +} + +/// Toggle 확장: TnT 스타일 적용 +public extension Toggle { + /// SwiftUI의 기본 `Toggle`에 TnT 스타일을 적용합니다. + func applyTToggleStyle() -> some View { + self.modifier(TToggleStyle()) + } +} From be9b1c4161fee13e95555b866cdc1d5d35790a7e Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Thu, 16 Jan 2025 02:20:41 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[Refactor]=20TPopUp=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20TCA=20=EA=B3=A0=EB=A0=A4=ED=95=9C=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/PopUp/TPopUp.swift | 189 ------------------ .../Components/PopUp/TPopUpAlertState.swift | 69 +++++++ .../Components/PopUp/TPopUpAlertView.swift | 87 ++++++++ .../Components/PopUp/TPopUpModifier.swift | 96 +++++++++ .../Sources/Utility/EquatableClosure.swift | 33 +++ 5 files changed, 285 insertions(+), 189 deletions(-) delete mode 100644 TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertState.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpModifier.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Utility/EquatableClosure.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift deleted file mode 100644 index 97d2077..0000000 --- a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUp.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// TPopUp.swift -// DesignSystem -// -// Created by 박민서 on 1/15/25. -// Copyright © 2025 yapp25thTeamTnT. All rights reserved. -// - -import SwiftUI - -/// 팝업 관련 네임스페이스 -public struct TPopUp { - /// 팝업 내부 콘텐츠의 기본 패딩 - public static let defaultInnerPadding: CGFloat = 20 - /// 팝업 배경의 기본 불투명도 - /// 0.0 = 완전 투명, 1.0 = 완전 불투명 - public static let defaultBackgroundOpacity: Double = 0.8 - /// 팝업의 기본 배경 색상 - public static let defaultPopUpBackgroundColor: Color = .white - /// 팝업 모서리의 기본 곡률 (Corner Radius) - public static let defaultCornerRadius: CGFloat = 16 - /// 팝업 그림자의 기본 반경 - public static let defaultShadowRadius: CGFloat = 10 - /// 팝업 콘텐츠의 기본 크기 - public static let defaultContentSize: CGSize = .init(width: 297, height: 175) -} - -extension TPopUp { - /// 팝업 컨테이너 (공통 레이아웃) - public struct Modifier: ViewModifier { - - /// 팝업에 표시될 내부 콘텐츠 클로저 - private let innerContent: () -> InnerContent - /// 팝업 표시 여부 - @Binding private var isPresented: Bool - - /// TPopupModifier 초기화 메서드 - /// - Parameters: - /// - isPresented: 팝업 표시 여부를 제어하는 Binding - /// - newContent: 팝업에 표시될 내부 콘텐츠 클로저 - public init( - isPresented: Binding, - newContent: @escaping () -> InnerContent - ) { - self._isPresented = isPresented - self.innerContent = newContent - } - - public func body(content: Content) -> some View { - ZStack { - // 기존 뷰 - content - .zIndex(0) - - if isPresented { - // 반투명 배경 - Color.black.opacity(defaultBackgroundOpacity) - .ignoresSafeArea() - .zIndex(1) - .onTapGesture { - isPresented = false - } - - // 팝업 뷰 - self.innerContent() - .frame(minWidth: TPopUp.defaultContentSize.width, minHeight: TPopUp.defaultContentSize.height) - .padding(defaultInnerPadding) - .background(defaultPopUpBackgroundColor) - .cornerRadius(defaultCornerRadius) - .shadow(radius: 10) - .padding() - .zIndex(2) - } - } - .animation(.bouncy, value: isPresented) - } - } -} - -extension TPopUp { - /// 팝업 Alert컨텐츠 - public struct Alert: View { - /// 팝업 제목 - private let title: String - /// 팝업 메시지 - private let message: String - /// 팝업 버튼 배열 - private let buttons: [TPopUp.ButtonContent] - - /// TPopUpAlert 초기화 메서드 - /// - Parameters: - /// - title: 팝업 제목 - /// - message: 팝업 메시지 - /// - buttons: 팝업 버튼 배열 - public init(title: String, message: String, buttons: [TPopUp.ButtonContent]) { - self.title = title - self.message = message - self.buttons = buttons - } - - public var body: some View { - VStack(spacing: 20) { - // 텍스트 Section - VStack(spacing: 8) { - Text(title) - .typographyStyle(.heading4, with: .neutral900) - .multilineTextAlignment(.center) - .padding(.top, 20) - - Text(message) - .typographyStyle(.body2Medium, with: .neutral500) - .multilineTextAlignment(.center) - } - - // 버튼 Section - HStack { - // TODO: 버튼 컴포넌트 완성시 대체 - ForEach(buttons, id: \.title) { button in - Button(action: button.action) { - Text(button.title) - .typographyStyle(.body1Medium, with: button.style == .primary ? Color.neutral50 : Color.neutral500) - .padding() - .frame(maxWidth: .infinity) - .background(button.style == .primary ? Color.neutral900 : Color.neutral100) - .foregroundColor(.white) - .cornerRadius(8) - } - } - } - } - } - } -} - -// TODO: 버튼 컴포넌트 완성시 대체 -extension TPopUp { - /// 팝업 버튼 스타일 - public struct ButtonContent { - /// 버튼 제목 - let title: String - /// 버튼 스타일 - let style: Style - /// 버튼 클릭 시 동작 - let action: () -> Void - - public enum Style { - case primary - case secondary - } - - /// TPopUpButtonContent 초기화 메서드 - /// - Parameters: - /// - title: 버튼 제목 - /// - style: 버튼 스타일 (기본값: `.primary`) - /// - action: 버튼 클릭 시 동작 - public init( - title: String, - style: Style = .primary, - action: @escaping () -> Void = {} - ) { - self.title = title - self.action = action - self.style = style - } - } -} - -public extension View { - /// 팝업 표시를 위한 View Modifier - /// - Parameters: - /// - isPresented: 팝업 표시 여부를 제어하는 Binding - /// - content: 팝업 내부에 표시할 콘텐츠 클로저 - /// - Returns: 팝업이 추가된 View - func tPopUp( - isPresented: Binding, - @ViewBuilder content: @escaping () -> Content - ) -> some View { - self.modifier(TPopUp.Modifier(isPresented: isPresented, newContent: content)) - } - - /// `TPopUp.Alert` 팝업 전용 View Modifier - /// - Parameters: - /// - isPresented: 팝업 표시 여부를 제어하는 Binding - /// - content: 팝업 알림 내용을 구성하는 클로저 - /// - Returns: 팝업이 추가된 View - func tPopUp(isPresented: Binding, content: @escaping () -> TPopUp.Alert) -> some View { - self.modifier(TPopUp.Modifier(isPresented: isPresented, newContent: content)) - } -} diff --git a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertState.swift b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertState.swift new file mode 100644 index 0000000..45f04e0 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertState.swift @@ -0,0 +1,69 @@ +// +// TPopupAlertState.swift +// DesignSystem +// +// Created by 박민서 on 1/15/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +/// TPopUpAlertView에 표시하는 정보입니다. +/// 팝업의 제목, 메시지, 버튼 정보를 포함. +public struct TPopupAlertState: Equatable { + /// 팝업 제목 + public var title: String + /// 팝업 메시지 (옵션) + public var message: String? + /// 팝업에 표시될 버튼 배열 + public var buttons: [ButtonState] + + /// TPopupAlertState 초기화 메서드 + /// - Parameters: + /// - title: 팝업의 제목 + /// - message: 팝업의 메시지 (선택 사항, 기본값: `nil`) + /// - buttons: 팝업에 표시할 버튼 배열 (기본값: 빈 배열) + public init( + title: String, + message: String? = nil, + buttons: [ButtonState] = [] + ) { + self.title = title + self.message = message + self.buttons = buttons + } +} + +public extension TPopupAlertState { + // TODO: 버튼 컴포넌트 완성 시 수정 + /// TPopUpAlertView.AlertButton에 표시하는 정보입니다. + struct ButtonState: Equatable { + /// 버튼 제목 + public let title: String + /// 버튼 스타일 + public let style: Style + /// 버튼 클릭 시 동작 + public let action: EquatableClosure + + public enum Style { + case primary + case secondary + } + + /// TPopupAlertState.ButtonState 초기화 메서드 + /// - Parameters: + /// - title: 버튼 제목 + /// - style: 버튼 스타일 (기본값: `.primary`) + /// - action: 버튼 클릭 시 동작 + public init( + title: String, + style: Style = .primary, + action: EquatableClosure + ) { + self.action = action + self.title = title + self.style = style + } + } +} diff --git a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift new file mode 100644 index 0000000..7729fa8 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift @@ -0,0 +1,87 @@ +// +// TPopUpAlertView.swift +// DesignSystem +// +// Created by 박민서 on 1/16/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// 팝업 Alert의 콘텐츠 뷰 +/// 타이틀, 메시지, 버튼 섹션으로 구성. +public struct TPopUpAlertView: View { + /// 팝업 상태 정보 + private let alertState: TPopupAlertState + + /// - Parameter alertState: 팝업에 표시할 상태 정보 + public init(alertState: TPopupAlertState) { + self.alertState = alertState + } + + public var body: some View { + VStack(spacing: 20) { + // 텍스트 Section + VStack(spacing: 8) { + Text(alertState.title) + .typographyStyle(.heading4, with: .neutral900) + .multilineTextAlignment(.center) + .padding(.top, 20) + if let message = alertState.message { + Text(message) + .typographyStyle(.body2Medium, with: .neutral500) + .multilineTextAlignment(.center) + } + } + + // 버튼 Section + HStack { + ForEach(alertState.buttons, id: \.title) { buttonState in + buttonState.toButton() + } + } + } + } +} + +public extension TPopUpAlertView { + // TODO: 버튼 컴포넌트 완성 시 수정 + struct AlertButton: View { + let title: String + let style: TPopupAlertState.ButtonState.Style + let action: () -> Void + + init( + title: String, + style: TPopupAlertState.ButtonState.Style, + action: @escaping () -> Void + ) { + self.title = title + self.style = style + self.action = action + } + + public var body: some View { + Button(action: action) { + Text(title) + .typographyStyle(.body1Medium, with: style == .primary ? Color.neutral50 : Color.neutral500) + .padding() + .frame(maxWidth: .infinity) + .background(style == .primary ? Color.neutral900 : Color.neutral100) + .foregroundColor(.white) + .cornerRadius(8) + } + } + } +} + +extension TPopupAlertState.ButtonState { + /// `ButtonState`를 `AlertButton`으로 변환 + func toButton() -> TPopUpAlertView.AlertButton { + TPopUpAlertView.AlertButton( + title: self.title, + style: self.style, + action: self.action.execute + ) + } +} diff --git a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpModifier.swift b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpModifier.swift new file mode 100644 index 0000000..28cedb7 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpModifier.swift @@ -0,0 +1,96 @@ +// +// TPopUpModifier.swift +// DesignSystem +// +// Created by 박민서 on 1/16/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// 팝업 컨테이너 (공통 레이아웃) +public struct TPopUpModifier: ViewModifier { + + /// 팝업 내부 콘텐츠의 기본 패딩 + private let defaultInnerPadding: CGFloat = 20 + /// 팝업 배경의 기본 불투명도 + /// 0.0 = 완전 투명, 1.0 = 완전 불투명 + private let defaultBackgroundOpacity: Double = 0.8 + /// 팝업의 기본 배경 색상 + private let defaultPopUpBackgroundColor: Color = .white + /// 팝업 모서리의 기본 곡률 (Corner Radius) + private let defaultCornerRadius: CGFloat = 16 + /// 팝업 그림자의 기본 반경 + private let defaultShadowRadius: CGFloat = 10 + /// 팝업 콘텐츠의 기본 크기 + private let defaultContentSize: CGSize = .init(width: 297, height: 175) + + /// 팝업에 표시될 내부 콘텐츠 클로저 + private let innerContent: () -> InnerContent + /// 팝업 표시 여부 + @Binding private var isPresented: Bool + + /// TPopupModifier 초기화 메서드 + /// - Parameters: + /// - isPresented: 팝업 표시 여부를 제어하는 Binding + /// - newContent: 팝업에 표시될 내부 콘텐츠 클로저 + public init( + isPresented: Binding, + newContent: @escaping () -> InnerContent + ) { + self._isPresented = isPresented + self.innerContent = newContent + } + + public func body(content: Content) -> some View { + ZStack { + // 기존 뷰 + content + .zIndex(0) + + if isPresented { + // 반투명 배경 + Color.black.opacity(defaultBackgroundOpacity) + .ignoresSafeArea() + .zIndex(1) + .onTapGesture { + isPresented = false + } + + // 팝업 뷰 + self.innerContent() + .frame(minWidth: defaultContentSize.width, minHeight: defaultContentSize.height) + .padding(defaultInnerPadding) + .background(defaultPopUpBackgroundColor) + .cornerRadius(defaultCornerRadius) + .shadow(radius: defaultShadowRadius) + .padding() + .zIndex(2) + } + } + .animation(.easeInOut, value: isPresented) + } +} + +public extension View { + /// 팝업 표시를 위한 View Modifier + /// - Parameters: + /// - isPresented: 팝업 표시 여부를 제어하는 Binding + /// - content: 팝업 내부에 표시할 콘텐츠 클로저 + /// - Returns: 팝업이 추가된 View + func tPopUp( + isPresented: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(TPopUpModifier(isPresented: isPresented, newContent: content)) + } + + /// `TPopUp.Alert` 팝업 전용 View Modifier + /// - Parameters: + /// - isPresented: 팝업 표시 여부를 제어하는 Binding + /// - content: 팝업 알림 내용을 구성하는 클로저 + /// - Returns: 팝업이 추가된 View + func tPopUp(isPresented: Binding, content: @escaping () -> TPopUpAlertView) -> some View { + self.modifier(TPopUpModifier(isPresented: isPresented, newContent: content)) + } +} diff --git a/TnT/Projects/DesignSystem/Sources/Utility/EquatableClosure.swift b/TnT/Projects/DesignSystem/Sources/Utility/EquatableClosure.swift new file mode 100644 index 0000000..691033a --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Utility/EquatableClosure.swift @@ -0,0 +1,33 @@ +// +// EquatableClosure.swift +// DesignSystem +// +// Created by 박민서 on 1/16/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 클로저를 Equatable로 사용할 수 있도록 래핑한 구조체입니다. +/// 고유 ID를 통해 클로저의 동등성을 비교하며, 내부에서 클로저를 실행할 수 있는 기능을 제공합니다. +public struct EquatableClosure: Equatable { + /// EquatableClosure를 고유하게 식별할 수 있는 UUID입니다. + private let id: UUID = UUID() + /// 실행할 클로저 + private let action: () -> Void + + public static func == (lhs: EquatableClosure, rhs: EquatableClosure) -> Bool { + lhs.id == rhs.id + } + + /// EquatableClosure 초기화 메서드 + /// - Parameter action: 실행할 클로저 + public init(action: @escaping () -> Void) { + self.action = action + } + + /// 클로저를 실행하는 메서드 + public func execute() { + action() + } +} From 5a7525ef1a19a36cde8d4c6be4da98377960b588 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Thu, 16 Jan 2025 02:28:31 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[Delete]=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Icons/icn_toggle_selected.imageset/Contents.json | 12 ------------ .../icn_toggle_selected.svg | 4 ---- .../icn_toggle_unselected.imageset/Contents.json | 12 ------------ .../icn_toggle_unselected.svg | 4 ---- .../Sources/DesignSystem/Image+DesignSystem.swift | 2 -- 5 files changed, 34 deletions(-) delete mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json delete mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg delete mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json delete mode 100644 TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json deleted file mode 100644 index e178778..0000000 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "icn_toggle_selected.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg deleted file mode 100644 index 2486023..0000000 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_selected.imageset/icn_toggle_selected.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json deleted file mode 100644 index 77f3f72..0000000 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "icn_toggle_unselected.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg deleted file mode 100644 index 13a74cf..0000000 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_toggle_unselected.imageset/icn_toggle_unselected.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift index c0bc548..2b1c705 100644 --- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift +++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift @@ -40,8 +40,6 @@ public extension ImageResource { static let icnStarFilled: ImageResource = DesignSystemAsset.icnStarFilled.imageResource static let icnHeartEmpty: ImageResource = DesignSystemAsset.icnHeartEmpty.imageResource static let icnHeartFilled: ImageResource = DesignSystemAsset.icnHeartFilled.imageResource - static let icnToggleUnselected: ImageResource = DesignSystemAsset.icnToggleUnselected.imageResource - static let icnToggleSelected: ImageResource = DesignSystemAsset.icnToggleSelected.imageResource } // MARK: Image From c0683f408c11952f7c48115258b62196ac07a476 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Thu, 16 Jan 2025 02:43:48 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[Chore]=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C,=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/Control/TControlButton.swift | 2 -- .../DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift b/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift index 497d411..ae4b0ff 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift @@ -36,7 +36,6 @@ public struct TControlButton: View { public var body: some View { Button(action: { - isSelected.toggle() tapAction() }, label: { type.image(isSelected: isSelected) @@ -59,7 +58,6 @@ public extension TControlButton { /// 선택 상태에 따른 이미지 반환 func image(isSelected: Bool) -> Image { switch self { - case .radio: return Image(isSelected ? .icnRadioButtonSelected : .icnRadioButtonUnselected) case .checkMark: diff --git a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift index 7729fa8..8b3403b 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/PopUp/TPopUpAlertView.swift @@ -75,7 +75,7 @@ public extension TPopUpAlertView { } } -extension TPopupAlertState.ButtonState { +public extension TPopupAlertState.ButtonState { /// `ButtonState`를 `AlertButton`으로 변환 func toButton() -> TPopUpAlertView.AlertButton { TPopUpAlertView.AlertButton(