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.

  1. Inicie o app e pressione Reveal insertion point. O editor deve receber foco e revelar sua posição final.
  2. 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.
  3. Selecione várias palavras e pressione o botão. A seleção deve permanecer selecionada; o comando não a recolhe em um cursor.
  4. Digite um rótulo distinto, como orange triangle, e pressione o botão várias vezes. O conteúdo editado deve permanecer intacto.
  5. 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.
  6. 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