-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[TNT-139] TControlButton, TPopUp 컴포넌트 코드 작성 #21
Changes from 2 commits
3291b3a
107bcc3
4c42ede
be9b1c4
5a7525e
c0683f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"images" : [ | ||
{ | ||
"filename" : "icn_toggle_selected.svg", | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"images" : [ | ||
{ | ||
"filename" : "icn_toggle_unselected.svg", | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Bool>, | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 여기에 토글이 왜 껴있는지 잘 모르겠네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위에서 멘션주신 부분에 답변드린 것과 같은 맥락으로, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저라면 분리할 것 같아요. 한번 작성해주신 스타일 열거형이 꼭 액션이 필요한게 무엇이 있을까, 바인딩에 의존하고 있는 것이 무엇일까 잘 판단하고 분리할 지 정해주세요 😃 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이마를 탁 때리게 되네요.. 더 고민하겠습니다! |
||
|
||
/// 선택 상태에 따른 이미지 반환 | ||
func image(isSelected: Bool) -> Image { | ||
switch self { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 불필요한 띄어쓰기! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아앗...!! 수정하겠습니다 감사합니다! |
||
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) | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<InnerContent: View>: ViewModifier { | ||
|
||
/// 팝업에 표시될 내부 콘텐츠 클로저 | ||
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: 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<Content: View>( | ||
isPresented: Binding<Bool>, | ||
@ViewBuilder content: @escaping () -> Content | ||
) -> some View { | ||
self.modifier(TPopUp.Modifier<Content>(isPresented: isPresented, newContent: content)) | ||
} | ||
|
||
/// `TPopUp.Alert` 팝업 전용 View Modifier | ||
/// - Parameters: | ||
/// - isPresented: 팝업 표시 여부를 제어하는 Binding | ||
/// - content: 팝업 알림 내용을 구성하는 클로저 | ||
/// - Returns: 팝업이 추가된 View | ||
func tPopUp(isPresented: Binding<Bool>, content: @escaping () -> TPopUp.Alert) -> some View { | ||
self.modifier(TPopUp.Modifier(isPresented: isPresented, newContent: content)) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
보통 이런 select들이 api 쏘는값이 많을텐데
네트워크 실패하면 다시 값 변경하는 로직을 추가하실건가요?? 저는 이또한 파편이라 생각합니다...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗 저기에 toggle빼는 걸 깜빡했습니다..! 죄송합니다..ㅠㅠ
해당 부분은
tapAction()
만 수행하며, 외부에서 변경되는 isSelected 값을 반영하는 것이 맞습니다!의 흐름으로 생각하며 작성했습니다