Last active
January 31, 2016 23:22
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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