Моя цель: у меня есть приложение для macOS, и я хочу...
NSResponder
.Код ниже является упрощением. Мне нужна помощь: как я могу перехватить значение из NSResponder и распечатать его?
import SwiftUI
import AppKit
struct ContentView: View {
private let emojiResponder = EmojiResponder()
var body: some View {
Button {
// open the emoji picker
if let window = NSApplication.shared.keyWindow {
window.makeFirstResponder(emojiResponder)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NSApp.orderFrontCharacterPalette(nil)
}
}
} label: {
Text("Emoji?")
}
.onAppear {
emojiResponder.onEmojiSelected = { selectedEmoji in
print(selectedEmoji)
}
}
}
}
// Custom Emoji NSResponder
class EmojiResponder: NSResponder {
var onEmojiSelected: ((String) -> Void)?
// use insertTest method from NSResponder
// I assume this allows me to get the input
override func insertText(_ insertString: Any) {
guard let selectedEmoji = insertString as? String else { return }
onEmojiSelected?(selectedEmoji)
print(selectedEmoji)
}
}
Может ли кто-нибудь дать мне подсказку, как это решить? Что происходит сейчас: окно выбора смайлов открывается, но после выбора смайла ничего не происходит.
Примечание. Я использую решение из этого обсуждения — https://stackoverflow.com/a/77797594/5133585. Но я надеюсь на решение без скрытого TextField.
Причина, по которой я не хочу использовать здесь TextField:
У меня есть список, который проходит через массив. В каждой строке есть 3 текстовых поля: [эмодзи] —— [имя] —— [дата]
Когда я выбираю строку и нажимаю Enter в macOS, она фокусируется на текстовом поле эмодзи. Я хочу сосредоточиться на имени TextField. Следовательно, я надеюсь удалить это текстовое поле смайлика и решить эту проблему.
Привет @Sweeper! Я должен был сказать, что использую это решение. Оно работает. Но со скрытым текстовым полем это немного хакерски. Я пытаюсь найти другое решение, не связанное с TextField. Я не знаю, может ли кнопка быть службой быстрого реагирования.
Что вы подразумеваете под «Когда я выбираю и нажимаю Enter в macOS, он фокусируется на текстовом поле эмодзи»? Что вы выбираете?
@Sweeper — Учитывая, что у меня есть список выбора, каждая строка имеет 3 текстовых поля. Когда я выбираю строку (из списка) и нажимаю Enter. Затем macOS автоматически фокусируется на первом TextField (смайлике). Надеюсь, это прояснит ситуацию.
Так что только не ставьте его первым TextField
тогда? Всё равно оно скрыто. Средство выбора смайлов должно сосредоточиться на чем-то, и это должно быть чем-то, что вы можете контролировать, чтобы получить то, что выбрано.
Или вы можете написать подкласс NSButton
, который будет действовать как кнопка «Эмодзи», тогда вы сможете сфокусироваться на этом средстве выбора эмодзи. Поможет ли это?
Я бы предложил отредактировать вопрос, включив в него минимально воспроизводимый пример вашей попытки применить решение из связанного поста и объяснить, как воспроизвести нежелательное поведение.
Средство выбора смайлов должно сосредоточиться на чем-то, и это должно быть чем-то, что вы можете контролировать, чтобы получить то, что выбрано.
Если вам не нравится фокусироваться на скрытом TextField
, вы можете сосредоточиться на кнопке, которая открывает окно выбора смайлов. Создайте подкласс NSButton
, соответствующий NSTextInputClient
, как в этом ответе, и оберните его в NSViewRepresentable
.
struct EmojiButton: NSViewRepresentable {
// I chose to use a callback closure here
// but you can easily make this into a @Binding if you prefer that
let onSelectEmoji: (String) -> Void
func makeNSView(context: Context) -> TextReceiverButton {
context.coordinator.button
}
func updateNSView(_ nsView: TextReceiverButton, context: Context) {
nsView.receiveText = onSelectEmoji
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
@MainActor
class Coordinator: NSObject {
let button = TextReceiverButton(title: "Emoji", target: nil, action: nil)
override init() {
super.init()
button.target = self
button.action = #selector(displayEmojiInButton(_:))
}
@objc func displayEmojiInButton(_ sender: Any) {
NSApp.orderFrontCharacterPalette(nil)
Task {
// this has to be done asynchronously, as in your attempt
button.window?.makeFirstResponder(button)
}
}
}
}
class TextReceiverButton: NSButton, NSTextInputClient {
var receiveText: ((String) -> Void)?
nonisolated func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
}
nonisolated func unmarkText() {
}
nonisolated func selectedRange() -> NSRange {
.init()
}
nonisolated func markedRange() -> NSRange {
.init()
}
nonisolated func hasMarkedText() -> Bool {
false
}
nonisolated func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
nil
}
nonisolated func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
.zero
}
nonisolated func characterIndex(for point: NSPoint) -> Int {
0
}
nonisolated func insertText(_ string: Any, replacementRange: NSRange) {
if let receivedText = string as? String {
Task { @MainActor in
receiveText?(receivedText)
}
}
}
nonisolated func validAttributesForMarkedText() -> [NSAttributedString.Key] {
return [.font, .paragraphStyle, .writingDirection]
}
}
Поскольку вы упомянули выбираемый List
со строками, содержащими текстовые поля, вот пример использования в аналогичной ситуации. Это просто распечатает выбранные смайлы.
struct ContentView: View {
@State var text = ""
@State var selection: Int?
var body: some View {
List(selection: $selection) {
ForEach(0..<10) { i in Row().tag(i) }
}
}
}
struct Row: View {
@State var name = ""
@State var date = ""
var body: some View {
HStack {
EmojiButton {
print("Selected", $0)
}
TextField("Name", text: $name)
TextField("Date", text: $date)
}
}
}
Чтобы средство выбора смайлов отображалось во всплывающем окне, предварительно сфокусированное представление перед выполнением orderFrontCharacterPalette
должно быть текстовым полем. Поэтому, если вы хотите, чтобы всплывающее окно отображалось на кнопке, вам все равно придется поставить невидимый NSTextView
в центре кнопки.
Возможно, поскольку это текстовое поле находится на стороне AppKit, SwiftUI не знает об этом и не будет иметь нежелательного поведения фокуса при нажатии клавиши возврата.
При нажатии кнопки установите для первого ответчика режим невидимого текстового представления, а затем асинхронно вызовите orderFrontCharacterPalette
.
Вот пример реализации.
struct EmojiButton: NSViewRepresentable {
let onSelectEmoji: (String) -> Void
func makeNSView(context: Context) -> NSView {
context.coordinator.view
}
func updateNSView(_ nsView: NSView, context: Context) {
context.coordinator.textView.receiveText = onSelectEmoji
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
@MainActor
class Coordinator: NSObject {
let button = NSButton(title: "Emoji", target: nil, action: nil)
let textView = TextReceiverView(frame: .init(x: 0, y: 0, width: 1, height: 1))
let view = NSView()
override init() {
super.init()
button.target = self
button.action = #selector(displayEmojiInButton(_:))
// remove the cursor
textView.insertionPointColor = .clear
button.translatesAutoresizingMaskIntoConstraints = false
textView.translatesAutoresizingMaskIntoConstraints = false
// add the textView first so the button goes above it
view.addSubview(textView)
view.addSubview(button)
NSLayoutConstraint.activate([
button.leftAnchor.constraint(equalTo: view.leftAnchor),
button.rightAnchor.constraint(equalTo: view.rightAnchor),
button.topAnchor.constraint(equalTo: view.topAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor),
textView.widthAnchor.constraint(equalToConstant: 1),
textView.heightAnchor.constraint(equalToConstant: 1),
textView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
@objc func displayEmojiInButton(_ sender: Any) {
button.window?.makeFirstResponder(textView)
Task {
try await Task.sleep(for: .milliseconds(100))
NSApp.orderFrontCharacterPalette(nil)
}
}
}
}
class TextReceiverView: NSTextView {
var receiveText: ((String) -> Void)?
override func insertText(_ string: Any, replacementRange: NSRange) {
if let receivedText = string as? String {
receiveText?(receivedText)
}
}
}
Спасибо. Это происходит в правильном направлении. Единственная проблема: средство выбора смайлов отображается в виде окна. Техника из этого: stackoverflow.com/a/77797594/5133585 заставляет палитру персонажей (выбор эмодзи) отображаться в виде всплывающей подсказки. (Это здорово, потому что оно следует за окном приложения.) Возможно ли это? В противном случае мне, возможно, придется подумать о другом подходе.
@dev_dev иногда показывает всплывающую подсказку. Кажется, это зависит от того, на чем вы сосредоточились, прежде чем нажать кнопку. Если фокус был на чем-то, для чего можно отобразить подсказку, подсказка будет отображаться оттуда.
@dev_dev Я обновил ответ, чтобы использовать совершенно другой подход. Попробуйте это.
ты лучший, спасибо тебе огромное. Я попробовал это на новом файле проекта, и он работает очень хорошо. Это наставило меня на правильный путь. Я попробую это в своем реальном проекте. Похоже, в таком случае изучение AppKit будет полезно. Я постараюсь изо всех сил изучить AppKit
См. stackoverflow.com/a/77797594/5133585