Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: Change @State variable through a function called externally?

So maybe I'm misunderstanding how SwiftUI works, but I've been trying to do this for over an hour and still can't figure it out.

struct ContentView: View, AKMIDIListener {
    @State var keyOn: Bool = false

    var key: Rectangle = Rectangle()

    var body: some View {
        VStack() {
            Text("Foo")
            key
                .fill(keyOn ? Color.red : Color.white)
                .frame(width: 30, height: 60)
        }
        .frame(width: 400, height: 400, alignment: .center)
    }

    func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel, portID: MIDIUniqueID? = nil, offset: MIDITimeStamp = 0) {
        print("foo")
        keyOn.toggle()
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

So the idea is really simple. I have an external midi keyboard using AudioKit. When a key on the keyboard is pressed, the rectangle should change from white to red.

The receivedMIDINoteOn function is being called and 'foo' is printed to the console, and despite keyOn.toggle() appearing in the same function, this still won't work.

What's the proper way to do this?

Thanks

like image 357
Thor Correia Avatar asked Dec 17 '19 07:12

Thor Correia


1 Answers

Yes, you are thinking of it slightly wrong. @State is typically for internal state changes. Have a button that your View directly references? Use @State. @Binding should be used when you don't (or shouldn't, at least) own the state. Typically, I use this when I have a parent view who should be influencing or be influenced by a subview.

But what you are likely looking for, is @ObservedObject. This allows an external object to publish changes and your View subscribes to those changes. So if you have some midi listening object, make it an ObservableObject.

final class MidiListener: ObservableObject, AKMIDIListener {
  // 66 key keyboard, for example
  @Published var pressedKeys: [Bool] = Array(repeating: false, count: 66)

  init() {
    // set up whatever private storage/delegation you need here
  }

  func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel, portID: MIDIUniqueID? = nil, offset: MIDITimeStamp = 0) {
    // how you determine which key(s) should be pressed is up to you. if this is monophonic the following will suffice while if it's poly, you'll need to do more work
    DispatchQueue.main.async {
      self.pressedKeys[Int(noteNumber)] = true
    }
  }
}

Now in your view:

struct KeyboardView: View {
  @ObservedObject private var viewModel = MidiListener()

  var body: some View {
    HStack {
      ForEach(0..<viewModel.pressedKeys.count) { index in
        Rectangle().fill(viewModel.pressedKeys[index] ? Color.red : Color.white)
      }
    }
  }
}

But what would be even better is to wrap your listening in a custom Combine.Publisher that posts these events. I will leave that as a separate question, how to do that.

like image 199
Procrastin8 Avatar answered Nov 15 '22 08:11

Procrastin8