User Authentication on iOS with Ruby on Rails and Swift

Learn how to secure your iOS app by adding user accounts using Swift and a custom Ruby on Rails backend. By Subhransu Behera.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Display Existing Selfies

Open SelfieCollectionViewController.swift and replace the existing implementation of viewDidAppear(_:) with the following:

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(true)
  
  let defaults = NSUserDefaults.standardUserDefaults()
  
  if defaults.objectForKey("userLoggedIn") == nil {
    if let loginController = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as? ViewController {
      self.navigationController?.presentViewController(loginController, animated: true, completion: nil)
    }
  } else {
    // check if API token has expired
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
    let userTokenExpiryDate : String? = KeychainAccess.passwordForAccount("Auth_Token_Expiry", service: "KeyChainService")
    let dateFromString : NSDate? = dateFormatter.dateFromString(userTokenExpiryDate!)
    let now = NSDate()
    
    let comparision = now.compare(dateFromString!)
    
    // check if should fetch new data
    if shouldFetchNewData {
      shouldFetchNewData = false
      self.setNavigationItems()
      loadSelfieData()
    }
    
    // logout and ask user to sign in again if token is expired
    if comparision != NSComparisonResult.OrderedAscending {
      self.logoutBtnTapped()
}

This checks if the user is signed in, and if not, or if the API token expired, then it’ll prompt the user to sign in. Otherwise, it’ll call loadSelfieData() to fetch any existing selfies.

Next, replace the contents of loadSelfieData() with the following:

func loadSelfieData () {
  // Create HTTP request and set request Body
  let httpRequest = httpHelper.buildRequest("get_photos", method: "GET",
    authType: HTTPRequestAuthType.HTTPTokenAuth)
  
  // Send HTTP request to load existing selfie
  httpHelper.sendRequest(httpRequest, completion: {(data:NSData!, error:NSError!) in
    // Display error
    if error != nil {
      let errorMessage = self.httpHelper.getErrorMessage(error)
      let errorAlert = UIAlertView(title:"Error", message:errorMessage as String, delegate:nil, cancelButtonTitle:"OK")
      errorAlert.show()
      
      return
    }
    
    var eror: NSError?
    
    if let jsonDataArray = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &eror) as? NSArray! {
      // load the collection view with existing selfies
      if jsonDataArray != nil {
        for imageDataDict in jsonDataArray {
          var selfieImgObj = SelfieImage()
          
          selfieImgObj.imageTitle = imageDataDict.valueForKey("title") as! String
          selfieImgObj.imageId = imageDataDict.valueForKey("random_id") as! String
          selfieImgObj.imageThumbnailURL = imageDataDict.valueForKey("image_url") as! String
          
          self.dataArray.append(selfieImgObj)
        }
        
        self.collectionView?.reloadData()
      }
    }
  })
}

This code makes a GET request to fetch the user’s existing selfies.

Once the task is complete, it iterates through the array of JSON objects it’s received and updates dataArray, which will be used by the collection view cell to display images and captions.

Here instead of using HTTP Basic Authentication the buildRequest(_:method:authType:requestContentType:requestBoundary:) method uses HTTP Token Authentication. This is indicated by passing the correct authType parameter.

httpHelper.buildRequest("get_photos", method: "GET", authType: HTTPRequestAuthType.HTTPTokenAuth)

The buildRequest(_:method:authType:requestContentType:requestBoundary:) method retrieves the API auth token from the Keychain and passes it in the Authorization header.

// This is implemented in buildRequest method in HTTPHelper struct 

case .HTTPTokenAuth:
// Retreieve Auth_Token from Keychain
if let userToken = KeychainAccess.passwordForAccount("Auth_Token", service: "KeyChainService") as String? {
  // Set Authorization header
  request.addValue("Token token=\(userToken)", forHTTPHeaderField: "Authorization")
}

Build and run. You should see the following screen if you’ve signed in at least once before, otherwise the application will prompt you to sign in.

Logout

Since you’ve signed in at least once, the application remembers the API auth_token for the user and sets the necessary flag in NSUserDefaults. Hence, it displays SelfieViewController without prompting you to login again.

