diff --git a/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift b/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift new file mode 100644 index 0000000..ae4b0ff --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Control/TControlButton.swift @@ -0,0 +1,74 @@ +// +// TControlButton.swift +// DesignSystem +// +// Created by 박민서 on 1/15/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +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 + /// 버튼 선택 상태 + @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: { + tapAction() + }, label: { + type.image(isSelected: isSelected) + .resizable() + .scaledToFit() + .frame(width: TControlButton.defaultSize.width, height: TControlButton.defaultSize.height) + }) + } +} + +public extension TControlButton { + /// TControlButton의 스타일입니다. + enum Style { + case radio + case checkMark + case checkbox + 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 .star: + return Image(isSelected ? .icnStarFilled : .icnStarEmpty) + case .heart: + return Image(isSelected ? .icnHeartFilled : .icnHeartEmpty) + } + } + } +} 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()) + } +} 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..8b3403b --- /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) + } + } + } +} + +public 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() + } +}