swiftui view isn't updated when data is changing

52 Views Asked by At

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)
0

There are 0 best solutions below