Watch this episode for free — or access the full course as a raywenderlich.com Professional Subscriber. Learn more here
Beginning Networking with URLSession
Part 1: Introduction to URLSession & Concurrency More Modern Concurrency
— iOS & Swift

Lesson Complete

Play Next Lesson
Next

More Modern Concurrency

Continuing the coverage of Swift’s modern concurrency features.

Contributors

Continuing from the previous episode, let’s take a look at more of Swift’s modern concurrency features.

Begin by opening this episode’s Starter playground. This playground contains the same fetchDomains function you wrote in the previous episode, as well as new model code for Playlists. We’ll take a look at that new code later in this episode.

For now, another cool capability of Swift’s concurrency is Asynchronous Sequences. These work similar to Sequence, where you can get a list of values one step at a time except asynchronously. You await to receive values in the sequence as they become available. Add the following code at the bottom of your playground:

func findTitle(url: URL) async throws -> String? {
  for try await line in url.lines {
    if line.contains("<title>") {
      return line.trimmingCharacters(in: .whitespaces)
    }
  }
    
  return nil
}

URL has a property called lines that returns an asynchronous sequence of strings for each file line. You loop over the strings with for try await, which can return the title as soon as it finds a line with the correct tag in it. To test this, add the following code at the bottom of your playground:

Task {
  if let title = try await findTitle(url: URL(string: "https://www.raywenderlich.com")!) {
    print(title)
  }
}

This code should look familiar given the concepts you learned about in the previous episode. Go ahead and run your code. Take a look at the printed results. Just what you wanted!

Read-only, computed properties can also be asynchronous, or async. Add the following code at the bottom of your playground:

extension Domains {
  static var domains: [Domain] {
    get async throws {
      try await fetchDomains()
    }
  }
}

This adds a static, async property on your existing Domains type. Because fetchDomains() is asynchronous and can throw an error, you mark the property’s getter as async throws. Try your new property by adding the following code:

Task {
  dump(try await Domains.domains)
}

Run your playground. Pretty neat, huh? :)

Read-only subscripts can also be asynchronous. Add this code to your playground:

extension Domains {
  enum Error: Swift.Error { case outOfRange }
    
  static subscript(_ index: Int) -> String {
    get async throws {
      let domains = try await Self.domains
      guard domains.indices.contains(index) else {
        throw Error.outOfRange
      }
      return domains[index].attributes.name
    }
  }
}

Task {
  dump(try await Domains[4])
}

Very similar to the async read-only property, except this allows you to asynchronously subscript into your type to get the element you want. Run your playground. The expected result gets printed via your async subscript. Yay!

You’ve seen how to add concurrency to your code, functions and properties. Concurrency has risks however, specially when multiple code paths are trying to access and change the same state at once.

Think of a bank’s ATM. If two people try to withdraw the entire account balance at the same exact time and the code that powers this isn’t written carefully, both withdrawals will succeed and the bank ended up giving out more money than what was in the account.

While this is difficult to actually happen, and banks have mechanisms for this in place, it helps us understand the issue at hand.

Swift concurrency includes the actor type that can help with this type of problem. In your playground, open the Playlist.swift file and take a look at the Playlist class.

There are four methods in the class that can all mutate songs at the same time. These methods are not thread-safe, a term often used to indicate they are not safe to use concurrently (think of the bank ATM example).

Your intuition and existing knowledge may tell you to just mark them as async to make them concurrent, but then you’d have multiple tasks changing the state of a Playlist simultaneously. To solve this problem you can convert Playlist from a class to an actor.

Just like classes, actors are reference types that represent a shared mutable state. Actors, however, prevent concurrent access to their state, so only one method can access and modify the state of a Playlist at any time. To convert Playlist from a class to an actor, start by changing the class declaration to the following:

