iOS Text Field Built-in Animated Character Counter

Working on my recent App, I needed to limit UITextField text length.

There is a straight-forward way for this with a few lines of code in UITextFieldDelegate method ShouldReturnCharacterInRange:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    guard let text = textField.text else { return true }

let newLength = text.characters.count + string.characters.count
- range.length
return newLength <= yourTextLimit
}

We will go through this code line by line later.

This will do the magic, but the iOS App is all about the User Experience. From the user standpoint, this approach doesn’t provide any feedback. It also raises a few questions:
1) Why I cannot enter more characters?
2) What is the character limit?
3) How many more characters can I add?

So, how can we make this more interactive?

In this tutorial I will show you how to integrate an interactive character counter right inside your text field. At the end, you will have this nice animated counter:


Let’s begin.

Part 1: Basic set-up

First, start a new project.

Create a new class as a subclass of UITextField.

It’s always a good practice to make your classes re-usable, so subclassing will be a good choice for this reason.

If you are a storyboard-type-iOS-developer, or if you want your class to be accessible from the Interface Builder, you should add the @IBDesignable keyword before the class declaration:

@IBDesignable class DRHTextFieldWithCharacterCount: UITextField {

}

We need to display the character count, so go ahead and create a UILabel.

private let countLabel = UILabel()
Another good iOS development practice is to create all your properties and methods as private, unless you need to use them from outside of the class (in this case, you can always change it back to public).

Create a few more properties, that will define the counter appearance. Add @IBInspectable, if you want to access this properties from the Interface Builder:

@IBInspectable var lengthLimit: Int = 0
@IBInspectable var countLabelTextColor: UIColor = UIColor.blackColor()
If you want access the @IBInspectable properties in the Interface Builder, make sure you explicitly set the property type. Due to the type inference, compiler will not give you any warnings if you don’t, but you will not see this properties in the Interface Builder.

We will use lengthLimit to set our limit either from the Interface Builder constructor, or by accessing this property in code.

countLabelTextColor will set the default counter text color to black.

We are ready to add new label to our project. Go to your storyboard and add a new UILabel. Use AutoLayout to position it right in the center of the screen, and set a width constraint to 150:

Switch to Identity Inspector and change the label class to our custom DRHTextFieldWithCharacterCount:

Go back to Attributes Inspector, and set the Length Limit and Count Label Text Color:

That’s all we need to do in the storyboard, so go back to your custom class file.

Next, we need to setup our label and display it inside the UITextField.


Part 2: Setting the Counter Label

Create a method inside your class:

private func setCountLabel() {

}

We are going to use the rightView to display our label. RightView is an existing UITextField subview, that is located on the right side by default.

First, we make this view visible:

rightViewMode = .Always

In this example, we want to display the count label at all the time (if you don’t want to show the label when the text field is not active, simply change the mode to WhileEditing).

Set the default counter label text font. For this project, we will set it to 10:

countLabel.font = font?.fontWithSize(10)

Set the counter label text color with user-defined property:

countLabel.textColor = countLabelTextColor

Align the text on the left side of the label:

countLabel.textAlignment = .Left

By default, the rightView is nil, so we need to initialize it with our label:

rightView = countLabel
How can we initialize a UIView (rightView) with a UILabel (counterLabel)? UILabel is a subclass in UIView, so this statement is totally valid.

Now, we need to set the initial counter text. Add the following line of code below the last statement:

countLabel.text = initialCounterValue(text)

This will give you a warning, that initialCounterValue() does not exist. Go ahead and create this method outside of the current method:

private func initialCounterValue(text: String?) -> String {
}

Part 3: Counting Characters of the String

Lets go back to the preview and see how our count label should look like:

We need to compose the String in the following format: currentCount/Limit.

The current character count is noting else but the length of the text String in our text field. But how can we get the character count?

The obvious way is to access the set of characters in the String and get their count:

let length = myString.characters.count

Another way is to acess the UTF16 characters count:

let length = myString.utf16.count
What’s the difference? If you want a detail answer, you can check the Swift Language Reference.

Making it simple, UTF16.count returns the number of Unicode characters, while the characters.count returns the number of 16 bits units.

Making it super-simple, consider the following example:

let emojiString = “🎬”
print(emojiString.characters.count) // output: 1
print(emojiString.utf16.count) // output: 2

Applying this to our project, the difference is following: while both this methods will work the same way, when you type (because you type character by character), it will give you a different result, when you paste an emoji or another Unicode symbol in the text field.

Since we need an exact character count, we will use the UTF16 method.

Go back to your initialCounterValue method declaration: it takes an optional String as a parameter, so in order to access it’s value, we need to safe-unwrap it. Add the following code inside initialCounterValue method:

if let text = text {

}
This is one of the top must-follow-swift-rules: never force-unwrap optionals. Basically, if you see an explanation mark somewhere in your code, it’s probably a bad sign. Try to avoid it by using an optional chaining or guard statements. We always want our code to be crash-safe, don’t we?

