1

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

jymen
  • 13
  • 3

1 Answers1

1

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)
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks for the working example ; but the main différence with my code is that I cannot use State / Binding since I need to make updates of the index from outside the SwiftUI this is the reason why I use ObservedObject an Observable Class instead ... When using ObservedObject instead of Binding the DispatchQueue statement will be executed, you'll see the correct print message ... But the combo textfield will not be updated unless you click on it to give it focus; if you click on it the value will show up . I do not see any reason why Binding is working and not ObservedObject ??? – jymen Nov 29 '19 at 22:47
  • Finally with your helping example I put it into work. The important point being that when you use ObservableObject classes for data communication purposes , you MUST still Bind Published properties and not directly use ObservedObject in children components. Thanks again for your help – jymen Nov 30 '19 at 17:02