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