Version: 1.1.0

@Mutation

The @Mutation property wrapper lets you use a GraphQL mutation to make changes on the server, and tracks the state of the request in your view.

Example#

Unlike queries and fragments, you don't define mutations in the same file as the view that uses them. Mutations are not necessarily specific to a particular view, so they are defined in their own files.

// ChangeTodoStatus.swift
import Relay
private let mutation = graphql("""
mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) {
changeTodoStatus(input: $input) {
todo {
id
complete
}
}
}
""")

Once the mutation is defined and the Relay compiler has generated the types for the mutation, you can use @Mutation to use the mutation from a SwiftUI view.

// ToDoItem.swift
import SwiftUI
import RelaySwiftUI
private let itemFragment = graphql("""
fragment ToDoItem_item on Item {
id
text
complete
}
""")
struct ToDoItem: View {
@Fragment<ToDoItem_item> var item
@Mutation<ChangeTodoStatusMutation> var changeStatus
var body: some View {
if let item = item {
HStack {
Button {
changeStatus.commit(variables: .init(
input: .init(id: item.id, complete: !item.complete)
))
} label: {
Image(systemName: item.complete ? "checkmark.square" : "square")
}
.disabled(changeStatus.isInFlight)
Text("\(item.text)")
}
}
}
}

Parameters#

  • Operation: A type parameter (surrounded in <>) for the type of the mutation to use. This type will be generated by the Relay compiler with a name matching the operation name in the GraphQL mutation. The Relay compiler will enforce that the operation name is <FileName>Mutation.

Property value#

The @Mutation property will be a read-only Mutator structure with the following API:

  • commit: A function that will execute the mutation. This function takes a number of different parameters, so its API is described in more detail below.
  • isInFlight: Bool: Returns true if any network requests for the mutation are in flight. This can be used to conditionally show progress UI or disable buttons while the mutation is being executed.

The commit function#

The commit function takes several possible parameters, most of them optional:

  • variables: The input variables for the mutation. The type for this structure and any fields within are generated by the Relay compiler.
  • optimisticResponse: [String: Any]: (optional) A response payload for the mutation that will be committed to the local Relay store immediately. For certain kinds of mutations where a successful response is predictable, this can make the app feel more responsive. When the actual response comes back from the server, the optimistic response will be rolled back and the real one will be applied.
  • optimisticUpdater: (RecordSourceSelectorProxy, SelectorData?) -> Void: (optional) An updater function that updates the Relay store immediately as though the mutation succeeded. This function runs after the optimisticResponse, if any, has been applied. You can use this to make updates to the store that go beyond updating the fields of existing records (which will already be handled by an optimistic response). In most cases, this can be the same function as updater.
  • updater: (RecordSourceSelectorProxy, SelectorData?) -> Void: (optional) An updater function that updates the Relay store once the mutation has succeeded and the response from the server has been committed to the Relay store. You can use this to make updates to the store that go beyond updating the fields of existing records (which will already be handled by Relay).
  • completion: (Result<Operation.Data?, Error>) -> Void: (optional) A function that will be called once the mutation has completed. You can use this to make imperative actions when the mutation completes or to update state that exists outside of Relay's store and therefore won't be updated automatically.

See Updater functions for more information about how to use the optimisticUpdater and updater parameters.

Providing a convenience API#

Depending on how your mutation's variables are structured, you can sometimes end up with an awkward interface for calling your mutator's commit function:

changeStatus.commit(variables: .init(input: .init(
complete: !todo.complete,
id: id,
userId: "me"
)))

This is likely to get even rougher if you want to include an optimistic response. You can move this complexity out of your view files by adding an extension to the Mutator specifically for a particular mutation:

extension Mutation.Mutator where Operation == ChangeTodoStatusMutation {
func commit(id: String, complete: Bool) {
commit(
variables: .init(input: .init(
complete: complete,
id: id,
userId: "me"
)),
optimisticResponse: [
"changeTodoStatus": [
"todo": [
"id": id,
"complete": complete,
]
]
]
)
}
}

With this extension, the call from the view is cleaner, and it's more obvious what the actual inputs to the mutation are:

changeStatus.commit(
id: id,
complete: !todo.complete
)