Search code examples
swiftuicombinepublisher

SwiftUI & Combine - Struggling to Get Publisher Working


I'd like to be able to watch a variable and get a Publisher stream of its current value and its previous value. I believe scan is the appropriate function to use for this purpose but I am struggling to get it working in a SwiftUI view.

Here's a simplified version of my code:

import Combine
import SwiftUI

struct ContentView: View {

    @State private var count: Int = 0
    @State private var previous: Int?
    @State private var current: Int?

    var body: some View {
        VStack {
            Button("Increment") {
                count += 1
            }
            Text("Previous: \(previous.text)")
            Text("Current: \(current.text)")
        }
        .onReceive(publisher) { (previous, current) in
            self.previous = previous
            self.current = current
        }
    }

    private var publisher: AnyPublisher<(previous: Int?, current: Int), Never> {
        Just(count)
            .scan(Optional<(Int?, Int)>.none) { ($0?.1, $1) }
            .compactMap { $0 }
            .eraseToAnyPublisher()
    }
}

The problem at the moment is that the previous value is always being sent as nil.

I'm pretty sure this is because the publisher needs to be a @State so it doesn't keep being recreated but I'm struggling to make that compile...

enter image description here

Can anybody assist?

PS: the solution needs to work with iOS 13 please


Solution

  • One way to solve the issue is to make count a CurrentValueSubject and, instead of making a separate instance variable, construct the pipeline based on it in the call to onReceive:

    import SwiftUI
    import Combine
    import PlaygroundSupport
    
    struct ContentView: View {
        @State private var count = CurrentValueSubject<Int, Never>(0)
        @State private var previous: Int?
        @State private var current: Int = 0
    
        var body: some View {
            VStack {
                Button("Increment") {
                  count.send(count.value + 1)
                }
              Text("Previous: \(String(describing: previous))")
              Text("Current: \(String(describing: current))")
            }
            .onReceive(count
              .dropFirst()
              .scan((previous: nil, current: count.value)) {
                (previous: $0.current, current: $1)
              }) {
                previous = $0.previous
                current = $0.current
            }
            .frame(width: 320, height: 640)
        }
    }
    
    let content = ContentView()
    PlaygroundSupport.PlaygroundPage.current.liveView = UIHostingController(rootView: content)