Search Docs
Loading...
Skip to content

Edit Text

Edit text content programmatically with range-based APIs for replacing, formatting, and querying text.

A text block rendered with mixed formatting: “CE.SDK” in bold and “GUIDE” in blue italic uppercase.

10 mins
estimated time
GitHub

CE.SDK provides text editing through range-based APIs that operate on Swift Range<String.Index> values. This guide covers replacing and removing text content, applying formatting to character ranges, querying text properties, and retrieving line and font information.

Creating a Text Block#

Create a text block and add it to the page. Set both width and height modes to .auto so the block resizes around its content.

// Create a text block and position it on the page.
let text = try engine.block.create(.text)
try engine.block.appendChild(to: page, child: text)
try engine.block.setPositionX(text, value: 100)
try engine.block.setPositionY(text, value: 150)
try engine.block.setWidthMode(text, mode: .auto)
try engine.block.setHeightMode(text, mode: .auto)

Setting a Typeface#

Define a typeface with the font variants the example needs and apply it with setFont(_:fontFileURL:typeface:). Including a bold variant in the typeface is what later lets setTextFontWeight(_:fontWeight:in:) and toggleBoldFont(_:in:) switch a range to bold.

// Define a Roboto typeface with regular, bold, italic, and bold-italic variants.
// Each variant the example formats with later (bold / italic / etc.) needs a matching font in the typeface.
let robotoBase = baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto")
let typeface = Typeface(
name: "Roboto",
fonts: [
Font(
uri: robotoBase.appendingPathComponent("Roboto-Regular.ttf"),
subFamily: "Regular",
weight: .normal,
style: .normal,
),
Font(
uri: robotoBase.appendingPathComponent("Roboto-Bold.ttf"),
subFamily: "Bold",
weight: .bold,
style: .normal,
),
Font(
uri: robotoBase.appendingPathComponent("Roboto-Italic.ttf"),
subFamily: "Italic",
weight: .normal,
style: .italic,
),
Font(
uri: robotoBase.appendingPathComponent("Roboto-BoldItalic.ttf"),
subFamily: "Bold Italic",
weight: .bold,
style: .italic,
),
],
)
try engine.block.setFont(text, fontFileURL: typeface.fonts[0].uri, typeface: typeface)
try engine.block.setTextFontSize(text, fontSize: 80)

Replacing and Removing Text#

Use replaceText(_:text:in:) to swap or insert text. Passing a zero-length subrange inserts at that position; passing a non-zero range replaces it. Omitting the subrange replaces the entire content.

// Replace the entire text content.
try engine.block.replaceText(text, text: "Hello World!")
// Replace "World" with "CE.SDK".
try engine.block.replaceText(text, text: "CE.SDK", in: "Hello World!".range(of: "World")!)
// Insert " Guide" before the exclamation mark.
let insertion = "Hello CE.SDK!".range(of: "!")!.lowerBound
try engine.block.replaceText(text, text: " Guide", in: insertion ..< insertion)

Use removeText(_:from:) to delete a character range. Omitting the subrange clears the block.

// Remove "Hello " to leave "CE.SDK Guide!".
try engine.block.removeText(text, from: "Hello CE.SDK Guide!".range(of: "Hello ")!)

The example builds the Range<String.Index> arguments from the expected text content ("Hello World!""Hello CE.SDK!""Hello CE.SDK Guide!"). In an interactive editor, pass the user’s current selection instead.

Applying Text Formatting#

Apply formatting to specific character ranges with the setText* setters. Each accepts an optional in: subrange — omit it to format the entire block.

// Apply bold weight to "CE.SDK".
try engine.block.setTextFontWeight(
text,
fontWeight: .bold,
in: "CE.SDK Guide!".range(of: "CE.SDK")!,
)
// Apply a blue color to "Guide".
try engine.block.setTextColor(
text,
color: .rgba(r: 0.2, g: 0.6, b: 1.0, a: 1.0),
in: "CE.SDK Guide!".range(of: "Guide")!,
)
// Apply italic style to "Guide".
try engine.block.setTextFontStyle(
text,
fontStyle: .italic,
in: "CE.SDK Guide!".range(of: "Guide")!,
)
// Uppercase the "Guide" range.
try engine.block.setTextCase(
text,
textCase: .uppercase,
in: "CE.SDK Guide!".range(of: "Guide")!,
)
  • setTextFontWeight(_:fontWeight:in:) applies a FontWeight such as .bold or .normal.
  • setTextColor(_:color:in:) takes a Color; .rgba(r:g:b:a:) constructs an sRGB value.
  • setTextFontStyle(_:fontStyle:in:) switches between .normal and .italic.
  • setTextFontSize(_:fontSize:in:) updates the font size in the scene’s font-size unit; when applied to the whole block, the block’s font size is updated.
  • setTextCase(_:textCase:in:) applies .titlecase, .uppercase, .lowercase, or .normal.

toggleBoldFont(_:in:) and toggleItalicFont(_:in:) flip the weight or style of a range. Gate each call on the matching canToggle*Font(_:in:) check, which returns false when the active typeface lacks a corresponding variant.

if try engine.block.canToggleBoldFont(text, in: "CE.SDK Guide!".range(of: "Guide")!) {
try engine.block.toggleBoldFont(text, in: "CE.SDK Guide!".range(of: "Guide")!)
}
if try engine.block.canToggleItalicFont(text, in: "CE.SDK Guide!".range(of: "CE.SDK")!) {
try engine.block.toggleItalicFont(text, in: "CE.SDK Guide!".range(of: "CE.SDK")!)
}

Use setTypeface(_:typeface:in:) to apply a typeface to the block or to a specific range. When the new typeface differs from the current one, existing weights and styles are preserved where possible, falling back to the closest available variant. setFont(_:fontFileURL:typeface:), in contrast, resets the block’s formatting.

