Skip to content

Instantly share code, notes, and snippets.

@lamprosg
Last active July 2, 2021 09:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lamprosg/e629b91376ef65bb8a79c9983c5d35a8 to your computer and use it in GitHub Desktop.
Save lamprosg/e629b91376ef65bb8a79c9983c5d35a8 to your computer and use it in GitHub Desktop.
(iOS) Swift 5.5. async await
//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!")
}
}
/*
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)
}
//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)
}
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
/* 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().
/* 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
/* 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.
//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