How Swift's result builders can help us write smarter, safer HTML.
Swift's result builders are a powerful language feature that let us create domain-specific languages right inside our Swift code. With a little thinking, this means we can actually create whole websites in Swift, with our code automatically being converted to valid, accessible Swift, and we can even sprinkle in a little SwiftUI magic to complete the effect.
Let's get to it…
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
For over 30 years, HTML has been a great language for describing the structure of web pages.
For example, we can write HTML like this:
<h1>Wise words</h1>
<p>"If you don't take risks, you can't create a future" - <em>Monkey D. Luffy</em> in <a href="https://en-wp.org/wiki/One_Piece">One Piece</a></p>
That has a heading, a paragraph of text with some emphasis, and a link to another page. But, what happens if we forget the closing </em>
tag? Without it, web browsers will assume everything that follows should also be emphasized.
That's not what I intended, but it's easy to do because HTML is just a bunch of text.
But even if you write perfect HTML, there are other, bigger problems:
Ultimately, all these boil down to one huge problem: most people don't have enough time to become experts in Swift and also experts in HTML.
And so I want to suggest that the answer is to not use HTML, or at least not directly. Instead, I would you like to propose that we use Swift to build our websites.
Back in 2019 when Apple announced SwiftUI there the usual What's New in Swift presentation. During that talk they showed the following HTML:
<html>
<head>
<title>Jessica's WWDC19 Blog</title>
</head>
<body>
<h2>Welcome to Jessica's WWDC19 Blog!</h2>
<br>
Check out the talk schedule and latest news from <a href="https://developer.apple.com/wwdc19/">the source</a>
</body>
</html>
And they also showed what it looked like in Swift:
"""<html>
<head>
<title>\(name)'s WWDC19 Blog</title>
</head>
<body>
<h2>Welcome to \(name)'s WWDC19 Blog!</h2>
<br>
Check out the talk schedule and latest news from <a href="https://developer.apple.com/wwdc19/">the source</a>
</body>
</html>
"""
Except of course really here they have just put the HTML into a Swift string – this code is objectively worse, because now we can't use HTML editors to get things like autocomplete or syntax highlighting.
But then they showed this version:
html {
head {
title("\(name)'s WWDC19 Blog")
}
body {
h2 { "Welcome to \(name)'s WWDC19 Blog!" }
br()
"Check out the talk schedule and latest news from "
a {
"the source"
}.href("https://developer.apple.com/wwdc19/")
}
}
This used what was then a new feature called function builders, now renamed to result builders, to define the structure of our web pages in pure Swift code, and used that to generate HTML.
Now the compiler can check our code is good, which means it's impossible to miss an ending tag by accident – doing so will trigger an error straight away.
So I want to look at how we can build this for real. Rather than just a simple demo on a WWDC slide, how can we use result builders to build a real HTML renderer?
We're going to start really small: in order to be valid HTML, a type must know how to render itself to a string, because ultimately everything we build must become a HTML string.
So, we can start with a protocol called HTML
, which says any conforming type must have a render()
method that returns a String
:
protocol HTML {
func render() -> String
}
And now if we look at that Swift code from 2019, we can choose a tag to build. Of all of them, the br
is easiest – it just creates the HTML string <br />
.
So, we could define a simple br
struct like this one:
struct br: HTML {
func render() -> String {
"<br />"
}
}
Then call render()
on it to get the result:
let result = br().render()
print(result)
// <br />
That's one done!
Next, let's look at this:
"Check out the talk schedule and latest news from "
Here Apple is putting a string right inside their HTML! We can't put this in its own struct because it's just a loose string. However, we can instead make Swift's String
type itself conform to our HTML
protocol, giving it a render()
method that simply returns the string itself:
extension String: HTML {
func render() -> String {
self
}
}
And now we can call render()
on a string, like this:
let result = "Push through the pain. Giving up hurts more.".render()
print(result)
Now obviously that's pretty pointless given that we're just printing the string itself, but it's important soon.
Back to Apple's code again, the next most complex is the title
tag. This becomes a HTML <title>
element, so it needs to accept a string to show in the page title.
Here we need a custom initializer, because the synthesized initializer won't match the code shown in Apple's presentation. So, we need this:
struct title: HTML {
var text: String
init(_ text: String) {
self.text = text
}
func render() -> String {
"<title>\(text)</title>"
}
}
Now we can go ahead and create page titles:
let result = title("Hacking with Swift").render()
print(result)
By now you're probably thinking two things:
Well, it doesn't use result builders, but that's about to change because the next piece of code we're going to implement is this:
h2 { "Welcome to \(name)'s WWDC19 Blog!" }
This makes a second-level heading in HTML, but as you can see this works differently from the <title>
tag we implemented earlier – this accepts a trailing closure of content to put inside the heading.
Whereas the <title>
tag only accepts plain text, you can put a range of things inside <h2>
– plain text, emphasis, links, and more.
This is where result builders come in: inside the closure being passed to h2
could be any kind of HTML, so we need a way to convert several pieces of HTML into an array that we can loop over.
But we don't actually need result builders to make such an array. Instead, we can create a h2
struct like this:
struct h2: HTML {
var content: () -> [any HTML]
func render() -> String {
// more code to come
}
}
That means it expects to be given a function that returns an array of HTML, which is exactly what we want.
When it comes to rendering its content, we need to call that content()
property as a function, because it will return the actual HTML to us. We can then loop over each item one by one and call its render()
method and send back the result.
Here's how that looks in code:
let tags = content()
let html = tags.map { $0.render() }.joined()
return "<h2>\(html)</h2>"
So now we have a new h2
struct that can work with any kind of HTML. Remember, this must be given a function that returns an array of types conforming to the HTML
protocol, so we use it like this:
let result = h2 {
[
"Welcome to \(name)'s WWDC19 Blog!"
]
}
That's not pleasant, but it is powerful – we can now put any different collection of HTML inside there, and it will all be rendered correctly:
let result = h2 {
[
"Welcome to \(name)'s WWDC19 Blog!",
br(),
"Check out the talk schedule and latest news."
]
}.render()
Here I want to take a brief diversion into what could have been, because result builders weren't the only major change introduced around Swift 5.1.
For example, SE-0254 introduced static subscripts. This isn't used much, but we could try it out in our current code – we could add a static subscript to our h2
struct that accepts zero or more HTML objects and renders a string:
static subscript(_ content: any HTML...) -> String {
// existing render() code
}
Why would we want that? Well, look at what our code becomes now:
let result = h2[
"Welcome to \(name)'s WWDC19 Blog!",
br(),
"Check out the talk schedule and latest news."
]
That's much nicer! But those commas are still there. Wouldn't it be nice if we could get rid of them too?
Well, SE-0257 tried to do exactly that. It was called "Eliding commas from multiline expression lists", and proposed that line breaks could be used in place of commas. It even said outright, "One case where the problem of visual clutter is especially pronounced is in domain-specific languages embedded within Swift. Particularly when they are declarative" – what does that remind you of?
This change would have allowed our code to be written without commas, like this:
let result = h2[
"Welcome to \(name)'s WWDC19 Blog!"
br()
"Check out the talk schedule and latest news."
]
If that doesn't seem familiar yet, imagine if we'd written this kind of code:
let result = VStack[
Text("Welcome to \(name)'s WWDC19 Blog!")
Divider()
Text("Check out the talk schedule and latest news.")
]
That's almost identical to modern SwiftUI, without using result builders.
Sadly, SE-0257 met an extremely negative response on Swift Evolution and was rejected, so we can't take this approach.
This is where result builders are required. First, let's go back to our closure approach:
h2 {
[
"Welcome to \(name)'s WWDC19 Blog!",
br(),
"Check out the talk schedule and latest news."
]
}
We still want to remove those commas, and we also want Swift to convert the contents of the closure into an array for us.
To make this happen, we'll make a new struct called HTMLBuilder
:
@resultBuilder
struct HTMLBuilder {
}
Notice that annotation at the start – this will be used as a result builder, so Swift will automatically look for static methods that convert different kinds of content into output.
There are lots of ways this can be called, but for now we going to implement the simplest: when we're given a variadic parameter containing 0 or more pieces of HTML, we'll send it back as a single array that can be looped over.
Internally Swift converts variadic parameters into an array for us, which means our output is just our input, like this:
static func buildBlock(
_ components: any HTML...
) -> [any HTML] {
components
}
That converts one block of content, which again can contain zero or more HTML objects, into an array of HTML objects.
And that's our result builder done, at least for now.
Let's put it to use. Right now our h2
struct has this content
property:
var content: () -> [any HTML]
To make that work with our result builder, we just need to mark the property with the @HTMLBuilder
attribute, like this:
@HTMLBuilder var content: () -> [any HTML]
And now using h2
is much nicer:
h2 {
"Welcome to \(name)'s WWDC19 Blog!"
br()
"Check out the talk schedule and latest news."
}
That one tiny result builder gives us incredibly clean syntax.
And in fact now it becomes trivial to go back to the original HTML Apple showed and implement more of it. For example, the body
type can be almost identical to h2
:
struct body: HTML {
@HTMLBuilder var content: () -> [any HTML]
func render() -> String {
"<body>\(content().render())</body>"
}
}
We can use a similar approach to implement the <a>
tag, but if you look at Apple's example HTML you'll see it has a href()
method call. This is a modifier just like we'd have in SwiftUI, so we can implement it in a similar way.
First, we'd create an a
struct that has a href
property to store the page it should link to. This will use the same @HTMLBuilder
we've used previously, so initially it looks like this:
struct a: HTML {
var href = "#"
@HTMLBuilder var content: () -> [any HTML]
func render() -> String {
"<a href=\"\(href)\">\(content().render())</a>"
}
}
Adding the href()
modifier means taking a copy of the current object, changing its href
property, then sending it back:
func href(_ newHRef: String) -> a {
var copy = self
copy.href = newHRef
return copy
}
And now our HTML is much closer to Apple's:
body {
h2 { "Welcome to \(name)'s WWDC19 Blog!" }
br()
"Check out the latest news from "
a {
"the source"
}.href("https://developer.apple.com/wwdc19")
}
From here we can carry on implementing the rest of Apple's HTML – html
and head
are all that remains, both of which are just more of the same.
But result builders are capable of so much more…
What if we wanted to generate different HTML based on a condition, like this:
if showLink {
"Check out the latest news from "
a {
"the source"
}.href("https://developer.apple.com/wwdc19")
}
This takes three small changes to our result builder.
First, we'll make it no longer work with individual HTML elements, and instead work only with arrays of HTML.
So, our current code is this:
static func buildBlock(_ components: any HTML...) -> [any HTML] {
components
}
But it's going to change to this:
static func buildBlock(_ components: [any HTML]...) -> [any HTML] {
components.flatMap { $0 }
}
We don't have arrays of HTML just yet, but for our second change we're going to tell our builder that every time it sees a single expression that generates HTML, it converts it into an array of HTML:
static func buildExpression(_ expression: any HTML) -> [any HTML] {
[expression]
}
That's enough to bring back our original behavior: Swift sees each individual part of our HTML structure as a single expression, then join each expression into a whole array of HTML.
But we want to add a condition in our builder – we want to be generate certain HTML only when the showLink
Boolean is true. This creates a problem: our builder wants to generate HTML, so what should it generate when the condition is false – when there is no HTML to generate?
In Swift we represent the potential absence of data using optionals, and result builders do exactly the same: we can implement a buildOptional()
method that will be given an optional array of HTML. If the array is present we just send it back, but if it's missing for some reason we'll send back an empty array – we don't want to render anything when there is nothing to render.
Here's how that looks in Swift:
static func buildOptional(_ component: [any HTML]?) -> [any HTML] {
component ?? []
}
And now our condition works correctly: when the Boolean is true extra HTML will be generated.
Let's go one step further: what if we want one set of HTML when the condition is true, and another when it's false?
Here's the kind of code we want to use:
if showLink {
"Check out the latest news from "
a {
"the source"
}.href("https://developer.apple.com/wwdc19")
} else {
"Are you ready to have your mind blown?"
}
This is easy for result builders: we need to implement two methods, buildEither(first:)
and buildEither(second:)
, both of which will be given an array of HTML from one side of a condition, and need to decide what to do with it.
Once again, we need nothing special here – we're given an array of HTML, and we need to send back an array of HTML, so our input is our output:
static func buildEither(first component: [any HTML]) -> [any HTML] {
component
}
static func buildEither(second component: [any HTML]) -> [any HTML] {
component
}
And now we get if/else
code working great. In fact, that change also enables switch/case
support – Swift compiles all the cases down to a binary tree of if/else
checks.
At this point we've built from scratch support for the HTML shown at WWDC19, and added a few extras to handle conditions.
All this so far is interesting, but not really useful. Yes, the compiler checks our code and that helps avoid mistakes, but think back to the four problems I mentioned earlier:
Our result builder solves none of those.
What we've seen so far is pretty standard – there are several existing Swift projects that build HTML like this, and all suffer from those same four problems.
And like I said at the beginning, all four of those problems boil down to one huge problem: most Swift experts aren't also HTML experts.
Right now our code might use the same features as SwiftUI, but it operates very differently.
When you make a TabView
in SwiftUI, it looks and works differently depending on your platform:
We haven't told SwiftUI how to adapt across various devices or screen sizes. We haven't told it how to render the various controls, or how to handle user interaction – that's all taken care of for us automatically.
SwiftUI wants us to take a high-level, declarative approach: we specify what we want to achieve, rather than the individual steps required to get there.
So, when we want a scrolling list of rows, we write code like this:
List(0..<100) { i in
Text("Row \(i)")
}
What List
becomes will vary by platform, but SwiftUI takes care of all of that for us, and also handles all interactions automatically. It backs onto UIKit or AppKit depending on what platform our code is running on, ensuring that our UI looks great everywhere – we don't care how complex the UIKit code is behind the scenes, because that all gets wrapped up behind a simple List
view.
If we're to achieve the same here – if we're to let users get a huge amount of functionality without needing to understand HTML – then we need to bring in an external framework too.
In this case, we're going to bring in a wildly popular web framework called Bootstrap. Bootstrap provides for us a big chunk of CSS and JavaScript that is considered standard on modern websites, including what's called a reset stylesheet – code makes websites look the same on all web browsers.
So before we even start doing any work, just by including Bootstrap in our project we solve the first of our four problems.
The second problem was making sure pages adapt to different screen sizes, and again Bootstrap solves this: we can place our content into rows of data, and tell it to render HTML elements inside there as columns of various sizes.
Bootstrap will then adapt its layout based on how much space is available: when the screen is wider content gets placed horizontally, but when space is restricted that same content gets placed vertically.
So, we could build a simple row
component like this:
struct row: HTML {
@HTMLBuilder var items: () -> [any HTML]
func render() -> String {
// rendering code
}
}
Just like our other components, that uses our HTML result builder so it can work with any kind of HTML.
This time, though, we're going to render the content using Bootstrap's CSS classes: one for the whole row, and one for each column to be sized for medium-sized displays:
let html = items().map {
"<div class=\"col-md\">\($0.render())</div>"
}
return "<div class=\"row\">\(html)</div>"
Using this new type looks just like our other HTML types:
row {
h2 { "Example Column" }
h2 { "Example Column" }
h2 { "Example Column" }
}
But now we're generating responsive HTML – HTML that automatically adapts to the device it's displaying on. That's our second problem solved, in a matter of minutes – Bootstrap is doing a lot of work for us here.
The third problem was trying to bring in more advanced UI elements such as dropdown menus, carousels, and accordions. This goes back to what SwiftUI does well: we say we want a List
or a TabView
, and it figures out how to make that happen, rather than us having to describe every step required to get there.
Again, Bootstrap has lots of predefined controls built in: alerts, badges, navigation bars, and more.
Let's look at one just briefly, in a simplified form: a dropdown button showing several options. This starts out similar to the a
struct we made earlier, because it needs a button title along with some items to show inside:
struct dropdown: HTML {
var title: String
@HTMLBuilder var items: () -> [any HTML]
func render() -> String {
// more code to come
}
}
Where things get more complicated is the render()
method, because in order to make a dropdown button work Bootstrap wants our data in a very specific form.
So, rendering starts out by converting all the items into HTML list items:
let html = items().map {
"<li>\(item.render())</li>"
}.joined()
Then we'll send back a big string of HTML, because Bootstrap wants things in a very specific format:
return """
<div class="dropdown">
<button class="dropdown-toggle">\(title)</button>
<ul class="dropdown-menu">\(html)</ul>"
</div>
"""
Now you might think is back to putting HTML into one big Swift string, but wait: what we've done is encapsulate all the HTML inside our dropdown
struct, so when it comes to using the component we are writing pure Swift:
dropdown(title: "Tap Me") {
a { "Example link 1" }
.href("https://www.apple.com")
a { "Example link 2" }
.href("https://www.swift.org")
}
Again, this is how SwiftUI works: we don't care what kind of UIKit code exists behind the scenes, because it's wrapped in neater, simpler code for us.
Best of all, Bootstrap supports a standard called Accessible Rich Internet Applications, or just ARIA for short, so controls like this dropdown button work great with systems such as VoiceOver.
And that's problems three and four solved: we've got lots of powerful UI components, and we're building sites everyone can use.
That leaves us with the one huge problem: how can we h help Swift developers write great HTML? Right now we're effectively encoding HTML in Swift – they still need to know what all those HTML tags do.
I've built this HTML result builder following the code shown in Apple's presentation from 2019. But what if we rethought this approach completely and built something optimized for SwiftUI developers?
What if rather than using structs like h2
and a
we could instead write code like this:
Text("Swift rocks")
.foregroundStyle(.secondary)
.font(.title1)
Link("Swift", target: "https://www.swift.org")
.linkStyle(.button)
Divider()
Image("logo.jpg")
.accessibilityLabel("The Swift logo.")
.padding()
That would be great, wouldn't it? SwiftUI-style code that can become HTML. I thought the same thing, so I built a new framework to do exactly that.
It's called Ignite, and it builds on Bootstrap to provide an incredible website building experience for Swift developers:
Ignite is built entirely in Swift, with SwiftUI-style result builders and modifiers – you don't need to know a any HTML. It's not trying to convert SwiftUI code to HTML; instead, it's a custom set of tools that help Swift developers build fast, beautiful, and accessible websites using the language they already know.
Best of all, Ignite is available for everyone, open source for everyone completely free of charge – visit https://github.com/twostraws/Ignite to try it out.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.