Last active
July 2, 2021 09:46
-
-
Save lamprosg/e629b91376ef65bb8a79c9983c5d35a8 to your computer and use it in GitHub Desktop.
(iOS) Swift 5.5. async await
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://www.hackingwithswift.com/articles/233/whats-new-in-swift-5-5 | |
// - OLD WAY | |
func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) { | |
// Complex networking code here; we'll just send back 100,000 random temperatures | |
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 our array then divide by the array size | |
DispatchQueue.global().async { | |
let total = records.reduce(0, +) | |
let average = total / Double(records.count) | |
completion(average) | |
} | |
} | |
func upload(result: Double, completion: @escaping (String) -> Void) { | |
// More complex networking code; we'll just send back "OK" | |
DispatchQueue.global().async { | |
completion("OK") | |
} | |
} | |
// - USING IT | |
fetchWeatherHistory { records in | |
calculateAverageTemperature(for: records) { average in | |
upload(result: average) { response in | |
print("Server response: \(response)") | |
} | |
} | |
} | |
// - NEW WAY | |
/* - Problems adddressed | |
* It’s possible for those functions to call their completion handler more than once, or forget to call it entirely. | |
* The parameter syntax @escaping (String) -> Void can be hard to read. | |
* At the call site we end up with a so-called pyramid of doom, with code increasingly indented for each completion handler. | |
* Until Swift 5.0 added the Result type, it was harder to send back errors with completion handlers. | |
*/ | |
//From Swift 5.5, we can now clean up our functions by marking them as asynchronously returning a value, like this: | |
func fetchWeatherHistory() 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" | |
} | |
// - USING NEW WAY | |
func processWeather() async { | |
let records = await fetchWeatherHistory() | |
let average = await calculateAverageTemperature(for: records) | |
let response = await upload(result: average) | |
print("Server response: \(response)") | |
} | |
// RULES | |
/* | |
* Synchronous functions cannot simply call async functions directly – it wouldn’t make sense, so Swift will throw an error. | |
* Async functions can call other async functions, but they can also call regular synchronous functions if they need to. | |
* If you have async and synchronous functions that can be called in the same way, | |
- Swift will prefer whichever one matches your current context | |
– if the call site is currently async then Swift will call the async function, otherwise will call the synchronous one. | |
*/ | |
// THROWING ERRORS | |
//Marking it as async throws we can throw errors | |
enum UserError: Error { | |
case invalidCount, dataTooLong | |
} | |
func fetchUsers(count: Int) async throws -> [String] { | |
if count > 3 { | |
// Don't attempt to fetch too many users | |
throw UserError.invalidCount | |
} | |
// Complex networking code here; we'll just send back up to `count` users | |
return Array(["Antoni", "Karamo", "Tan"].prefix(count)) | |
} | |
// USING THEM with try await | |
func updateUsers() async { | |
do { | |
let users = try await fetchUsers(count: 3) | |
print(users) | |
} catch { | |
print("Oops!") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Loop over asynchronous sequences of values using a new AsyncSequence protocol. | |
This is helpful for places when you want to process values in a sequence as they become available | |
rather than precomputing them all at once. | |
*/ | |
//Using AsyncSequence is almost identical to using Sequence, with the exception that | |
//your types should conform to AsyncSequence and AsyncIterator, and your next() method should be marked async. | |
struct DoubleGenerator: AsyncSequence { | |
typealias Element = Int | |
struct AsyncIterator: AsyncIteratorProtocol { | |
var current = 1 | |
mutating func next() async -> Int? { | |
defer { current &*= 2 } | |
if current < 0 { | |
return nil | |
} else { | |
return current | |
} | |
} | |
} | |
func makeAsyncIterator() -> AsyncIterator { | |
AsyncIterator() | |
} | |
} | |
// Use it | |
func printAllDoubles() async { | |
for await number in DoubleGenerator() { | |
print(number) | |
} | |
} | |
//The AsyncSequence protocol also provides default implementations such as map(), compactMap(), allSatisfy(), and more. | |
//For example, we could check whether our generator outputs a specific number like this: | |
func containsExactNumber() async { | |
let doubles = DoubleGenerator() | |
let match = await doubles.contains(16_777_216) | |
print(match) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Swift’s read-only properties to support the async and throws keywords | |
//Ex. | |
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 | |
} | |
} | |
} | |
} | |
// Use it | |
//Because contents is both async and throwing, we must use try await when trying to read it: | |
func printHighScores() async throws { | |
let file = BundleFile(filename: "highscores") | |
try await print(file.contents) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum LocationError: Error { | |
case unknown | |
} | |
// Async function | |
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 | |
} | |
} | |
// Sync function | |
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 | |
} | |
// Task -> allow us to run concurrent operations either individually | |
// TaskGroup -> or in a coordinated way. | |
/* Task */ | |
/* You can start concurrent work by creating a new Task object and passing it the operation you want to run */ | |
//This will start running on a background thread immediately, | |
//and you can use await to wait for its finished value to come back | |
//Call fibonacci many times on a background thread in order to calculate the first 50 numbers in the sequence: | |
func printFibonacciSequence() async { | |
let task1 = Task { () -> [Int] in //the task starts running as soon as it’s created | |
var numbers = [Int]() | |
for i in 0..<50 { | |
let result = fibonacci(of: i) | |
numbers.append(result) | |
} | |
return numbers | |
} | |
let result1 = await task1.value //the await will wait for the task to finish before continuing | |
print("The first 50 numbers in the Fibonacci sequence are: \(result1)") | |
} | |
//If the code is simpler we do not need to provide the return type. | |
//so this will produce the same result | |
let task1 = Task { | |
(0..<50).map(fibonacci) | |
} | |
// TASK PRIORITY | |
//Priorities: high, default, low, background. | |
let task1 = Task(priority: .high) { | |
(0..<50).map(fibonacci) | |
} | |
//But for the Apple platform the others work too: | |
// userInitiated -> in place of high | |
// utility -> in place of low | |
// userInteractive -> You can't access it because it's for the main thread only | |
// TASK static methds | |
Task.sleep() //sleep for a specific number of nanoseconds (1_000_000_000 -> 1 sec) | |
Task.cancel() //cancel task | |
Task.checkCancellation() //check whether someone has asked for this task to be cancelled and if so throw CancellationError | |
Task.yield() //suspend the current task for a few moments in order to give some time to any tasks that might be waiting | |
//Ex. | |
let task = Task { () -> String in | |
print("Starting") | |
await Task.sleep(1_000_000_000) | |
try Task.checkCancellation() | |
return "Done" | |
} | |
/* TASK GROUP */ | |
/* collections of tasks that work together to produce a finished value. */ | |
// Task groups are created using functions such as withTaskGroup( | |
//Ex. | |
func printMessage() async { | |
let string = await withTaskGroup(of: String.self) { group -> String in //group is the task group | |
group.async { "Hello" } //Add Task to your group (will start immediately) | |
group.async { "From" } //These actually return a single string | |
group.async { "A" } | |
group.async { "Task" } | |
group.async { "Group" } | |
var collected = [String]() | |
for await value in group { | |
collected.append(value) | |
} | |
return collected.joined(separator: " ") | |
} | |
print(string) | |
} | |
//Tip: All tasks in a task group must return the same type of data, | |
//so for complex work you might find yourself needing to return an enum with associated values | |
// If your TASKs throws, use - withThrowingTaskGroup- instead: | |
func printAllWeatherReadings() async { | |
do { | |
print("Calculating average weather…") | |
let result = try await withThrowingTaskGroup(of: [Double].self) { group -> String in | |
group.async { | |
try await getWeatherReadings(for: "London") | |
} | |
group.async { | |
try await getWeatherReadings(for: "Rome") | |
} | |
group.async { | |
try await getWeatherReadings(for: "San Francisco") | |
} | |
// Convert our array of arrays into a single array of doubles | |
let allValues = try await group.reduce([], +) | |
// Calculate the mean average of all our doubles | |
let average = allValues.reduce(0, +) / Double(allValues.count) | |
return "Overall average temperature is \(average)" | |
} | |
print("Done! \(result)") | |
} catch { | |
print("Error calculating data.") | |
} | |
} | |
cancelAll() //method in the taskgroup to cancel them all | |
asyncUnlessCancelled() // taskgroup method to skip adding work if the task has been cancelled |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Alternative to task groups if you want to return different types of results */ | |
//Struct with 3 different types that come from from 3 async functions | |
struct UserData { | |
let username: String | |
let friends: [String] | |
let highScores: [Int] | |
} | |
func getUser() async -> String { | |
"Taylor Swift" | |
} | |
func getHighScores() async -> [Int] { | |
[42, 23, 16, 15, 8, 4] | |
} | |
func getFriends() async -> [String] { | |
["Eric", "Maeve", "Otis"] | |
} | |
//If we wanted to create a User instance from all three of those values, async let is the easiest way | |
// – it run each function concurrently, wait for all three to finish, then use them to create our object. | |
func printUserDetails() async { | |
async let username = getUser() // (no await, async in the declaration) | |
async let scores = getHighScores() // They just bind the functions to the property | |
async let friends = getFriends() | |
let user = await UserData(name: username, friends: friends, highScores: scores) | |
print("Hello, my name is \(user.name), and I have \(user.friends.count) friends!") | |
} | |
//Important: | |
//You can only use async let if you are already in an async context, | |
//and if you don’t explicitly await the result of an async let, Swift will implicitly wait for it when exiting its scope. | |
//When working with throwing functions, you don’t need to use try with async let | |
//rather than typing try await someFunction() with an async let you can just write someFunction(). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Adapt older, completion handler-style APIs to modern async code. */ | |
//Example of old asynchronous code | |
func fetchLatestNews(completion: @escaping ([String]) -> Void) { | |
DispatchQueue.main.async { | |
completion(["Swift 5.5 release", "Apple acquires Apollo"]) | |
} | |
} | |
withCheckedContinuation() // creates a new continuation that can run whatever code you want | |
//Then call | |
resume(returning:) // to send a value back whenever you’re ready | |
//Example | |
func fetchLatestNews() async -> [String] { | |
await withCheckedContinuation { continuation in | |
fetchLatestNews { items in | |
continuation.resume(returning: items) | |
} | |
} | |
} | |
//Important: To be crystal clear, you must resume your continuation exactly once. | |
//So we can use it like this | |
func printNews() async { | |
let items = await fetchLatestNews() | |
for item in items { | |
print(item) | |
} | |
} | |
//The term “checked” continuation means that Swift is performing runtime checks on our behalf: | |
//are we calling resume() once and only once? | |
//This is important, because if you never resume the continuation then you will leak resources, | |
//but if you call it twice then you’re likely to hit problems. | |
// /As there is a runtime performance cost of checking your continuations Swift also provides | |
withUnsafeContinuation() //that works the same and does not perform any checks |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Actors */ | |
/* Conceptually similar to classes that are safe to use in concurrent environments | |
This is possible because Swift ensures that mutable state inside your actor | |
is only ever accessed by a single thread at any given time */ | |
// Actor isolation | |
// - stored properties and methods cannot be read from outside the actor object unless they are performed asynchronously | |
// - stored properties cannot be written from outside the actor object at all | |
//The async behavior isn’t there for performance; | |
//Swift automatically places these requests into a queue that is processed sequentially to avoid race conditions. | |
// - BEFORE | |
class RiskyCollector { | |
var deck: Set<String> | |
init(deck: Set<String>) { | |
self.deck = deck | |
} | |
func send(card selected: String, to person: RiskyCollector) -> Bool { | |
guard deck.contains(selected) else { return false } | |
deck.remove(selected) | |
person.transfer(card: selected) | |
return true | |
} | |
func transfer(card: String) { | |
deck.insert(card) | |
} | |
} | |
// In a single-threaded environment that code is safe | |
// In a multi-threaded environment our code has a potential race condition | |
/* | |
- The first thread checks whether the card is in the deck, and it is so it continues. | |
- The second thread also checks whether the card is in the deck, and it is so it continues. | |
- The first thread removes the card from the deck and transfer it to the other person. | |
- The second thread attempts to remove the card from the deck, but actually it’s already gone so nothing will happen. However, it still transfers the card to the other person. | |
*/ | |
// - AFTER | |
actor SafeCollector { | |
var deck: Set<String> | |
init(deck: Set<String>) { | |
self.deck = deck | |
} | |
//The send() method is marked with async, because it will need to suspend its work | |
//while waiting for the transfer to complete. | |
func send(card selected: String, to person: SafeCollector) async -> Bool { | |
guard deck.contains(selected) else { return false } | |
deck.remove(selected) | |
//Although the transfer(card:) method is not marked with async, | |
//we still need to call it with await because it will wait until | |
//the other SafeCollector actor is able to handle the reques | |
await person.transfer(card: selected) | |
return true | |
} | |
func transfer(card: String) { | |
deck.insert(card) | |
} | |
} | |
//To be clear, an actor can use its own properties and methods freely, asynchronously or otherwise, | |
//but when interacting with a different actor it must always be done asynchronously | |
//With these changes Swift can ensure that all actor-isolated state is never accessed concurrently, | |
//and more importantly this is done at compile time so that safety is guaranteed. | |
/* | |
Actors do not currently support inheritance, which makes their initializers much simpler | |
– there is no need for convenience initializers, overriding, the final keyword, and more. This might change in the future. | |
All actors implicitly conform to a new Actor protocol; no other concrete type can use this. | |
- This allows you to restrict other parts of your code so it can work only with actors. | |
*/ | |
/* Global Actors */ | |
//Introduction of an @MainActor global actor you can use to mark properties and methods | |
//that should be accessed only on the main thread. | |
//@MainActor is a global actor wrapper around the underlying MainActor struct | |
// Example: | |
//we might have a class to handle data storage in our app, and for safety reasons | |
//we refuse to write out change to persistent storage unless we’re on the main thread: | |
// - OLD WAY | |
class OldDataController { | |
func save() -> Bool { | |
guard Thread.isMainThread else { | |
return false | |
} | |
print("Saving data…") | |
return true | |
} | |
} | |
// - NEW WAY | |
class NewDataController { | |
@MainActor func save() { | |
print("Saving data…") | |
} | |
} | |
//Swift will make sure whenever you call save() on a data controller, that work will happen on the main thread. | |
//Note: Because we’re pushing work through an actor, you must call save() using await, async let, or similar. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Sendable data, which is data that can safely be transferred to another thread. | |
//This is accomplished through a new Sendable protocol, and an @Sendable attribute for functions. | |
/* | |
Many things are inherently safe to send across threads: | |
All of Swift’s core value types, including Bool, Int, String, and similar. | |
Optionals, where the wrapped data is a value type. | |
Standard library collections that contain value types, such as Array<String> or Dictionary<Int, String>. | |
Tuples where the elements are all value types. | |
Metatypes, such as String.self. | |
These have been updated to conform to the Sendable protocol. | |
*/ | |
// - For Custom types | |
// * Actors automatically conform to Sendable because they handle their synchronization internally. | |
// * Custom structs and enums you define will also automatically conform to Sendable | |
// if they contain only values that also conform to Sendable, similar to how Codable works. | |
// * Custom classes can conform to Sendable as long as they either inherits from NSObject or from nothing at all. | |
// All properties are constant and themselves conform to Sendable, and they are marked as final to stop further inheritance | |
- @Sendable attribute on functions or closure | |
//Ex. | |
func printScore() async { | |
let score = 1 | |
Task { print(score) } | |
Task { print(score) } | |
} | |
//he operation we pass into the Task initializer is marked @Sendable, | |
//which means this kind of code is allowed because the value captured by Task is a constant | |
//That code would not be allowed if score were a variable, | |
//because it could be accessed by one of the tasks while the other was changing its value. | |
//Enforcing Sendable by marking marking functions and closures | |
//Ex. | |
func runLater(_ function: @escaping @Sendable () -> Void) -> Void { | |
DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment