Is there a way for using the same structs with different requests?

Hello!

I have several Queries and Mutations which logically share the same object (struct). For example the getUser(id) method returns the same User as the listUsers (which returns an array of users in this example) and the login (which returns the logged in user’s details).

In the iOS app I want to have variables with a common User type to handle these results. Apollo’s code generation creates different models for each query, like GetUserQuery.Data.User, UsersQuery.Data.User and LoginMutation.Data.User. Do these structs have a common ancestor, protocol or anything I can use for my internal data structure? Obviously I will need users of the same data kind to be able to extend it with helper functions (for example a hasValidName()), to compare them and use them in general.

For this purpose my best solution was to introduce my own User struct, which had helper methods for every request type which converted an Apollo struct to a User type I created. Obviously this kills the simplicity of Apollo GraphQL instantly. Each model change caused me tens of source code lines and hours of work.

I am looking for a better way to do this. Maybe I overlooked something or missed an important note in the documentation? Please help me find the right way to solve this very common situation.

Hi @gklka, :wave: - you could try fragments as a way of sharing a struct between queries. Fragments will get placed into its own file and you would then be able to extend that type.

We’re also exploring sharing types outside of GraphQL fragments in Hoisted types · Issue #2191 · apollographql/apollo-ios · GitHub. It has the potential to dramatically reduce the generated code size in certain schemas.

Fragments don’t help in this problem. I think Apollo is not good for any usecases when you want to use the response objects as long living models in your app. You have to do either some kind of data conversion or use another GraphQL implementation, like SwiftGraphQL, which was created with exactly this problem in mind: Why? - SwiftGraphQL

For anyone stumbling on this in 2024, you can do this now :slight_smile:

The TL;DR:

  1. Set up a fragment for the type you want to share across queries/mutations
  2. Use this fragment in your queries/mutations
  3. Use the generated fragment type in your Swift code for functions accepting data of this type
  4. Convert the result from the query into the fragment type when passing the data from the relevant queries/mutations into the code accepting this data

Let’s base our example in the Linear API (explorer here).

Let’s first set up our fragment that we will resuse:

fragment IssueConnectionFragment on IssueConnection {
	nodes {
		id
		identifier
		dueDate
		title
		description
	}
}

We’ll then use this fragment in two queries that.

The first one fetches the issues for the user:

query MyIssues (
    $issuesFilter: IssueFilter
    $issuesSort: [IssueSortInput!]
) {
    issues(filter: $issuesFilter, sort: $issuesSort) {
        ...IssueConnectionFragment
    }
}

The second one fetches the same data in a different location, from a custom view:

query CustomView(
    $customViewId: String!
    $issuesFilter: IssueFilter
    $issuesSort: [IssueSortInput!]
) {
    customView(id: $customViewId) {
        issues(filter: $issuesFilter, sort: $issuesSort) {
            ...IssueConnectionFragment
        }
    }
}

And, then we generate our code as per the docs with ./apollo-ios-cli generate.

We’ve now got access to the queries, as per usual, but also the generated fragment code. In our case here the type for that ends up being IssueConnectionFragment.

Let’s create a function that transforms the query result into our own type, Issue:

func transformToIssues(for issueConnection: IssueConnectionFragment) -> [Issue] {
    var issues: [Issue] = []
    for issue in issueConnection.nodes {
        // Transform the data
    }
    return issues
}

But how do we pass the data to this function when our queries return different types? We use the fragments property on the data, which

Contains accessors for all of the fragments the SelectionSet can be converted to.

That’ll look something like this:

func myIssuesQuery() -> [Issue] {
    let query = MyIssuesQuery( /* ... your variables ... */ )
    guard let data = response.data else { /* Handle your error. */ return [] }
    let issues: [Issue] = transformToIssues(
        for: data.issues.fragments.issueConnectionFragment
    )
    return issues
}

func customViewQuery() -> [Issue] {
    let query = CustomViewQuery( /* ... your variables ... */ )
    guard let data = response.data else { /* Handle your error. */ return [] }
    let issues: [Issue] = transformToIssues(
        for: data.customView.issues.fragments.issueConnectionFragment
    )
    return issues
}

Our data is located in data.issues, and we convert it into our fragment type via fragments.issueConnectionFragment.

Voila! :slight_smile: