My team is just getting started on a new project in SwiftUI. Backend is GraphQL. We’re using Apollo on the iOS client.
Question we’ve been asking ourselves, and we’ve seen other projects do this in different ways, is do we use the Apollo generated models directly in our SwiftUI views, or do we map them to our own internally owned models? What’s the benefit of either/or?
The benefit we see of using the Apollo models directly is not maintaining the overhead of a “intermediary” model. We define our query and use it.
The benefit of using a model that is owned by the application is that it is extendable and initializable (ability to init models for previews and testing).
Each has its pros and cons, but wanted to see what others thoughts are on this!
Thanks!
I believe this comes down to what your needs are with the models, each project is different.
Question we’ve been asking ourselves, and we’ve seen other projects do this in different ways, is do we use the Apollo generated models directly in our SwiftUI views, or do we map them to our own internally owned models? What’s the benefit of either/or?
The benefit of using a model that is owned by the application is that it is extendable and initializable (ability to init models for previews and testing).
It’s worth asking what additional value would your own models give you?
The models generated by Apollo iOS are extensible and the new 1.0 test mocks make it easier to test those generated models. The generated models in 1.0 are immutable (a big difference from 0.x) and thus don’t come with property-based initializers. You could write your own but it’s going to be tedious and something you would need to maintain.
If you have properties that are client-side only and query-specific that might be a case for wrapping the generated models to add those properties. We do have plans to eventually support features like that but they’re not currently on our roadmap.
The benefit we see of using the Apollo models directly is not maintaining the overhead of a “intermediary” model. We define our query and use it.
The 1.0 models were designed to become the models you use and pass around your application to power the UI. They do have limitations depending on your needs though.
I’m also interested in this topic. How should we handle creating SwiftUI previews in our applications if we use the model directly - since the models are immutable as you’ve said, and no property initializers exist?
I started to use Apollo on 0.x and decided to map the apollo models to my own models. It is a bit of boiler plate code but at the end it’s just a thin layer of mapping that gives you abstraction.
When migrating from Apollo 0.x to 1.x, it was just a matter of changing my mappers without changing my logic code using my models models … which would have been painful.
Using directly the generated models can be faster to develop but you will need to depend on Apollo every where you use your models.
I meant if you’re not adding any new stored properties then writing type extensions is possible, no need to maintain a new type for that simple use case.
I’ve built a few client apps against a GraphQL API and previously I’ve used the generated models directly, but this time around we are using a separate model for the UI. In my experience even the best graphs expose a more complex API than you want to consume from your UI (currentUser.friendsList.edges.map { $0.node }, anyone ). Here’s an example from our current project.
When rendering the AvatarImage we need several pieces of data:
fragment AvatarImageUser on User {
givenName
familyName
displayName
avatarURL
}
But to render the View we just need these:
struct AvatarImageUser {
var initials: String
var avatarURL: URL?
}
struct AvatarImage: View {
var user: AvatarImageUser
var body: some View {
ZStack {
// oversimplified...
Text(user.initials)
AsyncImage(user.avatarURL)
}
}
}
I’ve found that there tends to be quite a bit of code required to massage API response (even from a great GraphQL API) into a renderable state. As a bonus, this code is extremely easy to unit test, especially with Apollo’s generated test mocks.
extension AvatarImageUser {
init(fragment: AvatarImageUserFragment) {
avatarURL = fragment.avatarURL
initials = // complicated code based on PersonNameComponentsFormatter...
}
}
Also, it’s a great idea to pair Views, Models and Fragments together, that way when you reuse the View you can reuse the fragments and models too. Composition across the board.
@Jeffrey_Weinberg we don’t have any resources for that to share from Apollo but someone else in the community may have a reference for you.
If you’re asking how to get the models initialized with the data you want to preview I recommend looking into the selection set initializers which have to be enabled within code generation.
I recently inherited an Apollo 0.x project that has custom models that more or less duplicate the Apollo-generated ones, and unlike @JanC’s experience (“When migrating from Apollo 0.x to 1.x, it was just a matter of changing my mappers without changing my logic code using my models models … which would have been painful”), I found the upgrade incredibly difficult for 2 reasons:
the conversion from generated model to custom model is incredibly brittle, using a combination of old-style JSONSerialization and Swift’s Codable.
we’re relying too much on custom scalars for things like arbitrary JSON strings that will get parsed further down the line. The upgrade to 1.x broke our existing code, and it took me too long to fully understand what was going on.
I’m currently refactoring by eliminating the custom models one by one and just sticking with the generated ones. Ironically, though, there’s talk amongst the backend devs about replacing GraphQL with REST, in which case we’d have to go back to our custom models anyway.
That’s yet another argument for not using Apollos models directly Generally speaking, I think the domain layer should be agnostic of the transport layer especially if the transport is a 3rd party SDK.