AWS SDK 2.2, iOS 9, Xcode 7 – Adventures in Learning

Well, it’s been three-and-a-half years, but I’ve finally got around to getting to a point of writing an iOS app. I wouldn’t hold your breath waiting to get a copy, though – it’s purely for my private use, to aid in monitoring and administering the IES project.

After doing enough tutorials and similar exercises to be comfortable in building the app and the UI, I got around to trying calls to the AWS infrastructure. This proved a bit more difficult than I anticipated – hence this aide-mémoire. This isn’t going to be useful for non-iOS developers, and I doubt it’s going to have anything new for more seasoned iOS developer; only iOS noobs like me need bother.

Setting up the Libraries

Get the SDK.

First, I needed to get a hold of the AWS SDK for iOS. Straightforward enough – I simply download it as noted on their site.

Try to use the SDK

Second, I created a new blank single-page iOS application to use as spike code. My goal with this application is purely to learn how to integrate the AWS SDK – linking in the library and making a call out. For this spike, I don’t need to do anything fancy – listing the AWS regions is a good start1. But before I do any of that, I need to be able to create credentials2.

To this end, I opened up the auto-generated ViewController.swift, define constants for the AWS Access and Secret Keys, and then try to create my credentials.

import UIKit

class ViewController: UIViewController {

  let awsAccessKey = "nottelling"
  let awsSecretKey = "reallynottelling"

  override func viewDidLoad() {
    super.viewDidLoad()

    let credentialsProvider = AWSStaticCredentialsProvider(
        accessKey:awsAccessKey,
        secretKey: awsSecretKey)
  }
}

Not surprisingly, this didn’t work – at this point, I haven’t added the AWS SDK to my project. This process turned out to be annoying enough to make me miss Maven; we’ve been spoiled over in Java-land.

Link in the Library (aka ‘Fix the compiler errors’)

The AWS SDK for iOS page does include instructions for doing this. But they’re out-of-date and didn’t work for me. The preferred way – using CocoaPods – didn’t work for me; I couldn’t install it. So that left the manual way. But Xcode 7 changed their setup enough that I couldn’t just follow the instructions.

Here’s what worked for me.

  • Adding the frameworks – I selected the project file in the Project navigator, which allowed me to edit the Project and Target build settings. By selecting the Target for the main application, I was able to select the ‘Build Phases’ page. This included a section “Link Binary with Libraries”. I then drag-and-dropped the AWS frameworks into that section (all I need right now are the AWSCore and AWSEC2 frameworks). I then added the other libraries mentioned in the AWS documentation – libsqlite3.tbd, libz.tbd and the System Configuration framework. Apparently tbd is the new dynamic-library format?

  • Trying to build still results in errors, though – Xcode can’t find the AWSCore and AWSEC2 frameworks. To fix this, I need to change the link path. To do this, I selected the project file again, this time selecting the Project itself, and accessing the ‘Build Settings’ page for the project. In that page, there is a group called “Search Paths”. In there, there is a section “Framework Search Paths”; I added the AWS SDK that I downloaded here. Building again clears the linker error – but Xcode still doesn’t know about the AWS classes.

  • Swift code isn’t like Java – there’s no class path that makes classes available. Nor is it exactly like C or Objective-C, where you import headers into each file. To include Objective-C based frameworks (like this one) into my Swift-based project, I need to create a bridging header. It took me a couple of tries to get this one down – the easy way is simply to add an Objective C file to the project (you can delete it later); Xcode then prompts you to add the bridging header automatically.

  • Once the bridging header was created, I could import the necessary libraries:

//
//  Use this file to import your target's public headers 
//  that you would like to expose to Swift.
//

#import <AWSCore/AWSCore.h>
#import <AWSEC2/AWSEC2.h>

Testing the credentials

At this point, I could build and run the project in the iOS simulator. Mind you, it doesn’t exactly do anything yet… so the next step is to get it to do the Describe Regions request.

The provided example code with the AWS SDK doesn’t include EC2 examples, but it wasn’t hard to work out what the code should be. It ends up something like this:

override func viewDidLoad() {
  super.viewDidLoad()
  let credentialsProvider = AWSStaticCredentialsProvider(
      accessKey:awsAccessKey, 
      secretKey: awsSecretKey)
  let defaultServiceConfiguration = AWSServiceConfiguration(
      region: .USEast1, credentialsProvider: credentialsProvider)

  AWSServiceManager.defaultServiceManager().defaultServiceConfiguration
     = defaultServiceConfiguration

  let ec2 = AWSEC2.defaultEC2()
  let regionsRequest = AWSEC2DescribeRegionsRequest()

  var regionNames: [String] = []
  ec2.describeRegions(regionsRequest).continueWithBlock() { 
      (task: AWSTask!) -> AnyObject! in
    if let result = task.result as! AWSEC2DescribeRegionsResult? {
      for region in result.regions {
        regionNames.append(region.regionName)
      }
      print(regionNames)
    } else {
     print("Could not list regions")
     print(task.error)
    }
    return nil
  }
}

