Skip to content

0. Mask

Egor Taflanidi edited this page Mar 16, 2020 · 8 revisions

Mask is the central class responsible for formatting the input, producing the output, and extracting the valuable characters.

class Mask {
    struct Result {
        let formattedText: CaretString
        let extractedValue: String
        let affinity: Int
        let complete: Bool
    }

    init(format: String, customNotations: [Notation] = []) throws

    class func getOrCreate(withFormat format: String, customNotations: [Notation] = []) throws -> Mask

    class func isValid(format: String, customNotations: [Notation] = []) -> Bool

    func apply(toText text: CaretString) -> Result

    var placeholder:           String
    var acceptableTextLength:  Int
    var totalTextLength:       Int
    var acceptableValueLength: Int
    var totalValueLength:      Int
}

Initialisation

Mask object is a state machine with its own internal graph of states to move through. The graph is compiled from the format during the initialisation. If the format is invalid, compiler throws an exception.

init(format: String, customNotations: [Notation] = []) throws

— the only constructor passes the list of custom notations and the format to the internal compiler.

class func getOrCreate(withFormat format: String, customNotations: [Notation] = []) throws -> Mask

— in order to avoid graph re-compilation overhead each time you make a Mask object, use the getOrCreate() class method — it has its own cache from where previously created Mask objects with similar graphs are fetched instead of being created.

class func isValid(format: String, customNotations: [Notation] = []) -> Bool

— is a convenience method to check whether or not this particular format will cause a compilation exception given the list of custom notations.

Applying the format

func apply(toText text: CaretString) -> Result

— use this method to format text without involving a UIView.

CaretString is a structure representing a string and a caret (cursor) position inside of it.

struct CaretString {
    let string:        String
    let caretPosition: String.Index
    let caretGravity:  CaretGravity
}

The caretGravity property is a flag containing the direction, towards which the cursor gravitates during the formatting.

Adding and removing characters leads to the cursor movements. For instance, if the input string contains unwanted symbols, they are removed, and the cursor moves left:

Mask:    [000]

Input:   1a2
           ^
Output:  12
          ^

However, adding a character at the exact position where the cursor is — might be a little bit tricky.
Simply moving the cursor right every time a character gets added (see below) looks like a way to go, though there's a catch:

Mask:  [00]/[00]

Input:  123
          ^
Output: 12/3
           ^

Imagine a sequence like this:

Input00:  12/3     // you hit backspace =>
             ^

Input01:  123      // formatting kicks in =>
            ^

Output01: 12/3
            ?      // where should we put the cursor?

Ideally, after you hit backspace you'd want an output like this:

Output01: 12/3
            ^

— thus, it would look like the cursor simply moved backwards without deleting the / character.
This is where the caretGravity comes in handy. With this flag, the Mask instance is aware about the preferred direction of cursor movement during the formatting, with backward gravity representing the situations like the latter one.

autocomplete flag

Autocompletion allows predictive insertion of constant characters (see constant blocks and separators).

For instance, having a MM/YY date field, / symbol would require a full keyboard, but you can actually get away with a numeric one:

[00]{/}[00]
Key pressed   Output

1             1
2             12/
3             12/3
4             12/34

Without the autocompletion, the picture is a bit different:

Key pressed   Output

1             1
2             12
3             12/3
4             12/34

Enabled autocompletion essentially helps user to understand that he doesn't need to type the dividing / by his hand, which makes data input more clear.

The autocomplete flag is attached to the CaretGravity.forward enum case: autocompletion cannot coexist with the .backward gravity.

autoskip flag

Automatic character skipping works as an "anti-autocompletion", erasing the constant characters (see constant blocks and separators) at the end of the line when hitting backspace.

This feature also allows the cursor to jump over those blocks of symbols in the middle of the text as if they were a single char.

The autoskip flag is attached to the CaretGravity.backward enum case: automatic character skipping cannot coexist with the .forward gravity.

Result

The resulting cursor position along with the output (formatted text), value completeness and calculated affinity are returned as a Mask.Result object:

struct Result {
    let formattedText:  CaretString
    let extractedValue: String
    let affinity:       Int
    let complete:       Bool
}

affinity is an integer value representing the similarity between the input and the mask pattern. This value is necessary to determine the most suitable mask.

complete flag shows if the extracted value contains all the mandatory characters.

Properties and metrics

Each mask has its metrics.

  • acceptableTextLength is the minimum length the output has to be in order to be acceptable (complete == true);
  • totalTextLength is the maximum output length, with all optional characters included;
  • acceptableValueLength is the minimum acceptable extracted value length;
  • totalValueLength is the maximum extracted value length.

placeholder produces a simple placeholder text that can serve as a text field hint (including optional symbols; a for arbitrary letters, 0 for arbitrary digits, - for arbitrary alphanumerics):

([009])[Aa] ~> (000)aa