├── .gitignore
├── README.md
├── TextMaster.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcuserdata
│ └── jageryoo.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
└── TextMaster
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ └── Contents.json
└── Contents.json
├── ContentView.swift
├── TextMaster.swift
└── TextMasterApp.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/swift
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift
3 |
4 | ### Swift ###
5 | # Xcode
6 | #
7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
8 |
9 | ## User settings
10 | xcuserdata/
11 |
12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
13 | *.xcscmblueprint
14 | *.xccheckout
15 |
16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
17 | build/
18 | DerivedData/
19 | *.moved-aside
20 | *.pbxuser
21 | !default.pbxuser
22 | *.mode1v3
23 | !default.mode1v3
24 | *.mode2v3
25 | !default.mode2v3
26 | *.perspectivev3
27 | !default.perspectivev3
28 |
29 | ## Obj-C/Swift specific
30 | *.hmap
31 |
32 | ## App packaging
33 | *.ipa
34 | *.dSYM.zip
35 | *.dSYM
36 |
37 | ## Playgrounds
38 | timeline.xctimeline
39 | playground.xcworkspace
40 |
41 | # Swift Package Manager
42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
43 | # Packages/
44 | # Package.pins
45 | # Package.resolved
46 | # *.xcodeproj
47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
48 | # hence it is not needed unless you have added a package configuration file to your project
49 | # .swiftpm
50 |
51 | .build/
52 |
53 | # CocoaPods
54 | # We recommend against adding the Pods directory to your .gitignore. However
55 | # you should judge for yourself, the pros and cons are mentioned at:
56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
57 | # Pods/
58 | # Add this line if you want to avoid checking in source code from the Xcode workspace
59 | # *.xcworkspace
60 |
61 | # Carthage
62 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
63 | # Carthage/Checkouts
64 |
65 | Carthage/Build/
66 |
67 | # Accio dependency management
68 | Dependencies/
69 | .accio/
70 |
71 | # fastlane
72 | # It is recommended to not store the screenshots in the git repo.
73 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
74 | # For more information about the recommended setup visit:
75 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
76 |
77 | fastlane/report.xml
78 | fastlane/Preview.html
79 | fastlane/screenshots/**/*.png
80 | fastlane/test_output
81 |
82 | # Code Injection
83 | # After new code Injection tools there's a generated folder /iOSInjectionProject
84 | # https://github.com/johnno1962/injectionforxcode
85 |
86 | iOSInjectionProject/
87 |
88 | # End of https://www.toptal.com/developers/gitignore/api/swift
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TextMaster
2 | ### 🗜Enhanced SwiftUI's TextEditor API powered by UITextView
3 |
4 |
5 |
6 | ## ✨ 설명
7 |
8 | `SwiftUI` 에서 여러 줄의 텍스트 입력을 받기 위해선 무엇을 사용해야 할까요?
9 |
10 | iOS 14.0+ 부터 제공되는 [TextEditor](https://developer.apple.com/documentation/swiftui/texteditor)가 있지만, 기능이 제한적입니다.
11 |
12 | 스크롤 기능을 켜고 끄거나, firstResponder 설정, 다이나믹 height 조절, 백그라운드 컬러 변경 등이 불가능합니다.
13 | 즉, TextEditor 로는 실무에서 필요한 복잡한 요구사항을 충족시키기 어렵습니다.
14 |
15 | `TextMaster` 는 UIKit 에서 iOS 2.0+ 부터 제공되는 근본 API 인 [UITextView](https://developer.apple.com/documentation/uikit/uitextview)의 기능을
16 | `SwiftUI` 에서 사용할 수 있도록 wrapping 한 구조체입니다.
17 |
18 | `TextMaster` 는 `iOS 15.0+` 부터 사용 가능합니다.
19 |
20 |
21 |
22 | 일부러 SPM 라이브러리로 만들지 않았습니다.
23 |
24 | 본 레포의 [TextMaster.swift](https://github.com/Jager-yoo/TextMaster/blob/main/TextMaster/TextMaster.swift) 파일 내부를 복사해서 가져가면, 바로 사용이 가능합니다.
25 |
26 | 커스텀이 필요한 부분은 스스로 수정해서 사용하셔도 됩니다. 😄
27 |
28 |
29 |
30 | ## ✨ 특징
31 |
32 | 실무에서 기획자/디자이너와 화면에 올리는 TextView 의 스펙을 이야기하다 보면, 단순히 고정 height 로 결정될 때도 있지만
33 |
34 | 간혹, 이런 스펙을 전달 받는 경우도 있습니다.
35 |
36 | > "처음엔 1줄만 표시되다가, 최대 5줄까지 늘어나고, 그 보다 많아지면 스크롤이 가능하도록 만들어주세요."
37 |
38 | > "아 근데 폰트는 24 정도로 좀 크게 해주시고요."
39 |
40 | > "처음에 이 페이지로 진입할 때, 바로 TextView 에 포커스가 들어오면서 키보드가 올라오게 해주세요."
41 |
42 | 충분히 합리적인 스펙이지만, 이 스펙을 SwiftUI 에서 구현하는 건 매우~ 까다롭습니다.
43 |
44 | 하지만 `TextMaster` 로는 쉽게 구현 가능합니다.
45 |
46 | ```swift
47 | TextMaster(
48 | text: $text, // @State 텍스트와 바인딩
49 | isFocused: $isTextMasterFocused, // @FocusState 와 바인딩
50 | minLine: 1, // 최소 1줄 (디폴트)
51 | maxLine: 5, // 최대 5줄 (라인이 더 늘어나면 스크롤 기능이 작동)
52 | fontSize: 24, // 폰트 사이즈 (Double 타입)
53 | becomeFirstResponder: true) // true 가 들어가면, 이 페이지가 나타날 때 자동으로 포커스가 잡히며 키보드 올라옴
54 | ```
55 |
56 | 
57 |
58 |
59 |
60 | 스펙이 이렇게 들어온다고 가정해보죠.
61 |
62 | > "최소 2줄에서 시작, 최대 4줄 까지만 커지다가 스크롤 작동, 폰트 사이즈는 16 으로 부탁드려요."
63 |
64 | > "아, 백그라운드는 quaternary 로 부탁드려요."
65 |
66 | ```swift
67 | TextMaster(
68 | text: $text,
69 | isFocused: $isTextMasterFocused,
70 | minLine: 2,
71 | maxLine: 5,
72 | fontSize: 16)
73 | .background(.quaternary)
74 | ```
75 |
76 | 
77 |
78 |
79 |
80 | 이런 스펙은 어떨까요?
81 |
82 | > "3줄 까지만 입력 가능하도록 고정시켜 주세요. 그 이상에선 스크롤이 작동해야 합니다."
83 |
84 | ```swift
85 | TextMaster(
86 | text: $text,
87 | isFocused: $isTextMasterFocused,
88 | minLine: 3,
89 | maxLine: 3,
90 | fontSize: 16)
91 | ```
92 |
93 | 
94 |
95 |
96 |
97 | 위와 같이, `TextMaster`는 SwiftUI 의 [TextEditor](https://developer.apple.com/documentation/swiftui/texteditor) API 의 한계를 벗어나고
98 |
99 | 실무에서 요구 받는 TextView 스펙을 쉽게 맞추기 위해 만들어졌습니다.
100 |
101 | 혹시나 더 추가를 원하는 기능이나 파라미터가 있다면, [Issues](https://github.com/Jager-yoo/TextMaster/issues)에 제안해주세요. 감사합니다! 😄
102 |
103 | - last edited: 2023-03-13(월)
104 | - author: [현대자동차 유재호 연구원](https://github.com/Jager-yoo)
105 |
--------------------------------------------------------------------------------
/TextMaster.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | C13F6EE029978D00004E6D58 /* TextMasterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F6EDF29978D00004E6D58 /* TextMasterApp.swift */; };
11 | C13F6EE229978D00004E6D58 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F6EE129978D00004E6D58 /* ContentView.swift */; };
12 | C13F6EE429978D00004E6D58 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C13F6EE329978D00004E6D58 /* Assets.xcassets */; };
13 | C13F6EEE299935F0004E6D58 /* TextMaster.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F6EED299935F0004E6D58 /* TextMaster.swift */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | C13F6EDC29978D00004E6D58 /* TextMaster.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TextMaster.app; sourceTree = BUILT_PRODUCTS_DIR; };
18 | C13F6EDF29978D00004E6D58 /* TextMasterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMasterApp.swift; sourceTree = ""; };
19 | C13F6EE129978D00004E6D58 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
20 | C13F6EE329978D00004E6D58 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
21 | C13F6EED299935F0004E6D58 /* TextMaster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMaster.swift; sourceTree = ""; };
22 | /* End PBXFileReference section */
23 |
24 | /* Begin PBXFrameworksBuildPhase section */
25 | C13F6ED929978D00004E6D58 /* Frameworks */ = {
26 | isa = PBXFrameworksBuildPhase;
27 | buildActionMask = 2147483647;
28 | files = (
29 | );
30 | runOnlyForDeploymentPostprocessing = 0;
31 | };
32 | /* End PBXFrameworksBuildPhase section */
33 |
34 | /* Begin PBXGroup section */
35 | C13F6ED329978D00004E6D58 = {
36 | isa = PBXGroup;
37 | children = (
38 | C13F6EDE29978D00004E6D58 /* TextMaster */,
39 | C13F6EDD29978D00004E6D58 /* Products */,
40 | );
41 | sourceTree = "";
42 | };
43 | C13F6EDD29978D00004E6D58 /* Products */ = {
44 | isa = PBXGroup;
45 | children = (
46 | C13F6EDC29978D00004E6D58 /* TextMaster.app */,
47 | );
48 | name = Products;
49 | sourceTree = "";
50 | };
51 | C13F6EDE29978D00004E6D58 /* TextMaster */ = {
52 | isa = PBXGroup;
53 | children = (
54 | C13F6EDF29978D00004E6D58 /* TextMasterApp.swift */,
55 | C13F6EE129978D00004E6D58 /* ContentView.swift */,
56 | C13F6EED299935F0004E6D58 /* TextMaster.swift */,
57 | C13F6EE329978D00004E6D58 /* Assets.xcassets */,
58 | );
59 | path = TextMaster;
60 | sourceTree = "";
61 | };
62 | /* End PBXGroup section */
63 |
64 | /* Begin PBXNativeTarget section */
65 | C13F6EDB29978D00004E6D58 /* TextMaster */ = {
66 | isa = PBXNativeTarget;
67 | buildConfigurationList = C13F6EEA29978D00004E6D58 /* Build configuration list for PBXNativeTarget "TextMaster" */;
68 | buildPhases = (
69 | C13F6ED829978D00004E6D58 /* Sources */,
70 | C13F6ED929978D00004E6D58 /* Frameworks */,
71 | C13F6EDA29978D00004E6D58 /* Resources */,
72 | );
73 | buildRules = (
74 | );
75 | dependencies = (
76 | );
77 | name = TextMaster;
78 | productName = TextMaster;
79 | productReference = C13F6EDC29978D00004E6D58 /* TextMaster.app */;
80 | productType = "com.apple.product-type.application";
81 | };
82 | /* End PBXNativeTarget section */
83 |
84 | /* Begin PBXProject section */
85 | C13F6ED429978D00004E6D58 /* Project object */ = {
86 | isa = PBXProject;
87 | attributes = {
88 | BuildIndependentTargetsInParallel = 1;
89 | LastSwiftUpdateCheck = 1420;
90 | LastUpgradeCheck = 1420;
91 | TargetAttributes = {
92 | C13F6EDB29978D00004E6D58 = {
93 | CreatedOnToolsVersion = 14.2;
94 | };
95 | };
96 | };
97 | buildConfigurationList = C13F6ED729978D00004E6D58 /* Build configuration list for PBXProject "TextMaster" */;
98 | compatibilityVersion = "Xcode 14.0";
99 | developmentRegion = en;
100 | hasScannedForEncodings = 0;
101 | knownRegions = (
102 | en,
103 | Base,
104 | );
105 | mainGroup = C13F6ED329978D00004E6D58;
106 | productRefGroup = C13F6EDD29978D00004E6D58 /* Products */;
107 | projectDirPath = "";
108 | projectRoot = "";
109 | targets = (
110 | C13F6EDB29978D00004E6D58 /* TextMaster */,
111 | );
112 | };
113 | /* End PBXProject section */
114 |
115 | /* Begin PBXResourcesBuildPhase section */
116 | C13F6EDA29978D00004E6D58 /* Resources */ = {
117 | isa = PBXResourcesBuildPhase;
118 | buildActionMask = 2147483647;
119 | files = (
120 | C13F6EE429978D00004E6D58 /* Assets.xcassets in Resources */,
121 | );
122 | runOnlyForDeploymentPostprocessing = 0;
123 | };
124 | /* End PBXResourcesBuildPhase section */
125 |
126 | /* Begin PBXSourcesBuildPhase section */
127 | C13F6ED829978D00004E6D58 /* Sources */ = {
128 | isa = PBXSourcesBuildPhase;
129 | buildActionMask = 2147483647;
130 | files = (
131 | C13F6EE229978D00004E6D58 /* ContentView.swift in Sources */,
132 | C13F6EE029978D00004E6D58 /* TextMasterApp.swift in Sources */,
133 | C13F6EEE299935F0004E6D58 /* TextMaster.swift in Sources */,
134 | );
135 | runOnlyForDeploymentPostprocessing = 0;
136 | };
137 | /* End PBXSourcesBuildPhase section */
138 |
139 | /* Begin XCBuildConfiguration section */
140 | C13F6EE829978D00004E6D58 /* Debug */ = {
141 | isa = XCBuildConfiguration;
142 | buildSettings = {
143 | ALWAYS_SEARCH_USER_PATHS = NO;
144 | CLANG_ANALYZER_NONNULL = YES;
145 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
146 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
147 | CLANG_ENABLE_MODULES = YES;
148 | CLANG_ENABLE_OBJC_ARC = YES;
149 | CLANG_ENABLE_OBJC_WEAK = YES;
150 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
151 | CLANG_WARN_BOOL_CONVERSION = YES;
152 | CLANG_WARN_COMMA = YES;
153 | CLANG_WARN_CONSTANT_CONVERSION = YES;
154 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
155 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
156 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
157 | CLANG_WARN_EMPTY_BODY = YES;
158 | CLANG_WARN_ENUM_CONVERSION = YES;
159 | CLANG_WARN_INFINITE_RECURSION = YES;
160 | CLANG_WARN_INT_CONVERSION = YES;
161 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
162 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
163 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
164 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
165 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
166 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
167 | CLANG_WARN_STRICT_PROTOTYPES = YES;
168 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
169 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
170 | CLANG_WARN_UNREACHABLE_CODE = YES;
171 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
172 | COPY_PHASE_STRIP = NO;
173 | DEBUG_INFORMATION_FORMAT = dwarf;
174 | ENABLE_STRICT_OBJC_MSGSEND = YES;
175 | ENABLE_TESTABILITY = YES;
176 | GCC_C_LANGUAGE_STANDARD = gnu11;
177 | GCC_DYNAMIC_NO_PIC = NO;
178 | GCC_NO_COMMON_BLOCKS = YES;
179 | GCC_OPTIMIZATION_LEVEL = 0;
180 | GCC_PREPROCESSOR_DEFINITIONS = (
181 | "DEBUG=1",
182 | "$(inherited)",
183 | );
184 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
185 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
186 | GCC_WARN_UNDECLARED_SELECTOR = YES;
187 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
188 | GCC_WARN_UNUSED_FUNCTION = YES;
189 | GCC_WARN_UNUSED_VARIABLE = YES;
190 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
191 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
192 | MTL_FAST_MATH = YES;
193 | ONLY_ACTIVE_ARCH = YES;
194 | SDKROOT = iphoneos;
195 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
196 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
197 | };
198 | name = Debug;
199 | };
200 | C13F6EE929978D00004E6D58 /* Release */ = {
201 | isa = XCBuildConfiguration;
202 | buildSettings = {
203 | ALWAYS_SEARCH_USER_PATHS = NO;
204 | CLANG_ANALYZER_NONNULL = YES;
205 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
207 | CLANG_ENABLE_MODULES = YES;
208 | CLANG_ENABLE_OBJC_ARC = YES;
209 | CLANG_ENABLE_OBJC_WEAK = YES;
210 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
211 | CLANG_WARN_BOOL_CONVERSION = YES;
212 | CLANG_WARN_COMMA = YES;
213 | CLANG_WARN_CONSTANT_CONVERSION = YES;
214 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
215 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
216 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
217 | CLANG_WARN_EMPTY_BODY = YES;
218 | CLANG_WARN_ENUM_CONVERSION = YES;
219 | CLANG_WARN_INFINITE_RECURSION = YES;
220 | CLANG_WARN_INT_CONVERSION = YES;
221 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
222 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
223 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
224 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
225 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
226 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
227 | CLANG_WARN_STRICT_PROTOTYPES = YES;
228 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
229 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
230 | CLANG_WARN_UNREACHABLE_CODE = YES;
231 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
232 | COPY_PHASE_STRIP = NO;
233 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
234 | ENABLE_NS_ASSERTIONS = NO;
235 | ENABLE_STRICT_OBJC_MSGSEND = YES;
236 | GCC_C_LANGUAGE_STANDARD = gnu11;
237 | GCC_NO_COMMON_BLOCKS = YES;
238 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
239 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
240 | GCC_WARN_UNDECLARED_SELECTOR = YES;
241 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
242 | GCC_WARN_UNUSED_FUNCTION = YES;
243 | GCC_WARN_UNUSED_VARIABLE = YES;
244 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
245 | MTL_ENABLE_DEBUG_INFO = NO;
246 | MTL_FAST_MATH = YES;
247 | SDKROOT = iphoneos;
248 | SWIFT_COMPILATION_MODE = wholemodule;
249 | SWIFT_OPTIMIZATION_LEVEL = "-O";
250 | VALIDATE_PRODUCT = YES;
251 | };
252 | name = Release;
253 | };
254 | C13F6EEB29978D00004E6D58 /* Debug */ = {
255 | isa = XCBuildConfiguration;
256 | buildSettings = {
257 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
258 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
259 | CODE_SIGN_STYLE = Automatic;
260 | CURRENT_PROJECT_VERSION = 1;
261 | DEVELOPMENT_ASSET_PATHS = "";
262 | ENABLE_PREVIEWS = YES;
263 | GENERATE_INFOPLIST_FILE = YES;
264 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
265 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
266 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
267 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
268 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
269 | LD_RUNPATH_SEARCH_PATHS = (
270 | "$(inherited)",
271 | "@executable_path/Frameworks",
272 | );
273 | MARKETING_VERSION = 1.0;
274 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.Jager-yoo.TextMaster";
275 | PRODUCT_NAME = "$(TARGET_NAME)";
276 | SWIFT_EMIT_LOC_STRINGS = YES;
277 | SWIFT_VERSION = 5.0;
278 | TARGETED_DEVICE_FAMILY = "1,2";
279 | };
280 | name = Debug;
281 | };
282 | C13F6EEC29978D00004E6D58 /* Release */ = {
283 | isa = XCBuildConfiguration;
284 | buildSettings = {
285 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
286 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
287 | CODE_SIGN_STYLE = Automatic;
288 | CURRENT_PROJECT_VERSION = 1;
289 | DEVELOPMENT_ASSET_PATHS = "";
290 | ENABLE_PREVIEWS = YES;
291 | GENERATE_INFOPLIST_FILE = YES;
292 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
293 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
294 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
295 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
296 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
297 | LD_RUNPATH_SEARCH_PATHS = (
298 | "$(inherited)",
299 | "@executable_path/Frameworks",
300 | );
301 | MARKETING_VERSION = 1.0;
302 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.Jager-yoo.TextMaster";
303 | PRODUCT_NAME = "$(TARGET_NAME)";
304 | SWIFT_EMIT_LOC_STRINGS = YES;
305 | SWIFT_VERSION = 5.0;
306 | TARGETED_DEVICE_FAMILY = "1,2";
307 | };
308 | name = Release;
309 | };
310 | /* End XCBuildConfiguration section */
311 |
312 | /* Begin XCConfigurationList section */
313 | C13F6ED729978D00004E6D58 /* Build configuration list for PBXProject "TextMaster" */ = {
314 | isa = XCConfigurationList;
315 | buildConfigurations = (
316 | C13F6EE829978D00004E6D58 /* Debug */,
317 | C13F6EE929978D00004E6D58 /* Release */,
318 | );
319 | defaultConfigurationIsVisible = 0;
320 | defaultConfigurationName = Release;
321 | };
322 | C13F6EEA29978D00004E6D58 /* Build configuration list for PBXNativeTarget "TextMaster" */ = {
323 | isa = XCConfigurationList;
324 | buildConfigurations = (
325 | C13F6EEB29978D00004E6D58 /* Debug */,
326 | C13F6EEC29978D00004E6D58 /* Release */,
327 | );
328 | defaultConfigurationIsVisible = 0;
329 | defaultConfigurationName = Release;
330 | };
331 | /* End XCConfigurationList section */
332 | };
333 | rootObject = C13F6ED429978D00004E6D58 /* Project object */;
334 | }
335 |
--------------------------------------------------------------------------------
/TextMaster.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TextMaster.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TextMaster.xcodeproj/xcuserdata/jageryoo.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | TextMaster.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/TextMaster/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/TextMaster/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/TextMaster/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/TextMaster/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ContentView: View {
4 |
5 | @State private var text: String = ""
6 | @FocusState private var isTextMasterFocused: Bool
7 |
8 | // MARK: 파라미터 조절
9 | private let minLine: Int = 1
10 | private let maxLine: Int = 5
11 | private let fontSize: Double = 24
12 |
13 | var body: some View {
14 | VStack(spacing: 30) {
15 | VStack(alignment: .leading) {
16 | Text("TextMaster 파라미터 값")
17 | .font(.title.bold())
18 | Text("- 최소 라인수: \(minLine)")
19 | Text("- 최대 라인수: \(maxLine)")
20 | Text("- 폰트 사이즈: \(String(format: "%.1f", fontSize))")
21 | }
22 | .padding()
23 | .background(
24 | RoundedRectangle(cornerRadius: 12)
25 | .fill(.yellow.opacity(0.2))
26 | )
27 |
28 | TextField("연결된 텍스트 필드", text: $text)
29 | .textFieldStyle(.roundedBorder)
30 |
31 | TextMaster(
32 | text: $text,
33 | isFocused: $isTextMasterFocused,
34 | minLine: minLine,
35 | maxLine: maxLine,
36 | fontSize: fontSize,
37 | becomeFirstResponder: true)
38 |
39 | HStack {
40 | Button("FOCUS IN", action: { isTextMasterFocused = true })
41 | .disabled(isTextMasterFocused)
42 |
43 | Button("FOCUS OUT", action: { isTextMasterFocused = false })
44 | .disabled(!isTextMasterFocused)
45 | }
46 | .buttonStyle(.borderedProminent)
47 | }
48 | }
49 | }
50 |
51 | struct ContentView_Previews: PreviewProvider {
52 | static var previews: some View {
53 | ContentView()
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/TextMaster/TextMaster.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TextMaster: View {
4 |
5 | @Binding var text: String
6 | @State private var dynamicHeight: CGFloat
7 |
8 | let isFocused: FocusState.Binding
9 | let minLine: Int
10 | let maxLine: Int
11 | let font: UIFont
12 | let becomeFirstResponder: Bool
13 |
14 | init(
15 | text: Binding,
16 | isFocused: FocusState.Binding,
17 | minLine: Int = 1,
18 | maxLine: Int,
19 | fontSize: CGFloat,
20 | becomeFirstResponder: Bool = false)
21 | {
22 | _text = text
23 | self.isFocused = isFocused
24 | self.minLine = minLine
25 | self.maxLine = maxLine
26 | self.becomeFirstResponder = becomeFirstResponder
27 |
28 | let font = UIFont.systemFont(ofSize: fontSize)
29 | self.font = font
30 | _dynamicHeight = State(initialValue: font.lineHeight * CGFloat(minLine) + 16) // textContainerInset 디폴트 값은 top, bottom 으로 각각 패딩 8 씩 들어감
31 | }
32 |
33 | var body: some View {
34 | UITextViewRepresentable(
35 | text: $text,
36 | dynamicHeight: $dynamicHeight,
37 | isFocused: isFocused,
38 | minLine: minLine,
39 | maxLine: maxLine,
40 | font: font,
41 | becomeFirstResponder: becomeFirstResponder)
42 | .frame(height: dynamicHeight)
43 | .focused(isFocused)
44 | .border(isFocused.wrappedValue ? Color.blue : Color.gray, width: 1)
45 | }
46 | }
47 |
48 | fileprivate struct UITextViewRepresentable: UIViewRepresentable {
49 |
50 | @Binding var text: String
51 | @Binding var dynamicHeight: CGFloat
52 |
53 | let isFocused: FocusState.Binding
54 | let minLine: Int
55 | let maxLine: Int
56 | let font: UIFont
57 | let becomeFirstResponder: Bool
58 |
59 | func makeUIView(context: UIViewRepresentableContext) -> UITextView {
60 | let textView = UITextView(frame: .zero)
61 | textView.delegate = context.coordinator
62 | textView.font = font
63 | textView.backgroundColor = .clear
64 | textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
65 | textView.isScrollEnabled = false
66 | textView.bounces = false
67 |
68 | if becomeFirstResponder {
69 | textView.becomeFirstResponder()
70 | }
71 |
72 | return textView
73 | }
74 |
75 | func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) {
76 | guard uiView.text == self.text else { // 외부에서 주입되는 텍스트에 대한 반응을 위해 필요
77 | uiView.text = self.text
78 | return
79 | }
80 | }
81 |
82 | func makeCoordinator() -> UITextViewRepresentable.Coordinator {
83 | Coordinator(
84 | text: $text,
85 | isFocused: isFocused,
86 | dynamicHeight: $dynamicHeight,
87 | minHeight: font.lineHeight * CGFloat(minLine) + 16,
88 | maxHeight: font.lineHeight * CGFloat(maxLine + (maxLine > minLine ? 1 : .zero)) + 16)
89 | }
90 |
91 | final class Coordinator: NSObject, UITextViewDelegate {
92 |
93 | @Binding var text: String
94 | @Binding var dynamicHeight: CGFloat
95 |
96 | let isFocused: FocusState.Binding
97 | let minHeight: CGFloat
98 | let maxHeight: CGFloat
99 |
100 | init(
101 | text: Binding,
102 | isFocused: FocusState.Binding,
103 | dynamicHeight: Binding,
104 | minHeight: CGFloat,
105 | maxHeight: CGFloat)
106 | {
107 | _text = text
108 | self.isFocused = isFocused
109 | _dynamicHeight = dynamicHeight
110 | self.minHeight = minHeight
111 | self.maxHeight = maxHeight
112 | }
113 |
114 | func textViewDidBeginEditing(_ textView: UITextView) {
115 | isFocused.wrappedValue = true
116 | }
117 |
118 | func textViewDidEndEditing(_ textView: UITextView) {
119 | isFocused.wrappedValue = false
120 | }
121 |
122 | func textViewDidChange(_ textView: UITextView) {
123 | self.text = textView.text ?? ""
124 |
125 | if text.isEmpty {
126 | dynamicHeight = minHeight
127 | textView.isScrollEnabled = false
128 | return
129 | }
130 |
131 | let newSize = textView.sizeThatFits(.init(width: textView.frame.width, height: .greatestFiniteMagnitude))
132 |
133 | print("\n🔽최대 높이 -> \(maxHeight)")
134 | print("❤️NEW SIZE -> \(newSize.height) / lineHeight -> \(textView.font!.lineHeight)")
135 | print("🔼최소 높이 -> \(minHeight)")
136 |
137 | if newSize.height < maxHeight, textView.isScrollEnabled { // 최대 높이 미만으로 줄어들면서, 스크롤이 true 라면...
138 | textView.isScrollEnabled = false
139 | print("📜 스크롤 뷰 꺼짐!")
140 | } else if newSize.height > maxHeight, !textView.isScrollEnabled { // 최대 높이 초과로 커지면서, 스크롤이 false 라면...
141 | textView.isScrollEnabled = true
142 | textView.flashScrollIndicators()
143 | print("🦋 스크롤 뷰 켜짐!")
144 | }
145 |
146 | guard newSize.height > minHeight, newSize.height < maxHeight else { return }
147 | dynamicHeight = newSize.height // 텍스트뷰의 동적 높이 조절
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/TextMaster/TextMasterApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct TextMasterApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------