Skip to content

Modifiers

A modifier changes how a view looks or behaves. You call it on a view with a dot, and you can chain as many as you like:

Text("Hi")
.font(size: 18, weight: .semibold)
.color(.secondary)
.padding(8)
.background(.blue, radius: 6)

A line that starts with . continues the expression on the line before, so long chains can be spread over several lines. Signatures on this page use the same notation as Views: _ means the argument has no label, and = value is a default.

Every modifier works on every view, built-in or your own. On a custom view, it applies to the view’s whole body.

The order of modifiers doesn’t matter. Each view has one set of settings, and each modifier fills in some of them. These two lines give the same result: a 100-point-wide box, with 10 points of padding inside it and a yellow background behind all of it:

Text("A").padding(10).frame(width: 100).background(.yellow)
Text("A").background(.yellow).frame(width: 100).padding(10)

Repeating a modifier replaces the earlier value, with two exceptions:

  • padding adds up: .padding(4).padding(horizontal: 20) gives 4 points above and below and 24 on each side.
  • font and frame only change the settings you pass: .font(size: 20).font(weight: .bold) is the same as .font(size: 20, weight: .bold).

Some settings are inherited. font, color, textAlignment and disabled on a stack (or any view with content) apply to everything inside it, unless a view inside sets its own:

VStack {
Text("Both lines are red and 18 points")
Text("This one is bold too").font(weight: .bold)
}
.font(size: 18)
.color(.red)

opacity and hidden aren’t inherited, but they affect everything drawn inside the view. All other modifiers affect only the view they’re on.

.padding(_ amount: Float = 8, horizontal: Float, vertical: Float,
top: Float, leading: Float, bottom: Float, trailing: Float)

Adds empty space around the view’s content, inside its frame and background.

Text("Default").padding()
Text("All sides").padding(16)
Text("Sides and ends").padding(horizontal: 24, vertical: 4)
Text("Two edges").padding(top: 20, leading: 40)
Text("8, but 20 below").padding(8, bottom: 20)
  • With no arguments, every edge gets 8 points.
  • With arguments, each edge takes the most specific value given: its own edge (top:, leading:, bottom:, trailing:), then its direction (vertical: for top and bottom, horizontal: for leading and trailing), then the unlabeled amount. Edges with no value get 0, so .padding(horizontal: 30) adds nothing above or below.
  • Paddings add up: .padding(8).padding(8) is the same as .padding(16).
.font(size: Float = inherited, weight: Weight = inherited)

Sets the text size (in points) and weight.

Text("Title").font(size: 24, weight: .bold)
Text("Note").font(size: 11)
  • Only the parts you pass change; the rest come from the view’s container. In a window, text starts at 14 points, .regular.
  • weight is one of .regular, .medium, .semibold, .bold (see Weight).
  • Inherited: set it on a stack to change all the text inside.
  • Icons are sized by the font size too. A CodeEditor always uses its own 13-point monospaced font.
.color(_ color: Color)

Sets the color of text, icons, and currentColor in SVGs.

Text("Saved").color(.green)
Icon(.warning).color(.orange)
  • The default is .primary (near-black). See Colors for all colors.
  • Inherited: set it on a stack to color everything inside.
  • A button’s label is blue unless .color is set on the button itself; a color inherited from a container doesn’t change it.
.background(_ color: Color, radius: Float = 0)

Fills the view’s whole area, padding included, with a color. radius rounds the corners.

Text("Tag")
.padding(horizontal: 8, vertical: 2)
.background(.lightGray, radius: 6)
  • The background is drawn behind the view’s content.
  • On a Button, it replaces the button’s own gray background.
.border(_ color: Color, width: Float = 1, radius: Float = 0)

Draws a line around the view’s edge. width is the line’s thickness and radius rounds the corners.

Text("Outlined")
.padding(8)
.border(.separator, width: 1, radius: 6)
  • The border follows the view’s outer edge (padding and frame included) and is drawn on top of the content.
  • It doesn’t take up space: it’s centered on the edge, so a thick border overlaps the content a little.
.frame(width: Float?, height: Float?,
minWidth: Float?, maxWidth: Float?,
minHeight: Float?, maxHeight: Float?,
alignment: Alignment)

