Search code examples
iosswiftnsurlcombine

Swift Combine: finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled"


I am trying to fetch data by Restcountries API, but there is a problem. When I try to fetch data the error immediately shows up:

2021-08-24 17:36:44.851463+0200 Countries[1498:19786] Task <3FF6E673-52AD-47FE-9342-229E2CE99859>.<1> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://restcountries.eu/rest/v2/all, NSLocalizedDescription=cancelled, NSErrorFailingURLKey=https://restcountries.eu/rest/v2/all}

It shows every single time. Any idea how to resolve that?

APIService:

final class APIService: APIServiceProtocol {


func fetchAllCountries(url: URL) -> AnyPublisher<[Country], APIError> {
    
    let request = URLRequest(url: url)
    
    return URLSession.DataTaskPublisher.init(request: request, session: .shared)
        .tryMap { data, response in
            guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
                throw APIError.unknown
            }
            
            return data
    }
    .decode(type: [Country].self, decoder: JSONDecoder())
    .mapError { error in
        if let error = error as? APIError {
            return error
        } else {
            return APIError.apiError(reason: error.localizedDescription)
        }
    }
    .eraseToAnyPublisher()
}

}

ListViewModel:

import SwiftUI
import Combine

class ListViewModel: ObservableObject {
    
    private let apiService: APIServiceProtocol
    @Published var countries = [Country]()
    
    init(apiService: APIServiceProtocol = APIService()) {
        self.apiService = apiService
    }
    
    func fetchCountries() {
        
        guard let url = URL(string: "https://restcountries.eu/rest/v2/all") else { return }
        
        let publisher = apiService.fetchAllCountries(url: url)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    break
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }, receiveValue: { data in
                self.countries = data
                print(data)
        })
        publisher.cancel()
    }
    
}

Solution

  • You have to assign the publisher to a strong reference otherwise it cancels itself instantly.

    Create a property

    var cancellable : AnyCancellable?
    

    assign

    cancellable = apiService.fetchAllCountries(url: url) ...
    

    and cancel the publisher in the finished scope

    case .finished:
        cancellable?.cancel()
    

    However if the publisher is a one-shot publisher the cancel line is redundant. When a publisher emits finished the pipeline terminates.