0

I have a quite simple view here, and I want to be able to change 'backgroundColor' in that view as soon as 'homeLightViewModel.locations[locationIndex].lightDevices[deviceIndex].on' changes even not inside of this view. The problem is that variable can be changed in my services or the view can have duplicates, and it does not change it's 'backgroundColor' automatically

import SwiftUI

struct LightDeviceCardView: View {
    
    @StateObject private var homeLightViewModel: HomeLightViewModel
    
    private let locationIndex: Int
    private let deviceIndex: Int
    
    @State public var backgroundColor: Color
    
    init(_ locationIndex: Int,
         _ deviceIndex: Int,
         _ homeLightViewModel: HomeLightViewModel) {
        
        self._homeLightViewModel = StateObject(wrappedValue: homeLightViewModel)
        self.locationIndex = locationIndex
        self.deviceIndex = deviceIndex
        self.backgroundColor = homeLightViewModel
            .locations[locationIndex]
            .lightDevices[deviceIndex]
            .on ? Color.yellow : Color.clear
    }
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            Rectangle()
                .fill(backgroundColor)
                .frame(width: 110, height: 110)
                .cornerRadius(40)
                .offset(x: 30, y: 30)
                .blur(radius: 30)
            Rectangle()
                .foregroundStyle(.ultraThinMaterial)
                .frame(width: 170, height: 170)
                .cornerRadius(25)
            Text(homeLightViewModel
                .locations[locationIndex]
                .lightDevices[deviceIndex]
                .name)
                .foregroundColor(.primary)
                .font(.system(size: 22).bold())
                .multilineTextAlignment(.leading)
                .frame(minWidth: 95, maxWidth: 130)
                .padding(.leading, 15)
                .padding(.top, 15)
        }
        .contextMenu {
            lightCardContextMenuItems
        }
        .onTapGesture {
            homeLightViewModel
                .toggleLightDevice(locationIndex, deviceIndex)
            buttonVisualToggle()
        }
    }
    
    func buttonVisualToggle() {
        withAnimation {
            self.backgroundColor = homeLightViewModel
                .locations[locationIndex]
                .lightDevices[deviceIndex]
                .on ? Color.yellow : Color.clear
        }
    }
    
    var lightCardContextMenuItems: some View {
        Group {
            Button("Action 1", action: { })
            Button("Action 2", action: { })
            Button("Action 3", action: { })
        }
    }
}

struct LightDeviceCardView_Previews: PreviewProvider {
    static var previews: some View {
        let location = Location(locationName: "Bedroom")
        let device = LightDevice(id: 11,
                                 name: "Bedroom Light",
                                 location: location,
                                 data: "",
                                 ipAddress: "",
                                 on: true)
        let error = location.addDevice(device)
        let homeLightViewModel = HomeLightViewModel(locations: [location])
        LightDeviceCardView(0, 0, homeLightViewModel)
    }
}

I assume I need to use @Bindings here and tried but the codebase became really large at this point, and I'm quite stuck and have no idea how to implement it

Jay
  • 3
  • 1
  • State should always be private and never depend on a parent variable. It is a source of truth per the documentation. Same thing with StateObject. – lorem ipsum Aug 24 '23 at 03:00

2 Answers2

0

You can use Binding or get the value directly from your ViewModel. With State you are assigning the initial value for the background but changed values have no relation with the background variable. For initializing with Binding value check Here.

SwiftUI: How to implement a custom init with @Binding variables

Yasha
  • 46
  • 7
  • Thank you for your answer. I understand how to initialize with @Binding, the question is what is the best way to store those bindings in the view model and access the one that corresponds to specific instance of this view. – Jay Aug 24 '23 at 02:41
0

It seems that LightDeviceCardView is a view of a light device. An easy way to model this would be to make LightDevice an ObservableObject. Instead of passing the whole data model to the view, you only need to pass the part of the model that it needs to observe and show a view of, in other words, just the light device.

When the light is toggled, this view (and any other view that is observing the same object) will automatically update because it is observing the device. You should never need to push through the update by calling your own function to toggle the view.

Here is a trimmed down version of your example to illustrate it working.

class LightDevice: Identifiable, ObservableObject {
    let id: Int
    let name: String
//    let location: Location
    let data: String
    let ipAddress: String
    @Published private var on: Bool

    init(id: Int, name: String, /*location: Location,*/ data: String, ipAddress: String, on: Bool) {
        self.id = id
        self.name = name
//        self.location = location
        self.data = data
        self.ipAddress = ipAddress
        self.on = on
    }

    var isOn: Bool {
        on
    }

    func toggle() {
        on.toggle()
    }
}

struct LightDeviceCardView: View {

    @ObservedObject var lightDevice: LightDevice

    private var backgroundColor: Color {
        lightDevice.isOn ? Color.yellow : Color.clear
    }

    var body: some View {
        ZStack(alignment: .topLeading) {
            Rectangle()
                .fill(backgroundColor)
                .frame(width: 110, height: 110)
                .cornerRadius(40)
                .offset(x: 30, y: 30)
                .blur(radius: 30)
            Rectangle()
                .foregroundStyle(.ultraThinMaterial)
                .frame(width: 170, height: 170)
                .cornerRadius(25)
            Text(lightDevice.name)
                .foregroundColor(.primary)
                .font(.system(size: 22).bold())
                .multilineTextAlignment(.leading)
                .frame(minWidth: 95, maxWidth: 130)
                .padding(.leading, 15)
                .padding(.top, 15)
        }
        .contextMenu {
            lightCardContextMenuItems
        }
        .onTapGesture {
            withAnimation { lightDevice.toggle() }
//            buttonVisualToggle()
        }
    }

    var lightCardContextMenuItems: some View {
        Group {
            Button("Action 1", action: { })
            Button("Action 2", action: { })
            Button("Action 3", action: { })
        }
    }
}

struct ContentView: View {
    @StateObject private var lightDevice: LightDevice

    init() {
        let lightDevice = LightDevice(
            id: 11,
            name: "Bedroom Light",
//            location: location,
            data: "",
            ipAddress: "",
            on: true
        )
        self._lightDevice = StateObject(wrappedValue: lightDevice)
    }

    var body: some View {
        LightDeviceCardView(lightDevice: lightDevice)
    }
}
Benzy Neez
  • 1,546
  • 2
  • 3
  • 10