Part 1: Introduction to the AmazonSwiftStarter demo app and a pattern for backend abstraction.
As explained in my previous post I am looking for a replacement of the Parse-backend of an app that I am developing. I decided to explore Amazon Web Services (AWS). I will share my first experiences in this blog. I will use a demo app called AmazonSwiftStarter. This app is a work in progress. I will expand it with new features, with associated blog posts. The code for this blog post (Swift2, Xcode7) is available on GitHub.
Update October 4, 2016: Code for Swift3, Xcode8
Disclaimer: My goal is to share my AWS “adventures” to help others, and to learn from your feedback. AWS can be pretty overwhelming for beginners. Therefore I like to keep things as simple as possible. My most important goal is to research the features I need for my commercial app, and prototype them in this demo app. The demo app will definitely not be of production quality!
Tutorial overview
Part 1: Introduction to the AmazonSwiftStarter demo app and a pattern for backend abstraction
Part 2: Configure AWS Services (Cognito, DynamoDB, S3, IAM) using the AWS Console
Part 3: Run AmazonSwiftStarter with the configured AWS services using the AWS iOS SDK
Amazon Mobile Hub
In my previous post I referred to the Amazon Mobile Hub: “AWS Mobile Hub lets you easily add and configure features for your mobile apps, including user authentication, data storage, backend logic, push notifications, content delivery, and analytics.” Amazon Mobile Hub has a dashboard where you select AWS services that you want to use. After a few steps all services are configured, and objective-c client code can be downloaded. I tried it, and it works well. However, I feel the need to know exactly what’s going on, how the services are configured, and how the app interacts with the services. That is why I am starting to build a Swift app using AWS from scratch. Another (less important, but still relevant) reason is that the Mobile Hub does not (yet) generate Swift code, and it does not offer assistance in setting up DynamoDB (the AWS NoSQL database service).
Update March 29, 2016: AWS Mobile Hub now supports Swift
Update May 22, 2016: AWS Mobile Hub now supports DynamoDB
Let’s get started
In this blog post, the first part of a series, I will describe version 1.0 of the AmazonSwiftStarter app. The app will let a user sign in anonymously, and let him/her create a profile. The profile consists of his/her name and a photo. Version 1.0 is prepared to access AWS, but in reality it does not yet access AWS at all. Instead it uses a simulated backend. In upcoming versions I will replace this simulated backend by a backend that does really access AWS.
Furthermore, I will show how I achieve a clean separation between the backend logic and the other code of the app. This clean separation makes it easier to change backend providers if needed 😉
App Features
I will start with a few screenshots. Take a look at the blue sections on the screens for a short description of each screen.
The app will use anonymous sign in with AWS Cognito. Cognito offers mobile identity management and data synchronization across devices. More about that later.
Directly after sign in, an empty user profile will be prepared and stored in AWS DynamoDB. Dynamo DB is the database that the app will use to store and retrieve data. More about that later. From the user perspective their is no profile yet. He can create a profile by tapping the button.
The user can edit his name and photo. When done, his name will be stored in the profile that was created in the previous step. The image will be saved in Amazon S3. Amazon S3 is typically used for storage and retrieval of images and static files. More about that later as well.
After signing in and creating a profile the user will have access to other features of the app. I have not yet planned any of these features. These will be covered in future blog posts.
Run the app
Update October 4, 2016: Code for Swift3, Xcode8
You can download the version that goes with this blog post (Swift2, Xcode7) from Github. After downloading, open AmazonSwiftStarter.xcworkspace in Xcode. Make sure to open the .xcworkspace and not the .xcodeproj
Build and run the app in the simulator. The app should be working immediately. “Hey!”, you might wonder, “how is this possible? I didn’t setup anything yet on AWS. I didn’t even login to their console”. You are right. Like I mentioned earlier, this app uses a simple simulation of a backend service.
You might have noticed that I use Cocoapods. I have committed all dependencies into the Git repository. This includes the AWS iOS SDK. Note that this SDK uses classname prefix AWS.
Backend Abstraction
When using 3d party services it is often a good idea to abstract from implementation aspects of the 3d party service. In the commercial app that I am developing I have applied this abstraction layer. My app “talks” to the backend via protocols. Now that Parse will shutdown I will have to replace the Parse implementation of these protocols with an AWS implementation. I do not have to adapt any other code.
In the AmazonSwiftStarter I use the same approach. Let’s first take a look at the project files.
In the Xcode project you see a RemoteServiceApi group that defines the protocols of the backend service. You also see a group AmazonService. This group contains the backend-simulation-implementation of the protocols. Later we will replace this implementation by logic that really accesses AWS. That logic will use the AWS iOS SDK a lot. I will prefix all classes and structs in this group with AMZ. As an example take a look at the protocol in RemoteService.swift and its implementation in AMZRemoteService.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
typealias UserDataResultBlock = (userData: UserData?, error: NSError?) -> Void typealias ErrorResultBlock = (error: NSError?) -> Void protocol RemoteService { var currentUser: UserData? {get} func createCurrentUser(userData: UserData? , completion: ErrorResultBlock) func updateCurrentUser(userData: UserData, completion: ErrorResultBlock) func fetchCurrentUser(completion: UserDataResultBlock ) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
class AMZRemoteService { var currentUser: UserData? private static var sharedInstance: AMZRemoteService? private init() {} static func defaultService() -> RemoteService { if sharedInstance == nil { sharedInstance = AMZRemoteService() } return sharedInstance! } private func randomWait() { let randomWaitingTime = arc4random_uniform(2 * 1000000) usleep(randomWaitingTime) } } extension AMZRemoteService: RemoteService { func createCurrentUser(userData: UserData? , completion: ErrorResultBlock) { assert(currentUser == nil, "currentUser should not exist when createCurrentUser(..) is called") assert(userData == nil || userData!.userId == nil, "You can not create a user with a given userId. UserIds are assigned automatically") NSOperationQueue().addOperationWithBlock { // simulate a network call delay self.randomWait() let newUserData = UserDataValue() if let userData = userData { newUserData.updateWithData(userData) } newUserData.userId = NSUUID().UUIDString self.currentUser = newUserData completion(error: nil) } } func updateCurrentUser(userData: UserData, completion: ErrorResultBlock) { assert(currentUser != nil, "currentUser should already exist when updateCurrentUser(..) is called") assert(userData.userId == nil || userData.userId == currentUser!.userId, "Updating current user with a different userId is not allowed") NSOperationQueue().addOperationWithBlock { // simulate a network call delay self.randomWait() self.currentUser!.updateWithData(userData) completion(error: nil) } } func fetchCurrentUser(completion: UserDataResultBlock ) { NSOperationQueue().addOperationWithBlock { // simulate a network call delay self.randomWait() // simulate the fetched result by returning the currentUser completion(userData: self.currentUser, error: nil) } } } |
We have to make sure that the app only accesses the backend via the protocols. It should not directly access the implementation. To achieve this AMZRemoteService has a private init(). We use the RemoteServiceFactory to create/access the remote service (the AMZRemoteService is a Singleton).
1 2 3 4 5 6 |
class RemoteServiceFactory { static func getDefaultService() -> RemoteService { return AMZRemoteService.defaultService() } } |
Below is a snippet from the WelcomeViewController where you can see how this works:
1 2 3 4 5 6 7 8 9 10 |
@IBAction func didTapSignInButton(sender: UIButton) { hideMessage() signInButton.startAnimating() RemoteServiceFactory.getDefaultService().createCurrentUser(nil) { (error) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in self.state = .Welcomed self.signInButton.stopAnimating() }) } } |
The highlighted line shows how the WelcomeViewController accesses the RemoteService without being coupled to the AMZRemoteService.
There are a few simple rules that we must stick to during development:
- Add all files with concepts that have dependencies with AWS to the AmazonService group and prefix them with AMZ.
- Always access the remote service via the RemoteServiceFactory.
- Never use a class or struct with AWS or AMZ prefix in your app logic (except in the AmazonService group).
Sticking to these rules consequently will help to achieve a 100% backend abstraction.
What’s next?
In part 2 of this series I will show how to configure the AWS services that we need for this app (Cognito, DynamoDB, S3 and IAM).
In part 3 I will start replacing the simulated backend with a real one that accesses AWS using the AWS iOS SDK.
Follow my Twitter to get notified about my next blogposts, or leave a reply and check “Notify me of new posts by email”.
2 Comments on “Exploring AWS as a backend for a Swift app (1)”
I need to prototype many apps without building custom backend I think this is good replacement for parse ,looking forward for complete blogs
Hi Peter,
First of all I want to thank you for putting effort in writing this blog. I have started to make mobile applications with Parse as a back-end. But now they announced that they’re pulling away their service.
Now I’m looking into Firebase and Amazon… Tomorrow evening I have time to look at your Application and code. I think it will be very useful since I need to implement profiles, images and authentication for a certain App. So tomorrow evening I’ll let you know how it’s going.
Once again, thanks a lot for your effort!
Richard