3

I have made a SwiftUI app that repeatedly fetches telemetry data to update custom views. The views use a variable stored in an EnvironmentObject.

struct updateEO{
    @EnvironmentObject var settings:UserSettings
    func pushSettingUpdate(telemetry: TelemetryData) {
        settings.info = telemetry
        print(settings.info)
    }
}

class DownloadTimer : ObservableObject {
    var timer : Timer!
    let didChange = PassthroughSubject<DownloadTimer,Never>()
    @Published var telemetry = TelemetryData()
    func start() {
        connectToClient()
        self.timer?.invalidate()
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {
            _ in
            guard let url = URL(string: "http://127.0.0.1:25555/api/telemetry") else {
                print("Invalid URL")
                return
            }

            let request = URLRequest(url: url)

            URLSession.shared.dataTask(with: request) { data, response, error in
                if let data = data {
                    if let decodedResponse = try? JSONDecoder().decode(TelemetryData.self, from: data) {
                        DispatchQueue.main.async {
                            updateEO().pushSettingUpdate(telemetry: decodedResponse)
                        }
                        return
                    }
                }
            }.resume()
        }
    }
}

At runtime, when the telemetry is passed to the pushSettingUpdate(telemetry: decodedResponse), the app crashes with an error of 'Fatal error: No ObservableObject of type UserSettings found.'. I understand I may need to pass the struct the EnvironmentObject but I am not sure on how to do that. Any help would be much appreciated. Thanks! :)

xp3dx
  • 43
  • 1
  • 4

1 Answers1

6

You should use @EnvironmentObject in your view and pass it down to your model if needed.

Here, struct updateEO is not a view.

I've created a simpler example to show you how to do this :

UserSettings

class UserSettings: ObservableObject {
    @Published var info: String = ""
}

DownloadTimer

class DownloadTimer: ObservableObject {
    var timer : Timer?
    
    func start(settings: UserSettings) {
        self.timer?.invalidate()
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { t in
            settings.info = t.fireDate.description
        }
    }
}

And you call start (with UserSettings as parameter) when the Text appears.

MyView

struct MyView: View {
    @StateObject private let downloadTimer = DownloadTimer()
    @EnvironmentObject var settings: UserSettings
    
    var body: some View {
        Text(settings.info)
            .onAppear {
                self.downloadTimer.start(settings: self.settings)
        }
    }
}

And don't forget to call .environmentObject function to inject your UserSettings in SceneDelegate.

SceneDelegate

let contentView = MyView().environmentObject(UserSettings())

You should see the text updating as time goes by.

malhal
  • 26,330
  • 7
  • 115
  • 133
Nicolas Mandica
  • 803
  • 1
  • 10
  • 16
  • I need the `class DownloadTimer` to update the Environment Object as it is used by multiple different views. Sorry if I might have misunderstood your answer. How would I get the class to update the EnvironmentObject? – xp3dx Apr 20 '20 at 12:36
  • @xp3dx I've updated my answer with a simplified example to show you how to do what you want. – Nicolas Mandica Apr 20 '20 at 14:18
  • Ah thank you, I changed it to a different approach but this works as well. – xp3dx Apr 22 '20 at 08:16
  • or you can define UserSettings with a shared instance, so you can call that like UserSettings.shared.xxx – LiangWang May 04 '21 at 12:28