SwiftUI Picker is looking very bad on OSX especially when dealing with long item lists
Swiftui Picker on OSX with a long item list
And since did find any solution to limit the number of item displayed in by Picker on Osx , I decided to interface NSComboBox to SwiftUI
Everythings looks fine until the selection index is modified programmatically using the @Published index of the Observable Comboselection class instance (see code below) :
combo.selectItem(at: selected.index)
combo.selectItem(at: selected.index)
combo.objectValue = combo.objectValueOfSelectedItem
print("populating index change \(selected.index) to Combo : (String(describing: combo.objectValue))")
is executed correctly and the printed log shows up the correct information
But the NSComboBox textfield is not refreshed with the accurate object value
Does somebody here have an explanation ?? ; is there something wrong in code ??
here the all code :
import SwiftUI
class ComboSelection : ObservableObject {
@Published var index : Int
init( index: Int ) {
self.index = index
}
func newSelection( newIndex : Int ) {
index = newIndex
}
}
//
// SwiftUI NSComboBox component interface
//
struct SwiftUIComboBox : NSViewRepresentable {
typealias NSViewType = NSComboBox
var content : [String]
var nbLines : Int
var selected : ComboSelection
final class Coordinator : NSObject ,
NSComboBoxDelegate {
var control : SwiftUIComboBox
var selected : ComboSelection
init( _ control: SwiftUIComboBox , selected : ComboSelection ) {
self.selected = selected
self.control = control
}
func comboBoxSelectionDidChange(_ notification: Notification) {
print ("entering coordinator selection did change")
let combo = notification.object as! NSComboBox
selected.newSelection( newIndex: combo.indexOfSelectedItem )
}
}
func makeCoordinator() -> SwiftUIComboBox.Coordinator {
return Coordinator(self, selected:selected)
}
func makeNSView(context: NSViewRepresentableContext<SwiftUIComboBox>) -> NSComboBox {
let returned = NSComboBox()
returned.numberOfVisibleItems = nbLines
returned.hasVerticalScroller = true
returned.usesDataSource = false
returned.delegate = context.coordinator // Important : not forget to define delegate
for key in content{
returned.addItem(withObjectValue: key)
}
return returned
}
func updateNSView(_ combo: NSComboBox, context: NSViewRepresentableContext<SwiftUIComboBox>) {
combo.selectItem(at: selected.index)
combo.objectValue = combo.objectValueOfSelectedItem
print("populating index change \(selected.index) to Combo : \(String(describing: combo.objectValue))")
}
}
Please see updated & simplified your code with added some working demo. The main reason of issue was absent update of SwiftUI view hierarchy, so to have such update I've used Binding, which transfers changes to UIViewRepresentable and back. Hope this approach will be helpful.
Here is demo
Below is one-module full demo code (just set
window.contentView = NSHostingView(rootView:TestComboBox())
in app delegate
struct SwiftUIComboBox : NSViewRepresentable {
typealias NSViewType = NSComboBox
var content : [String]
var nbLines : Int
@Binding var selected : Int
final class Coordinator : NSObject, NSComboBoxDelegate {
var selected : Binding<Int>
init(selected : Binding<Int>) {
self.selected = selected
}
func comboBoxSelectionDidChange(_ notification: Notification) {
print ("entering coordinator selection did change")
if let combo = notification.object as? NSComboBox, selected.wrappedValue != combo.indexOfSelectedItem {
selected.wrappedValue = combo.indexOfSelectedItem
}
}
}
func makeCoordinator() -> SwiftUIComboBox.Coordinator {
return Coordinator(selected: $selected)
}
func makeNSView(context: NSViewRepresentableContext<SwiftUIComboBox>) -> NSComboBox {
let returned = NSComboBox()
returned.numberOfVisibleItems = nbLines
returned.hasVerticalScroller = true
returned.usesDataSource = false
returned.delegate = context.coordinator // Important : not forget to define delegate
for key in content {
returned.addItem(withObjectValue: key)
}
return returned
}
func updateNSView(_ combo: NSComboBox, context: NSViewRepresentableContext<SwiftUIComboBox>) {
if selected != combo.indexOfSelectedItem {
DispatchQueue.main.async {
combo.selectItem(at: self.selected)
print("populating index change \(self.selected) to Combo : \(String(describing: combo.objectValue))")
}
}
}
}
struct TestComboBox: View {
@State var selection = 0
let content = ["Alpha", "Beta", "Gamma", "Delta", "Epselon", "Zetta", "Eta"]
var body: some View {
VStack {
Button(action: {
if self.selection + 1 < self.content.count {
self.selection += 1
} else {
self.selection = 0
}
}) {
Text("Select next")
}
Divider()
SwiftUIComboBox(content: content, nbLines: 3, selected: $selection)
Divider()
Text("Current selection: \(selection), value: \(content[selection])")
}
.frame(width: 300, height: 300)
}
}