…and I think I’m gonna need a bigger shovel.
Adapted from Pete Cheslock’s graphic, which I found I found via Leigh Honeywell.
…and I think I’m gonna need a bigger shovel.
Adapted from Pete Cheslock’s graphic, which I found I found via Leigh Honeywell.
My articles on working with dates and times in Swift have steadily been getting more readers, so I thought I’d gather all the links to them in one place and make them easier to find. I’m also in the process of gathering the content of these articles and pulling them together into a date/time utility module, which I’ll post on GitHub. I’ll let you know when that happens!
In the meantime, my “How to work with dates and times in Swift” articles (so far):
A very brief introduction to date formatting in Swift and iOS: The oversight in a mostly-good book on Swift programming led me down the path of writing articles about dates and times in Swift, starting with this one, where I look at NSDateFormatter
.
How to work with dates and times in Swift, part one: An introduction of Cocoa’s date and time classes, and how they work together. This article covers UTC (Coordinated Universal Time), and the key classes: NSDate
, NSCalendar
, NSDateComponents
.
How to work with dates and times in Swift, part two: Calculations with dates: Now that we’ve got the basics, it’s time to do some date arithmetic: comparing two dates to see which one is the earlier and later one, finding out how far apart two dates are, and adding and subtracting from dates.
How to work with dates and times in Swift, part three: Making date arithmetic more Swift-like: Cocoa’s date and time classes have an Objective-C heritage, which in the Swift context, feel kind of clunky. In this article, I look at ways — and by ways, I mean helper functions and class extensions — to make date calculations feel more like Swift.
How to work with dates and times in Swift, part four: A more Swift-like way to get the time interval between two dates: This quick article shows you how to make an operator overload that makes getting the time interval between two dates more like subtraction.
Click the chart to see it at full size.
If you have one of these — often called a “DSL modem”, but it’s more likely to be a DSL router — in your home or office…
…then you have this engineer to thank:
This is Joseph Lechleider, who used to be an engineer at Bellcore, Bell Telephone’s research wing formed after it was broken up into the “Baby Bells”. In the late 1980s, he came up with a clever solution to a problem that came with trying to send high-speed electronic signals over ordinary copper wire and into this familiar connection:
Electricity and magnetism are bound together in interesting ways, and this connection turns up when you send a current through a wire. The current induces a magnetic field around the wire:
The tight connection between electricity and magnetism means that the reverse is true as well: put a wire into a magnetic field, and you’ll induce a current in it.
This behavior made it difficult to send high-speed signals through copper wires, which to this day often make up the “last mile” between your wireline carrier and you. The magnetic fields induced by sending signals at very high speeds create a lot of interference, which mangles the data being carried, which slows down the rate at which you receive actual usable data.
Lechleider’s solution to this problem was both simple and clever: why not make it so that downloading (getting information from the network to you) was faster than uploading (sending information from you to the network)? This arrangement reduced the interference between the upload and download channels and offered more than twice the bandwidth that transmitting and receiving at the same rate did. His idea got incorporated into what we now know as ADSL (the “A” is for asymmetric), a key part of DSL service
The “receive data much faster than you transmit it” solution is an engineer’s dream: it’s clever, and it’s cheap, because it doesn’t require much change to existing infrastructure. It meant that phone companies didn’t have to spend a lot of money to provide broadband service, and it’s how DSL became popular. As recently as the end of 2012, it was the primary means of access for almost 60% of the world’s wireline broadband subscribers.
Lechleider died at home in Philadelphia on April 18th from cancer of the esophagus. We didn’t want his contribution to telecommunications to go unnoticed.
For your work in changing wireline broadband — as well as the way we live, work, and play — we thank you, Mr. Lechleider.
It’s been over two years since I published my visual catalog of iOS’ virtual keyboards on the iPhone, which covered iOS 6. At the time, it was the only place you could get a listing of the names of the iOS keyboards and see what they looked like. Even now, I think it’s still the only information source of its kind.
We’re now well into iOS 8’s run, and it’s likely that iOS 9 will be released sometime this year. Since there still isn’t a visual catalog of iOS 8’s built-in keyboards on the iPhone, and since I’m still of the “see a need, fill a need” ethos, I put one together. It starts with a table listing all the built-in keyboard types; you can click on the name to jump to pictures of all the views for the corresponding keyboard.
I hope you find it useful!
UIKeyboardType |
Based on | Emoji entry? | The official description, straight from Apple’s documentation |
ASCIICapable |
Typewriter | No | Use a keyboard that displays standard ASCII characters. |
DecimalPad |
Phone | No | Use a keyboard with numbers and a decimal point. |
Default |
Typewriter | Yes | Use the default keyboard for the current input method. |
EmailAddress |
Typewriter | Yes | Use a keyboard optimized for specifying email addresses. This type features the “@”, “.” and space characters prominently. |
NamePhonePad |
Typewriter and phone | Yes | Use a keypad designed for entering a person’s name or phone number. This keyboard type does not support auto-capitalization. |
NumberPad |
Phone | No | Use a numeric keypad designed for PIN entry. This type features the numbers 0 through 9 prominently. This keyboard type does not support auto-capitalization. |
NumbersAndPunctuation |
Typewriter | No | Use the numbers and punctuation keyboard. |
PhonePad |
Phone | No | Use a keypad designed for entering telephone numbers. This type features the numbers 0 through 9 and the “*” and “#” characters prominently. This keyboard type does not support auto-capitalization. |
Twitter |
Typewriter | Yes | Use a keyboard optimized for twitter text entry, with easy access to the @ and # characters. |
URL |
Typewriter | Yes | Use a keyboard optimized for URL entry. This type features “.”, “/”, and “.com” prominently. |
WebSearch |
Typewriter | Yes | Use a keyboard optimized for web search terms and URL entry. This type features the space and “.” characters prominently. |
iOS 8 features a total of 11 built-in virtual keyboards, all of which are variations on two basic types: the typewriter keyboard and the phone keypad, both pictured below:
Typically, the keyboard isn’t shown until the user taps on a text field or text view. You can specify the type of keyboard for a text field or text view visually in Interface Builder…
…or you can do so in code using the text field or text view’s keyboardType
property and values from the UIKeyboardType
enum:
vowelsOnlyTextField.keyboardType = UIKeyboardType.ASCIICapable noVowelsTextField.keyboardType = UIKeyboardType.Default digitsOnlyTextField.keyboardType = UIKeyboardType.NumberPad numericOnlyTextField.keyboardType = UIKeyboardType.NumbersAndPunctuation positiveIntegersOnlyTextField.keyboardType = UIKeyboardType.DecimalPad
If you don’t specify a keyboard type for a text field or text view, the typewriter-based Default keyboard is used.
Thanks to their growing popularity (first in Japan, and then the rest of the world), many of these keyboards feature a way to enter emoji, which have been available on iOS since version 5, which was released in mid-2011:
All of iOS’ virtual keyboards come in both a light and dark appearance, with the light version being the default:
As with the keyboard type, you can choose this appearance either visually in Interface Builder (via the Appearance menu) and in code using the text field or text view’s keyboardAppearance
property and values from the UIKeyboardAppearance
enum.
I haven’t been able to find anything in the Human Interface Guidelines on when the light and dark keyboards should or shouldn’t be used, so let your common sense be your guide, keeping in mind this fact about common sense:
Click the image to enjoy Deadpool’s wisdom at full size.
UIKeyboardType.ASCIICapable
This used to be the default keyboard; now it’s the “no-nonsense alphanumeric keyboard”. It lets you enter letters, numbers, punctuation, and symbols, and nothing else. Use this keyboard if you need the user to enter something that’s completely or primarily letters and don’t want them to enter emoji and especially for data entry in business, productivity, scientific, health, and other “serious” apps.
Primary view (accessed via the ABC key):
Alternate view 1 (accessed via the 123 key):
Alternate view 2 (accessed via the #+= key):
UIKeyboardType.DecimalPad
Use this keyboard if you want the user to enter either whole or fractional numbers. This keyboard provides the decimal separator appropriate for the user’s locale; the screenshots below show my locale settings, English/US. Note that there’s no return key of any sort on this keyboard.
UIKeyboardType.Default
If you don’t specify the keyboard type, you get this one, which lets the user enter letters, numbers, punctuation and symbol characters, and emoji. This is the most general-purpose keyboard in iOS.
Primary view (accessed via the ABC key):
Alternate view 1 (accessed via the 123 key):
Alternate view 2 (accessed via the #+= key):
Alternate view 3 (accessed via the the 😀 key):
UIKeyboardType.EmailAddress
If you want the user to provide an email address, this is the preferred keyboard.
Primary view (accessed via the ABC key):
Alternate view 1 (accessed via the 123 key):
Alternate view 2 (accessed via the #+= key):
Alternate view 3 (accessed via the the 😀 key):
UIKeyboardType.NamePhonePad
For fields where you want the user to enter data without punctuation — typically names or integers — use the NamePhonePad
keyboard. It features typewriter-style input for entering letters, and phone-style input for entering numbers.
Primary view (accessed via the ABC key):
Alternate view 1 (accessed via the 123 key):
Alternate view 2 (accessed via the the 😀 key):
UIKeyboardType.NumberPad
This is amother numeric entry keyboard. It lets the user enter digits, but not the decimal separator.
Note that there’s no return key of any sort on this keyboard.
Primary view:
UIKeyboardType.NumbersAndPunctuation
Think of this as the ASCIICapable
keyboard, but with an emphasis on numeric entry. Like the ASCIICapable
keyboard, it lets you enter letters, numbers, punctuation, and symbols, and nothing else.
Use this keyboard if you need the user to enter something that’s primarily numeric but may also include letters, symbols and punctuation, but not emoji. You’ll find it especially useful for numeric data entry in business, productivity, scientific, health, and other “serious” apps. You should also use this keyboard when you want the user to enter numbers in scientific notation.
Primary view (accessed via the 123 key):
Alternate view 1 (accessed via the ABC key):
Alternate view 2 (accessed via the #+= key):
UIKeyboardType.PhonePad
If you want the user to enter a phone number or a touch-tone sequence, this is the preferred keyboard.
Primary view (accessed via the 123 key):
Alternate view 2 (accessed via the +*# key):
UIKeyboardType.Twitter
If you want the user to enter a tweet, this is the preferred keyboard.
Primary view (accessed via the ABC key):
Alternate view 1 (accessed via the 123 key):
Alternate view 2 (accessed via the #+= key):
Alternate view 3 (accessed via the the 😀 key):
UIKeyboardType.URL
If you want the user to enter a URL, this is the preferred keyboard.
Primary view (accessed via the ABC key):
Alternate view 1 (accessed via the 123 key):
Alternate view 2 (accessed via the #+= key):
Alternate view 3 (accessed via the the 😀 key):
UIKeyboardType.WebSearch
This keyboard is very similar to the Default
keyboard, with two notable differences:
It’s meant for entering both URLs and web search terms, but it’s equally useful for any kind search or even typed commands.
Primary view (accessed via the ABC key):
Alternate view 1 (accessed via the 123 key):
Alternate view 2 (accessed via the #+= key):
Alternate view 3 (accessed via the the 😀 key):
The material in this article is still applicable, but you’ll also want to read a newer one titled A better way to program iOS text fields that have maximum lengths and accept or reject specific characters, which shows you how to make text fields that let you specify the following in Interface Builder or using less code:
I’m already using that material in a couple of projects, and I think you’ll find it handy, too. Check it out!
And now, the original article…
Click the image to see it at full size.
Update, August 26, 2015: I’ve updated this article so that its code works with Swift 2. It compiles under the latest version of Xcode 7, beta 6.
A little while back, I published an article that covered constraining text fields so that they accepted only values that evaluated to numeric ones, and limited them to a specified maximum number of characters (don’t bother looking for it; it redirects to this article now). This article expands and improves on it by showing you how to create iOS text fields that:
In order to demonstrate this, I’ve created a quick sample app, ConstrainedTextFieldDemo. You can download it here [90K Xcode project and associated files, zipped]. When you run it, you’ll the screen pictured above. It contains a set of text fields, each one with its own set of constraints:
In this article, I’ll walk you through the app and show you how to create your own constrained text fields in iOS. Better still, I’ll give you the project files so that you can experiment with the app.
Before we get into the explanations, let me cut to the chase and just give you the code.
For the purposes of discussing constrained text fields, we need to consider only two files:
Here’s ViewController.swift:
// // ViewController.swift // import UIKit class ViewController: UIViewController, UITextFieldDelegate { // MARK: Outlets @IBOutlet weak var vowelsOnlyTextField: UITextField! @IBOutlet weak var noVowelsTextField: UITextField! @IBOutlet weak var digitsOnlyTextField: UITextField! @IBOutlet weak var numericOnlyTextField: UITextField! @IBOutlet weak var positiveIntegersOnlyTextField: UITextField! // MARK: View events and related methods override func viewDidLoad() { super.viewDidLoad() initializeTextFields() } // Designate this class as the text fields' delegate // and set their keyboards while we're at it. func initializeTextFields() { vowelsOnlyTextField.delegate = self vowelsOnlyTextField.keyboardType = UIKeyboardType.ASCIICapable noVowelsTextField.delegate = self noVowelsTextField.keyboardType = UIKeyboardType.ASCIICapable digitsOnlyTextField.delegate = self digitsOnlyTextField.keyboardType = UIKeyboardType.NumberPad numericOnlyTextField.delegate = self numericOnlyTextField.keyboardType = UIKeyboardType.NumbersAndPunctuation positiveIntegersOnlyTextField.delegate = self positiveIntegersOnlyTextField.keyboardType = UIKeyboardType.DecimalPad } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } // Tap outside a text field to dismiss the keyboard // ------------------------------------------------ // By changing the underlying class of the view from UIView to UIControl, // the view can respond to events, including Touch Down, which is // wired to this method. @IBAction func userTappedBackground(sender: AnyObject) { view.endEditing(true) } // MARK: UITextFieldDelegate events and related methods func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { // We ignore any change that doesn't add characters to the text field. // These changes are things like character deletions and cuts, as well // as moving the insertion point. // // We still return true to allow the change to take place. if string.characters.count == 0 { return true } // Check to see if the text field's contents still fit the constraints // with the new content added to it. // If the contents still fit the constraints, allow the change // by returning true; otherwise disallow the change by returning false. let currentText = textField.text ?? "" let prospectiveText = (currentText as NSString).stringByReplacingCharactersInRange(range, withString: string) switch textField { // Allow only upper- and lower-case vowels in this field, // and limit its contents to a maximum of 6 characters. case vowelsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 6 // Allow any characters EXCEPT upper- and lower-case vowels in this field, // and limit its contents to a maximum of 8 characters. case noVowelsTextField: return prospectiveText.doesNotContainCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 8 // Allow only digits in this field, // and limit its contents to a maximum of 3 characters. case digitsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("0123456789") && prospectiveText.characters.count <= 3 // Allow only values that evaluate to proper numeric values in this field, // and limit its contents to a maximum of 7 characters. case numericOnlyTextField: return prospectiveText.isNumeric() && prospectiveText.characters.count <= 7 // In this field, allow only values that evalulate to proper numeric values and // do not contain the "-" and "e" characters, nor the decimal separator character // for the current locale. Limit its contents to a maximum of 5 characters. case positiveIntegersOnlyTextField: let decimalSeparator = NSLocale.currentLocale().objectForKey(NSLocaleDecimalSeparator) as! String return prospectiveText.isNumeric() && prospectiveText.doesNotContainCharactersIn("-e" + decimalSeparator) && prospectiveText.characters.count <= 5 // Do not put constraints on any other text field in this view // that uses this class as its delegate. default: return true } } // Dismiss the keyboard when the user taps the "Return" key or its equivalent // while editing a text field. func textFieldShouldReturn(textField: UITextField) -> Bool { textField.resignFirstResponder() return true; } }
I gave the outlets for the text fields sensible names, but I thought that it might be helpful to show you an annotated storyboard that points out which outlet belongs to which text field:
The code in the view controller calls on some string utility methods that I decided to put into their own module: the StringUtils.swift file:
// // StringUtils.swift // import Foundation extension String { // Returns true if the string has at least one character in common with matchCharacters. func containsCharactersIn(matchCharacters: String) -> Bool { let characterSet = NSCharacterSet(charactersInString: matchCharacters) return self.rangeOfCharacterFromSet(characterSet) != nil } // Returns true if the string contains only characters found in matchCharacters. func containsOnlyCharactersIn(matchCharacters: String) -> Bool { let disallowedCharacterSet = NSCharacterSet(charactersInString: matchCharacters).invertedSet return self.rangeOfCharacterFromSet(disallowedCharacterSet) == nil } // Returns true if the string has no characters in common with matchCharacters. func doesNotContainCharactersIn(matchCharacters: String) -> Bool { let characterSet = NSCharacterSet(charactersInString: matchCharacters) return self.rangeOfCharacterFromSet(characterSet) == nil } // Returns true if the string represents a proper numeric value. // This method uses the device's current locale setting to determine // which decimal separator it will accept. func isNumeric() -> Bool { let scanner = NSScanner(string: self) // A newly-created scanner has no locale by default. // We'll set our scanner's locale to the user's locale // so that it recognizes the decimal separator that // the user expects (for example, in North America, // "." is the decimal separator, while in many parts // of Europe, "," is used). scanner.locale = NSLocale.currentLocale() return scanner.scanDecimal(nil) && scanner.atEnd } }
Let’s take a closer look at the code…
The Delegate pattern is a fundamental part of iOS app development. You’ll encounter it often when programming user interfaces, including those times when you want to your program to react to what the user does with text fields.
The delegate pattern involves two categories of object:
My pet analogy for the delegate pattern is pictured above: an airplane and air traffic control. Unlike the driver of a car, who’s controlled only by traffic signals and pretty much free to choose any destination and route s/he pleases, the pilot of a plane has to delegate a lot of those choices to air traffic control. The airplane, which does the actual flying, is the delegator, and air traffic control, which gives clearance for takeoff and landing and tells the plane the heading, speed, and altitude at which it should fly, is the delegate.
If you look at the delegate pattern in Wikipedia, you’ll see that there are a number of ways to implement it. Here’s how it’s done in iOS (and Cocoa), whether you’re doing it in Objective-C or Swift:
There are three things in play:
Let’s make the above diagram a little more specific and talk about delegation in terms of iOS’ text fields:
iOS text fields — that is, instances of the UITextField
class — participate in a delegate pattern as delegators. They’ve got the power to control what happens when the user starts and stops editing their contents and what characters can be typed into them, but they offload the logic that handles those tasks to another object: the delegate.
A specific protocol, the UITextFieldDelegate
protocol, connects the text field and its delegate together. A protocol is simply a set of declarations of class members — instance properties, instance methods, type methods, operators, and subscripts. These instance properties, instance methods, type methods, operators, and subscripts are implemented in the delegate (implementing a protocol’s members is called adopting the protocol), and the delegator calls on these implemented members.
UITextFieldDelegate
Let’s look at the UITextFieldDelegate
protocol. You can actually check it out for yourself; the simplest way is to control-click or right-click on any occurrence of UITextField
in your code and then click on Jump to Definition in the contextual menu that appears:
You’ll be taken to UITextField.h, a header file that allows Swift to connect to the Objective-C code on which UITextField
is built. It contains the declarations for all the publicly-accessible parts of UITextField
, including the UITextFieldDelegate
protocol. You’ll find it near the end of the file. I’ve reproduced it below:
protocol UITextFieldDelegate : NSObjectProtocol { optional func textFieldShouldBeginEditing(textField: UITextField) -> Bool // return NO to disallow editing. optional func textFieldDidBeginEditing(textField: UITextField) // became first responder optional func textFieldShouldEndEditing(textField: UITextField) -> Bool // return YES to allow editing to stop and to resign first responder status. NO to disallow the editing session to end optional func textFieldDidEndEditing(textField: UITextField) // may be called if forced even if shouldEndEditing returns NO (e.g. view removed from window) or endEditing:YES called optional func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool // return NO to not change text optional func textFieldShouldClear(textField: UITextField) -> Bool // called when clear button pressed. return NO to ignore (no notifications) optional func textFieldShouldReturn(textField: UITextField) -> Bool // called when 'return' key pressed. return NO to ignore. }
ViewController
In order to become a delegate, a class has to adopt the protocol. If you’re familiar with languages like C# and Java, “adopting a protocol” is similar to “implementing an interface”: we add the protocol to a class’ definition, as if we’re inheriting it. In this case, we’ll have the view controller adopt the protocol:
class ViewController: UIViewController, UITextFieldDelegate {
This says that the ViewController
class inherits from the UIViewController
class and adopts the UITextFieldDelegate
protocol. Having the view controller act as the delegate makes sense: it controls the user interface, and the text fields are part of the user interface.
Just as you have to implement the methods in an inherited interface in C# and Java, you have to implement the methods in an adopted protocol in Swift. There is a difference, however: in Swift, you can choose not to implement methods marked as optional
.
You may have noticed that all the methods in the UITextFieldDelegate
protocol are optional
. This means that a delegate that adopts the protocol can implement as many or as few of its methods as necessary. For the purposes of our app, we’re implementing two of them in ViewController
:
textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String)
-> Bool
: The text field calls this whenever the user tries to change the contents of a text field, whether by typing in or deleting a character, or by cutting or pasting. The method should return true
if the change is to be accepted, and false
to reject the change and keep the contents of the text field the way they are. We’ll use it to limit the types of character that can be entered into the view’s text fields and set a maximum the number of characters that can be entered for each field.textFieldShouldReturn(textField: UITextField)
-> Bool
: The text field calls this whenever the user taps the Return key or its equivalent on the keyboard. We’ll use it to dismiss the keyboard when the user taps Return.We’ll talk about the implementation of these methods in the next section. We have to take care of the delegators first.
We’ve got a protocol, and we’ve got a delegate that adopts it. Now we need to set up the delegators, which in this case, are the text fields — we need to tell them who their delegates are. We do this by setting each text field’s delegate property in the initializeTextFields
method of the ViewController
class:
// Designate this class as the text fields' delegate // and set their keyboards while we're at it. func initializeTextFields() { vowelsOnlyTextField.delegate = self vowelsOnlyTextField.keyboardType = UIKeyboardType.ASCIICapable noVowelsTextField.delegate = self noVowelsTextField.keyboardType = UIKeyboardType.ASCIICapable digitsOnlyTextField.delegate = self digitsOnlyTextField.keyboardType = UIKeyboardType.NumberPad numericOnlyTextField.delegate = self numericOnlyTextField.keyboardType = UIKeyboardType.NumbersAndPunctuation positiveIntegersOnlyTextField.delegate = self positiveIntegersOnlyTextField.keyboardType = UIKeyboardType.DecimalPad }
By setting all the text fields’ delegate
properties to self
, we’re saying that this class is their delegate. Any events arising from editing the text fields will be handled by this class.
The magic that constrains a text field so that it’s vowels-only, numbers-only and so on happens inside the protocol method with the very long signature, textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
. This method needs to be coded so that it returns true
if we want the user’s changes to be accepted (which then updates the text field), or false
if we don’t want the user’s changes to be accepted (which leaves the text field unchanged).
Here’s its code:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { // We ignore any change that doesn't add characters to the text field. // These changes are things like character deletions and cuts, as well // as moving the insertion point. // // We still return true to allow the change to take place. if string.characters.count == 0 { return true } // Check to see if the text field's contents still fit the constraints // with the new content added to it. // If the contents still fit the constraints, allow the change // by returning true; otherwise disallow the change by returning false. let currentText = textField.text ?? "" let prospectiveText = (currentText as NSString).stringByReplacingCharactersInRange(range, withString: string) switch textField { // Allow only upper- and lower-case vowels in this field, // and limit its contents to a maximum of 6 characters. case vowelsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 6 // Allow any characters EXCEPT upper- and lower-case vowels in this field, // and limit its contents to a maximum of 8 characters. case noVowelsTextField: return prospectiveText.doesNotContainCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 8 // Allow only digits in this field, // and limit its contents to a maximum of 3 characters. case digitsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("0123456789") && prospectiveText.characters.count <= 3 // Allow only values that evaluate to proper numeric values in this field, // and limit its contents to a maximum of 7 characters. case numericOnlyTextField: return prospectiveText.isNumeric() && prospectiveText.characters.count <= 7 // In this field, allow only values that evalulate to proper numeric values and // do not contain the "-" and "e" characters, nor the decimal separator character // for the current locale. Limit its contents to a maximum of 5 characters. case positiveIntegersOnlyTextField: let decimalSeparator = NSLocale.currentLocale().objectForKey(NSLocaleDecimalSeparator) as! String return prospectiveText.isNumeric() && prospectiveText.doesNotContainCharactersIn("-e" + decimalSeparator) && prospectiveText.characters.count <= 5 // Do not put constraints on any other text field in this view // that uses this class as its delegate. default: return true } }
This method takes three parameters:
textField
: the text field that either had a character added to or removed from it.range
: the range of the characters within the text field that are to be replaced.string
: the replacement string.The first thing the method does is see if the change adds characters to the text field:
if string.characters.count == 0 { return true }
string
is non-empty and has a length greater than zero. In this case, we’ll want to do more processing.string
is empty and has a length of zero. In this case, we don’t want to do any more processing; removing characters means we don’t have to see if we want to disallow any added character or if the maximum number of characters for the text field has been exceeded. We’ll just exit the method, returning true so that the change still happens, whether it’s a deletion, a cut, or moving the insertion point.We want to figure out what the text field would contain if the change were allowed. We’ll call that the prospective text, which we’ll assign to a local constant called prospectiveText
. We can figure out what the prospective text is by using NSString
‘s stringByReplacingCharactersInRange
method on the contents of textField.text.
Here’s where we run into a problem:
NSString
‘s stringByReplacingCharactersInRange
method, we need to convert a Swift String
into an NSString
.text
property type isn’t String
, but String?
. That’s because a text field’s value can either be:
nil
when it’s emptyString
can be cast into NSString
; String?
can’t.To get around this problem, we’re going to create a String
constant called currentText
, which we’ll fill as follows:
nil
— we’ll simply assign currentText
the value of textField.text
.nil
— we’ll assign currenttext
the value ""
, the empty string. There’s a difference between nil
(which denotes no value) and the empty string (which is a value, just one that has a length of 0 characters).Here’s the code:
let currentText = textField.text ?? "" let prospectiveText = (currentText as NSString).stringByReplacingCharactersInRange(range, withString: string)
As we’ll see shortly, having prospectiveText
lets us set a maximum number of characters that can be put into a text field.
Now that we’ve dealt with cases where the change to the text field deletes characters and have created prospectiveText
, we can now start constraining text fields. This is handled in the switch
statement, which we use to separate the constraining logic for each text field:
switch textField { // Allow only upper- and lower-case vowels in this field, // and limit its contents to a maximum of 6 characters. case vowelsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 6 // Allow any characters EXCEPT upper- and lower-case vowels in this field, // and limit its contents to a maximum of 8 characters. case noVowelsTextField: return prospectiveText.doesNotContainCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 8 // Allow only digits in this field, // and limit its contents to a maximum of 3 characters. case digitsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("0123456789") && prospectiveText.characters.count <= 3 // Allow only values that evaluate to proper numeric values in this field, // and limit its contents to a maximum of 7 characters. case numericOnlyTextField: return prospectiveText.isNumeric() && prospectiveText.characters.count <= 7 // In this field, allow only values that evalulate to proper numeric values and // do not contain the "-" and "e" characters, nor the decimal separator character // for the current locale. Limit its contents to a maximum of 5 characters. case positiveIntegersOnlyTextField: let decimalSeparator = NSLocale.currentLocale().objectForKey(NSLocaleDecimalSeparator) as! String return prospectiveText.isNumeric() && prospectiveText.doesNotContainCharactersIn("-e" + decimalSeparator) && prospectiveText.characters.count <= 5 // Do not put constraints on any other text field in this view // that uses this class as its delegate. default: return true }
The case
s for each text field are listed in the order in which they appear onscreen. Let’s look at them one by one:
In this text field, we want the user to be able to enter only vowels — the upper- and lower-case versions of the letters a, e, i, o, and u. We also want to limit its contents to a maximum of 6 characters. Here’s the code that does this:
// Allow only upper- and lower-case vowels in this field, // and limit its contents to a maximum of 6 characters. case vowelsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 6
This code makes use of the String
extension method containsOnlyCharactersIn
, which I defined in StringUtils.swift. It returns true
if the String
contains only characters in the provided parameter.
If both conditions in the return statement evaluate to true
— prospectiveText
contains only vowels and has 6 characters or fewer — the method returns true
, the change to the text field is allowed, and the text field is updated. If both conditions don’t evaluate to true
, the method returns false
, the change to the text field is not allowed, and the text field’s contents remain the same.
In this text field, we want the user to be able to enter any character except vowels and limit its contents to a maximum of 8 characters. Here’s the code that does this:
// Allow any characters EXCEPT upper- and lower-case vowels in this field, // and limit its contents to a maximum of 8 characters. case noVowelsTextField: return prospectiveText.doesNotContainCharactersIn("aeiouAEIOU") && prospectiveText.characters.count <= 8
This code is similar to the code for the “Vowels only” text field. The major difference is that it makes use of another String
extension method defined in StringUtils.swift: doesNotContainCharactersIn
, which returns true
if the String
doesn’t contain any of the characters in the provided parameter.
In this text field, we want the user to be able to enter only digits, and no more than three of them at most. Here’s the code that does this:
// Allow only digits in this field, // and limit its contents to a maximum of 3 characters. case digitsOnlyTextField: return prospectiveText.containsOnlyCharactersIn("0123456789") && prospectiveText.characters.count <= 3
This code is almost the same as the code for the “Vowels only” text field.
Here’s an interesting one: a text field that accepts only user input that evaluates to a proper numeric value. That means it will accept the following characters:
Even when limiting the user to these characters, it’s possible for non-numeric values to be entered. Here are a couple of example non-numeric values that we don’t want the user to be able to enter:
We also want to limit the text field to a maximum of 7 characters.
Here’s the code:
// Allow only values that evaluate to proper numeric values in this field, // and limit its contents to a maximum of 7 characters. case numericOnlyTextField: return prospectiveText.isNumeric() && prospectiveText.characters.count <= 7
This code makes use of the String
extension method isNumeric
, which I defined in StringUtils.swift. It returns true
if the String
contains a value that evaluates to a numeric value. It’s powered by NSScanner
, a class that’s handy for going through strings and extracting useful data from them, and its scanDecimal
method, which returns true if it finds a value that can be evaluated as an NSDecimal
value.
If both conditions in the return statement evaluate to true
— prospectiveText
evaluates to a numeric value and has 7 characters or fewer — the method returns true
, the change to the text field is allowed, and the text field is updated. If both conditions don’t evaluate to true
, the method returns false
, the change to the text field is not allowed, and the text field’s contents remain the same.
This is a more strict version of the “Numeric values only” text field. It requires that anything entered into it needs to evaluate as a proper numeric value, but it also requires that the value be a positive integer and not be stated in scientific notation. It has a maximum length of 5 characters. Here’s the code:
// In this field, allow only values that evalulate to proper numeric values and // do not contain the "-" and "e" characters, nor the decimal separator character // for the current locale. Limit its contents to a maximum of 5 characters. case positiveIntegersOnlyTextField: let decimalSeparator = NSLocale.currentLocale().objectForKey(NSLocaleDecimalSeparator) as! String return prospectiveText.isNumeric() && prospectiveText.doesNotContainCharactersIn("-e" + decimalSeparator) && prospectiveText.characters.count <= 5
In order to disallow negative numbers, we use the String extension method doesNotContainCharactersIn to block out – characters. We disallow scientific notation by using the same method to block out e characters. The tricky part is disallowing the decimal separator, which can be either . or , depending on the user’s locale. We identify it with NSLocale.currentLocale().objectForKey(NSLocaleDecimalSeparator), which we add to the parameter for doesNotContainCharactersIn.
Finally, we handle the default case: any other text fields that might be on the screen, which we won’t constrain:
default: return true
This app has a couple of UI features that I’ll cover in a subsequent article:
It’s all in the code, so if you’d like to jump in and learn how it works on your own, go right ahead!
In case you missed it, here are the zipped project files for the demo project, ConstrainedTextFieldDemo [90K Xcode project and associated files, zipped].
Tulsa-based KTUL’s news team reports that two roommates are in the hospital after a drunken “Which is better, iPhone or Android?” argument escalated into a fight with improvised knives.
Both men smashed their beer bottles action movie-style and stabbed each other. One of them even smashed his bottle on the back of the other’s head (which to my mind sounds like the kind of cheap move a “Fandroid” would pull*).
* I’m kidding, Android fans. I myself own a Moto G (2nd generation). Please don’t stab me.
Some thoughts I had after reading the article:
If you’re the kind of person whose identity is so tied up with a smartphone platform that you can’t have an argument about your favorite one without getting into a murderous brawl, you might want to invest in a case with an integrated blade, like the TaskOne G3, pictured below:
Thanks to Ethan Henry for the find!