public actor Playlist {

Next, make both move methods async:

public func move(song: String, from playlist: Playlist) async
public func move(song: String, to playlist: Playlist) async 

Finally, because you access two playlists (self and the playlist passed in as a parameter) you need to await the calls to add or remove songs on playlist so no one else tries to access it while this method is executing. Add the following await calls inside each of the move methods:

public func move(song: String, from playlist: Playlist) async {
  await playlist.remove(song: song)
  add(song: song)
}
public func move(song: String, to playlist: Playlist) async {
   await playlist.add(song: song)
   remove(song: song)
}

With that, you can now safely use playlists in concurrent code. Test out your new code by removing all existing code in your playground and adding the following at the bottom:

let favoritesPlaylist = Playlist(title: "Favorite songs",
                                author: "Felipe",
                                songs: ["In And Out Of Love"])
let partyPlaylist = Playlist(title: "Party songs",
                            author: "Ray",
                            songs: ["Hello"])
Task {
 await favoritesPlaylist.move(song: "Hello", from: partyPlaylist)
 await favoritesPlaylist.move(song: "In And Out Of Love", to: partyPlaylist)
 await print(favoritesPlaylist.songs)
 await print(partyPlaylist.songs)
}

You use await to isolate the actor, and that also indicates that the method can suspend if some other piece of code is in the middle of accessing the Playlist. Because only one piece of code can access Playlist at a given time, this code is now thread-safe or concurrency-safe.

Note how inside the move methods you don’t use await when calling add or remove. This is because the compiler already knows you have exclusive access to the instance. Run your code and look at the results. Everything works well, and without errors, even if things are taking place asynchronously.

A fun piece of info is that your Playlist actor actually prepares two internal methods for every method of the actor. One version that is concurrent and needs await, and another, faster version that doesn’t. Depending on how you call your method the compiler knows which version to use to optimize performance.

So far you’ve seen how to make your code concurrent and run in background threads as opposed to on the main thread. What happens when you want to swift back to the main thread in order to perhaps to perform some UI updates? This is where MainActor comes into play.

We briefly discussed MainActor in the previous episode, but as a recap, MainActor is a global actor that provides an executor to perform tasks on the main thread. To try out an example scenario, remove all code from your playground and add the following:

let url = URL(string: "https://api.raywenderlich.com/api/domains")!
let session = URLSession.shared.dataTask(with: url) { data, _, _ in
  guard let data = data,
        let domain = try? JSONDecoder().decode(Domains.self, from: data).data.first
  else {
    print("Request failed")
        
    return
  }
    
  Task {
    await MainActor.run {
      print(domain)
      print(Thread.isMainThread)
    }
  }
}

session.resume()

This code uses the same concepts you’ve learned about so far. You create a URL and then download its data using URLSession’s closure-based dataTask method.

Inside the completion closure for the data task you verify the response and ensure you can decode it from JSON to an array of Domains. In case of failure you print an error and return, otherwise you print your domains and a boolean indicating whether your code is executing on the main thread (or main actor).

Notice, however, the new syntax within Task. In order to run a piece of code on the main actor, or main thread, from anywhere, you can use MainActor’s run, which takes a closure with the work to perform. Because run is asynchronous, you mark it as await and run it inside of a Task. Run your code and look at the results.

If you’re coming from using dispatch queues in Swift in order to execute work on the main thread, this is the equivalent with Swift’s modern concurrency features and syntax. Personally, I find it more readable and easy to follow than the closure-heavy, oftentimes non-intuitive syntax of dispatch queues.

Alternatively, instead of calling run on MainActor, you can mark the Task closure to run on the MainActor. Replace the Task code with the following:

Task { @MainActor in
  print(domain)
  print(Thread.isMainThread)
}

What if you want a function, or a method on your types to run on the MainActor? To see an example of how to do this, Add the following code to your playground:

extension Domains {
  func domainNames() -> [String] {
    print("Getting domain names in main thread? \(Thread.isMainThread)")
        
    return data.map { $0.attributes.name }
  }
}

let session2 = URLSession.shared.dataTask(with: url) { data, _, _ in
  guard let data = data,
        let domains = try? JSONDecoder().decode(Domains.self, from: data)
  else {
    print("Request failed")
        
    return
  }

  print(domains.domainNames())
}

session2.resume()

This extension on Domains adds a method that returns all the names of your domains as a String array. You download the domains using URLSession, as you’ve seen before, and print the domains if you are able to successfully parse the response. Run your playground and take a look at the console.

Your domainNames() method is not running on the main thread even though you know we always want it to. While you could use the same pattern you just learned about when wanting to run a task on MainActor, it’s easy to forget to wrap the call to domainNames() in order to always run it on the main thread.

Wouldn’t it just be easier if there was a way to tell Swift that this method should always run on the main thread? There is! Update your method declaration with the following:

@MainActor func domainNames() -> [String] {

The @MainActor attribute tells Swift that you always want to run this method on the MainActor, or main thread. Your playground may be giving you an error now about making a call to an actor-isolated method from a non-isolated context. Fix that by updating the last print statement with the following:

Task {
  await print(domains.domainNames())
}

Now run your and look at the results. As expected, domainNames() not gets executed on the MainActor.

Woohoo! You’ve covered all of the concurrency topics you needed to in order to have an easier time working with URLSession.

Before getting started with our deep dive into URLSession, however, it’s time for a small challenge to help put in practice the concepts you just learned. See ya there! :)

Reviews

Comments