Drive block colors, sizes, images and typefaces from typed variables, so one change updates every bound property.
Text variables fill {{key}} tokens in text, as the Text Variables guide shows. Color, number, image and typeface variables hold one typed value each. When you bind a block property to such a variable, the property takes the variable’s value and follows every change of it. One brand color can then drive every fill, border and text that uses it.
This guide covers the typed variables, binding and unbinding properties, the lock on bound properties, renaming variables, and the errors to expect.
Creating Typed Variables#
Each type has its own setter. set(key:color:), set(key:number:), set(key:imageURL:) and set(key:typeface:) create the variable when the key is new, and update it otherwise. set(key:value:) stays the setter for text variables.
// One variable per type. A setter creates the variable when the key is new.let roboto = Typeface( name: "Roboto", fonts: [ Font( uri: baseURL.appendingPathComponent("ly.img.typeface/fonts/Roboto/Roboto-Regular.ttf"), subFamily: "Regular", weight: .normal, style: .normal, ), ],)try engine.variable.set(key: "accent", color: .rgba(r: 0.48, g: 0.23, b: 0.93, a: 1))try engine.variable.set(key: "cornerRadius", number: 40)try engine.variable.set( key: "heroImage", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"),)try engine.variable.set(key: "headline", typeface: roboto)try engine.variable.set(key: "title", value: "Summer Sale")A number must be finite. A typeface needs a name and at least one font. One key holds one type, so set(key: "accent", number: 1) throws while accent is a color variable.
Reading Variables#
getColor(key:), getNumber(key:), getImageURL(key:) and getTypeface(key:) return the values. getType(key:) returns a VariableType: .string, .color, .number, .image or .typeface. findAll(type:) lists the keys of one type.
let accentType = try engine.variable.getType(key: "accent") // .colorlet cornerRadius = try engine.variable.getNumber(key: "cornerRadius") // 40let colorVariables = engine.variable.findAll(type: .color) // ["accent"]// findAll() lists text variables onlylet textVariables = engine.variable.findAll() // ["title"]print(accentType, cornerRadius, colorVariables, textVariables)findAll() lists text variables only, as before. Use findAll(type:) for the other types.
Binding Properties#
We create a card with a color fill and a rect shape, an image with an image fill, and a text that shows the title variable. A binding targets the block that owns the property: the fill owns fill/color/value, and the shape owns the corner radii.
// A card with a color fill and a rect shapelet card = try engine.block.create(.graphic)try engine.block.setShape(card, shape: engine.block.createShape(.rect))try engine.block.setFill(card, fill: engine.block.createFill(.color))try engine.block.setWidth(card, value: 720)try engine.block.setHeight(card, value: 520)try engine.block.setPositionX(card, value: 40)try engine.block.setPositionY(card, value: 40)try engine.block.appendChild(to: page, child: card)let cardFill = try engine.block.getFill(card)let cardShape = try engine.block.getShape(card)
// An image with an image filllet image = try engine.block.create(.graphic)try engine.block.setShape(image, shape: engine.block.createShape(.rect))let imageFill = try engine.block.createFill(.image)try engine.block.setFill(image, fill: imageFill)try engine.block.setWidth(image, value: 640)try engine.block.setHeight(image, value: 300)try engine.block.setPositionX(image, value: 80)try engine.block.setPositionY(image, value: 80)try engine.block.appendChild(to: page, child: image)
// A headline that shows the text variablelet text = try engine.block.create(.text)try engine.block.replaceText(text, text: "{{title}}")try engine.block.setWidth(text, value: 640)try engine.block.setHeightMode(text, mode: .auto)try engine.block.setPositionX(text, value: 80)try engine.block.setPositionY(text, value: 420)try engine.block.appendChild(to: page, child: text)setVariableBinding(_:property:key:) binds a property to a variable. The property takes the variable’s current value at once.
// Each binding applies the current value of the variable at oncetry engine.block.setVariableBinding(cardFill, property: "fill/color/value", key: "accent")for corner in ["TL", "TR", "BL", "BR"] { try engine.block.setVariableBinding(cardShape, property: "shape/rect/cornerRadius\(corner)", key: "cornerRadius")}try engine.block.setVariableBinding(imageFill, property: "fill/image/imageFileURI", key: "heroImage")try engine.block.setVariableBinding(text, property: "text/typeface", key: "headline")Each variable type binds to a fixed set of properties:
| Variable type | Properties it binds to |
|---|---|
| Color | Color properties, for example fill/color/value and fill/solid/color |
| Number | Float properties, for example opacity, width and shape/rect/cornerRadiusTL. Int properties, such as shape/star/points, do not bind |
| Image | fill/image/imageFileURI, fill/image/previewFileURI and fill/video/fileURI |
| Typeface | text/typeface and caption/typeface |
Text variables and gradient color stops cannot bind. On a text block, a bound typeface, text color or font size applies to the whole text.
Changing Variables#
Set a variable again to update every property that is bound to it.
// Every bound property follows the new valuestry engine.variable.set(key: "accent", color: .rgba(r: 0.05, g: 0.55, b: 0.55, a: 1))try engine.variable.set(key: "cornerRadius", number: 24)try engine.variable.set( key: "heroImage", imageURL: baseURL.appendingPathComponent("ly.img.image/images/sample_2.jpg"),)let updatedCardColor: Color = try engine.block.getColor(cardFill, property: "fill/color/value")An image variable also empties the source set of the fill, so the fill shows the URL of the variable. When one bound property rejects a value, for example an opacity above 1, the setter throws the first error. The variable and the other bound properties change anyway.
Reading Bindings#
getVariableBinding(_:property:) returns the key of the variable that a property is bound to, or nil. findAllVariableBindings(_:) lists the bound properties of a block.
let cardFillBinding = try engine.block.getVariableBinding(cardFill, property: "fill/color/value") // "accent"let opacityBinding = try engine.block.getVariableBinding(card, property: "opacity") // nil// The four corner radius properties of the shapelet shapeBindings = try engine.block.findAllVariableBindings(cardShape)Bound Properties Are Locked#
A bound property changes only through its variable. A direct write throws an EngineError whose catalogCode is .blockPropertyBound, and the value stays as it is.
// A bound property changes only through its variablevar boundWriteErrorCode: EngineErrorCode?do { try engine.block.setColor(cardFill, property: "fill/color/value", color: .rgba(r: 1, g: 0, b: 0, a: 1))} catch let error as EngineError { boundWriteErrorCode = error.catalogCode // .blockPropertyBound}Moving, resizing, rotating, cropping, aligning and scaling also leave a bound property unchanged. A bound typeface also locks setTypeface and setFont. Keyframes, undo and loading a scene still change a bound property. Changing the design unit also converts a bound length, such as a width or a corner radius. A number variable whose bound properties all hold its value converts with them. Otherwise the variable keeps its value, and the next write to the variable sets the lengths in the new unit.
Removing Bindings#
removeVariableBinding(_:property:) removes the binding. The property keeps its current value and no longer follows the variable.
// The bottom corners keep their current radius and stop followingtry engine.block.removeVariableBinding(cardShape, property: "shape/rect/cornerRadiusBL")try engine.block.removeVariableBinding(cardShape, property: "shape/rect/cornerRadiusBR")try engine.variable.set(key: "cornerRadius", number: 60)let detachedCornerRadius = try engine.block.getFloat(cardShape, property: "shape/rect/cornerRadiusBL") // 24let boundCornerRadius = try engine.block.getFloat(cardShape, property: "shape/rect/cornerRadiusTL") // 60remove(key:) removes a variable of any type. Its bound properties keep the last value and the binding. When you set a variable with the same key and type again, they follow it again.
Renaming Variables#
rename(key:to:) moves the value and the type of a variable to a new key. Every binding follows the new key. For a text variable, every {{key}} token in text changes to the new key.
// Bindings and {{tokens}} in text follow the new nametry engine.variable.rename(key: "accent", to: "brand/accent")try engine.variable.rename(key: "title", to: "campaign/title")let renamedBinding = try engine.block.getVariableBinding(cardFill, property: "fill/color/value") // "brand/accent"let renamedText = try engine.block.getString(text, property: "text/text") // "{{campaign/title}}"A new name must follow these rules:
- It is not empty.
- It does not start or end with whitespace. Spaces inside the name are allowed.
- It does not contain
{{or}}, because text tokens use them. - No other variable uses it. A rename to the current name does nothing.
// .blockVariableNameInvalid for the first three names,// .blockVariableNameTaken for the name of another variablevar renameErrorCodes: [EngineErrorCode?] = []for name in ["", " padded", "a{{b", "campaign/title"] { do { try engine.variable.rename(key: "brand/accent", to: name) } catch let error as EngineError { renameErrorCodes.append(error.catalogCode) }}Handling Type Errors#
A getter of another type and a binding to a property of another type throw errors. The variable and the property stay unchanged.
var typeMismatchErrorCode: EngineErrorCode?do { // A color variable has no number value _ = try engine.variable.getNumber(key: "brand/accent")} catch let error as EngineError { typeMismatchErrorCode = error.catalogCode // .blockVariableTypeMismatch}var bindingTypeErrorCode: EngineErrorCode?do { // Opacity is a number property, so it cannot bind to a color variable try engine.block.setVariableBinding(card, property: "opacity", key: "brand/accent")} catch let error as EngineError { bindingTypeErrorCode = error.catalogCode // .blockVariableBindingUnsupported}Saving and Undo#
A scene or an archive saves the typed variables and the bindings. Undo and redo restore variable values, bindings and renames.
Exporting the Result#
We export the page as a PNG. The card shows the last values of its variables.
let png = try await engine.block.export(page, mimeType: .png)print("Exported \(png.count) bytes")Troubleshooting#
- The variable does not exist (
.blockVariableNotFound): set the variable before you read or bind it. - The key holds another type (
.blockVariableTypeMismatch): use the getter and setter of the stored type, or remove the variable first. - The typeface is not accepted (
.blockVariableTypefaceInvalid): pass a typeface with a name and at least one font. - The property cannot bind to this variable (
.blockVariableBindingUnsupported): check the table in Binding Properties, and pass the block that owns the property. - Gradient stops do not bind (
.blockVariableBindingGradientStopsUnsupported): bind a color variable to a solid color fill instead. - A write to a property fails (
.blockPropertyBound): set the variable instead, or remove the binding first. - A rename fails with an invalid name (
.blockVariableNameInvalid): follow the name rules in Renaming Variables. - A rename fails with a taken name (
.blockVariableNameTaken): pick a name that no variable uses.
The Error Catalog lists every error code.
API Reference#
Methods#
| Method | Description |
|---|---|
engine.variable.set(key:color:) / getColor(key:) |
Set or read a color variable |
engine.variable.set(key:number:) / getNumber(key:) |
Set or read a number variable |
engine.variable.set(key:imageURL:) / getImageURL(key:) |
Set or read an image variable |
engine.variable.set(key:typeface:) / getTypeface(key:) |
Set or read a typeface variable |
engine.variable.set(key:value:) |
Set a text variable |
engine.variable.getType(key:) |
Get the type of a variable |
engine.variable.findAll(type:) |
List the keys of the variables of one type |
engine.variable.findAll() |
List the keys of the text variables |
engine.variable.rename(key:to:) |
Rename a variable of any type |
engine.variable.remove(key:) |
Remove a variable of any type |
engine.block.setVariableBinding(_:property:key:) |
Bind a property to a variable |
engine.block.getVariableBinding(_:property:) |
Get the key of the bound variable, or nil |
engine.block.findAllVariableBindings(_:) |
List the bound properties of a block |
engine.block.removeVariableBinding(_:property:) |
Remove a binding and keep the current value |
Next Steps#
- Text Variables: fill
{{key}}tokens in text with values. - Placeholders: mark the blocks that template users can replace.
- Undo and History: work with undo steps.