Having the unwrapped String value, we can configure the initial counter label text. If the text value is nil, we use value “zero” for counter:

if let text = text {
return "\(text.utf16.count)/\(lengthLimit)"
} else {
return "0/\(lengthLimit)"
}

The last step of configuring the counter label will be calling the setCountLabel(). Add the following code after at the end of our class:

override func awakeFromNib() {
super.awakeFromNib()

setCountLabel()
}
AwakeFromNib is called right after the view was initialized, so this is the good place to setup the label appearance.

We don’t need to setup the label, if the lengthLimit is set to zero, so wrap this statement in a simple condition check:

override func awakeFromNib() {
super.awakeFromNib()

if lengthLimit > 0 {
setCountLabel()
}
}

We created the counter label and set its appearance and default value. If you build and run your project, you will see this picture:

Wait, where is the counter label we’ve just created?

As I mentioned before, the default value of rightView was nil, so its frame was also nil. To initialize the new frame we need to override the UITextField method rightViewRectForBounds. Add the following code at the end of your class:

override func rightViewRectForBounds(bounds: CGRect) -> CGRect {
if maxLength > 0 {
return CGRect(x: frame.width — 35, y: 0, width: 30, height:
30)
} else {
return CGRect()
}
}

Again, if the lengthLimit is zero, we don’t need to create a frame, so just return CGRect().

I set the frame 35 points left from the right side of superview, and made the frame 30 points wide. This left me 5 points padding in the right side, that we will need later for animation.

Build and run the project now, and it will behave as expected:

We almost there! Let’s go to the final part: check the current text length, show the count, limit it, and provide a visual feedback.


Part 4: Do the Magic!

As I mentioned at the very beginning of this tutorial, we need to override the UITextFieldDelegate method. So far, our class does not conform to UITextFieldDelegate, so we need to make a class extension:

extension DRHTextFieldWithCharacterCount: UITextFieldDelegate {
}
Why don’t we add UITextFieldDelegate at the beginning in the class declaration? This is another good practice: keep all delegate methods inside an appropriate class extensions.

Don’t forget to set a delegate to self, so our class can actually use this delegate. Add this code at the end of awakeFromNib() method:

delegate = self

We will override shouldChangeCharacterInRange method. It is called every time you enter or remove a character in text field, right before that character is displayed in the UITextField. This method returns a Boolean value, so you can think of it as of a simple on/off switch. If it returns true, the text field will display the changes. If it returns false, no changes will be displayed, no matter what you entered.

shouldChangeCharacterInRange will also be called if you paste a text in the textField

Add this method inside the class extension:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
}

Don’t worry about compiler error, it will be gone once we return a Boolean value from this method.

First, we place a guard statement to check, if there is any text in the textField. The following code goes inside this new method:

guard let text = textField.text else { 
return true
}

If guard statement fails, we return true, that means the current changes in textField are allowed. We also need to make sure that current lengthLimit is set to something different than zero. A good way is to use a swift dynamic filter (a “where clause”), so we can avoid another if-else statement:

guard let text = textField.text where lengthLimit != 0 else { 
return true
}

Next, calculate the length of the current text:

let newLength = text.utf16.count + string.utf16.count — range.length

Lets take a look inside this statement:

  • text.utf16.count is the current number of characters before you edit it,
  • string.utf16.count is the number of characters you are about to add
  • range.length is the number of characters you removed.

As long as the newLenght is within the limit, update the counter:

if newLength <= lengthLimit {
countLabel.text = “\(newLength)/\(lengthLimit)”
} else {
   // animation code will go here
}

We agreed, that it would be great to give the user a visual feedback when he reaches the character limit. For this purpose, we will create a simple pulse animation. Inside the else statement, add the following code:

// 1
UIView.animateWithDuration(0.1, animations: {
// 2
self.countLabel.transform = CGAffineTransformMakeScale(1.1, 1.1)
}, completion: { (finish) in
// 3
UIView.animateWithDuration(0.1) {
// 4
self.countLabel.transform = CGAffineTransformIdentity
}
})

Lets go through this animation code line by line:

(1) create a UIViewAnimation with the duration of 0.1 seconds

(2) animate the scale transformation of our counter label in both X and Y axis. As you remember, we set the counter label frame big enough to fit the scale-up animation.

(3) on the completion of the first animation, create another animation with the same duration

(4) animate the label to scale back to original size

For more information about this animations, refer to Swift Language Documentation

Finally, we need to return from the shouldChangeCharactersInRange() method with Boolean value. We only allow to update the textField if the current text length is within the limit. Add this line at the end of the method:

return newLength <= lengthLimit

This will return true for all Strings shorter than user-defined lengthLimit.


That’s it! Build and run the project and enjoy the animated counter label inside your text field!

You can find the full source code on my Github repository:

Here is my app that is using this feature:

Feel free to use this textField in your projects, or make a pull request if you have an idea how to improve it.

Thank you for reading! If you liked this article, please hit ‘Recommend’ (the ❤ button) so other people can read it too.