Greetings, traveler!
We are concluding our series of articles about the text field. Today, I will guide you on effectively adding an input mask to a text field. This is particularly useful when creating an input field for a phone number, card number, or any special code. Crafting a good mask can be complex, but we have a solution. We will be using a ready-made option from RedMadRobot. In fact, they have already provided a masked text field option for SwiftUI. However, we will create our custom solution to adapt and customize it to our needs. Let’s get started.
Input Mask
First, let’s install the library via Swift Package Manager. Once that’s done, we can import it into the file where we placed our text field.
dependencies: [
.package(url: "https://github.com/RedMadRobot/input-mask-ios.git", .upToNextMajor(from: "7.0.0"))
]
To create a mask for the input in the text field, you must define an inputListener property with the optional MaskedTextInputListener type. This property should be present in both the text field and the coordinator. In the makeUIView function, we need to ensure that the value of the InputListener property is not nil. If it is not nil, we assign the coordinator as the delegate of the InputListener text field. If it is nil, then the coordinator itself becomes the delegate. This ensures the proper functioning of the mask in the text field.
struct CustomTextField: UIViewRepresentable {
var inputListener: MaskedTextInputListener?
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
if inputListener == nil {
textField.delegate = context.coordinator
} else {
textField.delegate = context.coordinator.inputListener
}
return textField
}
In the coordinator initializer, we ensure we have received the InputListener value. If we have received it, we set the textFieldDelegate of this class to be the coordinator itself. There is no need to perform further actions inside the coordinator, as the inputListener will handle all the work.
final class Coordinator: NSObject, UITextFieldDelegate {
var inputListener: MaskedTextInputListener?
var inputAccessoryViewFactory: InputAccessoryViewFactoryProtocol?
@Binding private var text: String
@Binding private var isFirstResponder: Bool
private var didBeginEditing: () -> Void
private var didChange: () -> Void
private var didEndEditing: () -> Void
private var shouldReturn: () -> Void
private var shouldClear: () -> Void
private var onValidate: (ValidationResult) -> Void
private var characterLimit: Int? = nil
private var isValidateAfterFinishEditing: Bool
private var isValidateWhileEditing: Bool
private var validations: [ValidatorProtocol]
private var wasEdited = false
init(
text: Binding<String>,
isFirstResponder: Binding<Bool>,
characterLimit: Int?,
isValidateAfterFinishEditing: Bool,
isValidateWhileEditing: Bool,
validations: [ValidatorProtocol],
inputListener: MaskedTextInputListener?,
didBeginEditing: @escaping () -> Void,
didChange: @escaping () -> Void,
didEndEditing: @escaping () -> Void,
shouldReturn: @escaping () -> Void,
shouldClear: @escaping () -> Void,
onValidate: @escaping (ValidationResult) -> Void
) {
self._text = text
self._isFirstResponder = isFirstResponder
self.characterLimit = characterLimit
self.isValidateAfterFinishEditing = isValidateAfterFinishEditing
self.isValidateWhileEditing = isValidateWhileEditing
self.validations = validations
self.didBeginEditing = didBeginEditing
self.didChange = didChange
self.didEndEditing = didEndEditing
self.shouldReturn = shouldReturn
self.shouldClear = shouldClear
self.onValidate = onValidate
super.init()
guard let inputListener else { return }
self.inputListener = inputListener
self.inputListener?.textFieldDelegate = self
}
...
}
Programmatic text insertion
Now, all the text that the user will enter will be masked. However, transmitting the text differently will leave it unmasked. And now we will fix it.
Whenever a change is made to the text field, the updateUIView function gets called. This is where we will place our code. To get started, we can create a boolean property named shouldInsertMaskedText, indicating whether we want this behavior. Then, in the updateUIView function, we will check if this property is set to true. If it is true and the isFirstResponder property is false, we will forcibly replace the text inside the field with a masked one.
func updateUIView(_ uiView: UITextField, context: Context) {
if shouldInsertMaskedText && !isFirstResponder {
inputListener?.put(text: uiView.text ?? "", into: uiView)
}
...
}
Customization
Now, let’s create tools to customize all of this. As usual, we will use the extension for our text field. After that, we can use it.
extension CustomTextField {
func masks(_ masks: String...) -> CustomTextField {
var view = self
view.inputListener = .init(affineFormats: masks)
return view
}
func shouldInsertMaskedText(_ shouldInsertMaskedText: Bool) -> iTextField {
var view = self
view.shouldInsertMaskedText = shouldInsertMaskedText
return view
}
}
struct ContentView: View {
@State private var text = ""
@State private var isFirstResponder = false
var body: some View {
CustomTextField(text: $text, isFirstResponder: $isFirstResponder)
.shouldInsertMaskedText(true)
.masks("{+1} ([000]) [000]-[00]-[00]")
}
}
Done!
Conclusion
We have completed our series of articles on creating a custom SwiftUI TextField. We started with the basics, creating a text field based on UITextField in SwiftUI View. We learned how to handle text field events and customize their behavior. Then, we learned how to customize the text field properties to our needs. We also created a universal mechanism for adding inputAccessoryView to our text field. After that, we learned how to validate the field’s contents to provide up-to-date feedback to the user improving the user experience. Finally, we implemented a mask for our text field. We have done an excellent job, I can say.
By the way, the complete code is available on the GitHub.
See you soon!