Search code examples
swiftuicombine

How to reset a cancelled subscription bound to SwiftUI


In the following example code, a SwiftUI form holds an Observable object that holds a trivial pipeline that passes a string through to a @Published value. That object is being fed by the top line of the SwiftUI form, and the output is being displayed on the second line.

The value in the text field in the first row gets propagated to the output line in the second row, whenever we hit the "Send" button, unless we hit the "End" button, which cancels the subscription, as we'd expect.

import SwiftUI
import Combine

class ResetablePipeline: ObservableObject {
    @Published var output = ""
    var input = PassthroughSubject<String, Never>()
    
    init(output: String = "") {
        self.output = output
        self.input
            .assign(to: &$output)
    }
    
    func reset()
    {
        // What has to go here to revive a completed pipeline?
        self.input
            .assign(to: &$output)

    }
}

struct ResetTest: View {
    @StateObject var pipeline = ResetablePipeline()
    @State private var str = "Hello"
    
    var body: some View {
        Form {
            HStack {
                TextField(text: $str, label: { Text("String to Send")})
                Button {
                    pipeline.input.send(str)
                } label: {
                    Text("Send")
                }.buttonStyle(.bordered)
                Button {
                    pipeline.input.send(completion: .finished)
                } label: {
                    Text("End")
                }.buttonStyle(.bordered)
            }
            Text("Output: \(pipeline.output)")
            Button {
                pipeline.reset()
            } label: {
                Text("Reset")
            }
        }
    }
}

struct ResetTest_Previews: PreviewProvider {
    static var previews: some View {
        ResetTest()
    }
}

My understanding is that hitting "End" and completing/cancelling the subscription will delete all the Combine nodes that were set up in the ResetablePipeline.init function (currently only the assign operator).

But if we wanted to reset that connection, how would we do that (without creating a new ResetablePipeline object). What would you have to do in reset() to reconnect the plumbing in the ResetablePipeline object, so that the Send button would work again? Why does the existing code not work?


Solution

  • Well I'll be. If I simply add input = PassthroughSubject<String, Never>() to the start of reset() (ie replace the original cancelled head-publisher with a fresh one), it seems to do the trick.

    Now, I'm not entirely sure if this code is not leaking something since I don't know exactly what assign(to:) does with the old subscription, but assuming that it's sensible, this might be OK.

    Can anyone see anything wrong with this approach?