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

1

13

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

Truth answered 17/12, 2019 at 7:9 Comment(4)
It is rather a question of how receivedMIDINoteOn is called. ContentView is a value, so if you pass somewhere ContentView() you just pass there a copy of view, which is different from that one which is drown in view hierarchy. Instead you have to use ObservableObject view model as listener.Abernethy
Okay, so I now have this observable object inside my ContentView. When I try to grab it to add it as a listener with midi.addListener(contentView.keyboardObject), I get an error Expression type '()' is ambiguous. What is going on here? How do I use this observable object?Truth
Ok so I've simply added 'as AKMIDIListener' to the end of that expression and it worked. HOWEVER, now I get the error '[SwiftUI] Publishing changes from background threads is not allowed;' and it doesn't work. Well it's not my fault that AudioKit uses a background thread — how do I rectify this issue?Truth
Use DispatchQueue.main.async {} in your callback to redirect to main queue.Abernethy
S
17

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.

Subhuman answered 17/12, 2019 at 8:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.