Next, replace the content of collectionView(_:cellForItemAtIndexPath:) inside SelfieCollectionViewController.swift with the following code:

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier,forIndexPath: indexPath) as! SelfieCollectionViewCell
  
  // Configure the cell
  var rowIndex = self.dataArray.count - (indexPath.row + 1)
  var selfieRowObj = self.dataArray[rowIndex] as SelfieImage
  
  cell.backgroundColor = UIColor.blackColor()
  cell.selfieTitle.text = selfieRowObj.imageTitle
  
  var imgURL: NSURL = NSURL(string: selfieRowObj.imageThumbnailURL)!
  
  // Download an NSData representation of the image at the URL
  let request: NSURLRequest = NSURLRequest(URL: imgURL)
  NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(),
    completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
      if error == nil {
        var image = UIImage(data: data)
        
        dispatch_async(dispatch_get_main_queue(), {
          cell.selfieImgView.image = image
        })
      } else {
        println("Error: \(error.localizedDescription)")
      }
  })
  
  return cell
}

The rowIndex displays the most recent selfie on top and puts the older ones below. This also sets the title and image of any individual collection view cell, then it downloads a remote image asynchronously without blocking the main thread.

You’ve just implemented the code to display existing selfies for the user, but you still need a selfie to work with here!

Uploading a Selfie to the Server

When the user taps the camera icon on the navigation bar, it calls cameraBtnTapped(_:), which in turn calls displayCameraControl() to bring up the image picker controller.

In SelfieCollectionViewController.swift locate imagePickerController(_:didFinishPickingMediaWithInfo:) and replace it with the following code:

func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
  // dismiss the image picker controller window
  self.dismissViewControllerAnimated(true, completion: nil)
  
  var image:UIImage
  
  // fetch the selected image
  if picker.allowsEditing {
    image = info[UIImagePickerControllerEditedImage] as! UIImage
  } else {
    image = info[UIImagePickerControllerOriginalImage] as! UIImage
  }
  
  presentComposeViewControllerWithImage(image)
}

In the above code snippet, you’re extracting the selected image and calling presentComposeViewControllerWithImage(_:) with the selected image. imagePickerController(_:didFinishPickingMediaWithInfo:) gets called when the user selects an image.

Implement presentComposeViewControllerWithImage(_:) now by adding the following:

func presentComposeViewControllerWithImage(image:UIImage!) {
  // instantiate compose view controller to capture a caption
  if let composeVC: ComposeViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ComposeViewController") as? ComposeViewController {
    composeVC.composeDelegate = self
    composeVC.thumbImg = image
    
    // set the navigation controller of compose view controlle
    let composeNavVC = UINavigationController(rootViewController: composeVC)
    
    // present compose view controller
    self.navigationController?.presentViewController(composeNavVC, animated: true, completion: nil)
  }
}

presentComposeViewControllerWithImage(_:) instantiates and presents the compose view controller where you’ll prompt the user to add a caption to the image.

In SelfieCollectionViewController.swift you’ll notice few extensions. These extensions conform to specific protocols and keep the related methods grouped together with the protocol. For example, camera extension groups the methods that are responsible to display the camera control and manages the image picker delegate methods.

Open ComposeViewController.swift and replace viewDidLoad() with the following:

override func viewDidLoad() {
  super.viewDidLoad()
  
  // Do any additional setup after loading the view.
  self.titleTextView.becomeFirstResponder()
  self.thumbImgView.image = thumbImg
  self.automaticallyAdjustsScrollViewInsets = false
  self.activityIndicatorView.layer.cornerRadius = 10
  
  setNavigationItems()
}

This uses the same image the user selects as a thumbnail image and asks user to enter a caption.

Next, replace the contents of postBtnTapped() with the following code:

func postBtnTapped() {
  // resign the keyboard for text view
  self.titleTextView.resignFirstResponder()
  self.activityIndicatorView.hidden = false
  
  // Create Multipart Upload request
  var imgData : NSData = UIImagePNGRepresentation(thumbImg)
  let httpRequest = httpHelper.uploadRequest("upload_photo", data: imgData, title: self.titleTextView.text)
  
  httpHelper.sendRequest(httpRequest, completion: {(data:NSData!, error:NSError!) in
    // Display error
    if error != nil {
      let errorMessage = self.httpHelper.getErrorMessage(error)
      self.displayAlertMessage("Error", alertDescription: errorMessage as String)
      
      return
    }
    
    var eror: NSError?
    let jsonDataDict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &eror) as! NSDictionary
    
    var selfieImgObjNew = SelfieImage()
    
    selfieImgObjNew.imageTitle = jsonDataDict.valueForKey("title") as! String
    selfieImgObjNew.imageId = jsonDataDict.valueForKey("random_id") as! String
    selfieImgObjNew.imageThumbnailURL = jsonDataDict.valueForKey("image_url") as! String
    
    self.composeDelegate.reloadCollectionViewWithSelfie(selfieImgObjNew)
    self.activityIndicatorView.hidden = true
    self.dismissViewControllerAnimated(true, completion: nil)
  })
}

