-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from YAPP-Github/TNT-139-createMinComponents-T…
…TextField [TNT-139] TTextField, TTextEditor 컴포넌트 코드 작성
- Loading branch information
Showing
6 changed files
with
552 additions
and
21 deletions.
There are no files selected for viewing
31 changes: 31 additions & 0 deletions
31
TnT/Projects/DesignSystem/Sources/Components/Divider/TDivider.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// TDivider.swift | ||
// DesignSystem | ||
// | ||
// Created by 박민서 on 1/14/25. | ||
// Copyright © 2025 yapp25thTeamTnT. All rights reserved. | ||
// | ||
|
||
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) | ||
} | ||
} |
194 changes: 194 additions & 0 deletions
194
TnT/Projects/DesignSystem/Sources/Components/TextField/TTextEditor.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String>, | ||
textEditorStatus: Binding<Status>, | ||
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<Status>, | ||
text: Binding<String> | ||
) { | ||
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 | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.