// Apply a different typeface to a range, preserving formatting where possible.
try engine.block.setTypeface(text, typeface: typeface, in: "CE.SDK Guide!".range(of: "Guide")!)
// Read back the block's base typeface and the unique typefaces in the block.
let baseTypeface = try engine.block.getTypeface(text)
let typefacesInBlock = try engine.block.getTypefaces(text)
print("Base typeface: \(baseTypeface.name), unique typefaces in block: \(typefacesInBlock.count)")

Querying Text Properties#

Each getText* getter returns an array of the unique values found in the queried range — text blocks can contain mixed formatting, so the array length tells you whether the range is uniform.

let colors = try engine.block.getTextColors(text)
let weights = try engine.block.getTextFontWeights(text)
let styles = try engine.block.getTextFontStyles(text)
let sizes = try engine.block.getTextFontSizes(text)
let cases = try engine.block.getTextCases(text)
print("Colors: \(colors)")
print("Weights: \(weights), styles: \(styles)")
print("Sizes: \(sizes), cases: \(cases)")

getTypeface(_:) returns the block’s base typeface, and getTypefaces(_:in:) returns the unique typefaces used by the runs in a range. Omit the subrange on any getter to query the entire block.

Line Information#

After the block has rendered, query its visible lines. getTextVisibleLineCount(_:) returns the number of rendered lines, getTextVisibleLineContent(_:lineIndex:) returns the line’s text, and getTextLineBoundingBoxRect(_:index:) returns the line’s bounding box in the scene’s global coordinate space.

let lineCount = try engine.block.getTextVisibleLineCount(text)
for index in 0 ..< lineCount {
let content = try engine.block.getTextVisibleLineContent(text, lineIndex: index)
let bounds = try engine.block.getTextLineBoundingBoxRect(text, index: index)
print("Line \(index): \"\(content)\" at \(bounds)")
}

Font Metrics#

engine.editor.getFontMetrics(fontFileURI:) returns the font’s raw design-unit metrics. The font is fetched asynchronously if it has not been loaded yet.

let metrics = try await engine.editor.getFontMetrics(fontFileURI: typeface.fonts[0].uri.absoluteString)
print(
"Ascender: \(metrics.ascender), descender: \(metrics.descender), unitsPerEm: \(metrics.unitsPerEm)",
)
print(
"Cap height: \(metrics.capHeight), x-height: \(metrics.xHeight), line gap: \(metrics.lineGap)",
)

The returned FontMetrics value exposes ascender, descender, unitsPerEm, lineGap, capHeight, xHeight, underlineOffset, underlineSize, strikeoutOffset, and strikeoutSize. Use these to compute line heights, align mixed-typeface runs, or position decorations.

Troubleshooting#

Range indices unexpected: Build ranges from the current text content. After a replaceText or removeText call, the indices into the old string are no longer valid.

Formatting not applied: Confirm the active typeface ships a variant for the requested weight or style. canToggleBoldFont(_:in:) and canToggleItalicFont(_:in:) return false when no matching variant is available.

Typeface change loses formatting: setFont(_:fontFileURL:typeface:) resets the block’s formatting. Use setTypeface(_:typeface:in:) to preserve weights and styles where possible.

Line count is zero: The block has to render before line information is available. Empty text blocks report zero lines.

API Reference#

Methods#

Method Description
engine.block.setFont(_:fontFileURL:typeface:) Set font and typeface, resetting formatting
engine.block.setTypeface(_:typeface:in:) Change typeface, preserving formatting where possible
engine.block.getTypeface(_:) Get the block’s base typeface
engine.block.getTypefaces(_:in:) Get unique typefaces in a range
engine.block.replaceText(_:text:in:) Replace or insert text at a range
engine.block.removeText(_:from:) Remove text in a range
engine.block.setTextColor(_:color:in:) Set color for a range
engine.block.getTextColors(_:in:) Get unique colors in a range
engine.block.setTextFontWeight(_:fontWeight:in:) Set font weight for a range
engine.block.getTextFontWeights(_:in:) Get unique font weights in a range
engine.block.setTextFontStyle(_:fontStyle:in:) Set font style for a range
engine.block.getTextFontStyles(_:in:) Get unique font styles in a range
engine.block.setTextFontSize(_:fontSize:in:) Set font size for a range
engine.block.getTextFontSizes(_:in:) Get unique font sizes in a range
engine.block.setTextCase(_:textCase:in:) Set text case for a range
engine.block.getTextCases(_:in:) Get unique text cases in a range
engine.block.canToggleBoldFont(_:in:) Whether bold can be toggled for a range
engine.block.canToggleItalicFont(_:in:) Whether italic can be toggled for a range
engine.block.toggleBoldFont(_:in:) Toggle bold for a range
engine.block.toggleItalicFont(_:in:) Toggle italic for a range
engine.block.getTextCursorRange() Get the user’s grapheme selection range; returns nil when no text block is being edited
engine.block.setTextCursorRange(_:) Set the cursor or selection; throws when no text block is being edited
engine.block.getTextVisibleLineCount(_:) Number of rendered lines
engine.block.getTextVisibleLineContent(_:lineIndex:) Text content of a rendered line
engine.block.getTextLineBoundingBoxRect(_:index:) Bounding box of a rendered line
engine.editor.getFontMetrics(fontFileURI:) Raw font metrics for a font file URI

Next Steps#

  • Add Text — Create and configure text blocks
  • Text Styling — Apply fonts, colors, and formatting
  • Text Designs — Create and customize text component libraries using predefined text designs that appear in your asset library
  • Auto-Size — Configure automatic text sizing