The above code uses uploadRequest(_:data:title:) method instead of buildRequest(_:method:authType:requestContentType:requestBoundary:) to create the request.

If you take a look at uploadRequest(path:data:title:) method in HTTPHelper.swift, you’ll notice the implementation is a little different from buildRequest(_:method:authType:requestContentType:requestBoundary:). So take a moment to understand it before you move ahead.

func uploadRequest(path: String, data: NSData, title: String) -> NSMutableURLRequest {
  let boundary = "---------------------------14737809831466499882746641449"
  var request = buildRequest(path, method: "POST", authType: HTTPRequestAuthType.HTTPTokenAuth,
    requestContentType:HTTPRequestContentType.HTTPMultipartContent, requestBoundary:boundary) as NSMutableURLRequest
  
  let bodyParams : NSMutableData = NSMutableData()
  
  // build and format HTTP body with data
  // prepare for multipart form uplaod
  
  let boundaryString = "--\(boundary)\r\n"
  let boundaryData = boundaryString.dataUsingEncoding(NSUTF8StringEncoding) as NSData!
  bodyParams.appendData(boundaryData)
  
  // set the parameter name
  let imageMeteData = "Content-Disposition: attachment; name=\"image\"; filename=\"photo\"\r\n".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(imageMeteData!)
  
  // set the content type
  let fileContentType = "Content-Type: application/octet-stream\r\n\r\n".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(fileContentType!)
  
  // add the actual image data
  bodyParams.appendData(data)
  
  let imageDataEnding = "\r\n".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(imageDataEnding!)
  
  let boundaryString2 = "--\(boundary)\r\n"
  let boundaryData2 = boundaryString.dataUsingEncoding(NSUTF8StringEncoding) as NSData!
  
  bodyParams.appendData(boundaryData2)
  
  // pass the caption of the image
  let formData = "Content-Disposition: form-data; name=\"title\"\r\n\r\n".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(formData!)
  
  let formData2 = title.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(formData2!)
  
  let closingFormData = "\r\n".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(closingFormData!)
  
  let closingData = "--\(boundary)--\r\n"
  let boundaryDataEnd = closingData.dataUsingEncoding(NSUTF8StringEncoding) as NSData!
  
  bodyParams.appendData(boundaryDataEnd)
  
  request.HTTPBody = bodyParams
  return request
}

The initial part should be familiar — the part where it uses buildRequest(_:method:authType:requestContentType:requestBoundary:) to create an instance of NSMutableURLRequest and sets an Authorization header. However, there are 2 additional parameters it passes to buildRequest(_:method:authType:requestContentType:requestBoundary:) method.

buildRequest(path, method: "POST", authType: HTTPRequestAuthType.HTTPTokenAuth, requestContentType:HTTPRequestContentType.HTTPMultipartContent, requestBoundary:boundary) as NSMutableURLRequest
  1. requestContentType:HTTPRequestContentType.HTTPMultipartContent
  2. requestBoundary:boundary

The Content-Type used here is different from the Content-Type that you passed in other requests. Instead of application/json you passed multipart/form-data which tells the server the request body is a series of parts. And each part is separated by a boundary.

That’s why in next few lines of code you’ll notice the boundary multiple times.

Usually a server delimits request parameters and value combinations by &. However, for uploading images, where you send the actual binary data, there might be one or more & in the data itself, so with the help of the boundary, it knows how to split the data that’s being sent.

Open SelfieCollectionViewController.swift and update reloadCollectionViewWithSelfie(_:) with the following:

func reloadCollectionViewWithSelfie(selfieImgObject: SelfieImage) {
  self.dataArray.append(selfieImgObject)
  self.collectionView?.reloadData()
}

This updates the dataArray and reloads the collection view

Build and run. Upload your first selfie, and make it a good one! :]

Note: If you’re using Simulator, you can select an image from the photo album. But if it’s empty, open Safari on the simulator, use Google to find an appropriate image, then hold and press the mouse button to invoke the context menu, and then save the image.

Photo Upload

Looks great! Well, go ahead and upload a few more — you’ll want a nice little collection. :]

If this didn’t work for you, then go back and double check that you followed all the steps to generate your Amazon S3 credentials and that the Heroku configuration is also correct.

Subhransu Behera

Contributors

Subhransu Behera

Author

Over 300 content creators. Join our team.