What's new in Swift 4, 4.1 and 4.2

What's new in Swift 4, 4.1 and 4.2

I continue my journey exploring what has been introduced in Swift 4, 4.1 and 4.2.

Strings

Strings are now seen as array of characters, so it's possible to loop them and invoke the usual methods usually available for collections:

let galaxy = "Milky Way 🐮"
for char in galaxy {
  print(char)
}

galaxy.count       // 11
galaxy.isEmpty     // false
galaxy.dropFirst() // "ilky Way 🐮"
String(galaxy.reversed()) // "🐮 yaW ykliM"

// Filter out any none ASCII characters
galaxy.filter { char in
  let isASCII = char.unicodeScalars.reduce(true, { $0 && $1.isASCII })
  return isASCII
} // "Milky Way "

Also new is the StringProtocol to offer the same functionality of Strings also to Substrings:

// Grab a subsequence of String
let endIndex = galaxy.index(galaxy.startIndex, offsetBy: 3)
var milkSubstring = galaxy[galaxy.startIndex...endIndex]   // "Milk"
type(of: milkSubstring)   // Substring.Type

// Concatenate a String onto a Substring
milkSubstring += "🥛"     // "Milk🥛"

// Create a String from a Substring
let milkString = String(milkSubstring) // "Milk🥛"

Dictionary and Set

It's possible to instantiate a Dictionary from an array of Keys and an array of Value:

let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B", "Barnard's Star", "Wolf 359"]
let nearestStarDistances = [4.24, 4.37, 4.37, 5.96, 7.78]

// Dictionary from sequence of keys-values
let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances))
// ["Alpha Centauri A": 4.37, "Barnard\'s Star": 5.96, "Wolf 359": 7.78, "Proxima Centauri": 4.24, "Alpha Centauri B": 4.37]

It's also possible to automatically handle duplicates during instantiation:

// Random vote of people's favorite stars
let favoriteStarVotes = ["Alpha Centauri A", "Wolf 359", "Alpha Centauri A", "Barnard's Star"]

// Merging keys with closure for conflicts
let mergedKeysAndValues = Dictionary(zip(favoriteStarVotes, repeatElement(1, count: favoriteStarVotes.count)), uniquingKeysWith: +) // ["Barnard's Star": 1, "Alpha Centauri A": 2, "Wolf 359": 1]

Dictionaries and Sets support now filtering:

// Filtering results into dictionary rather than array of tuples
let closeStars = starDistanceDict.filter { $0.value < 5.0 }
closeStars // Dictionary: ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Alpha Centauri B": 4.37]

Dictionaries gain also mapping:

// Mapping values directly resulting in a dictionary
let mappedCloseStars = closeStars.mapValues { "\($0)" }
mappedCloseStars // ["Proxima Centauri": "4.24", "Alpha Centauri A": "4.37", "Alpha Centauri B": "4.37"]

During initialization, it's possible to pass a default value, to be returned instead of nil when a value is not found:

// Subscript with a default value
let siriusDistance = mappedCloseStars["Wolf 359", default: "unknown"] // "unknown"

// Subscript with a default value used for mutating
var starWordsCount: [String: Int] = [:]
for starName in nearestStarNames {
  let numWords = starName.split(separator: " ").count
  starWordsCount[starName, default: 0] += numWords // Amazing 
}
starWordsCount // ["Wolf 359": 2, "Alpha Centauri B": 3, "Proxima Centauri": 2, "Alpha Centauri A": 3, "Barnard's Star": 2]

Dictionary instantiation from Sequences plus grouping:

// Grouping sequences by computed key
let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }
starsByFirstLetter
// ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]

Sequences and Dictionaries can reserve capacity in advance:

// Improved Set/Dictionary capacity reservation
starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity

Private Access Modifier

Now the private access modifier works inside a type plus its extensions in the same file.
For example, in the following code, the extension can use the private variable, because the type and its protocol extension are defined in the same file:

struct SpaceCraft {
  private let warpCode: String

  init(warpCode: String) {
    self.warpCode = warpCode
  }
}

extension SpaceCraft {
  func goToWarpSpeed(warpCode: String) {
    if warpCode == self.warpCode { // Error in Swift 3 unless warpCode is fileprivate
      print("Do it Scotty!")
    }
  }
}

