Async execution of long-running operations using Swift’s Task

In this article I will try to show you how to adopt Swift’s Task which is part of new Concurrency API’s with an example where we want to execute long-running operations on background thread without changing existing implementation of the operation. Instead making adoption by changing existing code we will build reusable component which will help us to adopt new Swift’s features with ease and move adoption part to the composition part of the application.

Let’s dive in.

Quick intro into how to use Task

As already mentioned Task is part of new Concurrency API and was released with Swift 5.5 version. As stated in Apple’s documentation Task represents a unit of asynchronous work.

“Asynchronous code can be suspended and resumed later, although only one piece of the program executes at a time. Suspending and resuming code in your program lets it continue to make progress on short-term operations like updating its UI while continuing to work on long-running operations like fetching data over the network or parsing files”

How we can recognise an asynchronous code?

Two common ways of indicating that clients of implemented code can expects that code is asynchronous is when we see method with completionBlock.

protocol UsersNamesLoader {
    func load(completion: @escaping (Result<[String], Error>) -> Void)
}

or when method is marked as async.

protocol UsersNamesLoader {
    func load() async -> Result<[String], Error>
}

Task accepts upon creation a priority and an operation. In our case operation would be load method from UsersNamesLoader.

Example when we use load method with completionBlock:

Task(priority: .high) {
    let loader: UsersNamesLoader
    let result = loader.load { result in 
        print(result)
    }
}

Or when we have implementation with async:

Task(priority: .high) {
    let loader: UsersNamesLoader
    let result = await loader.load()
    print(result)
}

Last but not least Task can also execute a synchronous operations as well. And this is focus of this article.

protocol UsersNamesLoader {
    func load() -> [String]
}

Task(priority: .high) {
    let loader: UsersNamesLoader
    let result = loader.load()
    print(result)
}

Motivation

In last example where UsersNamesLoader load method represents a synchronous operation. Let’s assume that load method is doing some heavy work behind the scenes. Let’s say that it needs to call a bunch of different queries to our database and do some heavy data processing.

If our task would be to display a list of users in a UITableView or List if we have already adopted SwiftUI a natural process would be to call load method somewhere from the UI part of the application. And we would call load directly from the UsersNamesLoader interface.

At first this doesn’t look like a problem but as we said before load is really really slow operation and our UI is starting to run slow and users complain about the slow responsiveness of the app.

Solution

We need to solve this but how?

The easiest and most straightforward solution would be to mark load method from UsersNamesLoader as async or add a completion block as seen in first two examples above and and change concrete implementation of UsersNamesLoader.

This approach forces us to adopt our codebase because we have issues on UI. To put it differently – clients of the code dictates implementation of load method and not vise versa.

Why this can lead into a problems? All other client which are using same interface to load users are now forced to adopt their code because we had issues on the UI. It might not be such a problem at first but this can into more people to working on same task / requirement and which at the end of the day takes longer and could be more expensive to do for a company as well.

Another straightforward solution would be to use Task as shown in last example.

class UsersViewController: UIViewController {
    var loader: UsersNamesLoader?
    
    func load() {
        Task(priority: .high) {
            let result = loader?.load()
            /* Execute UI update */
        }
    }
}

This would do the job, but we can do it even better. With approach demonstrated above we are leaking implementation of how concurrency is achieved into UI part of the app. Which is not most optimal way.

What we can do instead is make a component which wraps execution around Task API. Doing so will leave current UsersNamesLoader interface intact and move responsibility of threading and dispatching into composition part of the app.

First we need to create component which uses Task API.

Let’s call it ConcurrentActionExecutor.

final class ConcurrentActionExecutor<Input, Output> {
  typealias Action = (Input) -> Output
  typealias ActionOutputCompletion = (Output) -> Void
  
  let action: Action
  let priority: TaskPriority
  
  init(priority: TaskPriority = .high, action: @escaping Action) {
    self.action = action
    self.priority = priority
  }
  
  func execute(_ input: Input, completion: @escaping ActionOutputCompletion) {
    runTask(input, completion)
  }
  
  func execute(completion: @escaping ActionOutputCompletion) where Input == Void {
    runTask((), completion)
  }
  
  func executeAndDeliverOnMain(_ input: Input, completion: @MainActor @escaping (Output) -> Void) {
    runTaskAndDeliverOnMain(input, completion)
  }
  
  func executeAndDeliverOnMain(completion: @MainActor @escaping (Output) -> Void) where Input == Void {
    runTaskAndDeliverOnMain((), completion)
  }
}

// MARK: - Private
private extension ConcurrentActionExecutor {
  func runTask(_ input: Input, _ completion: @escaping (Output) -> Void) {
    Task(priority: priority) {
      let output = action(input)
      completion(output)
    }
  }
  
  func runTaskAndDeliverOnMain(_ input: Input, _ completion: @MainActor @escaping (Output) -> Void) {
    Task(priority: priority) {
      let output = action(input)
      await completion(output)
    }
  }
}

If we look closely it accepts priority and action as a parameters upon creation.

Priority represents TaskPriority so that we can decide how important this action is for us. To learn more about options I suggest reading documentation.

And Action represents a generic method which takes Input as parameter and delivers Output as a returning result.

There are multiple execute options available covering all cases where either output or input represents void making API of ConcurrentActionExecutor a bit easier to use . Two are special cases where @MainActor attribute is added – this guarantees that output is deliver on main queue.

Actual execution is done in runTask or runTaskAndDeliverOnMain depending on which execute method is used. Both methods wrap execution into Task with the priority that was injected when we created an instance of ConcurrentActionExecutor.

Now let’s put things in action and see how it would look like with UsersNamesLoader.

First we need to change signature of the loader property to decouple UsersViewController from concrete interface.

class UsersViewController: UIViewController {
    typealias Loader = ((_ completion: @escaping (Result<[String], Error>) -> Void) -> Void)
    var loader: Loader?
    
    func loadUsers() {
        loader?() { users in
            /* Execute UI update */
        }
    }
}

Next and final step is, as mentioned before, compose / migrate to new loader in the composition part of the app. If we are using composers / factories this is a really simple step which takes only one line of change.

final class UsersUIComposer {
    static func makeView(loader: UsersNamesLoader) -> UsersViewController {
        let controller = UsersViewController()
        /* Previous implementation
        controller.loader = loader
        */

        / * New with implementation with ConcurrentActionExecutor */
        controller.loader = ConcurrentActionExecutor(action: loader.load).executeAndDeliverOnMain
        return controller
    }
}

Doing so we achieved multiple things:

  • First and most important one is that we didn’t change existing code / implementation of load method
  • Decoupled UI components from how concurrency is achieved. Only thing that UI knows is that method is async.
  • Created reusable component which can be used in multiple places across the application and can help us adopt new Concurrency API with ease.

Conclusion

And that’s it. We made simple ConcurrentActionExecutor around Task API which enables us to run long-running task in the background. By using it we can move fast without breaking existing code and adopt to special requirements where needed instead.

If you are interested in actual code using async/await approaches as well I invite you to check it on GitHub as well.

In case of any questions or comments feel free to contact me or leave it in the comments section bellow.

Architectural Patterns – Decorator

/

In this article I will present you powerful architectural pattern that I have been using it a lot called Decorator.

1. What is Decorator

...

Detect user’s country without accessing their location

/

Sometimes we are faced with challenge where we would like to improve user experience of our app based on where in the world or better said in which country ...

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments