AWS Launches Multi-session Support

The AWS Management Console now supports simultaneous sign-in for multiple AWS accounts. I'm a bit slow to the trigger here, but I can't not share this because it's probably the most exciting feature AWS has launched in years.

Historically, managing several AWS identities in the same browser session was a huge pain. It's also a best practice to split infrastructure across several accounts, and it was not so easy to switch between them.

Here's how you set it up.

When you next log into AWS, you might see the following notification.

To turn it on, just click on your username in the top-right, and click "Turn on multi-session support".

A modal will appear to confirm this. Click "Turn on multi-session support".

Then, you'll be redirected to a new URL, and the top-right navigation will change a bit. It will show both your username and the account information. Before, it would just show the username.

Good job, AWS Management Console team! It's the small things that make all the difference.

Modern iOS Theming with UITraitCollection

Fairly recently (in iOS 17), Apple introduced a pretty nifty way to handle theme changes in iOS apps. It used to be kind of a pain to deal with this (as chronicled in several well-written blog posts from Christian Selig and Shadowfacts).

With the introduction of UITraitAppearance, things are no longer so bad.

This post is an end-to-end walkthrough of how to add theming in a real app. I'll also include sample code for a real project that you can download (or, you can just download it on GitHub right now if you just want to dive in).

Prerequisites

Before you use this approach, there are two things to keep in mind.

  • Your app must be targeting iOS 17 or later.
  • You must be using a scene delegate.

So, please confirm you're good to go on these before going further. Things won't work otherwise!

Now that we've gotten that out of the way, let's get started.

Understanding Trait Collections

Trait collections aren't really new in iOS; they've been around since iOS 8. So if you're an experienced iOS developer, you've probably had a few run-ins with this API.

But for those who might be newer, here's how the UIKit documentation defines a trait collection:

A collection of data that represents the environment for an individual element in your app’s user interface.

Cool. Let's read on.

The traitCollection property of the UITraitEnvironment protocol contains traits that describe the state of various elements of the iOS user interface, such as size class, display scale, and layout direction. Together, these traits compose the UIKit trait environment.

Now this is starting to feel a bit less abstract. UITraitCollection represents data about how things should look and behave, based on the specifics of the device itself and user preferences (dark mode is a good example).

One problem with UITraitCollection is that traits are immutable. How can we allow users to change themes if that's the case? This is where the UIMutableTraits protocol comes in (this is one of the key changes in iOS 17 that helps make this possible).

Here's what Apple says:

The UIMutableTraits protocol provides read-write access to get and set trait values on an underlying container. UIKit uses this protocol to facilitate working with instances of UITraitCollection, which are immutable and read-only. The UITraitCollection initializer init(mutations:) uses an instance of UIMutableTraits, which enables you to set a batch of trait values in one method call. UITraitOverrides conforms to UIMutableTraits, making it easy to set trait overrides on trait environments such as views and view controllers.

A little jargony, but this is saying that UIMutableTraits lets us manipulate traits on demand (seems useful 🤔).

All we need to do is to comform our trait to UITraitDefinition and then add getters and settings in a UIMutableTraits extension.

Defining Your Theme

We have all of the components we need, but how do you actually put it to use?

Let's start with a super simple theme that just has two variants (light and dark) and two properties (a foreground and background color).

We'll use use UIColor's dynamic initializer (init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)) to define our colors. From the docs:

Use this method to create a color object whose component values change based on the currently active traits. The block you provide creates a new color object based on the traits in the provided trait collection.

Perfect. Just what we need.

Theme.swift

enum Theme: Int {
  case light
  case dark

  var name: String {
    switch self {
    case .light: return "Light"
    case .dark: return "Dark"
    }
  }
  
  static var backgroundColor: UIColor {
    return UIColor { traitCollection in
      switch traitCollection.theme {
      case .light: return .white
      case .dark: return .black
      }
    }
  }
  
  static var foregroundColor: UIColor {
    return UIColor { traitCollection in
      switch traitCollection.theme {
      case .light: return .black
      case .dark: return .white
      }
    }
  }
}

Now create a trait definition.

All traits contained in a UITraitCollection conform to this protocol. You can create custom traits by defining your own conforming type.

Theme.swift (continued):

// ...

struct ThemeTrait: UITraitDefinition {
  typealias Value = Theme
  static let defaultValue = Theme.light
  static let affectsColorAppearance = true
  static var name: String = "theme"
  static var identifier = "com.company.theme"
}

Finally, extend UITraitCollection and add the theme to UIMutableTraits.

Theme.swift (continued):

// ...

