-
Notifications
You must be signed in to change notification settings - Fork 94
2. Text Field Listener
While the Mask
is the cornerstone class of this library, text field listeners are the objects you'll be dealing with most of the time.
These listeners encapsulate the internal logic, giving you a façade of methods, properties, and callbacks to be configured.
Text field listeners implement text field's event handling, cursor movement logic, autocompletion control and automatic mask switching. They also interface underlying mask metrics and compiler notations.
Input Mask
library provides three implementations of a text field listener.
MaskedTextFieldDelegate : UITextFieldDelegate
MaskedTextViewDelegate : UITextViewDelegate
MaskedTextInputListener : UITextFieldDelegate, UITextViewDelegate @available(iOS 11, *)
All three are essentially the same class with almost the same logic, yet they had been divided because of the iOS SDK limitations, see below.
That said, we are going to review the MaskedTextFieldDelegate
, others behave the same.
MaskedTextFieldDelegate
provides two ways for you to receive text editing events. It has its own MaskedTextFieldDelegateListener
protocol, which is a UITextFieldDelegate
:
@objc public protocol MaskedTextFieldDelegateListener: UITextFieldDelegate {
@objc optional func textField(
_ textField: UITextField,
didFillMandatoryCharacters complete: Bool,
didExtractValue value: String
)
}
MaskedTextFieldDelegate
forwards each received UITextFieldDelegate
call to its MaskedTextFieldDelegateListener
, yet the textField(textField:shouldChangeCharactersIn:replacementString:)
method is going to ignore your returned value: returning false
is essential for the correct library functioning.
weak var listener: MaskedTextFieldDelegateListener?
@IBOutlet var delegate: NSObject?
MaskedTextFieldDelegateListener
is assigned through the listener
property.
delegate
property provides additional Interface Builder support, allowing to wire up the listener directly on canvas; delegate
is essentially an IB outlet for the listener
field.
This callback:
var onMaskedTextChangedCallback: ((_ textField: UITextField, _ value: String, _ complete: Bool) -> ())?
— is the second way to receive text editing events. Here,
-
textField
is a text field instance, which created an event; -
value
is an extracted value; -
complete
flag shows if an extracted value is complete.
MaskedTextFieldDelegate
is an NSObject
, meaning that it can be dropped on a canvas as an Interface Builder object, initialised with its convenience
init method:
convenience init()
Using this method, a newly created MaskedTextFieldDelegate
will have all its properties set to default.
Otherwise, you may programmatically create a MaskedTextFieldDelegate
instance, providing all the settings through the designated initialiser:
init(
primaryFormat: String = "",
autocomplete: Bool = true,
autocompleteOnFocus: Bool = true,
autoskip: Bool = false,
rightToLeft: Bool = false,
affineFormats: [String] = [],
affinityCalculationStrategy: AffinityCalculationStrategy = .wholeString,
customNotations: [Notation] = [],
onMaskedTextChangedCallback: ((_ textInput: UITextInput, _ value: String, _ complete: Bool) -> ())? = nil
)
-
primaryFormat
— the main mask pattern; -
autocomplete
— autocompletion option; -
autocompleteOnFocus
— in case your mask pattern contains a fixed prefix (like a country code in phone numbers), this prefix will be automatically inserted on focus; -
autoskip
— automatic character skipping option; -
rightToLeft
— enable right-to-left text formatting; -
affineFormats
— a list of affine formats; -
affinityCalculationStrategy
— see affine formats; -
customNotations
— a list of compiler notations; -
onMaskedTextChangedCallback
— this closure is called on every text change, see above.
Some of the MaskedTextFieldDelegate
properties are @IBInspectable
, meaning that you may configure them through the Interface Builder inspector.
@IBInspectable open var primaryMaskFormat: String
@IBInspectable open var autocomplete: Bool
@IBInspectable open var autocompleteOnFocus: Bool
@IBInspectable open var autoskip: Bool
@IBInspectable open var rightToLeft: Bool
-
primaryMaskFormat
— the main mask pattern; -
autocomplete
— autocompletion option; -
autocompleteOnFocus
— in case your mask pattern contains a fixed prefix (like a country code in phone numbers), in will be automatically inserted on focus; -
autoskip
— automatic character skipping option; -
rightToLeft
— enable right-to-left text formatting.
var affineFormats: [String]
var affinityCalculationStrategy: AffinityCalculationStrategy
var customNotations: [Notation]
-
affineFormats
— a list of affine formats; -
affinityCalculationStrategy
— see affine formats; -
customNotations
— a list of compiler notations;
var primaryMask: Mask { get }
var placeholder: String { get }
var acceptableTextLength: Int { get }
var totalTextLength: Int { get }
var acceptableValueLength: Int { get }
var totalValueLength: Int { get }
Here, primaryMask
is the main Mask
object used to format the input.
Other readonly properties represent primary mask's properties and metrics.
@IBInspectable open var atomicCursorMovement: Bool = true
There is an actual iOS bug, that is evident in the Contacts.app
while you edit a phone number field. Try copy-pasting a 1234567890
string into the phone field and notice the actual cursor position.
Shortly after new text is being pasted from the clipboard, UITextField
receives a new value for its selectedTextRange
property from the system (this affects cursor position). This new range is not consistent with the formatted text and calculated cursor position most of the time, yet it's being assigned just after set cursorPosition
call.
To ensure correct cursor position is set, it is assigned asynchronously (presumably after a vanishingly small delay), if cursor movement is set to be non-atomic.
Default value is false
.
func put(text: String, into: UITextField, autocomplete: Bool? = nil) -> Mask.Result
This method is designed to programmatically insert raw text into the UITextField
, simultaneously applying the format.
So, what is this all about having three different text field listeners, that almost duplicate each other's code?
Well, first of all, UITextFieldDelegate
and UITextViewDelegate
are two different protocols, hence the need of a separate MaskedTextViewDelegate
.
UITextFieldDelegate
and UITextViewDelegate
callbacks are almost the same, except for the «shouldChangeTextInRange
». Both callbacks have the same type signature, yet for the empty UITextView
on Backspace
hit this callback is called, but for the empty UITextField
it is not.
With the autocompletion enabled this leads to the automatic mask prefix insertion, like during the autocompleteOnFocus
, but on every each Backspace
hit.
Next, there's a handy UITextInput
protocol, which unites UITextField
and UITextView
classes, making it possible to generalise related logic.
Except there's no way to read or replace the whole text
contained inside the UITextInput
. The only methods are
func text(in range: UITextRange) -> String?
func replace(_ range: UITextRange, withText text: String)
— both require an UITextRange
argument, which shouldn't be a problem since the UITextInput
has this:
func textRange(from fromPosition: UITextPosition, to toPosition: UITextPosition) -> UITextRange?
This method allows to get the "whole" text range by calling
let field: UITextInput = ...
let range: UITextRange = field.textRange(from: field.beginningOfDocument, to: field.endOfDocument)
Okay, puzzle solved? NOT SO FAST!
Prior to iOS 11, there's a bug, when non-focused text fields (and text views) have nil inside their beginningOfDocument
and endOfDocument
properties. Thus, you won't be able to put formatted text inside the UITextInput
while it's not focused.
For both UITextField
and UITextView
there's a working field.text
property, which does not require any UITextRange
arguments to be operational. Unfortunately, UITextInput
instances do not have this sophisticated ability.
Thus, MaskedTextInputListener
is only available on iOS 11 and higher, and I won't get rid of this code duplication until iOS 10 support is dropped.