PL

The Best New Features in Swift 4

September 13, 2017, by Mike Ash

Swift 4 is here, and it's bringing some nice changes. We're not getting a radical rework of the syntax like we did last year, nor are we getting a breathtaking pile of new features like we did for Swift 2, but there are some nice additions you can use to improve your code. Let's take a look!

Multi-Line String Literals

Sometimes you want long, multi-line strings in your code. It might be an HTML template, a blob of XML, or a long message for the user. Either way, they're painful to write in Swift 3.

You can write them out all on one line, which gets ugly fast:

let message = "Please disable your Frobnitz before proceeding.\n\nTo do this, visit Settings -> Frobnitz, then toggle the switch to \"off\".\n\nIf you need the Frobnitz to remain enabled, tap \"Proceed Anyway\" below."

You can split it onto multiple lines by concatenating strings:

let message =
    "Please disable your Frobnitz before proceeding.\n\n"
  + "To do this, visit Settings -> Frobnitz, then toggle the switch to \"off\".\n\n"
  + "If you need the Frobnitz to remain enabled, tap \"Proceed Anyway\" below."

There are other ways to do it too, but none of them are all that good.

Swift 4 solves this problem with multi-line string literals. To write a multi-line string literal, use three quote marks at the beginning and end:

let message = """
    Please disable your Frobnitz before proceeding.

    To do this, visit Settings -> Frobnitz, then toggle the switch to "off".

    If you need the Frobnitz to remain enabled, tap "Proceed Anyway" below.
    """

If you've used Python, then this new syntax will look familiar. However, it's not quite the same. There are some interesting limitations and features of this syntax in Swift.

This triple-quote syntax cannot be used on a single line. Something like the following will not compile:

// Will not compile
label.text = """Put your text in "quotes" to make them look quoted."""

This could be handy to avoid having to escape quotes, but it's not allowed. The content of the string must be on separate lines between the """ marks.

Multi-line strings can be indented in your code without indenting the final result. The multi-line string above indents each line in the code, but the string placed into message has no leading whitespace. This is really nice, but what if you want some indentation? This feature is based on the indentation of the closing """ mark. Its indentation will be stripped off all of the other lines. If for some reason you needed the contents of message to be indented, you can do so by indenting the text farther than the closing """ mark:

let message = """
        Please disable your Frobnitz before proceeding.

        To do this, visit Settings -> Frobnitz, then toggle the switch to "off".

        If you need the Frobnitz to remain enabled, tap "Proceed Anyway" below.
    """

