-
Codificação guiada: 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...
-
-
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'.