0

我正在尝试用 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

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

4

2 回答 2

0

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

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,您需要使用组合框新选择的索引从绑定选项中更新绑定值。

于 2021-04-24T22:14:56.170 回答
0
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
于 2021-04-21T12:49:16.347 回答