© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
A. NekrasovSwift Recipes for iOS Developershttps://doi.org/10.1007/978-1-4842-8098-0_6

6. Text Editing

Alexander Nekrasov1  
(1)
Moscow, Russia
 
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 ( Recipe 6-1 ).
extension ViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let text = textField.text,
           let textRange = Range(range, in: text) {
            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 {
    weak var delegate: EmailInputValidatorDelegate?
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let allowedRegEx = "[A-Z0-9a-z._%+-@]+"
        let allowedTest = NSPredicate(format: "SELF MATCHES %@", allowedRegEx)
        if !allowedTest.evaluate(with: string) && !string.isEmpty {
            return false
        }
        if let text = textField.text,
           let textRange = Range(range, in: text) {
            let updatedText = text.replacingCharacters(in: textRange, with: string)
            delegate?.validityChanged(isValid: updatedText.isValidEmail)
        }
        return true
    }
}
Recipe 6-2

Email Input Validator

You can use it in UIViewController like this:
class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    var emailValidator = EmailInputValidator()
    override func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = emailValidator
        emailValidator.delegate = self
    }
}
extension ViewController: EmailInputValidatorDelegate {
    func validityChanged(isValid: Bool) {
        print(isValid)
    }
}

Formatting User Input

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()
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if string.isEmpty {
            return true
        }
        if let text = textField.text,
           let textRange = Range(range, in: text) {
            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.
extension String {
    func group(by groupSize: Int = 4, separator: String = " ") -> String {
        if count <= groupSize { return self }
        let splitIndex = index(startIndex, offsetBy: groupSize)
        return String(self[..<splitIndex]) + separator +
            String(self[splitIndex...]).group(by: groupSize, separator:separator)
    }
}
class CreditCardViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet weak var cardNumberTextField: UITextField!
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if string.isEmpty {
            return true
        }
        if let text = textField.text,
           let textRange = Range(range, in: text) {
            let updatedText = text
                .replacingCharacters(in: textRange, with: string)
                .filter("0123456789".contains)
            let formattedString = updatedText.group()
                .trimmingCharacters(in: .whitespacesAndNewlines)
            DispatchQueue.main.async {
                textField.text = formattedString
            }
        }
        return true
    }
}
Recipe 6-4

Formatting Bank Card Numbers in UITextField

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 }
        return firstScalar.properties.isEmoji && firstScalar.value > 0x238C
    }
    var isCombinedIntoEmoji: Bool { unicodeScalars.count > 1 && unicodeScalars.first?.properties.isEmoji ?? 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 } }
    var containsOnlyEmoji: Bool { !isEmpty && !contains { !$0.isEmoji } }
    var emojiString: String { emojis.map { String($0) }.reduce("", +) }
    var emojis: [Character] { filter { $0.isEmoji } }
    var emojiScalars: [UnicodeScalar] { filter { $0.isEmoji }.flatMap { $0.unicodeScalars } }
}
Recipe 6-6

Checking If String Contains Emoji

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.
extension ViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if string.containsEmoji && !string.isEmpty {
            return false
        }
        // Validate if necessary
        return true
    }
}
Recipe 6-7

Blocking Emoji from User Input

Floating Prefix or Suffix

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.
mainText.paddingLeft = prefixLabel.bounds.width + 8
We add eight extra points for a gap between the prefix and the main text. The full solution is shown in Recipe 6-8.
class FloatingPrefixViewController: UIViewController {
    @IBOutlet weak var prefixLabel: UILabel!
    @IBOutlet weak var mainText: TextFieldWithPadding!
    static let gapWidth = CGFloat(8)
    override func viewDidLayoutSubviews() {
        mainText.paddingLeft = prefixLabel.bounds.width + FloatingPrefixViewController.gapWidth
    }
    func setPrefix(_ prefix: String) {
        prefixLabel.text = prefix
        DispatchQueue.main.async {
            self.mainText.paddingLeft = self.prefixLabel.bounds.width + FloatingPrefixViewController.gapWidth
        }
    }
}
extension FloatingPrefixViewController: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        setPrefix(textField.text ?? "")
        textField.text = ""
        return false
    }
}
Recipe 6-8

UITextField with Prefix

