Skip to content

Instantly share code, notes, and snippets.

@sarah-j-smith
Last active October 20, 2017 07:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sarah-j-smith/f10f313930dfd0e8662d3af0d3f605ce to your computer and use it in GitHub Desktop.
Save sarah-j-smith/f10f313930dfd0e8662d3af0d3f605ce to your computer and use it in GitHub Desktop.
AWS Mobile Hub + DynamoDB Low-Level API

AWS Mobile Hub AND Low-Level DynamoDB API

The AWS iOS SDK allows writing Swift code to work with DynamoDB and other AWS services.

AWS mobile hub auto-generates a lot of setup code & files for you - specifically you get the file

awsconfiguration.json

and some model files to use with the AWS Object Mapper.

This works well ... until you want to use the very powerful low-level DynamoDB API for a few calls and nothing works.

The AWSSignInManager and AWSDynamoDBObjectMapper are high level classes meant to make using the AWS api's a lot easier. And they do, but when you want to use the low-level calls in DynamoDB functionality for all its power, then you are blocked by these classes.

In particular its really not clear how to get a credentials provider so that you can set up your DynamoDB() low-level calls. Also error messages when you get stuff wrong are completely obscure.

Getting Started

Use the AWS mobile hub to configure your services and make them simple to start off with. Download the MySampleApp code and get it working first before you try to integrate with your app.

If Using Swift 4

The MySampleApp code and the SDK uses Swift 3, which seamlessly interoperates with Swift 4 in iOS11 and Xcode 9. But this can mean if you have Swift 4 you don't notice the version mis-match.

Have to put @objcMembers in front of auto-generated classes as Swift 4 has turned off exposing these members by default. The error is an obscure one about sending empty fields. When AWS fixes this issue in the SDK you can probably remove this work-around. Until then each time your refresh your model classes you'll have to re-instate this hack.

@objcMembers
class RuleDetails: AWSDynamoDBObjectModel, AWSDynamoDBModeling {
    
    var _hashKey: String?
    var _rangeKey: String?
    var _content: String?
    var _lastUse: NSNumber?
    var _tags: Set<String>?
    var _useCount: NSNumber?

    /// other auto-generated AWS Mobile Hub Boilerplate
}

Set the Default Service Object

See the getAWSConfig() function above. The code in the GitHub doco for AWS will work but you have to hard code information in your source files which was already included in the awsconfiguration.json file.

Working with Attributes

Getting information in and out of the API is horrible. See my code in the DBUtils class for putting info in to a low-level post using some attribute helper functions that throw on failure.

Config File Tips

Each and every time you change something on your AWS Mobile Hub, re-download the MySampleApp code generated for you by the AWS Mobile Hub, and copy across the awsconfiguration.json file from that. Its much more nicely formatted and also if there's issues you can at least know your Sample App should be working off the same configuration.

