Another important aspect of iOS development is text editing. On mobile platforms, adding text inputs is particularly difficult because of several complications:
The on-screen keyboard appears when you enter UITextField. It can hide other UI elements or even the text field itself.
The on-screen keyboard doesn’t disappear itself. In some cases, it can follow you to another screen.
The on-screen keyboard has different heights on different devices. Even if you know the sizes for all existing models, new iPhones or iPads will eventually appear, and the keyboard height on them may be different.
A physical keyboard can be connected to an iOS device. Besides special keyboards for iPads, any Bluetooth keyboard can be connected to said devices. When a physical keyboard is connected, the on-screen keyboard doesn’t appear.
Even setting the keyboard type doesn’t guarantee that the user won’t type a forbidden symbol.
Besides these mobile-specific problems, we have common problems of all apps allowing user input:
We may need to format the text in a specific way, for example, phone number or credit card number.
We often need floating text, which follows your input. Or prefix. It’s especially popular when you need input for money. It may require both formatting and floating text with currency.
We will discuss all these problems and their solutions in this chapter. And as a bonus, we’ll create a pin pad, a UI component containing four text inputs for four digits.
Ready? Open Xcode. Go!
Analyzing User Input in Real Time
Many beginner developers don’t understand how to process text input in real time
. There’s a UITextFieldDelegate delegate protocol containing many useful functions, but nothing like whatUserJustTyped or afterThisInputTextWillBeLikeThis.
Why would we need it? After all, we can always catch the moment when the user ends editing or accesses the text property of UITextField as they tap some button. Even though that’s true, modern applications are not like web forms from the 1990s – they’re interactive, and they follow your actions and adjust the UI accordingly. Here are several examples:
If you can log in with a phone number or an email address, the text input can change its formatting when it understands what you’re entering exactly. The app can also change some texts and icons around.
When the user enters a bank card number, it’s a good practice to show the logo of the card system (Visa, MasterCard, or other) after entering the first couple of digits. Some apps even show the logo of the issuing bank. When you use an app like that, you feel much more confident, and you feel that you’re on the right path.
When the user fills in a form and makes a mistake, apps often highlight the problematic fields. The app should remove warnings or errors when the user starts typing.
When the user enters a correct phone number, bank account number, taxpayer number, or any other data of the sort that can be verified, some apps show a success icon and/or move focus to the next field.
Pin pad. We’ll discuss it later, but a pin pad requires real-time input processing.
The key function for us is this
:
optional func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
It looks complicated, but this callback is extremely powerful. It provides us with all the necessary information:
textField is a UITextField object (or a subclass of UITextField) where editing occurred.
range shows a range of symbols removed from text input. If it has zero length, it’s just a position of insertion.
string contains new characters. It can be one character if the user tapped a key on the on-screen keyboard, or a whole text string if the user pasted it from the clipboard.
Method returns a Boolean value. If you return true, “replacement” (or insertion) will occur; otherwise, the input will be blocked.
Two components are missing:
Text before change
Text after change
Text before change is simply textField.text. Text after change requires some kind of calculation (Recipe6-1).
let updatedText = text.replacingCharacters(in: textRange, with: string)
// UpdatedText contains text after input is processed
}
return true
}
}
Recipe 6-1
Getting Text After Change
Please note that you must be very careful with the return value of this method. For example, if you return true only if validation passes, the user won’t be able to enter even the first symbol.
Usually, the code inside this delegate function should handle three situations:
The entered text is not valid and shouldn’t be entered at all.
The entered text is valid, but the result is not valid.
Everything is valid; we can show the user that they did well.
False should be returned in the first situation only, for example, when the user enters a phone number, and the string argument has a letter or emoji.
The second important note – always allow backspace or any other deletion method. If by mistake an illegal character appears in your text field, the user should be able to remove it. So if the string argument is empty, it should never return false.
Now let’s make a simple validator in Recipe 6-2. All validators are different, but the basic structure is similar. Let’s say we need the user’s email address. Our EmailInputValidator class should be assigned as a delegate of UITextField. It will remove all the characters that are not allowed in email addresses and make a call to its own delegate function when an address is valid.
Please find String extension isValidEmail in Recipe 3-3 or in full version of Recipe 6-2 on GitHub.
protocol EmailInputValidatorDelegate: AnyObject {
func validityChanged(isValid: Bool)
}
class EmailInputValidator: NSObject, UITextFieldDelegate {
There’s a certain type of data that needs visual decoration to be understandable, for example, phone numbers. They have different lengths in different countries, but on average, they’re around ten digits long. Seeing ten digits without any separation is hard for humans – you’ll hardly recognize your own phone number if presented that way. Adding some spaces, brackets, and dashes makes a phone number much more readable and understandable.
Another example is bank card numbers. It also varies, but the most common is separating digits in blocks by four.
We can’t expect perfect text styling from users. Even more, we usually allow to type only digits in such fields – on-screen keyboards don’t have spaces, dashes, or brackets. We need to do it automatically.
Formatting Phone Numbers
The next recipe will use the libPhoneNumber library
. You can add it from the following repository: https://github.com/iziz/libPhoneNumber-iOS. If you use Swift Package Manager, type the URL there; otherwise, use pod:
pod 'libPhoneNumber-iOS'
import libPhoneNumber_iOS
class PhoneViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var phoneNumberTextField: UITextField!
let countryId = "US"
let phoneUtil = NBPhoneNumberUtil.sharedInstance()
let updatedText = text.replacingCharacters(in: textRange, with: string)
if let phoneUtil = self.phoneUtil,
let phoneNumber = try? phoneUtil.parse(updatedText, defaultRegion: countryId),
let formattedString = try? phoneUtil.format(phoneNumber, numberFormat: .NATIONAL) {
DispatchQueue.main.async {
textField.text = formattedString
}
}
}
return true
}
}
Recipe 6-3
Formatting Phone Numbers in UITextField
Let’s review it line by line.
In your UI file (storyboard or xib), add a country selector and a UITextField. The country selector is usually a list of flags, country names, or codes. Changing country should change the countryId variable. We’ll leave this behind our scope. UITextField should have an @IBOutlet and View Controller as a delegate.
We create a constant phoneUtil as a reference to an NBPhoneNumberUtil singleton.
All the logic is inside a delegate method. If the string is empty, we return true without any verifications. It will allow us to freely delete symbols even if the result is not a valid phone number. Letting users delete symbols from UITextField is very important for a friendly UI.
if string.isEmpty {
return true
}
Then we calculate updatedText the way we discussed before in this chapter
.
let updatedText = text.replacingCharacters(in: textRange, with: string)
Now, when we have updated the text, we try to parse it and format it as a national number using countryId as a parameter. formattedString either has a formatted number or nil. If it has content, we set it as a new text.
And this part requires our attention as it’s very important. We can’t set text directly because the change of textField occurs after true is returned. If we change the text property inside this function, it will be overridden. That’s why we wrap it into DispatchQueue.main.async.
DispatchQueue.main.async {
textField.text = formattedString
}
Formatting Bank Card Numbers
The idea behind credit card number formatting
is exactly the same as the one behind phone number formatting, but in this case, we don’t need any libraries. Recipe 6-4 shows how it can be achieved.
In real life, credit card numbers can be more complicated, but we just group digits by four. In more complex cases, we can detect the card type by the first digits and apply proper formatting.
After getting updatedText, we leave only digits and drop all other characters (which are spaces). Then we add a space after each four digits and set new text inside the DispatchQueue.main.async block.
As a helper function
, we use the group extension method of String. It’s rather straightforward; it takes first groupSize characters of a string, adds a separator, and does the same with the rest of the string recursively. As a result, with default arguments, it groups digits by four and inserts spaces between.
Working with Emojis
Emojis appeared in the 1980s and became popular in the 1990s. Back then, there were no pictures, just text signs. Eyes were a colon; noses, a dash; and mouths – brackets. Like this: :-). You could see a face only by turning your head to the side, but it was better than nothing. Without seeing your interlocutor or hearing their voice, you didn’t know their intonation.
Within time, emojis became a big part of Internet culture and little by little migrated to other aspects of our lives. The Unicode Consortium adds more and more emojis to the standard; on-screen keyboards have a separate layout with emojis; finally, they became not just pictures, but often animated pictures.
At the same time, emojis can be a problem for us, developers. Even being standardized, emojis still can be encoded differently, and new emojis may not be supported by old devices or old versions of operating systems. That’s why it’s better to block them from most text inputs. They can be totally acceptable in social network posts, comments, and video descriptions, but users shouldn’t be able to use emojis in fields for email addresses, passwords, legal names, bank account numbers, etc.
To block users from entering emojis, we need two steps:
Detect that String has emojis.
Block texts with emojis from the UITextField delegate.
Detecting Emojis in Strings
Similarly to string analyzers
from Chapter 3, let’s add some extensions to detect if String has emojis (Recipe 6-5). As we know, String is a sequence of Character instances. To make sure String doesn’t have emojis, we need to check each Character. To check if String has emojis, we need to find at least one Character, which is an emoji.
extension Character {
var isSimpleEmoji: Bool {
guard let firstScalar = unicodeScalars.first else { return false }
var isEmoji: Bool { isSimpleEmoji || isCombinedIntoEmoji }
}
Recipe 6-5
Checking If Character Is Emoji
What’s the difference between a simple emoji and a combined one? The problem is that modern emojis are not just faces and flowers, they have many other characteristics like skin tone, hair color, and gender. If a simple yellow face can be represented as one Unicode symbol, more complicated ones are sequences of symbols.
The computed property isEmoji tells if Character is any type of emoji regardless of its complexity. Other extensions from Recipe 6-6 can be used to detect if String contains emojis.
extension String {
var isSingleEmoji: Bool { count == 1 && containsEmoji }
var containsEmoji: Bool { contains { $0.isEmoji } }
We don’t need all these computer properties to analyze user input, but they can be useful for other purposes. For example, if you ask your user to characterize some post with three emojis, the containsOnlyEmoji function will be handy.
Blocking Emoji from User Input
To block emojis
from user input, we just need to combine some of our previous recipes. Recipe 6-7 shows what we get.
Sometimes, we need to add noneditable text strings to the beginning or end of UITextField. It can be currency, a validation badge, or any other information.
Depending on the text position (prefix or suffix), there are different methods. To add a prefix, we need to add padding to the text input. Suffixes are more complicated; we need to move them every time the editable text is changed.
Adding Prefix
To add a prefix, you need two steps
:
Add a UILabel object and properly align it.
Add padding to UITextField, so the editable text will be to the right of the prefix.
Let’s create two objects and add outlets:
@IBOutlet weak var prefixLabel: UILabel!
@IBOutlet weak var mainText: TextFieldWithPadding!
Note
TextFieldWithPadding
is a class created in the "UITextField Paddings" section in Chapter 4. You can find its source code in Recipe 4-26.
We need to know the width of prefixLabel. If it’s static, we can adjust it in viewDidLayoutSubviews. If not, we need to readjust it every time the text is changed.
As you can see, the setPrefixmethod
has an asynchronous part. We need it to let UILabel update before using its new width. If asynchronous parts are not acceptable for you, you can calculate the text width manually
.
For demo purposes, this recipe contains an extension setting a new label text every time the user taps return. Don’t use this extension in a real-life app.
Adding Suffix
Unlike prefixes, suffixes
are not static; they move every time you enter a piece of text. Instead of adding padding when the layout is ready, we need to add the UITextField delegate and override func textField(_: UITextField, shouldChangeCharactersIn: NSRange, replacementString: String) -> Bool.
The vertical alignment is the same as in the previous recipe, but horizontally, UILabel should be aligned with the left (leading) side of UITextField. We will change this constraint every time the user changes input.
Another possible problem is the space on the right (trailing) side. If the entered text with a suffix is wider than our area, the label will go beyond the area, or the text will be cropped. That’s why we need to use the same class TextFieldWithPadding, but with rightPadding instead of leftPadding. Recipe 6-9 provides a solution for a floating suffix problem.
class FloatingSuffixViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var suffixLabel: UILabel!
@IBOutlet weak var mainText: TextFieldWithPadding!
@IBOutlet weak var suffixLeadingSpace: NSLayoutConstraint!
let updatedText = text.replacingCharacters(in: textRange, with: string)
if let font = mainText.font {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = (updatedText as NSString).size(withAttributes: fontAttributes)
suffixLeadingSpace.constant = size.width
view.layoutIfNeeded()
}
}
return true
}
}
Recipe 6-9
UITextField with Suffix
Width calculation here is performed through the NSString method size. It returns a CGSize structure containing text width and height with given attributes. The only attribute we set is NSAttributedString.Key.font. In iOS, the UIFont object contains not only the font but also font size, so the size method has enough information to calculate.
Keyboard Handling
As we discussed in the beginning of this chapter, on-screen keyboards can be a big problem for a developer. By default, the keyboard will appear when the user taps UITextField or UITextView, but it won’t disappear itself, and it will do nothing but to overlap UITextField or UITextView. If you add a field in the bottom of the screen, the keyboard will overlap it.
Hiding the Keyboard When the User Clicks the Outside Text Field
If the text field is not inside a scrollable area, it’s rather easy to hide it. You only need to override one function in UIViewController and call view.endEditing(true) as shown in Recipe 6-10.
class HidingKeyboardViewController: UIViewController {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
view.endEditing(true)
}
}
Recipe 6-10
Hiding the Keyboard
Hiding the Keyboard in Scrollable Areas
If you need text input inside a scrollable area
, which is rather typical for forms, the previous recipe won’t work. It doesn’t mean you shouldn’t use it; the user can tap the outside scrollable area, but it won’t be enough.
There are two solutions depending on your desired behavior:
Hiding the keyboard when the user scrolls
Hiding the keyboard when the user taps outside text fields
The first solution requires setting one attribute of UIScrollView. In Interface Builder (a storyboard editor), select your UIScrollView and set Keyboard to Dismiss on drag.
You can do the same using code:
scrollView.keyboardDismissMode = .onDrag
Hiding the Keyboard When the User Leaves the Screen
When the user leaves the screen
, it’s a good practice to hide the on-screen keyboard. There are usually two directions:
Moving to the next screen
Going back to the previous screen
Let’s say, you have a Next button with the @IBAction function. The first line of code in this function should be
view.endEditing(true)
As for going back, the solution is similar. In Chapter 4, we created a universal function goBack. Recipe 6-11 updates it to hide the keyboard. If the keyboard is not shown, it won’t harm the app.
public extension UIViewController {
@IBAction func goBack() {
view.endEditing(true)
if let nc = navigationController,
nc.viewControllers.count >= 2 {
nc.popViewController(animated: true)
} else {
dismiss(animated: true, completion: nil)
}
}
}
Recipe 6-11
Updated goBack Function
Changing Your Layout When the Keyboard Appears
When the keyboard appears, your layout needs to change. Always. Even if your text fields are always on top of the screen, there are objects in the bottom, which need to be moved. In case of using scrollable areas like UIScrollView, the content offset can be adjusted as well.
This can be done by handling keyboard notifications
as shown in Recipe 6-12.
class KeyboardListenerViewController: UIViewController {
// We don't usually need keyboard size when it's disappearing, so just set back your constraints
}
}
Recipe 6-12
Handling Keyboard Notifications
The notification argument is optional; you can create handlers without it if you don’t need keyboard size. Don’t forget that if the function signature changes, #selector should reflect these changes.
Now, when we know the moments of keyboard appearance and disappearance, let’s adjust our layout. One of the possible solutions is to create a constraint; let’s call it bottomConstraint, which defines the distance between the bottom of the screen (or safe area) and the lowest view. Usually, it’s the Next button.
From passed notification, we can extract keyboard height and move layout accordingly as shown in Recipe 6-13.
class KeyboardListenerViewController2: UIViewController {
@IBOutlet weak var bottomConstraint: NSLayoutConstraint!
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size {
bottomConstraint.constant = keyboardSize.height
view.layoutIfNeeded()
}
}
@objc func keyboardWillHide() {
bottomConstraint.constant = 0
view.layoutIfNeeded()
}
}
Recipe 6-13
Changing Layout
When Keyboard Is Appearing and Disappearing
Scrolling to Show Current Text Field
When the form is big like shown on Figure 6-1, we use a scrollable area, usually UIScrollView. When the keyboard is shown, we have a rather small area, and after two to three text fields, the cursor goes beyond its bounds. We need to automatically scroll UIScrollView to make the field that is being edited now visible.
There are two ways of handling it. First, you can shrink UIScrollView and make it completely above the keyboard. This way has a big disadvantage – every time the keyboard appears, the size of UIScrollView changes, and it can create unpleasant visual effects. The second way is to change the content offset (scroll position) every time the user enters another UITextField. As we know the position in parent, we know which content offset to set. Recipe 6-14 shows this solution.
class AutoscrollViewController: UIViewController {
case textField1: textField2.becomeFirstResponder()
case textField2: textField3.becomeFirstResponder()
case textField3: textField4.becomeFirstResponder()
case textField4: textField5.becomeFirstResponder()
case textField5: textField6.becomeFirstResponder()
case textField6: textField7.becomeFirstResponder()
case textField7: textField8.becomeFirstResponder()
default: textField.resignFirstResponder()
}
return false
}
}
Recipe 6-14
Scrolling to Make Text Field Visible
This code also includes a Return button handler. When you tap Return/Next/Submit or another button (it is always located in the bottom-right corner, and the name depends on the UITextField configuration), it goes to the next field.
You can invent a more interesting scrolling mechanism, for example, to make the previous field visible, or scroll only when the field is partially or fully covered. But this is a totally working solution, so feel free to use it as is.
Pin Pad UI Component
Passwords
are being used less and less, and it makes sense. Most users either forget their passwords or use the same password in all websites and mobile apps, which is extremely unsafe.
Many modern apps offer either a two-step verification with an SMS code or a code from a verification app (like Google Authenticator or Authy). This code contains four to six digits. Sometimes, they replace the password altogether.
The most beautiful and comfortable way to handle this is a pin pad. A regular UITextField doesn’t look good for this purpose.
Let’s write a simple four-digit pin pad.
First, add four UITextField objects to your view as shown in Figure 6-2. You can locate them wherever you like, but it will be better to make center alignment and put them on top, so we won’t need to change the layout when the keyboard appears.
Choose Number Pad as the keyboard type for all four of them. Assign them tags from 0 to 3 in order.
Make four outlets:
@IBOutlet weak var digit1TextField: UITextField!
...
@IBOutlet weak var digit4TextField: UITextField!
Make your UIViewController subclass (in our case, it’s PinPadViewController) a delegate for all of them. Implement UITextFieldDelegate. We will need these methods:
let updatedText = text.replacingCharacters(in: textRange,
with: string)
if updatedText.count > 1 {
return false
}
if !updatedText.containsOnlyDigits {
return false
}
return true
}
return false
}
}
Recipe 6-15
Pin Pad
You should make validation in method validateAndGo.
There are always many ways to implement one feature or another. This is a very simple way; it can be more complicated if you need visual effects or decorations.
Summary
In this chapter we described how to validate text while editing, how to block some characters from user input, how to add prefix or suffix to text field. As most of mobile devices don’t have keyboard, we should never forget about on-screen keyboard covering part of our user interface. To handle it, we need to subscribe to notifications and update layout. We also need to remember to hide keyboard when screen is changed. The last recipe shows one interesting use case of text fields – pin pad. Component for entering pin code or verification code from SMS. In the next chapter we’ll finish discussing UIKit and talk about animations and visual effects.