Controls the view’s size. All arguments are optional; pass only the ones you need, in this order.

Text("Fixed").frame(width: 120, height: 40)
Button("Full width") { save() }.frame(maxWidth: .infinity)
Button("OK") { save() }.frame(minWidth: 100)
Text("Right").frame(maxWidth: .infinity, alignment: .trailing)
ArgumentEffect
width, heightA fixed size. The view is exactly this big, and no longer flexible in that direction.
maxWidth, maxHeightThe view becomes flexible: it takes all the space it’s offered, up to this size, but never less than its content. Use .infinity for “as much as possible”.
minWidth, minHeightThe view is never smaller than this. It still grows with its content.
alignmentWhere the content sits when the frame is bigger than it.
  • The size includes the view’s padding.
  • .infinity is the only shorthand for a Float; you can write it wherever a Float is expected, but it’s meant for maxWidth: and maxHeight:.
  • Alignment. Text, Icon, Svg and SVG Image content is centered in a larger frame unless alignment: says otherwise. A stack with a frame spreads its children over the whole frame; with alignment:, it keeps its own size and sits at that position in the frame. Other views, like buttons and text fields, fill their frame.
  • Text wraps to fit the frame’s width.
  • Giving a width to an SVG or Image scales the drawing to fit.
.opacity(_ value: Float)

Makes the view and everything in it see-through: 0 is invisible, 1 is fully visible. Values outside 0…1 are clamped.

Text("Faded").opacity(0.4)

An invisible view still takes up space and still responds to clicks. To remove a view from the layout, use hidden.

.hidden(_ isHidden: Bool = true)

Hides the view when isHidden is true.

Text("Only when on").hidden(!isOn)
  • A hidden view takes up no space, as if it wasn’t there: stacks give it no spacing either. (In SwiftUI, a hidden view keeps its space.)
  • It isn’t drawn, doesn’t respond to clicks, and its shortcut doesn’t run.
  • It is still built, so state inside it is kept while it’s hidden. Using if instead removes the view, and its state, entirely.
.disabled(_ isDisabled: Bool = true)

Turns off interaction with the view when isDisabled is true.

Button("Send") { save() }.disabled(name.isEmpty)
  • A disabled view ignores clicks, and its shortcut doesn’t run. A disabled text field can’t be edited.
  • Text, controls and icons in it are drawn at 40% opacity.
  • Inherited: disabling a stack disables everything inside.
.onTap(_ action: fn())

Runs action when the view is clicked. Usually written with a block after the call.

Text("Click me").onTap {
count += 1
}
  • A click counts when the mouse button is pressed and released over the same view.
  • Controls inside the view (buttons, toggles, text fields) handle their own clicks; clicks on other parts of the view run action.
  • Doesn’t run while the view is disabled or hidden.
.onDrag(_ action: fn(Float, Float))

Runs action again and again while the mouse is moved with its button held down, starting on the view. It gets how far the mouse moved since the last time, across and down, in points.

Spacer()
.frame(width: 6, maxHeight: .infinity)
.background(.separator)
.onDrag { dx, dy in
sidebar = (sidebar + dx).clamped(min: 120, max: 400)
}
  • The drag goes on until the button is released, also when the mouse leaves the view (as it does at once, with a thin bar).
  • Add the movements up to follow the mouse: a width that’s dragged is width + dx. Keep the result in bounds yourself, as with clamped above.
  • Over the view, the pointer shows that it can be dragged: sideways arrows over a tall, thin view, up-and-down arrows over a wide, flat one, and a hand otherwise.
  • A view with onDrag doesn’t get onTap clicks.
  • Doesn’t run while the view is disabled or hidden.
.id(_ value: Int or String)

Gives the view an identity of its own, instead of its position among its siblings.

for item in items {
Text(item.name).id(item.id)
}

Tessel keeps each view’s state, text field focus and undo history by identity. By default the identity is the view’s position, so if a list is reordered, the state stays at the old positions. With .id(…), the state follows the item. The value must be an Int or a String, and should be unique among the views next to it. See Lists.

.tag(_ value: T)

Marks an option in a Picker or a page in a TabView. value must have the same type as the picker’s or tab view’s selection.

