Skip to content

Commit 5cf7d40

Browse files
committed
Refactor how first responder state is managed.
Trying to overload a single binding to represent 4 potential states was leading to some confusion.
1 parent 76aff30 commit 5cf7d40

File tree

3 files changed

+62
-32
lines changed

3 files changed

+62
-32
lines changed

Demo Project/ResponsiveTextFieldDemo/ContentView.swift

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

1919
@State
20-
var isEditingEmail: Bool = true
20+
var emailResponderState: ResponsiveTextField.FirstResponderState = .become
2121

2222
@State
23-
var isEditingPassword: Bool = false
23+
var passwordResponderState: ResponsiveTextField.FirstResponderState = .resigned
2424

2525
@State
2626
var isEnabled: Bool = true
2727

2828
@State
2929
var hidePassword: Bool = true
3030

31+
var isEditingEmail: Binding<Bool> {
32+
Binding(
33+
get: { emailResponderState == .current },
34+
set: { emailResponderState = $0 ? .become : .resign }
35+
)
36+
}
37+
38+
var isEditingPassword: Binding<Bool> {
39+
Binding(
40+
get: { passwordResponderState == .current },
41+
set: { passwordResponderState = $0 ? .become : .resign }
42+
)
43+
}
44+
3145
var body: some View {
3246
NavigationView {
3347
VStack {
3448
ResponsiveTextField(
3549
placeholder: "Email address",
3650
text: $email,
37-
isEditing: $isEditingEmail.animation(),
51+
firstResponderState: $emailResponderState.animation(),
3852
configuration: .email,
39-
handleReturn: { isEditingPassword = true }
53+
handleReturn: { passwordResponderState = .become }
4054
)
4155
.responsiveKeyboardReturnType(.next)
4256
.responsiveTextFieldTextColor(.blue)
@@ -48,11 +62,15 @@ struct ContentView: View {
4862
ResponsiveTextField(
4963
placeholder: "Password",
5064
text: $password,
51-
isEditing: $isEditingPassword.animation(),
65+
firstResponderState: $passwordResponderState.animation(),
5266
isSecure: hidePassword,
5367
configuration: .combine(.password, .lastOfChain),
54-
handleReturn: { isEditingPassword = false },
55-
handleDelete: { isEditingEmail = $0.isEmpty }
68+
handleReturn: { passwordResponderState = .resign },
69+
handleDelete: {
70+
if $0.isEmpty {
71+
emailResponderState = .become
72+
}
73+
}
5674
)
5775
.fixedSize(horizontal: false, vertical: true)
5876
.disabled(!isEnabled)
@@ -66,10 +84,10 @@ struct ContentView: View {
6684
}
6785
.padding(.bottom)
6886

69-
Toggle("Editing Email?", isOn: $isEditingEmail)
87+
Toggle("Editing Email?", isOn: isEditingEmail)
7088
.padding(.bottom)
7189

72-
Toggle("Editing Password?", isOn: $isEditingPassword)
90+
Toggle("Editing Password?", isOn: isEditingPassword)
7391
.padding(.bottom)
7492

7593
Toggle("Hide Password?", isOn: $hidePassword)

Sources/ResponsiveTextField/ResponsiveTextField.swift

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ public struct ResponsiveTextField {
1616
/// A binding to the text state that will hold the typed text
1717
let text: Binding<String>
1818

19-
/// A binding to the editing state of the text field.
19+
/// A binding to control the first responder state of the text field.
2020
///
21-
/// This will synchronise with the textfield's first responder state - it will get updated
22-
/// if the user taps on the textfield (making it first responder) or if the text field resigns
23-
/// first responder status. It can also allow the containing view to manually control
24-
/// the first responder state by setting it to true or false.
25-
let isEditing: Binding<Bool>
21+
/// If the text field becomes or resigns first responder as a result of a user interaction,
22+
/// this will be updated to `.current` or `.resigned` when the text field indicates
23+
/// that it has started or finished editing.
24+
///
25+
/// You can programatically set this to a value of `.become` to become first responder
26+
/// or `.resign` to resign first responder. Programatically setting it to any other value
27+
/// will not have any effect on the first responder state (it can only become `.current`
28+
/// or `.resigned` when the system indicates that its responder state has changed).
29+
let firstResponderState: Binding<FirstResponderState>
2630

2731
/// Enables secure text entry.
2832
///
@@ -80,7 +84,8 @@ public struct ResponsiveTextField {
8084
public init(
8185
placeholder: String,
8286
text: Binding<String>,
83-
isEditing: Binding<Bool>,
87+
//isEditing: Binding<Bool>,
88+
firstResponderState: Binding<FirstResponderState>,
8489
isSecure: Bool = false,
8590
configuration: Configuration = .empty,
8691
handleReturn: (() -> Void)? = nil,
@@ -89,7 +94,7 @@ public struct ResponsiveTextField {
8994
) {
9095
self.placeholder = placeholder
9196
self.text = text
92-
self.isEditing = isEditing
97+
self.firstResponderState = firstResponderState
9398
self.isSecure = isSecure
9499
self.configuration = configuration
95100
self.handleReturn = handleReturn
@@ -102,6 +107,13 @@ public struct ResponsiveTextField {
102107
callback()
103108
shouldUpdateView = true
104109
}
110+
111+
public enum FirstResponderState: Equatable {
112+
case resigned
113+
case become
114+
case current
115+
case resign
116+
}
105117
}
106118

107119
// MARK: - UIViewRepresentable
@@ -141,10 +153,10 @@ extension ResponsiveTextField: UIViewRepresentable {
141153
uiView.returnKeyType = returnKeyType
142154
uiView.font = font
143155

144-
switch (uiView.isFirstResponder, isEditing.wrappedValue) {
145-
case (true, false):
156+
switch (uiView.isFirstResponder, firstResponderState.wrappedValue) {
157+
case (true, .resign):
146158
uiView.resignFirstResponder()
147-
case (false, true):
159+
case (false, .become):
148160
uiView.becomeFirstResponder()
149161
default:
150162
break
@@ -158,20 +170,20 @@ extension ResponsiveTextField: UIViewRepresentable {
158170
var text: String
159171

160172
@Binding
161-
var isEditing: Bool
173+
var firstResponderState: FirstResponderState
162174

163175
init(textField: ResponsiveTextField) {
164176
self.parent = textField
165177
self._text = textField.text
166-
self._isEditing = textField.isEditing
178+
self._firstResponderState = textField.firstResponderState
167179
}
168180

169181
public func textFieldDidBeginEditing(_ textField: UITextField) {
170-
parent.skippingViewUpdates { self.isEditing = true }
182+
parent.skippingViewUpdates { self.firstResponderState = .current }
171183
}
172184

173185
public func textFieldDidEndEditing(_ textField: UITextField) {
174-
parent.skippingViewUpdates { self.isEditing = false }
186+
parent.skippingViewUpdates { self.firstResponderState = .resigned }
175187
}
176188

177189
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
@@ -309,13 +321,13 @@ struct ResponsiveTextField_Previews: PreviewProvider {
309321
var text: String = ""
310322

311323
@State
312-
var isEditing: Bool = false
324+
var firstResponderState: ResponsiveTextField.FirstResponderState = .resigned
313325

314326
var body: some View {
315327
ResponsiveTextField(
316328
placeholder: "Placeholder",
317329
text: $text,
318-
isEditing: $isEditing,
330+
firstResponderState: $firstResponderState,
319331
configuration: configuration,
320332
shouldChange: { $1.count <= 10 }
321333
)

Tests/ResponsiveTextFieldTests/ResponsiveTextFieldTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ final class ResponsiveTextFieldTests: XCTestCase {
1515
matching: ResponsiveTextField(
1616
placeholder: "Placeholder Text",
1717
text: .constant(""),
18-
isEditing: .constant(true),
18+
firstResponderState: .constant(.resigned),
1919
isSecure: false,
2020
configuration: .empty
2121
).padding(),
@@ -28,7 +28,7 @@ final class ResponsiveTextFieldTests: XCTestCase {
2828
matching: ResponsiveTextField(
2929
placeholder: "Placeholder Text",
3030
text: .constant("Textfield with some text"),
31-
isEditing: .constant(true),
31+
firstResponderState: .constant(.resigned),
3232
isSecure: false,
3333
configuration: .empty
3434
).padding(),
@@ -41,7 +41,7 @@ final class ResponsiveTextFieldTests: XCTestCase {
4141
matching: ResponsiveTextField(
4242
placeholder: "Placeholder Text",
4343
text: .constant("ssh this is top secret"),
44-
isEditing: .constant(true),
44+
firstResponderState: .constant(.resigned),
4545
isSecure: true,
4646
configuration: .empty
4747
).padding(),
@@ -54,7 +54,7 @@ final class ResponsiveTextFieldTests: XCTestCase {
5454
matching: ResponsiveTextField(
5555
placeholder: "Placeholder Text",
5656
text: .constant("Textfield with some text"),
57-
isEditing: .constant(true),
57+
firstResponderState: .constant(.resigned),
5858
isSecure: false,
5959
configuration: .empty
6060
)
@@ -70,7 +70,7 @@ final class ResponsiveTextFieldTests: XCTestCase {
7070
matching: ResponsiveTextField(
7171
placeholder: "Placeholder Text",
7272
text: .constant("Textfield with some text"),
73-
isEditing: .constant(true),
73+
firstResponderState: .constant(.resigned),
7474
isSecure: false,
7575
configuration: .empty
7676
)
@@ -83,7 +83,7 @@ final class ResponsiveTextFieldTests: XCTestCase {
8383
matching: ResponsiveTextField(
8484
placeholder: "Placeholder Text",
8585
text: .constant("Textfield with some text"),
86-
isEditing: .constant(true),
86+
firstResponderState: .constant(.resigned),
8787
isSecure: false,
8888
configuration: .empty
8989
)

0 commit comments

Comments
 (0)