Skip to content

Commit f5b885f

Browse files
authored
Merge pull request #21 from YAPP-Github/TNT-139-createMinComponents-ControlAndPopUp
[TNT-139] TControlButton, TPopUp 컴포넌트 코드 작성
2 parents 53c188f + c0683f4 commit f5b885f

File tree

6 files changed

+394
-0
lines changed

6 files changed

+394
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// TControlButton.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 1/15/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/// TnT 앱 내에서 전반적으로 사용되는 커스텀 컨트롤 버튼 컴포넌트입니다.
12+
public struct TControlButton: View {
13+
/// 버튼 기본 사이즈
14+
static private let defaultSize: CGSize = .init(width: 24, height: 24)
15+
/// 버튼 탭 액션
16+
private let tapAction: () -> Void
17+
/// 버튼 스타일
18+
private let type: Style
19+
/// 버튼 선택 상태
20+
@Binding private var isSelected: Bool
21+
22+
/// TControlButton 생성자
23+
/// - Parameters:
24+
/// - type: 버튼의 스타일. `TControlButton.Style` 사용.
25+
/// - isSelected: 버튼의 선택 상태를 관리하는 바인딩.
26+
/// - action: 버튼이 탭되었을 때 실행할 액션. (기본값: 빈 클로저)
27+
public init(
28+
type: Style,
29+
isSelected: Binding<Bool>,
30+
action: @escaping () -> Void = {}
31+
) {
32+
self.type = type
33+
self._isSelected = isSelected
34+
self.tapAction = action
35+
}
36+
37+
public var body: some View {
38+
Button(action: {
39+
tapAction()
40+
}, label: {
41+
type.image(isSelected: isSelected)
42+
.resizable()
43+
.scaledToFit()
44+
.frame(width: TControlButton.defaultSize.width, height: TControlButton.defaultSize.height)
45+
})
46+
}
47+
}
48+
49+
public extension TControlButton {
50+
/// TControlButton의 스타일입니다.
51+
enum Style {
52+
case radio
53+
case checkMark
54+
case checkbox
55+
case star
56+
case heart
57+
58+
/// 선택 상태에 따른 이미지 반환
59+
func image(isSelected: Bool) -> Image {
60+
switch self {
61+
case .radio:
62+
return Image(isSelected ? .icnRadioButtonSelected : .icnRadioButtonUnselected)
63+
case .checkMark:
64+
return Image(isSelected ? .icnCheckMarkFilled : .icnCheckMarkEmpty)
65+
case .checkbox:
66+
return Image(isSelected ? .icnCheckButtonSelected : .icnCheckButtonUnselected)
67+
case .star:
68+
return Image(isSelected ? .icnStarFilled : .icnStarEmpty)
69+
case .heart:
70+
return Image(isSelected ? .icnHeartFilled : .icnHeartEmpty)
71+
}
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// TToggleStyle.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 1/15/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/// TToggleStyle: ViewModifier
12+
/// SwiftUI의 `Toggle`에 TnT 스타일을 적용하기 위한 커스텀 ViewModifier입니다.
13+
/// 기본 크기와 토글 스타일을 설정하여 재사용 가능한 스타일링을 제공합니다.
14+
struct TToggleStyle: ViewModifier {
15+
/// 기본 토글 크기
16+
static let defaultSize: CGSize = .init(width: 44, height: 24)
17+
18+
/// `ViewModifier`가 적용된 뷰의 구성
19+
/// - Parameter content: 스타일이 적용될 뷰
20+
/// - Returns: TnT 스타일이 적용된 뷰
21+
func body(content: Content) -> some View {
22+
content
23+
.toggleStyle(SwitchToggleStyle(tint: .red500))
24+
.labelsHidden()
25+
.frame(width: TToggleStyle.defaultSize.width, height: TToggleStyle.defaultSize.height)
26+
}
27+
}
28+
29+
/// Toggle 확장: TnT 스타일 적용
30+
public extension Toggle {
31+
/// SwiftUI의 기본 `Toggle`에 TnT 스타일을 적용합니다.
32+
func applyTToggleStyle() -> some View {
33+
self.modifier(TToggleStyle())
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// TPopupAlertState.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 1/15/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
import ComposableArchitecture
11+
12+
/// TPopUpAlertView에 표시하는 정보입니다.
13+
/// 팝업의 제목, 메시지, 버튼 정보를 포함.
14+
public struct TPopupAlertState: Equatable {
15+
/// 팝업 제목
16+
public var title: String
17+
/// 팝업 메시지 (옵션)
18+
public var message: String?
19+
/// 팝업에 표시될 버튼 배열
20+
public var buttons: [ButtonState]
21+
22+
/// TPopupAlertState 초기화 메서드
23+
/// - Parameters:
24+
/// - title: 팝업의 제목
25+
/// - message: 팝업의 메시지 (선택 사항, 기본값: `nil`)
26+
/// - buttons: 팝업에 표시할 버튼 배열 (기본값: 빈 배열)
27+
public init(
28+
title: String,
29+
message: String? = nil,
30+
buttons: [ButtonState] = []
31+
) {
32+
self.title = title
33+
self.message = message
34+
self.buttons = buttons
35+
}
36+
}
37+
38+
public extension TPopupAlertState {
39+
// TODO: 버튼 컴포넌트 완성 시 수정
40+
/// TPopUpAlertView.AlertButton에 표시하는 정보입니다.
41+
struct ButtonState: Equatable {
42+
/// 버튼 제목
43+
public let title: String
44+
/// 버튼 스타일
45+
public let style: Style
46+
/// 버튼 클릭 시 동작
47+
public let action: EquatableClosure
48+
49+
public enum Style {
50+
case primary
51+
case secondary
52+
}
53+
54+
/// TPopupAlertState.ButtonState 초기화 메서드
55+
/// - Parameters:
56+
/// - title: 버튼 제목
57+
/// - style: 버튼 스타일 (기본값: `.primary`)
58+
/// - action: 버튼 클릭 시 동작
59+
public init(
60+
title: String,
61+
style: Style = .primary,
62+
action: EquatableClosure
63+
) {
64+
self.action = action
65+
self.title = title
66+
self.style = style
67+
}
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// TPopUpAlertView.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 1/16/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/// 팝업 Alert의 콘텐츠 뷰
12+
/// 타이틀, 메시지, 버튼 섹션으로 구성.
13+
public struct TPopUpAlertView: View {
14+
/// 팝업 상태 정보
15+
private let alertState: TPopupAlertState
16+
17+
/// - Parameter alertState: 팝업에 표시할 상태 정보
18+
public init(alertState: TPopupAlertState) {
19+
self.alertState = alertState
20+
}
21+
22+
public var body: some View {
23+
VStack(spacing: 20) {
24+
// 텍스트 Section
25+
VStack(spacing: 8) {
26+
Text(alertState.title)
27+
.typographyStyle(.heading4, with: .neutral900)
28+
.multilineTextAlignment(.center)
29+
.padding(.top, 20)
30+
if let message = alertState.message {
31+
Text(message)
32+
.typographyStyle(.body2Medium, with: .neutral500)
33+
.multilineTextAlignment(.center)
34+
}
35+
}
36+
37+
// 버튼 Section
38+
HStack {
39+
ForEach(alertState.buttons, id: \.title) { buttonState in
40+
buttonState.toButton()
41+
}
42+
}
43+
}
44+
}
45+
}
46+
47+
public extension TPopUpAlertView {
48+
// TODO: 버튼 컴포넌트 완성 시 수정
49+
struct AlertButton: View {
50+
let title: String
51+
let style: TPopupAlertState.ButtonState.Style
52+
let action: () -> Void
53+
54+
init(
55+
title: String,
56+
style: TPopupAlertState.ButtonState.Style,
57+
action: @escaping () -> Void
58+
) {
59+
self.title = title
60+
self.style = style
61+
self.action = action
62+
}
63+
64+
public var body: some View {
65+
Button(action: action) {
66+
Text(title)
67+
.typographyStyle(.body1Medium, with: style == .primary ? Color.neutral50 : Color.neutral500)
68+
.padding()
69+
.frame(maxWidth: .infinity)
70+
.background(style == .primary ? Color.neutral900 : Color.neutral100)
71+
.foregroundColor(.white)
72+
.cornerRadius(8)
73+
}
74+
}
75+
}
76+
}
77+
78+
public extension TPopupAlertState.ButtonState {
79+
/// `ButtonState`를 `AlertButton`으로 변환
80+
func toButton() -> TPopUpAlertView.AlertButton {
81+
TPopUpAlertView.AlertButton(
82+
title: self.title,
83+
style: self.style,
84+
action: self.action.execute
85+
)
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//
2+
// TPopUpModifier.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 1/16/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
/// 팝업 컨테이너 (공통 레이아웃)
12+
public struct TPopUpModifier<InnerContent: View>: ViewModifier {
13+
14+
/// 팝업 내부 콘텐츠의 기본 패딩
15+
private let defaultInnerPadding: CGFloat = 20
16+
/// 팝업 배경의 기본 불투명도
17+
/// 0.0 = 완전 투명, 1.0 = 완전 불투명
18+
private let defaultBackgroundOpacity: Double = 0.8
19+
/// 팝업의 기본 배경 색상
20+
private let defaultPopUpBackgroundColor: Color = .white
21+
/// 팝업 모서리의 기본 곡률 (Corner Radius)
22+
private let defaultCornerRadius: CGFloat = 16
23+
/// 팝업 그림자의 기본 반경
24+
private let defaultShadowRadius: CGFloat = 10
25+
/// 팝업 콘텐츠의 기본 크기
26+
private let defaultContentSize: CGSize = .init(width: 297, height: 175)
27+
28+
/// 팝업에 표시될 내부 콘텐츠 클로저
29+
private let innerContent: () -> InnerContent
30+
/// 팝업 표시 여부
31+
@Binding private var isPresented: Bool
32+
33+
/// TPopupModifier 초기화 메서드
34+
/// - Parameters:
35+
/// - isPresented: 팝업 표시 여부를 제어하는 Binding
36+
/// - newContent: 팝업에 표시될 내부 콘텐츠 클로저
37+
public init(
38+
isPresented: Binding<Bool>,
39+
newContent: @escaping () -> InnerContent
40+
) {
41+
self._isPresented = isPresented
42+
self.innerContent = newContent
43+
}
44+
45+
public func body(content: Content) -> some View {
46+
ZStack {
47+
// 기존 뷰
48+
content
49+
.zIndex(0)
50+
51+
if isPresented {
52+
// 반투명 배경
53+
Color.black.opacity(defaultBackgroundOpacity)
54+
.ignoresSafeArea()
55+
.zIndex(1)
56+
.onTapGesture {
57+
isPresented = false
58+
}
59+
60+
// 팝업 뷰
61+
self.innerContent()
62+
.frame(minWidth: defaultContentSize.width, minHeight: defaultContentSize.height)
63+
.padding(defaultInnerPadding)
64+
.background(defaultPopUpBackgroundColor)
65+
.cornerRadius(defaultCornerRadius)
66+
.shadow(radius: defaultShadowRadius)
67+
.padding()
68+
.zIndex(2)
69+
}
70+
}
71+
.animation(.easeInOut, value: isPresented)
72+
}
73+
}
74+
75+
public extension View {
76+
/// 팝업 표시를 위한 View Modifier
77+
/// - Parameters:
78+
/// - isPresented: 팝업 표시 여부를 제어하는 Binding
79+
/// - content: 팝업 내부에 표시할 콘텐츠 클로저
80+
/// - Returns: 팝업이 추가된 View
81+
func tPopUp<Content: View>(
82+
isPresented: Binding<Bool>,
83+
@ViewBuilder content: @escaping () -> Content
84+
) -> some View {
85+
self.modifier(TPopUpModifier<Content>(isPresented: isPresented, newContent: content))
86+
}
87+
88+
/// `TPopUp.Alert` 팝업 전용 View Modifier
89+
/// - Parameters:
90+
/// - isPresented: 팝업 표시 여부를 제어하는 Binding
91+
/// - content: 팝업 알림 내용을 구성하는 클로저
92+
/// - Returns: 팝업이 추가된 View
93+
func tPopUp(isPresented: Binding<Bool>, content: @escaping () -> TPopUpAlertView) -> some View {
94+
self.modifier(TPopUpModifier(isPresented: isPresented, newContent: content))
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// EquatableClosure.swift
3+
// DesignSystem
4+
//
5+
// Created by 박민서 on 1/16/25.
6+
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// 클로저를 Equatable로 사용할 수 있도록 래핑한 구조체입니다.
12+
/// 고유 ID를 통해 클로저의 동등성을 비교하며, 내부에서 클로저를 실행할 수 있는 기능을 제공합니다.
13+
public struct EquatableClosure: Equatable {
14+
/// EquatableClosure를 고유하게 식별할 수 있는 UUID입니다.
15+
private let id: UUID = UUID()
16+
/// 실행할 클로저
17+
private let action: () -> Void
18+
19+
public static func == (lhs: EquatableClosure, rhs: EquatableClosure) -> Bool {
20+
lhs.id == rhs.id
21+
}
22+
23+
/// EquatableClosure 초기화 메서드
24+
/// - Parameter action: 실행할 클로저
25+
public init(action: @escaping () -> Void) {
26+
self.action = action
27+
}
28+
29+
/// 클로저를 실행하는 메서드
30+
public func execute() {
31+
action()
32+
}
33+
}

0 commit comments

Comments
 (0)