//
// AppDelegate.swift
//
import UIKit
import AWSPinpoint
import AWSAuthCore
import AWSUserPoolsSignIn
import CocoaLumberjackSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
var isInitialized = false
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool
{
print("About to intercept application")
AWSSignInManager.sharedInstance().interceptApplication(application, open: url, sourceApplication: sourceApplication, annotation: annotation)
isInitialized = true
return true
}
func getAWSConfig()
{
// See file from AWS Mobile Hub: awsconfiguration.json which is loaded by AWSInfo - note that
// that scheme does not (yet) support low-level AWS DynamoDB calls so the call to DynamoDB.default()
// will crash; unless the defaultServiceConfiguration is setup to support it. Some code on the
// suggests creating a serviceConfiguration from hard-coded string values, but its a lot nicer
// to read them out of the AWSInfo object in case you move to another DC or service.
let awsInfo = AWSInfo.default()
let credentialsInfo = awsInfo.defaultServiceInfo("CognitoUserPool")
let serviceConfiguration = AWSServiceConfiguration(region: credentialsInfo!.region, credentialsProvider: credentialsInfo!.cognitoCredentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = serviceConfiguration
}
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
DDLog.add(DDTTYLogger.sharedInstance) // TTY = Xcode console
let fileLogger: DDFileLogger = DDFileLogger() // File Logger
fileLogger.rollingFrequency = TimeInterval(60*60*24) // 24 hours
fileLogger.logFileManager.maximumNumberOfLogFiles = 7
DDLog.add(fileLogger)
AWSSignInManager.sharedInstance().register(signInProvider: AWSCognitoUserPoolsSignInProvider.sharedInstance())
let didFinishLaunching = AWSSignInManager.sharedInstance().interceptApplication(application, didFinishLaunchingWithOptions: launchOptions)
// Set up service configuration for low-level DynamoDB
getAWSConfig()
if (!isInitialized) {
AWSSignInManager.sharedInstance().resumeSession(completionHandler: { (result: Any?, error: Error?) in
if result != nil
{
let identityManager = AWSIdentityManager.default()
let provider = AWSCognitoUserPoolsSignInProvider.sharedInstance()
let pool = provider.getUserPool()
let user = pool.currentUser()
let userName = user?.username
print("Log in ok: \(userName ?? identityManager.identityId ?? "Guest user")")
}
else
{
print("Log in failed: \(String(describing: error))")
}
})
isInitialized = true
}
return didFinishLaunching
}
}
enum AWSAttributeError: Error {
case CouldNotCreateAttribute(because: String)
case CouldNotCreateAttributeUpdate
case IdentityNotAvailable
case MissingData(fieldName: String)
}
fileprivate func awsAttr(of stringSet: Set<String>) throws -> AWSDynamoDBAttributeValue
{
let attr = AWSDynamoDBAttributeValue()
if attr == nil
{
throw AWSAttributeError.CouldNotCreateAttribute(because: "String set: \(stringSet.joined())")
}
attr!.ss = Array<String>(stringSet)
return attr!
}
fileprivate func awsAttr(of stringValue: String) throws -> AWSDynamoDBAttributeValue
{
let attr = AWSDynamoDBAttributeValue()
if attr == nil
{
throw AWSAttributeError.CouldNotCreateAttribute(because: "String attribute: \(stringValue)")
}
attr!.s = stringValue
return attr!
}
fileprivate func awsAttr(of dateValue: Date = Date()) throws -> AWSDynamoDBAttributeValue
{
let dateAttr = AWSDynamoDBAttributeValue()
if dateAttr == nil
{
throw AWSAttributeError.CouldNotCreateAttribute(because: "Date attribute: \(dateValue)")
}
let lastUseNow = NSNumber(value: dateValue.timeIntervalSinceReferenceDate)
dateAttr!.n = lastUseNow.stringValue // wtf? Numbers are stored as strings?
return dateAttr!
}
fileprivate func awsAttr(of numberValue: Int = 1) throws -> AWSDynamoDBAttributeValue
{
let numberAttribute = AWSDynamoDBAttributeValue()
if numberAttribute == nil
{
throw AWSAttributeError.CouldNotCreateAttribute(because: "Number attribute: \(numberValue)")
}
let number = NSNumber(value: numberValue)
numberAttribute!.n = number.stringValue
return numberAttribute!
}
fileprivate func awsPut(of awsAttr: AWSDynamoDBAttributeValue) throws -> AWSDynamoDBAttributeValueUpdate
{
let putAttrUpdate = AWSDynamoDBAttributeValueUpdate()
if putAttrUpdate == nil
{
throw AWSAttributeError.CouldNotCreateAttributeUpdate
}
putAttrUpdate!.action = .put
putAttrUpdate!.value = awsAttr
return putAttrUpdate!
}
fileprivate func awsAdd(of awsAttr: AWSDynamoDBAttributeValue) throws -> AWSDynamoDBAttributeValueUpdate
{
let addAttrUpdate = AWSDynamoDBAttributeValueUpdate()
if addAttrUpdate == nil
{
throw AWSAttributeError.CouldNotCreateAttributeUpdate
}
addAttrUpdate!.action = .add
addAttrUpdate!.value = awsAttr
return addAttrUpdate!
}
class DBUtils {
static let shared = DBUtils()
// I have a data object in my app called an AppRule and the DynamoDB mapper
// class (an AWSDynamoDBObjectModel) is the RuleDetails object - this func
// uses the low-level API to push the data for an AppRule into the RuleDetails
// table, and a "put" will overwrite any existing record that matches the
// same key - as well as "put" I can "add" which will increment number fields
// and add members to a set for that record.
func putDBRule(forRule rule: AppRule) throws
{
guard let update = AWSDynamoDBUpdateItemInput() else {
return
}
// Database key is a dictionary, because you have hash key & range key
var key = [String: AWSDynamoDBAttributeValue]()
key["hashKey"] = try awsAttr(of: rule.uniqueId)
key["rangeKey"] = try awsAttr(of: rule.inputTag
// The actual database changes for a AWSDynamoDBUpdateItemInput
// is a dictionary of AWS attribute value updates
var updates = [String: AWSDynamoDBAttributeValueUpdate]()
// A string attribute
let contentAttr = try awsAttr(of: "some string info")
updates["content"] = try awsPut(of: contentAttr)
// A date attribute
let lastUseAttr = try awsAttr(of: Date())
updates["lastUse"] = try awsPut(of: lastUseAttr)
// can update a set - can also do a ADD which will put items at the
// end of the set, if that set already exists - here I just do a put
let inputNamesSetAttr = try awsAttr(of: Set<String>(rule.inputFacts))
updates["names"] = try awsPut(of: inputNamesSetAttr)
// here's an attribute that increments by 1 every time you write
// to this record with the update
let useCountAttr = try awsAttr(of: 1)
updates["useCount"] = try awsAdd(of: useCountAttr)
// RuleDetails here is my AWS Mobile Hub generated code and will have
// the latest correct table name - which is something like:
// "mycoolapp-mobilehub-1552708683-RuleDetails"
update.tableName = RuleDetails.dynamoDBTableName()
// Uses AWSServiceManager.default().defaultServiceConfiguration
// Will crash if this is nil
let ddb = AWSDynamoDB.default()
ddb.updateItem(update) {(output: AWSDynamoDBUpdateItemOutput?, error: Error?) in
if output == nil
{
let reason = String(describing: error)
DDLogError("update for rule on AWS: \(rule.description) failed: \(reason)")
}
}
}
//
// ProfileViewController.swift
//
import UIKit
import AWSAuthUI
import AWSUserPoolsSignIn
class ProfileViewController: UIViewController
{
@IBOutlet weak var userNameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var loginButton: RoundButton!
@IBAction func loginPressed(_ sender: UIButton)
{
if (AWSSignInManager.sharedInstance().isLoggedIn)
{
loginButton.setTitle("Signing out...", for: UIControlState.normal)
userNameLabel.text = NSLocalizedString("Authenticated User",
comment: "Placeholder text for the authenticated in user.")
AWSSignInManager.sharedInstance().logout(completionHandler: {[unowned self] (result: Any?, error: Error?) in
self.updateProfileView()
})
}
else
{
loginButton.setTitle("Signing in...", for: UIControlState.normal)
presentAuthUIViewController()
}
}
func presentAuthUIViewController()
{
let config = AWSAuthUIConfiguration()
config.enableUserPoolsUI = true
config.logoImage = UIImage(named: "logo")
config.backgroundColor = UIColor.white
// you should have a navigation controller for your view controller
// the sign in screen is presented using the navigation controller
AWSAuthUIViewController.presentViewController(
with: navigationController!, // put your navigation controller here
configuration: config,
completionHandler: {(
_ signInProvider: AWSSignInProvider, _ error: Error?) -> Void in
if error == nil {
DispatchQueue.main.async {[unowned self] in
print("login success")
// handle successful callback here,
// e.g. pop up to show successful sign in
self.getUserDetails()
}
}
else {
print("login fail")
// end user faced error while loggin in,
// take any required action here
}
DispatchQueue.main.async {[unowned self] in
self.updateProfileView()
}
})
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
updateProfileView()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func getUserDetails()
{
let provider = AWSCognitoUserPoolsSignInProvider.sharedInstance()
let pool = provider.getUserPool()
guard let user = pool.currentUser() else {
print("Cannot get details while not logged in")
return
}
let task = user.getDetails()
task.continueOnSuccessWith { (response) -> Any? in
if let gotDetails = response.result
{
let email = gotDetails.userAttributes?.first { $0.name == "email" }
print(email?.value ?? "Unknown email")
if let gotEmail = email?.value
{
DispatchQueue.main.async {
self.emailLabel.text = gotEmail
}
}
}
return true
}
}
func updateProfileView()
{
let identityManager = AWSIdentityManager.default()
let provider = AWSCognitoUserPoolsSignInProvider.sharedInstance()
let pool = provider.getUserPool()
let user = pool.currentUser()
let userName = user?.username
if (AWSSignInManager.sharedInstance().isLoggedIn)
{
let userNameDisplay = userName ?? NSLocalizedString("Authenticated User",
comment: "Placeholder text for the authenticated in user.")
loginButton.setTitle("Sign out", for: UIControlState.normal)
userNameLabel.text = userNameDisplay
}
else
{
let userNameDisplay = userName ?? NSLocalizedString("Guest User",
comment: "Placeholder text for the guest in user.")
loginButton.setTitle("Sign in", for: UIControlState.normal)
userNameLabel.text = userNameDisplay
}
print("User: \( identityManager.identityId ?? "No identity")")
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment