In this paper, the www.whatsnewinswift.com/?from=5.4&t… Excerpts and translations were made

Compared to the new features and improvements in Xcode and SwiftUI, the changes to the Swift language itself are significant in version 5.5. Paul Hudson, in his “What’s New in Swift? The site has been updated with Swift 5.4 to Swift 5.5 changes, and the documentation and samples are detailed and trivial. The author chooses the features that he thinks are more important and explores them with readers in this article.

Hands-on learning is the best way to learn. We suggest that readers who want to try and learn the new features of Swift 5.5, but also want to save some effort, can download Paul’s Playground on Github and use the code in it to get started quickly. This article will also use code examples from this Playground directly when introducing the new Swift 5.5 features. But the structure of this article will be different from Paul’s, and try to be concise to save readers’ time.

what’s-new-in-swift-5-5

New features unrelated to concurrency

#ifPostfix member expression

Se-0308 enables Swift to use the #if condition before postfix member expressions. The code example is as follows:

Text("Welcome")
#if os(iOS)
    .font(.largeTitle)
#else
    .font(.headline)
#endif
Copy the code

Conditions can also be nested:

#if os(iOS)
    .font(.largeTitle)
    #if DEBUG
        .foregroundColor(.red)
    #endif
#else
    .font(.headline)
#endif
Copy the code

Note: Conditional branches must be followed by postfix expressions, not other types of expressions.

CGFloatDoubleTypes are used interchangeably

Se-0307 Introduced: Swift is now able to automatically and implicitly convert between CGFloat and Double on demand.

As Paul points out, this is really a small but significant improvement in the quality of life for Swift programmers.

Using the CGFloat API extensively, Swift now silently Bridges Double for you.

Of an enumeration type with associated valuesCodableAutomatic synthesis

The SE-0295 has upgraded Swift’s Codable system to now support enumerations with associated values. Such as:

enum Weather: Codable {
    case sun
    case wind(speed: Int)
    case rain(amount: Int, chance: Int)}Copy the code

The code above has a simple case, one with a single associated value, and one with two associated values. We can encode the following enumeration variables in Swift 5.5 using a JSONEncoder or other type of encoder and get a JSON string.

let forecast: [Weather] = [
    .sun,
    .wind(speed: 10),
    .sun,
    .rain(amount: 5, chance: 50)]do {
    let result = try JSONEncoder().encode(forecast)
    let jsonString = String(decoding: result, as: UTF8.self)
    print(jsonString)
} catch {
    print("Encoding error: \(error.localizedDescription)")}Copy the code

lazyKeywords can now also be used in local scopes

func printGreeting(to: String) -> String {
    print("In printGreeting()")
    return "Hello, \(to)"
}
    
func lazyTest(a) {
    print("Before lazy")
    lazy var greeting = printGreeting(to: "Paul")
    print("After lazy")
    print(greeting)
}
    
lazyTest()
Copy the code

In practice, this feature is very useful for selectively running code: you can prepare a result lazily, but only do the work when you actually use the result.

Property wrappers are now available for arguments to functions and closures

Se-0293 extends property wrappers so that they can now be applied to parameters of functions and closures.

Applying a property wrapper to a parameter does not change the immutable properties passed by the parameter, and you can still access the type encapsulated in the wrapper by underlining it.

Look at the following code:

func setScore1(to score: Int) {
    print("Setting score to \(score)")}/ / call
setScore1(to: 50)
setScore1(to: -50)
setScore1(to: 500)
Copy the code

If we want the score to be zero… With a range of 100, we can write a simple attribute wrapper:

@propertyWrapper
struct Clamped<T: Comparable> {
    let wrappedValue: T
    
    init(wrappedValue: T.range: ClosedRange<T>) {
        self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}
Copy the code

Then rewrite the above function as:

func setScore2(@Clamped(range: 0.100) to score: Int) {
    print("Setting score to \(score)")
}

setScore2(to: 50)
setScore2(to: -50)
setScore2(to: 500)
Copy the code

Calling setScore2() with the same value yields a different result than setScore() because the numbers clamped to 50,0,100.

Use static member lookup in a generic context

Se-0299 enables Swift to perform static member lookups in generic functions. This sounds obscure, but it’s easier to understand.

We might have written code like this earlier in SwiftUI:

Toggle("Example", isOn: .constant(true))
    .toggleStyle(SwitchToggleStyle())
Copy the code

Now you can change it to this:

Toggle("Example", isOn: .constant(true))
    .toggleStyle(.switch)
Copy the code

New features related to concurrency

asyncawaitThe keyword

Se-0296 introduces asynchronous functions for Swift, which allows us to handle asynchronous code as if we were writing synchronous code. In short, we need two steps: the first is to mark the function as asynchronous with the new async keyword, and the second is to call the asynchronous function with the await keyword. This is similar to C# and JavaScript.

Of course, besides C# and JavaScript, there are other programming languages with async/await mechanism, such as Python, F#, Kotlin, Rust, Dart, etc. Some of them are implemented as keywords, while others are implemented as functions or libraries. Swift’s pace is slower, but not too late.

Before introducing async and await, suppose we implement a logic that pulls massive data records from the server, evaluates them, and sends them back to the server. The code might look something like this:

func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) {
    // Omit the complex network code, we will use 100000 records instead
    DispatchQueue.global().async {
        let results = (1.100 _000).map { _ in Double.random(in: -10.30) }
        completion(results)
    }
}

func calculateAverageTemperature(for records: [Double].completion: @escaping (Double) - >Void) {
    // Sum the array and average it
    DispatchQueue.global().async {
        let total = records.reduce(0.+)
        let average = total / Double(records.count)
        completion(average)
    }
}

func upload(result: Double.completion: @escaping (String) - >Void) {
    // omit the network code and send it back to the server
    DispatchQueue.global().async {
        completion("OK")}}Copy the code

I have intentionally replaced the above network code with fabricated data, because the network part is irrelevant to our topic. All the reader needs to know is that these functions are time-consuming, so we use completion closures instead of blocking. When we use these functions, we need to link them together and provide completion closure for each function call. The code might look like this:

fetchWeatherHistory { records in
    calculateAverageTemperature(for: records) { average in
        upload(result: average) { response in
            print("Server response: \(response)")}}}Copy the code

Hopefully you can see the problem with the above approach:

  • Completion closures can be called multiple times, or they can be forgotten
  • @escaping (String) -> VoidSuch parametric syntax is relatively difficult to read
  • As each layer of completion closure increases, the caller’s code structure morphs into a so-called “pyramid of Doom” (also known as “callback hell”)
  • Introduced in Swift 5.0ResultPreviously, it was also more difficult to pass back errors to complete processing

In Swift 5.5, we can solve the above problem by marking these functions as asynchronous and returning values instead of relying on completion closures:

func fetchWeatherHistory(a) async- > [Double] {(1.100 _000).map { _ in Double.random(in: -10.30)}}func calculateAverageTemperature(for records: [Double]) async -> Double {
    let total = records.reduce(0.+)
    let average = total / Double(records.count)
    return average
}

func upload(result: Double) async -> String {
    "OK"
}
Copy the code

With asynchronous return syntax, we can already remove a lot of code, and the caller code is much more concise:

func processWeather(a) async {
    let records = await fetchWeatherHistory()
    let average = await calculateAverageTemperature(for: records)
    let response = await upload(result: average)
    print("Server response: \(response)")}Copy the code

As you can see, all closures and indentation are gone, leaving the form of the code to what is called “straight line code” — it looks like synchronous code minus the await keyword.

There are some pretty straightforward and specific rules for how asynchronous functions work:

  • Synchronous functions cannot call asynchronous functions directly — this does not make sense, so Swift throws an error
  • Asynchronous functions can call other asynchronous functions, but they can also call synchronous functions
  • Assuming you have both synchronous and asynchronous functions available, Swift selects the appropriate version based on the current context — Swift will call the asynchronous function if the caller is currently asynchronous, otherwise it will call the synchronous function.

The last point above is important because it allows library developers to provide both synchronous and asynchronous functions without having to name the asynchronous version.

