Articles, podcasts and news about Swift development, by John Sundell.

Introducing Plot Components: A new way to build HTML pages using Swift

Published on 11 May 2021

Today I’m launching a huge update to my suite of Swift static site generation tools — specifically a brand new version of Plot, the library that’s used to generate all of this website’s HTML, which adds a new API for building HTML components in a very SwiftUI-like way.

This new version has been in the works for over a year, and has been properly battle-tested in production. In fact, it was used to render the HTML for the article that you’re reading right now! So I couldn’t be more excited to now finally make it publicly available for the entire Swift community.

Instabug

Instabug: Whether it’s crashes, slow screen transitions, delayed network calls, or unresponsive UIs — Instabug automatically gives you all of the logs you need to fix bugs and issues, and to ship high-quality apps. Get started now.

From nodes to components

Up until this point, Plot has been using an API that represents all of the elements and attributes within an HTML page as nodes, which can then be composed and combined in various ways.

For example, here’s how an array of BlogPost models could be turned into a <ul>-element based feed of blog posts:

func makeBlogFeed(containing posts: [BlogPost]) -> Node<HTML.BodyContext> {
    .ul(
        .class("blog-feed"),
        .forEach(posts) { post in
            .li(.article(
                .img(.src(post.imageURL)),
                .h1(.text(post.title)),
                .p(.text(post.description)),
                .a("Continue reading", .href(post.url))
            ))
        }
    )
}

I’m still incredibly happy with the design of the API that’s used above, as it’s in many ways a perfect match for the very hierarchical nature of HTML and XML-based documents — but at the same time, I’ve become increasingly curious to see what a “SwiftUI makeover” of that API could look like.

Now, I don’t mean actually using SwiftUI itself to render HTML. Since it’s a closed-sourced project that was developed exclusively for building native views on Apple’s platforms, there’s really no reasonable way for a third party developer (like myself) to use it to render arbitrary HTML strings.

However, like we’ve taken a look at in articles like “The Swift 5.1 features that power SwiftUI’s API”, the public APIs that SwiftUI offers are all implemented using official language features (as of Swift 5.4 when result builders finally became a proper part of the language). So, rather than attempting to turn SwiftUI into an HTML renderer, I built my own SwiftUI-inspired API that’s been tailor-made for the task of building HTML components.

This is what our blog feed from before would look like if we instead implemented it using that new component-based API:

struct BlogFeed: Component {
    var posts: [BlogPost]

    var body: Component {
        List(posts) { post in
            Article {
                Image(post.imageURL)
                H1(post.title)
                Paragraph(post.description)
                Link("Continue reading", url: post.url)
            }
        }
        .class("blog-feed")
    }
}

Even though we’re now using a substantially different syntax to render our HTML, the end result will actually be identical to our earlier implementation. What’s really cool, though, is that we no longer need to manually construct elements like the <ul> and <li> tags that’s used to render our list — we can now simply create a List component and Plot will take care of all of those details for us.

Adapting some of SwiftUI’s core concepts for the web

Something that’s very important, though, is that the goal of this project wasn’t simply to copy SwiftUI’s API directly. After all, building statically generated websites is fundamentally different from native app development, so while I wanted this new API to feel familiar to developers who know SwiftUI, I also wanted it to feel right at home within the context of HTML.

One concrete example of that is Plot’s environment API, which — just like the one that SwiftUI offers — lets you modify the behavior of certain components by entering values into the overall environment that they’re being rendered in.

On the surface level, that API works the exact same way as when you apply modifiers like font and foregroundColor to a SwiftUI view hierarchy — you can apply such modifiers to a given component, and they will then be automatically forwarded to that component’s children.

For example, here’s how we could apply a certain list style and link target to every List/Link that appears within a given hierarchy:

struct ExternalLinks: Component {
    var body: Component {
        Div {
            H2("External Links")
            List {
                Link("My apps on the App Store", url: "...")
                Link("Twitter", url: "...")
                Link("GitHub", url: "...")
            }
        }
        .class("external-links")
        .listStyle(.ordered)
.linkTarget(.blank)
    }
}

The result of the above is that our List will be rendered as ordered (using the <ol> element), and that each of the <a> elements that will be generated for our Link components will be assigned the attribute target="_blank", which will make their URLs open in a new tab.

But if we now dive a little bit deeper into this feature and take a look at how we can define our very own environment values and keys, we can see that I made a few different decisions compared to how SwiftUI’s environment API is designed.

As an example, let’s take a look at this simplified version of the Menu component that I use to render the main menu of this very website — which uses Plot’s environment API to retrieve what section that’s currently selected:

// In Plot, environment keys are defined by extending a concrete
// EnvironmentKey type, rather than by conforming to a protocol:
extension EnvironmentKey where Value == SwiftBySundell.SectionID? {
    static var selectedSectionID: Self { Self() }
}

struct Menu: Component {
    // Environment values can be retrieved using the EnvironmentValue
    // property wrapper (which is the Plot equivalent of SwiftUI's
    // Environment wrapper):
    @EnvironmentValue(.selectedSectionID) private var selectedSectionID

    var body: Component {
        Navigation {
            List(SwiftBySundell.SectionID.allCases) { sectionID in
                Link(
                    sectionID.menuTitle,
                    url: sectionID.url
                )
                .class(classForSection(withID: sectionID))
            }
        }
    }

    private func classForSection(
        withID id: SwiftBySundell.SectionID
    ) -> String {
        // We can now use our environment value to make decisions
        // within this component — in this case in order to determine
        // whether a given menu item should be marked as selected:
        id == selectedSectionID ? "selected" : ""
    }
}

So while SwiftUI offers several different ways to interact with its environment API (through the Environment and EnvironmentObject property wrappers, the EnvironmentKey protocol, and so on), I chose to implement a simplified version that still offers enough power to be incredibly useful within the context of static site generation. After all, it wouldn’t make much sense for me to implement support for things like observable objects, since (unlike SwiftUI) Plot is not generating any dynamic views that need to be updated according to state changes.

Support Swift by Sundell by checking out this sponsor:

Instabug

Instabug: Whether it’s crashes, slow screen transitions, delayed network calls, or unresponsive UIs — Instabug automatically gives you all of the logs you need to fix bugs and issues, and to ship high-quality apps. Get started now.

Conclusion

So, essentially, this new version of Plot is my take on a SwiftUI-like API that’s specifically made for generating static HTML. It’s not meant to be a replacement for dynamic client-side web frameworks (like React or Vue.js), nor does it offer any APIs for applying custom styles to its components (that still has to be done using CSS).

In the future, I do hope to be able to continue to extend Plot to also support CSS generation, but that’s a project for another day. In the meantime, I’m incredibly excited to share this new component-based API with you and the rest of the Swift community. I hope that you’ll find it useful, and like our friends at Apple always like to say — I can’t wait to see what you’ll build with it!

If you’re looking for more examples of how this new API can be used, check out Plot’s README, or the new version of Publish, which has also been updated to support this new component-based API. In fact, all of Publish’s built-in Foundation theme has been rewritten using this new API. And if you’re an existing user of either Plot or Publish, then you just have to upgrade to the latest version — this new API is fully backward compatible, so you shouldn’t encounter any breaking changes.

I hope you’ll enjoy this new version of Plot, and feel free to send me either a tweet or an email if you have any questions, comments, or feedback.

Thanks for reading!