Para revelar o ponto de inserção no NSTextView encapsulado do seu app para Mac, restaure o foco do teclado e chame scrollRangeToVisible(_:) com a seleção atual. Configure a cor do cursor separadamente. Manter o cursor na tela e impedir que ele pisque exigem controles diferentes.
Este guia cria um pequeno editor local com um botão Reveal insertion point. Ele aborda uma tarefa comum em computadores: voltar à posição de edição depois de rolar o documento para outro lugar. A implementação preserva a seleção atual e permite que o AppKit gerencie a entrada normal de texto e o desenho do cursor.
Identifique por que o ponto de inserção está ausente
Comece pelo sintoma visível. Rolagem, foco, seleção e animação do cursor podem produzir resultados parecidos, mas cada um exige uma resposta diferente.
| O que você observa | O que verificar | Ação apropriada |
|---|---|---|
| Sua posição de edição está fora da área visível do documento | A seleção atual da visualização de texto | Rolar até que esse intervalo fique visível |
| Clicar em outro controle interrompe a digitação no editor | O primeiro respondedor da janela | Devolver o foco explicitamente quando solicitado |
| É difícil distinguir o cursor do plano de fundo | As cores do cursor e do plano de fundo | Configurar as propriedades de cor da visualização de texto |
| O texto está realçado em vez de mostrar um único cursor | Se o intervalo selecionado tem comprimento diferente de zero | Preservar a seleção, a menos que o usuário solicite uma mudança |
| O cursor desaparece e reaparece enquanto o editor está ativo | A animação normal do cursor | Tratar o piscar separadamente da rolagem |
A Apple documenta scrollRangeToVisible(_:) como uma forma de rolar a visualização de rolagem que contém o texto até que os primeiros caracteres do intervalo fornecido fiquem visíveis. Ela não promete centralizar esse intervalo nem exibir uma seleção longa inteira. Isso a torna adequada para revelar uma posição de inserção sem que você precise calcular os retângulos dos glifos. Documentação de rolagem da Apple.
O foco pertence à janela que contém a visualização. A Apple instrui explicitamente os chamadores a usar window.makeFirstResponder(textView) em vez de invocar diretamente becomeFirstResponder() da visualização de texto. A implementação abaixo segue esse caminho e verifica qual objeto realmente se tornou o primeiro respondedor. Documentação da Apple sobre NSTextView, documentação sobre a transição de primeiro respondedor.
Crie um editor local com um comando para revelar o ponto
Crie um projeto de app para macOS no Xcode usando SwiftUI e Swift. Defina o destino de implantação como macOS 13 ou posterior para este exemplo. Substitua o ponto de entrada do app gerado pelo primeiro arquivo abaixo e adicione o segundo arquivo ao mesmo destino do app. Juntos, esses arquivos compõem o app de demonstração completo.
O exemplo usa texto sintético e mantém as edições na visualização do AppKit durante toda a vida útil dela. Não há implementação para salvar documentos nem modelo de texto externo. Ao fechar o app, as edições são descartadas. O código foi conferido com a documentação consultada; ele não foi compilado nem executado aqui porque o ambiente disponível é Linux e não tem o SDK do AppKit.
Crie a janela do SwiftUI
O botão incrementa um número de solicitação. Isso dá ao encapsulador um comando específico para processar, em vez de fazer com que cada atualização do SwiftUI volte a focar ou rolar o editor.
// CaretDemoApp.swift
import SwiftUI
@main
struct CaretDemoApp: App {
var body: some Scene {
WindowGroup("Local Editor") {
EditorScreen()
}
}
}
struct EditorScreen: View {
@State private var revealRequest = 0
private let sampleText = (1...80)
.map { "Sample row \($0): blue square, green circle." }
.joined(separator: "\n")
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Button("Reveal insertion point") {
revealRequest += 1
}
CaretEditor(
initialText: sampleText,
revealRequest: revealRequest
)
}
.padding()
.frame(minWidth: 480, minHeight: 320)
}
}
Encapsule o editor rolável do AppKit
NSViewRepresentable é a ponte da Apple para gerenciar uma visualização do AppKit em uma hierarquia do SwiftUI. Aqui, a visualização representada é um NSScrollView, com um NSTextView como visualização do documento. O coordenador memoriza a última solicitação processada. A Apple fornece a fábrica de visualizações de texto roláveis usada durante a criação. NSViewRepresentable, scrollableTextView().
// CaretEditor.swift
import AppKit
import SwiftUI
@MainActor
struct CaretEditor: NSViewRepresentable {
let initialText: String
let revealRequest: Int
@MainActor
final class Coordinator {
var lastHandledRequest: Int
init(lastHandledRequest: Int) {
self.lastHandledRequest = lastHandledRequest
}
}
func makeCoordinator() -> Coordinator {
Coordinator(lastHandledRequest: revealRequest)
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSTextView.scrollableTextView()
scrollView.hasVerticalScroller = true
guard let textView = scrollView.documentView as? NSTextView else {
preconditionFailure("Expected an NSTextView document view")
}
textView.isRichText = false
textView.isEditable = true
textView.isSelectable = true
textView.allowsUndo = true
textView.drawsBackground = true
textView.backgroundColor = .textBackgroundColor
textView.textColor = .textColor
textView.insertionPointColor = .textColor
textView.font = .systemFont(ofSize: 15)
textView.textContainerInset = NSSize(width: 12, height: 12)
textView.string = initialText
let end = (initialText as NSString).length
textView.setSelectedRange(NSRange(location: end, length: 0))
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard revealRequest != context.coordinator.lastHandledRequest else {
return
}
context.coordinator.lastHandledRequest = revealRequest
guard let textView = scrollView.documentView as? NSTextView,
let window = textView.window,
textView.acceptsFirstResponder else {
return
}
guard window.makeFirstResponder(textView),
window.firstResponder === textView else {
return
}
textView.scrollRangeToVisible(textView.selectedRange())
}
}
O editor começa com uma seleção de comprimento zero no fim do texto de exemplo. Ao pressionar Reveal insertion point, o editor recebe foco e essa posição é revelada. Os pressionamentos seguintes usam a seleção atual, então mover o cursor para outro lugar altera a posição que o comando revela. O comando não substitui o texto nem move a seleção.
A posição final usa NSString.length, que conta unidades de código UTF-16. A visualização UTF-16 do Swift corresponde às APIs de NSString indexadas; já String.count do Swift conta caracteres. Ao construir intervalos de texto do AppKit, mantenha essas representações consistentes. Ler um selectedRange() existente evita calcular um deslocamento para o próprio comando de revelação. NSString length, Swift String.UTF16View, Swift String.count.
Preserve o estado de edição durante as atualizações do SwiftUI
A escolha fundamental de projeto é separar criação e comandos. makeNSView inicializa o editor uma única vez. updateNSView responde quando o número da solicitação muda e deixa o conteúdo do documento intacto. Isso segue o ciclo de vida do representável que a Apple descreve para criar e atualizar visualizações do AppKit. O SwiftUI também controla o layout da visualização representada, portanto este encapsulador não atribui o frame nem os limites da visualização de rolagem. Orientações da Apple sobre o ciclo de vida e o layout dos representáveis.
Evite atribuir textView.string = initialText em toda atualização neste exemplo. Isso substituiria o documento atual do usuário pelo texto de exemplo original. Da mesma forma, definir incondicionalmente a seleção no fim do documento faria com que a operação de revelação movesse a posição de edição.
Para um editor vinculado ao estado do SwiftUI, adicione uma sincronização bidirecional intencional: encaminhe as edições do usuário por meio de um coordenador e defina quando as mudanças recebidas do modelo substituem o texto. Preserve as seleções válidas durante essas substituições e defina como resolver mudanças simultâneas. Essa é uma responsabilidade distinta de revelar o cursor; o exemplo mantém intencionalmente um único responsável pelo conteúdo editável. A Apple documenta os coordenadores como o mecanismo para encaminhar interações de delegados e de destino e ação ao SwiftUI. Orientações sobre coordenadores.
As verificações de foco também são importantes. makeFirstResponder(_:) pode falhar quando o respondedor atual se recusa a ceder o foco. A Apple também observa que uma chamada bem-sucedida pode deixar a própria janela como primeiro respondedor se o respondedor solicitado não aceitar o foco. Verificar window.firstResponder === textView confirma o resultado real antes da rolagem. Valores de retorno e comportamento da transição de primeiro respondedor.
Se o editor não estiver anexado a uma janela, este exemplo consome a solicitação sem fazer nada. Pressione o botão novamente depois que ele estiver anexado. Para um recurso de foco inicial, projete uma solicitação explícita que leve em conta o anexo, em vez de reivindicar o foco repetidamente durante atualizações não relacionadas.
Configure a cor sem substituir o desenho do cursor
O exemplo atribui .textColor ao texto e ao ponto de inserção, e .textBackgroundColor ao plano de fundo do editor. Essas são cores semânticas documentadas para o texto e a área atrás dele. insertionPointColor é a propriedade documentada para definir a cor do cursor. Confira a aparência resultante em cada versão e modo de aparência do macOS compatíveis com seu app. Cor do cursor, cor do texto, cor do plano de fundo do texto.
Configurar a cor não solicita foco, não rola o documento nem escolhe um modo de exibição sem piscar. Mantenha essas responsabilidades separadas ao investigar um cursor ausente. Por exemplo, mudar a cor não pode revelar uma posição que continua fora da área visível da visualização de rolagem.
O exemplo também evita acessar layoutManager para calcular a geometria do cursor. A Apple documenta que acessar essa propriedade em uma visualização de texto ou contêiner de texto pode colocar a visualização em modo de compatibilidade usando NSLayoutManager no macOS 12 e posteriores. Chamar a API de alto nível para rolagem evita introduzir essa alteração de compatibilidade do TextKit apenas para revelar a seleção. Comportamento de compatibilidade do TextKit.
Entenda o limite de um cursor sempre visível
Se “manter visível” significa “nunca piscar”, o comando de rolagem não atende a esse requisito. A documentação moderna da Apple sobre cursores distingue a animação automática de um modo de exibição sempre visível.
No macOS 14 e posteriores, NSTextInsertionIndicator oferece os modos de exibição .automatic, .visible e .hidden. A Apple documenta .visible para exibir um cursor continuamente, com as animações desativadas, por exemplo, para indicar um destino de arrastar. As visualizações de texto padrão do AppKit usam automaticamente a interface de cursor do sistema. Guia para adotar o cursor de texto do sistema, NSTextInsertionIndicator.
O caminho documentado para cursores personalizados exige criar e posicionar seu próprio indicador. O modo de exibição controla essa instância; o guia da Apple não oferece uma configuração em NSTextView para mudar para .visible o indicador gerenciado internamente. Considere o caminho personalizado uma escolha arquitetural para interfaces de texto personalizadas, que inclui a responsabilidade de atualizar a geometria e o estado ativo do cursor. Não é uma alteração direta para este encapsulador.
Duas APIs com aparência mais antiga não oferecem essa configuração ausente. shouldDrawInsertionPoint é documentada como somente leitura. drawInsertionPoint(in:color:turnedOn:) desenha ou apaga o cursor, e a Apple diz que os chamadores não deveriam precisar invocá-la diretamente. Nenhuma delas é documentada como uma opção para mantê-lo sempre visível. Estado de desenho do ponto de inserção, método de desenho do ponto de inserção.
Para a tarefa comum de edição descrita neste artigo, mantenha a animação do cursor do AppKit e ofereça uma forma confiável de voltar à posição atual. Uma implementação de cursor personalizado precisa de um projeto próprio e, ao usar NSTextInsertionIndicator, de uma condição de disponibilidade para macOS 14.
Verifique o comportamento no seu app
Use o exemplo para separar as verificações de rolagem, seleção, foco e aparência. Os comportamentos abaixo são esperados com base na implementação; não são resultados de execução relatados.
- Inicie o app e pressione
Reveal insertion point. O editor deve receber foco e revelar sua posição final. - Clique dentro de uma linha anterior para mover o cursor. Role para longe sem clicar em outra posição do texto e pressione o botão. Ele deve revelar essa posição anterior.
- Selecione várias palavras e pressione o botão. A seleção deve permanecer selecionada; o comando não a recolhe em um cursor.
- Digite um rótulo distinto, como
orange triangle, e pressione o botão várias vezes. O conteúdo editado deve permanecer intacto. - Redimensione a janela e repita a operação de revelação. O comando deve continuar usando o intervalo atual, em vez de uma coordenada antiga da tela.
- Confira as aparências clara e escura nas versões compatíveis do macOS. Confirme que as cores do texto, do plano de fundo e do cursor continuam legíveis em conjunto.
Se a rolagem funcionar, mas o cursor ainda aparecer de forma intermitente, verifique se você está observando o piscar normal ou uma seleção de texto ativa. Se o comando não fizer nada, verifique primeiro as condições de anexação à janela e de primeiro respondedor. Vincule o reparo à etapa que falhou: preserve o texto, restaure o foco quando solicitado e revele o intervalo atual.
Fontes
- NSViewRepresentable — Apple
- NSTextView — Apple
- scrollableTextView() — Apple
- scrollRangeToVisible(_:) — Apple
- makeFirstResponder(_:) — Apple
- length — Apple
- String.UTF16View — Apple
- count — Apple
- insertionPointColor — Apple
- textColor — Apple
- textBackgroundColor — Apple
- Adopting the system text cursor in custom text views — Apple
- NSTextInsertionIndicator — Apple
- shouldDrawInsertionPoint — Apple
- drawInsertionPoint(in:color:turnedOn:) — Apple