Skip to content

Commit f68de6d

Browse files
committed
Extend change handler to support will begin/end editing delegate calls.
This now provides even finer grained control over when the text field can become or resign first responder and will override any programatic responder demand. An .animation() modifier returns a new change handler whose change is wrapped in a withAnimation closure.
1 parent d93c451 commit f68de6d

File tree

2 files changed

+75
-10
lines changed

2 files changed

+75
-10
lines changed

Demo Project/ResponsiveTextFieldDemo/ContentView.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ struct ContentView: View {
6464
text: $email,
6565
firstResponderDemand: $emailResponderDemand.animation(),
6666
configuration: .email,
67-
onFirstResponderStateChanged: .init { isFirstResponder in
68-
withAnimation {
69-
isEmailFirstResponder = isFirstResponder
70-
}
71-
},
67+
onFirstResponderStateChanged: FirstResponderStateChangeHandler(
68+
handleStateChange: { isEmailFirstResponder = $0 },
69+
canBecomeFirstResponder: { true },
70+
canResignFirstResponder: { true }
71+
).animation(),
7272
handleReturn: { passwordResponderDemand = .shouldBecomeFirstResponder },
7373
supportedStandardEditActions: [],
7474
standardEditActionHandler: .init(
@@ -90,11 +90,9 @@ struct ContentView: View {
9090
isSecure: hidePassword,
9191
firstResponderDemand: $passwordResponderDemand.animation(),
9292
configuration: .combine(.password, .lastOfChain),
93-
onFirstResponderStateChanged: .init { isFirstResponder in
94-
withAnimation {
95-
isPasswordFirstResponder = isFirstResponder
96-
}
97-
},
93+
onFirstResponderStateChanged: FirstResponderStateChangeHandler { isFirstResponder in
94+
isPasswordFirstResponder = isFirstResponder
95+
}.animation(),
9896
handleReturn: { passwordResponderDemand = .shouldResignFirstResponder },
9997
handleDelete: {
10098
if $0.isEmpty {

Sources/ResponsiveTextField/ResponsiveTextField.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,58 @@ public struct FirstResponderStateChangeHandler {
150150
///
151151
public var handleStateChange: (Bool) -> Void
152152

153+
/// Allows fine-grained control over if the text field should become the first responder.
154+
///
155+
/// This will be called when the text field's `shouldBeginEditing` delegate method is
156+
/// called and provides a final opportunity to prevent the text field becoming first responder.
157+
///
158+
/// If the responder change was triggered programatically by a `FirstResponderDemand`
159+
/// and this returns `false` the demand will still be marked as fulfilled and reset to `nil`.
160+
public var canBecomeFirstResponder: (() -> Bool)?
161+
162+
/// Allows fine-grained control over if the text field should become the first responder.
163+
///
164+
/// This will be called when the text field's `shouldEndEditing` delegate method is
165+
/// called and provides a final opportunity to prevent the text field from resigning first responder.
166+
///
167+
/// If the responder change was triggered programatically by a `FirstResponderDemand`
168+
/// and this returns `false` the demand will still be marked as fulfilled and reset to `nil`.
169+
public var canResignFirstResponder: (() -> Bool)?
170+
171+
/// Initialises a state change handler with a `handleStateChange` callback.
172+
///
173+
/// Most of the time this is the only callback that you will need to provide so this initialiser
174+
/// can be called with trailing closure syntax.
153175
public init(handleStateChange: @escaping (Bool) -> Void) {
154176
self.handleStateChange = handleStateChange
155177
}
156178

179+
public init(
180+
handleStateChange: @escaping (Bool) -> Void,
181+
canBecomeFirstResponder: (() -> Bool)? = nil,
182+
canResignFirstResponder: (() -> Bool)? = nil
183+
) {
184+
self.handleStateChange = handleStateChange
185+
self.canBecomeFirstResponder = canBecomeFirstResponder
186+
self.canResignFirstResponder = canResignFirstResponder
187+
}
188+
157189
func callAsFunction(_ isFirstResponder: Bool) {
158190
handleStateChange(isFirstResponder)
159191
}
192+
193+
/// Returns a new state change handler that wraps the underlying state change handler
194+
/// in a `withAnimation` closure - this is useful if you want state changes triggered by
195+
/// a first responder state change to be explicitly animated.
196+
public func animation() -> Self {
197+
.init(
198+
handleStateChange: { isFirstResponder in
199+
withAnimation { self.handleStateChange(isFirstResponder) }
200+
},
201+
canBecomeFirstResponder: canBecomeFirstResponder,
202+
canResignFirstResponder: canResignFirstResponder
203+
)
204+
}
160205
}
161206

162207
/// Represents a request to change the text field's first responder state.
@@ -238,11 +283,33 @@ extension ResponsiveTextField: UIViewRepresentable {
238283
parent.firstResponderDemandFulfilled()
239284
}
240285

286+
public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
287+
if let canBecomeFirstResponder = parent.onFirstResponderStateChanged?.canBecomeFirstResponder {
288+
let shouldBeginEditing = canBecomeFirstResponder()
289+
if !shouldBeginEditing {
290+
parent.firstResponderDemandFulfilled()
291+
}
292+
return shouldBeginEditing
293+
}
294+
return true
295+
}
296+
241297
public func textFieldDidBeginEditing(_ textField: UITextField) {
242298
parent.onFirstResponderStateChanged?(true)
243299
parent.firstResponderDemandFulfilled()
244300
}
245301

302+
public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
303+
if let canResignFirstResponder = parent.onFirstResponderStateChanged?.canResignFirstResponder {
304+
let shouldEndEditing = canResignFirstResponder()
305+
if !shouldEndEditing {
306+
parent.firstResponderDemandFulfilled()
307+
}
308+
return shouldEndEditing
309+
}
310+
return true
311+
}
312+
246313
public func textFieldDidEndEditing(_ textField: UITextField) {
247314
parent.onFirstResponderStateChanged?(false)
248315
parent.firstResponderDemandFulfilled()

0 commit comments

Comments
 (0)