let enterprise = SpaceCraft(warpCode: "KirkIsCool")
//enterprise.warpCode  // error: 'warpCode' is inaccessible due to 'private' protection level
enterprise.goToWarpSpeed(warpCode: "KirkIsCool") // "Do it Scotty!"

API Additions

Codable

To serialize and deserialize to Json, Swift introduces the Codable protocol to serialize and deserialize (and Encodable and Decodable if you need only one.).

How to implement Codable:

struct CuriosityLog: Codable {
  enum Discovery: String, Codable {
    case rock, water, martian
  }

  var sol: Int
  var discoveries: [Discovery]
}

// Create a log entry for Mars sol 42
let logSol42 = CuriosityLog(sol: 42, discoveries: [.rock, .rock, .rock, .rock])

How to serialize:

let jsonEncoder = JSONEncoder() // One currently available encoder

// Encode the data
let jsonData = try jsonEncoder.encode(logSol42)
// Create a String from the data
let jsonString = String(data: jsonData, encoding: .utf8) // "{"sol":42,"discoveries":["rock","rock","rock","rock"]}"

How to deserialize:

let jsonDecoder = JSONDecoder() // Pair decoder to JSONEncoder

// Attempt to decode the data to a CuriosityLog object
let decodedLog = try jsonDecoder.decode(CuriosityLog.self, from: jsonData)
decodedLog.sol         // 42
decodedLog.discoveries // [rock, rock, rock, rock]

Key Paths

Swift 4 introduces the possibility to reference properties of objects:

struct Lightsaber {
  enum Color {
    case blue, green, red
  }
  let color: Color
}

class ForceUser {
  var name: String
  var lightsaber: Lightsaber
  var master: ForceUser?

  init(name: String, lightsaber: Lightsaber, master: ForceUser? = nil) {
    self.name = name
    self.lightsaber = lightsaber
    self.master = master
  }
}

let sidious = ForceUser(name: "Darth Sidious", lightsaber: Lightsaber(color: .red))
let obiwan = ForceUser(name: "Obi-Wan Kenobi", lightsaber: Lightsaber(color: .blue))
let anakin = ForceUser(name: "Anakin Skywalker", lightsaber: Lightsaber(color: .blue), master: obiwan)

So now you can create a key-path with a backslash, followed by type.property:

// Create reference to the ForceUser.name key path
let nameKeyPath = \ForceUser.name

// Access the value from key path on instance
let obiwanName = obiwan[keyPath: nameKeyPath]  // "Obi-Wan Kenobi"

A more complex example with sub-objects:

// Use keypath directly inline and to drill down to sub objects
let anakinSaberColor = anakin[keyPath: \ForceUser.lightsaber.color]  // blue

// Access a property on the object returned by key path
let masterKeyPath = \ForceUser.master
let anakinMasterName = anakin[keyPath: masterKeyPath]?.name  // "Obi-Wan Kenobi"

// Change Anakin to the dark side using key path as a setter
anakin[keyPath: masterKeyPath] = sidious
anakin.master?.name // Darth Sidious

// Note: not currently working, but works in some situations
// Append a key path to an existing path
//let masterNameKeyPath = masterKeyPath.appending(path: \ForceUser.name)
//anakin[keyPath: masterKeyPath] // "Darth Sidious"

Multi-line strings

You can define multi-line strings with 3 double-quotes:

let star = "⭐️"
let introString = """
  A long string,
  spanning multiple lines,
  with also an interpolated value: \(star)
  """

One-Sided Ranges

It's possible now to define ranges without one of the two boundaries:

// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..<planets.endIndex]
let firstThree = planets[..<4]          // Before: planets[planets.startIndex..<4]

Generic Subscripts

Subscripts can now be generic:

struct GenericDictionary<Key: Hashable, Value> {
  private var data: [Key: Value]

  init(data: [Key: Value]) {
    self.data = data
  }

  subscript<T>(key: Key) -> T? {
    return data[key] as? T
  }
}

So you can use the generic subscript without casting:

// Dictionary of type: [String: Any]
var earthData = GenericDictionary(data: ["name": "Earth", "population": 7500000000, "moons": 1])

// Automatically infers return type without "as? String"
let name: String? = earthData["name"]