The new async/await works perfectly with try/catch, which means that asynchronous functions or constructors can throw errors on demand. The only restriction that Swift imposes here is the order of the keywords, and the callers and functions are just opposite.

Look at the following code:

enum UserError: Error {
    case invalidCount, dataTooLong
}

func fetchUsers(count: Int) async throws- > [String] {
    if count > 3 {
        throw UserError.invalidCount
    }

    return Array(["Antoni"."Karamo"."Tan"].prefix(count))
}

func save(users: [String]) async throws -> String {
    let savedUsers = users.joined(separator: ",")

    if savedUsers.count > 32 {
        throw UserError.dataTooLong
    } else {
        return "Saved \(savedUsers)!"}}func updateUsers(a) async {
    do {
        let users = try await fetchUsers(count: 3)
        let result = try await save(users: users)
        print(result)
    } catch {
        print("Oops!")}}Copy the code

We can see that the keyword order for defining an asynchronous function that can throw an error is async throws, and the caller needs to write a try await.

Async /await: async sequence

Se-0298 introduces a new protocol called AsyncSequence, which iterates over asynchronous sequences.

AsyncSequence is used in much the same way as Sequence, except that you need to implement a type that complies with AsyncSequence and AsyncIterator, and the next method must be marked async. When the iteration advances to the end of the Sequence, next() returns nil, just like Sequence.

For example, we implement a DoubleGenerator that starts at 1 and returns twice the previous value each time it is called:

struct DoubleGenerator: AsyncSequence {
    typealias Element = Int

    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 1

        mutating func next(a) async -> Int? {
            defer { current & * = 2 }

            if current < 0 {
                return nil
            } else {
                return current
            }
        }
    }

    func makeAsyncIterator(a) -> AsyncIterator {
        AsyncIterator()}}Copy the code

** Note: ** If you remove async from the above code, you have a Sequence that does the same thing — so the sequences are very similar. Of course, the corresponding protocol constraints also need to change

Once we have an asynchronous sequence, we can iterate over it in an asynchronous context with a for await statement like this:

func printAllDoubles(a) async {
    for await number in DoubleGenerator() {
        print(number)
    }
}
Copy the code

The AsyncSequence protocol also provides a default implementation of many common methods, including map(), compactMap(), allSatisfy(), and so on. For example, we can use Contains to check if the generator contains a particular number:

func containsExactNumber(a) async {
    let doubles = DoubleGenerator(a)let match = await doubles.contains(16 _777_216)
    print(match)
}
Copy the code

Of course, these methods all need to be used in an asynchronous context.

More efficient read-only properties

Se-0310 upgrades Swift’s read-only properties to support async and throws keywords (which can be used alone or together).

For example, if we create a BundleFile struct to load the contents of a file, we may encounter situations where the file does not exist, the contents of the file cannot be read, or the contents are too large and take too long to read, etc. We can mark the contents property as async throws like this:

enum FileError: Error {
    case missing, unreadable
}

struct BundleFile {
    let filename: String

    var contents: String {
        get async throws {
            guard let url = Bundle.main.url(forResource: filename, withExtension: nil) else {
                throw FileError.missing
            }

            do {
                return try String(contentsOf: url)
            } catch {
                throw FileError.unreadable
            }
        }
    }
}
Copy the code

Since contents is both asynchronous and loggable, we must use a try await to read:

func printHighScores(a) async throws {
    let file = BundleFile(filename: "highscores")
    try await print(file.contents)
}
Copy the code

Structured concurrency

Se-0304 introduces a whole set of operations to execute, cancel, and monitor concurrency based on the async/await keyword and asynchronous sequences.

For demonstration purposes, we introduce the following two functions — an asynchronous function that simulates pulling weather indices from a specific location, and a synchronous function that retrieves a number at a specified location in the Fibonacci sequence.

enum LocationError: Error {
    case unknown
}