As you can see, the setPrefix method 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!
    static let gapWidth = CGFloat(8)
    override func viewDidLayoutSubviews() {
        mainText.paddingRight = suffixLabel.bounds.width + FloatingPrefixViewController.gapWidth
    }
    func setSuffix(_ suffix: String) {
        suffixLabel.text = suffix
        DispatchQueue.main.async {
            self.mainText.paddingLeft = self.suffixLabel.bounds.width + FloatingPrefixViewController.gapWidth
        }
    }
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let text = textField.text,
           let textRange = Range(range, in: text) {
            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 {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(notification:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide(notification:)),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    override func viewDidDisappear(_ animated: Bool) {
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
        super.viewWillDisappear(animated)
    }
    @objc func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size {
            // Keyboard height is in keyboardSize.height
        }
    }
    @objc func keyboardWillHide(notification: NSNotification) {
        // 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!
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(notification:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    override func viewDidDisappear(_ animated: Bool) {
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
        super.viewWillDisappear(animated)
    }
    @objc func keyboardWillShow(notification: NSNotification) {
        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.

An image displays a mobile screen with empty text fields and a keyboard. The cursor is placed on the first textbox. The keyboard displays suggestions for text filling.

Figure 6-1

Moving UITextFields when the keyboard appears

class AutoscrollViewController: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var textField1: UITextField!
    @IBOutlet weak var textField2: UITextField!
    @IBOutlet weak var textField3: UITextField!
    @IBOutlet weak var textField4: UITextField!
    @IBOutlet weak var textField5: UITextField!
    @IBOutlet weak var textField6: UITextField!
    @IBOutlet weak var textField7: UITextField!
    @IBOutlet weak var textField8: UITextField!
    // ...
    static let gap = CGFloat(40)
}
extension AutoscrollViewController: UITextFieldDelegate {
    func textFieldDidBeginEditing(_ textField: UITextField) {
        let offset = textField.frame.origin.y - AutoscrollViewController.gap
        DispatchQueue.main.async {
            self.scrollView.setContentOffset(CGPoint(x: 0, y: offset), animated: true)
        }
    }
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        switch textField {
        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.

An image displays a mobile screen with empty text boxes and a number keypad. Out of 4 spaces, numbers 1 and 5 are entered, and the cursor is placed in the 3rd position.

Figure 6-2

Pin pad example

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:
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool

The first method should clean the selected text field and all text fields after it.

The second method should make input validation. We allow only digits and only one per field.

To move focus from one field to another, we’ll use the editingChanged action. Let’s use a method called textChanged:
@IBAction func textChanged(_ textField: UITextField)
To make it easier, we can pack text fields into an array.
class PinPadViewController: UIViewController {
    @IBOutlet weak var digit1TextField: UITextField!
    @IBOutlet weak var digit2TextField: UITextField!
    @IBOutlet weak var digit3TextField: UITextField!
    @IBOutlet weak var digit4TextField: UITextField!
    private var pinDigitTextFields: [UITextField] = []
    override func viewDidLoad() {
        super.viewDidLoad()
        pinDigitTextFields = [
            digit1TextField,
            digit2TextField,
            digit3TextField,
            digit4TextField
        ]
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        clear()
    }
    @IBAction func textChanged(_ textField: UITextField) {
        if textField.tag == 3 {
            view.endEditing(true)
            validateAndGo()
        } else {
            pinDigitTextFields[textField.tag + 1].becomeFirstResponder()
        }
    }
    private func clear() {
        pinDigitTextFields.forEach {
            $0.text = ""
        }
        digit1TextField.becomeFirstResponder()
    }
    private func getPin() -> String {
        let digit1 = digit1TextField.text ?? ""
        let digit2 = digit2TextField.text ?? ""
        let digit3 = digit3TextField.text ?? ""
        let digit4 = digit4TextField.text ?? ""
        return "(digit1)(digit2)(digit3)(digit4)"
    }
    private func validateAndGo() {
        let pin = getPin()
        if pin.count != 4 {
            clear()
            return
        }
        // Here you can send PIN to API to verify it (or do it locally)
    }
}
extension PinPadViewController: UITextFieldDelegate {
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        for i in textField.tag..<3 {
            pinDigitTextFields[i].text = ""
        }
        return true
    }
    public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let text = textField.text,
           let textRange = Range(range, in: text) {
            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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.19.208.13