Para manter os destaques de busca atualizados em um NSTextView integrado ao SwiftUI, invalide os atributos de renderização do TextKit 2 quando a consulta ou o texto mudar, reconstrua os destaques com base no conteúdo atual no validador de renderização e solicite uma nova exibição.
Este passo a passo cria um pequeno editor para Mac com rótulos sintéticos, como Tile A e Circle B. A tarefa concreta é destacar os rótulos correspondentes enquanto alguém altera o campo de busca ou edita o documento no mesmo app.
O que precisa ser atualizado quando um destaque muda?
Há duas entradas independentes: os caracteres do documento e a consulta de destaque. Editar o documento altera o conteúdo do texto. Alterar um campo de busca do SwiftUI muda o estado do app sem necessariamente modificar nada no sistema de texto. Seu wrapper precisa conectar os dois eventos ao mecanismo de destaque.
A Apple fornece renderingAttributesValidator como callback para fornecer atributos de renderização de um fragmento de layout. Ela também fornece invalidateRenderingAttributes(for:) para invalidar esses atributos em um intervalo de texto. Juntas, essas APIs permitem uma arquitetura em que a consulta é armazenada como estado atual e os destaques são reconstruídos quando a renderização precisa ser validada. Validador de atributos de renderização, Invalidar atributos de renderização.
Solicitar uma nova exibição é uma questão separada. Definir needsDisplay marca a view para ser desenhada na próxima passagem do loop de eventos. Isso não especifica qual regra de destaque mudou nem calcula novas correspondências. Por isso, a implementação abaixo invalida os atributos de renderização e solicita uma nova exibição. NSView needsDisplay.
| Evento | Estado a atualizar | Ação |
|---|---|---|
| O campo de busca muda | A consulta atual do text view | Invalidar os atributos de renderização e solicitar uma nova exibição |
| O texto do documento muda | O AppKit já contém o texto editado | Invalidar os destaques por meio do delegado de texto |
| Um fragmento precisa ser validado | Seus atributos temporários de destaque | Limpar o fundo anterior e aplicar as correspondências atuais |
| A consulta fica vazia | A consulta atual | Limpar os destaques anteriores sem adicionar substituições |
Mantenha o text view no TextKit 2
Use um destino de implantação do macOS 13 ou posterior para este exemplo e inicialize explicitamente o text view com NSTextView(usingTextLayoutManager: true). A Apple introduziu os construtores de conveniência que selecionam o mecanismo na transição descrita em sua sessão da WWDC22. As principais APIs do AppKit para atributos de renderização usadas aqui estão documentadas como disponíveis a partir do macOS 12. Novidades no TextKit e nos text views, NSTextLayoutManager.
Verifique textView.textLayoutManager ao diagnosticar o mecanismo ativo. Acessar layoutManager, inclusive pelo contêiner de texto, pode ativar o modo de compatibilidade do TextKit 1. A Apple descreve essa transição como unidirecional para o text view existente. Mantenha consultas ao gerenciador de layout legado fora do caminho do TextKit 2. Compatibilidade e modernização do TextKit.
O outro limite é a propriedade do layout pelo SwiftUI. A Apple recomenda não alterar diretamente frame nem bounds da view representada. Aqui, a view raiz representada é um NSScrollView; a atribuição do frame inicial cabe à subview de documento do AppKit. O SwiftUI controla o tamanho externo do scroll view. NSViewRepresentable.
Crie um exemplo editável completo
Crie um app SwiftUI para macOS no Xcode, defina o destino de implantação como macOS 13 ou posterior e substitua as declarações geradas do app e da view de conteúdo por este único arquivo. Mantenha exatamente uma declaração @main no destino.
O SwiftUI mantém a consulta de busca. O text view do AppKit mantém este documento de demonstração durante toda a sua existência. A busca é intencionalmente literal, diferencia maiúsculas de minúsculas e não permite sobreposição, para que o exemplo tenha um comportamento previsível com rótulos sintéticos. Esta é uma demonstração em memória; ela não implementa o salvamento do documento.
import SwiftUI
import AppKit
@main
struct HighlightDemoApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
struct ContentView: View {
@State private var query = "Tile"
var body: some View {
VStack {
TextField("Highlight label", text: $query)
HighlightEditor(query: query)
}
.padding()
.frame(minWidth: 480, minHeight: 300)
}
}
struct HighlightEditor: NSViewRepresentable {
let query: String
func makeCoordinator() -> Coordinator { Coordinator() }
func makeNSView(context: Context) -> NSScrollView {
let scroll = NSScrollView()
scroll.hasVerticalScroller = true
let view = HighlightTextView(usingTextLayoutManager: true)
view.frame = scroll.contentView.bounds
view.autoresizingMask = [.width]
view.isVerticallyResizable = true
view.isHorizontallyResizable = false
view.textContainer?.widthTracksTextView = true
view.textContainer?.containerSize = NSSize(
width: scroll.contentSize.width,
height: CGFloat.greatestFiniteMagnitude
)
view.isRichText = false
view.font = .systemFont(ofSize: 16)
view.string = "Tile A\nCircle B\nTile C\nSquare D"
view.delegate = context.coordinator
scroll.documentView = view
guard let manager = view.textLayoutManager else {
preconditionFailure("This demo requires TextKit 2")
}
manager.renderingAttributesValidator = { [weak view] manager, fragment in
view?.render(manager, fragment: fragment)
}
view.query = query
view.refreshHighlights()
return scroll
}
func updateNSView(_ scroll: NSScrollView, context: Context) {
guard let view = scroll.documentView as? HighlightTextView,
view.query != query else { return }
view.query = query
view.refreshHighlights()
}
@MainActor
final class Coordinator: NSObject, NSTextViewDelegate {
func textDidChange(_ notification: Notification) {
(notification.object as? HighlightTextView)?.refreshHighlights()
}
}
}
@MainActor
final class HighlightTextView: NSTextView {
var query = ""
func refreshHighlights() {
guard let manager = textLayoutManager else { return }
manager.invalidateRenderingAttributes(for: manager.documentRange)
needsDisplay = true
}
func render(_ manager: NSTextLayoutManager, fragment: NSTextLayoutFragment) {
let fragmentRange = fragment.rangeInElement
manager.removeRenderingAttribute(.backgroundColor, for: fragmentRange)
guard !query.isEmpty, let content = manager.textContentManager else { return }
let text = string as NSString
var cursor = 0
while cursor < text.length {
let match = text.range(of: query, options: [.literal],
range: NSRange(location: cursor, length: text.length - cursor))
guard match.location != NSNotFound else { break }
guard let start = content.location(content.documentRange.location,
offsetBy: match.location),
let end = content.location(start, offsetBy: match.length),
let range = NSTextRange(location: start, end: end) else {
assertionFailure("Match does not map to current text content")
return
}
if let part = range.intersection(fragmentRange), !part.isEmpty {
manager.addRenderingAttribute(.backgroundColor,
value: NSColor.systemYellow.withAlphaComponent(0.35), for: part)
}
cursor = NSMaxRange(match)
}
}
}
O código foi inspecionado com base nas declarações de API recuperadas, mas não foi compilado nem executado com o AppKit neste ambiente. Verificações determinísticas separadas confirmaram os deslocamentos UTF-16 ilustrativos discutidos abaixo; elas não comprovam o comportamento da interface gráfica.
Por que o wrapper usa dois caminhos de atualização
updateNSView recebe o novo estado do SwiftUI e copia a consulta alterada para o text view existente. O validador lê a consulta armazenada sempre que é executado. Sua referência fraca aponta para a view do AppKit, em vez de capturar um valor inicial do representable que poderia conter uma consulta desatualizada. A Apple define o método de atualização do representable como o local para aplicar novas informações do SwiftUI. Ciclo de vida de NSViewRepresentable.
A digitação no documento segue o caminho do coordenador. A Apple documenta textDidChange(_:) como uma notificação de delegado de que os caracteres ou os atributos de formatação mudaram. O coordenador atualiza os destaques imediatamente em resposta; ele não precisa de um binding do documento para acionar uma atualização do SwiftUI. textDidChange(_:).
O texto inicial de exemplo é atribuído somente durante a criação. Atualizações posteriores da consulta não alteram o documento. Se você adicionar um binding do documento mais tarde, compare o conteúdo recebido antes de substituí-lo e defina qual lado é responsável pelas edições. Atribuir novamente o texto de exemplo a cada atualização sobrescreveria o documento do usuário.
Por que a limpeza precisa acontecer antes da verificação de consulta vazia
Cada validação remove do fragmento o atributo .backgroundColor anterior do mecanismo de destaque antes de procurar correspondências. Assim, uma consulta vazia ainda executa a limpeza. Isso resolve o erro comum em que a busca funciona até o campo ser apagado, deixando visíveis os últimos destaques.
O exemplo usa as APIs documentadas de remoção e adição por chave, limitando suas alterações ao atributo de fundo. Ele pressupõe que este mecanismo de destaque é responsável pelos fundos de renderização; se outro recurso também os fornecer, combine essas regras em uma única política de renderização. Remover atributo de renderização, Adicionar atributo de renderização.
As correspondências também são intersectadas com o fragmento em validação. A Apple define rangeInElement em relação à origem do documento, e intersection(_:) retorna o intervalo de texto compartilhado. Isso permite que uma correspondência contribua apenas com a parte pertencente ao fragmento atual. Intervalo do fragmento, Interseção de intervalos de texto.
Mantenha os deslocamentos das correspondências consistentes com o documento atual
O exemplo busca em um instantâneo de NSString e usa o NSRange retornado. A Apple especifica que os comprimentos, índices e intervalos de NSString usam unidades de código UTF-16. O Swift também expõe a representação UTF-16 de uma string por meio de utf16; a contagem de caracteres do Swift não substitui esses deslocamentos. Indexação de NSString, String.utf16.
Como verificação ilustrativa, acrescente o símbolo neutro e espaço 🔷 ao início do documento de exemplo. O primeiro Tile começa então no deslocamento UTF-16 3, com comprimento 4. A segunda ocorrência começa no deslocamento 19. Insira X no início, e essas posições passam a ser 5 e 21. São exemplos calculados, não observações de uma execução do AppKit.
Converta cada correspondência usando o gerenciador de conteúdo atual: do início do documento até o início da correspondência e, dali, até o fim dela. Não crie localizações de texto a partir de índices inteiros por conta própria nem mantenha localizações antigas de correspondências após edições no documento. A Apple demonstra essa conversão por meio do gerenciador de conteúdo, cuja API de localização pode retornar nil para um deslocamento fora dos limites. Orientações para conversão de intervalos, Localizações do armazenamento de conteúdo de texto.
O loop de busca avança usando o fim efetivo da correspondência retornada. A Apple documenta que os resultados da busca são relativos ao início da string, mesmo ao buscar em um intervalo menor. A proteção para consulta vazia e o avanço para a frente no exemplo também evitam um loop sem avanço. Para um recurso de busca visível ao usuário, escolha critérios de correspondência localizados adequados; a Apple recomenda a busca padrão localizada para textos apresentados aos usuários. Busca de intervalo em NSString.
Diagnostique destaques desatualizados em uma ordem útil
Use esta sequência para distinguir uma atualização de estado ausente de um problema de renderização ou intervalo. Estas são verificações esperadas para o exemplo, não resultados de testes relatados.
- Comece com
Tile. Os dois rótulosTiledevem ter o fundo destacado. - Altere a consulta para
Circle. Os fundos anteriores devem desaparecer e o rótuloCircledeve ficar destacado. - Apague a consulta. Todos os fundos de busca devem desaparecer.
- Restaure
Tilee edite um dos rótulos correspondentes paraSquare. Esse rótulo editado deve deixar de corresponder. - Insira
🔷antes do primeiro rótulo. O destaque deve acompanhar o texto, em vez de permanecer nos deslocamentos inteiros anteriores. - Adicione linhas suficientes de rótulos sintéticos para permitir rolagem. Altere a consulta e, depois, percorra o documento para verificar os fragmentos exibidos recentemente.
Se a consulta mudar, mas nada for atualizado, verifique primeiro updateNSView, a consulta armazenada e a chamada de invalidação. Se as edições no documento falharem enquanto as mudanças na consulta funcionam, verifique a atribuição do delegado e textDidChange(_:). Se as correspondências anteriores continuarem coloridas, confira se a limpeza ocorre antes de cada retorno antecipado para uma consulta vazia.
Se textLayoutManager se tornar nil, investigue o modo de compatibilidade antes de adicionar chamadas de redesenho. A Apple fornece notificações de troca do gerenciador de layout para diagnosticar o fallback de um NSTextView. Esta demonstração exige TextKit 2 e não implementa um mecanismo de destaque do TextKit 1. Diagnóstico do modo de compatibilidade.
Adapte a implementação a documentos maiores
Esta implementação prioriza um caminho de atualização fácil de inspecionar: ela invalida os atributos de renderização do documento e percorre a string atual durante a validação de cada fragmento. Isso repete trabalho e é adequado para uma demonstração pequena, não para uma carga de trabalho com documentos grandes cujo desempenho não foi medido.
Para documentos maiores, calcule as correspondências uma vez por revisão do documento e da consulta e, depois, faça a validação intersectar essas correspondências com cada fragmento. Associe as correspondências em cache à revisão exata do texto que as produziu. Se a busca for assíncrona, descarte os resultados cuja revisão do documento ou da consulta tenha mudado antes de aplicá-los.
Mantenha o trabalho de renderização separado de solicitações de geometria para todo o documento. A documentação de layout da Apple distingue a invalidação de renderização da invalidação de layout, e suas orientações sobre a arquitetura do TextKit recomendam concentrar o trabalho de layout na área visível, pois garantir o layout fora dela pode ser dispendioso. Uma mudança na consulta da cor de fundo deve começar pela invalidação de renderização, sem acrescentar, por padrão, cálculos de layout para o documento inteiro. Operações do NSTextLayoutManager, Conheça o TextKit 2.
À medida que o editor cresce, mantenha explícita a sequência de atualização: receba o estado atual, invalide os atributos de renderização afetados, limpe os valores de destaque obsoletos e reconstrua as correspondências com base no texto atual. Verifique separadamente as mudanças de consulta, as edições no documento, a limpeza e a rolagem, para que cada falha aponte para uma parte específica da implementação.
Fontes
- NSViewRepresentable — Apple
- NSTextLayoutManager — Apple
- renderingAttributesValidator — Apple
- invalidateRenderingAttributes(for:) — Apple
- needsDisplay — Apple
- Novidades no TextKit e nos text views — Apple
- textDidChange(_:) — Apple
- removeRenderingAttribute(_:for:) — Apple
- addRenderingAttribute(_:value:for:) — Apple
- rangeInElement — Apple
- intersection(_:) — Apple
- NSString — Apple
- utf16 — Apple
- location(_:offsetBy:) — Apple
- range(of:options:range:) — Apple
- Conheça o TextKit 2 — Apple