Skip to content

Instantly share code, notes, and snippets.

@lamprosg
Last active July 2, 2021 09:46
Show Gist options
  • Select an option

  • Save lamprosg/e629b91376ef65bb8a79c9983c5d35a8 to your computer and use it in GitHub Desktop.

Select an option

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().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment