<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Swiftui on Man-You</title><link>https://man-you.ringum.net/tags/swiftui/</link><description>Recent content in Swiftui on Man-You</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 24 Mar 2026 09:30:00 +0100</lastBuildDate><atom:link href="https://man-you.ringum.net/tags/swiftui/index.xml" rel="self" type="application/rss+xml"/><item><title>The most useless way to port a macOS app</title><link>https://man-you.ringum.net/posts/clone-desktop/</link><pubDate>Tue, 24 Mar 2026 09:30:00 +0100</pubDate><guid>https://man-you.ringum.net/posts/clone-desktop/</guid><description>&lt;p&gt;I grew up fascinated by projects like GNUStep, Haiku, Etoile, Wine, and ReactOS. Engineering feats, all of them. They reverse-engineer or reimplement entire operating system APIs so that software written for one platform can run on another. And they almost always end up in the same place: impressive technically, starved for contributors, forever chasing a moving target they can never quite catch.&lt;/p&gt;
&lt;p&gt;I never liked the state of the Linux desktop either. Not because it&amp;rsquo;s bad per se, but because it&amp;rsquo;s fragmented. A KDE app on GNOME looks alien. Firefox rolls its own everything. GTK and Qt will never agree on anything. Every toolkit draws its own widgets, manages its own text rendering, handles its own accessibility story. The result is a desktop that feels like a coalition of independent projects rather than a coherent system.&lt;/p&gt;
&lt;p&gt;This project is not going to fix any of that. But it&amp;rsquo;s an interesting story about how a combination of prior work, Apple open-sourcing key components, and an AI pair programmer led me down a rabbit hole I didn&amp;rsquo;t plan to enter.&lt;/p&gt;
&lt;h2 id="the-prior-art-that-made-this-possible"&gt;The prior art that made this possible&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been working with wgpu (Rust&amp;rsquo;s WebGPU implementation) for a while now, building a &lt;a href="https://man-you.ringum.net/backroom/woosmap-tiles/"&gt;map renderer&lt;/a&gt; for Woosmap. That project taught me the fundamentals: how to manage GPU pipelines, how to do instanced rendering, how to deal with text atlases and glyph rasterization, how to bridge Rust and Swift through UniFFI.&lt;/p&gt;
&lt;p&gt;On the Swift side, I had two apps: &lt;strong&gt;Tunes&lt;/strong&gt;, a music player, and &lt;strong&gt;Leela&lt;/strong&gt;, an internal management tool for Woosmap services. Both are SwiftUI apps, both depend on Apple&amp;rsquo;s frameworks in the usual way.&lt;/p&gt;
&lt;p&gt;So one evening, half-curious and half-joking, I fed Claude the source of those projects alongside some context about GNUStep and friends, and typed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;What would it take to make Tunes build and run on Linux?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then things got out of control.&lt;/p&gt;
&lt;h2 id="what-claude-came-back-with"&gt;What Claude came back with&lt;/h2&gt;
&lt;p&gt;The answer was, predictably, &amp;ldquo;a lot.&amp;rdquo; But the interesting part was the breakdown. SwiftUI is the main dependency, and SwiftUI is closed-source. But Swift itself is open-source. Swift Foundation is open-source. The Swift Package Manager works on Linux. So the gap is really:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A SwiftUI implementation (the view hierarchy, state management, layout engine, modifiers)&lt;/li&gt;
&lt;li&gt;Some AppKit shims (NSColor, NSAppearance, the bits that SwiftUI still leans on)&lt;/li&gt;
&lt;li&gt;A rendering backend that isn&amp;rsquo;t Core Animation or Metal&lt;/li&gt;
&lt;li&gt;A compositor to manage windows, menus, and the desktop chrome&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Instead of stopping at &amp;ldquo;that&amp;rsquo;s insane, don&amp;rsquo;t do it,&amp;rdquo; I kept going. Claude kept going. We started building.&lt;/p&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The project (codenamed &lt;strong&gt;Clone&lt;/strong&gt;) splits pretty naturally along a language boundary.&lt;/p&gt;
&lt;p&gt;Rust does what Rust is good at: GPU work. A wgpu renderer with pipelines for rectangles, rounded rects (SDF-based, because I can&amp;rsquo;t stop using SDFs apparently), shadows, text via a glyph atlas, and wallpapers. It also runs the window compositor through winit.&lt;/p&gt;
&lt;p&gt;Swift does what Swift is good at: UI. A from-scratch SwiftUI implementation (about 50 View types, &lt;code&gt;@State&lt;/code&gt;/&lt;code&gt;@Binding&lt;/code&gt;/&lt;code&gt;@Environment&lt;/code&gt;, &lt;code&gt;ViewBuilder&lt;/code&gt;, layout, modifiers), the whole declarative stack. Plus enough AppKit shims to keep real apps happy, a SwiftData reimplementation backed by SQLite, and an IPC protocol over Unix sockets.&lt;/p&gt;
&lt;p&gt;UniFFI bridges the two. Each frame, Rust asks Swift for render commands, Swift resolves the view tree into a flat list of positioned primitives, and Rust batches them into instanced GPU draws. It&amp;rsquo;s the same bridge I use in the map renderer, so at least that part wasn&amp;rsquo;t new territory.&lt;/p&gt;
&lt;pre class="mermaid"&gt;graph LR
A["App.body"] --&gt; B["ViewBuilder"] --&gt; C["_resolve()"] --&gt; D["Layout"]
D --&gt; E["CommandFlattener"] --&gt; F["IPC
CGFloat → Float"] --&gt; G["Rust batcher"] --&gt; H["wgpu draws"]
style A fill:#c4a7e7,stroke:#6e6a86,color:#191724
style B fill:#c4a7e7,stroke:#6e6a86,color:#191724
style C fill:#c4a7e7,stroke:#6e6a86,color:#191724
style D fill:#c4a7e7,stroke:#6e6a86,color:#191724
style E fill:#f6c177,stroke:#6e6a86,color:#191724
style F fill:#f6c177,stroke:#6e6a86,color:#191724
style G fill:#9ccfd8,stroke:#6e6a86,color:#191724
style H fill:#9ccfd8,stroke:#6e6a86,color:#191724
&lt;/pre&gt;
&lt;h2 id="first-signs-of-life"&gt;First signs of life&lt;/h2&gt;
&lt;p&gt;The moment I&amp;rsquo;ll remember is the grey rectangle. &amp;ldquo;Clone Desktop&amp;rdquo; in the center, a dock at the bottom with colored squares. I&amp;rsquo;d been at it for hours, wrestling with UniFFI bindings and layout math, and suddenly there it was: pixels on screen, drawn by Swift code, pushed through a Rust GPU backend, on something that was decidedly not macOS.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-1_hu_60da9db4b46e573c.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-1_hu_60da9db4b46e573c.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-1_hu_cd2b29062a903b83.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="The first Clone Desktop render: a grey surface, centered label, and a color-swatch dock"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Day one: a grey box, a label, and a dock. It&amp;#39;s not much, but it compiles and renders.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="settings-and-finder"&gt;Settings and Finder&lt;/h2&gt;
&lt;p&gt;Once the basic views worked (stacks, text, lists, navigation), I needed something real to throw at them. Settings was a natural first target: sidebars, forms, toggles, text fields, the kind of layout variety that breaks things fast. Finder came next because browsing a directory with &lt;code&gt;List&lt;/code&gt; and &lt;code&gt;ForEach&lt;/code&gt; is such a fundamental SwiftUI pattern that if it didn&amp;rsquo;t work, nothing would.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-2_hu_c535fac1e06a428f.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-2_hu_c535fac1e06a428f.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-2_hu_d543a0938420ea6e.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Clone Desktop running Settings (Wi-Fi panel) and Finder side by side, dark mode"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Settings showing Wi-Fi preferences alongside Finder browsing the home directory. Both are SwiftUI apps running through Clone&amp;#39;s stack.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Dark mode just kind of happened. Once I shimmed &lt;code&gt;NSAppearance&lt;/code&gt; and implemented the semantic color system (&lt;code&gt;Color.primary&lt;/code&gt;, &lt;code&gt;.secondary&lt;/code&gt;, the system grays), flipping between light and dark was a toggle. A small thing, but unreasonably satisfying: it made the whole experiment feel like a real desktop for the first time.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-3_hu_7656abf32f2c59d8.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-3_hu_7656abf32f2c59d8.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-3_hu_477ea4514a69efe6.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Settings showing the General panel in light mode"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;The same Settings app in light mode. Appearance switching works through the reimplemented NSAppearance and Color system.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="tunes"&gt;Tunes&lt;/h2&gt;
&lt;p&gt;This was the whole point, remember? &amp;ldquo;What would it take to make Tunes build and run on Linux?&amp;rdquo; Well, it builds. The login sheet renders inside the app window rather than as a separate &lt;code&gt;NSPanel&lt;/code&gt; (a compromise, but not a terrible one), and the SwiftUI code is essentially unchanged. &lt;code&gt;TextField&lt;/code&gt;, &lt;code&gt;SecureField&lt;/code&gt;, &lt;code&gt;Toggle&lt;/code&gt;, &lt;code&gt;Button&lt;/code&gt;, &lt;code&gt;Link&lt;/code&gt;: they all resolve and render. Same source, different universe.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-4_hu_5ff44dcb7735661f.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-4_hu_5ff44dcb7735661f.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-4_hu_b08f4d867416f64c.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Tunes showing a login form with username/password fields, a toggle, and a registration link, alongside a Finder window"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Tunes running its login flow through Clone. The sheet renders in-window rather than as a separate panel, one of many compromises.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="leela"&gt;Leela&lt;/h2&gt;
&lt;p&gt;Leela is a management dashboard for Woosmap services: tabs, lists, nested navigation, version selectors, deploy queues. Most of the UI is driven by API responses, so it hammers &lt;code&gt;ForEach&lt;/code&gt; with dynamic data, &lt;code&gt;@StateObject&lt;/code&gt;, and conditional rendering. If Settings was a stroll through the park, Leela was a stress test.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/clone-desktop/clone-5_hu_2145acbb1b59b75f.webp"
srcset="https://man-you.ringum.net/posts/clone-desktop/clone-5_hu_2145acbb1b59b75f.webp 960w, https://man-you.ringum.net/posts/clone-desktop/clone-5_hu_a5c8020ac9f97b4f.webp 2784w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Leela showing a service management view with sidebar navigation, tab bar, version tags, and service listings"
width="960"
height="651"
loading="lazy"
decoding="async"
/&gt;
&lt;figcaption&gt;Leela&amp;#39;s services view running through Clone. Tabs, lists, version badges, sidebar navigation, all SwiftUI, all reimplemented.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;I won&amp;rsquo;t pretend it was smooth. Getting here meant implementing a surprising amount of SwiftUI&amp;rsquo;s surface area. The state management system alone (&lt;code&gt;StateGraph&lt;/code&gt;, scoped identity for &lt;code&gt;ForEach&lt;/code&gt;, call-index disambiguation) went through several iterations where everything would render once and then silently stop updating. The kind of bug where you stare at a diff for an hour before realizing a closure captured a copy instead of a reference.&lt;/p&gt;
&lt;h2 id="under-the-hood"&gt;Under the hood&lt;/h2&gt;
&lt;h3 id="text-rendering"&gt;Text rendering&lt;/h3&gt;
&lt;p&gt;Text goes through cosmic-text for shaping, then gets rasterized into a 4096x4096 glyph atlas (single-channel, R8). Each glyph is cached and rendered as an instanced GPU quad. Fonts are bundled: Inter for UI text, Phosphor for icons.&lt;/p&gt;
&lt;p&gt;This is almost certainly not how Apple does it. A real system would share GPU textures between the compositor and app, or pull from a system font cache. But it works, and there&amp;rsquo;s a special joy in watching a glyph atlas fill up character by character as the UI renders for the first time.&lt;/p&gt;
&lt;h3 id="layout"&gt;Layout&lt;/h3&gt;
&lt;p&gt;I reverse-engineered SwiftUI&amp;rsquo;s layout by reading &lt;a href="https://www.objc.io/books/thinking-in-swiftui/"&gt;objc.io&amp;rsquo;s&lt;/a&gt; thinking-in-swiftui and a lot of trial and error. The model: parents propose a size to children, children report back what they need, parents position them. Sounds simple until &lt;code&gt;ZStack&lt;/code&gt; enters the picture: nil-sized views from &lt;code&gt;.background()&lt;/code&gt; modifiers would expand to fill constraints and eat space from real siblings. That one took a while.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ScrollView&lt;/code&gt; was another head-scratcher: it fills its proposed size but lays out content unbounded in the scroll axis. Getting that inversion right, where the container constrains in one direction and is infinite in the other, broke my mental model twice before clicking.&lt;/p&gt;
&lt;h3 id="ipc"&gt;IPC&lt;/h3&gt;
&lt;p&gt;Each app is a separate process. The compositor (CloneDesktop) runs the Rust event loop and manages surfaces. Apps connect over a Unix socket at &lt;code&gt;/tmp/clone-compositor.sock&lt;/code&gt; and exchange length-prefixed JSON messages. There&amp;rsquo;s an annoying &lt;code&gt;CGFloat&lt;/code&gt;-to-&lt;code&gt;Float&lt;/code&gt; conversion at the wire boundary: Swift thinks in 64-bit coordinates, the GPU thinks in &lt;code&gt;f32&lt;/code&gt;, and someone has to reconcile that at the border.&lt;/p&gt;
&lt;h3 id="the-ycodebuild-trick"&gt;The &lt;code&gt;ycodebuild&lt;/code&gt; trick&lt;/h3&gt;
&lt;p&gt;This is probably my favorite hack in the project. To compile an existing macOS app against Clone instead of Apple&amp;rsquo;s frameworks, &lt;code&gt;ycodebuild&lt;/code&gt; generates a shadow SPM package that maps &lt;code&gt;import SwiftUI&lt;/code&gt; to Clone&amp;rsquo;s SwiftUI, &lt;code&gt;import AppKit&lt;/code&gt; to Clone&amp;rsquo;s shims, and so on. The app source code doesn&amp;rsquo;t change at all: you just build against a different package graph, and the compiled binary talks to the compositor over the socket. The same &lt;code&gt;.swift&lt;/code&gt; file, two completely different platforms.&lt;/p&gt;
&lt;h2 id="honest-assessment"&gt;Honest assessment&lt;/h2&gt;
&lt;p&gt;I should be upfront about the gap between &amp;ldquo;it renders&amp;rdquo; and &amp;ldquo;it works.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Animations are mostly stubbed. Accessibility is nonexistent. The compositor redraws every surface every frame like it&amp;rsquo;s 1997. There are &lt;code&gt;// TODO: implement&lt;/code&gt; scattered across the codebase where AppKit APIs return no-ops and hope nobody notices. Sheets render in-window because I never built the panel system. The glyph atlas will fall over the moment someone opens a CJK document.&lt;/p&gt;
&lt;p&gt;And (this is the awkward part) it doesn&amp;rsquo;t actually run on Linux yet. The whole premise was &amp;ldquo;what would it take,&amp;rdquo; and the answer turned out to include some Apple framework dependencies I haven&amp;rsquo;t replaced with their open-source equivalents. It&amp;rsquo;s doable. It&amp;rsquo;s just not done.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the thing that caught me off guard. The time from that initial Claude prompt to a state where Tunes and Leela compile and render recognizable UI was hours, not weeks. Not autonomous AI magic (plenty of manual fixes and architectural decisions along the way) but Claude carried an enormous amount of boilerplate: generating 50 View type stubs, wiring up modifier chains, implementing the state graph, setting up IPC. The kind of work that would&amp;rsquo;ve taken me days of tedious typing, compressed into a conversation.&lt;/p&gt;
&lt;h2 id="why-this-matters-a-little"&gt;Why this matters (a little)&lt;/h2&gt;
&lt;p&gt;Apple open-sourcing Swift and Foundation was a bigger deal than most people realize. Not because anyone&amp;rsquo;s going to ship a SwiftUI app on Linux tomorrow, but because it lowered the floor for experiments like this from &amp;ldquo;completely impossible&amp;rdquo; to &amp;ldquo;merely impractical.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The projects I admired growing up (GNUStep, Haiku, Wine) were built by small teams reverse-engineering closed systems over years. The combination of open-source language infrastructure and AI assistance compresses that timeline dramatically. Not to a point where it&amp;rsquo;s practical or production-ready, but to a point where a single person can explore the shape of the problem in a weekend.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the real takeaway. Not &amp;ldquo;I ported macOS to Linux.&amp;rdquo; I didn&amp;rsquo;t. But I went from a throwaway prompt to colored boxes on screen running real SwiftUI app code, and that felt like something worth writing about.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s next&lt;/h2&gt;
&lt;p&gt;Honestly? Probably nothing. The project scratched a twenty-year itch. But I know myself, and I know there&amp;rsquo;s a Kawase blur pipeline sitting unused in the renderer that&amp;rsquo;s going to call my name at 11pm some Tuesday. If I do keep going:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Per-window offscreen textures&lt;/strong&gt;: the compositor shouldn&amp;rsquo;t redraw everything every frame, that&amp;rsquo;s embarrassing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Glassmorphism / backdrop blur&lt;/strong&gt;: because what&amp;rsquo;s the point of reimplementing macOS if you can&amp;rsquo;t have the frosted glass&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actually running on Linux&lt;/strong&gt;: the whole original premise, still unfinished&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared GPU textures&lt;/strong&gt;: how a real compositor would work, instead of copying pixels through a socket like an animal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Or maybe it stays as a weekend experiment and a blog post. Either way, the kid who thought GNUStep was the coolest thing ever is pretty happy right now.&lt;/p&gt;</description></item><item><title>Tree: a browser that remembers where you came from</title><link>https://man-you.ringum.net/posts/tree-browser/</link><pubDate>Tue, 10 Feb 2026 10:00:00 +0200</pubDate><guid>https://man-you.ringum.net/posts/tree-browser/</guid><description>&lt;p&gt;Every browser has tabs. Linear, flat, disposable. Open thirty of them researching one topic and they sit in a row with no indication that twelve of them came from the same Wikipedia article. Close the wrong one and the context is gone.&lt;/p&gt;
&lt;p&gt;Tree is a macOS browser that replaces tabs with a tree. Every page knows its parent. Cmd+Click a link and it becomes a child node of the current page. The sidebar shows the full hierarchy: research paths branch naturally, and you can always trace back to where you started.&lt;/p&gt;
&lt;h2 id="the-data-model"&gt;The data model&lt;/h2&gt;
&lt;p&gt;The core is a SwiftData model with a self-referential parent-child relationship:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;Models/TreeNode.swift&lt;/figcaption&gt;
&lt;div class="highlight" title="Models/TreeNode.swift"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TreeNode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;Unique&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;([&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;faviconData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;lastUsedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isPinned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;sortOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inverse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deleteRule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The cascade delete rule means pruning a node takes its entire subtree with it. No orphans. The &lt;code&gt;parent&lt;/code&gt;/&lt;code&gt;children&lt;/code&gt; relationship is bidirectional. SwiftData maintains both sides automatically through the &lt;code&gt;inverse&lt;/code&gt; parameter.&lt;/p&gt;
&lt;p&gt;Each node tracks a &lt;code&gt;weight&lt;/code&gt; that increments on every visit, and an &lt;code&gt;isPinned&lt;/code&gt; flag. These feed the search ranking:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;searchRank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isPinned&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Pinned nodes get a massive boost. Frequently visited pages float to the top. It&amp;rsquo;s a simple heuristic but it works: the pages you care about surface first.&lt;/p&gt;
&lt;h2 id="branching"&gt;Branching&lt;/h2&gt;
&lt;p&gt;The key interaction is branching: Cmd+Click a link and the browser creates a child node instead of navigating away. The parent shifts to the secondary panel, and the child becomes the primary:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;State/NavigationState.swift&lt;/figcaption&gt;
&lt;div class="highlight" title="State/NavigationState.swift"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;selectFromBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;secondaryNode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;primaryNode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;primaryNode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;incrementWeight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;updateWebViewVisibility&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;saveLastSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;This is where the tree structure pays off. You&amp;rsquo;re reading documentation, Cmd+Click a linked API reference, and now you see both side by side: the original page on the right, the reference on the left. The sidebar shows them as parent and child. Navigate back up the tree anytime with keyboard shortcuts.&lt;/p&gt;
&lt;p&gt;Pinned nodes take this further. When a node is pinned, &lt;em&gt;all&lt;/em&gt; navigation from it creates children, not just Cmd+Click. Pin your project&amp;rsquo;s docs landing page and every link you follow branches automatically, building a research tree without any extra effort.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/tree-browser/tree-split-view-pr-review_hu_1624f2b46f7edfda.webp"
srcset="https://man-you.ringum.net/posts/tree-browser/tree-split-view-pr-review_hu_1624f2b46f7edfda.webp 960w, https://man-you.ringum.net/posts/tree-browser/tree-split-view-pr-review_hu_4b38275eb84f0fb6.webp 3050w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Split view showing a PR and its CI checks side by side, with the browsing tree in the sidebar"
width="960"
height="638"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="webview-management"&gt;WebView management&lt;/h2&gt;
&lt;p&gt;A browser can&amp;rsquo;t keep unlimited WebViews in memory. Tree uses an LRU cache that caps active WebViews at a configurable maximum (default 4):&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;WebKit/WebViewManager.swift&lt;/figcaption&gt;
&lt;div class="highlight" title="WebKit/WebViewManager.swift"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;evictIfNeeded&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;activeWebViews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxActive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;toEvict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accessOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;visibleNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;deallocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toEvict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;toEvict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accessOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;first&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;deallocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toEvict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The eviction prefers invisible nodes. If both panels are showing WebViews, those are protected: the cache evicts the least recently accessed node that isn&amp;rsquo;t currently on screen. Only when everything is visible does it fall back to strict LRU order.&lt;/p&gt;
&lt;p&gt;Before eviction, the WebView&amp;rsquo;s full browsing state gets persisted to disk.&lt;/p&gt;
&lt;h2 id="state-persistence"&gt;State persistence&lt;/h2&gt;
&lt;p&gt;WebKit exposes &lt;code&gt;interactionState&lt;/code&gt;, an opaque object that captures a WebView&amp;rsquo;s entire browsing state: history stack, scroll position, form data. Tree archives this to Application Support whenever a WebView is evicted, the app backgrounds, or the user closes a panel:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;WebKit/WebViewStateManager.swift&lt;/figcaption&gt;
&lt;div class="highlight" title="WebKit/WebViewStateManager.swift"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;saveState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WKWebView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactionState&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;NSKeyedArchiver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;archivedData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;withRootObject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;requiringSecureCoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;fileURL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateFileURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fileURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;When a node is revisited and needs a fresh WebView, the manager restores the archived state instead of loading the URL from scratch. The page appears exactly where the user left it: same scroll position, same back/forward history. The user never notices the WebView was recycled.&lt;/p&gt;
&lt;p&gt;A cleanup pass on launch removes orphaned state files for nodes that no longer exist in the database.&lt;/p&gt;
&lt;h2 id="the-sidebar"&gt;The sidebar&lt;/h2&gt;
&lt;p&gt;The sidebar is a recursive SwiftUI tree. Session roots (nodes with no parent) appear at the top level. Each node with children renders as a &lt;code&gt;DisclosureGroup&lt;/code&gt; with nested &lt;code&gt;TreeNodeRow&lt;/code&gt; views:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;Views/SidebarView.swift&lt;/figcaption&gt;
&lt;div class="highlight" title="Views/SidebarView.swift"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;Predicate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;sessionRoots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;displayedNodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;nodes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;navigationState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contextualRoot&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;sessionRoots&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;sorted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isPinned&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isPinned&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isPinned&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sortOrder&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sortOrder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Pinned nodes sort to the top. Below that, manual sort order (set via drag-and-drop reordering) controls the sequence.&lt;/p&gt;
&lt;p&gt;Drag-and-drop also handles reparenting: drag a node onto another node and it becomes a child. The drop handler checks for cycles (you can&amp;rsquo;t drop a parent onto its own descendant) by walking up the target&amp;rsquo;s ancestry chain:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;isDescendant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;ancestorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;parent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodeId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ancestorId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;h2 id="focus-mode"&gt;Focus mode&lt;/h2&gt;
&lt;p&gt;A deep tree gets unwieldy. Focus mode lets you temporarily treat any node as the root: the sidebar shows only its children, and a breadcrumb bar at the top shows the path from the true root:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;focusBreadcrumbs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextualRoot&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Click any breadcrumb to re-focus at that level, or click the home icon to unfocus entirely. The focus state persists across restarts via UserDefaults.&lt;/p&gt;
&lt;h2 id="ad-blocking"&gt;Ad blocking&lt;/h2&gt;
&lt;p&gt;Rather than building content blocking from scratch, Tree loads the full Ghostery browser extension via WebKit&amp;rsquo;s &lt;code&gt;WKWebExtensionController&lt;/code&gt; API. Every WebView gets the extension controller attached to its configuration:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webExtensionController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebExtensionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;This gives you enterprise-grade tracker and ad blocking with zero maintenance. Ghostery handles its own filter list updates, UI (accessible via a toolbar popover), and persistent storage. The browser just loads the extension at startup and grants permissions.&lt;/p&gt;
&lt;h2 id="keyboard-navigation"&gt;Keyboard navigation&lt;/h2&gt;
&lt;p&gt;Tree navigation is keyboard-driven. The tree structure maps to directional shortcuts:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+Up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go to parent node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+Down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go to first child&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+Left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Previous sibling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+Right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Next sibling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+J&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quick search overlay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prune node and subtree&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The spatial metaphor works because the sidebar &lt;em&gt;is&lt;/em&gt; a tree. Up means parent. Down means child. Left and right move between siblings. Once the mapping clicks, you rarely need the mouse for navigation.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img
src="https://man-you.ringum.net/posts/tree-browser/tree-sidebar-search-overlay_hu_f061c55a22f7e7f6.webp"
srcset="https://man-you.ringum.net/posts/tree-browser/tree-sidebar-search-overlay_hu_f061c55a22f7e7f6.webp 960w, https://man-you.ringum.net/posts/tree-browser/tree-sidebar-search-overlay_hu_2edff8a9d6417701.webp 3050w"
sizes="(max-width: 960px) 100vw, 960px"
alt="Quick search overlay with ranked results and visit counts"
width="960"
height="638"
loading="lazy"
decoding="async"
/&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="session-restoration"&gt;Session restoration&lt;/h2&gt;
&lt;p&gt;On launch, the app restores the last active primary and secondary nodes, plus the focus state, from UserDefaults:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;restoreLastSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;defaults&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;primaryIdString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastPrimaryKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;secondaryIdString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastSecondaryKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;contextualRootIdString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contextualRootKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Match UUIDs to loaded TreeNodes and restore state...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Combined with WebView state persistence, a restart puts you back exactly where you were: same pages, same scroll positions, same split view layout, same focused subtree.&lt;/p&gt;
&lt;h2 id="what-it-replaces"&gt;What it replaces&lt;/h2&gt;
&lt;p&gt;The thing it replaces isn&amp;rsquo;t another browser. It replaces the workflow of opening a dozen tabs, losing track of which ones are related, and eventually closing them all because you can&amp;rsquo;t remember what you were doing.&lt;/p&gt;
&lt;p&gt;With Tree, the context is the structure. A research session about some API has the docs page as the root, the getting started guide as one branch, the API reference as another, and Stack Overflow answers as leaves. Close the app, come back tomorrow, and the tree is still there, with every page exactly where you left it.&lt;/p&gt;</description></item></channel></rss>