Search Docs
Loading...
Skip to content

Form-Based Editing

Expose a template’s variables and placeholders through your own input controls so users customize designs by filling fields instead of manipulating the canvas — ideal for non-designers and consistent, on-brand output.

A personalized template rendered by form-based editing — the heading and tagline show variable values substituted into the text, above a swapped placeholder image.

10 mins
estimated time
GitHub

Form-based editing turns template adoption into structured data entry. Instead of asking users to locate and edit elements on the canvas, you read a template’s customization points with the headless Creative Engine and build your own form — text fields for variables, image pickers for placeholders — that writes values back through the engine API.

This guide walks through discovering a template’s variables and placeholders, reading and updating their values, replacing placeholder images, and validating input before export.

Understanding Form-Based Editing#

Form-based editing replaces direct canvas manipulation with input controls you design yourself. Two kinds of customization points drive a form:

  • Variables hold text values referenced from text blocks. Each variable maps to a text field.
  • Placeholders are image blocks marked as editable. Each maps to an image picker.

You discover both programmatically, render matching controls in a SwiftUI form, and write user input back through the engine. The engine updates the design immediately, so a live preview reflects every change. Because the form only exposes the fields you choose, the rest of the design stays locked and on-brand.

Discovering Template Metadata#

Inspect a loaded template to learn what it lets users customize. engine.variable.findAll() returns every defined variable name, and filtering graphic blocks by isPlaceholderEnabled(_:) finds the editable images.

// List every variable the template defines — render one form field per entry.
let variableNames = engine.variable.findAll()
// Find image placeholders: graphic blocks flagged as placeholders.
let graphicBlocks = try engine.block.find(byType: .graphic)
let placeholders = try graphicBlocks.filter { try engine.block.isPlaceholderEnabled($0) }
print("Variables:", variableNames, "Placeholders:", placeholders.count)

Use one form field per variable name and one image picker per placeholder. engine.block.findAllPlaceholders() is a convenience that returns the placeholder blocks directly if you do not need the intermediate graphic-block list.

Working with Variables#

Variables store text values that text blocks reference with a {{variableName}} token. The engine substitutes the value at render time.

Using Variables in Text#

Reference a variable by wrapping its name in double curly braces. referencesAnyVariables(_:) confirms whether a block depends on variables before you expose it in a form.

// Reference a variable in text by wrapping its name in double curly braces.
// The engine substitutes `{{tagline}}` with the variable's value at render time.
try engine.block.replaceText(subtitle, text: "{{tagline}}")
// `referencesAnyVariables(_:)` confirms a block depends on variable tokens.
let subtitleUsesVariables = try engine.block.referencesAnyVariables(subtitle)
print("Subtitle references variables:", subtitleUsesVariables)

Defining Variables#

Give each variable an initial value so the form shows something on first load. A template authored on the web ships with these defaults already set; when you build a template in code, define them with engine.variable.set(key:value:).

// Give each variable an initial value. A web-authored template ships with
// these defaults already set; here we define them so the form has something
// to show on first load.
try engine.variable.set(key: "tag", value: "Welcome")
try engine.variable.set(key: "tagline", value: "Your personalized design")

Updating Variables#

Read a variable with engine.variable.get(key:) to seed a field, then write the user’s edit back with engine.variable.set(key:value:). Call the setter from a TextField’s onChange(of:) handler so the design updates as the user types.

// Read a variable to seed a form field, then write the user's edit back.
// In SwiftUI, call the setter from a TextField's `onChange(of:)` handler.
let currentTag = try engine.variable.get(key: "tag")
print("Seeding field with:", currentTag)
try engine.variable.set(key: "tag", value: "Hello")

Replacing Placeholder Content#

Placeholders are graphic blocks marked editable, letting users replace images while the rest of the design stays fixed.

Reading the Current Image#

Read a placeholder’s fill to preview its current image in the form. engine.block.getURL(_:property:) returns the image’s file URL.

// Read a placeholder's current image so the form can preview it.
guard let placeholder = placeholders.first else { return }
let fill = try engine.block.getFill(placeholder)
let currentImageURL = try engine.block.getURL(fill, property: "fill/image/imageFileURI")
print("Current placeholder image:", currentImageURL.lastPathComponent)

Setting a New Image#

Swap the image by pointing the same fill at a new file URL with engine.block.setURL(_:property:value:). The URL can come from a photo picker, a bundled asset, or a downloaded file.

// Swap the placeholder's content when the user picks a new image. Point the
// fill at any local file URL — a photo from the picker, a bundled asset, or
// a downloaded file.
try engine.block.setURL(
fill,
property: "fill/image/imageFileURI",
value: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"),
)

Driving Updates from Your Own Form#

Keep your form state in a dictionary keyed by variable name, then write each entry back through the engine in one pass — for example when the user taps “Apply”. The same engine API works regardless of the UI framework you build the form with.

// Apply an entire form's current values in one pass — for example when the
// user taps "Apply". Keep your form state in a dictionary keyed by variable
// name and write each entry back through the engine.
let formValues: [String: String] = [
"tag": "Welcome Back",
"tagline": "Built from form input",
]
for (key, value) in formValues where variableNames.contains(key) {
try engine.variable.set(key: key, value: value)
}

Validating Before Export#

Before exporting, confirm every variable the form exposes has a value. Read each one with engine.variable.get(key:) and block the export while any are empty, then render the finished design with engine.block.export(_:mimeType:).

// Before exporting, confirm every variable the form exposes has a value.
let missingFields = try variableNames.filter { try engine.variable.get(key: $0).isEmpty }
guard missingFields.isEmpty else {
print("Cannot export — required fields are empty:", missingFields)
return
}
let exported = try await engine.block.export(page, mimeType: .png)
print("Exported personalized template:", exported.count, "bytes")

Error Handling#

Engine calls throw, so handle failures where they can occur:

  • Missing values: Validate before export and tell users which fields are required.
  • Invalid images: Check a file’s type before assigning it to a placeholder fill.
  • Unreachable files: Handle failures when loading images from a URL.
  • Unknown variables: engine.variable.get(key:) throws if the key was never defined — guard against keys your form does not recognize.

API Reference#

Method Description
engine.variable.findAll() List every variable name defined in the template
engine.variable.get(key:) Read a variable’s current value
engine.variable.set(key:value:) Set or update a variable’s value
engine.block.referencesAnyVariables(_:) Check whether a block depends on variables
engine.block.find(byType:) Find blocks by type, such as .graphic
engine.block.isPlaceholderEnabled(_:) Check whether a block is an editable placeholder
engine.block.findAllPlaceholders() Return every placeholder block in the scene
engine.block.getFill(_:) Get the fill block of a design block
engine.block.getURL(_:property:) Read a URL property, such as an image fill’s file URI
engine.block.setURL(_:property:value:) Set a URL property to replace placeholder content
engine.block.export(_:mimeType:) Export the finished design as image data

Next Steps#