Customizing text fields is a critical feature of almost every mobile app. We often use them when we need a user to fill out text as part of a form. Because we use them so often, we’re always trying to find ways to optimize implementing them in our app’s user interface.
Here is an example of a customized text field that has additionally inset text and an icon on the right:
Building a Customizable Text Field
To help us build a similar text field that is customizable, we’ll start by subclassing UITextField.
To achieve setting custom insets we‘ll need to override a few methods, specifically the bounds of the text field’s label. Overriding textRect(forBounds:) and editingRect(forBounds:) will yield the results we want:
We talked about adding custom views in the text field as well. Since we modified the bounds of the text field‘s label we’ll need to consider the adjusted insets. Similarly to the overridden methods above, we‘ll need to override rightViewRect(forBounds:) like this:
The frame of the right view‘s rectangle is also adjusted to include some padding. Since we have a foundation set up for the right view all that is left is to populate it. UITextField doesn‘t have a method like setImage to instantiate an image view for us like a UIButton would. Setting the right view with a UIButton will suffice. Here‘s what that would look like with some optional properties like text and tint color:
Setting the rightViewMode will notify the text field when the right view overlay should appear. We also need to update the insetTextRect(forBounds:) with the additional right view padding and width of the right view:
Configuring the customized text field
Applications typically use a standardized design pattern (such as MVP or MVVM), which mostly derive from conforming to the separation of concerns principle. How do we handle text field customization and responding to user initiated actions while separating logic?
The core of this subclass contains several properties. It looks like we can make use of a view model as the main source of configuration for the text field. So far we have the center inset and padding properties but we could benefit from adding a right view object along with the text field properties:
This is also a perfect use case for taking advantage of Apple‘s Combine framework. Pre-Combine, the text field responses could have been handled with closures or delegates. UIKit does not provide Combine support by default, so to simplify some of the following logic we‘ll use a 3rd party framework, CombineCocoa, for the UIKit publishers we need.
Installing CombineCocoa with Swift Package Manager
Adding a dependency via Apple‘s built in Swift Package Manager is quite easy. In Xcode, simply select File → Swift Packages → Add Package Dependency. There, you will be prompted to enter a package repository url where you can add the CombineCocoa GitHub url. Selecting next will default to the latest release tag from the repository. After confirming the package options you should now see the dependency added in the project navigator:
Adding Combine publishers
We did a lot of customization with the text field subclass we created. Let‘s address event handling and responses to the right view button.
Since CombineCocoa is a wrapper around UIKit, there are built in publishers we can use. We‘ll focus on textPublisher for text changes in the the text field and controlEventPublisher(for:) for control events from the button.
These publishers can be added at initialization:
Since the publishers are exclusive to private properties we‘ll need another one for accessing the selected state of the button. CombineCocoa’s ControlProperty property publisher offers a very useful solution for this. We could initialize this generic publisher with a specific control event and key path:
Now that we‘ve included the rightAccessoryButton‘s publisher we need to subscribe to the values it emits. In the DemoViewController this can be set up at viewDidLoad():
Adding @IBInspectable
After all these additions to customize the UITextField subclass there is another option to extend these properties to Interface Builder. To better visualize this in Interface Builder, let‘s mark the properties as @IBInspectable. This will allow us to edit those properties in the attributes inspector in Interface Builder. In code, that looks like this:
And in Interface Builder, it should look like this:
Since we have all the inspectables set up in Interface Builder, we can go a step further and have those values set up as the default view model, and then take a look at what the project‘s Interface Builder generates from the inspectables:
Running the project without having modified the view controller will reveal that the properties have been set directly from the Interface Builder:
Summary
Being able to set up inspectable properties directly in Interface Builder allows for more flexibility and clarity when composing reusable views. The source of truth in the view model also allows us to set default properties. In this post, we learned how simple it is to set up a chain of Combine publishers. We also leveraged an older feature like IBInspectable and paired it with a newer technology like Combine. We hope this provides useful insight on how to implement these reactive changes in your existing apps.