To avoid confusion, each line of text must be indented at least as much as the closing """ mark. A line with less indentation will produce an error.

You may want to split your text onto multiple lines without producing multiple lines in the output. You can remove a line break from the resulting string by adding a \ at the end of the line:

let message = """
    Please disable your Frobnitz before proceeding. \
    To do this, visit Settings -> Frobnitz, then toggle the switch to "off". \
    If you need the Frobnitz to remain enabled, tap "Proceed Anyway" below.
    """

One-Sided Ranges

This is a nice, small change that's mostly self explanatory. Ranges can now be one-sided, and the "empty" side is implied to be the minimum or maximum value that makes sense in context.

When subscripting a container, this means you can leave off things like string.endIndex or array.count. For example, if you want to split an array into halves:

let middle = array.count / 2
let firstHalf = array[..<middle]
let secondHalf = array[middle...]

Or if you want to get a substring up to a particular index:

let index = string.index(of: "e")!
string[..<index]

It can also be handy in switch statements:

switch x {
case ..<0:
    print("That's a negative.")
case 0:
    print("Nothing!")
case 1..<10:
    print("Pretty small.")
case 10..<100:
    print("Bigger.")
case 100...:
    print("Huge!")
default:
    // Unfortunately, the compiler can't figure out
    // that the above cases are exhaustive.
    break
}

For one-sided ranges up to a given value, you can use ..< for an exclusive range or ... for an inclusive range, just like two-sided ranges. For one-sided ranges starting at a given value, only ... is allowed, since the distinction between ... and ..< makes no sense there.

Combined Class and Protocol Types

Sometimes you need an object which both subclasses a class and conforms to a protocol. For example, you might need a UITableViewController that also implements KittenProvider. Swift 3 had no way to express this idea, requiring various ugly workarounds. Interestingly, Objective-C is able to express this idea:

UITableViewController<KittenProvider> *object;

Swift 4 can now express this concept as well by using the & symbol. This could already be used to combine multiple protocols into a single type, and now it can also be used to combine protocols with a class:

let object: UITableViewController & KittenProvider

Note that only one class can be included in any such type, since you can't subclass more than one class at a time anyway.

Generic Subscripts

Swift has supported generic methods forever, but before Swift 4 it did not support generic subscripts. You could overload subscripts by implementing more than one with different types, but you couldn't use generics. Now you can!

subscript<T: Hashable>(key: T) -> Value?

Generics are fully supported, so you can use things like where clauses:

subscript<S: Sequence>(key: S) -> [Value] where S.Element == Key

Just like methods, the generic type can be used as a return value as well:

subscript<T>(key: Key) -> T?

This could be really handy for dynamically-typed containers, such as when dealing with JSON objects.

Codable

Speaking of JSON, perhaps the biggest new feature in Swift 4 is the Codable protocol. The compiler will now auto-generate serialization and deserialization code for your types, and all you have to do is declare conformance to Codable.

Imagine you have a Person type:

struct Person {
    var name: String
    var age: Int
    var quest: String
}

If you wanted to read and write Person values to and from JSON, you previously had to write a bunch of annoying repetitive code to do so.

In Swift 4, you can make this happen by adding half a line:

struct Person: Codable {

If for some reason you only want to support encoding or decoding, but not both, you can declare conformance to Encodable or Decocable separately:

struct EncodablePerson: Encodable { ... }

struct DecodablePerson: Decodable { ... }

Conforming to Codable is just a shortcut for conforming to both.

Using a Codable type requires an encoder or decoder, which determines the serialization format and how Swift values are translated to and from serialized values. Swift provides encoders and decoders for JSON and property lists, and Foundation's archivers also support Codable types.

To encode something as JSON, create a JSONEncoder and call its encode method:

let jsonEncoder = JSONEncoder()
let data = try jsonEncoder.encode(person)

To decode, create a JSONDecoder and call decode, passing it the type you want to decode and the data to decode from:

let jsonDecoder = JSONDecoder()
let decodedPerson = try jsonDecoder.decode(Person.self, from: data)

Note that encoding and decoding methods are marked as throws because there are a lot of potential errors that can occur during the process, such as type mismatches or incomplete data, so you'll need to add try to these calls and catch the errors they throw.

Since JSON doesn't natively support dates or binary data, those values need to be converted to/from some other JSON representation. For example, it's common to use base64 encoding for data. JSONEncoder and JSONDecoder can be customized with different strategies for handling these values. For example, if you want to encode dates as ISO-8601 and data as base64:

let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .iso8601
jsonEncoder.dataEncodingStrategy = .base64
let data = try jsonEncoder.encode(person)

And on the decode side:

let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .iso8601
jsonDecoder.dataDecodingStrategy = .base64
let decodedPerson = try jsonDecoder.decode(Person.self, from: data)

They also provide the option for providing a totally custom strategy by writing your own code.

Property list coding is similar. Use PropertyListEncoder and PropertyListDecoder instead of the JSON coders. Since property lists can natively represent dates and binary data, the property list coders don't provide those options.

If you're already using NSCoding then you can mix and match it with Codable so that you don't have to change everything at once. NSKeyedArchiver provides a new encodeEncodable method which takes any Encodable type and encodes it under the given key. NSKeyedUnarchiver provides a corresponding decodeDecodable which can decode any Decodable type.

Codable is a flexible protocol with lots of room for custom behavior. The compiler provides a default implementation, but you can provide your own if you need different behavior. This makes it straightforward to write implementations that migrate old data into new types, use different names in the serialized representation than in the source code, or make other customizations.

A full discussion of all the possibilities of Codable is beyond the scope of this article, but you can read more about it in my Friday Q&A post about Swift.Codable.

Wrapping Up

Swift 4 hasn't brought us the dramatic changes we've seen in earlier years, but it's a solid improvement. Codable will make some really common tasks a lot easier, and it's probably my favorite feature out of the bunch. Other features like multi-line string literals and generic subscripts won't have the same impact, but put together they should make for nice improvement in the code we write.