-
Codificación conjunta: Crea una experiencia de texto enriquecida en SwiftUI con AttributedString
Aprende a crear una experiencia de texto enriquecida con la API TextEditor y AttributedString de SwiftUI. Descubre cómo habilitar la edición de texto enriquecido, crear controles personalizados que manipulen el contenido de tu editor y personalizar las opciones de formato disponibles. Explora las capacidades avanzadas de AttributedString que te ayudarán a crear las mejores experiencias de edición de texto.
Capítulos
- 0:00 - Introducción
- 1:15 - TextEditor y AttributedString
- 5:36 - Crea controles personalizados
- 22:02 - Define el formato de tu texto
- 34:08 - Próximos pasos
Recursos
Videos relacionados
WWDC25
WWDC22
WWDC21
-
Buscar este video…
-
-
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 - Introducción
Esta sección presenta la meta de la sesión: demostrar cómo crear experiencias de edición de texto enriquecidas en SwiftUI usando AttributedString. El presentador, Max, cubrirá tres áreas principales: actualizar TextEditor a fin de admitir texto enriquecido, crear controles personalizados para mejorar el editor y crear una definición de formato de texto personalizado para garantizar un estilo consistente.
- 1:15 - TextEditor y AttributedString
Esta sección se centra en la actualización de un TextEditor de SwiftUI para admitir texto enriquecido cambiando el tipo de datos subyacente de String a AttributedString. Esto permite inmediatamente el soporte para los controles de la IU del sistema para opciones de formato, como negrita, cursiva, tamaño de fuente, colores y Genmoji. TextEditor admite una amplia gama de atributos, incluido el estilo de párrafo. La sección también proporciona un repaso rápido de los conceptos básicos de AttributedString, incluida su estructura (caracteres y ejecuciones de atributos), su naturaleza de tipo de valor, su codificación UTF-8 y su conformidad con los protocolos Swift comunes. Destaca el uso de atributos predefinidos y la posibilidad de definir atributos personalizados.
- 5:36 - Crea controles personalizados
Esta sección explica cómo crear controles personalizados para un TextEditor que interactúen con el resto de la app. Se muestra cómo agregar un botón que permite al usuario marcar el texto seleccionado como ingrediente. Cubre el uso de teclas de preferencia para comunicarse entre el editor y otras partes de la IU. También se profundiza en las complejidades de las selecciones de texto en AttributedString y se explica por qué el tipo AttributedTextSelection usa un RangeSet en lugar de un solo Range para manejar texto bidireccional y selecciones no contiguas. También se destaca el uso de una API de subíndice que porciona un AttributedString con una selección directamente. Finalmente, en la sección se demuestra cómo utilizar un cierre para mutar el texto del editor marcando las ocurrencias del texto seleccionado con un atributo Ingredient personalizado. También se analiza el problema de la selección que se restablece después de mutar la cadena de atributos e introduce el concepto de índices AttributedString y la importancia de actualizarlos después de las mutaciones usando la función de transformación.
- 22:02 - Define el formato de tu texto
Esta sección se centra en el uso del protocolo de definición de formato de texto atribuido para controlar las opciones de formato disponibles en el TextEditor. Se explica cómo crear una definición de formato personalizada que especifica a qué AttributedStringKeys debe responder el editor y se limita a las opciones de formato disponibles. También se presenta AttributedTextValueConstraint para aplicar reglas de formato específicas, como garantizar que los ingredientes siempre estén resaltados en verde. En la sección se explica además cómo restringir los valores de los atributos usando el protocolo AttributedStringKey. Cubre propiedades, como inheritedByAddedText e invalidationConditions, para controlar cómo se heredan e invalidan los atributos durante las mutaciones de texto. Por último, se analiza la propiedad runBoundaries para imponer valores consistentes en todas las secciones de texto, como los párrafos.
- 34:08 - Próximos pasos
En esta sección se proporcionan consejos y recursos finales. Se menciona un proyecto de muestra que enseña las funcionalidades de arrastrar y soltar sin pérdida, exportación RTFD y persistencia de AttributedString con Swift Data. Se destaca que AttributedString es parte del proyecto de código abierto de la fundación Swift y que fomenta las contribuciones. En la sección también se alienta a los desarrolladores a agregar soporte para Genmoji a sus apps con el nuevo TextEditor.