Search Docs
Loading...
Skip to content

Variable Bindings

Drive block colors, sizes, images and typefaces from typed variables, so one change updates every bound property.

10 mins
estimated time
GitHub

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") // .color
let cornerRadius = try engine.variable.getNumber(key: "cornerRadius") // 40
let colorVariables = engine.variable.findAll(type: .color) // ["accent"]
// findAll() lists text variables only
let 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 shape
let 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 fill
let 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 variable
let 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 once
try 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 values
try 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 shape
let 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 variable
var 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 following
try 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") // 24
let boundCornerRadius = try engine.block.getFloat(cardShape, property: "shape/rect/cornerRadiusTL") // 60

remove(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 name
try 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 variable
var 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#