Search code examples
swiftswiftuipicker

How to improve MacOS SwiftUI Picker performance


In an ongoing exercise of migrating a small form app from UIKit to SwiftUI I have encountered a significant performance difference between UIKit PopUpButton and SwiftUI Picker. It is a Picker with a longish list (433 items - ISO639 language list). Under UIKit the PopUpButton opens instantly to all intents and purposes. Under SwiftUI the picker is taking 4-5 seconds from click to list being presented. Sufficiently long for the spinning beachball to appear.

I am guessing that it is dynamically creating the subview of items after the mouse click. Has anyone experience with long picker lists and encountered performance issues? I did an experiment with unrolling the ForEach loop that the picker is built from into a view with Groups within Groups within Groups (that did work) ....however, it took fractionally longer.

The PickerView is

struct ISO639Picker: View {
  @Binding var selection: ISO639LanguageCode
  var body: some View {
    Picker("", selection: $selection) {
      ForEach(codeSet.codes) { code in
        Text(code.alpha3B).tag(code)
      }
    }
  }
}

For completeness, codeSet is a global instance of Class that populates from an ISO639 text source and it is instantiated at app startup. The "codes" member is an array of structures as below.

public struct ISO639LanguageCode: Hashable,Identifiable {
  public var id = UUID()
  public var alpha3B: String
  public var alpha3T: String
  public var alpha2: String
  public var name: String
  public var family: String
}

Any suggestions on where the performance issue may be would be appreciated.


Solution

  • For anyone encountering the same issue, the resolution, for me, is to use Adams' suggestion of an NSPopupButton wrapped in an NSViewRepresentable. It took a while to determine the right notification to hook up with. An implementation that is sufficient for my needs is offered below;

    struct NSPopUpButtonView<ItemType>: NSViewRepresentable where ItemType:Equatable {
      @Binding var selection: ItemType
      var popupCreator: () -> NSPopUpButton
      
      typealias NSViewType = NSPopUpButton
      
      func makeNSView(context: NSViewRepresentableContext<NSPopUpButtonView>) -> NSPopUpButton {
        let newPopupButton = popupCreator()
        setPopUpFromSelection(newPopupButton, selection: selection)
        return newPopupButton
      }
      
      func updateNSView(_ nsView: NSPopUpButton, context: NSViewRepresentableContext<NSPopUpButtonView>) {
        setPopUpFromSelection(nsView, selection: selection)
      }
      
      func setPopUpFromSelection(_ button:NSPopUpButton, selection:ItemType)
      {
        let itemsList = button.itemArray
        let matchedMenuItem = itemsList.filter{($0.representedObject as! ItemType) == selection}.first
        if matchedMenuItem != nil
        {
          button.select(matchedMenuItem)
        }
      }
      
      func makeCoordinator() -> Coordinator {
        return Coordinator(self)
      }
      
      class Coordinator:NSObject {
        var parent: NSPopUpButtonView!
        
        init(_ parent: NSPopUpButtonView)
        {
          super.init()
          self.parent = parent
          NotificationCenter.default.addObserver(self,
                                                 selector: #selector(dropdownItemSelected),
                                                 name: NSMenu.didSendActionNotification,
                                                 object: nil)
        }
        
        @objc func dropdownItemSelected(_ notification: NSNotification)
        {
          let menuItem = (notification.userInfo?["MenuItem"])! as! NSMenuItem
          parent.selection = menuItem.representedObject as! ItemType
        }
      }
    }