Hey folks, shameless plug incoming, with a disclaimer up front: this is only useful if your app talks to a GraphQL server. If it does not, scroll on, there is nothing here for you, and sorry for the noise. If it does, give me a minute, because I honestly think this is kind of amazing.
It is called Baton: https://github.com/shergin/baton
The one-line version: Relay for SwiftUI. Relay is the GraphQL client React uses at Meta, and its big idea is that each component declares the fragment of data it reads right next to it, and a compiler assembles the screen's query from those fragments. Baton does that for SwiftUI, and then deletes most of the runtime Relay needs, because SwiftUI already has it.
Here is a row. The fragment is a macro on the property, in the same file as the view, as a real GraphQL document checked against the schema at build time:
struct IssueRow: View {
@Fragment("""
fragment IssueRow_issue on Issue {
number
title
author { login }
}
""")
var issue: IssueRow_issue
var body: some View {
Text("#\(issue.number) \(issue.title)")
}
}
The screen has one @Query that spreads the rows' fragments, and that is the whole screen. No view model, no service, no mapper from the response into a model, no .graphql file in another folder. The row can read only the fields it declared; the generated type simply has no accessor for anything else.
The interesting part is how well SwiftUI's model fits Relay's. Relay built a runtime to know which component read which field, so a change re-renders exactly those. Observation does that natively. So every record in the normalized store is an observable object, the generated fragment type reads its fields the way you read a property of an @Observable class, and that is the whole read side. A mutation lands, a title changes, the Text that read the title runs again, and the row next to it does not. Handles are @Observable classes too, so a UIViewController holds the same handle a view does, if you still have UIKit around.
Why it is fast. I tried hard to design it for performance from day one. Reads are synchronous on the main actor, and a handle resolves against the store before the first body runs, so a screen you already fetched has its data in the very first frame. Everything else happens before that frame or off it: the compiler settles every operation at build time, so nothing parses GraphQL at run time; the response's bytes are decoded straight into the store's records off the main actor, with no Codable model in between; and a commit only touches the fields that changed and only wakes the views that read them. The store persists on the system's SQLite, so a cold start renders from disk. The runtime depends on Foundation, Observation and SQLite, nothing vendored, Swift 6 with strict concurrency.
In my own app the numbers are great. Your mileage may vary, of course, so please share yours if you end up using it.
If you have a GraphQL backend, try it on one screen and tell me what would stop you from shipping it.