What's new in Swift 4.1?

Original author: Cosmin Pupăză
  • Transfer
Xcode 9.3 and Swift 4.1 are finally no longer beta! This release contains the long-awaited improvements to the standard library and the language itself. If you are not following the Swift Evolution process , then this article is for you.

Swift 4.1

In this article, you will learn about the most significant changes introduced in Swift 4.1 .

This article requires Xcode 9.3, so make sure that this version of Xcode is installed.

First steps


Swift 4.1 is compatible with the source code of Swift 4, so new features do not violate your code if you have already migrated your project to Swift 4 using Swift Migrator in Xcode.

In the following sections, you will see related tags, such as [SE-0001]. These are Swift Evolution offer numbers . I added a link to each proposal so that you can delve into the full details of each specific change. I recommend that you try the functions in practice using the Playground, in order to better understand what exactly will change in your work.

To get started, launch Xcode 9.3 and select File ▸ New ▸ Playground . Choose iOS as the platform and Blankas a template. Name and save it as you wish. To get the most out of this article, try practicing every feature on the Playground.

Note: If you missed what was changed in Swift 4 and are going to catch up? No problems! Check out Swift 4 by reading What's New in Swift 4 .

Language improvements


This release has a number of language improvements, including conditional matching, recursive restrictions on related types in protocols, and more.

Conditional Compliance


Conditional matching allows protocol matching for generic types, where type arguments satisfy certain conditions [SE-0143] . This is a powerful feature that makes code more flexible. You can see how it works with a few examples.

Conditional matching in the standard library


In Swift 4, you can compare arrays, dictionaries, and options if their elements comply with the Equatable protocol . This worked perfectly 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 verify equality in these examples was quite fair, since Int is Equatable in Swift 4. However, comparing option sets is a common situation that you might encounter in Swift 4, since options are not compliant with the Equatable protocol . Swift 4.1 corrects this problem using conditional matching, allowing you to compare additional types with those that underlie Equatable :

// 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 was solved when comparing arrays (for example, [[Int]]). In Swift 4, you can compare arrays of sets (for example, [Set]), since the sets conform to the Equatable protocol . Swift 4.1 solves this, because arrays (and dictionaries), as well as their underlying values, are Equatable .

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 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

Typically, Optional , Array, and Dictionary in Swift 4.1 now conform to the Equatable and Hashable protocols , whenever their base values ​​or elements conform to these protocols.

Here is an example of how conditional matching works in a standard library. Then you implement this in your code.

Conditional compliance in practice


We are now using conditional matching to create our own musical instrument group. Add the following block of code to the Playground .

// 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 is what this code does step by step:

  1. The LeadInstrument class conforms to the Equatable protocol. It has a specific brand and a tune () method that you will use to tune the tool.
  2. You override the tune () method in the Keyboard class to return the default settings for the object.
  3. You do the same for the Guitar class.

Then declare the tool group:

// 1  
class Band {
  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, rhs: Band) -> Bool {
    return lhs.name == rhs.name && lhs.lead == rhs.lead
  }
}

Here is what you do step by step:

  1. You create a class of Band type - LeadInstrument. Each group has a unique name (lead) and lead instrument (main instrument).
  2. You use where to make the Band conform to the Equatable protocol, just like LeadInstrument does certain things. This is where conditional matching manifests itself - you can assign compliance to the Equatable protocol for the generic LeadInstruments.

Then create your favorite tool groups 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 code snippet, you create two Keyboards and a Guitar along with their respective Bands. Then you compare the Bands directly, thanks to the conditional matching that you defined earlier.

Conditional match in JSON parsing


In Swift 4.1, arrays, dictionaries, sets, and add-ons conform to the Codable protocol if their elements also conform to this protocol. Add the following code to the Playground:

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)")
}

Do you use this code for encode [Student] , [String: Student] , Set and Student? . This code works well in Swift 4.1 since Student is Codable , which makes these collection types also Codable appropriate .

Converting between CamelCase and Snake_Case when working with JSON


