Exploring the new String API in Swift 4
Basics article available: StringsNow that WWDC 2017 is over (the best one since 2014 if you ask me) and the Xcode 9 beta is out, many Swift developers are starting to get their hands on Swift 4. What is really nice about this year’s new version is that it’s more of a refinement than a complete revamp of the language (like both Swift 2 and 3 were), which will make it a lot easier for most codebases to be upgraded.
One of those refinements is to the String
API, which has been made a lot easier to use (while also gaining power) in Swift 4. In past versions of Swift, the String
API was often brought up as an example of how Swift sometimes goes too far in favoring correctness over ease of use, with its cumbersome way of handling characters and substrings.
This week, let’s take a look at how it is to work with strings in Swift 4, and how we can take advantage of the new, improved API in various situations.
Multi-line string literals
Sometimes we have longer, static strings in our apps or scripts that span multiple lines. Before Swift 4, we had to do something like inline \n
across the string, add an appendOnNewLine()
method through an extension on String
or - in the case of scripting - make multiple print()
calls to add newlines to a long output.
For example, here is how TestDrive’s printHelp()
function (which is used to print usage instructions for the script) looks like in Swift 3:
func printHelp() {
print("🚘 Test Drive")
print("--------------")
print("Quickly try out any Swift pod or framework in a playground.")
print("\nUsage:")
print("- Simply pass a list of pod names or URLs that you want to test drive.")
print("- You can also specify a platform (iOS, macOS or tvOS) using the '-p' option")
print("- To use a specific version or branch, use the '-v' argument (or '-m' for master)")
print("\nExamples:")
print("- testdrive Unbox Wrap Files")
print("- testdrive https://github.com/johnsundell/unbox.git Wrap Files")
print("- testdrive Unbox -p tvOS")
print("- testdrive Unbox -v 2.3.0")
print("- testdrive Unbox -v swift3")
}
And here’s how it can be expressed using a multi-line string literal in Swift 4 instead (please excuse the strange syntax highlighting):
func printHelp() {
print(
"""
🚘 Test Drive
--------------
Quickly try out any Swift pod or framework in a playground.
Usage:
- Simply pass a list of pod names or URLs that you want to test drive.
- You can also specify a platform (iOS, macOS or tvOS) using the '-p' option
- To use a specific version or branch, use the '-v' argument (or '-m' for master)
Examples:
- testdrive Unbox Wrap Files
- testdrive https://github.com/johnsundell/unbox.git Wrap Files
- testdrive Unbox -p tvOS
- testdrive Unbox -v 2.3.0
- testdrive Unbox -v swift3
"""
)
}
As you can see above, the code becomes a lot more expressive and clean when a multi-line literal is used. We no longer need to add multiple \n
to add line breaks, and instead we can simply add actual line breaks within the string, making it super easy to see exactly what the output will look like before even running the script.
In terms of indentation, multi-line literals use the bottom """
to determine the base indentation of the string. So everything that’s aligned with those quotation marks will have no additional indentation within the string.
Strings are collections (again!)
In Swift 1, String
conformed to what was then called CollectionType
(Collection
in Swift 3+), which meant that you could perform all kinds of collection operations (like forEach()
, filter()
etc) on them. You could still do this in Swift 2 & 3 as well, by accessing the characters
property, but this quickly led to harder to read code.
In Swift 4, strings are again collections, which means that you can simply treat them as “a collection of characters”. This can become quite useful, for example if you want to filter out certain characters (let’s take exclamation marks as an example) from a string:
let filtered = string.filter { $0 != "!" }
The new Substring type
Swift 4 introduces a new way to deal with substrings, using a distinct Substring
type. It’s now returned from most methods that return substrings (such as split()
) and a new subscripting API has also been introduced that enables you to quickly access a substring:
// Access a substring from a given index until the end
let substring = string[index...]
Let’s take a look at an example where we return a substring by truncating a text from user input, limiting it to a certain length. In Swift 3, you’d write something like this:
extension String {
func truncated() -> String {
return String(characters.prefix(truncationLimit))
}
}
(Edit note: I originally had a more complex example above, thanks to Ole Begemann for pointing out this better solution)
The good news is that, since Swift 4 is mostly source compatible with Swift 3, the exact same code as above works in Swift 4 as well. But the even better news is that, since strings are collections in Swift 4, we can simplify the above to work on the string directly:
extension String {
func truncated() -> Substring {
return prefix(truncationLimit)
}
}
The above uses the prefix()
API that returns a subsequence of up to n
elements, while also performing bounds checking (so that we won’t run into an error if our collection - in this case our string - contains less than n
elements).
You can see above that the return type of the truncated()
method is now the new Substring
type. While it may seem cumbersome at first that strings can now be of different types, it gives us a big advantage in terms of memory predictability.
In order to avoid having to create many redundant copies, Swift strings uses the “copy on write” method to only have to perform copies when needed. This means that substrings often share the same underlying buffer in memory with their parent string. But in the case of truncated()
, we don’t want to retain the entire un-truncated string in memory, just to be able to use the truncated substring.
By giving us a Substring
type, instead of a full String
, Swift now “forces” us to perform the copy explicitly when needed, so that the parent string’s memory can be freed up. We do this by simply creating a new String
from our truncated substring, like this:
label.text = String(userInput.truncated())
Conclusion
Since almost all Swift apps and scripts deal with strings, it’s very nice to see these type of refinements to their API. I think the new API is a nice tradeoff between correctness and ease of use, while also requiring the programmer to make deliberate choices around copying - as in the case of Substring
.
There are also more advancements made to the string API that I didn’t cover in this post - such as Unicode 9 support for simpler character management and the ability to easily access the underlying unicode code points that make up a character.
We’ll dive deeper into string handling and encodings in an upcoming post, as well as other new Swift 4 APIs and the new frameworks and tools that were announced at WWDC. So stay tuned! 😉
How do you find the new string API in Swift 4? Will the fact that strings are now collections make your string processing simpler? Let me know, along with other questions, comments or feedback - on Twitter @johnsundell.
Thanks for reading! 🚀