Then build and run the project, and everything just works, right? I wish.

… nothing is ever simple.

The viewDidLoad method is called, naturally enough, just after the main view for this spike application is loaded in the iOS Simulator. The print statements go to the console, visible in the IDE. The Describe Regions request is sent just fine – but it returns an error: “An SSL error has occurred and a secure connection to the server cannot be made”

It turns out that, in iOS 9, Apple has tightened the network security layer – all outgoing calls now go through an App Transport Security layer. By default, that means all connections need to meet certain conditions that the AWS Web Service do not (and aren’t likely to anytime soon). To fix this, we need to add an exception to the security rules.

And now… it works! When I open the app in the simulator, I see the regions listed in the console. Obviously I need to do more (perhaps, for example, show them in a table, or in a drop-down where the user can select their preferred region), but this solves the technical problem of talking to AWS.

So how do I test this stuff?

One more thing… testing. Eventually, I’ll wrap the AWS communication with a domain-specific facade. Most of my code, when I test it, will talk to a mock facade. But I’m going to need to be able to test the real facade – which means I’m going to need to make these calls in my test code.

Xcode created a placeholder unit test class for me, so I move the AWS code there.
But when I go to build and run the tests, I get a bunch of linker errors! It turns out that the test build phase doesn’t automatically get the same dependencies as the main phase – so I need to repeat adding the frameworks into the test build phase.

Now the test builds and runs – but it finishes before the Describe Region request returns. This is because the AWS SDK for iOS is asynchronous by default; the test thread doesn’t wait for the request, so it ends early. Fortunately, one of the books I read while learning iOS programming – iOS 8 SDK Development – covered this perfectly:

var awsExpectation : XCTestExpectation?

func testExample() {
  let credentialsProvider = AWSStaticCredentialsProvider(
      accessKey:awsAccessKey, 
      secretKey: awsSecretKey)

  let defaultServiceConfiguration = AWSServiceConfiguration(
      region: .USEast1, 
      credentialsProvider: credentialsProvider)

  AWSServiceManager.defaultServiceManager().defaultServiceConfiguration
      = defaultServiceConfiguration

  let ec2 = AWSEC2.defaultEC2()
  let regionsRequest = AWSEC2DescribeRegionsRequest()

  var regionNames: [String] = []

  self.awsExpectation = expectationWithDescription("Loading Regions")
  ec2.describeRegions(regionsRequest).continueWithBlock() { 
      (task: AWSTask!) -> AnyObject! in
    if let result = task.result as! AWSEC2DescribeRegionsResult? {
      for region in result.regions {
        regionNames.append(region.regionName)
      }
    } else {
      print("Could not list regions")
      print(task.error)
    }
    self.awsExpectation!.fulfill()
    return nil
  }

  waitForExpectationsWithTimeout(5.0, handler: nil)
  print(regionNames)
}

Wrap it up

At this point, I’ve managed to get code in the main app to talk to AWS, as well as code in the test cases. This is the major technical spike needed for my back-office administration app right now – the bulk of what it needs to do is make AWS calls and show the results. It’s mainly going to talk to SimpleDB and CloudWatch, both of which have iOS APIs available; I could use an OpsWork API as well, but one doesn’t exist (yet – maybe I’ll write one if I really need it). The other technical spike I need to make is how to take the AWS keys and put them into KeyChain (maybe with TouchID as well?) – but that’s for another day.


  1. Actually, I’d prefer to use a call to the AWS Identity & Access Management API (IAM) to retrieve, for example, the user name. But IAM is one of the many APIs that Amazon haven’t yet built an iOS wrapper for. The Describe Region makes a good substitute, especially as I will probably want to be able to select the region from a list at some point. 
  2. For what I’m trying to do right now, I’m settling for the easy-but-not-recommended approach of using the Static Credentials Provider, and hardcoding my AWS keys – which I’m not showing. In the medium term, I will enter these through the UI and store them in the keychain; longer-term I might even shift to the harder-but-recommended approach of using the AWS Cognito service. 
Advertisements

Author: Robert Watkins

My name is Robert Watkins. I am a software developer and have been for over 18 years now. I currently work for people, but my opinions here are in no way endorsed by them (which is cool; their opinions aren’t endorsed by me either). My main professional interests are in Java development, using Agile methods, with a historical focus on building web based applications. I’m also a Mac-fan and love my iPhone, which I’m currently learning how to code for. I live and work in Brisbane, Australia, but I grew up in the Northern Territory, and still find Brisbane too cold (after 16 years here). I’m married, with two children and one cat. My politics are socialist in tendency, my religious affiliation is atheist (aka “none of the above”), my attitude is condescending and my moral standing is lying down.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s