├── .gitignore
├── .swift-version
├── .swiftformat
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Example
├── FloatingPromptTextFieldExample.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Shared
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ └── FloatingPromptTextFieldExampleApp.swift
├── LICENSE
├── Package.swift
├── README.md
├── Screenshots
├── Capture.gif
├── Screenshot0.png
└── Screenshot1.png
└── Sources
└── FloatingPromptTextField
├── EnvironmentValues.swift
├── FloatingPromptTextField.swift
├── HeightPreferenceKey.swift
└── ViewExtensions.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata
19 |
20 | ## Other
21 | *.xccheckout
22 | *.moved-aside
23 | *.xcuserstate
24 | *.xcscmblueprint
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 | *.ipa
29 |
30 | ## Playgrounds
31 | timeline.xctimeline
32 | playground.xcworkspace
33 |
34 | # Swift Package Manager
35 | #
36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
37 | # Packages/
38 | .build/
39 |
40 | # CocoaPods
41 | #
42 | # We recommend against adding the Pods directory to your .gitignore. However
43 | # you should judge for yourself, the pros and cons are mentioned at:
44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
45 | #
46 | # Pods/
47 |
48 | # Carthage
49 | #
50 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
51 | # Carthage/Checkouts
52 |
53 | Carthage/Build
54 |
55 | # fastlane
56 | #
57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
58 | # screenshots whenever they are needed.
59 | # For more information about the recommended setup visit:
60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
61 |
62 | fastlane/report.xml
63 | fastlane/screenshots/*/*.png
64 | fastlane/screenshots/screenshots.html
65 | .DS_Store
66 | UserInterfaceState.xcuserstate
67 | /fastlane/test_output
68 | /fastlane/screenshots/README.txt
69 | /fastlane/screenshots/README.md
70 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.5
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --exclude .github,Carthage,Example.xcodeproj,fastlane
2 |
3 | --indent tab
4 | --ifdef no-indent
5 | --self init-only
6 | --trimwhitespace nonblank-lines
7 | --disable blankLinesAtEndOfScope,blankLinesAtStartOfScope,wrapMultilineStatementBraces
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - anyobject_protocol
3 | - array_init
4 | - attributes
5 | - balanced_xctest_lifecycle
6 | - capture_variable
7 | - closure_end_indentation
8 | - closure_spacing
9 | - collection_alignment
10 | - contains_over_filter_count
11 | - contains_over_filter_is_empty
12 | - contains_over_first_not_nil
13 | - contains_over_range_nil_comparison
14 | - convenience_type
15 | - discarded_notification_center_observer
16 | - discouraged_assert
17 | - discouraged_object_literal
18 | - discouraged_optional_boolean
19 | - discouraged_optional_collection
20 | - empty_collection_literal
21 | - empty_count
22 | - empty_string
23 | - empty_xctest_method
24 | - enum_case_associated_values_count
25 | - expiring_todo
26 | - explicit_init
27 | - fatal_error_message
28 | - file_name_no_space
29 | - first_where
30 | - flatmap_over_map_reduce
31 | - force_unwrapping
32 | - identical_operands
33 | - implicit_return
34 | - implicitly_unwrapped_optional
35 | - joined_default_parameter
36 | - last_where
37 | - legacy_multiple
38 | - legacy_objc_type
39 | - legacy_random
40 | - literal_expression_end_indentation
41 | - lower_acl_than_parent
42 | - modifier_order
43 | # - multiline_arguments disabled temporarily due to false positives
44 | - multiline_function_chains
45 | - multiline_literal_brackets
46 | - multiline_parameters
47 | # - number_separator
48 | - operator_usage_whitespace
49 | - optional_enum_case_matching
50 | - overridden_super_call
51 | - pattern_matching_keywords
52 | - prefer_self_type_over_type_of_self
53 | - prefer_zero_over_explicit_init
54 | - private_action
55 | - private_outlet
56 | - private_subject
57 | - redundant_nil_coalescing
58 | - redundant_type_annotation
59 | - single_test_class
60 | - sorted_first_last
61 | - sorted_imports
62 | - static_operator
63 | - strong_iboutlet
64 | - test_case_accessibility
65 | - toggle_bool
66 | - trailing_closure
67 | - unavailable_function
68 | - unneeded_parentheses_in_closure_argument
69 | - untyped_error_in_catch
70 | - unused_import
71 | - xct_specific_matcher
72 | - yoda_condition
73 |
74 | disabled_rules:
75 | - deployment_target
76 | - no_fallthrough_only
77 | - trailing_comma
78 | - trailing_whitespace
79 | - unused_enumerated
80 | - vertical_parameter_alignment
81 |
82 | enum_case_associated_values_count:
83 | error: 10
84 |
85 | file_length:
86 | warning: 500
87 |
88 | function_body_length:
89 | warning: 50
90 |
91 | function_parameter_count:
92 | warning: 10
93 | error: 20
94 |
95 | identifier_name:
96 | min_length:
97 | error: 3
98 | max_length:
99 | error: 40
100 | excluded:
101 | - id
102 | - Id
103 | - i
104 | - j
105 | - k
106 | - x
107 | - y
108 | - z
109 | - on
110 | - us
111 |
112 | nesting:
113 | type_level:
114 | warning: 4
115 | error: 8
116 | function_level:
117 | warning: 6
118 | error: 10
119 |
120 | type_body_length:
121 | warning: 300
122 | error: 500
123 |
124 | type_name:
125 | min_length:
126 | warning: 3
127 | error: 1
128 | max_length:
129 | warning: 50
130 | error: 100
131 |
132 | large_tuple:
133 | warning: 4
134 | error: 6
135 |
136 | line_length:
137 | warning: 180
138 | ignores_comments: true
139 |
140 | excluded:
141 | - Pods
142 | - Carthage
143 | - "*Tests"
144 | - "*Preview"
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/FloatingPromptTextFieldExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | C80429FB267F76C70093774C /* FloatingPromptTextFieldExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80429ED267F76C60093774C /* FloatingPromptTextFieldExampleApp.swift */; };
11 | C80429FD267F76C70093774C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80429EE267F76C60093774C /* ContentView.swift */; };
12 | C80429FF267F76C70093774C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C80429EF267F76C70093774C /* Assets.xcassets */; };
13 | C8042A0D267F77100093774C /* FloatingPromptTextField in Frameworks */ = {isa = PBXBuildFile; productRef = C8042A0C267F77100093774C /* FloatingPromptTextField */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | C80429ED267F76C60093774C /* FloatingPromptTextFieldExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPromptTextFieldExampleApp.swift; sourceTree = ""; };
18 | C80429EE267F76C60093774C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
19 | C80429EF267F76C70093774C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
20 | C80429F4267F76C70093774C /* FloatingPromptTextFieldExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPromptTextFieldExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
21 | C8042A0A267F76E30093774C /* FocusTextField */ = {isa = PBXFileReference; lastKnownFileType = folder; name = FocusTextField; path = ..; sourceTree = ""; };
22 | /* End PBXFileReference section */
23 |
24 | /* Begin PBXFrameworksBuildPhase section */
25 | C80429F1267F76C70093774C /* Frameworks */ = {
26 | isa = PBXFrameworksBuildPhase;
27 | buildActionMask = 2147483647;
28 | files = (
29 | C8042A0D267F77100093774C /* FloatingPromptTextField in Frameworks */,
30 | );
31 | runOnlyForDeploymentPostprocessing = 0;
32 | };
33 | /* End PBXFrameworksBuildPhase section */
34 |
35 | /* Begin PBXGroup section */
36 | C80429E7267F76C60093774C = {
37 | isa = PBXGroup;
38 | children = (
39 | C80429EC267F76C60093774C /* Shared */,
40 | C8042A09267F76E30093774C /* Packages */,
41 | C80429F5267F76C70093774C /* Products */,
42 | C8042A0B267F77100093774C /* Frameworks */,
43 | );
44 | sourceTree = "";
45 | };
46 | C80429EC267F76C60093774C /* Shared */ = {
47 | isa = PBXGroup;
48 | children = (
49 | C80429ED267F76C60093774C /* FloatingPromptTextFieldExampleApp.swift */,
50 | C80429EE267F76C60093774C /* ContentView.swift */,
51 | C80429EF267F76C70093774C /* Assets.xcassets */,
52 | );
53 | path = Shared;
54 | sourceTree = "";
55 | };
56 | C80429F5267F76C70093774C /* Products */ = {
57 | isa = PBXGroup;
58 | children = (
59 | C80429F4267F76C70093774C /* FloatingPromptTextFieldExample.app */,
60 | );
61 | name = Products;
62 | sourceTree = "";
63 | };
64 | C8042A09267F76E30093774C /* Packages */ = {
65 | isa = PBXGroup;
66 | children = (
67 | C8042A0A267F76E30093774C /* FocusTextField */,
68 | );
69 | name = Packages;
70 | sourceTree = "";
71 | };
72 | C8042A0B267F77100093774C /* Frameworks */ = {
73 | isa = PBXGroup;
74 | children = (
75 | );
76 | name = Frameworks;
77 | sourceTree = "";
78 | };
79 | /* End PBXGroup section */
80 |
81 | /* Begin PBXNativeTarget section */
82 | C80429F3267F76C70093774C /* FloatingPromptTextFieldExample (iOS) */ = {
83 | isa = PBXNativeTarget;
84 | buildConfigurationList = C8042A03267F76C70093774C /* Build configuration list for PBXNativeTarget "FloatingPromptTextFieldExample (iOS)" */;
85 | buildPhases = (
86 | C80429F0267F76C70093774C /* Sources */,
87 | C80429F1267F76C70093774C /* Frameworks */,
88 | C80429F2267F76C70093774C /* Resources */,
89 | );
90 | buildRules = (
91 | );
92 | dependencies = (
93 | );
94 | name = "FloatingPromptTextFieldExample (iOS)";
95 | packageProductDependencies = (
96 | C8042A0C267F77100093774C /* FloatingPromptTextField */,
97 | );
98 | productName = "FloatingPromptTextFieldExample (iOS)";
99 | productReference = C80429F4267F76C70093774C /* FloatingPromptTextFieldExample.app */;
100 | productType = "com.apple.product-type.application";
101 | };
102 | /* End PBXNativeTarget section */
103 |
104 | /* Begin PBXProject section */
105 | C80429E8267F76C60093774C /* Project object */ = {
106 | isa = PBXProject;
107 | attributes = {
108 | BuildIndependentTargetsInParallel = 1;
109 | LastSwiftUpdateCheck = 1300;
110 | LastUpgradeCheck = 1320;
111 | TargetAttributes = {
112 | C80429F3267F76C70093774C = {
113 | CreatedOnToolsVersion = 13.0;
114 | };
115 | };
116 | };
117 | buildConfigurationList = C80429EB267F76C60093774C /* Build configuration list for PBXProject "FloatingPromptTextFieldExample" */;
118 | compatibilityVersion = "Xcode 13.0";
119 | developmentRegion = en;
120 | hasScannedForEncodings = 0;
121 | knownRegions = (
122 | en,
123 | Base,
124 | );
125 | mainGroup = C80429E7267F76C60093774C;
126 | productRefGroup = C80429F5267F76C70093774C /* Products */;
127 | projectDirPath = "";
128 | projectRoot = "";
129 | targets = (
130 | C80429F3267F76C70093774C /* FloatingPromptTextFieldExample (iOS) */,
131 | );
132 | };
133 | /* End PBXProject section */
134 |
135 | /* Begin PBXResourcesBuildPhase section */
136 | C80429F2267F76C70093774C /* Resources */ = {
137 | isa = PBXResourcesBuildPhase;
138 | buildActionMask = 2147483647;
139 | files = (
140 | C80429FF267F76C70093774C /* Assets.xcassets in Resources */,
141 | );
142 | runOnlyForDeploymentPostprocessing = 0;
143 | };
144 | /* End PBXResourcesBuildPhase section */
145 |
146 | /* Begin PBXSourcesBuildPhase section */
147 | C80429F0267F76C70093774C /* Sources */ = {
148 | isa = PBXSourcesBuildPhase;
149 | buildActionMask = 2147483647;
150 | files = (
151 | C80429FD267F76C70093774C /* ContentView.swift in Sources */,
152 | C80429FB267F76C70093774C /* FloatingPromptTextFieldExampleApp.swift in Sources */,
153 | );
154 | runOnlyForDeploymentPostprocessing = 0;
155 | };
156 | /* End PBXSourcesBuildPhase section */
157 |
158 | /* Begin XCBuildConfiguration section */
159 | C8042A01267F76C70093774C /* Debug */ = {
160 | isa = XCBuildConfiguration;
161 | buildSettings = {
162 | ALWAYS_SEARCH_USER_PATHS = NO;
163 | CLANG_ANALYZER_NONNULL = YES;
164 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
165 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
166 | CLANG_CXX_LIBRARY = "libc++";
167 | CLANG_ENABLE_MODULES = YES;
168 | CLANG_ENABLE_OBJC_ARC = YES;
169 | CLANG_ENABLE_OBJC_WEAK = YES;
170 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
171 | CLANG_WARN_BOOL_CONVERSION = YES;
172 | CLANG_WARN_COMMA = YES;
173 | CLANG_WARN_CONSTANT_CONVERSION = YES;
174 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
175 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
176 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
177 | CLANG_WARN_EMPTY_BODY = YES;
178 | CLANG_WARN_ENUM_CONVERSION = YES;
179 | CLANG_WARN_INFINITE_RECURSION = YES;
180 | CLANG_WARN_INT_CONVERSION = YES;
181 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
182 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
183 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
184 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
185 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
186 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
187 | CLANG_WARN_STRICT_PROTOTYPES = YES;
188 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
189 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
190 | CLANG_WARN_UNREACHABLE_CODE = YES;
191 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
192 | COPY_PHASE_STRIP = NO;
193 | DEBUG_INFORMATION_FORMAT = dwarf;
194 | ENABLE_STRICT_OBJC_MSGSEND = YES;
195 | ENABLE_TESTABILITY = YES;
196 | GCC_C_LANGUAGE_STANDARD = gnu11;
197 | GCC_DYNAMIC_NO_PIC = NO;
198 | GCC_NO_COMMON_BLOCKS = YES;
199 | GCC_OPTIMIZATION_LEVEL = 0;
200 | GCC_PREPROCESSOR_DEFINITIONS = (
201 | "DEBUG=1",
202 | "$(inherited)",
203 | );
204 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
205 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
206 | GCC_WARN_UNDECLARED_SELECTOR = YES;
207 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
208 | GCC_WARN_UNUSED_FUNCTION = YES;
209 | GCC_WARN_UNUSED_VARIABLE = YES;
210 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
211 | MTL_FAST_MATH = YES;
212 | ONLY_ACTIVE_ARCH = YES;
213 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
214 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
215 | };
216 | name = Debug;
217 | };
218 | C8042A02267F76C70093774C /* Release */ = {
219 | isa = XCBuildConfiguration;
220 | buildSettings = {
221 | ALWAYS_SEARCH_USER_PATHS = NO;
222 | CLANG_ANALYZER_NONNULL = YES;
223 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
224 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
225 | CLANG_CXX_LIBRARY = "libc++";
226 | CLANG_ENABLE_MODULES = YES;
227 | CLANG_ENABLE_OBJC_ARC = YES;
228 | CLANG_ENABLE_OBJC_WEAK = YES;
229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
230 | CLANG_WARN_BOOL_CONVERSION = YES;
231 | CLANG_WARN_COMMA = YES;
232 | CLANG_WARN_CONSTANT_CONVERSION = YES;
233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
236 | CLANG_WARN_EMPTY_BODY = YES;
237 | CLANG_WARN_ENUM_CONVERSION = YES;
238 | CLANG_WARN_INFINITE_RECURSION = YES;
239 | CLANG_WARN_INT_CONVERSION = YES;
240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
244 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
246 | CLANG_WARN_STRICT_PROTOTYPES = YES;
247 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
248 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
249 | CLANG_WARN_UNREACHABLE_CODE = YES;
250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
251 | COPY_PHASE_STRIP = NO;
252 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
253 | ENABLE_NS_ASSERTIONS = NO;
254 | ENABLE_STRICT_OBJC_MSGSEND = YES;
255 | GCC_C_LANGUAGE_STANDARD = gnu11;
256 | GCC_NO_COMMON_BLOCKS = YES;
257 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
258 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
259 | GCC_WARN_UNDECLARED_SELECTOR = YES;
260 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
261 | GCC_WARN_UNUSED_FUNCTION = YES;
262 | GCC_WARN_UNUSED_VARIABLE = YES;
263 | MTL_ENABLE_DEBUG_INFO = NO;
264 | MTL_FAST_MATH = YES;
265 | SWIFT_COMPILATION_MODE = wholemodule;
266 | SWIFT_OPTIMIZATION_LEVEL = "-O";
267 | };
268 | name = Release;
269 | };
270 | C8042A04267F76C70093774C /* Debug */ = {
271 | isa = XCBuildConfiguration;
272 | buildSettings = {
273 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
274 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
275 | CODE_SIGN_STYLE = Automatic;
276 | CURRENT_PROJECT_VERSION = 1;
277 | DEVELOPMENT_TEAM = FS696NSBK7;
278 | ENABLE_PREVIEWS = YES;
279 | GENERATE_INFOPLIST_FILE = YES;
280 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
281 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
282 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
283 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
284 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
285 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
286 | LD_RUNPATH_SEARCH_PATHS = (
287 | "$(inherited)",
288 | "@executable_path/Frameworks",
289 | );
290 | MARKETING_VERSION = 1.0;
291 | PRODUCT_BUNDLE_IDENTIFIER = com.emiliopelaez.FloatingPromptTextFieldExample;
292 | PRODUCT_NAME = FloatingPromptTextFieldExample;
293 | SDKROOT = iphoneos;
294 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
295 | SUPPORTS_MACCATALYST = NO;
296 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
297 | SWIFT_EMIT_LOC_STRINGS = YES;
298 | SWIFT_VERSION = 5.0;
299 | TARGETED_DEVICE_FAMILY = "1,2";
300 | };
301 | name = Debug;
302 | };
303 | C8042A05267F76C70093774C /* Release */ = {
304 | isa = XCBuildConfiguration;
305 | buildSettings = {
306 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
307 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
308 | CODE_SIGN_STYLE = Automatic;
309 | CURRENT_PROJECT_VERSION = 1;
310 | DEVELOPMENT_TEAM = FS696NSBK7;
311 | ENABLE_PREVIEWS = YES;
312 | GENERATE_INFOPLIST_FILE = YES;
313 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
314 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
315 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
316 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
317 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
318 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
319 | LD_RUNPATH_SEARCH_PATHS = (
320 | "$(inherited)",
321 | "@executable_path/Frameworks",
322 | );
323 | MARKETING_VERSION = 1.0;
324 | PRODUCT_BUNDLE_IDENTIFIER = com.emiliopelaez.FloatingPromptTextFieldExample;
325 | PRODUCT_NAME = FloatingPromptTextFieldExample;
326 | SDKROOT = iphoneos;
327 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
328 | SUPPORTS_MACCATALYST = NO;
329 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
330 | SWIFT_EMIT_LOC_STRINGS = YES;
331 | SWIFT_VERSION = 5.0;
332 | TARGETED_DEVICE_FAMILY = "1,2";
333 | VALIDATE_PRODUCT = YES;
334 | };
335 | name = Release;
336 | };
337 | /* End XCBuildConfiguration section */
338 |
339 | /* Begin XCConfigurationList section */
340 | C80429EB267F76C60093774C /* Build configuration list for PBXProject "FloatingPromptTextFieldExample" */ = {
341 | isa = XCConfigurationList;
342 | buildConfigurations = (
343 | C8042A01267F76C70093774C /* Debug */,
344 | C8042A02267F76C70093774C /* Release */,
345 | );
346 | defaultConfigurationIsVisible = 0;
347 | defaultConfigurationName = Release;
348 | };
349 | C8042A03267F76C70093774C /* Build configuration list for PBXNativeTarget "FloatingPromptTextFieldExample (iOS)" */ = {
350 | isa = XCConfigurationList;
351 | buildConfigurations = (
352 | C8042A04267F76C70093774C /* Debug */,
353 | C8042A05267F76C70093774C /* Release */,
354 | );
355 | defaultConfigurationIsVisible = 0;
356 | defaultConfigurationName = Release;
357 | };
358 | /* End XCConfigurationList section */
359 |
360 | /* Begin XCSwiftPackageProductDependency section */
361 | C8042A0C267F77100093774C /* FloatingPromptTextField */ = {
362 | isa = XCSwiftPackageProductDependency;
363 | productName = FloatingPromptTextField;
364 | };
365 | /* End XCSwiftPackageProductDependency section */
366 | };
367 | rootObject = C80429E8267F76C60093774C /* Project object */;
368 | }
369 |
--------------------------------------------------------------------------------
/Example/FloatingPromptTextFieldExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/FloatingPromptTextFieldExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Shared/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 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | },
93 | {
94 | "idiom" : "mac",
95 | "scale" : "1x",
96 | "size" : "16x16"
97 | },
98 | {
99 | "idiom" : "mac",
100 | "scale" : "2x",
101 | "size" : "16x16"
102 | },
103 | {
104 | "idiom" : "mac",
105 | "scale" : "1x",
106 | "size" : "32x32"
107 | },
108 | {
109 | "idiom" : "mac",
110 | "scale" : "2x",
111 | "size" : "32x32"
112 | },
113 | {
114 | "idiom" : "mac",
115 | "scale" : "1x",
116 | "size" : "128x128"
117 | },
118 | {
119 | "idiom" : "mac",
120 | "scale" : "2x",
121 | "size" : "128x128"
122 | },
123 | {
124 | "idiom" : "mac",
125 | "scale" : "1x",
126 | "size" : "256x256"
127 | },
128 | {
129 | "idiom" : "mac",
130 | "scale" : "2x",
131 | "size" : "256x256"
132 | },
133 | {
134 | "idiom" : "mac",
135 | "scale" : "1x",
136 | "size" : "512x512"
137 | },
138 | {
139 | "idiom" : "mac",
140 | "scale" : "2x",
141 | "size" : "512x512"
142 | }
143 | ],
144 | "info" : {
145 | "author" : "xcode",
146 | "version" : 1
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Shared/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Shared
4 | //
5 | // Created by Emilio Peláez on 20/6/21.
6 | //
7 |
8 | import FloatingPromptTextField
9 | import SwiftUI
10 |
11 | struct ContentView: View {
12 | @State var textZero: String = ""
13 | @State var textOne: String = "Hello World"
14 | @State var textTwo: String = "Hello World"
15 | @State var textThree: String = "Hello World"
16 | @State var textFour: String = "Hello World"
17 |
18 | enum Focus: Int, Hashable {
19 | case zero, one, two, three, four
20 | }
21 |
22 | @FocusState var focus: Focus?
23 |
24 | var body: some View {
25 | ZStack(alignment: .center) {
26 | Color.clear
27 | VStack(spacing: 25) {
28 | FloatingPromptTextField(text: $textZero, prompt: Text("Input Zero"))
29 | .animateFloatingPromptHeight(true)
30 | .focused($focus, equals: .zero)
31 | FloatingPromptTextField(text: $textOne) {
32 | Label("Input One", systemImage: "pencil.circle").foregroundStyle(.secondary)
33 | }
34 | .focused($focus, equals: .one)
35 | FloatingPromptTextField(text: $textTwo) {
36 | Label("Input Two", systemImage: "pencil.circle").foregroundStyle(.secondary)
37 | }
38 | .textFieldForegroundStyle(Color.red)
39 | .focused($focus, equals: .two)
40 | FloatingPromptTextField(text: $textThree) {
41 | Label("Input Three", systemImage: "pencil.circle").foregroundStyle(.secondary)
42 | }
43 | .floatingPrompt {
44 | Label("Input Three", systemImage: "pencil.circle.fill").foregroundStyle(Color.blue)
45 | }
46 | .focused($focus, equals: .three)
47 | FloatingPromptTextField(text: $textFour) {
48 | Label("Input Four", systemImage: "pencil.circle").foregroundStyle(.secondary)
49 | }
50 | .textFieldForegroundStyle(Color.red)
51 | .floatingPrompt {
52 | Label("Input Four", systemImage: "pencil.circle.fill").foregroundStyle(Color.blue)
53 | }
54 | .floatingPromptSpacing(5)
55 | .floatingPromptScale(0.65)
56 | .focused($focus, equals: .four)
57 | HStack {
58 | Spacer()
59 | Button(action: unfocusAction) {
60 | Text("Unfocus")
61 | }
62 | .buttonStyle(.bordered)
63 | Spacer()
64 | }
65 | }
66 | .padding()
67 | .cornerRadius(10)
68 | .padding()
69 | }
70 | }
71 |
72 | func unfocusAction() {
73 | focus = nil
74 | }
75 | }
76 |
77 | struct ContentView_Previews: PreviewProvider {
78 | static var previews: some View {
79 | ContentView()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Example/Shared/FloatingPromptTextFieldExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingPromptTextFieldExampleApp.swift
3 | // Shared
4 | //
5 | // Created by Emilio Peláez on 20/6/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct FloatingPromptTextFieldExampleApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Emilio Peláez
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 |
23 | Exceptions:
24 | The application icon ("Icon") is excluded from this license.
25 | A non-exclusive licensewas provided by Michael Flarup to Emilio Peláez for
26 | the personal usage of the Icon.
27 | The Icon does not constitute part of the Software.
28 |
--------------------------------------------------------------------------------
/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: "FloatingPromptTextField",
8 | platforms: [.iOS(.v15), .macOS(.v12)],
9 | products: [
10 | .library(
11 | name: "FloatingPromptTextField",
12 | targets: ["FloatingPromptTextField"]
13 | ),
14 | ],
15 | dependencies: [],
16 | targets: [
17 | .target(
18 | name: "FloatingPromptTextField",
19 | dependencies: []
20 | ),
21 | ]
22 | )
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FloatingPromptTextField
2 |
3 | []()
4 | [](https://developer.apple.com/swift)
5 | [](https://opensource.org/licenses/MIT)
6 | [](http://twitter.com/emiliopelaez)
7 |
8 | A prompt is the label in a text field that informs the user about the kind of content the text field expects. In a default `TextField` it disappears when the user starts typing, hiding this important information.
9 |
10 | A "floating" prompt/label/placeholder is a UX pattern pioneered by [JVFloatLabeledTextField](https://github.com/jverdi/JVFloatLabeledTextField) where the prompt floats over the text field when it becomes active, keeping this useful information visible even after the user has begun typing.
11 |
12 | `FloatingPromptTextField` is a SwiftUI version of this UI component. It uses the new Focus system and because of it requires iOS 15.
13 |
14 |
15 |
16 |
17 |
18 | ## Features
19 |
20 | * Use a `Text` view as the prompt
21 | * Use any `View` as the prompt
22 | * Use a different `View` as the floating prompt
23 | * Set the floating prompt scale
24 | * Set the floating prompt spacing
25 |
26 | ## Usage
27 |
28 | Usage is as simple as importing `FloatingPromptTextField`, declaring a `@State` `String` variable, and initializing `FloatingPromptTextField` with a `Text` or any `View`.
29 |
30 | ```swift
31 | @import FloatingPromptTextField
32 |
33 | ...
34 |
35 | @State var text: String = ""
36 |
37 | ...
38 |
39 | FloatingPromptTextField(text: $text, prompt: Text("Prompt"))
40 |
41 | FocusTextField(text: $text) {
42 | Label("Prompt", systemImage: "pencil.circle")
43 | }
44 | ```
45 |
46 | ## Customization
47 |
48 | All of the customization is done using modifier-style functions.
49 |
50 | ### Customizing the Floating Prompt
51 |
52 | The `floatingPrompt` receives a view that will replace the prompt as it becomes floating. For best results it's recommended to use a view that will have the same height as the prompt.
53 |
54 | In this example we use a `Text` view with the same font but different contents and foreground styles.
55 |
56 | ```swift
57 | FloatingPromptTextField(text: $text) {
58 | Text("Prompt")
59 | }
60 | .floatingPrompt {
61 | Text("Floating Prompt")
62 | .foregroundStyle(Color.blue)
63 | }
64 | ```
65 |
66 | Note: This function is exclusive to `FloatingPromptTextField`, so it must be called before calling other modifiers.
67 |
68 | ### TextField Color/Gradient
69 |
70 | ```swift
71 | FloatingPromptTextField(text: $text, prompt: Text("Prompt"))
72 | .textFieldForegroundStyle(Color.red)
73 | ```
74 |
75 | Note: This function is exclusive to `FloatingPromptTextField`, so it must be called before calling other modifiers.
76 |
77 | ### Floating Prompt Spacing, Scale and Animation
78 |
79 | `floatingPromptScale(_ scale: Double)` will determine the scale that will be used when the prompt becomes a floating label.
80 |
81 | `floatingPromptSpacing(_ spacing: Double)` will determine the spacing between the text field and the floating prompt.
82 |
83 | `animateFloatingPromptHeight(_ animate: Bool)` will determine whether or not the view will animate its height to accommodate the floating prompt, or if the height of the floating prompt will always be calculated into the height's view.
84 |
85 | ```swift
86 | FloatingPromptTextField(text: $text, prompt: Text("Prompt"))
87 | .floatingPromptScale(0.65)
88 | .floatingPromptSpacing(5)
89 | .animateFloatingPromptHeight(true)
90 | ```
91 |
92 | ## To Do
93 |
94 | - Accessibility
95 |
--------------------------------------------------------------------------------
/Screenshots/Capture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmilioPelaez/FloatingPromptTextField/eac6ebeed385c7527eab8e63eb0371708a271b12/Screenshots/Capture.gif
--------------------------------------------------------------------------------
/Screenshots/Screenshot0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmilioPelaez/FloatingPromptTextField/eac6ebeed385c7527eab8e63eb0371708a271b12/Screenshots/Screenshot0.png
--------------------------------------------------------------------------------
/Screenshots/Screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmilioPelaez/FloatingPromptTextField/eac6ebeed385c7527eab8e63eb0371708a271b12/Screenshots/Screenshot1.png
--------------------------------------------------------------------------------
/Sources/FloatingPromptTextField/EnvironmentValues.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Emilio Peláez on 9/1/22.
3 | //
4 |
5 | import SwiftUI
6 |
7 | struct FloatingPromptScaleKey: EnvironmentKey {
8 | static var defaultValue: Double = 0.65
9 | }
10 |
11 | struct FloatingPromptSpacingKey: EnvironmentKey {
12 | static var defaultValue: Double = 5
13 | }
14 |
15 | struct PromptLeadingMarginKey: EnvironmentKey {
16 | static var defaultValue: Double = 0
17 | }
18 |
19 | struct AnimateFloatingPromptHeightKey: EnvironmentKey {
20 | static var defaultValue = false
21 | }
22 |
23 | extension EnvironmentValues {
24 |
25 | var floatingPromptScale: Double {
26 | get { self[FloatingPromptScaleKey.self] }
27 | set { self[FloatingPromptScaleKey.self] = newValue }
28 | }
29 |
30 | var promptLeadingMargin: Double {
31 | get { self[PromptLeadingMarginKey.self] }
32 | set { self[PromptLeadingMarginKey.self] = newValue }
33 | }
34 |
35 | var floatingPromptSpacing: Double {
36 | get { self[FloatingPromptSpacingKey.self] }
37 | set { self[FloatingPromptSpacingKey.self] = newValue }
38 | }
39 |
40 | var animateFloatingPromptHeight: Bool {
41 | get { self[AnimateFloatingPromptHeightKey.self] }
42 | set { self[AnimateFloatingPromptHeightKey.self] = newValue }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/FloatingPromptTextField/FloatingPromptTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Emilio Peláez on 14/6/21.
3 | //
4 |
5 | import SwiftUI
6 |
7 | /// A text input control with a prompt that moves or "floats" when it
8 | /// becomes focused, and for as long as the input text is not empty.
9 | public struct FloatingPromptTextField: View {
10 |
11 | enum PromptState {
12 | case normal
13 | case floating
14 | }
15 |
16 | @FocusState private var isFocused: Bool
17 |
18 | private var text: Binding
19 | private let textFieldStyle: TextFieldStyle
20 | private let prompt: Prompt
21 | private let floatingPrompt: FloatingPrompt
22 |
23 | @Environment(\.floatingPromptScale) var floatingPromptScale
24 | @Environment(\.floatingPromptSpacing) var floatingPromptSpacing
25 | @Environment(\.promptLeadingMargin) var promptLeadingMargin
26 | @Environment(\.animateFloatingPromptHeight) var animateFloatingPromptHeight
27 |
28 | @State private var promptState: PromptState
29 | @State private var promptHeight: Double = 0
30 |
31 | private var floatingOffset: Double { floatingPromptSpacing + promptHeight * floatingPromptScale }
32 | private var topMargin: Double { animateFloatingPromptHeight && promptState == .normal ? 0 : floatingOffset }
33 |
34 | fileprivate init(text: Binding,
35 | textFieldStyle: TextFieldStyle,
36 | @ViewBuilder prompt: () -> Prompt,
37 | @ViewBuilder floatingPrompt: () -> FloatingPrompt) {
38 | self.text = text
39 | self.prompt = prompt()
40 | self.floatingPrompt = floatingPrompt()
41 |
42 | self.textFieldStyle = textFieldStyle
43 |
44 | _promptState = State(initialValue: text.wrappedValue.isEmpty ? .normal : .floating)
45 | }
46 |
47 | public var body: some View {
48 | ZStack(alignment: .leading) {
49 | TextField("", text: text)
50 | .foregroundStyle(textFieldStyle)
51 | .focused($isFocused)
52 | ZStack(alignment: .leading) {
53 | prompt
54 | .opacity(promptState == .normal ? 1 : 0)
55 | floatingPrompt
56 | .background(
57 | GeometryReader { proxy in
58 | Color.clear
59 | .preference(key: HeightPreferenceKey.self, value: proxy.size.height)
60 | }
61 | )
62 | .opacity(promptState == .floating ? 1 : 0)
63 | }
64 | .padding(.leading, promptLeadingMargin)
65 | .scaleEffect(promptState == .floating ? floatingPromptScale : 1, anchor: .topLeading)
66 | .offset(x: 0, y: promptState == .floating ? -floatingOffset : 0)
67 | }
68 | .padding(.top, topMargin)
69 | .onChange(of: text.wrappedValue) { _ in updateState() }
70 | .onChange(of: isFocused) { _ in updateState() }
71 | .onPreferenceChange(HeightPreferenceKey.self) { height in
72 | promptHeight = height
73 | }
74 | .onTapGesture { isFocused = true }
75 | .accessibilityRepresentation {
76 | TextField(text: text, prompt: nil) {
77 | switch promptState {
78 | case .normal:
79 | prompt
80 | case .floating:
81 | floatingPrompt
82 | }
83 | }
84 | }
85 | }
86 |
87 | func updateState() {
88 | withAnimation {
89 | promptState = (!text.wrappedValue.isEmpty || isFocused) ? .floating : .normal
90 | }
91 | }
92 | }
93 |
94 | private extension FloatingPromptTextField where TextFieldStyle == HierarchicalShapeStyle {
95 | init(text: Binding,
96 | @ViewBuilder prompt: () -> Prompt,
97 | @ViewBuilder floatingPrompt: () -> FloatingPrompt) {
98 | self.init(text: text,
99 | textFieldStyle: .primary,
100 | prompt: prompt,
101 | floatingPrompt: floatingPrompt)
102 | }
103 | }
104 |
105 | private extension FloatingPromptTextField where Prompt == FloatingPrompt {
106 | init(text: Binding,
107 | textFieldStyle: TextFieldStyle,
108 | @ViewBuilder prompt: () -> Prompt) {
109 | self.init(text: text,
110 | textFieldStyle: textFieldStyle,
111 | prompt: prompt,
112 | floatingPrompt: prompt)
113 | }
114 | }
115 |
116 | public extension FloatingPromptTextField where TextFieldStyle == HierarchicalShapeStyle, Prompt == FloatingPrompt {
117 | /// Creates a FloatingPromptTextField with a string binding and a view that will be used
118 | /// as the prompt.
119 | ///
120 | /// - Parameters:
121 | /// - text: A binding to the text to display and edit.
122 | /// - prompt: A view that will be used as a prompt when the text field
123 | /// is empty, and as a floating prompt when it's focused or not empty,
124 | init(text: Binding,
125 | @ViewBuilder prompt: () -> Prompt) {
126 | self.init(text: text,
127 | textFieldStyle: .primary,
128 | prompt: prompt,
129 | floatingPrompt: prompt)
130 | }
131 | }
132 |
133 | public extension FloatingPromptTextField where TextFieldStyle == HierarchicalShapeStyle, Prompt == Text, FloatingPrompt == Text {
134 | /// Creates a FloatingPromptTextField with a string binding and a Text view that will be
135 | /// used as the prompt.
136 | ///
137 | /// - Parameters:
138 | /// - text: A binding to the text to display and edit.
139 | /// - prompt: A Text view that will be used as a prompt when the text field
140 | /// is empty, and as a floating prompt when it's focused or not empty.
141 | init(text: Binding, prompt: Text) {
142 | self.init(text: text,
143 | textFieldStyle: .primary,
144 | prompt: { prompt.foregroundColor(.secondary) },
145 | floatingPrompt: { prompt.foregroundColor(.accentColor) })
146 | }
147 | }
148 |
149 | public extension FloatingPromptTextField {
150 | /// A `View` to be used as the floating prompt when the text field is focused
151 | /// or not empty.
152 | ///
153 | /// - Parameter floatingPrompt: The view that will be used as the floating
154 | /// prompt when the text field is focused or not empty.
155 | func floatingPrompt(_ floatingPrompt: () -> FloatingPromptType) -> FloatingPromptTextField {
156 | FloatingPromptTextField(
157 | text: text,
158 | textFieldStyle: textFieldStyle,
159 | prompt: { prompt },
160 | floatingPrompt: { floatingPrompt() }
161 | )
162 | }
163 |
164 | /// Sets the style for the text field. You can use this to set the color of the
165 | /// text in the text field.
166 | func textFieldForegroundStyle(_ style: Style) -> FloatingPromptTextField {
167 | FloatingPromptTextField(
168 | text: text,
169 | textFieldStyle: style,
170 | prompt: { prompt },
171 | floatingPrompt: { floatingPrompt }
172 | )
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/Sources/FloatingPromptTextField/HeightPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Emilio Peláez on 14/6/21.
3 | //
4 |
5 | import SwiftUI
6 |
7 | struct HeightPreferenceKey: PreferenceKey {
8 | static let defaultValue: CGFloat = 0
9 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
10 | value = nextValue()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/FloatingPromptTextField/ViewExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Emilio Peláez on 9/1/22.
3 | //
4 |
5 | import SwiftUI
6 |
7 | public extension View {
8 | /// Sets the scale at which the prompt will be displayed when floating
9 | /// over the text field.
10 | func floatingPromptScale(_ scale: Double) -> some View {
11 | environment(\.floatingPromptScale, scale)
12 | }
13 |
14 | /// Sets the spacing between the floating prompt and the text field.
15 | func floatingPromptSpacing(_ spacing: Double) -> some View {
16 | environment(\.floatingPromptSpacing, spacing)
17 | }
18 |
19 | /// Sets the leading margin for the prompt in both floating and regular states
20 | func promptLeadingMargin(_ margin: Double) -> some View {
21 | environment(\.promptLeadingMargin, margin)
22 | }
23 |
24 | /// Sets whether or not the view will animate its height to accommodate the
25 | /// floating prompt, or if the height of the floating prompt will
26 | /// always be calculated into the height's view.
27 | func animateFloatingPromptHeight(_ animate: Bool) -> some View {
28 | environment(\.animateFloatingPromptHeight, animate)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------