extension UITraitCollection {
  var theme: Theme { self[ThemeTrait.self] }
}

extension UIMutableTraits {
  var theme: Theme {
    get { self[ThemeTrait.self] }
    set { self[ThemeTrait.self] = newValue }
  }
}

Using Your Theme

Now that we have our theme, we can now apply theme values to UI elements dynamically. It's really this simple:

view.backgroundColor = Theme.backgroundColor

But wait, you might have noticed we left something out: the default trait will clearly work, but how do we change it? To do that, we'll need to use trait overrides.

Trait Overrides

A trait override is a way to change a mutable trait on a trait collection (this is the thing that was recently introduced in iOS 17, that makes this all possible). You'll probably want to do this on application startup and on theme updates.

To handle the trait override on application startup, it'll need to be included it in the scene(_:willConnectTo:options:) method in the scene delegate (another reminder: the approach we're using requires scenes).

Here's what a full scene delegate implementation might look like:

SceneDelegate.swift:

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    guard let windowScene = scene as? UIWindowScene else { return }

    let window = UIWindow(windowScene: windowScene)
    let rootViewController = NavigationController(rootViewController: ItemListViewController())
    window.rootViewController = rootViewController
    // Key line! Set the theme here ⬇️.
    windowScene.traitOverrides.theme = ThemeStore.shared.getTheme()
    self.window = window
    window.makeKeyAndVisible()
  }
}

And here's the corresponding configuration that stores the theme. You may already have an existing method of persisting defaults, so please don't use this code; it's just an illustration.

ThemeStore.swift:

import UIKit

struct ThemeStore {
  static let shared = ThemeStore()

  private let userDefaults: UserDefaults
  private let themeKey = "theme"

  private init(userDefaults: UserDefaults = .standard) {
    self.userDefaults = userDefaults
  }

  func setTheme(_ theme: Theme) {
    userDefaults.set(theme.rawValue, forKey: themeKey)
  }

  func getTheme() -> Theme {
    Theme(rawValue: userDefaults.integer(forKey: themeKey)) ?? .light
  }
}

Lastly, here's how to change the theme dynamically. This can be done anywhere where the windowScene is accessible, such as in a UIViewController:

let oldTheme = ThemeStore.shared.getTheme()
let newTheme = oldTheme == .dark ? Theme.light : Theme.dark

guard let windowScene = view.window?.windowScene else { return }

// Update the current theme in the theme store and update the trait overrides.
ThemeStore.shared.setTheme(newTheme)
windowScene.traitOverrides.theme = newTheme

With this, the theme trait is updated immediately, and all views that depends on this trait will be immediately updated, like magic. ✨

Conclusion

I've only scratched the surface on the theming possibilities with the UITraitCollection APIs (heck, I didn't even mention SwiftUI!). But it should be enough to get you started.

If you want to keep going, here's a few other resources I'd recommend:

Or, Just Ignore Everything I Just Wrote and Download the Sample Code

Check out iOSTraitCollectionThemingExample on GitHub for an example app that demonstrates this technique in action.

The Cult of Done Manifesto

This morning, Bryce Roberts shared this short clip of a conversation between him and Soleio, one of the first designers at Facebook (now Meta). Soleio was reflecting how in the early days of Facebook, he'd go out for beers and read it out loud with the rest of the team. Do yourself a favor, and read it; it's awesome. If you're a builder, it should invigorate you.

From the Internet Archive (unfortunately, the original is no longer available):

Dear Members of the Cult of Done,

I present to you a manifesto of done. This was written in collaboration with Kio Stark in 20 minutes because we only had 20 minutes to get it done.

The Cult of Done Manifesto

  1. There are three states of being: not knowing, action, and completion.
  2. Accept that everything is a draft. It helps to get it done.
  3. There is no editing stage.
  4. Pretending you know what you're doing is almost the same as knowing what you are doing, so just accept that you know what you're doing even if you don't and do it.
  5. Banish procrastination. If you wait more than a week to get an idea done, abandon it.
  6. The point of being done is not to finish but to get other things done.
  7. Once you're done, you can throw it away.
  8. Laugh at perfection. It's boring and keeps you from being done.
  9. People without dirty hands are wrong. Doing something makes you right.
  10. Failure counts as done. So do mistakes.
  11. Destruction is a variant of done.
  12. If you have an idea and publish it on the internet, that counts as a ghost of done.
  13. Done is the engine of more.

I Ditched Jekyll and Built a Static Site Generator Based on Go and SQLite

I've got quite the yak shave to share (have you shaved a yak before? If not, I highly recommend it).