Picker(selection: mode) {
Text("List").tag(Mode.list)
Text("Grid").tag(Mode.grid)
}

.tag is only allowed on the views directly inside a Picker or TabView block; anywhere else it’s an error.

.shortcut(_ key: String, action: fn())

Runs action when the user presses Cmd + key (Ctrl + key on Windows). The action is usually a block after the call.

Button("Save") { save() }
.shortcut("s") { save() }
  • key is one character that isn’t an uppercase letter or whitespace, like "s", "1" or ",". Shift isn’t taken into account: .shortcut("s") also runs on Shift+Cmd+S. The key names "enter", "escape", "tab", "backspace", "delete", "left", "right", "up", "down", "home" and "end" work too.
  • Any other key, such as "F5", "S" or "cmd+s", is an error. Don’t write Cmd or Ctrl: it’s added for you.
  • The shortcut works only while its view is shown and enabled. In a TabView, only the shortcuts on the selected page work.
  • The shortcut belongs to the view it’s on, not to a button: attaching it to a button doesn’t press the button. Put the same code in both, or call one function from both.
  • While text is being edited, Cmd+A, Cmd+C, Cmd+X, Cmd+V and Cmd+Z keep their editing meaning.
  • If several views have the same shortcut, the first one (from the top of the view tree) runs.
.onAppear(_ action: fn())

Runs action once when the view appears.

Text("{count} files").onAppear {
count = listFolder(currentFolder())?.count ?? 0
}
  • “Appears” means the view is there now but wasn’t in the previous update: when the app starts, or when an if or a for loop adds it. If the view goes away and comes back, action runs again. A view made invisible with .hidden() is still there, so hiding and showing it doesn’t run action.
  • It runs before the view is first drawn. It may change state; the screen is then updated before anything is drawn.
.onSubmit(_ action: fn())

On a TextField: runs action when the user presses Enter while editing it.

TextField("New item", text: name)
.onSubmit { save() }

Enter also stops editing the field. (In a CodeEditor, Enter starts a new line instead, so .onSubmit doesn’t apply.)

.tabItem(_ title: String)

The title of a page’s tab in a TabView. Pages without one are titled “Untitled”.

TabView(selection: tab) {
Text("Welcome!").tabItem("Home").tag("home")
}
.autofocus()

On a TextField or CodeEditor: starts editing it as soon as it appears, so the user can type right away.

TextField("Search", text: name).autofocus()

It takes effect when the view appears (see onAppear), not on every update. If several views appear with .autofocus() at the same time, the last one gets the focus.

.animation(_ seconds: Float = 0.25)

Changes to the view, and to the views inside it, take seconds to happen instead of happening at once: a view moves and resizes to its new place, its opacity and background color change gradually, and a view that appears fades in.

VStack {
Text("Details").frame(height: if expanded { 120 } else { 24 })
Button("More") { expanded = !expanded }
}
.animation()
  • Inherited: it covers everything inside the view. .animation(0) turns it off again for a part.
  • A change starts slowly, speeds up and slows down at the end. A change that comes while another is under way starts from where the view is.
  • A view that disappears is removed at once. Scrolling and resizing the window are never animated.
  • Views are told apart by their position, or by .id: give the views of a list an id so that the right ones move when one is added or removed.
  • Clicks go to where a view will end up, not to where it’s drawn on the way.
.hoverBackground(_ color: Color, radius: Float = 0)

A background that’s shown only while the mouse is over the view. radius rounds the corners.

Text("files")
.padding(4)
.onTap { save() }
.hoverBackground(.lightGray, radius: 4)

It’s drawn on top of a normal background.

Any view can have a hover background. If views with hover backgrounds are nested, only the innermost one under the pointer shows it.

.textAlignment(_ alignment: Alignment)

Lines up the lines of multi-line text inside the text’s own box: .leading (the default), .center or .trailing.

Text("Welcome to Tessel.\nLet's build something.")
.textAlignment(.center)
  • Only the horizontal part of the alignment counts: .topTrailing acts like .trailing, and .top or .bottom like .center.
  • It doesn’t move the text’s box. Where the box goes is up to its stack’s alignment: or its frame’s.
  • Inherited: set it on a stack to align all the text inside.