input-mask-ios

by RedMadRobot

RedMadRobot / input-mask-ios

User input masking library repo.

470 Stars 75 Forks Last release: 8 months ago (6.0.0) MIT License 159 Commits 32 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

Input Mask

Awesome Version Badge SPM compatible Carthage compatible license Build Status codebeat badge

PlatformAndroidiOSmacOS

Input Mask

Migration Guide: v.6

This update brings breaking changes. Namely, the

autocomplete
flag is now a part of the
CaretGravity
enum, thus the
Mask::apply
call is now single-argument, as all the necessary information is included into the
CaretString
structure.

v.6
introduces the «autoskip» feature, which allows the cursor to jump over formatting blocks of symbols in the middle of the text as if they were a single char when hitting
Backspace
, and this feature also allows to trim formatting characters on backspacing at the end of the line.

Make sure to take a look at our CHANGELOG.

Description

Input Mask
is an Android & iOS native library allowing to format user input on the fly.

The library provides you with a text field listener; when attached, it puts separators into the text while user types it in, and gets rid of unwanted symbols, all according to custom predefined pattern.

This allows to reformat whole strings pasted from the clipboard, e.g. turning pasted

8 800 123-45-67
into
8 (800) 123 45 67
.

Each pattern allows to extract valuable symbols from the entered text, returning you the immediate result with the text field listener's callback when the text changes. Such that, you'll be able to extract

1234567
from
8 (800) 123 45 67
or
19991234567
from
1 (999) 123 45 67
with two different patterns.

All separators and valuable symbol placeholders have their own syntax. We call such patterns "masks".

Mask examples:

  1. International phone numbers:
    +1 ([000]) [000] [00] [00]
  2. Local phone numbers:
    ([000]) [000]-[00]-[00]
  3. Names:
    [A][-----------------------------------------------------]
  4. Text:
    [A…]
  5. Dates:
    [00]{.}[00]{.}[9900]
  6. Serial numbers:
    [AA]-[00000099]
  7. IPv4:
    [099]{.}[099]{.}[099]{.}[099]
  8. Visa card numbers:
    [0000] [0000] [0000] [0000]
  9. MM/YY:
    [00]{/}[00]
  10. UK IBAN:
    GB[00] [____] [0000] [0000] [0000] [00]

Questions & Issues

Check out our wiki for further reading.
Please also take a closer look at our Known issues section before you incorporate our library into your project.

For your bugreports and feature requests please file new issues as usually.

Should you have any questions, search for closed issues or open new ones at StackOverflow with the

input-mask
tag.

We also have a community-driven cookbook of recipes, be sure to check it out, too.

Installation

CocoaPods

pod 'InputMask'

Carthage

git "https://github.com/RedMadRobot/input-mask-ios.git"

Swift Package Manager

dependencies: [
    .Package(url: "https://github.com/RedMadRobot/input-mask-ios", majorVersion: 5)
]

Manual

  1. git clone
    this repository;
  2. Add
    InputMask.xcodeproj
    into your project/workspace;
  3. Go to your target's settings, add
    InputMask.framework
    under the
    Embedded Binaries
    section
  4. For
    ObjC
    projects:
    • (~Xcode 8.x) make sure
      Build Options
      has
      Embedded Content Contains Swift Code
      enabled;
    • import bridging header.

Known issues

UITextFieldTextDidChange
notification and target-action
editingChanged
event

UITextField
with assigned
MaskedTextFieldDelegate
object won't issue
UITextFieldTextDidChange
notifications and
editingChanged
control events. This happens due to the
textField(_:shouldChangeCharactersIn:replacementString:)
method implementation, which always returns
false
.

Consider using following workaround in case if you do really need to catch editing events:

class NotifyingMaskedTextFieldDelegate: MaskedTextFieldDelegate {
    weak var editingListener: NotifyingMaskedTextFieldDelegateListener?

override func textField(
    _ textField: UITextField,
    shouldChangeCharactersIn range: NSRange,
    replacementString string: String
) -> Bool {
    defer {
        self.editingListener?.onEditingChanged(inTextField: textField)
    }
    return super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
}

}

protocol NotifyingMaskedTextFieldDelegateListener: class { func onEditingChanged(inTextField: UITextField) }

Please, avoid at all costs sending SDK events and notifications manually.

Carthage vs. IBDesignables, IBInspectables, views and their outlets

Interface Builder struggles to support modules imported in a form of a dynamic framework. For instance, custom views annotated as IBDesignable, containing IBInspectable and IBOutlet fields aren't recognized properly from the drag'n'dropped *.framework.

In case you are using our library as a Carthage-built dynamic framework, be aware you won't be able to easily wire your

MaskedTextFieldDelegate
objects and their listeners from storyboards in your project. There is a couple of workarounds described in the corresponding discussion, though.

Also, consider filing a radar to Apple, like this one.

Cut action doesn't put text into the pasteboard

When you cut text, characters get deleted yet you won't be able to paste them somewhere as they aren't actually in your pasteboard.

iOS hardwires

UIMenuController
's cut action to the
UITextFieldDelegate
's
textField(_:shouldChangeCharactersIn:replacementString:)
return value. This means "Cut" behaviour actually depends on the ability to edit the text.

Bad news are, our library returns

false
in
textField(_:shouldChangeCharactersIn:replacementString:)
, and heavily depends on this
false
. It would require us to rewrite a lot of logic in order to change this design, and there's no guarantee we'll be able to do so.

Essentially, there's no distinct way to differentiate "Cut selection" and "Delete selection" actions on the

UITextFieldDelegate
side. However, you may consider using a workaround, which will require you to subclass
UITextField
overriding its
cut(sender:)
method like this:
class UITextFieldMonkeyPatch: UITextField {
    override func cut(_ sender: Any?) {
        copy(sender)
        super.cut(sender)
    }
}

From our library perspective, this looks like a highly invasive solution. Thus, in the long term, we are going to investigate a "costly" method to bring the behaviour matching the iOS SDK logic. Yet, here "long term" might mean months.

Incorrect cursor position after pasting

Shortly after new text is being pasted from the clipboard, every

UITextInput
receives a new value for its
selectedTextRange
property from the system. This new range is not consistent with the formatted text and calculated caret position most of the time, yet it's being assigned just after
set caretPosition
call.

To ensure correct caret position is set, it might be assigned asynchronously (presumably after a vanishingly small delay), if caret movement is set to be non-atomic; see

MaskedTextFieldDelegate.atomicCursorMovement
property.

MaskedTextInputListener

In case you are wondering why do we have two separate

UITextFieldDelegate
and
UITextViewDelegate
implementations, the answer is simple: prior to iOS 11
UITextField
and
UITextView
had different behaviour in some key situations, which made it difficult to implement common logic.

Both had the same bug with the

UITextInput.beginningOfDocument
property, which rendered impossible to use the generic
UITextInput
protocol
UITextField
and
UITextView
have in common.

Since iOS 11 most of the things received their fixes (except for the

UITextView
edge case). In case your project is not going to support anything below 11, consider using the modern
MaskedTextInputListener
.

References

The list of projects that are using this library which were kind enough to share that information.

Feel free to add yours below.

Special thanks

These folks rock:

License

The library is distributed under the MIT LICENSE.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.