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...
Can anybody assist?
PS: the solution needs to work with iOS 13 please
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)