
-
Sessão prática de codificação: crie uma experiência com texto avançado no SwiftUI com o AttributedString
Saiba como criar uma experiência com texto avançado com a API TextEditor e o AttributedString do SwiftUI. Descubra como possibilitar a edição de texto avançado, criar controles personalizados que manipulam o conteúdo do editor e personalizar as opções de formatação disponíveis. Explore os recursos avançados do AttributedString que ajudam a criar as melhores experiências de edição de texto.
Capítulos
- 0:00 - Introdução
- 1:15 - TextEditor e AttributedString
- 5:36 - Criar controles personalizados
- 22:02 - Definir o formato do texto
- 34:08 - Próximas etapas
Recursos
Vídeos relacionados
WWDC25
WWDC22
WWDC21
-
Buscar neste vídeo...
Olá! Sou Max, engineer da equipe SwiftUI. E eu sou Jeremy, engineer na equipe Swift standard libraries! Vamos mostrar como você pode criar experiências de edição de texto com formatação usando recursos da SwiftUI e de AttributedString. Junto com Jeremy, abordarei todos os aspectos importantes das experiências de texto com formatação na SwiftUI: Abordarei a atualização de TextEditor para aceitar texto com formatação com AttributedString. Criarei controles personalizados para aprimorar o editor com recursos exclusivos. Vou criar minha própria definição de formatação de texto para aprimorar editor e conteúdo.
Sou engineer de dia, mas também sou um chef amador que tem a missão de fazer o croissant perfeito. Então, venho preparando um pequeno editor de receitas para monitorar as minhas tentativas.
Ele tem uma lista de receitas à esquerda, um TextEditor para editar a receita no meio e uma lista de ingredientes no inspetor à direita. Queria destacar as partes mais importantes da receita para poder identificá-las facilmente enquanto cozinho. Vou tornar isso possível atualizando o editor para permitir texto com formatação.
Esta é a view do editor, implementada pelo TextEditor da SwiftUI. Como se vê pelo tipo de estado de texto, String, ele só fornece texto sem formatação. Posso mudar String para AttributedString, aumentando os recursos da view.
Com o suporte para texto com formatação, posso usar a interface do sistema para aplicar negrito e todos os tipos de formatação, como aumentar a fonte.
Também posso inserir Genmoji do teclado
e, graças à natureza semântica dos atributos Font e Color da SwiftUI, o novo TextEditor pode permitir Modo Escuro e Fonte Dinâmica também.
O TextEditor permite negrito, itálico, sublinhado, tachado, fontes personalizadas, tamanho do ponto, cores de primeiro plano e plano de fundo, kerning, rastreamento, deslocamento de linha de base, Genmoji e outros estilos. Altura de linha, alinhamento de texto e direção de escrita base também existem como atributos AttributedString separados para a SwiftUI.
Todos esses atributos são consistentes com Text não editável da SwiftUI. Então, você pode pegar o conteúdo de um TextEditor e exibi-lo com Text. Assim como em Text, TextEditor substitui o valor padrão calculado no ambiente p por qualquer AttributedStringKeys por um valor nulo. Jeremy, consegui trabalhar com AttributedStrings até agora, mas eu gostaria de uma recapitulação para garantir que sei o necessário antes de começar a criar controles. Além disso, tenho que fazer essa massa de croissant, então você poderia fazer a recapitulação? Claro, Max! Enquanto você prepara a massa de croissant, vou abordar alguns conceitos básicos de AttributedString que serão úteis ao trabalhar com editores de texto com formatação. Em resumo, AttributedStrings contêm caracteres e execuções de atributo. Por exemplo, este AttributedString armazena o texto “Olá! Tudo pronto para cozinhar?” Ele também tem 3 execuções de atributos: a execução inicial com apenas uma fonte, a execução com cor de primeiro plano adicional e a execução final com o atributo Font.
O tipo AttributedString se encaixa perfeitamente com outros tipos do Swift. É um tipo de valor e, assim como String da biblioteca padrão, armazena o conteúdo usando a codificação UTF-8. AttributedString também permite a conformidade com vários protocolos comuns do Swift, como Equatable, Hashable, Codable e Sendable. Os SDKs da Apple são fornecidos com vários atributos predefinidos agrupados em escopos de atributos. AttributedString também aceita atributos personalizados definidos no app para estilo personalizado na interface.
Crio o AttributedString mencionado usando algumas linhas de código. Crio AttributedString com o início do texto. Crio a string “cozinhar”, defino a cor de primeiro plano como laranja e a adiciono ao texto. Concluo a frase com o ponto de interrogação. Defino a fonte do texto inteiro como a fonte largeTitle. Tudo pronto para exibir na interface no dispositivo!
Descubra os conceitos básicos da criação de AttributedStrings e como criar e usar seus atributos e escopos de atributos personalizados na sessão “Novidades do Foundation”, da WWDC 2021.
Max, parece que você terminou de fazer a massa! Está pronto para falar dos detalhes do uso de AttributedString no app de receitas? Sim, Jeremy, esses recursos são... massa. Tenho pensado em fornecer controles melhores conectando o editor de texto ao restante do app. Quero criar um botão que me permita adicionar ingredientes à lista no inspetor à direita, mas sem ter que digitar o nome do ingrediente manualmente de novo. Por exemplo, quero poder selecionar a palavra “manteiga” que já está na receita e marcá-la como um ingrediente com um único toque de um botão.
O inspetor já define uma chave preference que posso usar no editor para sugerir um novo ingrediente para a lista.
O valor do modificador de view preference aparecerá na hierarquia de visualização e será lido por NewIngredientPreferenceKey em views que usem o editor de receitas.
Definirei a propriedade computada para o valor abaixo do corpo da view.
Só preciso fornecer o nome para a sugestão, como AttributedString. Claro, não quero apenas sugerir todo o texto que está no editor. Mas sim o texto que está selecionado no momento, como mostrei com “manteiga”. TextEditor comunica o que é selecionado por meio do argumento selection opcional.
Vou vincular isso a um estado local do tipo AttributedTextSelection.
Agora que tenho todo o contexto de que preciso na minha view, volto à propriedade computando a sugestão de ingrediente.
Agora, preciso do texto que está selecionado.
Vou tentar usar texto subscrito no resultado da função índices em selection.
Não parece ser o tipo certo.
Retorna AttributedTextSelection.Indices. Vamos melhorar.
Interessante, só tenho uma selection, mas o segundo caso do tipo indices é representado por um conjunto de intervalos.
Jeremy, você pode explicar o motivo disso enquanto termino a massa de croissant? Claro. Também estou ansioso para provar esses croissants deliciosos. Mas, pode deixar Max, explico por que a API não usa o tipo Range que você esperava. Para explicar por que a API usa um RangeSet e não um único Range, vou explicar as seleções de AttributedString. Abordarei como vários intervalos podem formar seleções e demonstrarei como usar RangeSets no código. Você provavelmente já usou um único intervalo em APIs AttributedString ou outras APIs de coleção. Um único intervalo permite que você fatie uma parte de um AttributedString e execute uma ação nesse único bloco. Por exemplo, AttributedString fornece APIs que permitem aplicar rapidamente um atributo a uma parte do texto de uma só vez. Usei a API .range(of:) para encontrar o intervalo do texto “cozinhar” em AttributedString. Em seguida, uso o operador subscript para fatiar AttributedString com o intervalo e deixar a palavra “cozinhar” inteira laranja.
Um AttributedString fatiado com apenas um intervalo não é suficiente para representar seleções em um editor de texto para todos os idiomas. Por exemplo, posso usar o app de receitas para armazenar a receita de Sufganiyot que vou cozinhar nas férias e ela tem texto em hebraico. A receita diz “Put the Sufganiyot in the pan” e usa texto em inglês para as instruções e texto em hebraico para o nome tradicional do prato. No editor de texto, selecionarei uma parte da palavra “Sufganiyot” e a palavra “in” com apenas uma seleção. No entanto, isso na verdade são vários intervalos em AttributedString. Como o inglês é um idioma da esquerda para a direita, o editor expõe a frase da esquerda para a direita. A parte em hebraico, Sufganiyot, está disposta na direção oposta, já que o hebraico é uma língua da direita para a esquerda. Embora a natureza bidirecional desse texto afete o layout visual na tela, AttributedString ainda armazena todo o texto em uma ordem consistente. Essa ordem divide minha seleção em dois intervalos: O início da palavra “Sufganiyot” e a palavra “in”, excluindo o final do texto em hebraico. Por isso, o tipo de seleção de texto da SwiftUI usa vários intervalos em vez de um intervalo.
Para saber como localizar seu app para aceitar texto bidirecional, confira a sessão “Usar texto bidirecional” da WWDC 2022 e a sessão “Aprimorar a experiência multilíngue do seu app” deste ano.
Para aceitar os tipos de seleções, AttributedString permite fatiamento com um RangeSet, o tipo que Max notou anteriormente na API selection. Assim como você pode fatiar AttributedString com um só intervalo, também pode fatiá-lo com um RangeSet para gerar uma subcadeia descontínua. No caso, criei um RangeSet usando a função .indices(where:) na view de caracteres para encontrar todos os caracteres maiúsculos no texto. Definir a cor do primeiro plano como azul nesta fatia tornará todos os caracteres maiúsculos azuis, deixando os outros caracteres inalterados. A SwiftUI também fornece um subscript equivalente que fatia um AttributedString com uma selection diretamente. Max, se tiver terminado a massa do croissant, acho que usar a API subscript que aceita uma seleção pode resolver o erro de compilação no código. Vou testar! Posso usar subscript em um texto diretamente em selection e transformar o AttributedSubstring descontínuo em um novo AttributedString.
Isso é incrível! Ao executar isso no dispositivo e selecionar a palavra “manteiga”, a SwiftUI chama automaticamente a propriedade newIngredientSuggestion para calcular o novo valor, que aparece no restante do app. O inspetor adiciona automaticamente a sugestão na parte inferior da lista de ingredientes. Então, posso colocá-la na lista de ingredientes com um único toque. Recursos como esse valorizam um editor!
Estou muito feliz com isso, mas com tudo o que Jeremy me mostrou até agora, acho que posso ir ainda mais longe. Quero visualizar melhor os ingredientes no próprio texto. Preciso de um atributo personalizado que marque um intervalo de texto como um ingrediente. Vou definir isso em um novo arquivo.
O valor desse atributo será o ID do ingrediente ao qual ele se refere. Volto para a propriedade que calcula IngredientSuggestion no arquivo RecipeEditor.
IngredientSuggestion permite fornecer um fechamento como um segundo argumento.
O fechamento é chamado quando pressiono o botão de adição e o ingrediente é incluído na lista. Usarei esse fechamento para alterar o texto do editor, marcando ocorrências do nome com meu atributo Ingredient. Recebo o ID do ingrediente recém-criado passado para o fechamento.
Em seguida, preciso encontrar todas as ocorrências do nome do ingrediente sugerido no texto.
Posso fazer isso chamando ranges(of:) na view de caracteres de AttributedString.
Agora que tenho os intervalos, posso só atualizar o valor do atributo Ingredient para cada intervalo.
Uso um nome curto para IngredientAttribute que já havia definido.
Vou testar!
Não veremos nada novo aqui, pois o atributo personalizado não tem nenhuma formatação associada a ele. Vou selecionar “fermento” e pressionar o botão de adição.
Espere, o que é isso?! O cursor estava no topo, não no final! Vou tentar novamente!
Seleciono “sal”, pressiono o botão de adição e minha seleção salta para o final. Jeremy, tenho que abrir a massa, então não posso depurar isso agora. Você sabe por que minha seleção foi redefinida? Não é a experiência que queremos para os chefs que usarem seu app. Por que você não começa a abrir a massa e eu verifico esse comportamento inesperado?
Para demonstrar o que aconteceu aqui e como corrigir, explicarei os detalhes dos índices AttributedString que formam intervalos e seleções de texto usados por um TextEditor avançado.
AttributedString.Index representa um único local dentro do texto. Para oferecer suporte ao design avançado e eficiente, AttributedString armazena o conteúdo em uma estrutura de árvore e os índices armazenam caminhos na árvore. Como os índices formam elementos essenciais de seleções de texto na SwiftUI, o comportamento de seleção inesperado decorre de como os índices AttributedString se comportam nas árvores. Você deve ter dois pontos principais em mente ao trabalhar com índices AttributedString. Primeiro, modificações em AttributedString invalidam todos os índices, mesmo aqueles que não estão dentro dos limites das modificações. As receitas não dão certo quando usamos ingredientes vencidos, e isso também se aplica aqui quando usamos índices antigos com AttributedString. Segundo, você só deve usar índices com o AttributedString do qual eles foram criados.
Vou explorar índices do exemplo de AttributedString que criei anteriormente para explicar como funcionam! Como mencionei, AttributedString armazena o conteúdo em uma estrutura de árvore, e aqui tenho um exemplo simplificado da árvore. O uso de uma árvore oferece um desempenho melhor e evita copiar muitos dados ao alterar o texto.
AttributedString.Index referencia o texto armazenando um caminho pela árvore até o local referenciado. O caminho armazenado permite que AttributedString encontre rapidamente texto específico de um índice, mas também significa que o índice contém informações do layout de toda a árvore de AttributedString. Ao modificar o AttributedString, ele pode ajustar o layout da árvore. Isso invalida os caminhos registrados anteriormente, mesmo que o destino do índice ainda exista no texto.
Mesmo que dois AttributedStrings tenham o mesmo conteúdo de texto e atributo, suas árvores podem ter layouts diferentes, tornando os índices incompatíveis entre si.
Usar um índice para percorrer as árvores para encontrar informações requer usar o índice em uma das views do AttributedString. Embora os índices sejam específicos de um AttributedString, você pode usá-los nas views dos caracteres. O Foundation permite visualizar os caracteres, ou clusters de grafema, texto, escalares Unicode individuais que compõem cada caractere e execuções de atributo da string.
Saiba mais sobre as diferenças entre as visualizações escalares de caractere e Unicode na documentação para desenvolvedores da Apple sobre o tipo de de caractere do Swift.
Você pode acessar o conteúdo de nível inferior ao criar conexões com outros tipos semelhantes a strings que não usam o caractere do Swift, como NSString. O AttributedString permite visualizar os escalares UTF-8 e UTF-16 do texto. As duas views compartilham os mesmos índices das views existentes.
Agora, vou analisar o problema que Max encontrou com o app de receitas. O fechamento onApply em IngredientSuggestion modifica a string atribuída, mas não atualiza os índices na seleção. A SwiftUI detecta que esses índices não são mais válidos e move a seleção para o final do texto para evitar a falha do app. Para corrigir isso, use APIs AttributedString para atualizar os índices e seleções quando modificar texto.
Veja este exemplo simplificado de código que tem o mesmo problema do app de receitas. Primeiro, encontro o intervalo da palavra “cozinhar” no texto. Depois, defino o intervalo de “cozinhar” com a cor laranja em primeiro plano e insiro a palavra “chef” nos caracteres para adicionar temas de receita.
Modificar o texto pode alterar o layout da árvore de AttributedString.
Não é válido usar a variável cookingRange depois de modificar a string. Pode até travar o app. Em vez disso, AttributedString fornece uma função transform que usa um Range ou um conjunto de Ranges e um fechamento que modifica o AttributedString fornecido no local. No final, a função transform atualizará o intervalo fornecido com novos índices para garantir que se possa usar corretamente o intervalo no AttributedString resultante. Embora o texto possa ter mudado no AttributedString, o intervalo ainda aponta para o mesmo local semântico, no caso, a palavra “cozinhar”. A SwiftUI fornece uma função equivalente que atualiza a seleção, não o intervalo.
Nossa, Max, esses croissants parecem muito bons! Se você está pronto para voltar ao app, acho que usar a nova função transform ajudará a aprimorar o código. Obrigado! Isso é exatamente o que estava procurando. Vamos ver se posso aplicar no código.
Acho que não deveria fazer loops sobre os intervalos. Quando chego ao último intervalo, o texto é modificado muitas vezes e os índices ficam desatualizados. Posso evitar esse problema convertendo os Ranges em RangeSet.
Então, posso fatiar com isso e remover o loop.
Tudo vira apenas uma mudança, e não preciso atualizar os intervalos restantes após cada modificação.
Depois, ao lado dos intervalos que quero alterar, há também a seleção que representa a posição do cursor. Preciso sempre ter certeza de que corresponde ao texto transformado. Posso fazer isso usando a sobrecarga transform(updating:) da SwiftUI em AttributedString.
Agora a seleção é atualizada sempre que o texto é modificado.
Vamos ver se deu certo! Posso selecionar “leite”, que aparece na lista. Quando o adiciono, a seleção permanece intacta. Verificando novamente, quando pressiono Command + B no teclado, vejo a palavra “leite” ficando em negrito, exatamente como esperado.
Agora que tenho as informações no texto da receita, quero destacar os ingredientes com cores. Felizmente, o TextEditor fornece uma ferramenta para isso: o protocolo de definição de formatação de texto atribuído. A definição de formatação de texto personalizada estrutura-se em torno dos AttributedStringKeys ao qual o editor de texto responde e aos valores que podem ter. Já declarei um tipo em conformidade com o protocolo AttributedTextFormattingDefinition aqui.
Por padrão, o sistema usa o escopo SwiftUIAttributes, sem restrições nos valores dos atributos.
No escopo do editor de receitas, só quero permitir cor no primeiro plano, Genmoji e o atributo Ingredient personalizado.
No editor de receitas, posso usar o modificador attributedTextFormattingDefinition para passar a definição personalizada para o TextEditor da SwiftUI.
Com essa mudança, o TextEditor permitirá qualquer ingrediente, Genmoji e cor em primeiro plano.
Todos os outros atributos agora assumirão o valor padrão. Você pode alterar o valor padrão de todo o editor modificando o ambiente. Com base nessa alteração, o TextEditor já fez algumas alterações importantes na interface de formatação do sistema. Ela não oferece mais controles para alterar o alinhamento, altura da linha ou propriedades da fonte, já que os respectivos AttributedStringKeys não estão no escopo. Ainda posso usar o controle de cores para aplicar cores arbitrárias ao texto, mesmo que elas não façam sentido.
Essa não, o leite sumiu!
Só quero que os ingredientes sejam destacados em verde. Todo resto deve usar a cor padrão. Posso usar o protocolo AttributedTextValueConstraint da SwiftUI para implementar isso.
Vou voltar para o arquivo RecipeFormattingDefinition e declarar a restrição.
Para estar em conformidade com AttributedTextValueConstraint, especifico o escopo de AttributedTextFormattingDefinition ao qual ele pertence e o AttributedStringKey que desejo restringir, no caso, o atributo de cor em primeiro plano. A lógica real para restringir o atributo reside na função constrain. Na função, defino o valor de AttributeKey, a cor em primeiro plano, com o que considero válido.
No caso, a lógica depende da definição do atributo Ingredient.
Em caso afirmativo, a cor em primeiro plano deve ser verde, caso contrário, deve ser nula. Isso indica que o TextEditor deve usar a cor padrão.
Depois de definir a restrição, só preciso adicioná-la ao corpo de AttributedTextFormattingDefinition.
Nesse ponto, a SwiftUI cuida do restante. O TextEditor aplica automaticamente a definição e as restrições a qualquer parte do texto antes que ele apareça na tela.
Todos os ingredientes estão verdes agora!
Curiosamente, o TextEditor desativou o controle de cores, apesar de a cor em primeiro plano estar no escopo da definição de formatação. Faz sentido, considerando a restrição IngredientsAreGreen que adicionei. A cor em primeiro plano agora depende exclusivamente da marcação do texto com o atributo Ingredient. O TextEditor examina automaticamente AttributedTextValueConstraints para determinar se alguma alteração é válida para a seleção atual. Eu poderia tentar definir a cor em primeiro plano de “leite” como branco. Executar minha restrição IngredientsAreGreen depois mudaria a cor em primeiro plano de volta para verde, então o TextEditor sabe que essa não é uma alteração válida e desativa o controle. A restrição de valor também será aplicada ao texto que eu colar no editor. Quando copio um ingrediente usando Command + C e colo usando Command + V, o atributo Ingredient personalizado é preservado. Com CodableAttributedStringKeys, isso pode até funcionar entre TextEditors em apps diferentes, desde que os apps listem o atributo em AttributedTextFormattingDefinition.
Está muito bom, mas ainda há pontos para melhorar: Com o cursor no final do ingrediente “leite”, posso apagar caracteres ou continuar digitando e ele se comportará como texto normal. Isso dá a ideia de que é apenas um texto em verde, e não um ingrediente com um determinado nome. Para corrigir isso, não quero que o atributo Ingredient se expanda no final da execução. Quero que a cor em primeiro plano seja redefinida na palavra inteira de uma vez se eu modificá-la.
Jeremy, prometo que te darei um croissant extra se me ajudar a implementar isso. Não sei se um será suficiente. Posso fazer isso por alguns extras. Enquanto você vai buscar os croissants no forno, vou explicar quais APIs podem ajudar. Com as restrições de definição de formatação que o Max demonstrou, você pode restringir os atributos e os valores específicos que cada editor de texto pode exibir. Para ajudar nesse novo problema no editor, o protocolo AttributedStringKey fornece APIs adicionais para restringir como os valores de atributo são modificados em qualquer AttributedString.
Quando os atributos declaram restrições, AttributedString mantém os atributos consistentes entre si e com o conteúdo do texto para evitar um estado inesperado com um código mais simples e de melhor desempenho. Vamos ver alguns exemplos para explicar quando você pode usar essas APIs para os atributos. Falarei dos atributos cujos valores estão associados a outro conteúdo no AttributedString, como um atributo de verificação ortográfica.
O atributo de verificação ortográfica indica que a palavra “pronto” está incorreta com um sublinhado vermelho. Depois de verificar a ortografia do texto, preciso garantir que o atributo de verificação ortográfica permaneça aplicado apenas ao texto que já validei. Mas, se eu continuar digitando no editor de texto, todos os atributos do texto existente serão herdados pelo texto inserido. Não quero isso em um atributo de verificação ortográfica. Vou adicionar uma nova propriedade a AttributedStringKey para corrigir isso. Ao declarar uma propriedade inheritedByAddedText no tipo AttributedStringKey com um valor false, o texto adicionado não herdará esse valor de atributo.
Agora, ao adicionar texto à string, o novo texto não conterá o atributo de verificação ortográfica, pois ainda não verifiquei a ortografia dessas palavras. Infelizmente, encontrei outro problema nesse atributo. Quando adiciono texto no meio de uma palavra que foi marcada como incorreta, o atributo mostra uma quebra na linha vermelha abaixo do texto adicionado. Como o app ainda não verificou se essa palavra está incorreta, quero que o atributo seja removido da palavra para evitar informações obsoletas na interface.
Para corrigir o problema, adicionarei outra propriedade ao tipo AttributedStringKey: a propriedade invalidationConditions. Essa propriedade declara situações em que uma execução desse atributo deve ser removida do texto. O AttributedString fornece condições para quando o texto e certos atributos mudam, e as chaves de atributo podem invalidar itens de acordo com qualquer número de condições. Preciso remover este atributo sempre que o texto da execução do atributo for alterado, então usarei o valor textChanged.
Inserir texto no meio de uma execução de atributo invalidará o atributo em toda a execução, garantindo que eu evite esse estado inconsistente na interface. Acho que as duas APIs podem ajudar a manter o atributo Ingredient válido no app do Max. Enquanto Max termina de assar os croissants, vou mostra mais uma categoria de atributos: Atributos que exigem valores consistentes entre seções de texto. Por exemplo, um atributo de alinhamento de parágrafo.
Posso aplicar alinhamentos diferentes a cada parágrafo do texto, mas uma única palavra não pode usar um alinhamento diferente do resto do parágrafo. Para impor esse requisito durante as modificações de AttributedString, declararei a propriedade runBoundaries no tipo AttributedStringKey. O Foundation permite limitar a execução às bordas do parágrafo ou às bordas de um caractere especificado. Assim, definirei esse atributo como restrito aos limites de parágrafo para exigir que ele tenha um valor consistente do início ao fim de um parágrafo.
Agora, ficou difícil. Se eu aplicar um valor de alinhamento à esquerda a apenas a uma palavra, o AttributedString expandirá automaticamente o atributo a todo o intervalo do parágrafo. Além disso, quando enumero o atributo de alinhamento, o AttributedString enumera cada parágrafo individual, mesmo que dois parágrafos consecutivos contenham o mesmo valor de atributo. Outros limites se comportam assim: AttributedString expande valores de um limite para o próximo e garante que as execuções enumeradas sejam quebradas em todos os limites.
Max, os croissants cheiram bem! Se os croissants já estiverem no forno, você acha que algumas das APIs podem complementar a definição de formatação para obter o comportamento desejado do atributo personalizado? Parece que é o ingrediente secreto que eu precisava! Os croissants estão no forno, então posso testar isso agora.
No IngredientAttribute personalizado, implementarei o requisito opcional inheritedByAddedText para ter o valor false. Dessa forma, se eu digitar após um Ingredient, ele não será expandido.
Vou implementar invalidationConditions com textChanged. Então, quando eu apagar caracteres em um Ingredient, ele não será mais reconhecido.
Vamos testar! Ao adicionar uma letra no final de “leite”, ela não fica verde, e ao apagar um caractere de “leite”, o atributo Ingredient é removido de toda a palavra de uma vez. Com base no AttributedTextFormattingDefinition, o atributo de cor em primeiro plano continua seguindo o comportamento do atributo Ingredient personalizado.
Obrigado Jeremy, o app realmente ficou ótimo! De nada! E os croissants? Não se preocupe, estão quase prontos. Por que você não confere o forno? Estou um pouco preocupado que Luca possa roubá-los de nós. Já ouvi falar do Luca. Ele adora widgets e croissants. Pode deixar, chef! Antes de me juntar ao Jeremy, vou dar algumas dicas finais: Baixe o app como um projeto de exemplo para saber mais sobre como usar o Transferable Wrapper da SwiftUI para arrastar e soltar sem perdas ou exportar para RTFD e manter o AttributedString com dados do Swift. O AttributedString faz parte do projeto Foundation de código aberto do Swift. Encontre a implementação no GitHub para contribuir com o desenvolvimento ou participe da comunidade nos fóruns do Swift. Com o novo TextEditor, está mais fácil permitir o uso de Genmoji no app. Então, considere fazer isso. Estou animado para ver como você usará a API para melhorar a edição de texto em seus apps. Algumas mudanças já fazem toda a diferença.
Que delícia! Não, que RICO!
-
-
1:15 - TextEditor and String
import SwiftUI struct RecipeEditor: View { @Binding var text: String var body: some View { TextEditor(text: $text) } }
-
1:45 - TextEditor and AttributedString
import SwiftUI struct RecipeEditor: View { @Binding var text: AttributedString var body: some View { TextEditor(text: $text) } }
-
4:43 - AttributedString Basics
var text = AttributedString( "Hello 👋🏻! Who's ready to get " ) var cooking = AttributedString("cooking") cooking.foregroundColor = .orange text += cooking text += AttributedString("?") text.font = .largeTitle
-
5:36 - Build custom controls: Basics (initial attempt)
import SwiftUI struct RecipeEditor: View { @Binding var text: AttributedString @State private var selection = AttributedTextSelection() var body: some View { TextEditor(text: $text, selection: $selection) .preference(key: NewIngredientPreferenceKey.self, value: newIngredientSuggestion) } private var newIngredientSuggestion: IngredientSuggestion { let name = text[selection.indices(in: text)] // build error return IngredientSuggestion( suggestedName: AttributedString()) } }
-
8:53 - Slicing AttributedString with a Range
var text = AttributedString( "Hello 👋🏻! Who's ready to get cooking?" ) guard let cookingRange = text.range(of: "cooking") else { fatalError("Unable to find range of cooking") } text[cookingRange].foregroundColor = .orange
-
10:50 - Slicing AttributedString with a RangeSet
var text = AttributedString( "Hello 👋🏻! Who's ready to get cooking?" ) let uppercaseRanges = text.characters .indices(where: \.isUppercase) text[uppercaseRanges].foregroundColor = .blue
-
11:40 - Build custom controls: Basics (fixed)
import SwiftUI struct RecipeEditor: View { @Binding var text: AttributedString @State private var selection = AttributedTextSelection() var body: some View { TextEditor(text: $text, selection: $selection) .preference(key: NewIngredientPreferenceKey.self, value: newIngredientSuggestion) } private var newIngredientSuggestion: IngredientSuggestion { let name = text[selection] return IngredientSuggestion( suggestedName: AttributedString(name)) } }
-
12:32 - Build custom controls: Recipe attribute
import SwiftUI struct IngredientAttribute: CodableAttributedStringKey { typealias Value = Ingredient.ID static let name = "SampleRecipeEditor.IngredientAttribute" } extension AttributeScopes { /// An attribute scope for custom attributes defined by this app. struct CustomAttributes: AttributeScope { /// An attribute for marking text as a reference to an recipe's ingredient. let ingredient: IngredientAttribute } } extension AttributeDynamicLookup { /// The subscript for pulling custom attributes into the dynamic attribute lookup. /// /// This makes them available throughout the code using the name they have in the /// `AttributeScopes.CustomAttributes` scope. subscript<T: AttributedStringKey>( dynamicMember keyPath: KeyPath<AttributeScopes.CustomAttributes, T> ) -> T { self[T.self] } }
-
12:56 - Build custom controls: Modifying text (initial attempt)
import SwiftUI struct RecipeEditor: View { @Binding var text: AttributedString @State private var selection = AttributedTextSelection() var body: some View { TextEditor(text: $text, selection: $selection) .preference(key: NewIngredientPreferenceKey.self, value: newIngredientSuggestion) } private var newIngredientSuggestion: IngredientSuggestion { let name = text[selection] return IngredientSuggestion( suggestedName: AttributedString(name), onApply: { ingredientId in let ranges = text.characters.ranges(of: name.characters) for range in ranges { // modifying `text` without updating `selection` is invalid and resets the cursor text[range].ingredient = ingredientId } }) } }
-
17:40 - AttributedString Character View
text.characters[index] // "👋🏻"
-
17:44 - AttributedString Unicode Scalar View
text.unicodeScalars[index] // "👋"
-
17:49 - AttributedString Runs View
text.runs[index] // "Hello 👋🏻! ..."
-
18:13 - AttributedString UTF-8 View
text.utf8[index] // "240"
-
18:17 - AttributedString UTF-16 View
text.utf16[index] // "55357"
-
18:59 - Updating Indices during AttributedString Mutations
var text = AttributedString( "Hello 👋🏻! Who's ready to get cooking?" ) guard var cookingRange = text.range(of: "cooking") else { fatalError("Unable to find range of cooking") } let originalRange = cookingRange text.transform(updating: &cookingRange) { text in text[originalRange].foregroundColor = .orange let insertionPoint = text .index(text.startIndex, offsetByCharacters: 6) text.characters .insert(contentsOf: "chef ", at: insertionPoint) } print(text[cookingRange])
-
20:22 - Build custom controls: Modifying text (fixed)
import SwiftUI struct RecipeEditor: View { @Binding var text: AttributedString @State private var selection = AttributedTextSelection() var body: some View { TextEditor(text: $text, selection: $selection) .preference(key: NewIngredientPreferenceKey.self, value: newIngredientSuggestion) } private var newIngredientSuggestion: IngredientSuggestion { let name = text[selection] return IngredientSuggestion( suggestedName: AttributedString(name), onApply: { ingredientId in let ranges = RangeSet(text.characters.ranges(of: name.characters)) text.transform(updating: &selection) { text in text[ranges].ingredient = ingredientId } }) } }
-
22:03 - Define your text format: RecipeFormattingDefinition Scope
struct RecipeFormattingDefinition: AttributedTextFormattingDefinition { struct Scope: AttributeScope { let foregroundColor: AttributeScopes.SwiftUIAttributes.ForegroundColorAttribute let adaptiveImageGlyph: AttributeScopes.SwiftUIAttributes.AdaptiveImageGlyphAttribute let ingredient: IngredientAttribute } var body: some AttributedTextFormattingDefinition<Scope> { } } // pass the custom formatting definition to the TextEditor in the updated `RecipeEditor.body`: TextEditor(text: $text, selection: $selection) .preference(key: NewIngredientPreferenceKey.self, value: newIngredientSuggestion) .attributedTextFormattingDefinition(RecipeFormattingDefinition())
-
23:50 - Define your text format: AttributedTextValueConstraints
struct IngredientsAreGreen: AttributedTextValueConstraint { typealias Scope = RecipeFormattingDefinition.Scope typealias AttributeKey = AttributeScopes.SwiftUIAttributes.ForegroundColorAttribute func constrain(_ container: inout Attributes) { if container.ingredient != nil { container.foregroundColor = .green } else { container.foregroundColor = nil } } } // list the value constraint in the recipe formatting definition's body: var body: some AttributedTextFormattingDefinition<Scope> { IngredientsAreGreen() }
-
29:28 - AttributedStringKey Constraint: Inherited by Added Text
static let inheritedByAddedText = false
-
30:12 - AttributedStringKey Constraint: Invalidation Conditions
static let invalidationConditions: Set<AttributedString.AttributeInvalidationCondition>? = [.textChanged]
-
31:25 - AttributedStringKey Constraint: Run Boundaries
static let runBoundaries: AttributedString.AttributeRunBoundaries? = .paragraph
-
32:46 - Define your text format: AttributedStringKey Constraints
struct IngredientAttribute: CodableAttributedStringKey { typealias Value = Ingredient.ID static let name = "SampleRecipeEditor.IngredientAttribute" static let inheritedByAddedText: Bool = false static let invalidationConditions: Set<AttributedString.AttributeInvalidationCondition>? = [.textChanged] }
-
-
- 0:00 - Introdução
Esta seção apresenta o objetivo da sessão: demonstrar como criar experiências de edição de texto em SwiftUI usando AttributedString. O apresentador, Max, abordará três áreas principais: atualizar o 'TextEditor' para aceitar texto formatado, criar controles para aprimorar o editor e desenvolver uma definição de formatação de texto para garantir um estilo consistente.
- 1:15 - TextEditor e AttributedString
Esta seção concentra-se na atualização do 'TextEditor' do SwiftUI para aceitar texto formatado, alterando o tipo de dado de String para AttributedString. Isso ativa suporte imediato para controles de formatação no sistema, incluindo negrito, itálico, tamanho, cores e Genmoji. O 'TextEditor' aceita uma ampla gama de atributos, incluindo a formatação de parágrafos. A seção oferece uma rápida revisão dos conceitos básicos de 'AttributedString', incluindo sua estrutura (caracteres e blocos de atributos), a natureza como tipo por valor, a codificação UTF-8 e a conformidade com protocolos do Swift. Ela destaca o uso de atributos predefinidos e a possibilidade de criar atributos personalizados.
- 5:36 - Criar controles personalizados
Esta seção explica como criar controles para um 'TextEditor' que interajam com o restante do app. Ela mostra como adicionar um botão que permite marcar o texto selecionado como um ingrediente. Ela aborda o uso de chaves de preferência para se comunicar entre o editor e outras partes da interface. A seção explora as complexidades das seleções de texto em 'AttributedString', explicando por que o tipo 'AttributedTextSelection' usa um 'RangeSet' em vez de um único 'Range' para lidar com texto bidirecional e seleções descontínuas. Ele destaca o uso de uma API subscript que permite fatiar um AttributedString com uma seleção. A seção mostra como usar um fechamento para modificar o texto do editor, marcando as ocorrências do texto selecionado com um atributo 'Ingredient'. Também aborda o problema da seleção ser redefinida após a mutação do AttributedString e apresenta o conceito de índices do AttributedString, destacando a importância de atualizá-los após as mutações usando a função de transformação.
- 22:02 - Definir o formato do texto
Esta seção concentra-se no uso do protocolo de definição de formatação de texto atribuído para controlar as opções de formatação no 'TextEditor'. Explica como criar uma definição de formatação personalizada que determina quais AttributedStringKeys o editor deve reconhecer, restringindo as opções de formatação disponíveis. E apresenta a 'AttributedTextValueConstraint' para impor regras de formatação, como garantir que os ingredientes sejam sempre destacados em verde. A seção explica ainda como restringir os valores de atributos usando o protocolo 'AttributedStringKey'. Ele trata de propriedades como `inheritedByAddedText` e `invalidationConditions` que controlam a herança e a invalidação de atributos durante alterações no texto. Por fim, a seção discute a propriedade 'runBoundaries' para garantir valores consistentes em seções de texto, como parágrafos.
- 34:08 - Próximas etapas
Esta seção oferece dicas finais e recursos. E menciona um projeto que demonstra arrastar e soltar sem perdas, exportação RTFD e persistência de 'AttributedString' com Swift Data. Destaca que a 'AttributedString' faz parte do projeto Foundation de código aberto do Swift e incentiva contribuições. Também incentiva os desenvolvedores a adicionar suporte a Genmoji em seus apps usando o novo 'TextEditor'.