Xcode 9.3 and Swift 4.1 are finally out of beta! This release contains some long-awaited improvements to the standard library and language itself. If you haven’t been following the Swift Evolution Process closely, keep reading.
In this tutorial, you’ll learn about the most significant changes introduced in Swift 4.1.
This article requires Xcode 9.3, so make sure you have it installed and ready to go before getting started.
Getting Started
Swift 4.1 is source-compatible with Swift 4, so the new features won’t break your code if you’ve already migrated your project to Swift 4 using the Swift Migrator in Xcode.
In the sections below, you’ll see linked tags such as [SE-0001]. These are Swift Evolution proposal numbers. I’ve included the link to each proposal so you can dig into the full details of each particular change. I recommend you try out the features in a playground so you have a better understanding of everything that changes as you work.
To start, fire up Xcode 9.3 and select File ▸ New ▸ Playground. Choose iOS as the platform and Blank as its template. Name and save it as you like. To get the most out of this tutorial, try out each feature in your new playground as you work.
Language Improvements
There are a number of language improvements in this release, including conditional conformance, recursive constraints on associated types in protocols and more.
Conditional Conformance
Conditional conformance enables protocol conformance for generic types where the type arguments satisfy certain conditions [SE-0143]. This is a powerful feature that makes your code more flexible. You can see how it works with a few examples.
Conditional conformance in the standard library
In Swift 4, you could compare arrays, dictionaries and optionals as long as their elements were Equatable
. This worked absolutely fine for basic scenarios such as:
// Arrays of Int
let firstArray = [1, 2, 3]
let secondArray = [1, 2, 3]
let sameArray = firstArray == secondArray
// Dictionaries with Int values
let firstDictionary = ["Cosmin": 10, "George": 9]
let secondDictionary = ["Cosmin": 10, "George": 9]
let sameDictionary = firstDictionary == secondDictionary
// Comparing Int?
let firstOptional = firstDictionary["Cosmin"]
let secondOptional = secondDictionary["Cosmin"]
let sameOptional = firstOptional == secondOptional
Using the ==
operator to test equality in these examples worked since Int
is Equatable
in Swift 4. However, comparing collections of optionals was a common situation you might have run into with Swift 4 since optionals do not conform to Equatable
. Swift 4.1 fixes this issue using conditional conformance, letting optional types with underlying Equatable
types to be compared:
// Array of Int?
let firstArray = [1, nil, 2, nil, 3, nil]
let secondArray = [1, nil, 2, nil, 3, nil]
let sameArray = firstArray == secondArray
// Dictionary with Int? values
let firstDictionary = ["Cosmin": 10, "George": nil]
let secondDictionary = ["Cosmin": 10, "George": nil]
let sameDictionary = firstDictionary == secondDictionary
// Comparing Int?? (Optional of Optional)
let firstOptional = firstDictionary["Cosmin"]
let secondOptional = secondDictionary["Cosmin"]
let sameOptional = firstOptional == secondOptional
Int?
is Equatable
in Swift 4.1, so the ==
operator works for [Int?]
, [String: Int?]
and Int??
.
A similar problem has been solved when comparing arrays of arrays (e.g. [[Int]]
). In Swift 4, you could only compare arrays of sets (e.g. [Set<Int>]
), since sets conform to Equatable
. Swift 4.1 solves this, since arrays (and dictionaries) are Equatable
as long as their underlying values are, too.
let firstArrayOfSets = [Set([1, 2, 3]), Set([1, 2, 3])]
let secondArrayOfSets = [Set([1, 2, 3]), Set([1, 2, 3])]
// Will work in Swift 4 and Swift 4.1
// since Set<Int> is Equatable
firstArrayOfSets == secondArrayOfSets
let firstArrayOfArrays = [[1, 2, 3], [3, 4, 5]]
let secondArrayOfArrays = [[1, 2, 3], [3, 4, 5]]
// Caused an error in Swift 4, but works in Swift 4.1
// since Arrays are Equatable in Swift 4.1
firstArrayOfArrays == secondArrayOfArrays
Generally, Swift 4.1’s Optional
, Array
and Dictionary
now conform to Equatable
and Hashable
whenever their underlying values or elements conform to these protocols.
This is how conditional conformance works in the standard library. Next, you will implement it in your own code.
Conditional conformance in code
You’re going to use conditional conformance to create your own band of musical instruments. Add the following block of code at the bottom of the playground to get started:
// 1
class LeadInstrument: Equatable {
let brand: String
init(brand: String) {
self.brand = brand
}
func tune() -> String {
return "Standard tuning."
}
static func ==(lhs: LeadInstrument, rhs: LeadInstrument) -> Bool {
return lhs.brand == rhs.brand
}
}
// 2
class Keyboard: LeadInstrument {
override func tune() -> String {
return "Keyboard standard tuning."
}
}
// 3
class Guitar: LeadInstrument {
override func tune() -> String {
return "Guitar standard tuning."
}
}
Here’s what’s this does step-by-step:
LeadInstrument
conforms toEquatable
. It has a certain brand and a method named tune() that you’ll eventually use to tune the instrument.- You override
tune()
inKeyboard
to return keyboard standard tuning. - You do the same thing for
Guitar
.
Next, declare the band of instruments:
// 1
class Band<LeadInstrument> {
let name: String
let lead: LeadInstrument
init(name: String, lead: LeadInstrument) {
self.name = name
self.lead = lead
}
}
// 2
extension Band: Equatable where LeadInstrument: Equatable {
static func ==(lhs: Band<LeadInstrument>, rhs: Band<LeadInstrument>) -> Bool {
return lhs.name == rhs.name && lhs.lead == rhs.lead
}
}
Here’s what you’re doing step-by-step:
- You create a class called
Band
with a generic type –LeadInstrument
. Each band has an unique name and lead instrument. - You use
where
to constrainBand
to conform toEquatable
as long asLeadInstrument
does. Your ability to conform theBand
‘s genericLeadInstrument
toEquatable
is exactly where conditional conformance comes into play.
Next, define your favorite bands and compare them:
// 1
let rolandKeyboard = Keyboard(brand: "Roland")
let rolandBand = Band(name: "Keys", lead: rolandKeyboard)
let yamahaKeyboard = Keyboard(brand: "Yamaha")
let yamahaBand = Band(name: "Keys", lead: yamahaKeyboard)
let sameBand = rolandBand == yamahaBand
// 2
let fenderGuitar = Guitar(brand: "Fender")
let fenderBand = Band(name: "Strings", lead: fenderGuitar)
let ibanezGuitar = Guitar(brand: "Ibanez")
let ibanezBand = Band(name: "Strings", lead: ibanezGuitar)
let sameBands = fenderBand == ibanezBand
In this piece of code, you create two Keyboard
s and Guitar
s along with their appropriate Band
s. You then compare the bands directly, thanks to the conditional conformance you defined earlier.
Conditional conformance in JSON parsing
Arrays, dictionaries, sets and optionals conform to Codable
if their elements conform to Codable
in Swift 4.1. Add the following code to your playground to try this:
struct Student: Codable, Hashable {
let firstName: String
let averageGrade: Int
}
let cosmin = Student(firstName: "Cosmin", averageGrade: 10)
let george = Student(firstName: "George", averageGrade: 9)
let encoder = JSONEncoder()
// Encode an Array of students
let students = [cosmin, george]
do {
try encoder.encode(students)
} catch {
print("Failed encoding students array: \(error)")
}
// Encode a Dictionary with student values
let studentsDictionary = ["Cosmin": cosmin, "George": george]
do {
try encoder.encode(studentsDictionary)
} catch {
print("Failed encoding students dictionary: \(error)")
}
// Encode a Set of students
let studentsSet: Set = [cosmin, george]
do {
try encoder.encode(studentsSet)
} catch {
print("Failed encoding students set: \(error)")
}
// Encode an Optional Student
let optionalStudent: Student? = cosmin
do {
try encoder.encode(optionalStudent)
} catch {
print("Failed encoding optional student: \(error)")
}
You use this code to encode [Student]
, [String: Student]
, Set<Student>
and Student?
. This works smoothly in Swift 4.1 since Student
is Codable
, which makes these collection types conform to it as well.
Convert Between Camel Case and Snake Case During JSON Encoding
Swift 4.1 lets you convert CamelCase properties to snake_case keys during JSON encoding:
var jsonData = Data()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted
do {
jsonData = try encoder.encode(students)
} catch {
print(error)
}
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
When creating your encoder, you set keyEncodingStrategy
to .convertToSnakeCase
. Looking at your console, you should see:
[
{
"first_name" : "Cosmin",
"average_grade" : 10
},
{
"first_name" : "George",
"average_grade" : 9
}
]
You can also go back from snake case keys to camel case properties during JSON decoding:
var studentsInfo: [Student] = []
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
studentsInfo = try decoder.decode([Student].self, from: jsonData)
} catch {
print(error)
}
for studentInfo in studentsInfo {
print("\(studentInfo.firstName) \(studentInfo.averageGrade)")
}
This time, you set keyDecodingStrategy
to .convertFromSnakeCase
.
Equatable and Hashable Protocols Conformance
Swift 4 required you to write boilerplate code to make structs conform to Equatable
and Hashable
:
struct Country: Hashable {
let name: String
let capital: String
static func ==(lhs: Country, rhs: Country) -> Bool {
return lhs.name == rhs.name && lhs.capital == rhs.capital
}
var hashValue: Int {
return name.hashValue ^ capital.hashValue &* 16777619
}
}
Using this code, you implemented ==(lhs:rhs:)
and hashValue
to support both Equatable
and Hashable
. You could compare countries, add them to sets and even use them as dictionary keys:
let france = Country(name: "France", capital: "Paris")
let germany = Country(name: "Germany", capital: "Berlin")
let sameCountry = france == germany
let countries: Set = [france, germany]
let greetings = [france: "Bonjour", germany: "Guten Tag"]
Swift 4.1 adds default implementations in structs for Equatable
and Hashable
as long as all of their properties are Equatable
and Hashable
as well [SE-0185].
This highly simplifies your code, which can simply be rewritten as:
struct Country: Hashable {
let name: String
let capital: String
}
Enumerations with associated values also needed extra code to work with Equatable
and Hashable
in Swift 4:
enum BlogPost: Hashable {
case tutorial(String, String)
case article(String, String)
static func ==(lhs: BlogPost, rhs: BlogPost) -> Bool {
switch (lhs, rhs) {
case let (.tutorial(lhsTutorialTitle, lhsTutorialAuthor), .tutorial(rhsTutorialTitle,
rhsTutorialAuthor)):
return lhsTutorialTitle == rhsTutorialTitle && lhsTutorialAuthor == rhsTutorialAuthor
case let (.article(lhsArticleTitle, lhsArticleAuthor), .article(rhsArticleTitle, rhsArticleAuthor)):
return lhsArticleTitle == rhsArticleTitle && lhsArticleAuthor == rhsArticleAuthor
default:
return false
}
}
var hashValue: Int {
switch self {
case let .tutorial(tutorialTitle, tutorialAuthor):
return tutorialTitle.hashValue ^ tutorialAuthor.hashValue &* 16777619
case let .article(articleTitle, articleAuthor):
return articleTitle.hashValue ^ articleAuthor.hashValue &* 16777619
}
}
}
You used the enumeration’s cases to write implementations for ==(lhs:rhs:)
and hashValue
. This enabled you to compare blog posts and use them in sets and dictionaries:
let swift3Article = BlogPost.article("What's New in Swift 3.1?", "Cosmin Pupăză")
let swift4Article = BlogPost.article("What's New in Swift 4.1?", "Cosmin Pupăză")
let sameArticle = swift3Article == swift4Article
let swiftArticlesSet: Set = [swift3Article, swift4Article]
let swiftArticlesDictionary = [swift3Article: "Swift 3.1 article", swift4Article: "Swift 4.1 article"]
As the case was with Hashable
, this code’s size is vastly reduced in Swift 4.1 thanks to default Equatable
and Hashable
implementations:
enum BlogPost: Hashable {
case tutorial(String, String)
case article(String, String)
}
You just saved yourself from maintaining 20 lines of boilerplate code!
Hashable Index Types
Key paths may have used subscripts if the subscript parameter’s type was Hashable
in Swift 4. This enabled them to work with arrays of double
; for example:
let swiftVersions = [3, 3.1, 4, 4.1]
let path = \[Double].[swiftVersions.count - 1]
let latestVersion = swiftVersions[keyPath: path]
You use keyPath
to get the current Swift version number from swiftVersions
.
Swift 4.1 adds Hashable
conformance to all index types in the standard library [SE-0188]:
let me = "Cosmin"
let newPath = \String.[me.startIndex]
let myInitial = me[keyPath: newPath]
The subscript returns the first letter of the string. It works since String
index types are Hashable
in Swift 4.1.
Recursive Constraints on Associated Types in Protocols
Swift 4 didn’t support defining recursive constraints on associated types in protocols:
protocol Phone {
associatedtype Version
associatedtype SmartPhone
}
class IPhone: Phone {
typealias Version = String
typealias SmartPhone = IPhone
}
In this example, you defined a SmartPhone
associated type, but it might have proved useful to constrain it to Phone
, since all smartphones are phones. This is now possible in Swift 4.1 [SE-0157]:
protocol Phone {
associatedtype Version
associatedtype SmartPhone: Phone where SmartPhone.Version == Version, SmartPhone.SmartPhone == SmartPhone
}
You use where
to constrain both Version
and SmartPhone
to be the same as the phone’s.
Weak and Unowned References in Protocols
Swift 4 supported weak
and unowned
for protocol properties:
class Key {}
class Pitch {}
protocol Tune {
unowned var key: Key { get set }
weak var pitch: Pitch? { get set }
}
class Instrument: Tune {
var key: Key
var pitch: Pitch?
init(key: Key, pitch: Pitch?) {
self.key = key
self.pitch = pitch
}
}
You tuned an instrument in a certain key
and pitch
. The pitch may have been nil
, so you’d model it as weak
in the Tune
protocol.
But both weak
and unowned
are practically meaningless if defined within the protocol itself, so Swift 4.1 removes them and you will get a warning using these keywords in a protocol [SE-0186]:
protocol Tune {
var key: Key { get set }
var pitch: Pitch? { get set }
}
Index Distances in Collections
Swift 4 used IndexDistance
to declare the number of elements in a collection:
func typeOfCollection<C: Collection>(_ collection: C) -> (String, C.IndexDistance) {
let collectionType: String
switch collection.count {
case 0...100:
collectionType = "small"
case 101...1000:
collectionType = "medium"
case 1001...:
collectionType = "big"
default:
collectionType = "unknown"
}
return (collectionType, collection.count)
}
typeOfCollection(_:)
returned a tuple, which contained the collection’s type and count. You could use it for any kind of collections like arrays, dictionaries or sets; for example:
typeOfCollection(1...800) // ("medium", 800)
typeOfCollection(greetings) // ("small", 2)
You could improve the function’s return type by constraining IndexDistance
to Int
with a where
clause:
func typeOfCollection<C: Collection>(_ collection: C) -> (String, Int) where C.IndexDistance == Int {
// same code as the above example
}
Swift 4.1 replaces IndexDistance
with Int
in the standard library, so you don’t need a where
clause in this case [SE-0191]:
func typeOfCollection<C: Collection>(_ collection: C) -> (String, Int) {
// same code as the above example
}
Structure Initializers in Modules
Adding properties to public
structs could lead to source-breaking changes in Swift 4. For this tutorial, make sure the Project Navigator is visible in Xcode by going to View\Navigators\Show Project Navigator. Next, right-click on Sources and select New File from the menu. Rename the file DiceKit.swift. Replace its contents with the following block of code:
public struct Dice {
public let firstDie: Int
public let secondDie: Int
public init(_ value: Int) {
let finalValue: Int
switch value {
case ..<1:
finalValue = 1
case 6...:
finalValue = 6
default:
finalValue = value
}
firstDie = finalValue
secondDie = 7 - finalValue
}
}
The struct's initializer makes sure both dice have valid values between 1 and 6. Switch back to the playground and add this code at the end of it:
// 1
let dice = Dice(0)
dice.firstDie
dice.secondDie
// 2
extension Dice {
init(_ firstValue: Int, _ secondValue: Int) {
firstDie = firstValue
secondDie = secondValue
}
}
// 3
let newDice = Dice(0, 7)
newDice.firstDie
newDice.secondDie
Here's what you did with this code:
- You created a valid pair of dice.
- You extended
Dice
with another initializer that has direct access to its properties. - You defined an invalid pair of dice with the struct's new initializer.
In Swift 4.1, cross-target initializers should call the default one. Change your extension on Dice
to:
extension Dice {
init(_ firstValue: Int, _ secondValue: Int) {
self.init(abs(firstValue - secondValue))
}
}
This change makes structs behave like classes: cross-module initializers must be convenience initializers in Swift 4.1 [SE-0189].
Platform Settings and Build Configuration Updates
Swift 4.1 adds some much-needed platform and build features for code testing:
Build Imports
In Swift 4, you tested if a module was available on a certain platform by checking the operating system itself; for example:
#if os(iOS) || os(tvOS)
import UIKit
print("UIKit is available on this platform.")
#else
print("UIKit is not available on this platform.")
#endif
UIKit
is available on iOS and tvOS, so you imported it if the test succeeded. Swift 4.1 further simplifies this by letting you check for the module itself instead:
#if canImport(UIKit)
print("UIKit is available if this is printed!")
#endif
In Swift 4.1, you use #if canImport(UIKit)
to confirm a certain framework is available for importing [SE-0075].
Target Environments
When writing Swift 4 code, the most well-known way to check whether you were running on a simulator or a physical device was by checking both the architecture and operation system:
#if (arch(i386) || arch(x86_64)) && (os(iOS) || os(tvOS) || os(watchOS))
print("Testing in the simulator.")
#else
print("Testing on the device.")
#endif
If your architecture was Intel-based and your operating system was iOS, tvOS or watchOS, you were testing in the simulator. Otherwise, you were testing on the device.
This test was very cumbersome and was also very non-descriptive of the issue at hand. Swift 4.1 makes this test much more straightforward; just use targetEnvironment(simulator)
[SE-0190] like so:
#if targetEnvironment(simulator)
print("Testing in the simulator.")
#endif
Miscellaneous Bits and Pieces
There are a few other updates in Swift 4.1 that are worth knowing:
Compacting Sequences
In Swift 4, it was fairly common to use flatMap(_:)
to filter out nil
values from a sequence:
let pets = ["Sclip", nil, "Nori", nil]
let petNames = pets.flatMap { $0 } // ["Sclip", "Nori"]
Unfortunately, flatMap(_:)
was overloaded in various ways and, in that specific scenario, the flatMap(_:)
naming wasn't very descriptive of the action taken.
For these reasons, Swift 4.1 introduces a rename of flatMap(_:)
to compactMap(_:)
to make its meaning clearer and unique [SE-0187]:
let petNames = pets.compactMap { $0 }
Unsafe Pointers
Swift 4 used temporary unsafe mutable pointers to create and mutate unsafe mutable buffer pointers:
let buffer = UnsafeMutableBufferPointer<Int>(start: UnsafeMutablePointer<Int>.allocate(capacity: 10),
count: 10)
let mutableBuffer = UnsafeMutableBufferPointer(start: UnsafeMutablePointer(mutating: buffer.baseAddress),
count: buffer.count)
Swift 4.1 lets you work with unsafe mutable buffer pointers directly, using the same approach as with unsafe mutable pointers [SE-0184]:
let buffer = UnsafeMutableBufferPointer<Int>.allocate(capacity: 10)
let mutableBuffer = UnsafeMutableBufferPointer(mutating: UnsafeBufferPointer(buffer))
New Playground Features
Swift 4 allowed you to customize type descriptions in Xcode playgrounds:
class Tutorial {}
extension Tutorial: CustomPlaygroundQuickLookable {
var customPlaygroundQuickLook: PlaygroundQuickLook {
return .text("raywenderlich.com tutorial")
}
}
let tutorial = Tutorial()
You implemented CustomPlaygroundQuickLookable
for Tutorial
to return a custom quick-look playground description. The description’s type in customPlaygroundQuickLook
was limited to PlaygroundQuickLook
cases. This is no longer the case (pun intended) in Swift 4.1:
extension Tutorial: CustomPlaygroundDisplayConvertible {
var playgroundDescription: Any {
return "raywenderlich.com tutorial"
}
}
You implement CustomPlaygroundDisplayConvertible
this time. The description’s type is Any
now, so you can return anything from playgroundDescription
. This simplifies your code and makes it more flexible [SE-0198].
Where to Go From Here?
You can download the final playground using the Download Materials link at either the top or bottom of this tutorial.
Swift 4.1 polishes up some Swift 4 features in preparation for more serious changes that will be coming in Swift 5 later this year. These include ABI stability, improved generics and strings, new memory ownership and concurrency models and more.
If you're feeling adventurous, head over and look at the Swift standard library diffs or to the official Swift CHANGELOG where you can read more information about all changes in this version. You can also use this to keep an eye out for what's coming in Swift 5!
If you're curious about what changes are coming in Swift 5 and beyond, we also recommend that you check out Swift Evolution proposals, where you can see which new features, changes and additions are being proposed. If you're really keen, why not give feedback on one of the current proposals under review or even pitch a proposal yourself!
What do you like or dislike about Swift 4.1 so far? Let us know in the forum discussion below!
The post What’s New in Swift 4.1? appeared first on Ray Wenderlich.