Search code examples
macosswiftuipickernscombobox

OSX SwiftUI integrating NSComboBox Not refreshing current sélected


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) :

  • the updateNSView function of the NSViewRepresentable instance is called correctly then (print message visible on the log)
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))")
  }
}


Solution

  • 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

    SwiftUI UIComboBox

    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)
        }
    }