Những điểm mới trong Swift 4

What’s New in Swift 4?

Swift 4 là bản release mới nhất của Apple, dự định sẽ được đưa ra vào mùa thu năm 2017. Swift 4 tập trung cung cấp sự tương thích với Swift 3, trong bài này tôi sẽ giới thiệu các phần thay đổi của Swift mà ảnh hưởng tới code cũ nhiều nhất. Let's get started!

Getting Started

Swift 4 được đi kèm trong Xcode 9, bạn có thể download bản mới nhất của Xcode 9 từ trang chủ Apple developer (cần có tài khoản developer) tại đây: Download Here Bạn nên thử các tính năng mới của Swift 4 trên playground, thử các example, sửa thử chạy nó với các tình huống khác nhau để hiểu rõ hơn về nó.

Migrating to Swift 4

Việc migration từ Swift 3 sang Swift 4 sẽ bớt cồng kềnh vất vả hơn so với việc chuyển từ 2.2 sang 3, hầu hết các phần thay đổi là phần thêm vào và không cần phải can thiệp sửa rất nhiều như khi lên Swift 3. Vì vậy mà Swift migration tool sẽ xử lý được hầu hết phần này cho chúng ta. Xcode 9 đồng thời support cả Swift 4, 3, 3.2, mỗi target trong project ta có thể chọn theo version Swift.

API Changes

Strings

String trong Swift 4 có rất nhiều cải tiến rất tiện lợi. String được coi như collection giống như trong swift 2.0, do đó ta không cần gọi tới thuộc tính charators của String như ở version 3.0. Ví dụ khi in từng kí tự trong string ta có thể làm như sau:

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

Thay vì như trước đây:

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

Không chỉ có lặp, String còn được kế thừa các ưu điểm từ Sequence và Collection:

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 "

Cả String và SubString đều thực thi StringProtocol nên chúng giống nhau gần hết các function.

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

Sequence Based Initialization Đầu tiên là khả năng tạo dictionary từ mảng cặp key-value (tuple)

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)) 
// ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Barnard's Star": 5.96]

Duplicate Key Resolution Bạn có thể xử lý việc khởi tạo dictionary khi key bị lặp lại, điều này sẽ giúp tránh cho việc ghi đè cặp key-value

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

Đoạn code trên đã giúp ta xử lý việc lặp key "Alpha Centauri A" trong dictionary. Filtering Cả Dictionary và Set bây giờ đều có khả năng filter kết quả vào object mới:

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

Dictionary Mapping Dictionary nhận được phương thức rất hữu ích cho việc mapping giá trị trực tiếp:

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

Dictionary Default Values

// 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 Grouping 1 khả năng tuyệt vời nữa đó là việc khởi tạo Dictionary từ Sequence sau đó group lại theo tiêu chí khác nhau:

// Grouping sequences by computed key
let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }

// ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]

Reserving Capacity Cả Sequence và Dictionary đều có khả năng reverse lại dung lượng

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

API Additions

Bây giờ chúng ta sẽ xem các tính năng mới thêm vào của Swift 4. Những sự thay đổi này ko ảnh hướng tới code hiện có, chúng đơn giản là thêm tính năng mới vào.

Archival and Serialization

Để seriallize và archive kiểu custom types do chúng ta tạo ra ở bản cũ khá loằng ngoằng. Với kiểu class ta cần subclass NSObject và thực thi NSCoding protocol, kiểu giá trị như struc hay enum chúng ta cần thực hiện các bước như tạo sub object có thể kế thừa NSObject, NSCoding ... Swift 4 giải quyết vấn đề này bằng cách mang serialization tới tất cả 3 kiêu type của Swift

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

Trong ví dụ này chúng ta có thể thấy rằng để object Swift có thể Ecode và decode chúng ta chỉ cần thực thi Codable protocol. Để encode object ta cần truyền object này tới encoder.

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

Bước cuối cùng là decode dữ liệu vào object cụ thể:

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

Swift 4 ta có thể tạo reference key paths tới instance

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)

Ta vừa tạo ra vài instances của ForceUser bằng cách set name, lightsaber và master. Để tạo key path ta làm như sau:

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

Ở đây ta tạo key path cho thuộc tính name của ForceUser, sau đó sử dụng key path này :

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

Swift 4 đã hỗ trợ đối với text có nhiều dòng:

let star = "⭐️"
let introString = """
  A long time ago in a galaxy far,
  far away....

  You could write multi-lined strings
  without "escaping" single quotes.

  The indentation of the closing quotes
       below deside where the text line
  begins.

  You can even dynamically add values
  from properties: \(star)
  """
print(introString) // prints the string exactly as written above with the value of star

One-Sided Ranges

Ở Swift 3 ta bắt buộc phải có start, end index, nhưng ở Swift 4 ta chỉ cần 1 index là có thể tạo được sub array:

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

Infinite Sequence

Swift 4 cho phép định nghĩa mảng infinite khi start index là kiểu countable

// Infinite range: 1...infinity
var numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (8, "Neptune")]

planets.append("Pluto")
numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (9, "Pluto")]

Pattern Matching

Một trong những ưu điểm của one-sided ranges là parten matching:

// Pattern matching

func temperature(planetNumber: Int) {
  switch planetNumber {
  case ...2: // anything less than or equal to 2
    print("Too hot")
  case 4...: // anything greater than or equal to 4
    print("Too cold")
  default:
    print("Justtttt right")
  }
}

temperature(planetNumber: 3) // Earth

Generic Subscripts

Ở Swift 4 subscripts có thể là 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
  }
}

Trong ví dụ này kiểu giá trị trả về là generic, ta có thể sử dụng generic subscript như sau: 
// 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"]

Khônng chỉ return type có thể sử dụng là generic, actual subscript cũng có thể là generic:

extension GenericDictionary {
  subscript<Keys: Sequence>(keys: Keys) -> [Value] where Keys.Iterator.Element == Key {
    var values: [Value] = []
    for key in keys {
      if let value = data[key] {
        values.append(value)
      }
    }
    return values
  }
}

// Array subscript value
let nameAndMoons = earthData[["moons", "name"]]        // [1, "Earth"]
// Set subscript value
let nameAndMoons2 = earthData[Set(["moons", "name"])]  // [1, "Earth"]

References

https://www.raywenderlich.com/163857/whats-new-swift-4