From d1de356f65e21213a19ee673c8cee8d2f07a5a30 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:18:37 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[Fix]=20Tuist=20-=20Font=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/Divider/TDivider.swift | 9 ++ .../Components/TFormatterUtility.swift | 9 ++ .../Sources/Components/TTextField.swift | 9 ++ .../Components/TextField/TTextField.swift | 120 ++++++++++++++++++ .../DesignSystem/Font+DesignSystem.swift | 34 ++--- 5 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/TTextField.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift b/TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift new file mode 100644 index 0000000..0cd54e1 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift @@ -0,0 +1,9 @@ +// +// TDivider.swift +// DesignSystem +// +// Created by 박민서 on 1/14/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation diff --git a/TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift b/TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift new file mode 100644 index 0000000..0000231 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift @@ -0,0 +1,9 @@ +// +// TFormatterUtility.swift +// DesignSystem +// +// Created by 박민서 on 1/14/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation diff --git a/TnT/Projects/DesignSystem/Sources/Components/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TTextField.swift new file mode 100644 index 0000000..00c9c8d --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/TTextField.swift @@ -0,0 +1,9 @@ +// +// TTextField.swift +// DesignSystem +// +// Created by 박민서 on 1/14/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation diff --git a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift new file mode 100644 index 0000000..81764a0 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift @@ -0,0 +1,120 @@ +// +// TTextField.swift +// DesignSystem +// +// Created by 박민서 on 1/14/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +enum TextFieldStatus { + case empty + case focused + case invalid + case filled +} + +struct TTextField: View { + // 필수 속성 + let placeholder: String + let limitCount: Int + + // 바인딩 상태 + @Binding var text: String + @Binding var textFieldStatus: TextFieldStatus + + // 선택 속성 + let header: String? + let footer: String? + let rightButtonAction: (() -> Void)? + let unitText: String? + + // 내부 상태 + private var lineColor: Color { + switch textFieldStatus { + case .empty: return .gray + case .focused: return .blue + case .invalid: return .red + case .filled: return .green + } + } + + private var textColor: Color { + return textFieldStatus == .invalid ? .red : .black + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + // Header (옵션) + if let header = header { + Text(header) + .font(.headline) + } + + // 텍스트 필드 영역 + HStack { + TextField(placeholder, text: $text, onEditingChanged: { isEditing in + textFieldStatus = isEditing ? .focused : (text.isEmpty ? .empty : .filled) + }) + .onChange(of: text) { newValue in + if newValue.count > limitCount { + text = String(newValue.prefix(limitCount)) + textFieldStatus = .invalid + } else if !newValue.isEmpty { + textFieldStatus = .filled + } else { + textFieldStatus = .empty + } + } + .foregroundColor(textColor) + .padding(8) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(lineColor, lineWidth: 1) + ) + .keyboardType(.default) + + // Right Button (옵션) + if let action = rightButtonAction { + Button(action: action) { + Image(systemName: "arrow.right") + .foregroundColor(.blue) + .padding(8) + .background(Circle().fill(Color.gray.opacity(0.2))) + } + } + + // 단위 텍스트 (옵션) + if let unit = unitText { + Text(unit) + .foregroundColor(.gray) + .padding(.trailing, 8) + } + } + + // Footer (옵션) + if let footer = footer { + Text(footer) + .font(.caption) + .foregroundColor(.gray) + } + } + .padding(.vertical, 4) + } +} + +#Preview { + TTextField( + placeholder: "example", + limitCount: 20, + text: .constant("ㅅㄷㄴㅅㄷㄴㅅㄷ"), + textFieldStatus: .constant(.filled), + header: "오호라", + footer: "오호라2", + rightButtonAction: { + print("action") + }, + unitText: "???" + ) +} diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift index b96115f..e6e0d0d 100644 --- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift +++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift @@ -16,30 +16,22 @@ public struct Typography { /// Pretendard 폰트의 굵기(enum) 정의 public enum Weight { case thin, extraLight, light, regular, medium, semibold, bold, extrabold, black - - public var name: String { + + public var fontConvertible: DesignSystemFontConvertible { switch self { - case .thin: return "Pretendard-Thin" - case .extraLight: return "Pretendard-ExtraLight" - case .light: return "Pretendard-Light" - case .regular: return "Pretendard-Regular" - case .medium: return "Pretendard-Medium" - case .semibold: return "Pretendard-SemiBold" - case .bold: return "Pretendard-Bold" - case .extrabold: return "Pretendard-ExtraBold" - case .black: return "Pretendard-Black" + + case .thin: return DesignSystemFontFamily.Pretendard.thin + case .extraLight: return DesignSystemFontFamily.Pretendard.extraLight + case .light: return DesignSystemFontFamily.Pretendard.light + case .regular: return DesignSystemFontFamily.Pretendard.regular + case .medium: return DesignSystemFontFamily.Pretendard.medium + case .semibold: return DesignSystemFontFamily.Pretendard.semiBold + case .bold: return DesignSystemFontFamily.Pretendard.bold + case .extrabold: return DesignSystemFontFamily.Pretendard.extraBold + case .black: return DesignSystemFontFamily.Pretendard.black } } } - - /// 주어진 Weight와 크기로 커스텀 폰트를 생성합니다. - /// - Parameters: - /// - weight: Pretendard의 폰트 굵기 - /// - size: 폰트 크기 - /// - Returns: SwiftUI Font 객체 - public static func customFont(_ weight: Pretendard.Weight, size: CGFloat) -> Font { - return Font.custom(weight.name, size: size) - } } /// 폰트, 줄 높이, 줄 간격, 자간 등을 포함한 스타일 정의를 위한 구조체입니다. @@ -56,7 +48,7 @@ public struct Typography { /// - lineHeightMultiplier: 줄 높이 배율 (CGFloat) /// - letterSpacing: 자간 (CGFloat) init(_ weight: Pretendard.Weight, size: CGFloat, lineHeightMultiplier: CGFloat, letterSpacing: CGFloat) { - self.font = Pretendard.customFont(weight, size: size) + self.font = weight.fontConvertible.swiftUIFont(size: size) self.lineHeight = size * lineHeightMultiplier self.lineSpacing = (size * lineHeightMultiplier) - size self.letterSpacing = letterSpacing From a366f1786d7455fe4ab2653c409d2ebf5f99edc5 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:19:16 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[Feat]=20TTextField=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/TFormatterUtility.swift | 9 - .../Sources/Components/TTextField.swift | 9 - .../Components/TextField/TTextField.swift | 269 ++++++++++++------ 3 files changed, 183 insertions(+), 104 deletions(-) delete mode 100644 TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift delete mode 100644 TnT/Projects/DesignSystem/Sources/Components/TTextField.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift b/TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift deleted file mode 100644 index 0000231..0000000 --- a/TnT/Projects/DesignSystem/Sources/Components/TFormatterUtility.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// TFormatterUtility.swift -// DesignSystem -// -// Created by 박민서 on 1/14/25. -// Copyright © 2025 yapp25thTeamTnT. All rights reserved. -// - -import Foundation diff --git a/TnT/Projects/DesignSystem/Sources/Components/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TTextField.swift deleted file mode 100644 index 00c9c8d..0000000 --- a/TnT/Projects/DesignSystem/Sources/Components/TTextField.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// TTextField.swift -// DesignSystem -// -// Created by 박민서 on 1/14/25. -// Copyright © 2025 yapp25thTeamTnT. All rights reserved. -// - -import Foundation diff --git a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift index 81764a0..b4ea0f8 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift @@ -8,113 +8,210 @@ import SwiftUI -enum TextFieldStatus { - case empty - case focused - case invalid - case filled +public extension TTextField { + /// TextField에 표시되는 상태입니다 + enum Status { + case empty + case focused + case invalid + case valid + case filled + + /// Status에 따른 컬러웨이입니다 + var style: (statusColor: Color, textColor: Color) { + switch self { + case .empty, .filled: + return (.neutral200, .neutral400) + case .focused: + return (.neutral600, .neutral600) + case .invalid: + return (.red500, .neutral600) + case .valid: + return (.blue500, .neutral600) + } + } + } } -struct TTextField: View { - // 필수 속성 - let placeholder: String - let limitCount: Int - - // 바인딩 상태 - @Binding var text: String - @Binding var textFieldStatus: TextFieldStatus - - // 선택 속성 - let header: String? - let footer: String? - let rightButtonAction: (() -> Void)? - let unitText: String? +public extension TTextField { + /// TextField 상단 헤더 설정입니다 + struct HeaderContent { + /// 필수 여부를 표시 + let isRequired: Bool + /// 헤더의 제목 + let title: String + /// 입력 가능한 글자 수 제한 + let limitCount: Int? + } - // 내부 상태 - private var lineColor: Color { - switch textFieldStatus { - case .empty: return .gray - case .focused: return .blue - case .invalid: return .red - case .filled: return .green - } + /// TextField 우측 버튼 설정입니다 + struct ButtonContent { + /// 버튼에 표시될 텍스트 + let title: String + /// 버튼 클릭 시 실행되는 동작 + let tapAction: (() -> Void)? } +} - private var textColor: Color { - return textFieldStatus == .invalid ? .red : .black +/// TnT 앱 내에서 전반적으로 사용되는 커스텀 텍스트 필드 컴포넌트입니다. +public struct TTextField: View { + + /// Placeholder 텍스트 + private let placeholder: String + /// 상단 헤더 설정 + private let header: HeaderContent? + /// 하단 푸터 텍스트 + private let footerText: String? + /// 우측 단위 텍스트 + private let unitText: String? + /// 우측 버튼 설정 + private let button: ButtonContent? + + /// 입력 텍스트 + @Binding private var text: String + /// 텍스트 필드 상태 + @Binding private var status: Status + + /// - Parameters: + /// - placeholder: Placeholder 텍스트 (기본값: "내용을 입력해주세요") + /// - header: 상단 헤더 설정 (옵션) + /// - footerText: 하단 푸터 텍스트 (옵션) + /// - unitText: 우측 단위 텍스트 (옵션) + /// - button: 우측 버튼 설정 (옵션) + /// - text: 입력 텍스트 (Binding) + /// - textFieldStatus: 텍스트 필드 상태 (Binding) + public init( + placeholder: String = "내용을 입력해주세요", + header: HeaderContent? = nil, + footerText: String? = nil, + unitText: String? = nil, + button: ButtonContent? = nil, + text: Binding, + textFieldStatus: Binding + ) { + self.placeholder = placeholder + self.header = header + self.footerText = footerText + self.unitText = unitText + self.button = button + self._text = text + self._status = textFieldStatus } - var body: some View { + public var body: some View { VStack(alignment: .leading, spacing: 8) { - // Header (옵션) - if let header = header { - Text(header) - .font(.headline) + // Header + if let header { + HStack(spacing: 0) { + Text(header.title) + .typographyStyle(.body1Bold, with: .neutral900) + if header.isRequired { + Text("*") + .typographyStyle(.body1Bold, with: .red500) + } + + Spacer() + + if let limitCount = header.limitCount { + Text("\(text.count)/\(limitCount)자") + .typographyStyle(.label1Medium, with: .neutral400) + .padding(.horizontal, 4) + .padding(.vertical, 2) + } + } } - // 텍스트 필드 영역 - HStack { - TextField(placeholder, text: $text, onEditingChanged: { isEditing in - textFieldStatus = isEditing ? .focused : (text.isEmpty ? .empty : .filled) - }) - .onChange(of: text) { newValue in - if newValue.count > limitCount { - text = String(newValue.prefix(limitCount)) - textFieldStatus = .invalid - } else if !newValue.isEmpty { - textFieldStatus = .filled - } else { - textFieldStatus = .empty + // Text Field + VStack(spacing: 0) { + HStack(spacing: 0) { + TextField(placeholder, text: $text) + .multilineTextAlignment(unitText != nil ? .trailing : .leading) + .padding(8) + + if let unitText { + Text(unitText) + .typographyStyle(.body1Medium, with: status.style.textColor) + .padding(.horizontal, 12) + .padding(.vertical, 3) } - } - .foregroundColor(textColor) - .padding(8) - .background( - RoundedRectangle(cornerRadius: 8) - .stroke(lineColor, lineWidth: 1) - ) - .keyboardType(.default) - - // Right Button (옵션) - if let action = rightButtonAction { - Button(action: action) { - Image(systemName: "arrow.right") - .foregroundColor(.blue) - .padding(8) - .background(Circle().fill(Color.gray.opacity(0.2))) + + if let button { + // TODO: 추후 버튼 컴포넌트 나오면 대체 + Button(action: button.tapAction ?? { print("Button tapped") }) { + Text(button.title) + .typographyStyle(.label2Medium, with: .neutral50) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background(Color.neutral900) + .clipShape(.rect(cornerRadius: 8)) + } + .padding(.vertical, 5) } } - // 단위 텍스트 (옵션) - if let unit = unitText { - Text(unit) - .foregroundColor(.gray) - .padding(.trailing, 8) - } + TDivider(color: status.style.statusColor) } - // Footer (옵션) - if let footer = footer { + // Footer + if let footer = footerText { Text(footer) - .font(.caption) - .foregroundColor(.gray) + .typographyStyle(.body2Medium, with: status.style.statusColor) } } - .padding(.vertical, 4) } } #Preview { - TTextField( - placeholder: "example", - limitCount: 20, - text: .constant("ㅅㄷㄴㅅㄷㄴㅅㄷ"), - textFieldStatus: .constant(.filled), - header: "오호라", - footer: "오호라2", - rightButtonAction: { - print("action") - }, - unitText: "???" - ) + ScrollView { + VStack(spacing: 16) { + // Empty 상태 + TTextField( + header: .init(isRequired: true, title: "필수 입력", limitCount: 10), + footerText: "빈 필드 상태입니다.", + text: .constant(""), + textFieldStatus: .constant(.empty) + ) + .padding() + + // Focused 상태 + TTextField( + header: .init(isRequired: false, title: "내 초대 코드", limitCount: 20), + footerText: "현재 입력 중입니다.", + button: .init(title: "확인", tapAction: { print("확인 버튼 클릭") }), + text: .constant("입력 중..."), + textFieldStatus: .constant(.focused) + ) + .padding() + + // Invalid 상태 + TTextField( + header: .init(isRequired: false, title: "이메일", limitCount: nil), + footerText: "유효하지 않은 입력입니다.", + text: .constant("invalid email"), + textFieldStatus: .constant(.invalid) + ) + .padding() + + // Valid 상태 + TTextField( + header: .init(isRequired: false, title: "이름", limitCount: nil), + footerText: "올바른 입력입니다.", + text: .constant("John Doe"), + textFieldStatus: .constant(.valid) + ) + .padding() + + // Filled 상태 + TTextField( + header: .init(isRequired: true, title: "주소", limitCount: 50), + footerText: "최대 글자 수를 초과하지 않도록 작성하세요.", + unitText: "단위", + button: .init(title: "검색", tapAction: { print("검색 버튼 클릭") }), + text: .constant("서울특별시 강남구 테헤란로"), + textFieldStatus: .constant(.filled) + ) + .padding() + } + } } + From b9f1fbe21d6fba30f91000dc6446ee64527f94d1 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:19:26 +0900 Subject: [PATCH 03/10] =?UTF-8?q?TDivider=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/Divider/TDivider.swift | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift b/TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift index 0cd54e1..6035688 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift @@ -6,4 +6,26 @@ // Copyright © 2025 yapp25thTeamTnT. All rights reserved. // -import Foundation +import SwiftUI + +/// TnT 앱 내에서 전반적으로 사용되는 커스텀 디바이더입니다 +public struct TDivider: View { + /// Divider 높이 + let height: CGFloat + /// Divider 컬러 + let color: Color + + /// - Parameters: + /// - height: Divider의 두께 (기본값: `1`) + /// - color: Divider의 색상 + public init(height: CGFloat = 1, color: Color) { + self.height = height + self.color = color + } + + public var body: some View { + Rectangle() + .fill(color) + .frame(height: height) + } +} From 2edfdb0cc2f344c16a4af17fd0ef544d3a5af0f6 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:20:42 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[Chore]=20=ED=94=84=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C,=20extension=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9C=84=EC=B9=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/TextField/TTextField.swift | 135 ++++++------------ 1 file changed, 40 insertions(+), 95 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift index b4ea0f8..27bd5af 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift @@ -8,51 +8,6 @@ import SwiftUI -public extension TTextField { - /// TextField에 표시되는 상태입니다 - enum Status { - case empty - case focused - case invalid - case valid - case filled - - /// Status에 따른 컬러웨이입니다 - var style: (statusColor: Color, textColor: Color) { - switch self { - case .empty, .filled: - return (.neutral200, .neutral400) - case .focused: - return (.neutral600, .neutral600) - case .invalid: - return (.red500, .neutral600) - case .valid: - return (.blue500, .neutral600) - } - } - } -} - -public extension TTextField { - /// TextField 상단 헤더 설정입니다 - struct HeaderContent { - /// 필수 여부를 표시 - let isRequired: Bool - /// 헤더의 제목 - let title: String - /// 입력 가능한 글자 수 제한 - let limitCount: Int? - } - - /// TextField 우측 버튼 설정입니다 - struct ButtonContent { - /// 버튼에 표시될 텍스트 - let title: String - /// 버튼 클릭 시 실행되는 동작 - let tapAction: (() -> Void)? - } -} - /// TnT 앱 내에서 전반적으로 사용되는 커스텀 텍스트 필드 컴포넌트입니다. public struct TTextField: View { @@ -161,57 +116,47 @@ public struct TTextField: View { } } -#Preview { - ScrollView { - VStack(spacing: 16) { - // Empty 상태 - TTextField( - header: .init(isRequired: true, title: "필수 입력", limitCount: 10), - footerText: "빈 필드 상태입니다.", - text: .constant(""), - textFieldStatus: .constant(.empty) - ) - .padding() - - // Focused 상태 - TTextField( - header: .init(isRequired: false, title: "내 초대 코드", limitCount: 20), - footerText: "현재 입력 중입니다.", - button: .init(title: "확인", tapAction: { print("확인 버튼 클릭") }), - text: .constant("입력 중..."), - textFieldStatus: .constant(.focused) - ) - .padding() - - // Invalid 상태 - TTextField( - header: .init(isRequired: false, title: "이메일", limitCount: nil), - footerText: "유효하지 않은 입력입니다.", - text: .constant("invalid email"), - textFieldStatus: .constant(.invalid) - ) - .padding() - - // Valid 상태 - TTextField( - header: .init(isRequired: false, title: "이름", limitCount: nil), - footerText: "올바른 입력입니다.", - text: .constant("John Doe"), - textFieldStatus: .constant(.valid) - ) - .padding() - - // Filled 상태 - TTextField( - header: .init(isRequired: true, title: "주소", limitCount: 50), - footerText: "최대 글자 수를 초과하지 않도록 작성하세요.", - unitText: "단위", - button: .init(title: "검색", tapAction: { print("검색 버튼 클릭") }), - text: .constant("서울특별시 강남구 테헤란로"), - textFieldStatus: .constant(.filled) - ) - .padding() +public extension TTextField { + /// TextField에 표시되는 상태입니다 + enum Status { + case empty + case focused + case invalid + case valid + case filled + + /// Status에 따른 컬러웨이입니다 + var style: (statusColor: Color, textColor: Color) { + switch self { + case .empty, .filled: + return (.neutral200, .neutral400) + case .focused: + return (.neutral600, .neutral600) + case .invalid: + return (.red500, .neutral600) + case .valid: + return (.blue500, .neutral600) + } } } } + +public extension TTextField { + /// TextField 상단 헤더 설정입니다 + struct HeaderContent { + /// 필수 여부를 표시 + let isRequired: Bool + /// 헤더의 제목 + let title: String + /// 입력 가능한 글자 수 제한 + let limitCount: Int? + } + /// TextField 우측 버튼 설정입니다 + struct ButtonContent { + /// 버튼에 표시될 텍스트 + let title: String + /// 버튼 클릭 시 실행되는 동작 + let tapAction: (() -> Void)? + } +} From e694a6a8566cc55830999b009b41e3d22742b0f9 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:53:37 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[Feat]=20Header,=20ButtonContent=20init?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/TextField/TTextField.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift index 27bd5af..48eb830 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift @@ -150,6 +150,12 @@ public extension TTextField { let title: String /// 입력 가능한 글자 수 제한 let limitCount: Int? + + public init(isRequired: Bool, title: String, limitCount: Int?) { + self.isRequired = isRequired + self.title = title + self.limitCount = limitCount + } } /// TextField 우측 버튼 설정입니다 @@ -158,5 +164,10 @@ public extension TTextField { let title: String /// 버튼 클릭 시 실행되는 동작 let tapAction: (() -> Void)? + + public init(title: String, tapAction: (() -> Void)?) { + self.title = title + self.tapAction = tapAction + } } } From e6c20ffd1cd2a432ec9b0b100986b04810e130f9 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:54:01 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[Feat]=20=ED=82=A4=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/TextField/TTextField.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift index 48eb830..c1f5f73 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift @@ -21,6 +21,8 @@ public struct TTextField: View { private let unitText: String? /// 우측 버튼 설정 private let button: ButtonContent? + /// 키보드 타입 + private let keyboardType: UIKeyboardType /// 입력 텍스트 @Binding private var text: String @@ -33,6 +35,7 @@ public struct TTextField: View { /// - footerText: 하단 푸터 텍스트 (옵션) /// - unitText: 우측 단위 텍스트 (옵션) /// - button: 우측 버튼 설정 (옵션) + /// - keyboardType: 텍스트 필드 키보드 타입 (기본값: .default) /// - text: 입력 텍스트 (Binding) /// - textFieldStatus: 텍스트 필드 상태 (Binding) public init( @@ -41,6 +44,7 @@ public struct TTextField: View { footerText: String? = nil, unitText: String? = nil, button: ButtonContent? = nil, + keyboardType: UIKeyboardType = .default, text: Binding, textFieldStatus: Binding ) { @@ -49,6 +53,7 @@ public struct TTextField: View { self.footerText = footerText self.unitText = unitText self.button = button + self.keyboardType = keyboardType self._text = text self._status = textFieldStatus } @@ -80,6 +85,7 @@ public struct TTextField: View { VStack(spacing: 0) { HStack(spacing: 0) { TextField(placeholder, text: $text) + .keyboardType(keyboardType) .multilineTextAlignment(unitText != nil ? .trailing : .leading) .padding(8) From fa5e43a6201ac91e347637e1d20104ee5953a24f Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Wed, 15 Jan 2025 07:43:06 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[Feat]=20Typo=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20TextGeight=20=EA=B3=84=EC=82=B0=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Font+DesignSystem.swift | 4 ++ .../Sources/Utility/TextUtility.swift | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 TnT/Projects/DesignSystem/Sources/Utility/TextUtility.swift diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift index e6e0d0d..3718792 100644 --- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift +++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Font+DesignSystem.swift @@ -37,6 +37,8 @@ public struct Typography { /// 폰트, 줄 높이, 줄 간격, 자간 등을 포함한 스타일 정의를 위한 구조체입니다. public struct FontStyle { public let font: Font + public let uiFont: UIFont + public let size: CGFloat public let lineHeight: CGFloat public let lineSpacing: CGFloat public let letterSpacing: CGFloat @@ -49,6 +51,8 @@ public struct Typography { /// - letterSpacing: 자간 (CGFloat) init(_ weight: Pretendard.Weight, size: CGFloat, lineHeightMultiplier: CGFloat, letterSpacing: CGFloat) { self.font = weight.fontConvertible.swiftUIFont(size: size) + self.uiFont = weight.fontConvertible.font(size: size) + self.size = size self.lineHeight = size * lineHeightMultiplier self.lineSpacing = (size * lineHeightMultiplier) - size self.letterSpacing = letterSpacing diff --git a/TnT/Projects/DesignSystem/Sources/Utility/TextUtility.swift b/TnT/Projects/DesignSystem/Sources/Utility/TextUtility.swift new file mode 100644 index 0000000..dafe094 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Utility/TextUtility.swift @@ -0,0 +1,50 @@ +// +// TextUtility.swift +// DesignSystem +// +// Created by 박민서 on 1/15/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import UIKit + +public struct TextUtility { + /// 주어진 텍스트와 스타일을 기준으로 텍스트 높이를 계산합니다. + /// + /// 이 함수는 `NSTextStorage`, `NSLayoutManager`, `NSTextContainer`를 사용하여 텍스트 렌더링의 실제 높이를 계산합니다. + /// 텍스트가 주어진 너비를 초과할 경우 줄바꿈과 스타일을 고려하여 계산된 높이를 반환합니다. + /// + /// - Parameters: + /// - boxWidth: 텍스트가 렌더링될 컨테이너의 가로 길이. (최대 너비) + /// - text: 높이를 계산할 텍스트 문자열. + /// - style: `Typography.FontStyle`로 정의된 폰트, 줄 간격, 자간 등의 스타일. + /// - Returns: 주어진 스타일로 렌더링된 텍스트의 높이 (CGFloat). + static func calculateTextHeight( + boxWidth: CGFloat, + text: String, + style: Typography.FontStyle + ) -> CGFloat { + // 1. 텍스트 렌더링을 위한 핵심 클래스 설정 + let textStorage: NSTextStorage = NSTextStorage(string: text) + let textContainer: NSTextContainer = NSTextContainer(size: CGSize(width: boxWidth, height: .greatestFiniteMagnitude)) + let layoutManager: NSLayoutManager = NSLayoutManager() + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + // 2. 텍스트 스타일 지정 + let paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = style.lineSpacing + paragraphStyle.alignment = .left + + textStorage.addAttributes([ + .font: style.uiFont, + .paragraphStyle: paragraphStyle, + .kern: style.letterSpacing + ], range: NSRange(location: 0, length: text.count)) + + // 3. 텍스트 높이 계산 + let estimatedHeight: CGFloat = layoutManager.usedRect(for: textContainer).height + return estimatedHeight + } +} From 6b9e7dc3786d5f7ee381ac4b8a99ed9626b95a07 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Wed, 15 Jan 2025 07:44:47 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[Feat]=20TTextField=20Header,=20Footer=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC,=20ViewModifier=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/TextField/TTextField.swift | 305 +++++++++++------- 1 file changed, 192 insertions(+), 113 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift index c1f5f73..473bee0 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextField.swift @@ -11,169 +11,248 @@ import SwiftUI /// TnT 앱 내에서 전반적으로 사용되는 커스텀 텍스트 필드 컴포넌트입니다. public struct TTextField: View { + /// 텍스트 필드 우측에 표시될 RightView + private let rightView: RightView? /// Placeholder 텍스트 private let placeholder: String - /// 상단 헤더 설정 - private let header: HeaderContent? - /// 하단 푸터 텍스트 - private let footerText: String? - /// 우측 단위 텍스트 - private let unitText: String? - /// 우측 버튼 설정 - private let button: ButtonContent? - /// 키보드 타입 - private let keyboardType: UIKeyboardType /// 입력 텍스트 @Binding private var text: String /// 텍스트 필드 상태 @Binding private var status: Status + /// 텍스트 필드 포커스 상태 + @FocusState var isFocused: Bool /// - Parameters: /// - placeholder: Placeholder 텍스트 (기본값: "내용을 입력해주세요") - /// - header: 상단 헤더 설정 (옵션) - /// - footerText: 하단 푸터 텍스트 (옵션) - /// - unitText: 우측 단위 텍스트 (옵션) - /// - button: 우측 버튼 설정 (옵션) - /// - keyboardType: 텍스트 필드 키보드 타입 (기본값: .default) /// - text: 입력 텍스트 (Binding) /// - textFieldStatus: 텍스트 필드 상태 (Binding) + /// - rightView: 텍스트 필드 우측에 표시될 `TTextField.RightView`를 정의하는 클로저. public init( placeholder: String = "내용을 입력해주세요", - header: HeaderContent? = nil, - footerText: String? = nil, - unitText: String? = nil, - button: ButtonContent? = nil, - keyboardType: UIKeyboardType = .default, text: Binding, - textFieldStatus: Binding + textFieldStatus: Binding, + @ViewBuilder rightView: () -> RightView? = { nil } ) { self.placeholder = placeholder - self.header = header - self.footerText = footerText - self.unitText = unitText - self.button = button - self.keyboardType = keyboardType self._text = text self._status = textFieldStatus + self.rightView = rightView() } - + public var body: some View { - VStack(alignment: .leading, spacing: 8) { - // Header - if let header { - HStack(spacing: 0) { - Text(header.title) - .typographyStyle(.body1Bold, with: .neutral900) - if header.isRequired { - Text("*") - .typographyStyle(.body1Bold, with: .red500) - } - - Spacer() - - if let limitCount = header.limitCount { - Text("\(text.count)/\(limitCount)자") - .typographyStyle(.label1Medium, with: .neutral400) - .padding(.horizontal, 4) - .padding(.vertical, 2) - } + // Text Field + VStack(spacing: 0) { + HStack(spacing: 0) { + TextField(placeholder, text: $text) + .autocorrectionDisabled() + .focused($isFocused) + .font(Typography.FontStyle.body1Medium.font) + .lineSpacing(Typography.FontStyle.body1Medium.lineSpacing) + .kerning(Typography.FontStyle.body1Medium.letterSpacing) + .tint(Color.neutral800) + .foregroundStyle(status.textColor) + .padding(8) + .frame(height: 42) + + if let rightView { + rightView } } - // Text Field - VStack(spacing: 0) { - HStack(spacing: 0) { - TextField(placeholder, text: $text) - .keyboardType(keyboardType) - .multilineTextAlignment(unitText != nil ? .trailing : .leading) - .padding(8) + TDivider(color: status.underlineColor(isFocused: isFocused)) + } + } +} - if let unitText { - Text(unitText) - .typographyStyle(.body1Medium, with: status.style.textColor) - .padding(.horizontal, 12) - .padding(.vertical, 3) - } - - if let button { - // TODO: 추후 버튼 컴포넌트 나오면 대체 - Button(action: button.tapAction ?? { print("Button tapped") }) { - Text(button.title) - .typographyStyle(.label2Medium, with: .neutral50) - .padding(.horizontal, 12) - .padding(.vertical, 7) - .background(Color.neutral900) - .clipShape(.rect(cornerRadius: 8)) - } - .padding(.vertical, 5) - } - } +public extension TTextField.RightView { + /// + enum Style { + case unit(text: String, status: TTextField.Status) + case button(title: String, tapAction: () -> Void) + } +} + +public extension TTextField { + /// TextField 우측 컨텐츠 뷰입니다 + struct RightView: View { + /// 컨텐츠 스타일 + private let style: RightView.Style + + public init(style: RightView.Style) { + self.style = style + } + + public var body: some View { + + switch style { + case let .unit(text, status): + Text(text) + .typographyStyle(.body1Medium, with: status.textColor) + .padding(.horizontal, 12) + .padding(.vertical, 3) - TDivider(color: status.style.statusColor) + case let .button(title, tapAction): + // TODO: 추후 버튼 컴포넌트 나오면 대체 + Button(action: tapAction) { + Text(title) + .typographyStyle(.label2Medium, with: .neutral50) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background(Color.neutral900) + .clipShape(.rect(cornerRadius: 8)) + } + .padding(.vertical, 5) } - - // Footer - if let footer = footerText { - Text(footer) - .typographyStyle(.body2Medium, with: status.style.statusColor) + } + } + + /// TextField 상단 헤더입니다 + struct Header: View { + /// 필수 여부를 표시 + private let isRequired: Bool + /// 헤더의 제목 + private let title: String + /// 입력 가능한 글자 수 제한 + private let limitCount: Int? + /// 입력된 텍스트 + @Binding private var text: String + + public init( + isRequired: Bool, + title: String, + limitCount: Int?, + text: Binding + ) { + self.isRequired = isRequired + self.title = title + self.limitCount = limitCount + self._text = text + } + + public var body: some View { + HStack(spacing: 0) { + Text(title) + .typographyStyle(.body1Bold, with: .neutral900) + if isRequired { + Text("*") + .typographyStyle(.body1Bold, with: .red500) + } + + Spacer() + + if let limitCount { + Text("\(text.count)/\(limitCount)자") + .typographyStyle(.label1Medium, with: .neutral400) + .padding(.horizontal, 4) + .padding(.vertical, 2) + } } } } + + /// TextField 하단 푸터입니다 + struct Footer: View { + /// 푸터 텍스트 + private let footerText: String + /// 텍스트 필드 상태 + @Binding private var status: Status + + public init(footerText: String, status: Binding) { + self.footerText = footerText + self._status = status + } + + public var body: some View { + Text(footerText) + .typographyStyle(.body2Medium, with: status.footerColor) + } + } } public extension TTextField { /// TextField에 표시되는 상태입니다 enum Status { case empty - case focused + case filled case invalid case valid - case filled - /// Status에 따른 컬러웨이입니다 - var style: (statusColor: Color, textColor: Color) { + /// 밑선 색상 설정 + func underlineColor(isFocused: Bool) -> Color { + switch self { + case .empty: + return isFocused ? .neutral600 : .neutral200 + case .filled: + return isFocused ? .neutral600 : .neutral200 + case .invalid: + return .red500 + case .valid: + return .blue500 + } + } + + /// 텍스트 색상 설정 + var textColor: Color { + switch self { + case .empty: + return .neutral400 + case .filled, .invalid, .valid: + return .neutral600 + } + } + + /// 푸터 색상 설정 + var footerColor: Color { switch self { case .empty, .filled: - return (.neutral200, .neutral400) - case .focused: - return (.neutral600, .neutral600) + return .clear case .invalid: - return (.red500, .neutral600) + return .red500 case .valid: - return (.blue500, .neutral600) + return .blue500 } } } } -public extension TTextField { - /// TextField 상단 헤더 설정입니다 - struct HeaderContent { - /// 필수 여부를 표시 - let isRequired: Bool - /// 헤더의 제목 - let title: String - /// 입력 가능한 글자 수 제한 - let limitCount: Int? - - public init(isRequired: Bool, title: String, limitCount: Int?) { - self.isRequired = isRequired - self.title = title - self.limitCount = limitCount - } +struct TTextFieldModifier: ViewModifier { + /// Textfield 상단에 표시될 헤더 + private let header: TTextField.Header? + /// Textfield 하단에 표시될 푸터 + private let footer: TTextField.Footer? + + public init(header: TTextField.Header?, footer: TTextField.Footer?) { + self.header = header + self.footer = footer } - /// TextField 우측 버튼 설정입니다 - struct ButtonContent { - /// 버튼에 표시될 텍스트 - let title: String - /// 버튼 클릭 시 실행되는 동작 - let tapAction: (() -> Void)? - - public init(title: String, tapAction: (() -> Void)?) { - self.title = title - self.tapAction = tapAction + func body(content: Content) -> some View { + VStack(alignment: .leading, spacing: 8) { + // 헤더 추가 + if let header { + header + } + + // 본체(TextField) + content + + // 푸터 추가 + if let footer { + footer + } } } } + +public extension TTextField { + /// 헤더와 푸터를 포함한 레이아웃을 텍스트 필드에 적용합니다. + /// + /// - Parameters: + /// - header: `TTextField.Header`로 정의된 상단 헤더. (옵션) + /// - footer: `TTextField.Footer`로 정의된 하단 푸터. (옵션) + /// - Returns: 헤더와 푸터가 포함된 새로운 View. + func withSectionLayout(header: TTextField.Header? = nil, footer: TTextField.Footer? = nil) -> some View { + self.modifier(TTextFieldModifier(header: header, footer: footer)) + } +} From e032b09855577d8b6addd9942d50614d0376dea8 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Wed, 15 Jan 2025 07:45:29 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[Feat]=20TTextEditor=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/TextField/TTextEditor.swift | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/TextField/TTextEditor.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextEditor.swift b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextEditor.swift new file mode 100644 index 0000000..56c9a54 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/TextField/TTextEditor.swift @@ -0,0 +1,194 @@ +// +// TTextEditor.swift +// DesignSystem +// +// Created by 박민서 on 1/14/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// TnT 앱 내에서 전반적으로 사용되는 커스텀 텍스트 에디터 컴포넌트입니다. +public struct TTextEditor: View { + + /// TextEditor 수평 패딩 값 + private static let horizontalPadding: CGFloat = 16 + /// TextEditor 수직 패딩 값 + private static let verticalPadding: CGFloat = 12 + /// TextEditor 기본 높이값 + public static let defaultHeight: CGFloat = 130 + + /// 하단에 표시되는 푸터 뷰 + private let footer: Footer? + /// Placeholder 텍스트 + private let placeholder: String + /// 텍스트 필드 상태 + @Binding private var status: Status + /// 입력된 텍스트 + @Binding private var text: String + + /// 내부에서 동적으로 관리되는 텍스트 에디터 높이 + @State private var textHeight: CGFloat = defaultHeight + /// 텍스트 에디터 포커스 상태 + @FocusState var isFocused: Bool + + /// TTextEditor 생성자 + /// - Parameters: + /// - placeholder: Placeholder 텍스트 (기본값: "내용을 입력해주세요"). + /// - text: 입력된 텍스트를 관리하는 바인딩. + /// - textEditorStatus: 텍스트 에디터 상태를 관리하는 바인딩. + /// - footer: Textfield 하단에 표시될 `TTextEditor.FooterView`를 정의하는 클로저. + public init( + placeholder: String = "내용을 입력해주세요", + text: Binding, + textEditorStatus: Binding, + footer: () -> Footer? = { nil } + ) { + self.placeholder = placeholder + self._text = text + self._status = textEditorStatus + self.footer = footer() + } + + public var body: some View { + GeometryReader { geometry in + VStack(alignment: .leading, spacing: 8) { + ZStack(alignment: .topLeading) { + TextEditor(text: $text) + .autocorrectionDisabled() + .scrollDisabled(true) + .focused($isFocused) + .font(Typography.FontStyle.body1Medium.font) + .lineSpacing(Typography.FontStyle.body1Medium.lineSpacing) + .kerning(Typography.FontStyle.body1Medium.letterSpacing) + .tint(Color.neutral800) + .frame(minHeight: textHeight, maxHeight: .infinity) + .padding(.vertical, TTextEditor.verticalPadding) + .padding(.horizontal, TTextEditor.horizontalPadding) + .background(Color.common0) + .scrollContentBackground(.hidden) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(status.borderColor(isFocused: isFocused), lineWidth: status.borderWidth(isFocused: isFocused)) + ) + .onChange(of: text) { + withAnimation { + textHeight = getNewHeight(geometry: geometry) + } + } + .onAppear { + textHeight = getNewHeight(geometry: geometry) + } + + if text.isEmpty { + Text(placeholder) + .typographyStyle(.body1Medium, with: .neutral400) + .padding(.vertical, TTextEditor.verticalPadding + 8) + .padding(.horizontal, TTextEditor.horizontalPadding + 4) + } + } + if let footer { + footer + } + } + } + .frame(height: TTextEditor.defaultHeight) + } + + private func getNewHeight(geometry: GeometryProxy) -> CGFloat { + let newHeight: CGFloat = TextUtility.calculateTextHeight( + boxWidth: geometry.size.width - TTextEditor.horizontalPadding * 2, + text: text, + style: .body1Medium + ) + TTextEditor.verticalPadding * 2 + return max(newHeight, TTextEditor.defaultHeight) + } +} + +public extension TTextEditor { + /// TTextEditor의 Footer입니다 + struct Footer: View { + /// 최대 입력 가능 글자 수 + private let textLimit: Int + + /// 텍스트 필드 상태 + @Binding private var status: Status + /// 입력된 텍스트 + @Binding private var text: String + + /// Footer 생성자 + /// - Parameters: + /// - textLimit: 최대 입력 가능 글자 수. + /// - status: 텍스트 에디터의 상태를 관리하는 바인딩. + /// - text: 입력된 텍스트를 관리하는 바인딩. + public init( + textLimit: Int, + status: Binding, + text: Binding + ) { + self.textLimit = textLimit + self._status = status + self._text = text + } + + public var body: some View { + HStack { + Spacer() + Text("\(text.count)/\(textLimit)") + .typographyStyle(.label2Medium, with: status.footerColor) + } + } + } +} + +public extension TTextEditor { + /// TextEditor에 표시되는 상태입니다 + enum Status { + case empty + case filled + case invalid + + /// 테두리 두께 설정 + func borderWidth(isFocused: Bool) -> CGFloat { + switch self { + case .invalid: + return 2 // Focus와 상관없이 2 + default: + return isFocused ? 2 : 1 + } + } + + /// 테두리 색상 설정 + func borderColor(isFocused: Bool) -> Color { + switch self { + case .invalid: + return .red500 // Focus와 상관없이 .red500 + case .empty: + return isFocused ? .neutral900 : .neutral200 + case .filled: + return isFocused ? .neutral900 : .neutral600 + } + } + + /// 텍스트 색상 설정 + var textColor: Color { + switch self { + case .empty: + return .neutral400 + case .filled, .invalid: + return .neutral600 + } + } + + /// 푸터 색상 설정 + var footerColor: Color { + switch self { + case .empty, .filled: + return .neutral300 + case .invalid: + return .red500 + } + } + } +} From f29fba19b8f9daeeaf10886584119d72c7e1dbb5 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Wed, 15 Jan 2025 08:06:43 +0900 Subject: [PATCH 10/10] =?UTF-8?q?[Setting]=20=EB=9D=BC=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=20=EB=AA=A8=EB=93=9C=20=EA=B3=A0=EC=A0=95=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TnT/Tuist/Config/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TnT/Tuist/Config/Info.plist b/TnT/Tuist/Config/Info.plist index 39592c4..db58c8f 100644 --- a/TnT/Tuist/Config/Info.plist +++ b/TnT/Tuist/Config/Info.plist @@ -62,5 +62,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIUserInterfaceStyle + Light