|
| 1 | +// |
| 2 | +// TBoxTextEditor.swift |
| 3 | +// DesignSystem |
| 4 | +// |
| 5 | +// Created by ๋ฐ๋ฏผ์ on 11/22/25. |
| 6 | +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. |
| 7 | +// |
| 8 | + |
| 9 | +import SwiftUI |
| 10 | + |
| 11 | +/// ๋ฐ์คํ ํ
๋๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ์ปค์คํ
ํ
์คํธ ์๋ํฐ์
๋๋ค. |
| 12 | +/// ๊ธฐ์กด TTextEditor์ ๋์ผํ ์ธํฐํ์ด์ค๋ก ์ํ/ํธํฐ๋ฅผ ์ง์ํฉ๋๋ค. |
| 13 | +public struct TBoxTextEditor: View { |
| 14 | + |
| 15 | + /// TextEditor ์ํ ํจ๋ฉ ๊ฐ |
| 16 | + private static let horizontalPadding: CGFloat = 16 |
| 17 | + /// TextEditor ์์ง ํจ๋ฉ ๊ฐ |
| 18 | + private static let verticalPadding: CGFloat = 12 |
| 19 | + /// TextEditor ๊ธฐ๋ณธ ๋์ด๊ฐ |
| 20 | + public static let defaultHeight: CGFloat = 40 |
| 21 | + |
| 22 | + /// ํ๋จ์ ํ์๋๋ ํธํฐ ๋ทฐ |
| 23 | + private let footer: Footer? |
| 24 | + /// Placeholder ํ
์คํธ |
| 25 | + private let placeholder: String |
| 26 | + /// ํ
์คํธ ์๋ํฐ ์ฌ์ด์ฆ |
| 27 | + private let size: Size |
| 28 | + /// ํ
์คํธ ํ๋ ์ํ |
| 29 | + @Binding private var status: Status |
| 30 | + /// ์
๋ ฅ๋ ํ
์คํธ |
| 31 | + @Binding private var text: String |
| 32 | + |
| 33 | + /// ๋ด๋ถ์์ ๋์ ์ผ๋ก ๊ด๋ฆฌ๋๋ ํ
์คํธ ์๋ํฐ ๋์ด |
| 34 | + @State private var textHeight: CGFloat = defaultHeight |
| 35 | + /// ํ
์คํธ ์๋ํฐ ํฌ์ปค์ค ์ํ |
| 36 | + @FocusState var isFocused: Bool |
| 37 | + |
| 38 | + /// TBoxTextEditor ์์ฑ์ |
| 39 | + /// - Parameters: |
| 40 | + /// - placeholder: Placeholder ํ
์คํธ (๊ธฐ๋ณธ๊ฐ: "๋ด์ฉ์ ์
๋ ฅํด์ฃผ์ธ์"). |
| 41 | + /// - size: ํ
์คํธ ์๋ํฐ ์ฌ์ด์ฆ. |
| 42 | + /// - text: ์
๋ ฅ๋ ํ
์คํธ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐ์ธ๋ฉ. |
| 43 | + /// - textEditorStatus: ํ
์คํธ ์๋ํฐ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐ์ธ๋ฉ. |
| 44 | + /// - footer: TextEditor ํ๋จ์ ํ์๋ `TBoxTextEditor.Footer`๋ฅผ ์ ์ํ๋ ํด๋ก์ . |
| 45 | + public init( |
| 46 | + placeholder: String = "๋ด์ฉ์ ์
๋ ฅํด์ฃผ์ธ์", |
| 47 | + size: Size = .large, |
| 48 | + text: Binding<String>, |
| 49 | + textEditorStatus: Binding<Status>, |
| 50 | + footer: () -> Footer? = { nil } |
| 51 | + ) { |
| 52 | + self.placeholder = placeholder |
| 53 | + self.size = size |
| 54 | + self._text = text |
| 55 | + self._status = textEditorStatus |
| 56 | + self.footer = footer() |
| 57 | + } |
| 58 | + |
| 59 | + public var body: some View { |
| 60 | + VStack(alignment: .leading, spacing: 8) { |
| 61 | + ZStack(alignment: .topLeading) { |
| 62 | + TextEditor(text: $text) |
| 63 | + .autocorrectionDisabled() |
| 64 | + .scrollDisabled(true) |
| 65 | + .focused($isFocused) |
| 66 | + .font(Typography.FontStyle.body1Medium.font) |
| 67 | + .lineSpacing(Typography.FontStyle.body1Medium.lineSpacing) |
| 68 | + .kerning(Typography.FontStyle.body1Medium.letterSpacing) |
| 69 | + .foregroundColor(status.textColor) |
| 70 | + .tint(Color.neutral800) |
| 71 | + .frame(minHeight: textHeight, maxHeight: .infinity) |
| 72 | + .padding(.vertical, TBoxTextEditor.verticalPadding) |
| 73 | + .padding(.horizontal, TBoxTextEditor.horizontalPadding) |
| 74 | + .background(Color.common0) |
| 75 | + .scrollContentBackground(.hidden) |
| 76 | + .cornerRadius(8) |
| 77 | + .overlay( |
| 78 | + RoundedRectangle(cornerRadius: 8) |
| 79 | + .stroke(status.borderColor(isFocused: isFocused), lineWidth: 1) |
| 80 | + ) |
| 81 | + .frame(height: size.height) |
| 82 | + |
| 83 | + if text.isEmpty { |
| 84 | + Text(placeholder) |
| 85 | + .typographyStyle(.body1Medium, with: .neutral400) |
| 86 | + .padding(.vertical, TBoxTextEditor.verticalPadding + 8) |
| 87 | + .padding(.horizontal, TBoxTextEditor.horizontalPadding + 4) |
| 88 | + } |
| 89 | + } |
| 90 | + if let footer { |
| 91 | + footer |
| 92 | + } |
| 93 | + } |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +public extension TBoxTextEditor { |
| 98 | + /// TBoxTextEditor์ Footer์
๋๋ค |
| 99 | + struct Footer: View { |
| 100 | + /// ์ต๋ ์
๋ ฅ ๊ฐ๋ฅ ๊ธ์ ์ |
| 101 | + private let textLimit: Int |
| 102 | + /// ์
๋ ฅ๋ ํ
์คํธ ์นด์ดํธ |
| 103 | + private var textCount: Int |
| 104 | + /// ๊ฒฝ๊ณ ํ
์คํธ |
| 105 | + private var warningText: String |
| 106 | + /// ํ
์คํธ ํ๋ ์ํ |
| 107 | + @Binding private var status: Status |
| 108 | + |
| 109 | + /// Footer ์์ฑ์ |
| 110 | + /// - Parameters: |
| 111 | + /// - textLimit: ์ต๋ ์
๋ ฅ ๊ฐ๋ฅ ๊ธ์ ์. |
| 112 | + /// - status: ํ
์คํธ ์๋ํฐ์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐ์ธ๋ฉ. |
| 113 | + /// - textCount: ์
๋ ฅ๋ ํ
์คํธ ๊ธ์ ์. |
| 114 | + public init( |
| 115 | + textLimit: Int, |
| 116 | + status: Binding<Status>, |
| 117 | + textCount: Int, |
| 118 | + warningText: String = "๊ธ์ ์๋ฅผ ์ด๊ณผํ์ด์" |
| 119 | + ) { |
| 120 | + self.textLimit = textLimit |
| 121 | + self.textCount = textCount |
| 122 | + self._status = status |
| 123 | + self.warningText = warningText |
| 124 | + } |
| 125 | + |
| 126 | + public var body: some View { |
| 127 | + HStack { |
| 128 | + if status == .invalid { |
| 129 | + Text(warningText) |
| 130 | + .typographyStyle(.label2Medium, with: status.footerColor) |
| 131 | + } |
| 132 | + Spacer() |
| 133 | + Text("\(textCount)/\(textLimit)") |
| 134 | + .typographyStyle(.label2Medium, with: status.footerColor) |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +public extension TBoxTextEditor { |
| 141 | + /// TextEditor์ ํ์๋๋ ์ํ์
๋๋ค |
| 142 | + enum Status { |
| 143 | + case empty |
| 144 | + case filled |
| 145 | + case invalid |
| 146 | + |
| 147 | + /// ํ
๋๋ฆฌ ์์ ์ค์ |
| 148 | + func borderColor(isFocused: Bool) -> Color { |
| 149 | + switch self { |
| 150 | + case .empty: |
| 151 | + return isFocused ? .neutral600 : .neutral300 |
| 152 | + case .filled: |
| 153 | + return isFocused ? .neutral600 : .neutral300 |
| 154 | + case .invalid: |
| 155 | + return .red500 |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + /// ํ
์คํธ ์์ ์ค์ |
| 160 | + var textColor: Color { |
| 161 | + switch self { |
| 162 | + case .empty: |
| 163 | + return .neutral400 |
| 164 | + case .filled, .invalid: |
| 165 | + return .neutral600 |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + /// ํธํฐ ์์ ์ค์ |
| 170 | + var footerColor: Color { |
| 171 | + switch self { |
| 172 | + case .empty, .filled: |
| 173 | + return .neutral300 |
| 174 | + case .invalid: |
| 175 | + return .red500 |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + /// TextEditor์ ํฌ๊ธฐ |
| 181 | + enum Size { |
| 182 | + case small |
| 183 | + case large |
| 184 | + |
| 185 | + /// ๋์ด |
| 186 | + var height: CGFloat { |
| 187 | + switch self { |
| 188 | + case .small: |
| 189 | + return 52 |
| 190 | + case .large: |
| 191 | + return 130 |
| 192 | + } |
| 193 | + } |
| 194 | + } |
| 195 | +} |
0 commit comments