10 months ago

Integrating Tailwind into your Swift projects

In recent years, Tailwind has gained popularity as a web styling tool. Frameworks such as Ruby on Rails and Phoenix default to using Tailwind for new projects. If you’re unfamiliar with Tailwind, it provides a set of well-defined and configurable utility classes that ensure consistent styling and allow for atomicity of markup and style. While JavaScript-based UI solutions like React or Vue already offer this functionality, they rely on the JavaScript ecosystem of tools and packages. Tailwind, on the other hand, brings these benefits to other ecosystems without introducing additional dependencies. It accomplishes this through a simple CLI that runs during build time and outputs CSS, removing any unused classes from the project. Essentially, using Tailwind doesn’t require developers to install new system dependencies like NodeJS, making it easier to contribute to a project.

When I returned to working with Swift, I noticed that the Swift ecosystem lacked an easy way to integrate Tailwind into Swift on the Server projects, such as those based on Vapor or Publish. To address this, I took it upon myself to create a new Swift package called SwiftyTailwind. It allows for the lazy downloading and execution of the Tailwind CLI using system processes. Below, you’ll find an example of how to use SwiftyTailwind:

let tailwind = SwiftyTailwind()
try await tailwind.run(input: .init(validating: "/app/app.css"), output: .init(validating: "/app/build/app.css"))

Integrating it with Publish

Integrating SwiftyTailwind is a breeze with the use of plugins. Simply instantiate and pass a plugin, and Publish will execute the code within the closure, generating the CSS file at ./Output/output.css. Remember to include the necessary code in the <head> tag of your website to load these styles.

try Site().publish(withTheme: .tailwind, plugins: [
    .init(name: "Tailwind", installer: { context in
        let rootDirectory = try! AbsolutePath(validating: try context.folder(at: "/").path)
        try await tailwind.run(input: rootDirectory.appending(components: ["Style", "input.css"]),
                               output: rootDirectory.appending(components: ["Output", "output.css"]))
    })
])

Integrating it with Vapor

Integrating SwiftyTailwind with a Vapor project is a seamless experience. To begin, you need to configure the app to serve static assets located in the default Public directory by Vapor. Here’s an example of the code:

app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))

Next, create an instance of SwiftyTailwind and invoke it by providing the file Resources/Styles/app.css as input. Additionally, set the options watch and content to point to Resources/Views/**/*.leaf. Take a look at the code example below:

// Tailwind
let publicDirectory = try AbsolutePath(validating: app.directory.publicDirectory)
let inputCSSPath = try AbsolutePath(validating: app.directory.resourcesDirectory).appending(.init("Styles/app.css"))
let outputCSSPath = publicDirectory.appending(component: "app.css")
async let runTailwind: () = try tailwind.run(input: inputCSSPath, output: outputCSSPath, options: .watch, .content("Resources/Views/**/*"))
async let runApp: () = try await app.runFromAsyncMainEntrypoint()

_ = await [try runTailwind, try runApp]

Finally, update the <head> section of your views to load the generated app.css file from the Public directory. Here’s an example:

<link rel="stylesheet" href="/app.css">

And that’s it! SwiftyTailwind will monitor file changes in your Resources/Views directory and automatically regenerate the CSS output.

You can check out the Examples directory in the project repository.

Contributing to Swift on the Server

When compared to programming languages that have a more established presence in web development, Swift’s ecosystem is still in its early stages. However, I am determined to contribute to changing that by building and sharing utilities with the community. Tailwind was my initial contribution, but I also have plans to introduce ESBuild and Orogene to Swift. These additions will enable a more advanced build pipeline in Vapor and facilitate fetching Node dependencies without relying on the NodeJS runtime. Exciting developments are on the horizon, so stay tuned for more updates!

About Pedro Piñera

I created XcodeProj and Tuist, and co-founded Tuist Cloud. My work is trusted by companies like Adidas, American Express, and Etsy. I enjoy building delightful tools for developers and open-source communities.