Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated Apr 29, 2024

Manipulating data

Create and update

To write data to the DataStore, pass an instance of a model to Amplify.DataStore.save():

do {
try await Amplify.DataStore.save(
Post(title: "My first post",
status: .active,
content: "Amplify.DataStore is awesome!")
)
print("Created a new post successfully")
} catch let error as DataStoreError {
print("Error creating post - \(error)")
} catch {
print("Unexpected error \(error)")
}
let saveSink = Amplify.Publisher.create {
try await Amplify.DataStore.save(
Post(title: "My first post",
status: .active,
content: "Amplify.DataStore is awesome!")
)}.sink {
if case let .failure(error) = $0 {
print("Error updating post - \(error.localizedDescription)")
}
} receiveValue: {
print("Updated the existing post: \($0)")
}

The save method creates a new record, or in the event that one already exists in the local store, it updates the record.

var existingPost: Post = /* get an existing post */
existingPost.title = "[updated] My first post"
do {
try await Amplify.DataStore.save(existingPost)
print("Updated the existing post")
} catch let error as DataStoreError {
print("Error updating post - \(error)")
} catch {
print("Unexpected error \(error)")
}
var existingPost: Post = /* get an existing post */
existingPost.title = "[updated] My first post"
let postForUpdate = existingPost
let saveSink = Amplify.Publisher.create {
try await Amplify.DataStore.save(existingPost)
}.sink {
if case let .failure(error) = $0 {
print("Error updating post - \(error)")
}
}
receiveValue: {
print("Updated the existing post: \($0)")
}

Avoid working with stale data!

Model instances which store values, such as those from the results of a DataStore.Query operation, can become stale and outdated when their properties are updated. This can be the result of a manual change or even a side effect of real time data being received by the application. In order to ensure you are performing mutations on the latest committed state to the system, either perform a query directly before the DataStore.save() operation or observe the model to keep the state updated at all times and perform mutations on that latest state referencing the model instance. The preceding example demonstrates one approach. The following example demonstrates the observeQuery approach.

import SwiftUI
import Amplify
import Combine
/*
Example showing how to observe the model and keep the state updated before performing a save. This uses the
`@Published` property for views to observe and re-render changes to the model.
*/
class PostViewModel: ObservableObject {
@Published var post: Post?
var subscription: AnyCancellable?
init() {
}
func observe(postId: String) {
self.subscription = Amplify.Publisher.create(
Amplify.DataStore.observeQuery(
for: Post.self,
where: Post.keys.id == postId
)
)
.sink { completion in
print("Completion event: \(completion)")
} receiveValue: { snapshot in
guard let post = snapshot.items.first else {
return
}
DispatchQueue.main.async {
self.post = post
}
}
}
func updateTitle(_ title: String) async {
guard var post = post else {
return
}
post.title = title
do {
let updatedPost = try await Amplify.DataStore.save(post)
print("Updated post successfully: \(updatedPost)")
} catch let error as DataStoreError {
print("Failed to update post: \(error)")
} catch {
print("Unexpected error \(error)")
}
}
}
struct PostView: View {
@StateObject var vm = PostViewModel()
@State private var title = ""
let postId: String
init(postId: String) {
self.postId = postId
}
var body: some View {
VStack {
Text("Post's current title: \(vm.post?.title ?? "")")
TextField("Enter new title", text: $title)
Button("Click to update the title to '\(title)'") {
Task { await vm.updateTitle(title) }
}
}.onAppear(perform: {
Task { await vm.observe(postId: postId) }
})
}
}

Delete

To delete an item, simply pass in an instance.

do {
try await Amplify.DataStore.delete(post)
print("Post deleted!")
} catch let error as DataStoreError {
print("Error deleting post - \(error)")
} catch {
print("Unexpected error \(error)")
}
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.delete(post)
}.sink {
if case let .failure(error) = $0 {
print("Fetch session failed with error \(error)")
}
}
receiveValue: {
print("Post deleted!")
}

A delete can also be achieved by a type and its id.

do {
try await Amplify.DataStore.delete(Post.self, withId: "123")
print("Post deleted!")
} catch let error as DataStoreError {
print("Error deleting post - \(error)")
} catch {
print("Unexpected error \(error)")
}
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.delete(Post.self, withId: "123")
}.sink {
if case let .failure(error) = $0 {
print("Error deleting post - \(error)")
}
}
receiveValue: {
print("Post deleted!")
}

Query Data

Queries are performed against the local store. When cloud synchronization is enabled, the local store is updated in the background by the DataStore Sync Engine.

For more advanced filtering, such as matching arbitrary field values on an object, you can supply a query predicate.

do {
let result = try await Amplify.DataStore.query(Post.self)
// result will be of type [Post]
print("Posts: \(result)")
} catch let error as DataStoreError {
print("Error on query() for type Post - \(error)")
} catch {
print("Unexpected error \(error)")
}
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(Post.self)
}.sink {
if case let .failure(error) = $0 {
print("Error on query() for type Post - \(error)")
}
}
receiveValue: { result in
print("Posts: \(result)")
}

Query by identifier

Query has an alternative signature that allows to fetch a single item by its id:

do {
let result = try await Amplify.DataStore.query(Post.self, byId: "123")
// result will be a single object of type Post?
print("Post: \(result)")
} catch {
print("Error on query() for type Post - \(error)")
}
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(Post.self, byId: "123")
}.sink {
if case let .failure(error) = $0 {
print("Error on query() for type Post - \(error)")
}
}
receiveValue: { result in
print("Posts: \(result)")
}

Predicates

Predicates are filters that can be used to match items in the DataStore. When applied to a query(), they constrain the returned results. When applied to a save(), they act as a pre-requisite for updating the data. You can match against fields in your schema by using the following predicates:

Strings: eq | ne | le | lt | ge | gt | contains | notContains | beginsWith | between

Numbers: eq | ne | le | lt | ge | gt | between

Lists: contains | notContains

For example if you wanted a list of all Post Models that have a rating greater than 4:

let p = Post.keys
do {
let result = try await Amplify.DataStore.query(Post.self, where: p.rating > 4)
print("Posts: \(result)")
} catch let error as DataStoreError {
print("Error listing posts - \(error)")
} catch {
print("Unexpected error \(error)")
}
let p = Post.keys
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(Post.self, where: p.rating > 4)
}.sink {
if case let .failure(error) = $0 {
print("Error listing posts - \(error)")
}
}
receiveValue: { result in
print("Posts: \(result)")
}

Multiple conditions can also be used, like the ones defined in GraphQL Transform condition statements. For example, fetch all posts that have a rating greater than 4 and are ACTIVE:

let p = Post.keys
do {
let result = try await Amplify.DataStore.query(
Post.self,
where: p.rating > 4 && p.status == PostStatus.active
)
// result of type [Post]
print("Published posts with rating greater than 4: \(result)")
} catch let error as DataStoreError {
print("Error listing posts - \(error)")
} catch {
print("Unexpected error \(error)")
}
let p = Post.keys
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(
Post.self,
where: p.rating > 4 && p.status == PostStatus.active
)
}.sink {
if case let .failure(error) = $0 {
print("Error listing posts - \(error)")
}
}
receiveValue: { result in
print("Published posts with rating greater than 4: \(result)")
}

You can also write this in a compositional function manner by replacing the operators with their equivalent predicate statements such as .gt, .and, etc:

let p = Post.keys
try await Amplify.DataStore.query(
Post.self,
where: p.rating.gt(4).and(p.status.eq(PostStatus.active))
)
let p = Post.keys
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(
Post.self,
where: p.rating > 4 && p.status == PostStatus.active
)
}.sink {
// handle result
}

Alternatively, the or logical operator can also be used:

let p = Post.keys
do {
let result = try await Amplify.DataStore.query(
Post.self,
where: p.rating == nil || p.status == PostStatus.active
)
// result of type [Post]
print("Posts in draft or without rating: \(result)")
} catch let error as DataStoreError {
print("Error listing posts - \(error)")
} catch {
print("Unexpected error \(error)")
}
let p = Post.keys
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(
Post.self,
where: p.rating == nil || p.status == PostStatus.active
)
}.sink {
if case let .failure(error) = $0 {
print("Error listing posts - \(error)")
}
}
receiveValue: { result in
// result of type [Post]
print("Posts in draft or without rating: \(result)")
}

Sort

Query results can also be sorted by one or more fields.

For example, to sort all Post objects by rating in ascending order:

do {
let result = try await Amplify.DataStore.query(
Post.self,
sort: .ascending(Post.keys.rating))
print("Posts: \(result)")
} catch let error as DataStoreError {
print("Error listing posts - \(error)")
} catch {
print("Unexpected error \(error)")
}
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(
Post.self,
sort: .ascending(Post.keys.rating))
}.sink {
if case let .failure(error) = $0 {
print("Error listing posts - \(error)")
}
}
receiveValue: { result in
print("Posts: \(result)")
}

To get all Post objects sorted first by rating in ascending order, and then by title in descending order:

do {
let result = try await Amplify.DataStore.query(
Post.self,
sort: .by(
.ascending(Post.keys.rating),
.descending(Post.keys.title)
)
)
print("Posts: \(result)")
} catch let error as DataStoreError {
print("Failed with error \(error)")
} catch {
print("Error listing posts - \(error)")
}
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(
Post.self,
sort: .by(
.ascending(Post.keys.rating),
.descending(Post.keys.title)
)
)
}.sink {
if case let .failure(error) = $0 {
print("Error listing posts - \(error)")
}
}
receiveValue: { result in
print("Posts: \(result)")
}

Pagination

Query results can also be paginated by passing in a page number (starting at 0) and an optional limit (defaults to 100). This will return a list of the first 100 items:

let posts = try await Amplify.DataStore.query(
Post.self,
paginate: .page(0, limit: 100))
let sink = Amplify.Publisher.create {
try await Amplify.DataStore.query(
Post.self,
paginate: .page(0, limit: 100))
}.sink {
// handle result
}

The paginate arguments takes an object of type QueryPaginationInput. That object can be created with the following factory functions:

Query, then observe changes

To both query and observe subsequent changes to a Model, consider using observeQuery.