Search code examples
swiftuicombine

Combine: dataTaskPublisher(for:) doesn't output value


I try to separate network layer code with Combine, create a class HTTPAsyncManager to hold network request. But I found the output is not triggered from dataTaskPublisher(for: url).

And I think the problem is on this line private var requests = Set<AnyCancellable>(), if I move it and network code into contentView and create a State @State private var requests = Set<AnyCancellable>() for Cancellable. It could output the data. But I do want to separate it from ContentView. What should I change?

import Combine
import SwiftUI


class HTTPAsyncManager {
    private var requests = Set<AnyCancellable>()
    
    func fetch(_ url: URL) {
        let decoder = JSONDecoder()
        
        URLSession.shared.dataTaskPublisher(for: url)
            .handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
                              receiveOutput: { print("Receive output: \($0)") },
                              receiveCompletion: { print("Receive completion: \($0)") },
                              receiveCancel: { print("Receive cancel") },
                              receiveRequest: { print("Receive request: \($0)") })
            .map(\.data)
            .decode(type: User.self, decoder: decoder)
            .replaceError(with: User.default)
            .sink(receiveValue: { print($0.name) })
            .store(in: &requests)
    }
}
import Combine
import SwiftUI

struct User: Decodable {
    var id: UUID
    var name: String

    static let `default` = User(id: UUID(), name: "Anonymous")
}

struct ContentView: View {
    
    var body: some View {
        VStack {
            Button("Fetch user") {
                let url = URL(string: "https://www.hackingwithswift.com/samples/user-24601.json")!
                HTTPAsyncManager().fetch(url)
            }
        }
        .padding()
    }
}

Console output

Receive subscription: DataTaskPublisher
Receive request: unlimited
Receive cancel

Solution

  • The problem here is that you are not holding any reference to your HTTPAsyncManager in your view. So it gets deallocated immediately. Your view should look like:

    struct ContentView: View {
        private let httpAsyncManager = HTTPAsyncManager()
    
        var body: some View {
            VStack {
                Button("Fetch user") {
                    let url = URL(string: "https://www.hackingwithswift.com/samples/user-24601.json")!
                    httpAsyncManager.fetch(url)
                }
            }
            .padding()
        }
    }