├── .gitattributes
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── issue_template.md
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── LICENSE
├── Package.swift
├── README.md
├── README_ENG.md
├── Sources
└── AutoHeightEditor
│ ├── AutoHeightEditor.swift
│ ├── Constant.swift
│ └── Extension.swift
└── Tests
└── AutoHeightEditorTests
└── AutoHeightEditorTests.swift
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj binary merge=union
2 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## 개요 및 관련 이슈
2 | - 메인 홈 뷰의 UI를 전체 구현했습니다.(예시)
3 |
4 | ## 작업 사항
5 | - 관리자용 대시보드 구현(예시)
6 | - 관리자용 권한 수정 버튼 추가(예시)
7 |
8 | ## 주요 로직(Optional)
9 | ```diff
10 | - print("변경 전")
11 | + print("변경 후")
12 | ```
13 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue
3 | about: 해당 Issue 대한 작업(Task)를 적어주세요!
4 | title: ""
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### 📝 작업 목적
11 |
12 | [기능 혹은 버그에 대한 설명]
13 |
14 | **기대 효과:** [작업 기대 효과]
15 |
16 | **구현 효과:** [실제 구현 내용]
17 |
18 | ---
19 |
20 | ### 🛠️ 작업 체크리스트
21 |
22 | * [ ] line 1
23 | * [ ] line 2
24 |
25 | ---
26 |
27 | ### ⚙️ Working Env.
28 |
29 | - Target Ver : iOS 15.0
30 | - xcode 14.1
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
92 | # Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpackagemanager,firebase,xcode,objective-c,cocoapods
93 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpackagemanager,firebase,xcode,objective-c,cocoapods
94 |
95 | ### CocoaPods ###
96 | ## CocoaPods GitIgnore Template
97 |
98 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing
99 | # - Also handy if you have a large number of dependant pods
100 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE
101 | Pods/
102 |
103 | ### Firebase ###
104 | .idea
105 | **/node_modules/*
106 | **/.firebaserc
107 |
108 | ### Firebase Patch ###
109 | .runtimeconfig.json
110 | .firebase/
111 |
112 | ### Objective-C ###
113 | # Xcode
114 | #
115 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
116 |
117 | ## User settings
118 | xcuserdata/
119 |
120 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
121 | *.xcscmblueprint
122 | *.xccheckout
123 |
124 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
125 | build/
126 | DerivedData/
127 | *.moved-aside
128 | *.pbxuser
129 | !default.pbxuser
130 | *.mode1v3
131 | !default.mode1v3
132 | *.mode2v3
133 | !default.mode2v3
134 | *.perspectivev3
135 | !default.perspectivev3
136 |
137 | ## Obj-C/Swift specific
138 | *.hmap
139 |
140 | ## App packaging
141 | *.ipa
142 | *.dSYM.zip
143 | *.dSYM
144 |
145 | # CocoaPods
146 | # We recommend against adding the Pods directory to your .gitignore. However
147 | # you should judge for yourself, the pros and cons are mentioned at:
148 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
149 | # Pods/
150 | # Add this line if you want to avoid checking in source code from the Xcode workspace
151 | # *.xcworkspace
152 |
153 | # Carthage
154 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
155 | # Carthage/Checkouts
156 |
157 | Carthage/Build/
158 |
159 | # fastlane
160 | # It is recommended to not store the screenshots in the git repo.
161 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
162 | # For more information about the recommended setup visit:
163 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
164 |
165 | fastlane/report.xml
166 | fastlane/Preview.html
167 | fastlane/screenshots/**/*.png
168 | fastlane/test_output
169 |
170 | # Code Injection
171 | # After new code Injection tools there's a generated folder /iOSInjectionProject
172 | # https://github.com/johnno1962/injectionforxcode
173 |
174 | iOSInjectionProject/
175 |
176 | ### Objective-C Patch ###
177 |
178 | ### Swift ###
179 | # Xcode
180 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
181 |
182 |
183 |
184 |
185 |
186 |
187 | ## Playgrounds
188 | timeline.xctimeline
189 | playground.xcworkspace
190 |
191 | # Swift Package Manager
192 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
193 | # Packages/
194 | # Package.pins
195 | # Package.resolved
196 |
197 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
198 | # hence it is not needed unless you have added a package configuration file to your project
199 | # .swiftpm
200 |
201 | .build/
202 |
203 | # CocoaPods
204 | # We recommend against adding the Pods directory to your .gitignore. However
205 | # you should judge for yourself, the pros and cons are mentioned at:
206 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
207 | # Pods/
208 | # Add this line if you want to avoid checking in source code from the Xcode workspace
209 | # *.xcworkspace
210 |
211 | # Carthage
212 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
213 | # Carthage/Checkouts
214 |
215 |
216 | # Accio dependency management
217 | Dependencies/
218 | .accio/
219 |
220 | # fastlane
221 | # It is recommended to not store the screenshots in the git repo.
222 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
223 | # For more information about the recommended setup visit:
224 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
225 |
226 |
227 | # Code Injection
228 | # After new code Injection tools there's a generated folder /iOSInjectionProject
229 | # https://github.com/johnno1962/injectionforxcode
230 |
231 |
232 | ### SwiftPackageManager ###
233 | Packages
234 | xcuserdata
235 |
236 |
237 |
238 | ### Xcode ###
239 |
240 | ## Xcode 8 and earlier
241 |
242 | ### Xcode Patch ###
243 | *.xcodeproj/*
244 | !*.xcodeproj/project.pbxproj
245 | !*.xcodeproj/xcshareddata/
246 | !*.xcodeproj/project.xcworkspace/
247 | !*.xcworkspace/contents.xcworkspacedata
248 | /*.gcno
249 | **/xcshareddata/WorkspaceSettings.xcsettings
250 |
251 | # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpackagemanager,firebase,xcode,objective-c,cocoapods
252 |
253 | # 프로젝트의 모든 경로에서 .DS_Store 파일을 포함하지 않도록 함
254 | **/.DS_Store
255 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Won Taeyoung
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "AutoHeightEditor",
8 | platforms: [
9 | .iOS(.v14)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, making them visible to other packages.
13 | .library(
14 | name: "AutoHeightEditor",
15 | targets: ["AutoHeightEditor"]),
16 | ],
17 | targets: [
18 | // Targets are the basic building blocks of a package, defining a module or a test suite.
19 | // Targets can depend on other targets in this package and products from dependencies.
20 | .target(
21 | name: "AutoHeightEditor"),
22 | .testTarget(
23 | name: "AutoHeightEditorTests",
24 | dependencies: ["AutoHeightEditor"]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://developer.apple.com/ios/)
2 | [](https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-release-notes)
3 | [](https://developer.apple.com/xcode/swiftui/)
4 | [](https://github.com/apple/swift-package-manager)
5 | [](https://github.com/wontaeyoung/AutoHeightEditor/blob/main/LICENSE)
6 | [](https://www.github.com/wontaeyoung)
7 |
8 | [Read English Translation](https://github.com/wontaeyoung/AutoHeightEditor/blob/main/README_ENG.md)
9 |
10 |
11 |
12 | `AutoHeightEditor`는 Dynamic Height 기능이 있는 커스텀 `TextEditor` 라이브러리입니다.
13 |
14 |
15 |
16 | # 제작 배경
17 |
18 | 이 라이브러리는 제가 프로젝트에 필요해서 직접 구현하게 된 커스텀 `TextEditor`입니다.
19 |
20 | 제가 진행하고 있는 프로젝트에서 동적으로 높이가 조절되는 입력 인터페이스가 요구사항이었는데, **iOS 16**부터는 `TextField`의 `axis` 파라미터를 통해 Dynamic Height로 동작하는 입력 인터페이스를 쉽게 사용할 수 있습니다.
21 |
22 | 하지만 프로젝트 최소 지원버전이 **iOS 15.0**+로 결정되었고, 여러 줄의 텍스트 입력을 받기 위해서는 `TextEditor`를 사용해야했습니다.
23 |
24 | 기본 API로 제공되는 `TextEditor`를 사용해보신분들은 공감하시겠지만 지원하는 기능이 `TextField`에 비해 부족하고, 특히 별도로 높이를 지정해주지 않으면 차지할 수 있는 최대 높이를 가지게 됩니다.
25 |
26 | 이를 해결하기 위해서 적절한 높이를 동적으로 계산해주는 `AutoHeightEditor`를 커스텀으로 제작하게 되었습니다.
27 |
28 | 자세한 제작 배경 및 구현 과정은 [블로그](https://velog.io/@wontaeyoung/swiftui4)에서 확인할 수 있습니다.
29 |
30 |
31 |
32 | # 라이브러리 소개
33 |
34 | `AutoHeightEditor`는 기본적으로 Dynamic Height이 가장 큰 특징입니다.
35 |
36 | 이를 구현하기 위해 폰트 높이, 행간, 텍스트 길이, 개행문자를 통해서 적절한 높이로 TextEditor의 높이를 실시간으로 변경합니다.
37 |
38 |
39 |
40 | ## 주요 로직
41 |
42 | 1. 텍스트에서 `\n`(개행문자)의 갯수를 계산합니다.
43 | 2. `TextEditor`의 가로 길이와 입력된 텍스트의 길이를 계산해서 자동 줄바꿈이 몇 번 일어나야하는지 계산합니다.
44 | 3. 1번과 2번을 합쳐서 총 줄바꿈 횟수를 계산합니다.
45 | 4. 폰트 크기, 행간, 줄바꿈 수를 계산하여 `TextEditor`의 총 높이를 계산합니다.
46 |
47 |
48 |
49 | ## 사용자 편의
50 |
51 | 최소 1줄 ~ maxLine까지 사용자의 입력에 따라 높이가 변경되고 최대 라인 수, 사용되는 폰트, 행간, 활성화 여부와 같은 선택사항을 파라미터로 전달받아서 반영합니다.
52 |
53 | 개인적으로 사용하는 컴포넌트를 라이브러리에 맞춰서 수정한만큼, 제가 아닌 다른 사용자가 사용하는 환경을 고려하여 아래 사항들을 추가했습니다.
54 |
55 | - isEnabled를 바인딩 받아서 외부에서 활성화 여부 관리 가능
56 | - 고정으로 존재하는 Border 스트로크 사용 여부 선택 가능
57 | - Disabled 안내 문구 커스텀 가능
58 | - 전달받은 정규식 매치 여부를 계산해서 바인딩 된 Bool 변수에 반영
59 |
60 |
61 |
62 | 제작 배경에서 설명한 것처럼 **iOS 16**은 아직은 실무에 적용하기 부담스러운 버전이기 때문에, 이를 고려하여 `TextEditor`가 처음 나온 **iOS 14**부터 사용 가능하도록 구현했습니다.
63 |
64 |
65 |
66 | # 파라미터 리스트
67 |
68 | ```swift
69 | public init (
70 | text: Binding,
71 | font: Font = .body,
72 | lineSpace: CGFloat = 2,
73 | maxLine: Int,
74 | hasBorder: Bool,
75 | isEnabled: Binding,
76 | disabledPlaceholder: String,
77 | regExpUse: RegExpUse
78 | )
79 | ```
80 |
81 |
82 |
83 | ```swift
84 | text: Binding
85 | ```
86 |
87 | 에디터에 바인딩되는 입력 텍스트 문자열입니다. 외부에서 바인딩으로 주입해서 사용합니다.
88 |
89 |
90 |
91 | ```swift
92 | font: Font
93 | ```
94 |
95 | 텍스트에 적용할 폰트 타입입니다. Default Value로 `body`가 주입되고, 원하는 다른 폰트가 있다면 주입해서 사용 가능합니다.
96 |
97 |
98 |
99 | ```swift
100 | lineSpace: CGFloat
101 | ```
102 |
103 | 텍스트 라인 사이에 들어가는 행 간격입니다. Default Value로 2가 주입되고, 원하는 다른 값이 있다면 주입해서 사용 가능합니다.
104 |
105 |
106 |
107 | ```swift
108 | maxLine: Int
109 | ```
110 |
111 | 에디터의 높이가 증가하는 상한선 라인 수입니다. 입력 라인이 늘어날 때 `maxLine`까지 에디터 높이가 증가하고, 그 이후로는 늘어나지 않습니다.
112 |
113 |
114 |
115 | ```swift
116 | hasBorder: Bool
117 | ```
118 |
119 | 기본으로 제공되는 `Stroke`의 사용 여부를 결정합니다. 기본 `Stroke`는 Gray 컬러에 20의 CornerRadius 값을 가지고 있습니다.
120 |
121 |
122 |
123 | ```swift
124 | isEnabled: Binding
125 | ```
126 |
127 | 에디터의 활성화 여부를 결정합니다. 외부에서 바인딩으로 주입하고, 조절해서 사용합니다.
128 |
129 |
130 |
131 | ```swift
132 | disabledPlaceholder: String
133 | ```
134 |
135 | 에디터가 비활성화 되어있을 때, 사용자에게 안내하기 위한 문구입니다.
136 |
137 |
138 |
139 | ```swift
140 | public enum RegExpUse {
141 | case use(pattern: String, isMatched: Binding)
142 | case none
143 | }
144 |
145 | regExpUse: RegExpUse
146 | ```
147 |
148 | 정규식 매치 여부를 사용하는지를 결정하는 타입입니다.
149 |
150 | 사용하지 않으면 `none`, 사용한다면 `use`를 전달합니다.
151 |
152 | `pattern`은 매칭에 사용할 정규식 패턴, `isMatched`는 외부에서 주입하고 활용할 바인딩 값입니다.
153 |
154 | 텍스트가 업데이트 될 때마다 정규식을 검사해서 `isMatched`에 전달된 바인딩 변수를 자동으로 업데이트합니다.
155 |
156 |
157 |
158 | # 사용 가이드
159 |
160 | 우선 기본적인 동작을 확인해보기 위해 `AutoHeightEditor`를 초기화 해보겠습니다.
161 |
162 | 처음에는 1줄 높이로 시작하고, 입력된 텍스트에 따라 최대 5줄까지 높이가 동적으로 늘어납니다.
163 |
164 | `\n`(개행문자)로 일어나는 줄바꿈 뿐만 아니라, 텍스트가 길어져서 자동으로 줄바꿈이 발생하는 순간도 감지해서 높이에 반영합니다.
165 |
166 | ```swift
167 | AutoHeightEditor(
168 | text: $text,
169 | maxLine: 5,
170 | hasBorder: true,
171 | isEnabled: $isEnabled,
172 | disabledPlaceholder: "This editor has been disabled",
173 | regExpUse: .none)
174 | ```
175 |
176 |
177 |
178 |
179 |
180 | ### 최대 라인 수 수정하기
181 |
182 | `maxLine`을 조정해서 최대 높이 라인 수를 결정할 수 있습니다.
183 |
184 | 아래 예시에서는 `7`을 전달해서 7줄 높이까지 늘어나도록 해보겠습니다.
185 |
186 | ```swift
187 | AutoHeightEditor(
188 | text: $text,
189 | maxLine: 7,
190 | hasBorder: true,
191 | isEnabled: $isEnabled,
192 | disabledPlaceholder: "This editor has been disabled",
193 | regExpUse: .none)
194 | ```
195 |
196 |
197 |
198 |
199 |
200 | ### 폰트와 행 간격 수정하기
201 |
202 | > 현재 버전에서는 SwiftUI의 기본 `Font` 타입에 없는 값은 사용이 불가합니다. 폰트의 사이즈를 구하기 위해 내부에서 `UIFont`와 1:1 매핑을 하기 때문입니다.
203 |
204 | `font`와 `lineSpace`에는 기본값으로 `body`와 `2`가 전달되고 있습니다.
205 |
206 | 원하는 값이 있다면 Default Value 대신에 새로운 값을 전달할 수 있습니다.
207 |
208 | 아래 예시에서는 `title2`와 `10`을 전달해서 폰트 사이즈를 키우고 행 간격도 넓혀보겠습니다.
209 |
210 | ```swift
211 | AutoHeightEditor(
212 | text: $text,
213 | font: .title2,
214 | lineSpace: 10,
215 | maxLine: 5,
216 | hasBorder: true,
217 | isEnabled: $isEnabled,
218 | disabledPlaceholder: "This editor has been disabled",
219 | regExpUse: .none)
220 | ```
221 |
222 |
223 |
224 |
225 |
226 | ### Border Stroke 커스텀
227 |
228 | `hasBorder`를 통해 기본으로 제공되는 테두리 사용 여부를 결정할 수 있습니다.
229 |
230 | 기본 `Stroke`는 Gray 컬러에 20의 CornerRadius 값을 가지고 있습니다.
231 |
232 | 아래 예시에서는 `hasBorder`의 값을 false로 전달하여 테두리를 삭제해보겠습니다.
233 |
234 | ```swift
235 | AutoHeightEditor(
236 | text: $text,
237 | maxLine: 5,
238 | hasBorder: false,
239 | isEnabled: $isEnabled,
240 | disabledPlaceholder: "This editor has been disabled",
241 | regExpUse: .none)
242 | ```
243 |
244 |
245 |
246 |
247 |
248 | 외부에서 `overlay`를 사용하여 원하는 디자인을 커스텀으로 작성할 수 있습니다.
249 |
250 | 아래 예시에서는 기본 테두리를 삭제하고, overlay로 사각형 스타일의 테두리를 그려보겠습니다.
251 |
252 | ```swift
253 | AutoHeightEditor(
254 | text: $text,
255 | maxLine: 5,
256 | hasBorder: false,
257 | isEnabled: $isEnabled,
258 | disabledPlaceholder: "This editor has been disabled",
259 | regExpUse: .none)
260 | .overlay {
261 | Rectangle()
262 | .stroke()
263 | }
264 | ```
265 |
266 |
267 |
268 |
269 |
270 | ### 에디터 사용 활성화 관리
271 |
272 | `isEnabled`로 에디터의 터치 이벤트 수신 여부를 조정할 수 있습니다.
273 |
274 | 외부에서 관리하기 위해 바인딩으로 전달받아 사용합니다.
275 |
276 | `disabled`되면 `disabledPlaceholder`에 전달된 텍스트를 플레이스홀더로 표시합니다.
277 |
278 | 아래 예시에서는 isEnabled에 변수를 바인딩하고, Toggle로 외부에서 관리해보겠습니다.
279 |
280 | ```swift
281 | AutoHeightEditor(
282 | text: $text,
283 | maxLine: 5,
284 | hasBorder: true,
285 | isEnabled: $isEnabled,
286 | disabledPlaceholder: "This editor has been disabled",
287 | regExpUse: .none)
288 | ```
289 |
290 |
291 |
292 |
293 |
294 | 만약에 따로 비활성화 하는 경우가 없다면 `.constant()`로 전달하고, `disabledPlaceholder`에는 빈 문자열을 전달하면 됩니다.
295 |
296 | ```swift
297 | AutoHeightEditor(
298 | text: $text,
299 | maxLine: 5,
300 | hasBorder: true,
301 | isEnabled: .constant(true),
302 | disabledPlaceholder: "",
303 | regExpUse: .none)
304 | ```
305 |
306 |
307 |
308 | ### 정규식 사용
309 |
310 | `regExpUse` 열거형으로 정규식 사용 여부를 결정할 수 있습니다.
311 |
312 | 사용하지 않는다면 `none`, 사용한다면 `use`를 주입하면 됩니다.
313 |
314 | `use`에는 연관값으로 `pattern`과 `isMatched`를 전달할 수 있습니다.
315 |
316 | `pattern` 텍스트와 비교할 정규식 패턴 문자열입니다.
317 |
318 | `isMatched`는 외부에서 바인딩 받는 변수로, 내부에서 정규식 일치 여부를 업데이트 받습니다.
319 |
320 | 아래 예시에서는 이메일 패턴을 전달해보겠습니다.
321 |
322 | ```swift
323 | AutoHeightEditor(
324 | text: $text,
325 | maxLine: 5,
326 | hasBorder: true,
327 | isEnabled: $isEnabled,
328 | disabledPlaceholder: "This editor has been disabled",
329 | regExpUse: .use(
330 | pattern: #"^[a-zA-Z0-9+-\_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]{2,3}+$"#,
331 | isMatched: $isMatched))
332 | ```
333 |
334 |
335 |
336 |
337 |
338 | ### 포커스 관리
339 |
340 | `@FocusState`를 패키지 내부에 포함시키면 최소 지원버전이 **iOS 15**로 올라가기 때문에 포함시키지 않았습니다.
341 |
342 | 트레이드 오프를 생각해봤을 때, 파라미터로 전달받는 사용성보다 지원 버전을 낮추는게 더 메리트가 있다고 생각했습니다.
343 |
344 | 프로젝트 지원 버전이 15.0+인 사용자분들은 외부에서 `FocusState`를 사용해서 포커스를 관리할 수 있습니다.
345 |
346 | ```swift
347 | AutoHeightEditor(
348 | text: $text,
349 | maxLine: 5,
350 | hasBorder: true,
351 | isEnabled: $isEnabled,
352 | disabledPlaceholder: "This editor has been disabled",
353 | regExpUse: .none)
354 | .focused($isFocus)
355 | ```
356 |
357 |
358 |
359 |
360 |
361 | ### 다크모드 지원
362 |
363 | 현재 버전에서는 내부 `foregroundColor`에서 `primary`를 전달해서 기본적인 다크모드 대응만 지원하고 있습니다.
364 |
365 | 기본 제공 `Stroke` 색상은 라이트 / 다크 모두 `gray` 고정입니다.
366 |
367 |
368 |
369 |
370 |
371 | # 라이센스
372 |
373 | `AutoHeightEditor`는 MIT 라이센스의 범위 내에서 사용 가능합니다.
374 |
375 | 자세한 정보는 [라이센스](https://github.com/wontaeyoung/AutoHeightEditor/blob/main/LICENSE)에서 확인해주세요.
376 |
377 |
378 |
379 | **작성자**: [원태영](https://github.com/wontaeyoung)
380 |
--------------------------------------------------------------------------------
/README_ENG.md:
--------------------------------------------------------------------------------
1 | [](https://developer.apple.com/ios/)
2 | [](https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-release-notes)
3 | [](https://developer.apple.com/xcode/swiftui/)
4 | [](https://github.com/apple/swift-package-manager)
5 | [](https://github.com/wontaeyoung/AutoHeightEditor/blob/main/LICENSE)
6 | [](https://www.github.com/wontaeyoung)
7 |
8 | **`AutoHeightEditor`** is a custom **`TextEditor`** library with Dynamic Height functionality.
9 |
10 |
11 |
12 | # **Background**
13 |
14 | I created this custom **`TextEditor`** because it was a requirement for a project I was working on.
15 |
16 | In the project, an input interface that dynamically adjusts in height was needed. From iOS 16, an input interface that works as Dynamic Height can be easily used through the **`axis`** parameter of **`TextField`**.
17 |
18 | However, the minimum supported version of the project was decided to be iOS 15.0+, and for multiple lines of text input, **`TextEditor`** had to be used.
19 |
20 | As those who have used the standard API **`TextEditor`** might agree, it lacks some features compared to **`TextField`**, especially it takes up the maximum possible height unless a specific height is designated.
21 |
22 | To solve this, I created the **`AutoHeightEditor`**, which dynamically calculates the appropriate height.
23 |
24 | You can check out the detailed background and implementation process on [my blog](https://velog.io/@wontaeyoung/swiftui4).
25 |
26 |
27 |
28 | # **Library Introduction**
29 |
30 | The main feature of **`AutoHeightEditor`** is essentially Dynamic Height.
31 |
32 | It changes the height of the TextEditor in real-time based on font height, line spacing, text length, and new line characters.
33 |
34 |
35 |
36 | ## **Main Logic**
37 |
38 | 1. Count the number of **`\n`** (new line characters) in the text.
39 | 2. Calculate the width of the **`TextEditor`** and the length of the entered text to determine how many times automatic line breaks should occur.
40 | 3. Combine the counts from steps 1 and 2 to calculate the total number of line breaks.
41 | 4. Calculate the total height of the **`TextEditor`** by considering the font size, line spacing, and number of line breaks.
42 |
43 |
44 |
45 | ## **User Convenience**
46 |
47 | The height changes from a minimum of 1 line to a maximum of **`maxLine`** based on user input. Options like maximum line numbers, used font, line spacing, and activation status are taken as parameters.
48 |
49 | As I customized a component for personal use to fit into a library, I considered the environment of other users and added the following features:
50 |
51 | - Accepts **`isEnabled`** binding for external control of activation status.
52 | - Option to choose the use of a fixed Border stroke.
53 | - Customizable disabled placeholder text.
54 | - Reflects the result of regular expression matching in a bound Bool variable.
55 |
56 |
57 |
58 | As mentioned in the production background, **iOS 16** is still a version that is burdensome to apply in practice. Therefore, it was implemented to be usable from **iOS 14**, where **`TextEditor`** first appeared.
59 |
60 |
61 |
62 | # **Parameter List**
63 |
64 | ```swift
65 |
66 | public init (
67 | text: Binding,
68 | font: Font = .body,
69 | lineSpace: CGFloat = 2,
70 | maxLine: Int,
71 | hasBorder: Bool,
72 | isEnabled: Binding,
73 | disabledPlaceholder: String,
74 | regExpUse: RegExpUse
75 | )
76 | ```
77 |
78 |
79 |
80 | ```swift
81 | text: Binding
82 | ```
83 |
84 | The text string bound to the editor. It is used by injecting a binding from the outside.
85 |
86 |
87 |
88 | ```swift
89 | font: Font
90 | ```
91 |
92 | The font type applied to the text. **`body`** is injected as the Default Value, and if you have a different desired font, you can inject and use it.
93 |
94 |
95 |
96 | ```swift
97 | lineSpace: CGFloat
98 | ```
99 |
100 | The line spacing between text lines. **`2`** is injected as the Default Value, and if you have a different desired value, you can inject and use it.
101 |
102 |
103 |
104 | ```swift
105 | maxLine: Int
106 | ```
107 |
108 | The maximum number of lines for which the editor's height increases. As the input lines increase, the height of the editor grows up to **`maxLine`**, and then does not increase further.
109 |
110 |
111 |
112 | ```swift
113 | hasBorder: Bool
114 | ```
115 |
116 | Determines whether to use the default provided **`Stroke`**. The basic **`Stroke`** comes with a Gray color and a CornerRadius of 20.
117 |
118 |
119 |
120 | ```swift
121 | isEnabled: Binding
122 | ```
123 |
124 | Determines whether the editor is active. It is injected and used from the outside via binding.
125 |
126 |
127 |
128 | ```swift
129 | disabledPlaceholder: String
130 | ```
131 |
132 | A message to guide the user when the editor is disabled.
133 |
134 |
135 |
136 | ```swift
137 | public enum RegExpUse {
138 | case use(pattern: String, isMatched: Binding)
139 | case none
140 | }
141 |
142 | regExpUse: RegExpUse
143 | ```
144 |
145 | The type that determines whether to use regular expression matching.
146 |
147 | If not used, pass **`none`**; if used, pass **`use`**.
148 |
149 | **`pattern`** is the regular expression pattern to compare with the text, and **`isMatched`** is a binding variable to be injected and used from the outside.
150 |
151 | The text is checked against the regular expression whenever it is updated, and the bound variable **`isMatched`** is automatically updated.
152 |
153 |
154 |
155 | # **Usage Guide**
156 |
157 | Let's start by initializing **`AutoHeightEditor`** to check its basic functionality.
158 |
159 | Initially, it starts with a height of 1 line, and the height dynamically increases up to a maximum of 5 lines depending on the entered text.
160 |
161 | It detects not only line breaks caused by **`\n`** (new line characters) but also moments when the text becomes long enough to cause automatic line breaks, and reflects this in the height.
162 |
163 | ```swift
164 | AutoHeightEditor(
165 | text: $text,
166 | maxLine: 5,
167 | hasBorder: true,
168 | isEnabled: $isEnabled,
169 | disabledPlaceholder: "This editor has been disabled",
170 | regExpUse: .none)
171 | ```
172 |
173 |
174 |
175 |
176 |
177 | ### **Modifying the Maximum Line Count**
178 |
179 | You can determine the maximum line height by adjusting **`maxLine`**.
180 |
181 | In the example below, we'll pass **`7`** to allow it to expand up to a height of 7 lines.
182 |
183 | ```swift
184 | AutoHeightEditor(
185 | text: $text,
186 | maxLine: 7,
187 | hasBorder: true,
188 | isEnabled: $isEnabled,
189 | disabledPlaceholder: "This editor has been disabled",
190 | regExpUse: .none)
191 | ```
192 |
193 |
194 |
195 |
196 |
197 | ### **Modifying Font and Line Spacing**
198 |
199 | > In the current version, values not in SwiftUI's basic Font type are not available. This is because we do a 1:1 mapping with UIFont internally to get the font size.
200 |
201 | **`font`** and **`lineSpace`** come with the Default Values of **`body`** and **`2`**, respectively.
202 |
203 | If you have desired values other than these Default Values, you can inject new ones.
204 |
205 | In the example below, we'll pass **`title2`** and **`10`** to increase the font size and line spacing.
206 |
207 | ```swift
208 | AutoHeightEditor(
209 | text: $text,
210 | font: .title2,
211 | lineSpace: 10,
212 | maxLine: 5,
213 | hasBorder: true,
214 | isEnabled: $isEnabled,
215 | disabledPlaceholder: "This editor has been disabled",
216 | regExpUse: .none)
217 | ```
218 |
219 |
220 |
221 |
222 |
223 | ### **Customizing the Border Stroke**
224 |
225 | You can decide whether to use the provided default border stroke with **`hasBorder`**.
226 |
227 | The basic **`Stroke`** is Gray in color with a CornerRadius of 20.
228 |
229 | In the example below, we'll set **`hasBorder`** to false to remove the border.
230 |
231 | ```swift
232 | AutoHeightEditor(
233 | text: $text,
234 | maxLine: 5,
235 | hasBorder: false,
236 | isEnabled: $isEnabled,
237 | disabledPlaceholder: "This editor has been disabled",
238 | regExpUse: .none)
239 | ```
240 |
241 |
242 |
243 |
244 |
245 | You can use **`overlay`** from outside to write a custom design.
246 |
247 | In the example below, we'll delete the default border and draw a rectangular style border with overlay.
248 |
249 | ```swift
250 | AutoHeightEditor(
251 | text: $text,
252 | maxLine: 5,
253 | hasBorder: false,
254 | isEnabled: $isEnabled,
255 | disabledPlaceholder: "This editor has been disabled",
256 | regExpUse: .none)
257 | .overlay {
258 | Rectangle()
259 | .stroke()
260 | }
261 | ```
262 |
263 |
264 |
265 |
266 |
267 | ### **Managing Editor Activation**
268 |
269 | **`isEnabled`** controls whether the editor receives touch events.
270 |
271 | It is injected and used from the outside via binding.
272 |
273 | When disabled, the text passed to **`disabledPlaceholder`** is displayed as a placeholder.
274 |
275 | In the example below, we'll bind a variable to isEnabled and manage it with a Toggle from the outside.
276 |
277 | ```swift
278 | AutoHeightEditor(
279 | text: $text,
280 | maxLine: 5,
281 | hasBorder: true,
282 | isEnabled: $isEnabled,
283 | disabledPlaceholder: "This editor has been disabled",
284 | regExpUse: .none)
285 | ```
286 |
287 |
288 |
289 |
290 |
291 | If there is no separate case for disabling, you can pass **`.constant()`** and an empty string for **`disabledPlaceholder`**.
292 |
293 | ```swift
294 | AutoHeightEditor(
295 | text: $text,
296 | maxLine: 5,
297 | hasBorder: true,
298 | isEnabled: .constant(true),
299 | disabledPlaceholder: "",
300 | regExpUse: .none)
301 | ```
302 |
303 |
304 |
305 | ### **Using Regular Expressions**
306 |
307 | **`regExpUse`** enum determines whether to use regular expression matching.
308 |
309 | If not used, pass **`none`**; if used, pass **`use`**.
310 |
311 | **`use`** allows you to pass associated values **`pattern`** and **`isMatched`**.
312 |
313 | **`pattern`** is the regular expression pattern string to be compared with the text.
314 |
315 | **`isMatched`** is a binding variable injected and used from the outside.
316 |
317 | In the example below, we'll pass an email pattern.
318 |
319 | ```swift
320 | AutoHeightEditor(
321 | text: $text,
322 | maxLine: 5,
323 | hasBorder: true,
324 | isEnabled: $isEnabled,
325 | disabledPlaceholder: "This editor has been disabled",
326 | regExpUse: .use(
327 | pattern: #"^[a-zA-Z0-9+-\_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]{2,3}+$"#,
328 | isMatched: $isMatched))
329 | ```
330 |
331 |
332 |
333 |
334 |
335 | ### **Focus Management**
336 |
337 | I chose not to include **`@FocusState`** within the package to keep the minimum supported version at **iOS 14** rather than raising it to **iOS 15**.
338 |
339 | After considering the trade-offs, I thought it more beneficial to lower the version support than to enhance usability by passing parameters.
340 |
341 | Users with project support versions of 15.0+ can manage focus externally using **`FocusState`**.
342 |
343 | ```swift
344 | AutoHeightEditor(
345 | text: $text,
346 | maxLine: 5,
347 | hasBorder: true,
348 | isEnabled: $isEnabled,
349 | disabledPlaceholder: "This editor has been disabled",
350 | regExpUse: .none)
351 | .focused($isFocus)
352 | ```
353 |
354 |
355 |
356 |
357 |
358 | ### **Dark Mode Support**
359 |
360 | The current version only supports basic dark mode adaptation by passing **`primary`** to the internal **`foregroundColor`**.
361 |
362 | The default provided **`Stroke`** color remains fixed at **`gray`** for both light and dark modes.
363 |
364 |
365 |
366 |
367 |
368 | # **License**
369 |
370 | **`AutoHeightEditor`** is available under the MIT license.
371 |
372 | Please see the [License](https://github.com/wontaeyoung/AutoHeightEditor/blob/main/LICENSE) for more information.
373 |
374 |
375 |
376 | **Author**: [Won Tae-young](https://github.com/wontaeyoung)
377 |
--------------------------------------------------------------------------------
/Sources/AutoHeightEditor/AutoHeightEditor.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct AutoHeightEditor: View {
4 | public enum RegExpUse {
5 | case use(pattern: String, isMatched: Binding)
6 | case none
7 | }
8 |
9 | // MARK: - Property
10 | private let const = TextEditorConst.self
11 |
12 | private let text: Binding
13 | private let font: Font
14 | private let lineSpace: CGFloat
15 | private let isEnabled: Binding
16 | private let hasBorder: Bool
17 | private let disabledPlaceholder: String
18 | private let regExpUse: RegExpUse
19 |
20 | // MARK: Initializer에서 계산을 통해 결정되는 프로퍼티
21 | private let maxLineCount: CGFloat
22 | private let uiFont: UIFont
23 | private let maxHeight: CGFloat
24 |
25 | @State private var currentTextEditorHeight: CGFloat = 0
26 | @State private var maxTextWidth: CGFloat = 0
27 |
28 | // MARK: - Initializer
29 | /// 파라미터 font = .body, lineSpace = 2 기본값 지정
30 | public init (
31 | text: Binding,
32 | font: Font = .body,
33 | lineSpace: CGFloat = 2,
34 | maxLine: Int,
35 | hasBorder: Bool,
36 | isEnabled: Binding,
37 | disabledPlaceholder: String,
38 | regExpUse: RegExpUse
39 | ) {
40 | // MARK: Required
41 | self.text = text
42 | self.font = font
43 | self.lineSpace = lineSpace
44 | self.isEnabled = isEnabled
45 | self.hasBorder = hasBorder
46 | self.disabledPlaceholder = disabledPlaceholder
47 | self.regExpUse = regExpUse
48 |
49 | // MARK: Calculated
50 | self.maxLineCount = (maxLine < 1 ? 1 : maxLine).asFloat
51 | self.uiFont = UIFont.fontToUIFont(from: font)
52 | self.maxHeight = (maxLineCount * (uiFont.lineHeight + lineSpace)) + const.TEXTEDITOR_FRAME_HEIGHT_FREESPACE
53 | }
54 |
55 | // MARK: - View
56 | public var body: some View {
57 | if isEnabled.wrappedValue {
58 | enabledEditor
59 | } else {
60 | disabledEditor
61 | }
62 | }
63 | }
64 |
65 | // MARK: - Calculate Line
66 | private extension AutoHeightEditor {
67 | /// 현재 text에 개행문자에 의한 라인 갯수가 몇 줄인지 계산합니다.
68 | var newLineCount: CGFloat {
69 | let currentText: String = text.wrappedValue
70 | let currentLineCount: Int = currentText
71 | .filter { $0 == "\n" }
72 | .count + 1
73 | let newLineCount: CGFloat = currentLineCount > maxLineCount.asInt
74 | ? maxLineCount
75 | : currentLineCount.asFloat
76 |
77 | return newLineCount
78 | }
79 |
80 | /// 개행 문자 기준으로 텍스트를 분리하고, 각 텍스트 길이가 Editor 길이를 초과하는지 체크하여 필요한 줄바꿈 수를 계산합니다.
81 | var autoLineCount: CGFloat {
82 | var counter: Int = 0
83 | text
84 | .wrappedValue
85 | .components(separatedBy: "\n")
86 | .forEach { line in
87 | let label = UILabel()
88 | label.font = .fontToUIFont(from: font)
89 | label.text = line
90 | label.sizeToFit()
91 | let currentTextWidth = label.frame.width
92 | counter += (currentTextWidth / maxTextWidth).asInt
93 | }
94 |
95 | return counter.asFloat
96 | }
97 | }
98 |
99 | // MARK: - Calculate Width / Height
100 | private extension AutoHeightEditor {
101 | /// textEditor 시작 높이를 설정합니다.
102 | func setTextEditorStartHeight() {
103 | currentTextEditorHeight = uiFont.lineHeight + const.TEXTEDITOR_FRAME_HEIGHT_FREESPACE
104 | }
105 |
106 | /// text가 가질 수 있는 최대 길이를 설정합니다.
107 | func setMaxTextWidth(proxy: GeometryProxy) {
108 | maxTextWidth = proxy.size.width - (const.TEXTEDITOR_INSET_HORIZONTAL * 2 + const.TEXTEDITOR_WIDTH_HORIZONTAL_BUFFER)
109 | }
110 |
111 | /// line count를 통해 textEditor 현재 높이를 계산해서 업데이트합니다.
112 | func updateTextEditorCurrentHeight() {
113 | // 총 라인 갯수
114 | let totalLineCount = newLineCount + autoLineCount
115 |
116 | // 총 라인 갯수가 maxCount 이상이면 최대 높이로 고정
117 | guard totalLineCount < maxLineCount else {
118 | currentTextEditorHeight = maxHeight
119 | return
120 | }
121 |
122 | // 라인 갯수로 계산한 현재 Editor 높이
123 | let currentHeight = (totalLineCount * (uiFont.lineHeight + lineSpace))
124 | - lineSpace + const.TEXTEDITOR_FRAME_HEIGHT_FREESPACE
125 |
126 | // View의 높이를 결정하는 State 변수에 계산된 현재 높이를 할당하여 뷰에 반영
127 | currentTextEditorHeight = currentHeight
128 | }
129 | }
130 |
131 | // MARK: - Regular Expression
132 | private extension AutoHeightEditor {
133 | /// 정규식 프로퍼티와 현재 텍스트가 일치하는지 제공하는 인터페이스 프로퍼티
134 | private var isPatternMatched: Bool {
135 | switch regExpUse {
136 | case .use(let pattern, _):
137 | guard text.wrappedValue.isEmpty == false else {
138 | return false
139 | }
140 |
141 | let isMathed: Bool = text.wrappedValue.range(
142 | of: pattern,
143 | options: .regularExpression) != nil
144 |
145 | return isMathed
146 |
147 | case .none:
148 | return true
149 | }
150 | }
151 |
152 | /// 바인딩 되어있는 isPatternMatched에 정규식 패턴 매치 여부를 업데이트합니다.
153 | func updatePatternMatched() {
154 | switch regExpUse {
155 | case .use(_, let isMatched):
156 | isMatched.wrappedValue = self.isPatternMatched
157 |
158 | case .none:
159 | break
160 | }
161 | }
162 | }
163 |
164 | // MARK: - Editor View
165 | private extension AutoHeightEditor {
166 | var enabledEditor: some View {
167 | GeometryReader { proxy in
168 | ZStack {
169 | TextEditor(text: text)
170 | .autocorrectionDisabled()
171 | .autocapitalization(.none)
172 | .modifier(
173 | AutoHeightEditorLayoutModifier(
174 | font: font,
175 | color: .primary,
176 | lineSpace: lineSpace,
177 | maxHeight: currentTextEditorHeight,
178 | horizontalInset: const.TEXTEDITOR_INSET_HORIZONTAL,
179 | bottomInset: const.TEXTEDITOR_INSET_BOTTOM))
180 |
181 | if hasBorder {
182 | RoundedRectangle(cornerRadius: const.TEXTEDITOR_STROKE_CORNER_RADIUS)
183 | .stroke()
184 | .foregroundColor(.gray)
185 | }
186 | }
187 | .onAppear {
188 | setTextEditorStartHeight()
189 | setMaxTextWidth(proxy: proxy)
190 | }
191 | .onChange(of: text.wrappedValue) { _ in
192 | updateTextEditorCurrentHeight()
193 | updatePatternMatched()
194 | }
195 | }
196 | .frame(maxHeight: currentTextEditorHeight)
197 | }
198 |
199 | var disabledEditor: some View {
200 | ZStack {
201 | TextEditor(
202 | text: .constant(disabledPlaceholder)
203 | )
204 | .modifier(
205 | AutoHeightEditorLayoutModifier(
206 | font: font,
207 | color: .primary,
208 | lineSpace: lineSpace,
209 | maxHeight: currentTextEditorHeight,
210 | horizontalInset: const.TEXTEDITOR_INSET_HORIZONTAL,
211 | bottomInset: const.TEXTEDITOR_INSET_BOTTOM))
212 | .disabled(true)
213 |
214 | if hasBorder {
215 | RoundedRectangle(cornerRadius: const.TEXTEDITOR_STROKE_CORNER_RADIUS)
216 | .stroke()
217 | .foregroundColor(.gray)
218 | .background(
219 | Color.gray.opacity(0.7))
220 | .cornerRadius(const.TEXTEDITOR_STROKE_CORNER_RADIUS)
221 | }
222 | }
223 | .frame(maxHeight: currentTextEditorHeight)
224 | .onAppear {
225 | setTextEditorStartHeight()
226 | }
227 | }
228 | }
229 |
230 | private struct AutoHeightEditorLayoutModifier: ViewModifier {
231 | let font: Font
232 | let color: Color
233 | let lineSpace: CGFloat
234 | let maxHeight: CGFloat
235 | let horizontalInset: CGFloat
236 | let bottomInset: CGFloat
237 |
238 | func body(content: Content) -> some View {
239 | content
240 | .font(font)
241 | .foregroundColor(color)
242 | .lineSpacing(lineSpace)
243 | .frame(maxHeight: maxHeight)
244 | .padding(.horizontal, horizontalInset)
245 | .padding(.bottom, bottomInset)
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/Sources/AutoHeightEditor/Constant.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum TextEditorConst {
4 | static let TEXTEDITOR_DEFAULT_LINE_COUNT: Int = 1
5 | static let TEXTEDITOR_MAX_LINE_COUNT: Int = 5
6 | static let TEXTEDITOR_INSET_HORIZONTAL: CGFloat = 10
7 | static let TEXTEDITOR_WIDTH_HORIZONTAL_BUFFER: CGFloat = 17
8 | static let TEXTEDITOR_INSET_BOTTOM: CGFloat = 0
9 | static let TEXTEDITOR_STROKE_CORNER_RADIUS: CGFloat = 20
10 | static let TEXTEDITOR_FRAME_HEIGHT_FREESPACE: CGFloat = 20
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/AutoHeightEditor/Extension.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension CGFloat {
4 | var asInt: Int {
5 | switch self {
6 | case .infinity, .nan:
7 | return 0
8 | default:
9 | return Int(self)
10 | }
11 | }
12 | }
13 |
14 | extension Int {
15 | var asFloat: CGFloat {
16 | return CGFloat(self)
17 | }
18 | }
19 |
20 | extension UIFont {
21 | /// SwiftUI Font를 UIFont 타입으로 변환합니다.
22 | static func fontToUIFont(from font: Font) -> UIFont {
23 | let style: UIFont.TextStyle
24 |
25 | switch font {
26 | case .largeTitle: style = .largeTitle
27 | case .title: style = .title1
28 | case .title2: style = .title2
29 | case .title3: style = .title3
30 | case .headline: style = .headline
31 | case .subheadline: style = .subheadline
32 | case .callout: style = .callout
33 | case .caption: style = .caption1
34 | case .caption2: style = .caption2
35 | case .footnote: style = .footnote
36 | case .body: style = .body
37 | default: style = .body
38 | }
39 |
40 | return UIFont.preferredFont(forTextStyle: style)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/AutoHeightEditorTests/AutoHeightEditorTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import AutoHeightEditor
3 |
4 | final class AutoHeightEditorTests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 |
--------------------------------------------------------------------------------