Search code examples
iosswiftcombine

Trying to Mock a DataService Publisher in Combine Swift


When I use a publisher that actually makes an API call everything works fine. However, if I try to Mock it I get a crash that says EXC_BAD_ACCESS (code=2, address=0x16b827ed0).

Here is the viewModel:

class ViewModel: ObservableObject {
    
    @Published var dataString: String = ""
    
    let dataService: DataServiceProtocol
    
    init(dataService: DataServiceProtocol) {
        
        self.dataService = dataService
        self.nasaPublisher
            .assign(to: &$dataString). // The app crashes here when i use the mock data service
    }
    
    private lazy var nasaPublisher: AnyPublisher<String, Never> = {
        $dataString
            .flatMap { [unowned self] string in
                
                self.dataService.getNasaData()
            }
            .eraseToAnyPublisher()
    }()
}

`

And here is the data service:

`

enum Endpoint: String {
    case search = "/search"
}

protocol DataServiceProtocol {
    func getNasaData() -> AnyPublisher<String, Never>
}

class DataService: DataServiceProtocol {
    
    let host = "https://images-api.nasa.gov"
    
    func getNasaData() -> AnyPublisher<String, Never> {

        var components = URLComponents()
        components.scheme = "https"
        components.host = "images-api.nasa.gov"
        components.path = "/search"
        components.queryItems = [
            URLQueryItem(name: "q", value: "apollo")
        ]
        
        guard let url = components.url else {
            return Just("false").eraseToAnyPublisher()
        }
        
        return URLSession.shared.dataTaskPublisher(for: url)
          .map { data, response in
            do {
              let decoder = JSONDecoder()

                let str = String(decoding: data, as: UTF8.self)
                
                print(data.prettyPrintedJSONString)
              return str
            }
//            catch {
//              return "false"
//            }
          }
          .replaceError(with: "false")
          .eraseToAnyPublisher()

    }
}

class MockDataService: DataServiceProtocol {
    
    let mock = Just("Test string")
        .setFailureType(to: Never.self)
        .eraseToAnyPublisher()
    
    func getNasaData() -> AnyPublisher<String, Never> {
        
        return mock
    }
    
}

Any advice on what I'm doing wrong would be much appreciated.

I've ran and tested the code with a real API call and it works fine but crashes if I use the mock API call.


Solution

  • In init you have:

            self.nasaPublisher
                .assign(to: &$dataString)
    

    So this is a publisher that assigns its output to the published variable dataString

    Then nasaPublisher is defined as:

        private lazy var nasaPublisher: AnyPublisher<String, Never> = {
            $dataString
                .flatMap { [unowned self] string in
                    
                    self.dataService.getNasaData()
                }
                .eraseToAnyPublisher()
        }()
    

    This is a publisher that reacts whenever the value in dataString changes.

    When dataString changes it causes nasaPublisher to emit a value. That value is assigned to dataString which causes nasaPublisher to emit a value. That value is assigned to dataString which causes nasaPublisher to emit a value. That value is assigned to dataString ...

    It's an infinite loop.

    When you're using your real data service, it creates a unique publisher each time the loop calls getNasaData.

    When using your mock service, however it creates one publisher and getNasaData returns the same one every time.

    That one publisher is going to execute once then release any assets it has. I suspect the second time through the infinite loop one of those assets has been released, causing a crash.