{"id":22112363,"url":"https://github.com/lukeredpath/swift-responsive-textfield","last_synced_at":"2025-10-12T15:31:09.691Z","repository":{"id":47072836,"uuid":"347671191","full_name":"lukeredpath/swift-responsive-textfield","owner":"lukeredpath","description":"A SwiftUI wrapper around UITextField with binding-based state and responder control","archived":false,"fork":false,"pushed_at":"2024-08-02T13:13:02.000Z","size":297,"stargazers_count":85,"open_issues_count":7,"forks_count":16,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-21T21:03:32.528Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lukeredpath.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-14T15:16:08.000Z","updated_at":"2024-12-28T06:15:23.000Z","dependencies_parsed_at":"2024-08-02T14:41:21.613Z","dependency_job_id":"c814eb55-35c3-4ceb-8855-fd0f1f694c8b","html_url":"https://github.com/lukeredpath/swift-responsive-textfield","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeredpath%2Fswift-responsive-textfield","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeredpath%2Fswift-responsive-textfield/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeredpath%2Fswift-responsive-textfield/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeredpath%2Fswift-responsive-textfield/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lukeredpath","download_url":"https://codeload.github.com/lukeredpath/swift-responsive-textfield/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236239242,"owners_count":19117154,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-01T10:57:42.311Z","updated_at":"2025-10-12T15:31:04.360Z","avatar_url":"https://github.com/lukeredpath.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ResponsiveTextField - a better SwiftUI text field\n\n[![CI](https://github.com/lukeredpath/swift-responsive-textfield/actions/workflows/ci.yml/badge.svg)](https://github.com/lukeredpath/swift-responsive-textfield/actions/workflows/ci.yml)\n[![GitHub license](https://img.shields.io/github/license/lukeredpath/swift-responsive-textfield.svg)](https://github.com/lukeredpath/swift-responsive-textfield/blob/master/LICENSE)\n\nThis library aims to achieve one goal, which is provide a reasonably flexible\nand useful SwiftUI wrapper around UITextField that provides more control over\nit's first responder status, one of the most glaring omissions from SwiftUI's\nnative TextField even in iOS 14.\n\n## Features\n\nAt a high level, it provides the ability to:\n\n* Use of SwiftUI bindings to capture entered text and control the text field's\n  first responder status.\n* Observe and react to the text field's first responder status.\n* Set the text field's placeholder.\n* Enable secure text entry.\n* Easily handle return key and delete key taps with a simple callback.\n* Style the text field using SwiftUI-style view modifiers.\n* Support for enabling and disabling the text field using the SwiftUI\n  `.disabled` view modifier.\n* Configure the properties of the underlying text field using a composable\n  text field configuration system.\n* Control over how and when text changes should be permitted.\n* Control over if the text field should begin or end editing.\n* Customise which standard edit actions are available (e.g. copy, paste).\n* Override and customise the handling of standard edit actions.\n\nThe following features are not currently supported:\n\n* Control over how text should be cleared.\n* Managing the text selection.\n* Any of the built-in attributed string supporting APIs.\n\nMost UITextField APIs that are not exposed directly can be managed using the\ntext field configuration system.\n\n## Installation\n\nThe library is made available as a Swift package and can be added to your\nproject using Xcode's built-in package management tools.\n\n## Usage\n\n[API Documentation](https://lukeredpath.github.io/swift-responsive-textfield/)\n\n### Getting Started\n\nTo use `ResponsiveTextField` you will need to provide it with, at a minimum,\na placeholder string and a `Binding\u003cString\u003e` to capture the text entered into\nthe text field.\n\n```swift\nstruct ExampleView: View {\n  @State var email: String = \"\"\n\n  var body: some View {\n    VStack {\n      ResponsiveTextField(\n          placeholder: \"Email address\",\n          text: $email\n      )\n    }\n  }\n}\n```\n\nOut of the box, `ResponsiveTextField` will fill the width of it's container and\nwill not expand if text overflows the available space. It will also try and fill\nthe height of its container - you can fix its height to its intrinsic content\nsize using the `.fixedSize` modifier:\n\n```swift\nResponsiveTextField(\n    placeholder: \"Email address\",\n    text: $email\n)\n.fixedSize(horizontal: false, vertical: true)\n```\n\nAs the user types in the field, it will update the state that the binding was\nderived from.\n\nYou can enable secure text entry by passing in the `isSecure` property:\n\n```swift\nResponsiveTextField(\n    placeholder: \"Email address\",\n    text: $email,\n    isSecure: true\n)\n```\n\nThe `isSecure` property can be updated when the view is updated so it is\npossible to control this via some external state property, i.e. to dynamically\nenable or disable secure text entry.\n\n### Disabling the text field\n\nYou can disable the text field using the standard SwiftUI `.disabled` modifier:\n\n```swift\n\nResponsiveTextField(\n    placeholder: \"Email address\",\n    text: $email\n)\n.disabled(true)\n```\n\nThis uses the SwiftUI Environment system so it does not need to be called\ndirectly on the `ResponsiveTextField` element itself - you can also attach it\nto any parent view.\n\nThe disabled state can be updated and the text field will update it's state\naccordingly. This means you can use an `@State` variable to control the disabled\nstate. No binding is required for this.\n\nDisabling the text field will make it ignore any taps and will also resign the\nfirst responder status if the user was editing when it is disabled.\n\n### Customising the text field appearance\n\nYou can control the appearance of the text field, including it's font, text\ncolor, text alignment and return key type using custom view modifiers. These\nmodifiers also use the Environment system so can be called on any container\nview as well as the text field itself. Note - these modifiers take UIKit values\nfor fonts and colors, not SwiftUI values:\n\n```swift\n/// Sets the return key type\ntextField.responsiveKeyboardReturnType(.next)\n\n/// Sets the text color\ntextField.responsiveTextFieldTextColor(.red)\n\n/// Sets the font\ntextField.responsiveTextFieldTextColor(.preferredFont(forTextStyle: .headline))\n\n/// Sets the text alignment\ntextField.responsiveTextFieldTextAlignment(.center)\n```\n\n### Advanced re-usable configuration\n\nFor more detailed configuration, you can pass a\n`ResponsiveTextField.Configuration` value to the initialiser. This is a value\ntype that takes a single argument, a closure of `(UITextField) -\u003e Void`. This\nallows you to have full control over the properties of the `UITextField`.\n\nIts important to note that this configuration will be called early during the\n`makeUIView()` function meaning that certain properties will be overwritten.\n\n```swift\nResponsiveTextField(\n    placeholder: \"Email address\",\n    text: $email,\n    configuration: .init {\n      $0.autocorrectionType = .no\n      $0.clearButtonModde = .whileEditing\n    }\n)\n```\n\nThe real power of this type is the ability to create pre-defined configurations\nthat you can re-use throughout your app. You can define these as static values\nin an extension on `ResponsiveTextField.Configuration`.\n\nFor example, we may define an `email` configuration that sets the keyboard\ntype and disables autocorrection:\n\n```swift\npublic extension ResponsiveTextField.Configuration {\n  static let emailField = Self {\n        $0.keyboardType = .emailAddress\n        $0.autocorrectionType = .no\n        $0.autocapitalizationType = .none\n        $0.spellCheckingType = .no\n        $0.clearButtonMode = .whileEditing\n    }\n}\n```\n\nYou can now use this anywhere within your app in a concise way:\n\n```swift\nResponsiveTextField(\n    placeholder: \"Email address\",\n    text: $email,\n    configuration: .emailField\n)\n```\n\nThe real power is being able to create small focused configurations that do just\none thing, then combining them to create higher-level configurations. For\nexample, we could refactor the previous configuration into smaller ones and\nthen combine them in different ways:\n\n```swift\npublic extension ResponsiveTextField.Configuration {\n  static let noCorrection = Self {\n    $0.autocorrectionType = .no\n    $0.autocapitalizationType = .none\n    $0.spellCheckingType = .no\n  }\n\n  static func keyboardType(_ type: UIKeyboardType) -\u003e Self {\n    $0.keyboardType = type\n  }\n\n  static let clearWhileEditing = Self {\n    $0.clearButtonMode = .whileEditing\n  }\n\n  static let emailField = .combine(\n    .keyboardtype(.emailAddress),\n    .noCorrection,\n    .clearWhileEditing\n  )\n\n  static let passwordField = .combine(\n    .noCorrection,\n    .clearWhileEditing\n  )\n}\n```\n\n## First Responder Control\n\n`ResponsiveTextField` uses the SwiftUI binding system to give programmatic\ncontrol over the first responder status of the control. This is one of the\nmajor pieces of missing behaviour from the native `TextField` type.\n\n### Observing the first responder state\n\nWhen initialised you can pass in a callback function using the parameter\n`onFirstResponderStateChanged:` - this takes a value of type\n`FirstResponderStateChangeHandler`, which wraps a closure that will be called\nwith the updated first responder state whenever it changes, either as a result\nof some user interaction or as the result of a change in the\n`FirstResponderDemand` (see below).\n\nThe first responder state is represented as  a single `Bool` value where `true`\nindicates that the text field has become  first responder and `false` indicates\nthat it has resigned first responder.\n\n```swift\nstruct ExampleView: View {\n  var body: some View {\n    ResponsiveTextField(\n        placeholder: \"Email address\",\n        text: $email,\n        configuration: .emailField,\n        onFirstResponderStateChanged: .init { isFirstResponder in\n          // do something with first responder state\n        }\n    )\n  }\n}\n```\n\nIf you need to track this state you can store it in some external state, such as\nan `@State` property or an `@ObservableObject` (like your view model):\n\n```swift\nstruct ExampleView: View {\n  @State\n  var isFirstResponder = false\n\n  var body: some View {\n    ResponsiveTextField(\n        placeholder: \"Email address\",\n        text: $email,\n        configuration: .emailField,\n        onFirstResponderStateChanged: .init {\n          isFirstResponder = $0\n        }\n    )\n  }\n}\n```\n\nIf all you need to do is update some external state, you can use the built-in\n`.updates` state changed handler, passing in a binding to that state. The above\nexample can be simplified to:\n\n```swift\nstruct ExampleView: View {\n  @State\n  var isFirstResponder = false\n\n  var body: some View {\n    ResponsiveTextField(\n        placeholder: \"Email address\",\n        text: $email,\n        configuration: .emailField,\n        onFirstResponderStateChanged: .updates($isFirstResponder)\n    )\n  }\n}\n```\n\n`FirstResponderStateChangeHandler` can also be initialised with a\n`canBecomeFirstResponder` and `canResignFirstResponder` closures that both\nreturn a `Bool` - if provided, these will be called in the text field's\n`shouldBeginEditing` and `shouldEndEditing` delegate calls and provide flexible\ncontrol over if the text field's responder state should change. If these\nclosures are not provided these delegate methods will return `true`.\n\n### Progamatically controlling the first responder state\n\n`ResponsiveTextField` also supports binding-based control over the field's\nfirst responder state. To control the first responder state, you must\ninitialise the field with a `Binding\u003cFirstResponderDemand?\u003e`:\n\n```swift\nstruct ExampleView: View {\n  @State\n  var responderDemand: FirstResponderDemand?\n\n  var body: some View {\n    ResponsiveTextField(\n        placeholder: \"Email address\",\n        text: $email,\n        firstResponderDemand: $responderDemand\n    )\n  }\n}\n```\n\nWhenever the binding's wrapped value changes, it will attempt to trigger  a\nresponder state change unless the text field's current responder state already\nfulfils the demand. Once the demand has been fulfilled the binding's wrapped\nvalue will be set back to `nil`.\n\n#### Becoming first responder\n\nTo make the text field become first responder, set the demand to\n`.shouldBecomeFirstResponder`. If the text field is already first responder the\nbinding's wrapped value will be automatically set back to `nil`, otherwise\n`becomeFirstResponder()` will be called and the binding's wrapped value will\nbe set to `nil` once the first responder state has become `isFirstResponder`.\n\n#### Resigning first responder\n\nTo make the text field resign first responder, set the demand to\n`.shouldResignFirstResponder`. If the text field is not the first responder the\nbinding's wrapped value will be automatically set back to `nil`, otherwise\n`resignFirstResponder()` will be called and the binding's wrapped value will\nbe set to `nil` once the first responder state has become `notFirstResponder`.\n\n### Avoiding nested view updates\n\nWhen using a `firstResponderStateChangeHandler` to update some state that\ntriggers a view update in combination with state-driven first responder changes, it \nis possible to end up in a situation where you are triggering a view update in the \nmiddle of existing view update cycle which will result in a runtime warning about\nundefined behaviour.\n\nThis can occur because state-driven first responder changes cause the text field \nto become first responder as part of a view update - this means that the change \nhandler itself will be called during that view update so if it was to trigger another\nview update when called, it would happen within the current view update. \n\nIn the following example, a warning would occur because the change to the \n`@State` variable results in a nested view update:\n\n```swift\nstruct ExampleView: View {\n  @State\n  var someString: String\n  \n  @State\n  var firstText: String\n  \n  @State\n  var secondText: String\n  \n  @State\n  var secondResponderDemand: FirstResponderDemand\n\n  var body: some View {\n    Text(\"The text is: \\(someString)\")\n    ResponsiveTextField(\n        placeholder: \"First\",\n        text: $firstText,\n        handleReturn: {\n            // make the second field become first responder\n            secondResponderDemand = .shouldBecomeFirstResponder\n        }\n    )\n    ResponsiveTextField(\n        placeholder: \"Second\",\n        text: $secondText,\n        firstResponderDemand: \n        onFirstResponderStateChanged: .init { _ in\n          // This will be called during the view update triggered\n          // by mutating `shouldBecomeFirstResponder` in the first\n          // field's `handleReturn` closure.\n          // This will trigger a nested state change!\n          someString = \"Hello World\"\n        }\n    )\n  }\n}\n```\n\nTo workaround this problem, rather than the library explicitly calling the state change\nhandler on the next runloop tick or on an asynchronous `DispatchQueue`, which\nmight not be necessary if there is no nested state change, you can avoid the \nproblem by ensuring that the view update your state change handler triggers \nalways happens after the view update completes.\n\nA convenience modifier on `FirstResponderStateChangeHandler`, `receive(on:)`\nallows you to do this by passing in a scheduler such as a runloop or dispatch queue.\nThe above example can be fixed with the following change to the second text field:\n\n```swift\nResponsiveTextField(\n    placeholder: \"Second\",\n    text: $secondText,\n    firstResponderDemand: \n    onFirstResponderStateChanged: .init { _ in\n      // This will now be triggered on the next runloop tick and\n      // will not trigger a nested state change warning.\n      someString = \"Hello World\"\n    }.receive(on: RunLoop.main)\n)\n```\n\n### Example: Using `@State` to become first responder on view appear\n\n```swift\nstruct ExampleView: View {\n  @State var email: String = \"\"\n  @State var password: String = \"\"\n  @State var emailFirstResponderDemand: FirstResponderDemand? = .shouldBecomeFirstResponder\n\n  var body: some View {\n    VStack {\n      /// This field will become first responder automatically\n      ResponsiveTextField(\n          placeholder: \"Email address\",\n          text: $email,\n          firstResponderDemand: $emailFirstResponderDemand\n      )\n    }\n  }\n}\n```\n\nYou could also trigger the field to become first responder after a short\ndelay after appearing:\n\n```swift\nstruct ExampleView: View {\n  @State var email: String = \"\"\n  @State var password: String = \"\"\n  @State var emailFirstResponderDemand: FirstResponderDemand?\n\n  var body: some View {\n    VStack {\n      /// This field will become first responder automatically\n      ResponsiveTextField(\n          placeholder: \"Email address\",\n          text: $email,\n          firstResponderDemand: $emailFirstResponderDemand\n      )\n    }\n  }\n}\n.onAppear {\n  DispatchQueue.main.asyncAfter(deadline: .now() + 1) {\n    emailFirstResponderDemand = .shouldBecomeFirstResponder\n  }\n}\n```\n\nYou could also use the built-in keyboard handling closure to move from one\nfield to the next when the keyboard return button is tapped:\n\n```swift\nstruct ExampleView: View {\n  @State var email: String = \"\"\n  @State var password: String = \"\"\n  @State var emailFirstResponderDemand: FirstResponderDemand? = .shouldBecomeFirstResponder\n  @State var passwordFirstResponderDemand: FirstResponderDemand?\n\n  var body: some View {\n    VStack {\n      /// Tapping return will make the password field first responder\n      ResponsiveTextField(\n          placeholder: \"Email address\",\n          text: $email,\n          firstResponderDemand: $emailFirstResponderDemand,\n          configuration: .emailField,\n          handleReturn: { passwordFirstResponderDemand = .shouldBecomeFirstResponder }\n      )\n      /// Tapping return will resign first responder and hide the keyboard\n      ResponsiveTextField(\n          placeholder: \"Password\",\n          text: $password,\n          firstResponderDemand: $passwordFirstResponderDemand,\n          configuration: .passwordField,\n          handleReturn: { passwordFirstResponderDemand = .shouldResignFirstResponder }\n      )\n    }\n  }\n}\n```\n\nWhen using programatic responder state demands and the `canBecomeFirstResponder`\nand `canResignFirstResponder` closures on `FirstResponderStateChangeHandler`,\nits important to note that the latter will take priority. If either of these\nclosures return `false`, the demand will be ignored and marked as fulfilled,\nresetting it back to `nil`.\n\n## Licence\n\nThis library is released under the Apache v2.0 licence. See [LICENCE](LICENCE)\nfor details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukeredpath%2Fswift-responsive-textfield","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukeredpath%2Fswift-responsive-textfield","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukeredpath%2Fswift-responsive-textfield/lists"}