Skip to content

Commit 0a15057

Browse files
committed
Separate first responder state from first responder demand.
The former now communicates its changes using a simple callback system, whereas the latter is still controlled using a binding. This avoids some potential confusion with having a single enum where only the text field itself can set the the "is or is not" first responder state and only the "become or resign" state can be set externally.
1 parent dd88868 commit 0a15057

File tree

5 files changed

+245
-110
lines changed

5 files changed

+245
-110
lines changed

Demo Project/ResponsiveTextFieldDemo/ContentView.swift

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ struct ContentView: View {
1717
var password: String = ""
1818

1919
@State
20-
var emailResponderState: ResponsiveTextField.FirstResponderState = .shouldBecomeFirstResponder
20+
var emailResponderDemand: FirstResponderDemand? = .shouldBecomeFirstResponder
2121

2222
@State
23-
var passwordResponderState: ResponsiveTextField.FirstResponderState = .notFirstResponder
23+
var emailResponderState: FirstResponderState = .notFirstResponder
24+
25+
@State
26+
var passwordResponderDemand: FirstResponderDemand?
27+
28+
@State
29+
var passwordResponderState: FirstResponderState = .notFirstResponder
2430

2531
@State
2632
var isEnabled: Bool = true
@@ -32,7 +38,7 @@ struct ContentView: View {
3238
Binding(
3339
get: { emailResponderState == .isFirstResponder },
3440
set: {
35-
emailResponderState = $0
41+
emailResponderDemand = $0
3642
? .shouldBecomeFirstResponder
3743
: .shouldResignFirstResponder
3844
}
@@ -43,7 +49,7 @@ struct ContentView: View {
4349
Binding(
4450
get: { passwordResponderState == .isFirstResponder },
4551
set: {
46-
passwordResponderState = $0
52+
passwordResponderDemand = $0
4753
? .shouldBecomeFirstResponder
4854
: .shouldResignFirstResponder
4955
}
@@ -56,9 +62,14 @@ struct ContentView: View {
5662
ResponsiveTextField(
5763
placeholder: "Email address",
5864
text: $email,
59-
firstResponderState: $emailResponderState.animation(),
65+
firstResponderDemand: $emailResponderDemand.animation(),
6066
configuration: .email,
61-
handleReturn: { passwordResponderState = .shouldBecomeFirstResponder }
67+
onFirstResponderStateChanged: { responderState in
68+
withAnimation {
69+
emailResponderState = responderState
70+
}
71+
},
72+
handleReturn: { passwordResponderDemand = .shouldBecomeFirstResponder }
6273
)
6374
.responsiveKeyboardReturnType(.next)
6475
.responsiveTextFieldTextColor(.blue)
@@ -70,13 +81,18 @@ struct ContentView: View {
7081
ResponsiveTextField(
7182
placeholder: "Password",
7283
text: $password,
73-
firstResponderState: $passwordResponderState.animation(),
7484
isSecure: hidePassword,
85+
firstResponderDemand: $passwordResponderDemand.animation(),
7586
configuration: .combine(.password, .lastOfChain),
76-
handleReturn: { passwordResponderState = .shouldResignFirstResponder },
87+
onFirstResponderStateChanged: { responderState in
88+
withAnimation {
89+
passwordResponderState = responderState
90+
}
91+
},
92+
handleReturn: { passwordResponderDemand = .shouldResignFirstResponder },
7793
handleDelete: {
7894
if $0.isEmpty {
79-
emailResponderState = .shouldBecomeFirstResponder
95+
emailResponderDemand = .shouldBecomeFirstResponder
8096
}
8197
}
8298
)

README.md

Lines changed: 112 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,18 @@ project using Xcode's built-in package management tools.
4747
### Getting Started
4848

4949
To use `ResponsiveTextField` you will need to provide it with, at a minimum,
50-
a placeholder string, a `Binding<String>` to capture the text entered into the
51-
text field and a `Binding<Bool>` to manage the text field's first responder
52-
status.
50+
a placeholder string and a `Binding<String>` to capture the text entered into
51+
the text field.
5352

5453
```swift
5554
struct ExampleView: View {
5655
@State var email: String = ""
57-
@State var isEditingEmail: Bool = false
5856

5957
var body: some View {
6058
VStack {
6159
ResponsiveTextField(
6260
placeholder: "Email address",
63-
text: $email,
64-
isEditing: $isEditingEmail
61+
text: $email
6562
)
6663
}
6764
}
@@ -76,15 +73,28 @@ size using the `.fixedSize` modifier:
7673
```swift
7774
ResponsiveTextField(
7875
placeholder: "Email address",
79-
text: $email,
80-
isEditing: $isEditingEmail
76+
text: $email
8177
)
8278
.fixedSize(horizontal: false, vertical: true)
8379
```
8480

8581
As the user types in the field, it will update the state that the binding was
8682
derived from.
8783

84+
You can enable secure text entry by passing in the `isSecure` property:
85+
86+
```swift
87+
ResponsiveTextField(
88+
placeholder: "Email address",
89+
text: $email,
90+
isSecure: true
91+
)
92+
```
93+
94+
The `isSecure` property can be updated when the view is updated so it is
95+
possible to control this via some external state property, i.e. to dynamically
96+
enable or disable secure text entry.
97+
8898
### Disabling the text field
8999

90100
You can disable the text field using the standard SwiftUI `.disabled` modifier:
@@ -93,8 +103,7 @@ You can disable the text field using the standard SwiftUI `.disabled` modifier:
93103

94104
ResponsiveTextField(
95105
placeholder: "Email address",
96-
text: $email,
97-
isEditing: $isEditingEmail
106+
text: $email
98107
)
99108
.disabled(true)
100109
```
@@ -146,7 +155,6 @@ Its important to note that this configuration will be called early during the
146155
ResponsiveTextField(
147156
placeholder: "Email address",
148157
text: $email,
149-
isEditing: $isEditingEmail,
150158
configuration: .init {
151159
$0.autocorrectionType = .no
152160
$0.clearButtonModde = .whileEditing
@@ -179,7 +187,6 @@ You can now use this anywhere within your app in a concise way:
179187
ResponsiveTextField(
180188
placeholder: "Email address",
181189
text: $email,
182-
isEditing: $isEditingEmail,
183190
configuration: .emailField
184191
)
185192
```
@@ -224,38 +231,90 @@ public extension ResponsiveTextField.Configuration {
224231
control over the first responder status of the control. This is one of the
225232
major pieces of missing behaviour from the native `TextField` type.
226233

227-
The control is passed a `Binding<Bool>` on initialisation which allows two-way
228-
communication about the text field's responder state. When the user taps on
229-
the text field, it will become first responder unless it has been disabled.
230-
This will update the state that the binding was derived from to `true`.
231-
Similarly, if another control becomes first responder, the text field will
232-
resign it's first responder status and set the underlying state to `false`.
234+
### Observing the first responder state
235+
236+
When initialised you can pass in a callback function using the parameter
237+
`onFirstResponderStateChanged:` - this takes a closure that will be called
238+
with the updated `FirstResponderState` whenever it changes, either as a result
239+
of some user interaction or as the result of a change in the
240+
`FirstResponderDemand` (see below).
241+
242+
If you need to track this state you can store it in some external state, such as
243+
an `@State` property or an `@ObservableObject` (like your view model):
233244

234-
Update the external state will update the text field and will make it become
235-
or resign first responder. For example, on a screen with two text fields, you
236-
could make the first text field become first responder automatically, causing
237-
the keyboard to appear when the view is shown, by simply setting the default
238-
value of the state to `true`:
245+
```swift
246+
struct ExampleView: View {
247+
@State
248+
var responderState: FirstResponderState = .notFirstResponder
249+
250+
var body: some View {
251+
ResponsiveTextField(
252+
placeholder: "Email address",
253+
text: $email,
254+
configuration: .emailField,
255+
onFirstResponderStateChanged: { responderState = $0 }
256+
)
257+
}
258+
}
259+
```
260+
261+
### Progamatically controlling the first responder state
262+
263+
`ResponsiveTextField` also supports binding-based control over the field's
264+
first responder state. To control the first responder state, you must
265+
initialise the field with a `Binding<FirstResponderDemand?>`:
266+
267+
```swift
268+
struct ExampleView: View {
269+
@State
270+
var responderDemand: FirstResponderDemand?
271+
272+
var body: some View {
273+
ResponsiveTextField(
274+
placeholder: "Email address",
275+
text: $email,
276+
firstResponderDemand: $responderDemand
277+
)
278+
}
279+
}
280+
```
281+
282+
Whenever the binding's wrapped value changes, it will attempt to trigger a
283+
responder state change unless the text field's current responder state already
284+
fulfils the demand. Once the demand has been fulfilled the binding's wrapped
285+
value will be set back to `nil`.
286+
287+
#### Becoming first responder
288+
289+
To make the text field become first responder, set the demand to
290+
`.shouldBecomeFirstResponder`. If the text field is already first responder the
291+
binding's wrapped value will be automatically set back to `nil`, otherwise
292+
`becomeFirstResponder()` will be called and the binding's wrapped value will
293+
be set to `nil` once the first responder state has become `isFirstResponder`.
294+
295+
#### Resigning first responder
296+
297+
To make the text field resign first responder, set the demand to
298+
`.shouldResignFirstResponder`. If the text field is not the first responder the
299+
binding's wrapped value will be automatically set back to `nil`, otherwise
300+
`resignFirstResponder()` will be called and the binding's wrapped value will
301+
be set to `nil` once the first responder state has become `notFirstResponder`.
302+
303+
### Example: Using `@State` to become first responder on view appear
239304

240305
```swift
241306
struct ExampleView: View {
242307
@State var email: String = ""
243308
@State var password: String = ""
244-
@State var isEditingEmail: Bool = true
245-
@State var isEditingPassword: Bool = false
309+
@State var emailFirstResponderDemand: FirstResponderDemand? = .shouldBecomeFirstResponder
246310

247311
var body: some View {
248312
VStack {
249313
/// This field will become first responder automatically
250314
ResponsiveTextField(
251315
placeholder: "Email address",
252316
text: $email,
253-
isEditing: $isEditingEmail
254-
)
255-
ResponsiveTextField(
256-
placeholder: "Password",
257-
text: $password,
258-
isEditing: $isEditingPassword
317+
firstResponderDemand: $emailFirstResponderDemand
259318
)
260319
}
261320
}
@@ -266,16 +325,25 @@ You could also trigger the field to become first responder after a short
266325
delay after appearing:
267326

268327
```swift
269-
VStack {
270-
ResponsiveTextField(
271-
placeholder: "Email address",
272-
text: $email,
273-
isEditing: $isEditingEmail
274-
)
328+
struct ExampleView: View {
329+
@State var email: String = ""
330+
@State var password: String = ""
331+
@State var emailFirstResponderDemand: FirstResponderDemand?
332+
333+
var body: some View {
334+
VStack {
335+
/// This field will become first responder automatically
336+
ResponsiveTextField(
337+
placeholder: "Email address",
338+
text: $email,
339+
firstResponderDemand: $emailFirstResponderDemand
340+
)
341+
}
342+
}
275343
}
276344
.onAppear {
277345
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
278-
isEditingEmail = true
346+
emailFirstResponderDemand = .shouldBecomeFirstResponder
279347
}
280348
}
281349
```
@@ -287,26 +355,26 @@ field to the next when the keyboard return button is tapped:
287355
struct ExampleView: View {
288356
@State var email: String = ""
289357
@State var password: String = ""
290-
@State var isEditingEmail: Bool = true
291-
@State var isEditingPassword: Bool = false
358+
@State var emailFirstResponderDemand: FirstResponderDemand? = .shouldBecomeFirstResponder
359+
@State var passwordFirstResponderDemand: FirstResponderDemand?
292360

293361
var body: some View {
294362
VStack {
295363
/// Tapping return will make the password field first responder
296364
ResponsiveTextField(
297365
placeholder: "Email address",
298366
text: $email,
299-
isEditing: $isEditingEmail,
367+
firstResponderDemand: $emailFirstResponderDemand,
300368
configuration: .emailField,
301-
handleReturn: { isEditingPassword = true }
369+
handleReturn: { passwordFirstResponderDemand = .shouldBecomeFirstResponder }
302370
)
303371
/// Tapping return will resign first responder and hide the keyboard
304372
ResponsiveTextField(
305373
placeholder: "Password",
306374
text: $password,
307-
isEditing: $isEditingPassword,
375+
firstResponderDemand: $passwordFirstResponderDemand,
308376
configuration: .passwordField,
309-
handleReturn: { isEditingPassword = false }
377+
handleReturn: { passwordFirstResponderDemand = .shouldResignFirstResponder }
310378
)
311379
}
312380
}

0 commit comments

Comments
 (0)