Skip to content

Commit

Permalink
Merge pull request #21 from YAPP-Github/TNT-139-createMinComponents-C…
Browse files Browse the repository at this point in the history
…ontrolAndPopUp

[TNT-139] TControlButton, TPopUp 컴포넌트 코드 작성
  • Loading branch information
FpRaArNkK authored Jan 19, 2025
2 parents 53c188f + c0683f4 commit f5b885f
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<Bool>,
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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// TPopUpModifier.swift
// DesignSystem
//
// Created by 박민서 on 1/16/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import SwiftUI

/// 팝업 컨테이너 (공통 레이아웃)
public struct TPopUpModifier<InnerContent: View>: 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<Bool>,
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<Content: View>(
isPresented: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
) -> some View {
self.modifier(TPopUpModifier<Content>(isPresented: isPresented, newContent: content))
}

/// `TPopUp.Alert` 팝업 전용 View Modifier
/// - Parameters:
/// - isPresented: 팝업 표시 여부를 제어하는 Binding
/// - content: 팝업 알림 내용을 구성하는 클로저
/// - Returns: 팝업이 추가된 View
func tPopUp(isPresented: Binding<Bool>, content: @escaping () -> TPopUpAlertView) -> some View {
self.modifier(TPopUpModifier(isPresented: isPresented, newContent: content))
}
}
33 changes: 33 additions & 0 deletions TnT/Projects/DesignSystem/Sources/Utility/EquatableClosure.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit f5b885f

Please sign in to comment.