I am currently writing a utility, and as part of this, a string is received (through WebSockets and the Starscream library) and the string value is then displayed in the SwiftUI view (named ReadingsView).
The structure of the code is as follows - there are two classes, the WSManager class which manages the WebSocket connections, and the GetReadings class which has the ObservableObject property, which manages and stores the readings.
When the string is received using the didReceive method in the WSManager class, it is decoded by the decodeText method in the WSManager class, which then calls the parseReceivedStrings method in the GetReadings class.
class WSManager : WebSocketDelegate {
func didReceive(event: WebSocketEvent, client: WebSocket) {
case .text(let string):
// Decode the text
DispatchQueue.main.async {
self.decodeText(recvText: string)
print("Received text: \(string)")
}
recvString = string
}
func decodeText(recvText: String) {
// If the message is allowed, then pass it to getReadings
print("Decoding")
if recvText.hasPrefix("A=") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .allreadings)
print("All readings received")
} else if recvText.hasPrefix("T = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .temperature)
} else if recvText.hasPrefix("P = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .pressure)
} else if recvText.hasPrefix("H = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .humidity)
} else {
print("Unrecognised string.")
}
}
}
enum ReadingType {
case allreadings
case temperature
case pressure
case humidity
}
class GetReadings: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
@Published var temp: Float = 0.0 {
willSet {
print("Temp new = " + String(temp))
objectWillChange.send()
}
}
@Published var pressure: Float = 0.0 {
willSet {
print("Pressure new = " + String(pressure))
objectWillChange.send()
}
}
@Published var humidity: Float = 0.0 {
willSet {
print("Humidity new = " + String(humidity))
objectWillChange.send()
}
}
func getAll() {
//print(readings.count)
//print(readings.count)
wsManager.socket.write(string: "get_all")
}
func parseReceivedStrings (recvText: String, readingType: ReadingType) {
if readingType == .allreadings {
// Drop first two characters
let tempText = recvText.dropFirst(2)
// Split the string into components
let recvTextArray = tempText.components(separatedBy: ",")
// Deal with the temperature
temp = (recvTextArray[0] as NSString).floatValue
// Pressure
pressure = (recvTextArray[1] as NSString).floatValue
// Humidity
humidity = (recvTextArray[2] as NSString).floatValue
}
}
}
When the values are parsed, I would expect the values in the ReadingsView to update instantly, as I have marked the variables as @Published, as well as using the objectWillChange property to manually push the changes. The print statements within the willSet parameters reflect the new values, but the text does not update. In the ReadingsView code, I have compensated for this by manually calling the parseReceivedString method when the refresh button is pressed (this is used as part of the WebSocket protocol to send the request), but this causes the readings to be one step behind where they should be. Ideally, I would want the readings to update instantly once they have been parsed in the method described in the previous paragraph.
struct ReadingsView: View {
@ObservedObject var getReadings: GetReadings
var body: some View {
VStack {
Text(String(self.getReadings.temp))
Text(String(self.getReadings.pressure))
Text(String(self.getReadings.humidity))
Button(action: {
print("Button clicked")
self.getReadings.getAll()
self.getReadings.parseReceivedStrings(recvText: wsManager.recvString, readingType: .allreadings)
}) {
Image(systemName: "arrow.clockwise.circle.fill")
.font(.system(size: 30))
}
.padding()
}
}
}
I am wondering whether I have used the right declarations or whether what I am trying to do is incompatible with using multiple classes - this is my first time using SwiftUI so I may be missing a few nuances. Thank you for your help in advance.
Edited - added code
ContentView
struct ContentView: View {
@State private var selection = 0
var body: some View {
TabView(selection: $selection){
ReadingsView(getReadings: GetReadings())
.tabItem {
VStack {
Image(systemName: "thermometer")
Text("Readings")
}
} .tag(0)
SetupView()
.tabItem {
VStack {
Image(systemName: "slider.horizontal.3")
Text("Setup")
}
}
.tag(1)
}
}
}
If you are using ObservableObject
you don't need to write objectWillChange.send()
in willSet
of your Published
properties.
Which means you can as well remove:
let objectWillChange = ObservableObjectPublisher()
which is provided by default in ObservableObject
classes.
Also make sure that if you're updating your @Published
properties you do it in the main queue (DispatchQueue.main
). Asynchronous requests are usually performed in background queues and you may try to update your properties in the background which will not work.
You don't need to wrap all your code in DispatchQueue.main
- just the part which updates the @Published
property:
DispatchQueue.main.async {
self.humidity = ...
}
And make sure you create only one GetReadings
instance and share it across your views. For that you can use an @EnvironmentObject
.
In the SceneDelegate
where you create your ContentView
:
// create GetReadings only once here
let getReadings = GetReadings()
// pass it to WSManager
// ...
// pass it to your views
let contentView = ContentView().environmentObject(getReadings)
Then in your ReadingsView
you can access it like this:
@EnvironmentObject var getReadings: GetReadings
Note that you don't need to create it in the TabView anymore:
TabView(selection: $selection) {
ReadingsView()
...
}