Swift 4.1 allows you to convert CamelCase properties to snake_case keys during JSON parsing:

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 you create an encoder object, you set the keyEncodingStrategy property to .convertToSnakeCase . Looking at the console, you should see:

[
  {
    "first_name" : "Cosmin",
    "average_grade" : 10
  },
  {
    "first_name" : "George",
    "average_grade" : 9
  }
]

You can also convert back from snake_case to CamelCase while working with JSON:

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, for the keyDecodingStrategy property , you assign the value .convertFromSnakeCase .

Compliance and Compatibility of Equatable and Hashable Protocols


Swift 4 required you to write boilerplate code so that the structures conform to the Equatable and Hashable protocols :

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 implement == (lhs: rhs :) and hashValue to support both Equatable and Hashable . You can compare Country objects , add them to Set’s and even use them as keys for a dictionary:

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 for the structures corresponding to Equatable and Hashable , since all its properties are also Equalable and Hashable [ SE-0185 ].

This greatly simplifies your code, which can simply be rewritten as:

struct Country: Hashable {
  let name: String
  let capital: String
}

Enumerations with related values ​​also require additional 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 enumeration cases to write implementations == (lhs: rhs :) and hashValue . This allowed 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"]

Unlike Hashable , the size of this code is significantly smaller in Swift 4.1, thanks to the implementations of Equatable and Hashable :

enum BlogPost: Hashable {
  case tutorial(String, String)
  case article(String, String)
}

You just saved yourself from working with 20 lines of template code!

image

Hashable Index Types


Key paths could use indexes if the index parameter type was Hashable in Swift 4. This allowed them to work with double arrays; eg:

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 matching to all subscript types in the standard library [ SE-0188 ]:

let me = "Cosmin"
let newPath = \String.[me.startIndex]
let myInitial = me[keyPath: newPath]

By index, the first letter of the string is returned. It works because String index types are Hashable in Swift 4.1.

Recursive restrictions on related types in protocols


Swift 4 did not support defining recursive constraints on related types in protocols:

protocol Phone {
  associatedtype Version
  associatedtype SmartPhone
}
class IPhone: Phone {
  typealias Version = String
  typealias SmartPhone = IPhone
}

In this example, you have identified the type associated with the SmartPhone, but it might be useful to limit it to Phone, since all smartphones are phones. Now it is 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 restrict both Version and SmartPhone so that they are the same as the phone.

Weak and not busy links in protocols


Swift 4 supports weak and non- unowned 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 the instrument in a specific key and pitch. The step may have been zero, so you will simulate it as weak in the Tune protocol.

But both weak and unowned are almost meaningless if they are defined in the protocol itself, so Swift 4.1 deletes them, and you will receive a warning using these keywords in the 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 the collection:

func typeOfCollection(_ 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)
}

The typeOfCollection (_ :) method returns a tuple that contains the type and amount of the collection. You can use it for any collections, such as arrays, dictionaries or collections; eg:

typeOfCollection(1...800) // ("medium", 800)
typeOfCollection(greetings) // ("small", 2)

You can improve the return type of a function by restricting IndexDistance to Int with a where clause :

func typeOfCollection(_ collection: C) -> (String, Int) where C.IndexDistance == Int {
  // тот же код, что и в приведенном выше примере
}

Swift 4.1 replaces IndexDistance with Int in the standard library, so in this case you do not need the where clause [ SE-0191 ]:

func typeOfCollection(_ collection: C) -> (String, Int) {
  // тот же код, что и в приведенном выше примере
}

Structure initializers in modules


Adding properties to public structures can lead to initial changes in Swift 4. In this article, make sure Project Navigator is visible in Xcode by going to View \ Navigators \ Show Project Navigator. Then right-click “Sources” and select “New File” from the menu. Rename the DiceKit.swift file . Replace its contents with the following code block:

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 structure initializer ensures that both dice have valid values ​​between 1 and 6. Return to the Playground and add this code at the end:

