Using and customizing Text Input Controls
Since text input is a primary control for many applications, it’s especially important to become familiar with text-based controls and the mechanisms used to modify their behavior.
SwiftUI provides three main controllers you can use to capture input text:
TextField, which is generally used to capture a single line of text, as input ( name, e-mail address etc.). It can be used to capture large amounts of text, but it will turn the input text into one single line (it would drop all newline characters, list markers, text decorations, links etc.).
SecureField, which is generally used to capture confidential and private information. This field takes your unmodified input, but displayed a redacted version, by replacing characters with circles or stars. You should use SecureField to capture passwords, Card Verification Values etc.
TextEditor, used to capture paragraphs of data. You would typically use this control to capture notes in a notes application. With iOS26, the TextEditor control received a lot of extra functionality, allowing it to work with AttributedStrings. Unless you need to support older OS versions, you can use this control for a full Rich Text Editor, in native SwiftUI code.
UIKit/AppKit views wrapped in a UIViewRepresentable, such as UITextView and NSTextView, used to capture rich text data. Prior to iOS26 and MacOS26, you would use these controls to create rich text editors in SwiftUI.
All SwiftUI text input controls require a binding to a state property. The controls themselves provide the mechanisms your end-users need, in order to interact with that state property. As a result of this implementation detail, these controls provide you with the ability to easily validate and\or process the input, on the fly (for example, using the .onChange modifier). Additionally, the text controls provide enough customization options, to make them fit into most designs.
For context, login screens or user registration forms both use text input controls, grouped and styled appropriately, to take user information, such as user name and password, and pass it to some other component or service. Another very common use case is the payment details form, where users provide credit card information.
When connected to a debugger, both in the simulator and on a connected device, all text input controls can display errors and they can block the interface when they load. In general, these issues are safe to ignore, since they will not occur when the application runs in standard mode (when the debugger is not attached to the process). If you encounter general slowness with text input controls, before investing time to debug, try running the application by directly loading it from the device interface, instead of pushing it from Xcode (so that the debugger does not connect).
To showcase some of the customization possibilities, as well as the way they interact with the rest of the interface you would build, some views in this section are going to be displayed in a ZStack, on top of an animated background view. We are going to explore this in far more detail later but, for now, it’s useful to know that, in SwiftUI, you can animate a vast majority of the views’ properties, including their scale and color. SwiftUI provides several tools you can use to create animations, such as the withAnimation function, the .animation view modifier or the .animation Binding method. Apple also hosted a few presentations regarding SwiftUI’s Animation APIs, such as Explore SwiftUI animations and Wind your way through advanced animations in SwiftUI. Apple’s SwiftUI team also maintains a brief SwiftUI animation tutorial.
The animated background view provided below creates a set of randomly colored circles, scales them up and down automatically and moves them on the screen. To animate each circle’s color and position, we tie them to a scale @Stateproperty. Using the onAppear modifier, we force SwiftUI to transition between the initial value of the scale property, assigned on initialization, and the new value, provided in the modifier’s closure. To keep the animation running indefinitely, we can simply use the repeatForever method. The blur effect is used to create the illusion of a thick material between the screen and the underlying animated circles and it was very commonly used to give the impression of translucent glass or plastic, before iOS26 and Apple’s Liquid Glass.
To be useful, the animated background view needs to create and moves circles in an area that is visible. You can use compiler directives to condition the area in which the colored circles can move, based on the Operating System. There are other, more precise mechanisms (such as the GeometryReader container), but this will suffice for our current requirements.
struct AnimatedBackground: View {
@State private var scale : CGFloat = 1
@State private var nbOfCircles: Int = 25
let bubbleColors:[Color] = [.red,.mint,.purple,.pink,.purple,.blue,.cyan,.orange,.red,.teal]
var body: some View {
ZStack {
ForEach (0...nbOfCircles, id:\.self) { index in
Circle ()
.foregroundColor(scale < 2 ? bubbleColors[index < bubbleColors.count - 1 ? index : bubbleColors.count - 1] : bubbleColors[index < bubbleColors.count - 1 ? index % bubbleColors.count : index % (bubbleColors.count-2)])
.animation (Animation.easeInOut(duration: 1)
.repeatForever(autoreverses: true)
.speed (.random(in: 0.05...0.5))
.delay(.random (in: 0...1)), value: scale
)
.scaleEffect(self.scale * .random(in: 0.5...3))
.frame(width: .random(in: 50...100),
height: CGFloat.random (in:50...100),
alignment: .center)
#if os(iOS)
.position(CGPoint(x: scale < 2 ? .random(in: 50...200) + index : .random(in: 100...300) - index,
y: scale < 2 ? .random(in:50...980) + index : .random(in: 100...800) - index))
#else
.position(CGPoint(x: scale < 2 ? .random(in: 10...2000) + index : .random(in: 100...1300) - index,
y: scale < 2 ? .random(in:10...600) + index : .random(in: 100...800) - index))
#endif
.animation (Animation.easeInOut(duration: 1)
.repeatForever(autoreverses: true)
.speed (.random(in: 0.05...0.5))
.delay(.random (in: 0...1)), value: scale
)
.blendMode(.destinationOver)
}
}
.onAppear {
self.scale = 2
}
.background(
Rectangle()
.foregroundColor(.gray))
.blur(radius: 60)
.ignoresSafeArea()
}
}
The TextInputControls view below acts as the main test view, used to showcase the text input controls and their customization options. The input controls are going to be added in the VStack.
struct TextInputControls: View {
@State private var input = ""
var body: some View {
ZStack{
AnimatedBackground()
VStack{
Text("The **raw** input value is: \(input)")
}
}
}
}
The TextField Control
The TextField control is used to capture a single, unformatted line of text. It is an expansive view, unlike the fixed-sizeText primitive. The image below showcases some common use cases for the TextField control (or its UIKit equivalent, UITextField), in Apple’s first party applications.
The snippet below exemplifies a TextField control in its most basic form. It takes a localized string as a label and a binding to a State property as the text parameter. It is, therefore, common to surround the TextField control with some padding, to create some space between the edges of the screen and the control.
TextField("Card Title", text:$input).padding()
The screenshots below represent the same view, but rendered on various platforms. Notice how, on iOS and on visionOS, the TextField controls do not provide, in their default styling, any background. There are multiple mechanisms you can use to add a background, depending on the OS version your application targets, and we are going to explore some of them shortly. It’s also useful to note that the animated background may make sense on some devices and in some contexts, but not on others.
Depending on your particular use case, you may want to change the visual style of the control. For example, you may want to make it more visible on compact platforms, like the iPhone and Vision Pro. With iOS26, on the iPhone, you can use the glassEffect modifier, to quickly apply LiquidGlass. If you feel LiquidGlass doesn’t match your application, you could use the textFieldStyle modifier, or you could simply add a background modifier, directly. The snippet below showcases a few examples, both with and without Liquid Glass and using various initializers.
TextField("Card Title", text:$input).padding().glassEffect().padding()
TextField("Card Title", text:$input).padding().textFieldStyle(.roundedBorder)
TextField("Card Title", text:$input).padding()
.background{
Capsule(style: .circular).foregroundStyle(.ultraThinMaterial)
}
.tint(.red)
.foregroundColor(.red)
.padding()
TextField(text: $input){
Text("Card Title").foregroundStyle(.red)
}.padding()
.background{
Capsule(style: .circular).foregroundStyle(.ultraThinMaterial)
}.padding()
TextField("Card Title", text: $input, prompt: Text("Card Title").foregroundStyle(.white), axis: .vertical).padding().glassEffect(.regular.tint(.mint)).padding()
The image below showcases the resulting interface, both on iOS and on macOS.
Note that Vision Pro does not currently support Liquid Glass (at least not with VisionOS 26). If your multi platform application uses Liquid Glass, you should ensure there is a style to fall back onto. Otherwise, the application will not build for the VisionOS target.
Customizing the Virtual Keyboard, the Quick Type Bar the Keyboard Toolbar
A distinct characteristic of text input controls is that, on compact platforms such as iOS, iPadOS, visionOS, watchOS and tvOS, when the control is in focus (active, selected) you also get a pop-up or a modal keyboard. Using the .keyboardType modifier, you can change the default keyboard to a style that better matches the type of information the text field is meant to capture. The snippet and image below showcase an example you would use when the input you request from your end-users is a phone number. There are other styles, which suit other types of input better (.numbersAndPunctuation, .emailAddress etc).
You could use the default keyboard, but the dedicated style provides a much better experience and many Apple users expect this type of care from their apps.
Since macOS does not support the keyboardType modifier, we can simply condition the addition of the modifier with a compiler directive.
When working on Multiplatform applications, there will often be cases where specific interface elements will need their own implementation. Sometimes, it’s sufficient to keep a single struct and condition modifiers based on the OS, as shown in this example. In more complex cases, though, you will be a lot better off by simply creating individual structs (in the same file, or in dedicated files) and use compiler conditional directives to choose which view to render.
import SwiftUI
struct TextInputControls: View {
@State private var input = ""
var body: some View {
ZStack{
AnimatedBackground()
VStack{
TextField("Phone Number", text: $input, prompt: Text("Phone Number").foregroundStyle(.black))
.padding(20)
#if (os(iOS) || os(macOS))
.glassEffect()
.padding(20)
#endif
#if os(visionOS)
.background{
Capsule(style: .circular)
.stroke(lineWidth: 5)
.foregroundStyle(.ultraThinMaterial)
}
.padding(50)
#endif // os(visionOS)
#if !os(macOS)
.keyboardType(.phonePad)
#endif
Text("The **raw** input value is: \(input)").padding()
}
}
}
}
#Preview {
TextInputControls()
}
You can also further improve the experience you provide to your end-users, by specifying the type of data associated to the text field. For example, by using the .textContentType modifier, you can control the options your end-users receive in the quick type bar (the autocomplete area above the keyboard). You can also modify the way the Submit (return) key looks, on the keyboard, using the .submitLabel modifier. Apple provides a set of SubmitLabels you can use, but you cannot easily add your own. Using the .toolbar modifier, you can also add custom buttons right above the keyboard. That is where you would add custom buttons, or other types of custom views. In the image below, you can see some of the common use cases of each modifier (although they are not necessarily combined this way in practice).
Adding custom behavior on updates
As an input control, the main purpose of the TextField is to effect a change in your application’s state. In the previous example, it updates the input state property. In some interfaces, you may want to verify the input your end-user provides (for example, your input property should contain no more than 10 characters, similar to a normal phone number), or you may want to reformat or modify the input ( for example, reformat the text as a phone number, as seen in the Contacts application). There are multiple mechanisms you can use, but the easiest ones to use are the .onChange (updated with iOS17) and .onSubmitmodifiers.
Limiting the length of the input to 10 characters
When used strictly for formatting purposes, the formatting logic can be maintained inside the view modifier. For example, the snippet below simply ensures that your end user would not be able to insert an 11th character. Note that the key presses are still registered. In the simulator, you can cause this closure to stop executing, if you use a physical keyboard and spam keys.
TextField("Name", text: $input, prompt: Text("Name").foregroundStyle(.black))
.padding(20)
.submitLabel(.send)
.onChange(of: input){ new, old in
guard new.count <= 10 else { return }
input = new
}
}
As a reminder, the guard statement is typically used as an early return mechanism. Its purpose is to return from the current scope, if a set of conditions are not met. The onChange closure above can also be written as :
onChange(of: input) { new, old in
if new.count <= 10 { input = new }
}
In general, with very few exceptions, logic that controls or affects the input (limiting the number of characters, adding or removing characters etc.) should be placed in onChange closures and not in onSubmit closures. Otherwise, you may hide the effects of the closure away from your end user. This is especially true if you interface jumps to another text field on submit, because your end user may miss the changes your application made to their input. For example, your end user may type 11 or more characters, but their input would get truncated after the value is submitted.
Using TextField validation to enable and disable Buttons (e-mail validation)
Another common requirement in interfaces is to condition the interactivity of a button (whether it’s active or not) to the validity of the TextField’s input. For example, if a TextField takes in an e-mail address, you may want to allow the end user to save the input only if the value contains the @ sign and, at least one character after it, a . symbol. With version 5.7, Swift introduced dedicated types forregular expressions, or regexes. They also added a framework you can use to build regexes in a more human readable way. In prior Swift versions, the String struct’s range() instance method. Regardless of the approach you choose, the mechanism remains the same. You would build a regular expression using one of the rules in the table below and you would then try to match the input in the TextField to the regex.
| Type of Regex | Example for e-mail validation | Expression Meaning | Rules for regex |
|---|---|---|---|
range(of: options: range: locale:) |
"^[A-Z0-9a-z.%+-](#)+@[A-Za-z0-9.-](#)+.[A-Za-z](#){2,}$" |
Starts with (^) one or more group of characters ([A-Z0-9a-z.%+-]+), then the @ symbol, followed by one or more groups of alphanumeric characters, as well as . or - ([A-Za-z0-9.-]+), a . and two or more uppercase or lowercase letters. The $ sign essentially indicates that the input needs to end in a pattern that matches .[A-Za-z]{2,}. |
International Components of Unicode Regex rules |
.firstMatch(of:) |
/^[A-Z0-9a-z.\_%+-](#)+@[A-Za-z0-9.-](#)+.[A-Za-z](#){2,}$/ |
Same as above | Same as above |
Regex {…} |
Regex {/^/;OneOrMore { CharacterClass(.anyOf("._%+-"),("A"..."Z"),("0"..."9"),("a"..."z"))};"@";OneOrMore {CharacterClass(.anyOf(".-"),("A"..."Z"),("a"..."z"),("0"..."9"))};/./;Repeat(2...) {CharacterClass(("A"..."Z"),("a"..."z"))};/$/} |
The /^/ group of symbols indicates the line needs to start with the first classifier that succeeds it (in this case, OneOrMore)`. Conversely, the /$/ set of symbols requires the line to end with the classifier that precedes it (in this case, the Repeat(2…) block). |
Regex Builder Documentation) |
In the example below, the isEmailValid computed property returns true if input matches the regex, or false if it does not. The commented regexes are equivalent to the uncommented one. Then, we use the .disabled modifier to determine if the button is active. Since isEmailValid returns true for a valid e-mail, we use the negation operator ! to reverse the value (.disabled is true when isEmailValid is false).
Alternatively, the value of isEmailValid could have been re-evaluated in a .onChange modifier closure.
import SwiftUI
import RegexBuilder
struct Regexes: View {
@State var input:String = ""
var isEmailValid: Bool {
// ^ = starts with, $ = ends with, + after ] or } = one or more, * fter ] or } = zero or more
// input.range(of: "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,}$", options: .regularExpression) != nil
// input.firstMatch(of: /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,}$/) != nil
input.firstMatch(of: Regex {/^/
OneOrMore {
CharacterClass(
.anyOf("._%+-"),
("A"..."Z"),
("0"..."9"),
("a"..."z")
)}
"@"
OneOrMore {CharacterClass(
.anyOf(".-"),("A"..."Z"),("a"..."z"),("0"..."9"))}
/./
Repeat(2...) {CharacterClass(("A"..."Z"),("a"..."z"))}
/$/
}) != nil
var body: some View {
TextField("e-mail address", text: $input)
Button("Submit"){
}.disabled(!isEmailValid)
}
}
Although RegexBuilder seems more human readable at first, especially when spaced properly on new lines, I would strongly recommend you become familiar with the C-style Unicode version. RegexBuilder is unique to Apple, whereas Unicode regexes are almost universal. In other words, Unicode regexes are transferrable. Additionally, Unicode regexes are a lot more succinct.
Changing input using Formatters
There may be cases where, for aesthetic or clarity reasons, you need to modify your end-users’ input for display. For example, if the input represents a phone number, you may want to surround the first three digits with rounded braces, to represent the Area Code, then surround the next three digits with a space or - symbol to represent the central office code. This would be work your application should do, not the user. In other words, your end user would type 1231231234, but the phone number would be displayed as (123) - 123 - 1234. There are other similar examples, such as credit card numbers, dates, numbers with various decimal places and so on.
Apple provides several tools you can use to rearrange data to be displayed in text form. One of the older such tools is the Formatter class, with its many subclasses, included in the Foundation package with iOS 2.0. Since it’s a legacy component, it has both an Objective-C and a Swift version, and you can use either of the two. With iOS 15, Apple introduced a redesigned, Swift-native replacement for the Formatter class, in the form of the FormatStyle, ParseableFormatStyle and ParseStrategy protocols.
Both the old and the new formatting tools function on similar principles. They provide a way for you to describe how to transform text input to a different concrete type and vice versa. They can also be passed as arguments to the TextField control initializer. You can pass a Formatter class as an argument to TextField(\_ titleKey:LocalizedStringKey,value: Binding<V>,formatter: Formatter) , or a FormatStyle as an argument to TextField(\_ titleKey: LocalizedStringKey, value: Binding<F.FormatInput>,format: Format).
Although tutorials often demonstrate the functionality by converting between string formats, their purpose is more complex. Formatters, together with FormatStyles, represent a mechanism to convert (or format) any type of data (ranging from currency, dates, textual sentences or complex objects) into a localized string for presentation, and to parse user-entered text back into those concrete types. Their main advantage is that, under the hood, they interact with other APIs on Apple devices. As Apple introduces new capabilities in localization and formatting capabilities, they would be available to your application, with no additional effort on your part.
To exemplify both approaches, in a SwiftUI TextField control, we are going to assume our application runs on a device set to the US region and that the phone numbers we are going to store are typed by a user in the US, in the domestic format (123) 123 - 1234. We are not going to handle the + symbol.
If formatting the phone numbers is a core function of your application, it would be useful to choose the formatting rules based on the device’s settings. This way, you make sure the formatting is aligned to your end user’s customs. To do so, you would create a more complex data model and you could, for example, base your formatting decision on the value of Locale.current.region. For example:
if Locale.current.region == .unitedStates { // add formatting logic }
Correct and complete localization in general is a complex topic and is out of scope for now, even when it comes to the format of a phone number.
The “legacy” approach relies on subclassing (inheriting) the Formatter class and on overriding a set of methods that get called when SwiftUI performs specific actions (when loading the view or when the bound value changes). You would essentially define how the data should be showed (by overriding the string method) and how the underlying data should be saved (by overriding the getObjectValue method).
A common approach consists of the use of a mask. First, you would choose a placeholder (or control) symbol, to represent a phone digit (for example, X), then lay out the structure of the formatted result. For a phone number, you could use (###) ### - #### as the mask. You can then write a simple method that parses the mask and replaces the placeholder with the appropriate values from your end-user’s input. The snippet below represents the starting point.
class PhoneFormatter: Formatter {
let mask = "(###) ### - ####"
override func string(for obj: Any?) -> String? {
// Converts the raw digits to the formatted version (for display)
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
// Converts the formatted string to raw digits (for the Binding)
}
func format(numbers: String) -> String {
// Altough you could put the formatting code inside the string function, it is customary to put this in a dedicated method.
}
}
Since it’s straight-forward, we can start with the getObjectValue method. As shown in the comment, its purpose is to take whatever input the end-user provides and convert to raw digits. The string parameter will represent the input that needs to be converted (the text displayed in the TextField), and the obj parameter is a reference to the destination object (in this case, it will be the TextField’s Binding). This is required because the end user can paste an already formatter phone number, so we need to be able to convert it to the underlying storage format (in this case, digits). The snippet below represents the updated method. Note that the commented version uses methods supported in older Swift versions, which did not support regexes. If you need to support iOS15 or earlier, you would use the string.components method.
class PhoneFormatter: Formatter {
//...
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
// obj?.pointee = string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() as AnyObject
obj?.pointee = string.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression) as AnyObject
return true
}
//...
}
The string method’s purpose is the opposite of getObjectValue. It’s meant to take the raw value (digits stored in the TextField binding) and convert them to the formatted value, matching the mask we had defined previously. First, we need to get a String from the obj parameter. To do so, we could cast it using a type cast operator (as? if the cast may fail or as! if the cast will surely succeed). This would change the type of the obj parameter, from the existential type Any to a concrete type, without making changes to the underlying data. It generally faster and, if you use the optional cast operator as?, it is safe in Swift, but may not be so in other languages. Since the TextField’s Binding is a String, and we are going to use the formatter to work with that binding, this mechanism would suffice. Alternatively, we could convert obj to a String, for example using a String initializer (String(data:, encoding:)). This would actually perform a conversion, making it generally safer to use if there’s no way to predict the type of the value passed to the Formatter.
Then, we can use a separate method ( in this example, we can call it format) to do the actual processing. In the end, this method needs to return the converted value (the formatted phone number).
class PhoneFormatter: Formatter {
//...
override func string(for obj: Any?) -> String? {
guard let string = obj as? String else { return nil }
// guard let string = String(data: obj as! Data, encoding: .utf8) else { return nil }
return format(numbers: string)
}
//...
}
Due to the way the String struct is implemented, you can iterate through the characters of String variables just as you would a Swift Array. Because of this detail, the formatting mask problem is, essentially, a problem of operations over arrays:
The mask String is, in essence, the array
['(', 'X', 'X', 'X', ')', ' ', 'X', 'X', 'X', ' ', '-', ' ', 'X', 'X', 'X', 'X']A full phone number entered in the input field is represented by the array
['1', '2', '3', '1', '2', '3', '1', '2', '3', '4']
To apply the mask, you would iterate through its elements and, when a control character is encountered, you would replace it with the appropriate element in the phone number array. Note that, because the mask contains additional characters compared to the , the indexes of the corresponding elements in each array will not match. Therefore, you would need to iterate through the mask String. The snippet below represents the format function.
class PhoneFormatter: Formatter {
//...
func format(numbers: String) -> String {
var result = ""
var index = numbers.startIndex
for char in mask {
if index >= numbers.endIndex { break }
if char == "#" {
result.append(numbers[index])
index = numbers.index(after: index)
} else {
result.append(char)
}
}
return result
}
//...
}
The index(after: ) method is used for variety. You could just as easily use index += 1. The out of bounds check is performed in the conditional if index >= numbers.endIndex { break }.
You can then use the PhoneNumber formatter in a regular TextField SwiftUI control, as shown in the snippet below.
import SwiftUI
struct SampleWithFormatter: View {
@State var input: String = ""
var body: some View {
TextField("Phone Number", value: $input, formatter: PhoneFormatter()).padding().glassEffect().padding(30)
Text(input).padding().glassEffect()
}
}
#Preview {
SampleWithFormatter()
}
If you test this out, you will quickly discover that the text is not reformatted as you type. This is because SwiftUI does not call the formatter’s string method until the view goes out of focus. This means your end-user can input a lot more than 10 characters and they have no visual feedback until they either submit the new value or they select another control. In the section Changing input through an intermediate binding, we are going to explore a potential workaround for this.
As you find new ways to accomplish specific tasks, you may find it valuable to properly test the implementation’s limitations, if you have the time and energy. Even though I am trying to outline the main limitations of each approach, you may encounter limitations in use cases I may not have considered. This is why, in software development, experience is still more valuable than pure syntactic knowledge.
More examples of format functions
To expand on the idea of the formatting functions presented in the previous section, for more complex types of input, you could also use more control characters. For example, you could use a control character to indicate an uppercase letter (C), and another one to indicate lowercase letters (c). In that case, a switch statement would likely be more feasible. The snippet below represents an example. It would take the input abcdef and turn it to abc-DEF.
func format(input: String) -> String {
var result = ""
var index = input.startIndex
final mask = "ccc-CCC"
for char in mask {
if index >= input.endIndex { break }
switch char {
case "c":
result.append(input[index].lowercased())
index = input.index(after: index)
case "C":
result.append(input[index].uppercased())
index = input.index(after: index)
default:
result.append(char)
}
}
return result
}
A less flexible (but perhaps more concise) option would be to use regexes. Instead of using a mask, you would capture groups of digits, then use them to construct the number. The snippet below represents one such implementation. Notice how, as a fallback, if the regex fails, you should return the unmodified input.
class PhoneFormatter: Formatter {
//...
func format(numbers: String) -> String {
var result = ""
guard let matched = numbers.firstMatch(of: /(\d{0,3})(\d{0,3})(\d{0,4})$/) else {
return numbers
}
print(matched)
return "(\(matched.1)) \(matched.2) - \(matched.3)"
}
//...
}
You would often use regexes in the pattern showed above when you need to operate with specific portions of a string. For example, you may want to capture the domain of an e-mail and capitalize it. In that case, the function would become similar to the snippet below.
func format(input: String) -> String {
var result = ""
guard let matched = input.firstMatch(of: /^.*@(\w+).*$/) else {
return input
}
return "\(matched.1.uppercased())"
}
//print(format(input: "josh.tea@prudent.leap.com"))
//PRUDENT
//print(format(input: "josh.tea@prudentleap.com"))
//PRUDENTLEAP
As seen above, regular expressions are powerful. However, they have also been the source of numerous service outages throughout the years. You should always validate regexes carefully, and you should make them as restrictive as you possibly can.
Changing input using FormatStyle, ParseStrategy and ParseableFormatStyle
Apple’s more modern formatting APIs rely on a set of protocols, instead of the Formatter class, to accomplish the same functionality in a more modern and concise way. A SwiftUI TextField using this approach will have the same limitations as one using a Formatter, as the view lifecycle does not change.
FormatStyle, which ensures that conforming types take an input of an underlying type and convert it to a formatted output. Conforming types implement a
format(\_ value: InputType) -> OutputTypemethod, which is the equivalent of thestringmethod in the Formatter class.ParseStrategy, which ensures that conforming types take a formatted value and convert to an underlying input data type . Conforming types implement a
parse(_ value: OutputType) throws -> InputTypemethod, which is the equivalent of thegetObjectValuemethod in the Formatter class.ParseableFormatStyle , which acts as a wrapper over FormatStyle and ParseStrategy. Conforming types implement a parseStrategy read-only computed property that instantiates a ParseStrategy, as well as a
formatmethod.To make it easier to use, you can extend the FormatStyle base protocol with a static accessor. This is known as static member lookup over generics, and it has been introduced in Swift 5.5.
The snippet below provides the same formatting capabilities as the previous Formatter-based example, but in a more modern way. For modern code bases, it should be the preferred implementation.
struct PhoneNumberStyle: ParseableFormatStyle {
var parseStrategy: PhoneParseStrategy {
PhoneParseStrategy()
}
func format(_ value: String) -> String {
let mask = "(###) ### - ####"
var result = ""
var index = value.startIndex
for char in mask {
if index >= value.endIndex { break }
if char == "#" {
result.append(value[index])
index = value.index(after: index)
} else {
result.append(char)
}
}
return result
}
}
struct PhoneParseStrategy: ParseStrategy {
func parse(_ value: String) throws -> String {
return value.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
}
}
extension FormatStyle where Self == PhoneNumberStyle {
static var phoneNumber: PhoneNumberStyle { PhoneNumberStyle() }
}
struct SampleWithFormatter: View {
@State var input: String = ""
var body: some View {
TextField("Phone Number", value: $input, format: .phoneNumber).padding().glassEffect().padding(30)
TextField("Sample", text:$input).padding().glassEffect().padding(30)
Text(input).padding().glassEffect()
}
}
Changing input through an intermediate binding
There are other ways you can obtain a similar effect to formatters. As an example, you could also use a computed property wrapped in a binding to handle the translation logic. This would, essentially, act as a proxy between the TextField and the State Property it should control. The mechanisms remain, essentially, the same:
A mechanism to take the input and change it to an underlying, cleaned up format. In the case of a computed property, we could use the
setmethodA mechanism to provide the formatted value, when SwiftUI needs to display the updated view. In a computed property, this is the function of the
getmethod.
The snippet below showcases this type of implementation. Notice how the phoneFilterBinding computed property itself is a read-only property and it implicitly returns a Binding via the Binding(get:() -> Value , set: (Value) -> Void)initializer.
struct SampleWithFormatter: View {
@State var input: String = ""
private var phoneFilterBinding : Binding<String> {
Binding(
get: {
let mask = "(###) ### - ####"
var result = ""
var index = input.startIndex
for char in mask {
if index >= input.endIndex { break }
if char == "#" {
result.append(input[index])
index = input.index(after: index)
} else {
result.append(char)
}
}
return result},
set: { newValue in
input = newValue.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
}
)
}
var body: some View {
TextField("Phone Number", text: phoneFilterBinding, prompt: Text("(###) ### - ####").foregroundStyle(.background))
.padding().glassEffect().padding(30)
}
}
This approach has the same limitations as all of the previous ones, but it adds an additional bug. The mask is applied to the computed property, rather than directly to the state property. This means the state property will temporarily hold one or two additional characters, until you either submit the value or another view comes into focus.
However, this can be fixed. The TextField view automatically updates when its text binding changes. Since the TextFieldis bound to a computed property instead of the actual state property, any updates done to the input state property will force the TextField view to update.
As a reminder, when a view displays a computed property, any update to the value it’s based on will invalidate the view, forcing a view update. When the view updates, it re-evaluates the computed property.
We can leverage this behavior, by using the .onChange(of:) modifier to react to changes to the input state property’s value. Any assignment to input would invalidate the view, which will cause the computed property to be re-evaluated. The most obvious operation to perform there is to limit the number of characters input can take, via a String.prefix method, as shown in the snippet below.
TextField("Phone Number", text: phoneFilterBinding, prompt: Text("(###) ### - ####").foregroundStyle(.background))
.onChange(of: input){ new, old in // this could have been "new _ in" instead
input = String(new.prefix(10))
}
.padding().glassEffect().padding(30)
To clarify, the reason this works here and it does not work if you bind the TextField directly to the State property is that the value of input is only changed on value submission. The value submission operations occurs implicitly when the view goes out of focus, or when the Submit operation is explicitly called (for example, by touching the submit button on the phone’s keyboard, or the return key on a Mac keyboard).
The SecureField Control and its limitations
Sometimes, your application needs to accept an end-user’s password, which is highly sensitive. It’s common practice to replace the characters your users type with * or with other symbols. Although you could apply one of the techniques we explored previously, Apple already provides a control you can use. In addition to masking characters and providing a quick link to Passwords as auto-fill options, the SecureField control also completely blanks out its content when you take screenshots or screen recordings and it blocks the ability to copy its contents , among other security measures. The snippet below and the adjoining screen shots exemplify a typical use case. Notice how the SecureField redacted characters are visible in normal use (left) but are hidden when a phone screen grab is taken(right).
struct SecureFieldSample: View {
@State private var password = ""
var body: some View {
Text("Login Form")
SecureField("Password", text: $password)
.textContentType(.newPassword)
.padding()
.glassEffect()
.padding()
}
}
The control does have its own limitations, though:
At the time of writing, there is no built-in mechanism to show the password.
You cannot customize the redaction characters, from the default
*If the control loses focus, it no longer supports in-place edits. In other words, any attempted changes will overwrite the existing text entirely.
If needed, you could implement a variant of the show/hide function using a state variable to signify whether the password should be visible. If true, you would display a TextField and, if false, you would display a SecureField. The snippet below represents such an example.
struct SecureField_ShowPassword: View {
@State var showPassword = false
@State var password = ""
var body: some View {
HStack{
if showPassword == true {
TextField("Password",text: $password)
.padding()
} else {
SecureField("Password",text: $password)
.padding()
}
Button(action: {
showPassword.toggle()
}, label: {
Image(systemName: showPassword == false ? "eye" : "eye.slash")
.foregroundStyle(showPassword == true ? .gray : .accentColor )
})
}
.padding(.horizontal)
.glassEffect()
.padding(.horizontal,40)
}
}
However, this implementation brings its own limitations:
Pressing the show/hide password button will cause the SecureField to re-render. As mentioned previously, the control loses in-place edit capabilities when it loses focus
The TextField control is not blocked for broadcast. Its content will be picked up by screenshots and screen recordings
The images below showcase the difference.
If the first issue is more of a nuisance, the second is a security concern. We can mitigate both by using UIKIt controls wrapped in a SwiftUI view, and we are going to explore this next. However, just because a solution is technically possible or even feasible, doesn’t mean it should be implemented. If a control misses the type of functionality you have in mind, you should always consider why.
In the case of UIKit’s UITextField, the same control is used both for normal text and for passwords. The security features are all linked to the instance property isSecureTextEntry. If you were to implement the same show/hide functionality, you would set that property to false, in order to show the password in plain text.
In the case of SwiftUI, Apple specifically chose to create two distinct controls. You would use SecureField for things you need to protect and TextField for anything else.
From a security standpoint, if the user can see the password, so can any potential bystanders.This type of threat is commonly known as shoulder surfing. Most likely, Apple specifically did not implement a show/hide password functionality for SecureField controls for this reason. With this constraint, in-place edits can cause potential issues with password mismatches, when you can’t visually confirm the edits.
A counter-argument to the shoulder surfing scenario is that the input mechanism itself (a keyboard on the screen) would pose a similar threat. This, however, is a false equivalence. A password displayed in plain text, even a moment, is immediately and passively readable. Conversely, deciphering what someone is typing requires active, sustained effort. Having said that, this potential threat is likely one of the reasons Apple introduced Keychain and Password Manager integration for secure fields. By eliminating the need to physically type in the password you remove the entire threat. Accessing secure features on the device using biometric authentication serves the same purpose. Not having to type or see the password ultimately makes it harder to leak information, even if the phone screen is accidentally captured by a CCTV camera. Ultimately, security is not a binary property, but rather a spectrum, and these features collectively push the experience towards the more secure end of that range, without sacrificing usability.