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.

Revisions

  1. lamprosg revised this gist Jul 2, 2021. 1 changed file with 47 additions and 0 deletions.
    47 changes: 47 additions & 0 deletions 8.Sendable.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,47 @@
    //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)
    }
  2. lamprosg revised this gist Jun 11, 2021. 1 changed file with 119 additions and 0 deletions.
    119 changes: 119 additions & 0 deletions 7.Actors.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,119 @@
    /* 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.
  3. lamprosg revised this gist Jun 11, 2021. 1 changed file with 10 additions and 1 deletion.
    11 changes: 10 additions & 1 deletion 6. Continuation.swift
    Original file line number Diff line number Diff line change
    @@ -19,6 +19,7 @@ func fetchLatestNews() async -> [String] {
    }
    }
    }
    //Important: To be crystal clear, you must resume your continuation exactly once.

    //So we can use it like this
    func printNews() async {
    @@ -27,4 +28,12 @@ func printNews() async {
    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
  4. lamprosg revised this gist Jun 11, 2021. 1 changed file with 30 additions and 0 deletions.
    30 changes: 30 additions & 0 deletions 6. Continuation.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,30 @@
    /* 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)
    }
    }
    }

    //So we can use it like this
    func printNews() async {
    let items = await fetchLatestNews()

    for item in items {
    print(item)
    }
    }
  5. lamprosg revised this gist Jun 11, 2021. 2 changed files with 40 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions 1.AsyncAwait.swift
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    //https://www.hackingwithswift.com/articles/233/whats-new-in-swift-5-5

    // - OLD WAY

    func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) {
    38 changes: 38 additions & 0 deletions 5.letBindings.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    /* 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().
  6. lamprosg revised this gist Jun 11, 2021. 1 changed file with 64 additions and 5 deletions.
    69 changes: 64 additions & 5 deletions 4.Concurrency.swift
    Original file line number Diff line number Diff line change
    @@ -85,8 +85,67 @@ Task.yield() //suspend the current task for a few moments in order to give some

    //Ex.
    let task = Task { () -> String in
    print("Starting")
    await Task.sleep(1_000_000_000)
    try Task.checkCancellation()
    return "Done"
    }
    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
  7. lamprosg revised this gist Jun 11, 2021. 1 changed file with 92 additions and 0 deletions.
    92 changes: 92 additions & 0 deletions 4.Concurrency.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,92 @@
    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"
    }
  8. lamprosg revised this gist Jun 11, 2021. 3 changed files with 32 additions and 0 deletions.
    File renamed without changes.
    32 changes: 32 additions & 0 deletions 3.ReadOnlyProperties.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,32 @@
    //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)
    }
  9. lamprosg revised this gist Jun 10, 2021. No changes.
  10. lamprosg revised this gist Jun 7, 2021. 1 changed file with 9 additions and 0 deletions.
    9 changes: 9 additions & 0 deletions SequenceWithAsyncAwait.swift
    Original file line number Diff line number Diff line change
    @@ -35,4 +35,13 @@ 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)
    }
  11. lamprosg revised this gist Jun 7, 2021. 1 changed file with 38 additions and 0 deletions.
    38 changes: 38 additions & 0 deletions SequenceWithAsyncAwait.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    /*
    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)
    }
    }
  12. lamprosg revised this gist Jun 7, 2021. 1 changed file with 28 additions and 0 deletions.
    28 changes: 28 additions & 0 deletions AsyncAwait.swift
    Original file line number Diff line number Diff line change
    @@ -81,3 +81,31 @@ func processWeather() async {
    – 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!")
    }
    }
  13. lamprosg created this gist Jun 7, 2021.
    83 changes: 83 additions & 0 deletions AsyncAwait.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,83 @@
    // - 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.
    */