Skip to content

Instantly share code, notes, and snippets.

@derrh
Created October 16, 2023 19:53
Show Gist options
  • Save derrh/d64f089e38f8a7bcc4e563d5da46785b to your computer and use it in GitHub Desktop.
Save derrh/d64f089e38f8a7bcc4e563d5da46785b to your computer and use it in GitHub Desktop.
GraphQL + SwiftUI: Declarative Data and Declarative UI

Imagining what the future might be like with direct support for GraphQL in SwiftUI.

Rationale

SwiftUI and GraphQL both represent new paradigms, these new paradigms—when paired together—offer an unparalleled, streamlined, client development experience. Today the most widely used and robust Swift GraphQL clients lack direct SwiftUI support, as well as support for #8 and #9 of Jordan Eldridge's "GraphQL Maturity Model".

Goals

  • Each View declares it's data requirements using Fragments
  • Each View observes it's fragment data from the GraphQL normalized cache, limiting large-tree re-renders
  • Sensible defaults for cache policies and other configuration cover most cases, but can be overridden in intuitive ways
  • As Views are decomposed into subviews, queries are decomposed into fragments
  • As Views are reused, their data requirements are automatically picked up by parent views' queries
  • Type Safety is retained
  • Codegen happens at compilation (some pre-compilation step will likely be necessary due to Swift Macro limitations)
  • Others?
let USER_AVATAR_FRAGMENT = #gql("""
fragment UserAvatar on User {
id
initials
avatarURL
}
""")
struct UserAvatar: View {
@Fragment(USER_AVATAR_FRAGMENT)
var user
init(userId: String) {
$user.setId(userId)
}
var body: some View {
// Design System's Avatar
Avatar(
initials: user?.initials,
highlightColor: (user?.highlightColor).flatMap(Color.init(hex:)) ?? .gray,
avatarURL: (user?.avatarURL).flatMap(URL.init(string:))
)
}
}
let USER_ROW_FRAGMENT = #gql("""
fragment UserRow on User {
id
name
company { name }
...UserAvatar
}
""", USER_AVATAR_FRAGMENT)
struct UserRow: View {
var userId: String
@Fragment(USER_ROW_FRAGMENT)
var user
init(userId: String) {
self.userId = userId
$user.setId(userId)
}
var body: some View {
HStack {
UserAvatar(userId: self.userId)
VStack {
Text(user?.name ?? "")
.listTitle()
Text(user?.company?.name ?? "")
.listSubtitle()
}
}
}
}
let USERS_QUERY = #gql("""
query Users($after: Cursor) {
listUsers(first: 15, after: $after) {
edges {
node {
id
...UserRow
}
}
}
}
""", USER_ROW_FRAGMENT)
struct Users: View {
@Query(USERS_QUERY)
var users
var body: some View {
if users.loading {
ShimmeringListLoader()
} else {
List {
ForEach(users.data?.edges.map { $0.node } ?? []) { user in
UserRow(userId: user.id)
}
}
.refreshable {
$user.reload()
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment