├── .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 | [![Platforms](https://img.shields.io/badge/platforms-iOS-lightgray.svg)]() 4 | [![Swift 5.5](https://img.shields.io/badge/swift-5.5-red.svg?style=flat)](https://developer.apple.com/swift) 5 | [![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://opensource.org/licenses/MIT) 6 | [![Twitter](https://img.shields.io/badge/twitter-@emiliopelaez-blue.svg)](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 | Lock Screen 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 | --------------------------------------------------------------------------------