// Automatically infers return type without "as? Int"
let population: Int? = earthData["population"]

Miscellanuos

Here other minor changes:

  • The MutableCollection has a new swapAt method;
  • Types associated to protocols can now be constrained with where;
  • A type can now conform to a class and to multiple protocols

Language improvements

Conditional conformance

Now it's possible to compare collections of Equatables even if they are optional:

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

Convert between Camel Case and Snake Case During JSON Encoding

When converting to JSON, you can specify that you want snake case:

encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted

When converting back, you can specify that you want camel casing:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

Equatable and Hashable Protocols Conformance

Swift now provides default implementations of Equatable and Hashable protocols, as long as all type properties are both Equatable and Hashable:

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

Platform Settings and Build Configuration Updates

Check if framework is available

Before it was needed to check the OS containing the framework:

#if os(iOS) || os(tvOS)
  import UIKit
  print("UIKit is available on this platform.")
#else
  print("UIKit is not available on this platform.")
#endif

Now it's possible to check directly the framework:

#if os(iOS) || os(tvOS)
  import UIKit
  print("UIKit is available on this platform.")
#else
  print("UIKit is not available on this platform.")
#endif

Check if simulator mode

Before it was needed to check for the architecture and operative 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

Now it's possible to check directly for the target environment:

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

Language improvements

Generation of Random Numbers

Before, it was needed to use arc4random_uniform:

let digit = Int(arc4random_uniform(10))

Now, the numerical types have the random method:

// 1  
let digit = Int.random(in: 0..<10)

// 2
if let anotherDigit = (0..<10).randomElement() {
  print(anotherDigit)
} else {
  print("Empty range.")
}

// 3
let double = Double.random(in: 0..<1)
let float = Float.random(in: 0..<1)
let cgFloat = CGFloat.random(in: 0..<1)
let bool = Bool.random()

It's also possible to get a random element from an array:

let playlist = ["Nothing Else Matters", "Stairway to Heaven", "I Want to Break Free", "Yesterday"]
if let song = playlist.randomElement() {
  print(song)
} else {
  print("Empty playlist.")
}

And it's also possible to shuffle an array:

let shuffledPlaylist = playlist.shuffled()
names.shuffle()

Dynamic members

It's possible to define dynamic types:

// 1
@dynamicMemberLookup
class Person {
  let name: String
  let age: Int
  private let details: [String: String]
  
  init(name: String, age: Int, details: [String: String]) {
    self.name = name
    self.age = age
    self.details = details
  }
  
  // 2
  subscript(dynamicMember key: String) -> String {
    switch key {
      case "info":
        return "\(name) is \(age) years old."
      default:
        return details[key] ?? ""
    }
  }
}


// 3
let details = ["title": "Author", "instrument": "Guitar"]
let me = Person(name: "Cosmin", age: 32, details: details)
me.info   // "Cosmin is 32 years old."
me.title  // "Author"

Enumeration Cases Collections

To get all cases of an enum in a collection, make it to conform to CaseIterable:

// 1
enum Seasons: String, CaseIterable {
  case spring = "Spring", summer = "Summer", autumn = "Autumn", winter = "Winter"
}

enum SeasonType {
  case equinox
  case solstice
}

// 2
for (index, season) in Seasons.allCases.enumerated() {
  let seasonType = index % 2 == 0 ? SeasonType.equinox : .solstice
  print("\(season.rawValue) \(seasonType).")
}

It's also possible to return only some cases to the resulting collection:

enum Months: CaseIterable {
  case january, february, march, april, may, june, july, august, september, october, november, december          
  
  static var allCases: [Months] {
    return [.june, .july, .august]
  }
}

Testing Sequence Elements

There is a new helper method to check if all elements of a collection satisfy a condition:

let values = [10, 8, 12, 20]
let allEven = values.allSatisfy { $0 % 2 == 0 }

Removing Elements From Collection

You can remove all elements satisfying a condition from a collection:

var greetings = ["Hello", "Hi", "Goodbye", "Bye"]
greetings.removeAll { $0.count > 3 }

Toggling Boolean State

Now it's easy to toggle a boolean:

var isOn = true
isOn.toggle()

New compiler directives

There are the new directives #warning and \error.