func getWeatherReadings(for location: String) async throws- > [Double] {
    switch location {
    case "London":
        return (1.100).map { _ in Double.random(in: 6.26)}case "Rome":
        return (1.100).map { _ in Double.random(in: 10.32)}case "San Francisco":
        return (1.100).map { _ in Double.random(in: 12.20)}default:
        throw LocationError.unknown
    }
}

func fibonacci(of number: Int) -> Int {
    var first = 0
    var second = 1

    for _ in 0..<number {
        let previous = first
        first = second
        second = previous + first
    }

    return first
}
Copy the code

The main change in structured concurrency is the introduction of two new types, Task and TaskGroup, which allow concurrent operations to be performed independently or collaboratively.

The simplest form of use is to create a Task object and pass it the asynchronous operations you want to perform. This will immediately start an asynchronous thread to execute and we can await the result with await.

For example, we could call Fibonacci (of:) multiple times in the background thread to replace the first 50 digits in the sequence:

func printFibonacciSequence(a) async {
    let task1 = Task { () -> [Int] in
        var numbers = [Int] ()for i in 0..<50 {
            let result = fibonacci(of: i)
            numbers.append(result)
        }

        return numbers
    }

    let result1 = await task1.value
    print("The first 50 numbers in the Fibonacci sequence:\(result1)")}Copy the code

As you can see, I explicitly wrote Task {() -> [Int] in} so that Swift knows the Task will return. We could also use type inference and the map function to write more minimalist code like this:

let task1 = Task{(0..<50).map(fibonacci)
}
Copy the code

Again, the task will run as soon as it is created. PrintFibonacciSequence () continues execution on the thread on which it is located, and the Fibonacci sequence is computed.

** Tip: ** Our task operation is a non-escape closure because the task is running in real time. So when you use Task in a class or structure, you don’t need to use self to access properties or methods.

While reading the finished number, await Task1.value ensures that printFibonacciSequence() is pending until the task is finished and the output is ready. Assuming you don’t care about the outcome of the task — just start it and let it die — you don’t need to store the task.

For task operations that throw uncaught errors, reading the value attribute of the task automatically throws those errors as well. Therefore, we can write multiple tasks simultaneously in code and wait for them all to complete:

func runMultipleCalculations(a) async throws {
    let task1 = Task{(0..<50).map(fibonacci)
    }

    let task2 = Task {
        try await getWeatherReadings(for: "Rome")}let result1 = await task1.value
    let result2 = try await task2.value
    print("The first 50 numbers in the Fibonacci sequence:\(result1)")
    print("Weather index for Rome:\(result2)")}Copy the code

Swift provides high, default, Low, and background built-in Task priorities that can be customized by the constructor Task(Priority:.high) at Task creation time. For apple platforms only, you can also use the more familiar userInitiated instead of hight and utility instead of low, but userInteractive is reserved for the main thread.

In addition to performing operations, Task gives us some static methods to control how our code runs:

  • callTask.sleep()Causes the current task to sleep for a specified nanosecond, so to specify 1 second, provide 1_000_000_000
  • callTask.checkCancellation()They check to see if anyone passescancel()Method cancels the task and throws one if it existsCancellationError.
  • callTask.yield()The current task is suspended for a period of time to make room for other waiting tasks. This API is important, especially if you are doing very expensive work in a loop.

We can use the following code to understand the above operations:

func cancelSleepingTask(a) async {
    let task = Task { () -> String in
        print("Starting")
        await Task.sleep(1 _000_000_000)
        try Task.checkCancellation()
        return "Done"
    }

    // The task has started, but we cancel it while it is dormant
    task.cancel()

    do {
        let result = try await task.value
        print("The result:\(result)")}catch {
        print("Mission has been canceled.")}}Copy the code

In the above code, task.checkCancellation () will find that the Task has been canceled and immediately raise a CancellationError, but it won’t come to us right away until we try to read task.value.

Tip: We can use task.result to get a result value, which contains the success or failure of the task. For example, in the code above we get Result

. This does not require a try statement because we need to handle success and failure cases ourselves.
,>

To avoid getting too long, Swift 5.5’s introduction to new features will be split into two articles.


For more articles, please pay attention to the wechat public number: Swift Garden