The published events aren't propagated as expected. I've tried adding @State , @ObservableObject , @StateObject , @EnvironmentObject etc. etc., but can't get it to work properly
I've created a minimal example showing the behavior and using the modern @Observable macro.
NOTE: I've also tried the older @ObservableObject paradigm with @Published annotations as well
import SwiftUI
import SwiftData
@main
struct DebugPublishApp: App {
@State var dataSource = Datasource()
typealias ChannelName = String
@State var publishers = [ChannelName: (EventAStream, RollingWindowBuffer)]()
@State var pressed = false
var body: some Scene {
WindowGroup {
Button("Connect to channelA") {
let publisher = dataSource.subscribeEventsOfTypeA(channel: "channelA")
// this doesn't work when i want to re-use the RollingWindowBuffer!
publishers["channelA"] = (publisher, RollingWindowBuffer(publisher: publisher))
}
if let (stream, buffer) = publishers["channelA"] {
ScrollView {
// doesn't work!
RollingView(rollingData: buffer).border(.pink, width: 3)
// creating it inline, the view will udpate but the sinks get called exponentially for each new event that got added
RollingView(rollingData: RollingWindowBuffer(publisher: stream))
}
}
Button("Another way ???") {
pressed.toggle()
}
// try another way...
if pressed {
let publisher = dataSource.subscribeEventsOfTypeA(channel: "channelB")
RollingView(rollingData: RollingWindowBuffer(publisher: publisher))
}
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
struct RollingView: View {
// doesn't work! View won't update
//@State var rollingData : RollingWindowBuffer
var rollingData : RollingWindowBuffer
var body: some View {
Group {
Text("\(rollingData.rollingWindow.count) Events").bold()
ForEach(rollingData.rollingWindow) { event in
Text("\(event.time) , \(event.aData)")
}
}.background {
Rectangle().foregroundStyle(.orange)
}.padding()
}
}
import Foundation
import Combine
// represents a data source manager which routes channels of data coming from async calls
//
// https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app#Make-model-data-observable
// To make data changes visible to SwiftUI, apply the Observable() macro to your data model.
@Observable class Datasource {
// https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app#Change-model-data-in-a-view
// With Observation in SwiftUI, a view can support data changes without using property wrappers or bindings
private var streams: [String: EventAStream] = [:]
// a stream of events comming from channel
func subscribeEventsOfTypeA(channel: String) -> EventAStream {
// reuse existing stream (AKA publisher) if exists
if let stream = streams[channel] {
print("returning existing stream")
return stream
}
print("creating stream on channel \(channel)")
let stream = EventAStream()
streams[channel] = stream
self.mockEventsAStream(channel: channel)
// DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// print("mocking events stream with a timer")
// self.mockEventsAStream(channel: channel)
// }
return stream
}
var cancelable: Cancellable?
func mockEventsAStream(channel: String) {
let timerPublisher = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
var runningCounter = 0
cancelable = timerPublisher.sink { time in
print("mock timer triggered:", time)
runningCounter += 1
let event = EventA(time: time, aData: runningCounter)
print("appending event \(event)")
self.streams[channel]?.events.append(event)
}
}
}
// a generic "Publisher" which is observable on the events array
// views can use the events or chain a window handler
@Observable class DataPublisher<T> {
// the raw events data which should be published
var events: [T] = []
// in reallity there are a few more methods here but irrelevant for this example...
}
struct EventA: Identifiable {
let time: Date
let aData: Int
var id: Date { time }
}
struct EventB {
let time: Date
let bData: String
}
typealias EventAStream = DataPublisher<EventA>
typealias EventBStream = DataPublisher<EventB>
@Observable class RollingWindowBuffer {
let retentionSeconds = 10
var rollingWindow : [EventA] = []
// also tried retaining the stream to see if it would help make the view get updated but had no effect
// var stream : EventAStream
init(publisher: EventAStream) {
print("init called on RollingWindowBuffer")
var lastCalled = DispatchTime.now()
let cancelable = publisher.events.publisher.sink { event in
// why does the sink get called so many times???? it should be called for the single event
// for each publisher event, the sink get's called for the existing events again!
let now = DispatchTime.now()
let latency = now.rawValue - lastCalled.rawValue
lastCalled = now
print("\(Date()) sink called for event \(event)")
self.append(event)
}
var integers = [0, 1, 2, 3]
let xxxx = integers.publisher
.sink { print("Received \($0)") } // sink doesn't get called for numbers that are added
for i in [4, 5, 6, 7, 8 ,9, 10, 11, 12, 13, 14, 15] {
print("appending \(i)")
integers.append(i)
}
}
func append(_ event: EventA) {
let cutOff = Date().addingTimeInterval(-Double(retentionSeconds))
let index = self.rollingWindow.firstIndex { event in
event.time > cutOff
}
if let index {
rollingWindow.removeFirst(index)
}
// this update should cause the observable to be triggered
rollingWindow.append(event)
}
}
Here are some logs:
creating stream on channel channelA
init called on RollingWindowBuffer
init called on RollingWindowBuffer
mock timer triggered: 2024-02-08 14:37:48 +0000
appending event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
init called on RollingWindowBuffer
2024-02-08 14:37:48 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
mock timer triggered: 2024-02-08 14:37:49 +0000
appending event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
init called on RollingWindowBuffer
2024-02-08 14:37:49 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:49 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
mock timer triggered: 2024-02-08 14:37:50 +0000
appending event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
init called on RollingWindowBuffer
2024-02-08 14:37:50 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:50 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:50 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
mock timer triggered: 2024-02-08 14:37:51 +0000
appending event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
init called on RollingWindowBuffer
2024-02-08 14:37:51 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:51 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:51 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
2024-02-08 14:37:51 +0000 sink called for event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
mock timer triggered: 2024-02-08 14:37:52 +0000
appending event EventA(time: 2024-02-08 14:37:52 +0000, aData: 5)
init called on RollingWindowBuffer
2024-02-08 14:37:52 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:52 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:52 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
2024-02-08 14:37:52 +0000 sink called for event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
2024-02-08 14:37:52 +0000 sink called for event EventA(time: 2024-02-08 14:37:52 +0000, aData: 5)
mock timer triggered: 2024-02-08 14:37:53 +0000
appending event EventA(time: 2024-02-08 14:37:53 +0000, aData: 6)
init called on RollingWindowBuffer
2024-02-08 14:37:53 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:53 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:53 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
2024-02-08 14:37:53 +0000 sink called for event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
2024-02-08 14:37:53 +0000 sink called for event EventA(time: 2024-02-08 14:37:52 +0000, aData: 5)
2024-02-08 14:37:53 +0000 sink called for event EventA(time: 2024-02-08 14:37:53 +0000, aData: 6)
mock timer triggered: 2024-02-08 14:37:54 +0000
appending event EventA(time: 2024-02-08 14:37:54 +0000, aData: 7)
init called on RollingWindowBuffer
2024-02-08 14:37:54 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:54 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:54 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
2024-02-08 14:37:54 +0000 sink called for event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
2024-02-08 14:37:54 +0000 sink called for event EventA(time: 2024-02-08 14:37:52 +0000, aData: 5)
2024-02-08 14:37:54 +0000 sink called for event EventA(time: 2024-02-08 14:37:53 +0000, aData: 6)
2024-02-08 14:37:54 +0000 sink called for event EventA(time: 2024-02-08 14:37:54 +0000, aData: 7)
mock timer triggered: 2024-02-08 14:37:55 +0000
appending event EventA(time: 2024-02-08 14:37:55 +0000, aData: 8)
init called on RollingWindowBuffer
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:52 +0000, aData: 5)
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:53 +0000, aData: 6)
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:54 +0000, aData: 7)
2024-02-08 14:37:55 +0000 sink called for event EventA(time: 2024-02-08 14:37:55 +0000, aData: 8)
mock timer triggered: 2024-02-08 14:37:56 +0000
appending event EventA(time: 2024-02-08 14:37:56 +0000, aData: 9)
init called on RollingWindowBuffer
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:52 +0000, aData: 5)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:53 +0000, aData: 6)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:54 +0000, aData: 7)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:55 +0000, aData: 8)
2024-02-08 14:37:56 +0000 sink called for event EventA(time: 2024-02-08 14:37:56 +0000, aData: 9)
mock timer triggered: 2024-02-08 14:37:57 +0000
appending event EventA(time: 2024-02-08 14:37:57 +0000, aData: 10)
init called on RollingWindowBuffer
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:48 +0000, aData: 1)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:49 +0000, aData: 2)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:50 +0000, aData: 3)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:51 +0000, aData: 4)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:52 +0000, aData: 5)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:53 +0000, aData: 6)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:54 +0000, aData: 7)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:55 +0000, aData: 8)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:56 +0000, aData: 9)
2024-02-08 14:37:57 +0000 sink called for event EventA(time: 2024-02-08 14:37:57 +0000, aData: 10)