From 4c50cd672c9bcab7efa5cd4c12707b944b4f4ffa Mon Sep 17 00:00:00 2001
From: Michael Law <1365977+lawmicha@users.noreply.github.com>
Date: Fri, 10 May 2024 12:42:12 -0400
Subject: [PATCH] fix(swift): Data - Set up Amplify Data code improvements
(#7555)
---
.../data/connect-to-API/index.mdx | 12 +-
.../data/set-up-data/index.mdx | 244 ++++++++++--------
.../[platform]/start/quickstart/index.mdx | 4 +-
3 files changed, 142 insertions(+), 118 deletions(-)
diff --git a/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx b/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx
index a88bcc6799d..0427a514f03 100644
--- a/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx
+++ b/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx
@@ -633,9 +633,12 @@ RxAmplify.addPlugin(plugin);
-To include custom headers in your outgoing requests, add an `URLRequestInterceptor` to the `AWSAPIPlugin`. Also specify the name of one of the APIs configured in your **amplify_outputs.json** file.
+To include custom headers in your outgoing requests, add an `URLRequestInterceptor` to the `AWSAPIPlugin`.
```swift
+import Amplify
+import AWSAPIPlugin
+
struct CustomInterceptor: URLRequestInterceptor {
func intercept(_ request: URLRequest) throws -> URLRequest {
var request = request
@@ -643,10 +646,11 @@ struct CustomInterceptor: URLRequestInterceptor {
return request
}
}
-let apiPlugin = try AWSAPIPlugin()
-try Amplify.addPlugin(apiPlugin)
-try Amplify.configure(with: .amplifyOutputs)
+let apiPlugin = AWSAPIPlugin(modelRegistration: AmplifyModels())
try apiPlugin.add(interceptor: CustomInterceptor(), for: AWSAPIPlugin.defaultGraphQLAPI)
+try Amplify.add(plugin: apiPlugin)
+try Amplify.configure(with: .amplifyOutputs)
+
```
diff --git a/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx b/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx
index d11e88c0dcf..10c79f13bbc 100644
--- a/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx
+++ b/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx
@@ -101,8 +101,6 @@ npx ampx sandbox --outputs-out-dir
npx ampx sandbox --outputs-out-dir
```
-Drag and drop the **amplify_outputs.json** file into your Xcode project to add the generate file.
-
@@ -118,14 +116,14 @@ Once the cloud sandbox is up and running, it will also create an `amplify_output
To connect your frontend code to your backend, you need to:
-1. configure the Amplify library with the Amplify client configuration file (`amplify_outputs.json`)
-2. generate a new API client from the Amplify library
-3. make an API request with end-to-end type-safety
-
-First, install the Amplify client library to your project:
+1. Configure the Amplify library with the Amplify client configuration file (`amplify_outputs.json`)
+2. Generate a new API client from the Amplify library
+3. Make an API request with end-to-end type-safety
+First, install the Amplify client library to your project:
+
```bash title="Terminal" showLineNumbers={false}
npm add aws-amplify
```
@@ -246,7 +244,7 @@ npx ampx generate graphql-client-code --format modelgen --model-target java --ou
Drag and drop the **amplify_outputs.json** file from the Finder into Xcode.
-Next, add Amplify Library for Swift through the Swift Package Manager. In Xcode, select **File** > **Add Packages...**.
+Next, add Amplify Library for Swift through Swift Package Manager. In Xcode, select **File** > **Add Packages...**.
Then, enter the Amplify Library for Swift GitHub repo URL (https://github.com/aws-amplify/amplify-swift) into the search bar and hit **Enter**.
@@ -445,20 +443,54 @@ com.example.MyAmplifyApp I/MyAmplifyApp: Added Todo with id: SOME_TODO_ID
-Go to your **ContentView.swift** and add a button to create a new todo:
+Create a new file called `TodoViewModel.swift` and the `createTodo` function with the following code:
-```swift title="ContentView.swift"
-import SwiftUI
-// highlight-next-line
+```swift title="TodoViewModel.swift"
+import Foundation
import Amplify
+@MainActor
+class TodoViewModel: ObservableObject {
+ func createTodo() {
+ let todo = Todo(
+ content: "Build iOS Application",
+ isDone: false
+ )
+ Task {
+ do {
+ let result = try await Amplify.API.mutate(request: .create(todo))
+ switch result {
+ case .success(let todo):
+ print("Successfully created todo: \(todo)")
+ case .failure(let error):
+ print("Got failed result with \(error.errorDescription)")
+ }
+ } catch let error as APIError {
+ print("Failed to create todo: ", error)
+ } catch {
+ print("Unexpected error: \(error)")
+ }
+ }
+ }
+}
+
+```
+
+Update `ContentView.swift` with the following code:
+
+```swift title="ContentView.swift"
struct ContentView: View {
+ // highlight-start
+ // Create an observable object instance.
+ @StateObject var vm = TodoViewModel()
+ // highlight-end
+
var body: some View {
// highlight-start
VStack {
Button(action: {
- addNewTodo()
+ vm.createTodo()
}) {
HStack {
Text("Add a New Todo")
@@ -469,29 +501,10 @@ struct ContentView: View {
}
// highlight-end
}
-
- // highlight-start
- private func addNewTodo() {
- Task {
- do {
- let item = Todo(content: "Build iOS Application", isDone: false)
- let result = try await Amplify.API.mutate(request: .create(item))
- switch result {
- case .success(let todo):
- print("Successfully created todo: \(todo)")
- case .failure(let error):
- print("Got failed result with \(error.errorDescription)")
- }
- } catch {
- print("Could not save item: \(error)")
- }
- }
- }
- // highlight-end
}
```
-Now if you run the application, and click on the "Create Todo" button, you should see a log indicating a todo was created:
+Now if you run the application, and click on the "Add a New Todo" button, you should see a log indicating a todo was created:
```console title="Logs" showLineNumbers={false}
Successfully created todo: Todo(id: XYZ ...)
@@ -649,57 +662,66 @@ If you build and rerun the application, you should see the todo that was created
-Start by adding a new state that stores the todos. Then add a `fetchTodos()` function and display the todos in the view:
+Update the `listTodos` function in the `TodoViewModel.swift` for listing to-do items:
+
+```swift title="TodoViewModel.swift"
+@MainActor
+class TodoViewModel: ObservableObject {
+
+ // highlight-next-line
+ @Published var todos: [Todo] = []
+
+ func createTodo() {
+ /// ...
+ }
+
+ // highlight-start
+ func listTodos() {
+ Task {
+ do {
+ let result = try await Amplify.API.query(request: .list(Todo.self))
+ switch result {
+ case .success(let todos):
+ print("Successfully retrieved list of todos: \(todos)")
+ self.todos = todos.elements
+ case .failure(let error):
+ print("Got failed result with \(error.errorDescription)")
+ }
+ } catch let error as APIError {
+ print("Failed to query list of todos: ", error)
+ } catch {
+ print("Unexpected error: \(error)")
+ }
+ }
+ }
+ // highlight-end
+}
+```
+
+Now let's update the UI code to observe the todos.
```swift title="ContentView.swift"
import SwiftUI
import Amplify
struct ContentView: View {
- // highlight-next-line
- @State private var todos: [Todo] = []
+ @StateObject var vm = TodoViewModel()
var body: some View {
VStack {
// highlight-start
- List(todos, id: \.id) { todo in
+ List(vm.todos, id: \.id) { todo in
Text(todo.content ?? "")
}
// highlight-end
- Button(action: {
- addNewTodo()
- }) {
- HStack {
- Text("Add a New Todo")
- Image(systemName: "plus")
- }
- }
- .accessibilityLabel("New Todo")
+ // .. Add a new Todo button
}
// highlight-start
.task {
- await fetchTodos()
+ await vm.listTodos()
}
// highlight-end
}
-
- // highlight-start
- private func fetchTodos() async {
- do {
- let request = GraphQLRequest.list(Todo.self)
- let result = try await Amplify.API.query(request: request)
- switch result {
- case .success(let todos):
- self.todos = todos.elements
- print("Successfully retrieved list of todos: \(todos)")
- case .failure(let error):
- print("Got failed result with \(error.localizedDescription)")
- }
- } catch {
- print("Failed to query list of todos: \(error)")
- }
- }
- // highlight-end
}
```
@@ -935,56 +957,23 @@ setContent {
To add real-time updates, you can use the subscription feature of Amplify Data. It allows to subscribe to `onCreate`, `onUpdate`, and `onDelete` events of the application. In our example, let's append the list every time a new todo is added.
-First, add a private variable to store the subscription. Then, on `init()` establish the subscription and on disappear, cancel the subscription.
+First, add a private variable to store the subscription. Then create the subscription on the `init()` initializer, and add the `subscribe()` and `cancel()` functions.
-```swift title="ContentView.swift"
-import SwiftUI
-import Amplify
-struct ContentView: View {
- @State private var todos: [Todo] = []
- // highlight-next-line
- private var subscription: AmplifyAsyncThrowingSequence>
-
- var body: some View {
- VStack {
- // ...
- }
- // highlight-start
- .onDisappear {
- self.subscription.cancel()
- }
- // highlight-end
- .task {
- // ...
- }
- }
+```swift title="TodoViewModel.swift"
+@MainActor
+class TodoViewModel: ObservableObject {
+ @Published var todos: [Todo] = []
// highlight-start
+ private var subscription: AmplifyAsyncThrowingSequence>
+
init() {
- self.subscription = Amplify.API.subscribe(request: .subscription(of: Todo.self, type: .onCreate))
+ self.subscription = Amplify.API.subscribe(request: .subscription(of: Todo.self, type: .onCreate))
}
- // highlight-end
-
- // ... fetchTodos() and addNewTodo()
-}
-```
-
-Next, add a handler as new todo creation events are received:
-
-```swift title="ContentView.swift"
-// .. imports
-struct ContentView: View {
- // ... state & subscription vars
-
- var body: some View {
- VStack {
- // ...
- }
- .task {
- await fetchTodos()
- // highlight-start
+ func subscribe() {
+ Task {
do {
for try await subscriptionEvent in subscription {
handleSubscriptionEvent(subscriptionEvent)
@@ -992,11 +981,9 @@ struct ContentView: View {
} catch {
print("Subscription has terminated with \(error)")
}
- // highlight-end
}
}
- // highlight-start
private func handleSubscriptionEvent(_ subscriptionEvent: GraphQLSubscriptionEvent) {
switch subscriptionEvent {
case .connection(let subscriptionConnectionState):
@@ -1011,8 +998,43 @@ struct ContentView: View {
}
}
}
+
+ func cancel() {
+ self.subscription.cancel()
+ }
// highlight-end
- // ... init(), fetchTodos(), and addNewTodo()
+
+ func createTodo() {
+ /// ...
+ }
+
+ func listTodos() {
+ /// ...
+ }
+}
+```
+
+Then in `ContentView.swift`, when the view appears, call `vm.subscribe()`. On disappear, cancel the subscription.
+
+
+```swift title="ContentView.swift"
+struct ContentView: View {
+ @StateObject var vm = TodoViewModel()
+
+ var body: some View {
+ VStack {
+ // ...
+ }
+ // highlight-start
+ .onDisappear {
+ vm.cancel()
+ }
+ .task {
+ vm.listTodos()
+ vm.subscribe()
+ }
+ // highlight-end
+ }
}
```
diff --git a/src/pages/[platform]/start/quickstart/index.mdx b/src/pages/[platform]/start/quickstart/index.mdx
index ff9979678c5..34fee223574 100644
--- a/src/pages/[platform]/start/quickstart/index.mdx
+++ b/src/pages/[platform]/start/quickstart/index.mdx
@@ -1857,7 +1857,7 @@ init() {
}
```
-Create a new file called `TodoViewModel.swift` and the `createTodo` function the following code:
+Create a new file called `TodoViewModel.swift` and the `createTodo` function with the following code:
```swift title="TodoViewModel.swift"
import Amplify
@@ -1925,8 +1925,6 @@ class TodoViewModel: ObservableObject {
}
}
}
-
-
```
This will assign the value of the fetched todos into a Published object.