swift posts

Subscribe to just swift posts via RSS.

Your View's Lifetime Is Not Yours

When SwiftUI was announced in 2019 (oh boy, more than 5 years ago), one of the big things that the Apple engineers emphasized was that a SwiftUI View is not like a UIKit/AppKit view. As Apple was at pains to note, SwiftUI may evaluate the body property of a View arbitrarily often. It’s easy to miss an important consequence this has, though: your View will also be initilaized arbitrarily often. This is why SwiftUI views are supposed to be structs which are simple value types and can be stack-allocated: constructing a View needs to be cheap.

More precisely, what I mean by the title of this post is that the lifetime of a struct that conforms to View is unmoored from that of the conceptual thing representing a piece of your user interface.

Parsing HTML Slower

Last time, I wrote about how to parse HTML and convert it to NSAttributedStrings quickly. Unfortunately, in the time since then, it’s gotten slower. It’s still a good deal faster than it was before all that work, mind you. At fault is not any of the optimizations I discussed last time, fortunately. Rather, to get the correct behavior across a whole slew of edge cases, there was more work that needed to be done.

The root of all this complexity is the fact that I’m essentially trying to replicate a portion of the CSS layout algorithm using only the information provided by the HTML tokenization process (that is, the text that is emitted and the start/end tags) while flattening into a single string all the structure used to achieve those results.

Parsing HTML Fast

The urge to overengineer something recently befell me, and the target of that urge was the way Tusker handles HTML parsing. Mastodon provides the content of posts (and profile descriptions, and some other things) as HTML, which means that, in order to display them properly, you need to parse the HTML, a notoriously straightforward and easy task. For a long time, the approach I took was to use SwiftSoup to parse the HTML and then walk the resulting node tree to build up a big NSAttributedString that could be displayed.

And while that technique worked perfectly well, there were a number of downsides that I wasn’t entirely satisfied with. First: SwiftSoup alone is a big dependency, at around ten thousand lines of code. Since it’s shipping in my app, I take the view that I’m responsible for making sure it doesn’t break—and that’s a lot of surface area to cover (moreover, since it was ported from a Java library, the code is extremely un-Swift-y). Second: it’s not especially fast. Partly because of its aforementioned nature as a Java port (everything is a class, lots of unnecessary copies), but primarily because it’s just doing more than I need it to (the first rule of optimization is do less work): it was parsing a string into a tree and then I was flattening that tree back into a string.

The last time I needed to do something similar, I used lol-html, Cloudflare’s streaming HTML rewriter. But, while that worked (and is still in use for my RSS reader app), it’s written in Rust, and I didn’t want to introduce significant additional complexity to the build process for Tusker. So, writing a new HTML parser in Swift seemed like a great idea.

Theming iOS Apps is No Longer Hard

Well, at least not for the same reasons. I figured I’d write a brief follow-up post, but unless you’ve been living under a rock, you’ll have heard that UIKit gained support for custom traits in UITraitCollection with iOS 17. They work very similarly to SwiftUI’s environment and are exactly what I wanted.

A Hero View Controller Transition in SwiftUI

Out of the box, SwiftUI has a matchedGeometryEffect modifier that makes it relatively easy to build hero transitions (a style of transition where a new screen is presented and part of the source screen changes position and size to reach it’s place on the new screen). It’s cool that SwiftUI includes this out of the box, but unfortunately it has a few limitations that make it unsuitable for certain use cases. Particularly for me, that it doesn’t work with presenting another view. Most examples on the internet[1] work around this by faking a custom presented view: just slap a full-screen background color down and show your content on top of it. That’s essentially the same as a presenting a full-screen view, with the one major caveat that the pseudo-presented view can only cover the containing hosting controller. And if that controller isn’t full-screen (say, if it’s presented as a sheet), you can’t present anything that’s truly full-screen. So, let’s build a custom hero transition that actually presents it’s content across the entire screen.