It goes like this:

You start with the desire to wax your car.

To wax your car, you need a water hose. Only, your water hose is busted so you need to go down to the hardware store to get a new hose.

To get to the hardware store, you have to drive across a bridge. The bridge requires a pass or ticket. You can't find your pass, but you know your neighbor has one.

However, your neighbor won't lend you his pass until you return a pillow that you borrowed. The reason you haven't returned it is because the pillow is missing some stuffing.

The pillow was originally stuffed with yak hair. In order to re-stuff the pillow you need to get some new yak hair.

And that's how you end up shaving a yak, when all you really wanted to do was wax your car.

In summary: I had an interesting idea I wanted to write in long form (not on Twitter!), but it had been quite a while since I wrote a blog post. Unfortunately, so many years had passed since my previous blog post that I couldn't even get Jekyll to work. So, naturally, I ended up building a static site generator.

But First, Some History

My first blog was based on WordPress. It served as my journal while I studied abroad in China in 2007.

It worked pretty well! However, it used a gnarly cPanel-based "infrastructure" and was hosted on a fairly iffy host called midPhase (which is still around, somehow?).

For one reason or another, I decided to not renew my hosting plan with them, so they shut down my account and deleted all of my content. Years later, I regretted this decision.

Fortunately, I had been syndicating all of the content of that blog through Feedburner, and I was able to extract all of the posts I made using their RSS feed. I lost my photos, though.

Aside: I'm not really sure what happened to Feedburner, but whatever it is now, was not what it used to be. I just logged in and don't really understand what it is.

Several years later (2009/2010), I started blogging again, albeit with some infrequency, on Blogger. I posted only a few times there, but it still lives. I haven't had the heart, or desire, to take it down.

The Age of Jekyll

At some point soon thereafter (2010-2011ish), I must have gotten annoyed with having all of my content owned and hosted by a third-party (having been burned by my experience with my original WordPress blog), and also wanted to try out a cool new static-site generator called Jekyll written by Tom Preston-Werner. This post from Chris Parsons is emblematic of the Jekyll zeitgeist in late 2009 (HN submission here).

At the time, Jekyll was also the only supported service for deploying static sites to GitHub Pages. This made the setup process very straightforward and easy.

For the next several years, writing new posts was a breeze; create new markdown document. Save. Commit. Push. Published.

About a year later, I decided to add a gem or two that wasn't compatible with GitHub Pages, so as a result, I migrated everything to CloudFront and S3. That was fun.

And then I took a little bit of a blogging break.

Oops.

What I hadn't considered was that Ruby is not a platform or language that cares too much about backwards-compatibility. I also didn't consider that Jekyll was early-stage software and would change a lot (anyone could have predicted this 😬).

To be fair, this wasn't a problem for quite a while. But as the years piled on, the Ruby ecosystem's penchant for breaking changes started to slowly bite me: Jekyll 4 was released in 2019. And then Ruby 3 came out in 2020. And soon enough I realized I literally couldn't publish a new blog post without upgrading >10 gems (iykyk).

So I just stopped!

Which, when you think about it, is kind of wild: I stopped blogging because I literally couldn't run bundle exec jekyll build. And it just wasn't going to be worth the time to migrate everything to the latest versions. I kept pushing it off, and, before you know it, several years had passed by.

Durable, Portable, and Easy-to-understand

In the last two weeks, I decided enough was enough. I needed to make the call on whether to nuke my entire setup, or try and figure out a migration strategy to a new system. Jekyll just wasn't going to cut it; even if I fixed the issues now, in just a few years this entire problem would resurface just like a game of whack-a-mole.

Not to mention, I also have a few other static sites running Jekyll, all with the same issues, and they would need to be moved too. But what system to migrate to? Obviously, it's never anyone's immediate thought to build their own static site generator, so I wasn't going to do that. I'm not that unhinged (or am I?).

So I came up with a list of good alternatives, and I literally ported my blog to each one of them. Aside: I still kind of can't believe I did that.

With some human assistance, ChatGPT threw together this feature matrix, which does a fairly good job of summarizing the pros/cons between the options I landed on:

FeatureHugoVite / React RouterAstro
Speed🟢 Fastest🟠 Moderate (JavaScript-heavy)🟢 Fast (optimized builds)
Mental Overhead🟢 Low (simple templates)🟠 Moderate (React concepts, routing)🟢 Low (modern, intuitive workflow)
Interactivity🔴 Limited🟢 Full React SPA/MPA🟢 Hybrid (JS islands)
Flexibility🟢 Content-focused🟢 Component-based SPA🟢 Best of both worlds
Community Support🟢 Mature🟢 Mature (React ecosystem)🟠 Growing
Future Proof🟢 Stable and reliable🟠 Moderate (npm/Node issues)🟠 Moderate (npm/Node issues)
SEO Optimization🟢 Excellent (purely static)🟠 Requires extra effort (SSR or hydration)🟢 Excellent (static-first with flexibility)

