Update View every second does not work

I am trying to create a progress bar which updates based on two values received via bluetooth (should represent a race like thing, so based on which value is higher, the progress bar grows or shrinks). For updating the view I followed this tutorial: https://dippnerd.com/swiftui-timer-ui-refresh, but for me it is not working. The timer works just fine, but my view does not update. In my root view I do not use the TimerWrapper, I only use it in a secondary view, so I do not have the @EnvironmentObject of TimerWrapper in my root view controller.


TimerWrapper:

import SwiftUI
import Combine

class TimerWrapper : ObservableObject {
    let willChange = PassthroughSubject<TimerWrapper, Never>()
    
    var timer : Timer!
    
    
    func start(withTimeInterval interval: Double) {        
        self.timer?.invalidate()
        self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
            print("timer fired")

            self.willChange.send(self)
        }
    }
}


The progressBar view looks as following:

import SwiftUI

struct MultiplayerBar: View {
    
    @EnvironmentObject var timerWrapper: TimerWrapper
    
    
    
    var body: some View {

        GeometryReader { geometryReader in
            
            Rectangle()
                .fill(Color.green)

            
            Rectangle()
                .fill(Color.red)
                .frame(width: getPlayer1Progress(maxWidth: geometryReader.size.width))
                .animation(.easeIn)
        }
        .frame(height: 200)
        .cornerRadius(15)
        .padding()
    }
}

struct MultiplayerBar_Previews: PreviewProvider {
    static var previews: some View {
        MultiplayerBar()
            .environmentObject(TimerWrapper())
    }
}


And it is called like this:

MultiplayerBar()
     .shadow(radius: 5)
     .environmentObject(TimerWrapper())


The timer is started in the SceneDelegate.swift file:

if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            
            
            let timer = TimerWrapper()
            timer.start(withTimeInterval: 1)
            
            
            window.rootViewController = UIHostingController(rootView: ShowTrainingView()
                .environmentObject(BLEControl.BLESingleton)
                .environmentObject(UserData()))
            
            self.window = window
            window.makeKeyAndVisible()
        }


Actually I am not quite sure if my approach is right and how I could fix it. Thanks for any help in advance.

Replies

The article you reference is slightly outdated and the publisher in the ObservableObject protocol has been renamed from willChange to objectWillChange so you'll have to start by making that change to your TimerWrapper class for SwiftUI to properly react to changes


It also looks like the timer that you pass to your view is never actually started


MultiplayerBar()
     .shadow(radius: 5)
     .environmentObject(TimerWrapper()) // start(withTimeInterval:) not called


If the MultiplayerBar is a subview of your rootView then you could pass the timer created and started in your SceneDelegate as an environmentObject to the rootView and it will be made available to all interested subviews. Or you need to start the timer you pass to the MultiplayerBar above.

If the timer is only going to be used to update this particular view, another alternative would be to skip the wrapper althogether and just create a timer locally and make the code a bit more explicit, for example


import SwiftUI
struct MultiplayerBar: View {
    @State var player1Progress = 0.0
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View {
        GeometryReader { geometryReader in
            Rectangle()
                .fill(Color.green)
            Rectangle()
                .fill(Color.red)
                .frame(width: player1Progress * geometryReader.size.width))
                .animation(.easeIn)
        }
        .frame(height: 200)
        .cornerRadius(15)
        .padding()
        .onReceive(timer) { _ in
             self.player1Progress = getPlayer1Progress()
        }
    }
}

struct MultiplayerBar_Previews: PreviewProvider {
    static var previews: some View {
        MultiplayerBar()
    }
}

I tried to update the TimerWrapper class and pass it through all my views, but that did not work. Anyway the implementation directly into the view needing the timer worked just fine, so thanks for your answer.

I am also already in thouch with the author of the article and he wants to update it.