Combine: Sink/Assign not called when not assigned to a variable

Hey,

I want to request https://swift.org. If I do not assign the result of the publisher/subject sink(receiveCompletion:receiveValue:) of type AnyCancellable, the request will not executed.

Do you know if this behaviour is correct? Or does the compiler removes this code?

import SwiftUI
import Combine

struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()
    
    var body: some View {
        Form {
            Text(self.viewModel.content)
            Text(self.viewModel.output)
            Button("noVar") {
                self.viewModel.noVar()
            }
            Button("variable") {
                self.viewModel.variable()
            }
        }
    }
}
final class ViewModel: ObservableObject {
    @Published var content = "Loading swift.org"
    @Published var output = "no output"
    private var cancellable: AnyCancellable! = nil
    
    func noVar() {
        self.output = "noVar start"
        _ = URLSession.shared.dataTaskPublisher(for: URL(string: "https://swift.org")!) //never called
            .receive(on: RunLoop.main)
            .tryMap { (data, response) in
                self.output = "_gotRespond"
                let result = String(data: data, encoding: .utf8)!
                print(result) // no print
                self.output = "_isOnline \(result)"
                return result
            }
            .mapError { error -> Error in
                self.output = error.localizedDescription
                return error
            }
            .sink { completion in
                switch completion {
                case .failure(let error):
                    self.output = error.localizedDescription
                case .finished:
                    self.output = "noVar ends"
                }
            } receiveValue: {
                self.content = $0
            }
    }
    
    func variable() {
        self.output = "variable"
        self.cancellable = URLSession.shared.dataTaskPublisher(for: URL(string: "https://swift.org")!)
            .receive(on: RunLoop.main)
            .tryMap { (data, response) in
                self.output = "v_gotRespond"
                let result = String(data: data, encoding: .utf8)!
                print(result)
                self.output = "v_isOnline \(result)"
                return result
            }
            .mapError { error -> Error in
                self.output = error.localizedDescription
                return error
            }
            .replaceError(with: "v_Error printed")
            .assign(to: \.content, on: self)
    }
    
    deinit {
        print("canceling")
        self.cancellable.cancel()
    }
}
1 Like

This is expected behavior. The lifetime of the cancellation token controls the lifetime of the observation and underlying work, like the network request.

2 Likes

Thank you! I think, maybe this behaviour was changed, because my old code, created 2019 with Combine (iOS 13) does not execute the publisher anymore... But now, the reason why is clear and logically.

This behavior is racy between the cancellation of the token once it's deinitd and the completion of the network request, so I would expect differing behavior through a variety of variables.