Front-End Web & Mobile

Announcing watchOS and tvOS Support on AWS Amplify Library for Swift

June 27, 2024: This blog post covers Amplify Gen 1. For new Amplify apps, we recommend using Amplify Gen 2. You can learn more about Gen 2 in our launch blog post.

We are excited to announce that Amplify Library for Swift now supports watchOS and tvOS platforms!

Amplify Library for Swift allows developers building apps for the Apple platforms to easily connect and include cloud features like authentication, storage, maps, and more. Amplify Library for Swift is open source on GitHub, and we deeply appreciate the feedback we get from the community.

In this blog post, you will learn how to build an app for the iOS, macOS, watchOS, and tvOS platforms using a single Swift project powered by Amplify Library for Swift.

What are we building

We are building a Todo app that deploys to iOS, macOS, and tvOS platforms. You can add new Todo items and see them update in real time across all the devices that the app is deployed to.

Prerequisites

  • Xcode version 14.3 or later
  • An AWS Account with AWS Amplify Command Line Interface (CLI) setup. You can follow this documentation to setup the Amplify CLI.

Create your app

First, we are going to create a new Multiplatform Swift Project

Creating new Multiplatform Swift Project

Ensure you have added the iOS, macOS, tvOS, and watchOS targets to your project. You might need to separately install the simulators.

Adding Deployment Targets

For watchOS, you want to create a new target from File > New > Target, and add a new watchOS app

Adding watchOS as Target

Create UI for different screens

For simplicity, I am using the default Swift boilerplate code to show how you can customize the UI for each screen as needed.

I have modified the ContentView file with the following code to have different texts show up on the respective platforms.

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            #if os(iOS)
                Text("Hello, iOS!")
            #elseif os(macOS)
                Text("Hello, macOS!")
            #elseif os(watchOS)
                Text("Hello, watchOS!")
            #elseif os(tvOS)
                Text("Hello, tvOS!")
            #endif
        }
        .padding()
    }
}

In order to get the watchOS to compile from the same files under our main project, we need to add the Apple Watch app target to the target membership of all the Swift files.

Add to Target Membership

Run the app in different simulators

Now, let’s run this code on the device simulators to see how I was able to quickly customize the experience across different Apple devices from a single codebase!

App running on multiple platforms

Connect to Amplify

First, let’s add the required Amplify packages using the Swift Package Manager to our project. Search for https://github.com/aws-amplify/amplify-swift/ and specify “2.12.0” for the version until next major version. Then, add the following packages.

Adding Amplify Plugins

After adding these packages to our current target, we also need to make sure the same packages are available for the watchOS target. Add the ‘Amplify’, ‘AWSAPIPlugin’, and ‘AmplifyDataStorePlugin’ under Watch App Target > General > Frameworks, Libraries, and Embedded Content.

Add Amplify plugins for watchOS

Finally, add the amplifyconfiguration.json file to Watch App Target > Build Phases > Copy Bundle Resources

Add Amplify Config File

Now, let’s configure Amplify in our project. Add the imports and Amplify configuration as shown in the code snippet below in your Swift App file.

import SwiftUI
import Amplify
import AWSAPIPlugin
import AWSDataStorePlugin

@main
struct HelloWorldAppApp: App {
    
