Skip to content

Instantly share code, notes, and snippets.

@miguelfermin
Last active January 31, 2016 23:22
Show Gist options
  • Save miguelfermin/c65c77eff9460c52e80e to your computer and use it in GitHub Desktop.
Save miguelfermin/c65c77eff9460c52e80e to your computer and use it in GitHub Desktop.
Snippet of a view controller with logic to authenticate to a Fitbit account. Note: the code is functional but it requires a Storyboard and a JSON file, see notes within code.
//
// ViewController.swift
// FitBitClient
//
// Created by Miguel Fermin on 1/17/16.
// Copyright © 2016 MAF Software, LLC. All rights reserved.
//
import UIKit
import SafariServices
private let FBService = "com.fitbit.dev"
private let FBAccessTokenKey = "access_token"
private let FBRefreshTokenKey = "refresh_token"
private let FBUserIDKey = "user_id"
class ViewController: UIViewController {
/// Loaded from a json file called **"FitbitAPI.json"** which contains the *OAuth 2.0* data needed for Fitbit authentication. See the *loadAPI()* method.
/// > Below is a template to create your own json file. Once you register your app with Fitbit, will have this info.
/// ```swift
/// {
/// "clientID" : "xxxx",
/// "clientSecret" : "xxxx",
/// "authURI" : "https://www.fitbit.com/oauth2/authorize",
/// "tokenRequestURI" : "https://api.fitbit.com/oauth2/token",
/// "redirect_uri" : "com.yourdomain.FitBitClient%3A%2F%2F"
/// }
/// ```
var apiInfo: [String : AnyObject]!
/// OAuth 2.0 Client ID
var clientID: String {
return apiInfo["clientID"] as! String
}
/// Client (Consumer) Secret
var clientSecret: String {
return apiInfo["clientSecret"] as! String
}
/// Where Fitbit should send the user after the user grants or denies consent.
var redirectURIString: String {
return apiInfo["redirect_uri"] as! String
}
/// OAuth 2.0: Authorization URI
var authorizationURI: NSURL {
let authURI = apiInfo["authURI"] as! String
let scope = "activity%20nutrition%20heartrate%20location%20nutrition%20profile%20settings%20sleep%20social%20weight"
let authURIString = "\(authURI)?response_type=code&client_id=\(clientID)&redirect_uri=\(redirectURIString)&scope=\(scope)"
return NSURL(string: authURIString)!
}
/// OAuth 2.0: Access/Refresh Token Request URI
var tokenRequestURI: String {
return apiInfo["tokenRequestURI"] as! String
}
// MARK: - IB Outlets
// NOTE: You'll need a Storyboard with the corresponding controls
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var ageLabel: UILabel!
@IBOutlet weak var genderLabel: UILabel!
@IBOutlet weak var heightLabel: UILabel!
@IBOutlet weak var weightLabel: UILabel!
@IBOutlet weak var countryLabel: UILabel!
@IBOutlet weak var avatarImageView: UIImageView!
// MARK: - View Controllers
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
loadAPI()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "handleURL:", name: MAFDidLaunchedWithURLNotification, object:nil)
makeRequest(self)
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Keychain Stack
func addString(string: String, forKey key: String, service: String)
{
let encodedString = string.dataUsingEncoding(NSUTF8StringEncoding)!
let query: NSDictionary = [
"\(kSecClass)" : kSecClassGenericPassword,
"\(kSecAttrAccount)" : key,
"\(kSecAttrService)" : service,
"\(kSecValueData)" : encodedString
]
let status = SecItemAdd(query, nil)
print("status: \(status)")
}
func stringForKey(key: String, service: String) -> String?
{
let query: NSDictionary = [
"\(kSecClass)" : kSecClassGenericPassword,
"\(kSecAttrAccount)" : key,
"\(kSecAttrService)" : service,
"\(kSecReturnData)" : true
]
var data: AnyObject?
let status = SecItemCopyMatching(query, &data)
print("status: \(status)")
if let data = data as? NSData, let str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String {
print("decoded item: \(str)")
return str
}
return nil
}
func deleteItemWithKey(key: String, service: String)
{
let query: NSDictionary = [
"\(kSecClass)" : kSecClassGenericPassword,
"\(kSecAttrAccount)" : key,
"\(kSecAttrService)" : service
]
let status = SecItemDelete(query)
print("status: \(status)")
}
func updateString(string: String, forKey key: String, service: String)
{
let query: NSDictionary = [
"\(kSecClass)" : kSecClassGenericPassword,
"\(kSecAttrAccount)" : key,
"\(kSecAttrService)" : service
]
let encodedItem = string.dataUsingEncoding(NSUTF8StringEncoding)!
let change: NSDictionary = ["\(kSecValueData)" : encodedItem]
let status = SecItemUpdate(query, change)
print("status: \(status)")
}
// MARK: - Encoding and Decoding Base64
func base64Encoded(string: String) -> String
{
let data = string.dataUsingEncoding(NSUTF8StringEncoding)
let base64String = data?.base64EncodedStringWithOptions([.Encoding64CharacterLineLength])
return base64String!
}
func base64Decoded(base64String: String) -> String
{
let decodedData = NSData(base64EncodedString: base64String, options: .IgnoreUnknownCharacters)
let decodedString = String(data: decodedData!, encoding: NSUTF8StringEncoding)
return decodedString!
}
// MARK: - Networking
func handleURL(notification: NSNotification)
{
if let URL = notification.object as? NSURL
{
if let queryString = URL.query
{
let code = queryString.componentsSeparatedByString("=").last!
let dataString = "client_id=\(clientID)&grant_type=authorization_code&redirect_uri=\(redirectURIString)&code=\(code)"
let data = dataString.dataUsingEncoding(NSUTF8StringEncoding)!
let encodedID = base64Encoded("\(clientID):\(clientSecret)")
let headers = ["Authorization" : "Basic \(encodedID)", "Content-Type" : "application/x-www-form-urlencoded"]
HTTPPost(URL: tokenRequestURI, headers: headers, data: data) { data, error in
if error != nil
{
print("error: \(error)")
}
else if let jsonData = data
{
do {
let dataDict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: []) as! [String : AnyObject]
if let accessToken = dataDict["access_token"] as? String
{
self.addString(accessToken, forKey: FBAccessTokenKey, service: FBService)
}
if let refreshToken = dataDict["refresh_token"] as? String
{
self.addString(refreshToken, forKey: FBRefreshTokenKey, service: FBService)
}
if let userID = dataDict["user_id"] as? String
{
self.addString(userID, forKey: FBUserIDKey, service: FBService)
}
}
catch {
print(NSLocalizedString("Error", comment: "Failed to parse the JSON data"))
}
}
}
}
}
self.dismissViewControllerAnimated(true, completion: nil)
}
private func HTTPGet(URL URLString: String, headers: [String : String], completion: (data: NSData?, error: NSError?) -> Void)
{
let request = NSMutableURLRequest(URL: NSURL(string: URLString)!)
request.HTTPMethod = "GET"
for (field, value) in headers
{
request.setValue(value, forHTTPHeaderField: field)
}
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
NSURLSession(configuration: config).dataTaskWithRequest(request) { data, response, error in
if error != nil
{
completion(data: nil, error: error)
}
else
{
completion(data: data, error: nil)
}
}.resume()
}
private func HTTPPost(URL URLString: String, headers: [String : String], data: NSData, completion: ((data: NSData?, error: NSError?) -> Void)?)
{
let request = NSMutableURLRequest()
request.URL = NSURL(string: URLString)!
request.HTTPMethod = "POST"
for (field, value) in headers
{
request.setValue(value, forHTTPHeaderField: field)
}
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
NSURLSession(configuration: config).uploadTaskWithRequest(request, fromData: data) { data, response, error in
if error != nil
{
completion?(data: nil, error: error)
}
else
{
let res = response as! NSHTTPURLResponse
if res.statusCode == 200
{
completion?(data: data!, error: nil)
}
else
{
let err = NSError(domain: "FitBitErrorDomainNetwork", code: res.statusCode, userInfo: ["Request failed with status code" : res.statusCode])
completion?(data: nil, error: err)
}
}
}.resume()
}
// MARK: - User Actions
// NOTE: You'll need a Storyboard with the corresponding controls
@IBAction func showAuthView(sender: AnyObject)
{
let viewController = SFSafariViewController(URL: authorizationURI)
viewController.modalPresentationStyle = .FormSheet
self.presentViewController(viewController, animated: true, completion: nil)
}
@IBAction func makeRequest(sender: AnyObject)
{
guard let token = stringForKey(FBAccessTokenKey, service: FBService) else {
showAuthView(self)
return
}
HTTPGet(URL: "https://api.fitbit.com/1/user/-/profile.json", headers: ["Authorization" : "Bearer \(token)"]) { data, error in
if error != nil
{
print("error: \(error)")
}
else if let jsonData = data
{
do
{
let dataDict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: []) as! [String : AnyObject]
var myDict: [String : AnyObject]!
for (_, val) in dataDict {
myDict = val as? [String : AnyObject]
}
guard
let name = myDict["fullName"] as? String,
let age = myDict["age"] as? Int,
let gender = myDict["gender"] as? String,
let height = myDict["height"] as? Double,
let weight = myDict["weight"] as? Double,
let country = myDict["country"] as? String,
let avatar = myDict["avatar"] as? String
else { return }
let user = User(name: name, age: age, gender: gender, height: height, weight: weight, country: country)
dispatch_async(dispatch_get_main_queue()) {
self.updateInterfaceWithUser(user)
}
let imgData = NSData(contentsOfURL: NSURL(string: avatar)!)!
let image = UIImage(data: imgData)
dispatch_async(dispatch_get_main_queue()) {
self.avatarImageView.image = image
}
}
catch
{
print(NSLocalizedString("Error", comment: "Failed to parse the JSON data"))
}
}
}
}
@IBAction func deleteAllKeys()
{
deleteItemWithKey(FBUserIDKey, service: FBService)
deleteItemWithKey(FBAccessTokenKey, service: FBService)
deleteItemWithKey(FBRefreshTokenKey, service: FBService)
}
@IBAction func refreshToken()
{
guard let refreshToken = stringForKey(FBRefreshTokenKey, service: FBService)
else {
showAuthView(self)
return
}
let dataString = "grant_type=refresh_token&refresh_token=\(refreshToken)"
let reqData = dataString.dataUsingEncoding(NSUTF8StringEncoding)!
let encodedID = base64Encoded("\(clientID):\(clientSecret)")
let headers = ["Authorization" : "Basic \(encodedID)", "Content-Type" : "application/x-www-form-urlencoded"]
HTTPPost(URL: tokenRequestURI, headers: headers, data: reqData) { data, error in
if error != nil
{
print("error: \(error)")
}
else
{
if let jsonData = data
{
do {
let dataDict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: []) as! [String : AnyObject]
if let accessToken = dataDict["access_token"] as? String
{
self.updateString(accessToken, forKey: FBAccessTokenKey, service: FBService)
}
if let refreshToken = dataDict["refresh_token"] as? String
{
self.updateString(refreshToken, forKey: FBRefreshTokenKey, service: FBService)
}
}
catch {
print(NSLocalizedString("Error", comment: "Failed to parse the JSON data"))
}
}
}
}
}
// MARK: - Helpers
private func loadAPI()
{
if let jsonURL = NSBundle.mainBundle().URLForResource("FitbitAPI", withExtension: "json"), let jsonData = NSData(contentsOfURL: jsonURL) {
do {
apiInfo = try NSJSONSerialization.JSONObjectWithData(jsonData, options: []) as! [String : AnyObject]
}
catch {
print(NSLocalizedString("Error", comment: "Failed to parse the JSON file"))
}
}
}
private func updateInterfaceWithUser(user: User)
{
nameLabel.text = user.name
ageLabel.text = "\(user.age)"
genderLabel.text = user.gender
heightLabel.text = "\(user.height)"
weightLabel.text = "\(user.weight)"
countryLabel.text = user.country
}
}
// MARK: - Model
struct User {
var name: String, age: Int, gender: String, height: Double, weight: Double, country: String
init(name: String, age: Int, gender: String, height: Double, weight: Double, country: String) {
self.name = name
self.age = age
self.gender = gender
self.height = height
self.weight = weight * 2.20462
self.country = country
}
}
// IMPORTANT:
// In order for the handleURL method to work, you have to implement the "application:openURL:sourceApplication:annotation"
// method in the app delegate, as follows:
/*
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
NSNotificationCenter.defaultCenter().postNotificationName(MAFDidLaunchedWithURLNotification, object: url)
return true
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment