I have a sub class of UITextField and I want the sub class to control which input values are valid. That I have solved by overriding the shouldChangeText(in:, replacementText:) -> Bool method in the sub class. But this is only called when using the on-screen keyboard. If I use a hardware keyboard it doesn't get called.
The textField(_ textField:, shouldChangeCharactersIn:, replacementString:) -> Bool is called on the UITextFieldDelegate, when using a hardware keyboard. But I do not want to assign the delegate to the text field itself, since I need the delegate in some view controllers for other purposes. So I need an alternative method to validate the input values, like the shouldChangeText(in:, replacementText:) -> Bool gives me for the on-screen keyboard.
I can see on the stack trace from the delegate method, that the system has called a [UITextField keyboardInput:shouldInsertText:isMarkedText:], but I can't override that.
Is there any way to solve this, without assigning the delegate?
Here's an alternate solution that allows both your "real" text field delegate and your custom text field class to implement one more delegate methods.
The code below allows this custom text field to implement any of the needed UITextField delegate methods while still allowing the real delegate to implement any that it needs. The only requirement is that for any delegate method implemented inside the custom text field, you must check to see if the real delegate also implements it and call it as needed. Any delegate methods implemented in the real delegate class should be written normally.
This simple custom text field example is setup to only allow numbers. But it leaves the real delegate of the text field to do other validations such as only allowing a certain length of numbers or a specific range or whatever.
import UIKit
class MyTextField: UITextField, UITextFieldDelegate {
private var realDelegate: UITextFieldDelegate?
// Keep track of the text field's real delegate
override var delegate: UITextFieldDelegate? {
get {
return realDelegate
}
set {
realDelegate = newValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
// Make the text field its own delegate
super.delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Make the text field its own delegate
super.delegate = self
}
// This is one third of the magic
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if let realDelegate = realDelegate, realDelegate.responds(to: aSelector) {
return realDelegate
} else {
return super.forwardingTarget(for: aSelector)
}
}
// This is another third of the magic
override func responds(to aSelector: Selector!) -> Bool {
if let realDelegate = realDelegate, realDelegate.responds(to: aSelector) {
return true
} else {
return super.responds(to: aSelector)
}
}
// And the last third
// This only allows numbers to be typed into the text field.
// Of course this can be changed to do whatever validation you need in this custom text field
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) != nil {
return false // Not a number - fail
} else {
// The string is valid, now let the real delegate decide
if let delegate = realDelegate, delegate.responds(to: #selector(textField(_:shouldChangeCharactersIn:replacementString:))) {
return delegate.textField!(textField, shouldChangeCharactersIn: range, replacementString: string)
} else {
return true
}
}
}
}
The only other complication I leave as an exercise is a case where the custom text field wants to modify the changing text and then allow the real delegate to determine if that modified string/range should be allowed.
Related
I am writing a custom UITextField right now in Swift, and encountered the following:
class MyField: UITextField {
open override var text: String? {
didSet {
// some logic that normalizes the text
}
}
private func setup() { //called on init
addTarget(self, action: #selector(textEditingChanged(_:)), for: .editingChanged)
}
#objc func textEditingChanged(_ sender: UITextField) {
}
}
Now, when testing this, I observed that, when the user is typing something, textEditingChanged is called, but text.didSet is not. Neither is text.willSet, for that matter. However, in textEditingChanged, the textfield's text will already be updated to the new value.
Can anyone explain what is going on here? Is Swift circumventing the setter on purpose? How am I supposed to know when/if the setter is called, is there any logic to this in UIKit?
The text property of the UITextField is only for external use, when you are typing UIKit handles the update internally. Can't really tell whats going on under the hood, as we do not have access to UIKit implementation but one possible answer to the question is that UITextField is using an other internal variable for storing the text property. When you are getting the text property, you are actually getting that internal value, and when you are setting it, you are setting that internal value also. Of course under the hood it is a bit more complicated but it may look something like this(SampleUITextField represents the UITextField):
// This how the UITextField implementation may look like
class SampleUITextField {
private var internalText: String = ""
var text: String {
get {
internalText
} set {
internalText = newValue
}
}
// This method is just to demonstrate that when you update the internalText didSet is not called
func updateInternalTextWith(text: String) {
internalText = text
}
}
And when you subclass it it looks like:
class YourTextField: SampleUITextField {
override var text: String {
didSet {
print("DidSet called")
}
}
}
Now when you set the text property directly the didSet is called because the text value updates. You can check it:
let someClass = YourTextField()
someClass.text = "Some new text"
// didSet is called
But now when you call updateInternalTextWith the didSet is not called:
let someClass = YourTextField()
someClass.updateInternalTextWith(text: "new text")
// didSet is not called
That's because you are not updating the text value directly, but just the internal text property. A similar method is called to update the internal text variable of the UITextField when you are typing, and that's the reason the didSet is not called in your case.
For that reason, it is not enough to override the text variable when we want to be notified when the text properties changes, and we need to use delegate(UITextFieldDelegate) methods or notifications (textFieldDidChange) to catch the actual change.
What kind of formatting are you trying to perform on the UITextField?
Shouldn't it be the responsibility of a ViewModel (or any model managing the business logic for the view containing this text field) instead?
Also, you shouldn't use didSet to perform changes, its mainly here to allow you to respond to changes, not chain another change.
I have created a "utility class" whose sole purpose is to be a UITextFieldDelegate.
Basically, it is supposed to enable a UIAlertAction only when there is text in a textfield, otherwise the action is disabled.
class ActionDisabler: NSObject, UITextFieldDelegate {
let action: UIAlertAction
init(action: UIAlertAction, textField: UITextField) {
self.action = action
super.init()
textField.delegate = self
// Initialize it as enabled or disabled
if let text = textField.text {
action.isEnabled = !text.isEmpty
} else {
action.isEnabled = false
}
}
deinit {
print("Too early?")
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text {
action.isEnabled = text.utf8.count - string.utf8.count > 0
} else {
action.isEnabled = false
}
return true
}
}
As you can see, it takes an alert action and a textfield in its constructor and makes itself the delegate of the text field. Simple, right?
Well, this is how delegate of UITextField is defined:
weak open var delegate: UITextFieldDelegate?
Due to this, ActionDisabler is de-initialized after the closure (a configuration handler for a textfield being added to a UIAlertController it is defined in is no longer in scope.
alert.addTextField(configurationHandler: { textField in
_ = ActionDisabler(action: join, textField: textField)
})
Subclassing UIAlertController and having each alert controller be the delegate of its own textfield is not an option, as explained by this answer.
Is there any way to make ActionDisabler not be de-initialized until the textfield that it is a delegate of is no longer alive?
The by far best solution is to keep a reference to your ActionDisabler where the reference to your UITextField is kept, so both will get deallocated at the same time.
If that is not possible or would make your code ugly, you could subclass UITextField to hold the delegate twice. Once with a strong reference:
class UIStrongTextField: UITextField {
// The delegate will get dealocated when the text field itself gets deallocated
var strongDelegate: UITextFieldDelegate? {
didSet {
self.delegate = strongDelegate
}
}
}
Now you set textField.strongDelegate = self and ActionDisabler will get deallocated together with the text field.
If you can't hold a reference and you can't subclass UITextField. We have to get creative. The ActionDisabler can hold a reference to itself, and set that reference to nil, to release itself. (aka manual memory management)
class ActionDisabler: NSObject, UITextFieldDelegate {
let action: UIAlertAction
var deallocPreventionAnchor: ActionDisabler?
init(action: UIAlertAction, textField: UITextField) {
self.deallocPreventionAnchor = self
self.action = action
When you don't need the ActionDisabler anymore you set self.deallocPreventionAnchor = nil and it will get deallocated. If you forget to set it to nil, you will leak the memory, because this is indeed manual memory management by exploiting the reference counter. And I can already see people screaming in the comments at me because that is kinda hacky :) (This also only makes sense when the deallocation will be triggered by a delegate function, otherwise you would have to keep a reference somewhere anyway)
I'm trying to modify the text rendered in the UITextField based on certain events such as Touch Down or Touch Down Repeat. I want the UITextField to be responsive only to events but prevent users from modifying the actual value in the UITextField.
I have tried unchecking the Enabled state of the UITextField but that causes it to not respond to touch events either.
How do I prevent users from changing the contents of the UITextField without affecting the response to touch events, specifically using Swift 3?
So, I was finally able to get this working in the expected manner. What I did was -
1 - Extend the UITextFieldDelegate in the ViewController class
class ViewController: UIViewController, UITextFieldDelegate {
2 - Inside the function viewDidLoad(), assign the textfield delegate to the controller
override func viewDidLoad() {
super.viewDidLoad()
textfield.delegate = self
}
(you will have assign each UITextField's delegate that you want to be prevented from editing)
3 - Lastly, implement your custom behavior in the textFieldShouldBeginEditing function
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return false
}
For reference, check out this API Reference on Apple Developer
Override textFieldShouldBeginEditing , and return false.
func textFieldShouldBeginEditing(state: UITextField) -> Bool {
return false
}
you can also disable for specific text field.
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
if(textField == self.myTextField){
return false
}
return true;
}
I'm working on Android TV Remote Controller - iOS version
I need to detect cursor change event in UITextField and send this event to Android TV.
I can't find any Delegate or notification will send UITextfield cursor change event.
Is there any way to get this event?
Many thanks.
As far as I know, you can KVO or subclass.
Since #NSLeader gave the answer for KVO, I'll explain the latter.
Here is a subclass example:
class MyUITextFieldThatEmitsCursorChangeEvents: UITextField
{
//Override this, but don't prevent change to its default behavior by
//calling the super getter and setter.
override var selectedTextRange: UITextRange? {
get {return super.selectedTextRange}
set {
self.emitNewlySetCursor(event: newValue) //<- Intercept the value
super.selectedTextRange = newValue //Maintain normal behavior
}
}
//I'm going to use a closure to pass the cursor position out,
//but you can use a protocol, NotificationCenter, or whatever floats your
//boat.
weak var cursorPosnDidChangeEvent: ((Int) -> ())?
//I'm going to abstract the logic here to keep the previous code slim.
private func emitNewlySetCursor(event range: UITextRange?)
{
//Now you have access to the start and end in range.
//If .start and .end are different, then it means text is highlighted.
//If you only care about the position where text is about to be
//entered, then only worry about .start.
//This is an example to calculate the cursor position.
if let rawRangeComponent = range?.start
{
let cursorPosition = offset(from: beginningOfDocument,
to: rawRangeComponent)
//Emit the value to whoever cares about it
self.cursorPosnDidChangeEvent?(cursorPosition)
}
}
}
Then, for example, if we're in a UIViewController:
override func viewDidLoad()
{
super.viewDidLoad()
let tf = MyUITextFieldThatEmitsCursorChangeEvents(frame: .zero)
self.view.addSubview(tf)
tf.cursorPosnDidChangeEvent = { newCursorPosn in
print(newCursorPosn) //( ͡° ͜ʖ ͡°)
}
}
Observe selectedTextRange property.
Example on RxSwift:
textField.rx.observeWeakly(UITextRange.self, "selectedTextRange")
.observeOn(MainScheduler.asyncInstance)
.skip(1)
.bind { (newTextRange) in
print(newTextRange)
}
.disposed(by: disposeBag)
In the following code I am trying to transfer control from UITextField to the next via a next button.
I am doing this by calling becomeFirstResponder on the next UITextField.
If I don't type anything in the first and current UITextField the next button works as expected. The keyboard stays up and the focus is transferred.
If I do type something, and only if the field is empty. The method becomeFirstResponder for the next field is called and returns true, yet the keyboard is dismissed and focus is not transferred.
public func numberPad(numberPad: APNumberPad, functionButtonAction:UIButton, textInput: UIResponder) {
var current:UITextField?
for field in editCells {
if (current != nil) {
field.valueTextField.becomeFirstResponder()
return;
}
if (field.valueTextField == activeField) {
current = field.valueTextField
}
}
textInput.resignFirstResponder()
}
This function is called when the NEXT or DONE button is pressed on the keyboard. Which is a custom number keypad. APNumberPad specifically.
https://github.com/podkovyrin/APNumberPad
It is my delegate function.
Anyone know any reason becomeFirstResponder would return true and not work, only in some cases, but work in others?
And yes this is the main UI thread. Adding a call to resignFirstResponder on the current field, then a delay and calling becomeFirstResponder works. This causes the keypad to flicker, no matter how small the delay though.
Edit... I am now doing this... and am living with the keyboard flicker for now:
Delay is a helper function for GCD
public func numberPad(numberPad: APNumberPad, functionButtonAction:UIButton, textInput: UIResponder) {
var current:UITextField?
for field in editCells {
if (current != nil) {
current?.resignFirstResponder()
delay (0) {
field.valueTextField.becomeFirstResponder()
}
return;
}
if (field.valueTextField == activeField) {
current = field.valueTextField
}
}
textInput.resignFirstResponder()
}
I don't know if it helps you, or not. I wrote a simple UITextField extension that contains a returnView variable which decides what the textfield should do on return key press:
turn to next text field (if the returnView is an UITextField)
simulate button touch (if the returnView is a UIButton)
or hide keyboard
class NextTextField: UITextField, UITextFieldDelegate {
#IBOutlet weak var returnView: UIView? {
didSet {
if returnView is UITextField {
returnKeyType = UIReturnKeyType.Next
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
delegate = self
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
if let nextTextField = self.returnView as? UITextField {
nextTextField.becomeFirstResponder()
} else if let nextButton = self.returnView as? UIButton {
nextButton.sendActionsForControlEvents(.TouchUpInside)
} else {
self.resignFirstResponder()
}
return true
}
}