Text view in SwiftUI is probably the most used view by every developer, as presenting content is a fundamental part of making an app. Usually we display String values, and we also read String values in text fields too. But often values must be formatted before shown to users or when inserted into an app. Such values could be, for instance, temperatures, percentages, properly formatted dates, names, and more.
Thankfully, SwiftUI makes it really easy to style both presented values in Text views, and to get formatted values as input in text fields too. But keep in mind that not everything that can be formatted for output can also be formatted for input; you’ll find out more about that while you read on in the next parts. So, what you’ll meet next include how to format in order to display, and read when possible, numbers (notation, grouping, sign, decimal precision, etc), percentages, currencies, dates (date components, date intervals, relative dates, ISO 8601 dates), temperatures, distance, file size, concatenating collections of items, names, URLs.
Let’s jump straight in!
Presenting formatted numbers
Let’s get started with two Text views that present a numerical value:
|
1 2 3 4 5 6 |
let count = 12450 Text("\(count)“) Text(count, format: .number) |
Notice the format parameter in the second Text view with the .number argument. It tells Text view that the displayed value is a number and it should be treated accordingly.
If we run and see the above values, we’ll notice that they’re exactly the same:

So, what makes the number format different?
In the first Text view, the count value is simply interpolated and presented as a String. If converting to String is not sufficient, then specifying the number format is actually the correct way to show a numerical value. Text view respects the user’s locale and displayed number can be customized further.
Important Note: To avoid any confusion, it’s necessary to say that according to my locale settings, digits are grouped with the dot “.” symbol, and fractions with the comma “,” symbol.
More particularly, we can customize the appearance of the numerical value in various ways, by accessing a series of methods being available through the .number argument. Let’s go through some examples:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Text(count, format: .number.notation(.compactName)) // Output: 12K Text(count, format: .number.notation(.scientific)) // Output: 1.245E4 Text(count, format: .number.grouping(.never)) // Output: 12450 Text(count, format: .number.sign(strategy: .always())) // Output: +12,450 |
- The first two notation variations represent the number in compact and scientific formats respectively.
- In the third Text view, we’re commanding Text view to omit showing the grouping symbol among digits.
- The last Text view will always show the number sign (+, -).

It’s worth saying that we can build complex format expressions by concatenating format options one after the other:
|
1 2 3 4 5 6 |
Text(count, format: .number.notation(.scientific) .sign(strategy: .always()) ) |
Providing numbers to text fields
It’s quite often necessary to accept numerical values as input to text fields. Without any data formatting parameters, we’d need a String property to store the input value, then validate or sanitise it, and finally assign it to a numerical property. For instance, suppose that we have the following property that represents page margins:
|
1 2 3 |
@State private var margin = 56.0 |
Using a TextField we can ask a value from users like so:
|
1 2 3 |
TextField("Margin", text: $margin) |
However, this particular line is wrong, as margin should be a String value. In fact, the above two lines produce the following error:
Cannot convert value of type ‘Binding<Double>’ to expected argument type ‘Binding<String>’
We can easily avoid all the mess simply by using the proper TextField initializer; one that contains the format parameter, where we provide the .number argument:
|
1 2 3 |
TextField("Margin", value: $margin, format: .number) |
Now, even if users type invalid characters, the text field drops them and keeps the numerical value only, storing it in the margin state property. Moreover, it also formats the number correctly by placing grouping symbols to the right places.
There’s another noteworthy detail to consider when accepting numerical values as input with text fields. The data type of the property that stores the numerical value defines the behavior of the text field, forcing it to keep only valid values, discarding everything else.
Say that we have the following two state properties with explicit data types:
|
1 2 3 4 |
@State private var pageCount: Int = 120 @State private var width: Double = 800.0 |
See that pageCount is an integer, while width is a double value. Let’s add now two text fields to gather values for these two properties as shown next:
|
1 2 3 4 |
TextField("Page Count", value: $pageCount, format: .number) TextField("Width", value: $width, format: .number) |
By testing both, it’s easy to notice that the first text fields keeps only the integer part of any provided number. On the other hand, we can properly set any double value to the second text field.
And as I’ve said already, text field automatically fixes grouping of digits.
Presenting numbers with decimals precision
We often need to show decimal numbers with specific length of fraction digits. We can manage that exactly as shown in the next Text view:
|
1 2 3 4 5 |
let rating = 4.6789 Text(rating, format: .number.precision(.fractionLength(2))) |
The precision method that follows the .number argument gets a fraction length as its own argument, which in this particular example we set to two digits. As a result, the displayed number is rounded to 4.68:

It’s also possible to specify a range of digits. Just like we’d do in a Text view, it’s possible to read a numerical value with a variable fraction length in a text field too:
|
1 2 3 4 5 6 7 8 9 |
@State private var lineHeight = 1.4 TextField( "Line Height", value: $lineHeight, format: .number.precision(.fractionLength(1…2)) ) |
No matter how many fraction digits we type, when the text field loses its focus it either keeps one digit only, or rounds and limits them to two if they’re more than one.
Working with percentage values
So far all examples demonstrate the .number value as argument to the format parameter of both Text views and text fields. However, there are more than that, and the next interesting value we’ll meet is the .percent.
With it, we can present any Double as a percentage without any additional effort on our behalf. Note that the displayed percentage is always the underlying value multiplied by 100. For example, 72% is the 0.72 double value in code:
|
1 2 3 4 |
let progress = 0.72 Text(progress, format: .percent) |

We can also read percentage values, with text fields automatically append the “%” symbol to the number and discard any invalid characters. In this case we write the actual percentage, even though the stored value is the percentage / 100. Here’s an example of percentage input:
|
1 2 3 4 |
@State private var discount = 0.15 TextField("Discount", value: $discount, format: .percent) |
Presenting and reading currencies
Currency is another kind of value where formatting is really important and crucial. Many apps need to show currency signs next to numerical values, and SwiftUI makes it really easy to manage it.
All it takes is to specify the .currency format, providing at the same time the currency code as shown next. Note that Text view respects the user’s locale settings, presenting the appropriate currency symbol either before, or after the amount:
|
1 2 3 4 5 6 7 |
let price = 29.99 Text(price, format: .currency(code: "USD")) Text(price, format: .currency(code: "GBP")) Text(price, format: .currency(code: "EUR")) |

These examples have the currency code hardcoded. We can, however, get it dynamically through the Locale type like so:
|
1 2 3 |
Locale.current.currency?.identifier |
Notice that this is an optional value, so we should always provide a fallback. For example:
|
1 2 3 4 5 6 7 |
Text(price, format: .currency( code: Locale.current.currency?.identifier ?? "EUR" ) ) |
In a similar way we can also accept currency values from text fields too:
|
1 2 3 4 5 |
@State private var newPrice = 9.99 TextField("Price", value: $newPrice, format: .currency(code: "EUR")) |
Formatting dates
Dates are important for many apps, and regardless of how they’re handled internally, it’s equally essential how they’re presented to users. The date picker is the best control to let users insert date values, however when using Text views to display them we have some options to format them as needed.
Let’s start with the following:
|
1 2 3 4 |
let date = Date() Text(date, format: .dateTime) |
This .dateTime argument results to a formatted date and time respecting the locale settings.

We can make that more specific and show only day, month, and year:
|
1 2 3 |
Text(date, format: .dateTime.day().month().year()) |

There are more methods available to use and format even further the output and what will be displayed. For instance, the following shows the current week day, day of month, and the month:
|
1 2 3 |
Text(date, format: .dateTime.weekday().day().month()) |

In addition, we can choose how numerical and literal values will appear, like number of digits in days, or names of days and months:
|
1 2 3 4 5 6 7 |
Text(date, format: .dateTime.weekday(.wide) .day(.twoDigits) .month(.narrow) .year()) |

Also, let’s not forget about time components. Sometimes we need to display only parts of the time, so we can ask for that too:
|
1 2 3 |
Text(date, format: .dateTime.hour().minute()) |

Presenting date intervals
A Text view lets us also present date or time intervals. See the following example, for instance, where the end date is two hours later from the start date, and it’s shown as a range automatically simply by using the .interval argument in the format parameter:
|
1 2 3 4 5 |
let start = Date() let end = start.addingTimeInterval(60 * 60 * 2) Text(start..<end, format: .interval.hour().minute()) |

Omitting the .hour().minute() suffix makes the date appear too:
|
1 2 3 |
Text(start..<end, format: .interval) |

Presenting relative dates
Relative dates are dates expressed in relation to another date, usually the current one, and presented as a literal string instead of having the usual date format, such as “Tomorrow” or “in 5 hours”. An app that presents relative dates can be benefited by the formatting that Text view allows to make, relieving us from any effort to manually create such content.
For example, let’s calculate the date exactly 24 hours from now, meaning tomorrow:
|
1 2 3 |
let tomorrow = Date.now.addingTimeInterval(60 * 60 * 24) |
Using the .relative format with the .named presentation type in a Text view, we get the literal “tomorrow” instead of the tomorrow date, which is way better experience when fits than just showing a date:
|
1 2 3 |
Text(tomorrow, format: .relative(presentation: .named)) |

Let’s go through one more example, where we calculate a date value three hours from now, and we display it also as a relative date:
|
1 2 3 4 |
let futureDate = Date.now.addingTimeInterval(60 * 60 * 3) Text(futureDate, format: .relative(presentation: .numeric)) |

You might wonder how we can combine such values with text content. Simply enough, use String interpolation with the format parameter, and formatted values will be combined with strings. For instance:
|
1 2 3 |
Text("I'll be there \(futureDate, format: .relative(presentation: .numeric)).”) |

ISO 8601 date formatting
Date formatting according to ISO 8601 standard is not unknown for most developers, especially if they deal with web services and dates in server responses. We can easily present an ISO 8601 date, as all we have to do is to provide the .iso8601 argument to format parameter:
|
1 2 3 |
Text(date, format: .iso8601) |

Presenting temperatures
Some kind of apps, such as weather apps, need to present temperature values to users. With data formatting, Text views can display temperatures without much effort on our side.
To demonstrate that, let’s define a Measurement property with a temperature value:
|
1 2 3 |
let temperature = Measurement(value: 23, unit: UnitTemperature.celsius) |
The above allows to choose among Celsius, Fahrenheit and Kelvin units. To present the value with the temperature symbol, we pass the .measurement argument to format parameter with the desired width. See next how the same temperature is displayed in both abbreviated and wide widths:
|
1 2 3 4 |
Text(temperature, format: .measurement(width: .abbreviated)) Text(temperature, format: .measurement(width: .wide)) |

Note that, even though we specified the Celsius unit in this example, conversion to other units is done automatically based on the user’s locale settings. For instance, let’s change temporarily my locale value as shown next; temperature is now shown in Fahrenheit units:
|
1 2 3 4 |
Text(temperature, format: .measurement(width: .abbreviated)) .environment(\.locale, Locale(identifier: "en_US")) |

Presenting distance values
Displaying distance is useful in a variety of apps, like fitness, maps, travel, and more. Values that represent distance can be formatted accordingly in Text views in a similar fashion as temperatures.
Programmatically, distance is another Measurement value:
|
1 2 3 |
let distance = Measurement(value: 4.5, unit: UnitLength.kilometers) |
And just like temperatures, we display such a value with the appropriate measurement width:
|
1 2 3 4 |
Text(distance, format: .measurement(width: .abbreviated)) Text(distance, format: .measurement(width: .wide)) |

And just like before, conversion to the user’s locale settings is done automatically. For instance, with no additional effort, original kilometres are converted to miles for a different locale:
|
1 2 3 4 |
Text(distance, format: .measurement(width: .abbreviated)) .environment(\.locale, Locale(identifier: "en_US")) |

Presenting file size
Text views can also show file sizes in KB, MB, etc, but under one condition; expressed value must be an Int64, not Int:
|
1 2 3 |
let fileSize: Int64 = 2450000 |
We can present the above value now in MB with the .byteCount argument, specifying the .file style as shown next:
|
1 2 3 |
Text(fileSize, format: .byteCount(style: .file)) |

Note that long numbers like the above can be written with underscores in order to group digits:
|
1 2 3 |
let fileSize: Int64 = 2_450_000 |
This doesn’t change the actual number, it just makes it more readable.
Concatenating collections of items
We often have collections of values that we need to display as a single string separated with commas, and appending either the “and” or the “or” conjunction to the last item of the list. Text views can easily do that for us when we provide the .list argument and the conjunction type, choosing between .and and .or. For example:
|
1 2 3 4 5 6 |
let tags = ["iOS", "macOS", "watchOS"] Text(tags, format: .list(type: .and)) Text(tags, format: .list(type: .or)) |

Presenting person names
Another styling option regards person names, and various ways to display them. Note that name can include a prefix, a middle name, a suffix, a nickname, and more. The essential detail here before enjoying the benefits of automatic formatting, is to represent a person’s name as a PersonNameComponents value:
|
1 2 3 4 5 6 7 8 9 |
@State private var name = PersonNameComponents() name.namePrefix = "Dr." name.givenName = "John" name.middleName = "Michael" name.familyName = "Smith" name.nameSuffix = "Jr." |
The .name argument with the desired style to the format parameter of a Text view results to a properly formatted name:
|
1 2 3 4 5 6 |
Text(name, format: .name(style: .medium)) Text(name, format: .name(style: .short)) Text(name, format: .name(style: .long)) Text(name, format: .name(style: .abbreviated)) |

URL formatting
URLs can be styled in Text views too, so we can display parts of them, omitting what we don’t really need to show. The .url formatting option you’ll see next has various methods for tempering with the URL appearance, which I invite you to explore.
Let’s get started with an example URL:
|
1 2 3 |
let url = URL(string: "https://www.apple.com/macbook-pro")! |
The entire URL appears if we show it without any formatting. Instead, we can easily hide the scheme and make it better for the eye like so:
|
1 2 3 |
Text(url, format: .url.scheme(.never)) |
The scheme method with the .never argument applied on the .url option vanishes the “https://” part from the URL on display:

However, the really interesting side is that we can chain methods together and keep only the part of the URL we want. In the next example, we hide both the scheme and the path of the URL, keeping the domain only:
|
1 2 3 4 5 6 7 8 |
Text( url, format: .url .scheme(.never) .path(.never) ) |

Let’s take a look at one more example. The following hides the scheme and the domain, keeping the path with the leading slash:
|
1 2 3 4 5 6 7 8 |
Text( url, format: .url .scheme(.never) .host(.never) ) |

Wrapping up
The format parameter in the SwiftUI Text view can accept a variety of arguments that style the presented values in many ways. As you’ve seen in all previous parts, it’s possible to provide format options in both Text views and text fields, even though not everything in formatting applies to the latter. As a rule of thumb, text fields handle formatted values only when converting to, and from string, is possible. That said, in this rather long tutorial we went through some specific examples, without having covered all available APIs. However, it’s really interesting and fun to play around with formatted values, so go ahead and take your time to explore this part of SwiftUI. I hope you enjoyed this post, thanks for reading!