    init() {
        let apiPlugin = AWSAPIPlugin(modelRegistration: AmplifyModels())
        let dataStorePlugin = AWSDataStorePlugin(modelRegistration: AmplifyModels())
        do {
            try Amplify.add(plugin: apiPlugin)
            try Amplify.add(plugin: dataStorePlugin)
            try Amplify.configure()
        } catch {
            print("An error occurred setting up Amplify: \(error)")
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

You can now initialize Amplify by running amplify init in your project’s root folder and follow the defaults.

And in order to push our updates to the backend we want to also add the API category by running amplify add api in the CLI. Note that in order to use DataStore with the API category, we should explicitly set up the conflict resolution when prompted in the command line. Your response should look as follows:

? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue Conflict detection (required for DataStore): Disabled
? Enable conflict detection? Yes
? Select the default resolution strategy Auto Merge
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)

Run amplify codegen models to generate the necessary schema files to start manipulating data in your multiplatform app

Finally, run amplify push to publish your configuration to the backend so you can now start storing data in the cloud. When asked “Do you want to generate code for your newly created GraphQL API?”, respond with “No”.

Build a Multiplatform Todo Application

First, let’s build our ViewModel where we add functions to add, query, and update the Todo list.

For our Multiplatform Todo app, we want to be able to add Todo items from iOS and macOS platforms but only view the items for tvOS and watchOS platforms. For this, we want to create new Todo items and refresh the Todo list across all platforms whenever there is a new item added to the list.

In the following code snippet,

  • createNewTodo() is adding a new Todo item with Name and Description fields saving it using Amplify.DataStore.save().
  • refreshTodos() is refreshing the list after querying the DataStore using Amplify.DataStore.query().
  • Finally, in order to be able to update the Todo list across all platforms when there is a new Todo item, we have the observeTodos() which calls Amplify.DataStore.observeQuery() which updates the list.
import Foundation
import SwiftUI
import Amplify

extension ContentView {
    
    @MainActor
    final class ViewModel: ObservableObject {
        @Published var todoName  = ""
        @Published var todoDescription = ""
        @Published var todos = [Todo]()

        func createNewTodo() {
            Task {
                do {
                    // create Todo from todoName and todoDescription
                    let newTodo = Todo(name: todoName, description: todoDescription)
                    // save Todo to DataStore
                    let savedTodo = try await Amplify.DataStore.save(newTodo)
                    // Clear previous todoName and todoDescription
                    self.todoName = ""
                    self.todoDescription = ""
                    print("Saved Todo: \(savedTodo.name)")
                    // query Todos
                    self.refreshTodos()
                } catch {
                    print("Could not save item to DataStore: \(error)")
                }
            }
        }
        
        func refreshTodos() {
            Task {
                do {
                    let todos = try await Amplify.DataStore.query(Todo.self)
                    self.todos = todos
                } catch {
                    print("Could not query DataStore: \(error)")
                }
            }
        }
    
        var snapshots: AmplifyAsyncThrowingSequence<DataStoreQuerySnapshot<Todo>>?
        func observeTodos() {
            Task {
                do {
                    // start an observeQuery to query current results
                    // and listen for changes
                    let snapshots = Amplify.DataStore.observeQuery(for: Todo.self)
                    // keep a reference to the AsyncSequence
                    self.snapshots = snapshots
                    
                    // iterate through snapshots, filtering on `isSynced`
                    // assign synced snapshot to `todos`
                    for try await snapshot in snapshots where snapshot.isSynced {
                        print("Subscription got this value: \(snapshot)")
                        self.todos = snapshot.items
                    }
                } catch {
                    print("Unable to observe mutation events")
                }
            }
        }
    
}

Now that we have our query operations, we will now call this in our ContentView file to update the UI accordingly.

In the following code snippet, we have added input fields to only iOS and macOS platforms. We are also observing changes to the Todo list so that our view updates when there’s a new Todo item.

import SwiftUI
import Amplify

struct ContentView: View {    
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            
            #if os(iOS) || os(macOS)
            TextField("Name", text: $viewModel.todoName)
            TextField("Description", text: $viewModel.todoDescription)
            
            Button("Add Item", action: { viewModel.createNewTodo() })
            #endif
            
            List {
                ForEach(viewModel.todos, id: \.id) { item in
                    Text(item.name)
                }
            }
            
            Text("Hello, \(os)!")
        }
        .padding()
        .onAppear(perform: viewModel.observeTodos)
    }
    
    private var os: String {
    #if os(iOS)
        "iOS"
    #elseif os(macOS)
        "macOS"
    #elseif os(watchOS)
        "watchOS"
    #elseif os(tvOS)
        "tvOS"
    #endif
    }
}

You are app is now connected to Amplify and you can create Todo items that update in real-time across iPhones, iPads, MacBooks, Apple Watches, and Apple TVs, from a single Swift project 🎉

Multiplatform Todo App

Refer to the Amplify Library for Swift documentation on more details on how to use the APIs and discover many more features that will help you easily build a cloud connected app experience for your users.

Conclusion

You can start using the latest version of the Amplify Library for Swift (>= 2.12.0) today to start building apps for iOS, macOS, watchOS, and tvOS platforms from a single codebase. We would love to get your feedback on this release and learn more about how you are using Amplify to build delightful experiences for users across these platforms. Please reach out to us via our GitHub repository or via our Discord channel for any enhancements or issues you are facing.