I am trying to wrap NSComboBox with NSViewRepresentable for use in SwiftUI. I would like to pass in both the list of dropdown options and the text value of the combo box as bindings. I would like the text value binding to update on every keystroke, and on selection of one of the dropdown options. I would also like the text value/selection of the combo box to change if binding is changed externally.
Right now, I am not seeing my binding update on option selection, let alone every keystroke, as demonstrated by the SwiftUI preview at the bottom of the code.
My latest lead from reading old documentation is that maybe in an NSComboBox the selection value and the text value are two different properties and I've written this wrapping as if they are one and the same? Trying to run that down. For my purposes, they will be one and the same, or at least only the text value will matter: it is a form field for arbitrary user string input, that also has some preset strings.
Here is the code. I think this should be paste-able into a Mac-platform playground file:
import AppKit
import SwiftUI
public struct ComboBoxRepresentable: NSViewRepresentable {
private var options: Binding<[String]>
private var text: Binding<String>
public init(options: Binding<[String]>, text: Binding<String>) {
self.options = options
self.text = text
}
public func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.usesDataSource = true
comboBox.dataSource = context.coordinator
return comboBox
}
public func updateNSView(_ comboBox: NSComboBox, context: Context) {
comboBox.stringValue = text.wrappedValue
comboBox.reloadData()
}
}
public extension ComboBoxRepresentable {
final class Coordinator: NSObject {
var options: Binding<[String]>
var text: Binding<String>
init(options: Binding<[String]>, text: Binding<String>) {
self.options = options
self.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(options: options, text: text)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
public func comboBoxSelectionDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
text.wrappedValue = comboBox.stringValue
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
guard options.wrappedValue.indices.contains(index) else { return nil }
return options.wrappedValue[index]
}
public func numberOfItems(in comboBox: NSComboBox) -> Int {
options.wrappedValue.count
}
}
#if DEBUG
struct ComboBoxRepresentablePreviewWrapper: View {
@State private var text = "four"
var body: some View {
VStack {
Text("selection: \(text)")
ComboBoxRepresentable(
options: .constant(["one", "two", "three"]),
text: $text
)
}
}
}
struct ComboBoxRepresentable_Previews: PreviewProvider {
@State private var text = ""
static var previews: some View {
ComboBoxRepresentablePreviewWrapper()
.frame(width: 200, height: 100)
}
}
#endif
Thank you in advance if you have any suggestions!