// 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 is what you did with this code:

  1. You have created a valid pair of dice.
  2. You added Dice through another initializer that has direct access to its properties.
  3. You have identified an invalid pair of dice with a new structure initializer.

In Swift 4.1, cross-target initializers must cause a default value. Change the Dice extension to:

extension Dice {
  init(_ firstValue: Int, _ secondValue: Int) {
    self.init(abs(firstValue - secondValue))
  }
}

This change causes structures to behave like classes: cross-module initializers must be convenience initializers in Swift 4.1 [ SE-0189 ].

image

In Swift 4.1, you can no longer cheat the dice game!

Platform Settings and Configuration Updates


Swift 4.1 adds some of the necessary platform and build features for code testing:

Build Imports


In Swift 4, you tested a module if it is available on a specific platform, defining 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 was successful. Swift 4.1 simplifies this process by letting you check the module itself:

#if canImport(UIKit)
print("UIKit is available if this is printed!")
#endif

In Swift 4.1, you use #if canImport (UIKit) to confirm that a specific structure is available for import [ SE-0075 ].

Target environments


When writing code in Swift 4, the most famous way to check code execution on a simulator or physical device was to check the architecture and operating 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

Was your processor architecture based on Intel, and your operating system - iOS, tvOS or watchOS, you tested in the simulator. Otherwise, you tested the device.
This test was very cumbersome, and it also did not fully describe the type of errors. Swift 4.1 makes this test easier; just use targetEnvironment (simulator) [SE-0190] as follows:

#if targetEnvironment(simulator)
  print("Testing in the simulator.")
#endif

Miscellaneous Bits and Pieces


There are a few other updates to Swift 4.1 that are worth knowing:

Compacting Sequences


In Swift 4, it was fairly common to use flatMap (_ :) to filter 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 this particular scenario, the assignment of flatMap (_ :) did not describe the actions taken very well.

For these reasons, Swift 4.1 introduces the renaming of flatMap (_:) to compactMap (_ :) to make its meaning more clear and unique [ SE-0187 ]:

let petNames = pets.compactMap {$ 0}

Unsafe Pointers / Unsafe Pointers


Swift 4 used temporary unsafe mutable pointers to create and modify unsafe mutable buffer pointers:

let buffer = UnsafeMutableBufferPointer(start: UnsafeMutablePointer.allocate(capacity: 10), 
                                             count: 10)
let mutableBuffer = UnsafeMutableBufferPointer(start: UnsafeMutablePointer(mutating: buffer.baseAddress), 
                                               count: buffer.count)

Swift 4.1 allows you to work with unsafe mutable buffer pointers directly, using the same approach as with unsafe mutable buffer pointers [ SE-0184 ]:

New Playground Features


Swift 4 allowed you to customize type descriptions in Playground Xcode:

class Tutorial {}
extension Tutorial: CustomPlaygroundQuickLookable {
  var customPlaygroundQuickLook: PlaygroundQuickLook {
    return .text("raywenderlich.com tutorial")
  }
}
let tutorial = Tutorial()

You have implemented CustomPlaygroundQuickLookable for the Tutorial and return a short description. The description type in customPlaygroundQuickLook was limited to PlaygroundQuickLook cases . such a pun in Swift 4.1 is no longer there:

extension Tutorial: CustomPlaygroundDisplayConvertible {
  var playgroundDescription: Any {
    return "raywenderlich.com tutorial"
  }
}

This time you are implementing CustomPlaygroundDisplayConvertible . The description type is Any , so you can return anything from the playgroundDescription. This simplifies your code and makes it more flexible [ SE-0198 ].

What next?


Swift 4.1 enhances some of the features of Swift 4 in preparation for the more serious changes that will appear in Swift 5 this year. These include ABI stability, improved generics and strings, new patterns of memory ownership and concurrency, and much more.

If you feel like an adventurer, go and look at the standard Swift library or the official Swift CHANGELOG website , where you can read more information about all the changes in this version.

If you are interested in what changes will be in Swift 5, we also recommend that you familiarize yourself with the Swift Evolution offers , where you can see new features, changes and additions.

Also popular now: