Page updated Jan 16, 2024

Manipulating data

Amplify iOS v1 is now in Maintenance Mode until May 31st, 2024. This means that we will continue to include updates to ensure compatibility with backend services and security. No new features will be introduced in v1.

Please use the latest version (v2) of Amplify Library for Swift to get started.

If you are currently using v1, follow these instructions to upgrade to v2.

Amplify libraries should be used for all new cloud connected applications. If you are currently using the AWS Mobile SDK for iOS, you can access the documentation here.

Create and update

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

1Amplify.DataStore.save(
2 Post(title: "My first post",
3 description: "Amplify.DataStore is awesome!",
4 status: .draft)
5) {
6 switch $0 {
7 case .success:
8 print("Created a new post successfully")
9 case .failure(let error):
10 print("Error creating post - \(error.localizedDescription)")
11 }
12}
1let saveSink = Amplify.DataStore.save(
2 Post(
3 title: "My first post",
4 description: "Amplify.DataStore is awesome!",
5 status: .draft
6 )
7)
8.sink {
9 if case let .failure(error) = $0 {
10 print("Error creating post - \(error.localizedDescription)")
11 }
12}
13receiveValue: {
14 print("Created a new post successfully: \($0)")
15}

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

1let existingPost: Post = /* get an existing post */
2existingPost.title = "[updated] My first post"
3
4Amplify.DataStore.save(existingPost) {
5 switch $0 {
6 case .success:
7 print("Updated the existing post")
8 case .failure(let error):
9 print("Error updating post - \(error.localizedDescription)")
10 }
11}
1let existingPost: Post = /* get an existing post */
2existingPost.title = "[updated] My first post"
3
4let saveSink = Amplify.DataStore.save(existingPost).sink {
5 if case let .failure(error) = $0 {
6 print("Error updating post - \(error.localizedDescription)")
7 }
8}
9receiveValue: {
10 print("Updated the existing post: \($0)")
11}

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.

1import SwiftUI
2import Amplify
3import Combine
4
5/*
6 Example showing how to observe the model and keep the state updated before performing a save. This uses the
7 `@Published` property for views to observe and re-render changes to the model.
8 */
9class PostViewModel: ObservableObject {
10 @Published var post: Post?
11 var subscription: AnyCancellable?
12
13 init() {
14 }
15
16 func observe(postId: String) {
17 self.subscription = Amplify.DataStore.observeQuery(for: Post.self,
18 where: Post.keys.id == postId).sink { completion in
19 print("Completion event: \(completion)")
20 } receiveValue: { snapshot in
21 guard let post = snapshot.items.first else {
22 return
23 }
24 DispatchQueue.main.async {
25 self.post = post
26 }
27 }
28 }
29
30 func updateTitle(_ title: String) {
31 guard var post = post else {
32 return
33 }
34 post.title = title
35 Amplify.DataStore.save(post) { result in
36 switch result {
37 case .success(let updatedPost):
38 print("Updated post successfully: \(updatedPost)")
39 case .failure(let error):
40 print("Failed to update post: \(error)")
41 }
42 }
43 }
44}
45struct PostView: View {
46 @StateObject var vm = PostViewModel()
47 @State private var title = ""
48 let postId: String
49
50 init(postId: String) {
51 self.postId = postId
52 }
53
54 var body: some View {
55 VStack {
56 Text("Post's current title: \(vm.post?.title ?? "")")
57 TextField("Enter new title", text: $title)
58 Button("Click to update the title to '\(title)'") {
59 vm.updateTitle(title)
60 }
61 }.onAppear(perform: {
62 vm.observe(postId: postId)
63 })
64 }
65}

Delete

To delete an item, simply pass in an instance.

1Amplify.DataStore.delete(post) {
2 switch $0 {
3 case .success:
4 print("Post deleted!")
5 case .failure(let error):
6 print("Error deleting post - \(error.localizedDescription)")
7 }
8}
1let sink = Amplify.DataStore.delete(post).sink {
2 if case let .failure(error) = $0 {
3 print("Fetch session failed with error \(error)")
4 }
5}
6receiveValue: {
7 print("Post deleted!")
8}

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

1Amplify.DataStore.delete(Post.self, withId: "123") {
2 switch $0 {
3 case .success:
4 print("Post deleted!")
5 case .failure(let error):
6 print("Error deleting post - \(error.localizedDescription)")
7 }
8}
1let sink = Amplify.DataStore.delete(Post.self, withId: "123").sink {
2 if case let .failure(error) = $0 {
3 print("Error deleting post - \(error.localizedDescription)")
4 }
5}
6receiveValue: {
7 print("Post deleted!")
8}

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.

1Amplify.DataStore.query(Post.self) {
2 switch $0 {
3 case .success(let result):
4 // result will be of type [Post]
5 print("Posts: \(result)")
6 case .failure(let error):
7 print("Error on query() for type Post - \(error.localizedDescription)")
8 }
9}
1let sink = Amplify.DataStore.query(Post.self).sink {
2 if case let .failure(error) = $0 {
3 print("Error on query() for type Post - \(error.localizedDescription)")
4 }
5}
6receiveValue: { result in
7 print("Posts: \(result)")
8}

Query by id

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

1Amplify.DataStore.query(Post.self, byId: "123") {
2 switch $0 {
3 case .success(let result):
4 // result will be a single object of type Post?
5 print("Posts: \(result)")
6 case .failure(let error):
7 print("Error on query() for type Post - \(error.localizedDescription)")
8 }
9}
1let sink = Amplify.DataStore.query(Post.self, byId: "123").sink {
2 if case let .failure(error) = $0 {
3 print("Error on query() for type Post - \(error.localizedDescription)")
4 }
5}
6receiveValue: { result in
7 print("Posts: \(result)")
8}

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:

1let p = Post.keys
2Amplify.DataStore.query(Post.self, where: p.rating > 4) {
3 switch $0 {
4 case .success(let result):
5 print("Posts: \(result)")
6 case .failure(let error):
7 print("Error listing posts - \(error.localizedDescription)")
8 }
9}
1let p = Post.keys
2let sink = Amplify.DataStore.query(Post.self, where: p.rating > 4).sink {
3 if case let .failure(error) = $0 {
4 print("Error listing posts - \(error.localizedDescription)")
5 }
6}
7receiveValue: { result in
8 print("Posts: \(result)")
9}

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:

1let p = Post.keys
2Amplify.DataStore.query(Post.self,
3 where: p.rating > 4 && p.status == PostStatus.active) {
4 switch $0 {
5 case .success(let result):
6 // result of type [Post]
7 print("Published posts with rating greater than 4: \(result)")
8 case .failure(let error):
9 print("Error listing posts - \(error.localizedDescription)")
10 }
11}
1let p = Post.keys
2let sink = Amplify.DataStore.query(
3 Post.self,
4 where: p.rating > 4 && p.status == PostStatus.active
5).sink {
6 if case let .failure(error) = $0 {
7 print("Error listing posts - \(error.localizedDescription)")
8 }
9}
10receiveValue: { result in
11 print("Published posts with rating greater than 4: \(result)")
12}

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

1let p = Post.keys
2Amplify.DataStore.query(Post.self,
3 where: p.rating.gt(4).and(p.status.eq(PostStatus.active))) {
4 // handle the callback like in the previous example
5}
1let p = Post.keys
2let sink = Amplify.DataStore.query(
3 Post.self,
4 where: p.rating > 4 && p.status == PostStatus.active
5).sink {
6 // handle the callback like in the previous example
7}

Alternatively, the or logical operator can also be used:

1let p = Post.keys
2Amplify.DataStore.query(Post.self,
3 where: { p.rating == nil || p.status == PostStatus.active }) {
4 switch $0 {
5 case .success(let result):
6 // result of type [Post]
7 print("Posts in draft or without rating: \(result)")
8 case .failure(let error):
9 print("Error listing posts - \(error.localizedDescription)")
10 }
11}
1let p = Post.keys
2let sink = Amplify.DataStore.query(
3 Post.self,
4 where: { p.rating == nil || p.status == PostStatus.active }
5).sink {
6 if case let .failure(error) = $0 {
7 print("Error listing posts - \(error.localizedDescription)")
8 }
9}
10receiveValue: { result in
11 // result of type [Post]
12 print("Posts in draft or without rating: \(result)")
13}

Sort

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

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

1Amplify.DataStore.query(Post.self, sort: .ascending(Post.keys.rating)) {
2 switch $0 {
3 case .success(let result):
4 print("Posts: \(result)")
5 case .failure(let error):
6 print("Error listing posts - \(error.localizedDescription)")
7 }
8}
1let sink = Amplify.DataStore.query(Post.self, sort: .ascending(Post.keys.rating)).sink {
2 if case let .failure(error) = $0 {
3 print("Error listing posts - \(error.localizedDescription)")
4 }
5}
6receiveValue: { result in
7 print("Posts: \(result)")
8}

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

1Amplify.DataStore.query(Post.self, sort: .by(.ascending(Post.keys.rating),
2 .descending(Post.keys.title))) {
3 switch $0 {
4 case .success(let result):
5 print("Posts: \(result)")
6 case .failure(let error):
7 print("Error listing posts - \(error.localizedDescription)")
8 }
9}
1let sink = Amplify.DataStore.query(Post.self, sort: .by(.ascending(Post.keys.rating),
2 .descending(Post.keys.title))).sink {
3 if case let .failure(error) = $0 {
4 print("Error listing posts - \(error.localizedDescription)")
5 }
6}
7receiveValue: { result in
8 print("Posts: \(result)")
9}

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:

1Amplify.DataStore.query(Post.self, paginate: .page(0, limit: 100)) {
2 // handle result
3}
1let sink = Amplify.DataStore.query(Post.self, paginate: .page(0, limit: 100)).sink {
2 // handle result
3}

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.