【问题标题】:What wiring code is necessary to wrap NSComboBox with NSViewRepresentable?用 NSViewRepresentable 包装 NSComboBox 需要什么接线代码?
【发布时间】:2021-04-21 06:56:24
【问题描述】:

我正在尝试用 NSViewRepresentable 包装 NSComboBox 以在 SwiftUI 中使用。我想将下拉选项列表和组合框的文本值作为绑定传递。我希望文本值绑定在每次击键时更新,并在选择下拉选项之一时更新。如果在外部更改绑定,我还希望更改组合框的文本值/选择。

现在,我没有看到选项选择的绑定更新,更不用说每次击键了,正如代码底部的 SwiftUI 预览所示。

我阅读旧文档的最新线索是,可能在 NSComboBox 中,选择值和文本值是两个不同的属性,而我已经编写了这个包装,就好像它们是相同的一样?试图运行它。出于我的目的,它们将是相同的,或者至少只有文本值很重要:它是用于任意用户字符串输入的表单字段,也有一些预设字符串。

这里是代码。我认为这应该可以粘贴到 Mac 平台的游乐场文件中:

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

如果您有任何建议,请提前感谢您!

【问题讨论】:

    标签: swiftui appkit nscombobox nsviewrepresentable


    【解决方案1】:
    public struct ComboBoxRepresentable: NSViewRepresentable {
        //If the options change the parent should be an @State or another source of truth if they don't change just remove the @Binding
        @Binding private var options: [String]
        @Binding private var text: 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
            comboBox.stringValue = text
            comboBox.reloadData()
            return comboBox
        }
        
        public func updateNSView(_ comboBox: NSComboBox, context: Context) {
            //You don't need anything here the delegate updates text and the combobox is already updated
        }
    }
    
    public extension ComboBoxRepresentable {
        final class Coordinator: NSObject {
            //This is a much simpler init and injects the new values directly int he View vs losing properties in a class updates can be unreliable
            var parent: ComboBoxRepresentable
            init(_ parent: ComboBoxRepresentable) {
                self.parent = parent
            }
        }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    }
    
    extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
        
        public func comboBoxSelectionDidChange(_ notification: Notification) {
            guard let comboBox = notification.object as? NSComboBox else { return }
            //It is a known issue that this has to be ran async for it to have the current value
            //https://stackoverflow.com/questions/5265260/comboboxselectiondidchange-gives-me-previously-selected-value
            DispatchQueue.main.async {
                self.parent.text = comboBox.stringValue
            }
        }
        
    }
    
    extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
        
        public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
            guard parent.options.indices.contains(index) else { return nil }
            return parent.options[index]
        }
        
        public func numberOfItems(in comboBox: NSComboBox) -> Int {
            parent.options.count
        }
    }
    
    #if DEBUG
    struct ComboBoxRepresentablePreviewWrapper: View {
        @State private var text = "four"
        //If they dont update remove the @Binding
        @State private var options = ["one", "two", "three"]
        var body: some View {
            VStack {
                Text("selection: \(text)")
                
                ComboBoxRepresentable(
                    options: $options,
                    text: $text
                )
            }
        }
    }
    
    struct ComboBoxRepresentable_Previews: PreviewProvider {
        @State private var text = ""
        static var previews: some View {
            ComboBoxRepresentablePreviewWrapper()
                .frame(width: 200, height: 100)
        }
    }
    #endif
    

    【讨论】:

      【解决方案2】:

      好的,我想我已经找到了一个满足我在问题中提出的要求的解决方案:

      public struct ComboBoxRepresentable: NSViewRepresentable {
          private let title: String
          private var text: Binding<String>
          private var options: Binding<[String]>
          private var onEditingChanged: (Bool) -> Void
      
          public init(
              _ title: String,
              text: Binding<String>,
              options: Binding<[String]>,
              onEditingChanged: @escaping (Bool) -> Void = { _ in }
          ) {
              self.title = title
              self.text = text
              self.options = options
              self.onEditingChanged = onEditingChanged
          }
      
          public func makeNSView(context: Context) -> NSComboBox {
              let comboBox = NSComboBox()
              comboBox.delegate = context.coordinator
              comboBox.usesDataSource = true
              comboBox.dataSource = context.coordinator
              comboBox.placeholderString = title
              comboBox.completes = true
      
              return comboBox
          }
      
          public func updateNSView(_ comboBox: NSComboBox, context: Context) {
              comboBox.stringValue = text.wrappedValue
              comboBox.reloadData()
          }
      }
      
      public extension ComboBoxRepresentable {
          final class Coordinator: NSObject {
              private var parent: ComboBoxRepresentable
      
              init(parent: ComboBoxRepresentable) {
                  self.parent = parent
              }
          }
      
          func makeCoordinator() -> Coordinator {
              Coordinator(parent: self)
          }
      }
      
      extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
          public func comboBoxSelectionDidChange(_ notification: Notification) {
              guard let comboBox = notification.object as? NSComboBox,
                    parent.options.wrappedValue.indices.contains(comboBox.indexOfSelectedItem) else { return }
              parent.text.wrappedValue = parent.options.wrappedValue[comboBox.indexOfSelectedItem]
          }
      
          public func controlTextDidChange(_ notification: Notification) {
              guard let comboBox = notification.object as? NSComboBox else { return }
              parent.text.wrappedValue = comboBox.stringValue
          }
      
          public func controlTextDidBeginEditing(_ notification: Notification) {
              parent.onEditingChanged(true)
          }
      
          public func controlTextDidEndEditing(_ notification: Notification) {
              parent.onEditingChanged(false)
          }
      }
      
      extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
          public func comboBox(_ comboBox: NSComboBox, completedString string: String) -> String? {
              parent.options.wrappedValue.first { $0.hasPrefix(string) }
          }
      
          public func comboBox(_ comboBox: NSComboBox, indexOfItemWithStringValue string: String) -> Int {
              guard let index = parent.options.wrappedValue.firstIndex(of: string) else { return NSNotFound }
              return index
          }
      
          public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
              guard parent.options.wrappedValue.indices.contains(index) else { return nil }
              return parent.options.wrappedValue[index]
          }
      
          public func numberOfItems(in comboBox: NSComboBox) -> Int {
              parent.options.wrappedValue.count
          }
      }
      

      关于在用户键入时更新绑定值的问题,您已经实现了父 NSTextField 委托方法 controlTextDidChange

      然后在comboBoxSelectionDidChange 中,您需要使用组合框新选择的索引从绑定选项中更新绑定值。

      【讨论】:

        猜你喜欢
        • 2019-01-20
        • 2011-01-09
        • 1970-01-01
        • 2012-04-24
        • 2012-08-04
        • 1970-01-01
        • 1970-01-01
        • 2013-10-01
        • 1970-01-01
        相关资源
        最近更新 更多