0

I have the following code that makes an API call, receives data and assigns it to Core Data managed objects. This works well, and updates my data.

func importUsers(url: URL) {
    URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .sink(receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("DataImporter.runImport failed with error: \(error)")
            }
        }, receiveValue: { [weak self] data in
            guard let self = self
            else { return }
            
            self.importContext.perform {
                do {
                    // 2. Decode the response. This decodes directly to the Core Data Store
                    let users = try self.decoder.decode([GitUser].self, from: data)
                    
                    try? self.importContext.save()
                } catch {
                    print("DataImporter.runImport failed to decode json with error: \(error)")
                }
            }
        })
        .store(in: &self.cancellables) // store the returned cancellable in a property on `DataImporter`
}

However, I need to return the number of objects returned and decoded as a result of this call. If it fails, I return 0. Essentially, I want this:

func importUsers(url: URL) -> Int {
    URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .sink(receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("DataImporter.runImport failed with error: \(error)")
            }
        }, receiveValue: { [weak self] data in
            guard let self = self
            else { return 0 }
            
            var users: [GitUser] = []
            self.importContext.perform {
                do {
                    // 2. Decode the response. This decodes directly to the Core Data Store
                    users = try self.decoder.decode([GitUser].self, from: data)
                    
                    try? self.importContext.save()
                } catch {
                    print("DataImporter.runImport failed to decode json with error: \(error)")
                }
            }
            return users.count
        }).store(in: &self.cancellables) // error: Cannot convert return expression of type '()' to return type 'Int'
}

How do I return the count of objects received as a result of the network call?

Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Do you expect you can just instantly get a result from a network call? How about returning a `AnyPublisher`? – Sweeper Jun 04 '22 at 16:05
  • I actually did try that, but I got completely lost in how to return it in light of the `cancellable`. And no, I don't expect an instant return from a network call. – Yrb Jun 04 '22 at 16:21
  • `perform` works asynchronously, you need `performAndWait`. – vadian Jun 04 '22 at 16:29
  • @vadian how does this help return the `Int` or `AnyPublisher` ? – Yrb Jun 04 '22 at 17:10
  • `users.count` returns 0 because the body of `perform` is executed after the `return` statement. Just replace `perform` with `performAndWait`. As the name implies it waits. – vadian Jun 04 '22 at 17:13
  • And then just `return users.count`? When I attempt to have the function return an `Int` I get the error "error: Cannot convert return expression of type '()' to return type 'Int'" as noted above. How do I get around that? – Yrb Jun 04 '22 at 17:17
  • Right, my bad, you cannot return something from within the pipeline. Assign the result to a property and use a property observer or a completion handler. Or put the creation of the Core Data records into the pipeline and `sink` the `count`. – vadian Jun 04 '22 at 17:24

1 Answers1

1

My suggestion is to decode the data and create the Core Data records within the pipeline and return a publisher. In another function subscribe to the publisher and sink the number of items and/or handle the error.

I don't have your custom items, you have to manage self properly

func importUsers(url: URL) -> AnyPublisher<Int,Error> {
    URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .tryMap{data -> Int in
            var users: [GitUser] = []
            var cdError : Error?
            self.importContext.performAndWait {
                do {
                    let users = try self.decoder.decode([GitUser].self, from: data)
                    try self.importContext.save()
                } catch {
                    cdError = error
                }
            }
            if let error = cdError { throw error }
            return users.count
        }
        .eraseToAnyPublisher()
}

However you could also use async/await

func importUsers(url: URL) async throws -> Int {
    let (data, _) = try await URLSession.shared.data(from: url)
    let users = try await self.importContext.perform {
        try self.decoder.decode([GitUser].self, from: data)
        try self.importContext.save()
    }
    return users.count
}

Or an iOS 13 compatible async version, here perform can be asynchronous

func importUsers(url: URL) async throws -> Int {
    try await withCheckedThrowingContinuation { continuation in
        let task = URLSession.shared.dataTask(with: url) { [unowned self] (data, _ , error) in
            if let error = error {  continuation.resume(with: .failure(error)); return }
            self.importContext.perform {
                do {
                    let users = try self.decoder.decode([GitUser].self, from: data!)
                    try self.importContext.save()
                    continuation.resume(with: .success(users.count))
                } catch {
                    continuation.resume(with: .failure(error))
                }
            }
        }
        task.resume()
    }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Thank you for the example. Since this is a network call, shouldn't it be stored in a `Set`? This is what has been defeating my attempts. Or, should I store it after the sink in the subscriber? Can I even do that? – Yrb Jun 04 '22 at 17:54
  • With this syntax call `importUsers(url: someURL). sink{...}.store(in: &self.cancellables) – vadian Jun 04 '22 at 17:57
  • Thanks. I updated my comment and that is where I was going with my thinking. – Yrb Jun 04 '22 at 17:58
  • I love the async/await syntax. One question, you used `.perform` instead of `.performAndWait`, is using `.performAndWait` redundant as a result of `await`? – Yrb Jun 04 '22 at 18:06
  • Yes, `...AndWait` is redundant. – vadian Jun 04 '22 at 18:08
  • Well, I forget to state that I am stuck with a target of iOS 13, so neither `data(from:delegate:)` nor `perform(schedule:_:)` are available. – Yrb Jun 04 '22 at 18:09
  • Please see the edit. – vadian Jun 04 '22 at 18:29
  • That worked. I actually combined it with another async func that provided the URL. There is a `guard` statement with s simple `return` that is not causing issues even thought function returns an `Int`. Not sure why the compiler is not complaining. Is is inside the `continuation`. – Yrb Jun 04 '22 at 19:08