Search code examples
swifterror-handlingasync-awaitfuturecombine

cancel Task inside Future (async/await with combine)


I decide to use async/ await with the Combine framework. I used an extension on the Future class and put a Task block inside it. It seems OK, and I get a result or error, but I have a problem with this, If I want to cancel progress on my async function only I canceled the publisher.

I used below the extension and I learned on https://tanaschita.com/20220822-bridge-async-await-to-combine-future/

extension Future where Failure == Error {

    convenience init(taskPriority: TaskPriority? = nil, asyncFunc: @escaping () async throws -> Output) {
        self.init { promise in
            Task(priority: taskPriority) {
                do {
                    let result = try await asyncFunc()
                    promise(.success(result))
                } catch {
                    promise(.failure(error))
                }
            }
        }
    }
}

and write two helper functions for saving data into core data (for example):

func saveIntoCoreData() throws {
        throw DatabaseError.canNotSave
    }

    func doSomeThing() async throws -> String {
        return try await withCheckedThrowingContinuation { continuation in
            do {
                Thread.sleep(forTimeInterval: 5)
                try saveIntoCoreData()
                continuation.resume(with: .success("Result"))
            } catch {
                continuation.resume(with: .failure(error))
            }
        }
    }

and finally, I called these methods with Future and I expected to get a cancel error and stop running for saving data into core data but just got a canceled event from the publisher, and data was saved into core data.

cancelable = Future(taskPriority: .userInitiated) { [weak self] in
            return try await self?.doSomeThing()
        }.eraseToAnyPublisher()
            .sink { completion in
                switch completion {
                case .finished:
                    print("Finished")
                case .failure(let error):
                    print(error.localizedDescription)
                }
            } receiveValue: { value in
                print("result: \(value)")
            }

        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            self.cancelable?.cancel()
        }

I used try Task.checkCancellation() but the task is not canceled and did not work.

Does anyone have an idea to stop the Task?


Solution

  • Futures can't be canceled, but Publishers can.

    Use Ian Keen's AnyPublisher extension

    Then you can:

    extension AnyPublisher where Failure == Error {
        init(taskPriority: TaskPriority? = nil, asyncFunc: @escaping () async throws -> Output) {
            self.init { subscriber in
                let task = Task(priority: taskPriority) {
                    do {
                        subscriber.send(try await asyncFunc())
                        subscriber.send(completion: .finished)
                    } catch {
                        subscriber.send(completion: .failure(error))
                    }
                }
                return AnyCancellable { task.cancel() }
            }
        }
    }