Какой код подключения необходим, чтобы обернуть NSComboBox с помощью NSViewRepresentable?

Я пытаюсь обернуть NSComboBox NSViewRepresentable для использования в 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

Заранее благодарим вас, если у вас есть предложения!

2 ответа

      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

Хорошо, я думаю, что пришел к решению, которое удовлетворяет требованиям, которые я изложил в вопросе:

      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, вам необходимо обновить связанное значение из связанных параметров, используя вновь выбранный индекс поля со списком.

Другие вопросы по тегам