I'm starting coding in Swift, but trying to make an app well, using best practices.
Application should control Philips Hue bridge to change lights states in home. It reads and send http requests from the Hue bridge and must sync it with visible controllers in my app.
Objects received from Hue bridge are stored in HueSession() class and its subclasses and are mapped initially to array of ViewModels and shown on screen. Everything works well up to this point.
Then I want to subscribe to these ViewModels and receive event when user changes application controls. This would allow me to send http request back to Hue bridge.
Unfortunately, although the label on Toggle changes properly, I'm receiving just one event per light when application starts, like this...
false set on Lightstrip 1
false set on Hue color lamp 2
false set on Hue color lamp 1
false set on Hue lightstrip plus
Changing Toggle state in my app does not print the message, but just changes Toggle label text: ON or OFF. Am I using sink in a wrong way? Or appending class to array makes a copy of it, instead a reference?
ApplicationData.swift
class ApplicationData: ObservableObject {
@Published var hueResources: HueSession
@Published var bulbs: [BulbViewModel]
var hue = HueController()
init(){
bulbs = []
hueResources = HueSession()
hue.GetLightsList() {
resources in
if resources != nil {
self.hueResources = resources!
self.hueResources.data.map(){
value in
let bulb = BulbViewModel(id: value.id)
bulb.name = value.metadata.name
bulb.isOn = value.on.on
bulb.$isOn.sink { value in print("\(value) set on \(bulb.name)") }
self.bulbs.append(bulb)
}
}
}
}
}
BulbViewModel.swift
class BulbViewModel: ObservableObject, Identifiable {
@Published
var color = 250.0
@Published
var amplitude = 250.0
@Published
var isOn = false
var isOnText: String {
get {
isOn ? "ON" : "OFF"
}
}
@Published
var name: String = ""
@Published
var id: String
init(id: String){
self.id = id
}
}
BulbView.swift
struct BulbView: View {
@ObservedObject var bulbViewModel: BulbViewModel
var body: some View {
VStack{
Text("Light: \(bulbViewModel.name)")
Slider(value: $bulbViewModel.amplitude, in: 1...254, step: 1.0)
Slider(value: $bulbViewModel.color, in: 153...500, step: 1.0)
Toggle("\(bulbViewModel.isOnText)", isOn: $bulbViewModel.isOn)
}
}
}
First I tried subscribing to on change methods on the View components, but it does not sound like a good practice. I don't want to receive tasks in UI layer.
Changed ViewModel to struct and instead of subscribing to publisher, using now simply didSet for properties that should trigger event.
struct BulbViewModel: Identifiable {
var color: Double = 250.0 {
didSet {
print("\(oldValue) => \(color)")
}
}
...
Longer explanation of the solution:
Created commandAggregator class to collect data
class CommandAggregator {
var notifyEvent: (HueResource) -> Void
var stateChanges: [HueResource] = []
init(notifyEvent: @escaping (HueResource) -> Void) {
self.notifyEvent = notifyEvent
}
func Change(bulbViewModel: BulbViewModel) {
let hueResource = HueResourceBulbVMMapper.MapFromBulbVM(bulbViewModel: bulbViewModel)
stateChanges.append(hueResource)
if notifyEvent != nil {
self.notifyEvent(hueResource)
}
}
func setNotify(notifyEvent: @escaping (HueResource) -> Void){
self.notifyEvent = notifyEvent
}
func onNewCommand(executeTask: @escaping (HueResource) -> Void){
self.notifyEvent = executeTask
}
}
and linking it to every BulbViewModel struct during initialization.
var bulb = BulbViewModel(id: value.id, commandAggregator: self.commandAggregator)
struct BulbViewModel ... {
...
var isOn: Bool = false {
didSet {
Update(key: "on", value: String(isOn))
}
}
func Update(key: String, value: String) {
if commandAggregator != nil {
commandAggregator!.Change(bulbViewModel: self)
}
}
}
Then the commandAggregator class triggers notification.
commandAggregator.setNotify(notifyEvent: {
hueObject in
print("\(hueObject.id) => \(hueObject.on!.on)")
self.hueCommandsQueue.async {
self.hue.SetLight(hueResource: hueObject)
}
})
Any better/simpler solution would be welcome.