So that feature matrix is maybe helpful, but my actual experiences went like this:

Hugo

  • Converting templates was frustrating since error messages were hard to understand.
  • Documentation was extensive but poorly organized. I often found myself digging through pages of search results to uncover how to do simple things, like how to display an image.
  • Tons of mental overhead with respect to how templates are actually rendered. See their documentation on template lookup order as an example.
  • Unclear when to use shortcodes versus variables / other alternatives.
  • Tons of boilerplate to do simple things, such as including a CSS file. E.g.:
    {{ $opts := dict "transpiler" "libsass" "targetPath" "css/style.css" }}
    {{ with resources.Get "sass/main.scss" | toCSS $opts | minify | fingerprint }}
      <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
    {{ end }}
    

Vite / React Router

  • I'm super familiar with this stack, so getting set up was quick.
  • Had to roll my own post management system, and there wasn't anything off the shelf that would essentially work as a CMS.
  • The site was fast to compile and run.
  • Even though I could prerender all of the pages, my prediction is that SEO would take a hit due to the sloppy HTML.
  • Based on the Node stack so I would expect the entire thing to require an upgrade in a few years. :(

Astro

  • Heard tons of good things about this setup lately so I decided why the heck not.
  • I thought the entire frontmatter architecture, where TypeScript code lives at the top of .astro files, was very weird.
  • Didn't love the custom file format (".astro"). Requires an editor plugin to read them correctly and none of my auto formatting tools worked out of the box.
  • It was fast-ish.
  • Also built on the Node stack, and a newer technology, so inevitably I'd run into some issues in the future.

Recap

In the end, Hugo's documentation and verbosity ultimately scared me away. Vite/React Router was nice, but it wouldn't be great SEO and I'd have to deal with Node.js. Astro was a nice experience, but I thought the entire .astro file format and the somewhat proprietary "feeling" of the stack to be a turnoff. And being JavaScript-ecosystem-based as well, I'd still have to deal with the future-proofing thing.

Maybe I'd have to settle. But there was something I realized in the process of trying all of these services out that led me down a unexpected path. First, let's review how I ported things over.

Porting

Trying out a new static site generator involved several steps:

  1. Read docs for 15m+ to learn the basics.
  2. Setup stack.
  3. Copy all markdown files from old Jekyll setup to new location.
  4. Copy all templates to from old Jekyll setup to new location.
  5. Copy static files.
  6. Run dev server and iterate for several hours until all posts rendered somewhat correctly.
    • This involved much searching and replacing, and repeated updates. Even with AI assistance, this was a slog.

So that was fun. At some point, while porting to Astro, I realized it could plug into a custom loader, and read from a database. "That's interesting", I thought. "Could I just port all of my blog posts to a database, and then make text edits simply by running a SQL query?" Well, yes. I could.

And then, if I ever needed to move to a different static-site generator in the future, I could just keep using the same database file to regenerate the files or read them from a loader. Sweet. Maybe Astro is the move!

So I wrote a little script that converted all of my markdown files to a SQLite database, and started writing some code in Astro to read those posts. But after about an hour of wrangling with the docs, I couldn't get it to work! It was just too confusing. At this point I realized this was not sustainable. If I took a month break from blogging this was not going to be something I was going to remember.

I felt lost and frustrated. What was I to do?

But then I stepped back, and I realized that my entire mental model of how a static site generator should work was off. I had been thinking about it completely wrong.

Logic != Data

Blog posts are data! Static pages are data! Templates, however, are code. They contain "business logic". Business logic belongs in code.

Data belongs in a database. Why am I storing my blog posts in files, and treating them like code? They should live in a database that any static site generator can read from. That way my data is permanent and not tied to my static site generator. That's how it should be.

Weirdly, though, not many (any?) popular static site generators use a database as a backend. Pretty much everyone uses flat files. Which is kinda nice (you can see diffs!) but honestly, I don't really care about diffs. I just want my blog to work, be stable, and my data to be portable.

At this point I realized where I was headed: I was going to need to build a static site generator.

It would need to be based on technology that would be permanently future proof, with a data backend that would never become outdated.

A Wild Static Site Generator Appears

A Wild Static Site Generator Appears

The fact that Hugo was built on Go gave me some hints about what direction to take this in, and I'd built much software in Go that still worked without changes for over a decade. So the programming language choice was clear: I would build it in Go.

For the data side, the choice was also quite clear since I already had a SQLite database from my Astro experiment with all of my blog posts and pages. I didn't even need to do any additional work there. There was some schema and data modification, sure, but it just involved a few SQL queries and I was set.

Some Notes on SQLite

First of all, SQLite is just awesome. It's incredible software. It can literally do anything. Ok, maybe not, but it's really flexible and solid.

One thing you may not know is that SQLite is a great "filesystem". It can store files super efficiently, and there's even a tool written by the SQLite authors called sqlar which is effectively equivalent to ZIP in terms of performance and space requirements, with the added benefit that you can treat the entire archive as a database!

You might be wondering: "Dan, can you really store a SQLite database in a Git repo?" The answer is yes.

I had an intuition that Git's delta compression would store SQLite quite efficiently, and based on my own research and that of others, it turns out that I was right: Git excels at storing SQLite databases in version control. It's on par, even, with plain text.

Add in a custom diff handler, specify which filetypes should get treatment with .gitattributes, and we now have a setup that shows diffs and is as space-efficient as storing raw markdown files.

Go

Go is remarkably solid. I've written programs in Go over a decade ago that run without modification to anything, even the build system.

And that's expected, as one of Go's core design principles is backwards compatibility. From the linked document:

In the quoted text from “Go 1 and the Future of Go Programs” at the top of this post, the ellipsis hid the following qualifier:

At some indefinite point, a Go 2 specification may arise, but until that time, [… all the compatibility details …].

That raises an obvious question: when should we expect the Go 2 specification that breaks old Go 1 programs?

The answer is never. Go 2, in the sense of breaking with the past and no longer compiling old programs, is never going to happen. Go 2 in the sense of being the major revision of Go 1 we started toward in 2017 has already happened.

There will not be a Go 2 that breaks Go 1 programs. Instead, we are going to double down on compatibility, which is far more valuable than any possible break with the past. In fact, we believe that prioritizing compatibility was the most important design decision we made for Go 1.

So what you will see over the next few years is plenty of new, exciting work, but done in a careful, compatible way, so that we can keep your upgrades from one toolchain to the next as boring as possible.

How fricking awesome is that? Whatever you write in Go, will continue to work, forever.

Go does have its own warts, but so does every language, and one must prioritize needs in order to make software design decisions, and in my case, reliability and future proofing trumped almost everything else.

Go is also very simple. Simple is good!

Doing The Thing

So yeah, I built a static site generator using Go, using SQLite as the database, stored in version control, and it's now running this very blog, right now. I wrote a GitHub action that generates the pages (takes <20s to build and deploy the entire site!) and it's now hosted on GitHub Pages.

GitHub Actions

I even wrote a little editor that runs when I run the site locally using go run, so I can edit posts in the browser instead of using a database management tool (I got tired of that quite quickly!).

I'm super happy with how everything turned out. Dependency management is a breeze, and the entire thing is blazing fast. It preprocesses SCSS and code blocks, and minifies images, CSS, HTML, and JavaScript.

There are obviously a lot of rough edges given that this is pre-alpha software, but those will be smoothed out soon enough as I port a few other websites over to the new system.

Some things I want to do next:

  • Allow an entire website to be bundled as a single executable file, with all static content included, so it can run as a full web server. Is there something like Cosmopolitan, but for Go?
  • Make it go installable so it can just work as a binary.
  • Write a standard spec for the database schema that will handle most static sites.
  • Turn it into a library so I can simply import it into another Go file to extend behavior.
  • Improve the editing experience, using something like Editor.js.
  • Open source it. 🎉

All in all, this was a fun learning experience and I think I built something pretty cool. I'm really excited to migrate my other websites and see my work pay off (I'm really hoping this will be the last time! 😅🤞🏻).

Creativity Inc.

This is a list of my 7 favorite things about the book Creativity, Inc.: Overcoming the Unseen Forces That Stand in the Way of True Inspiration, by Ed Catmull, co-founder of Pixar Animation.

  1. Ed Catmull is a fantastic writer and it was a pleasure to read.
  2. There is a formula to encourage creativity in large organizations.
  3. The portrait of Steve Jobs was eye-opening, and Ed’s interactions with him reflected him in a more realistic light.
  4. Creative success can take decades.
  5. Creative organizations require constant conflict to succeed.
  6. What might have worked in the past will probably not work in the future.
  7. Creators rarely can predict what their inspiration will lead to.