mitsu's techlog

Customer Reliability Engineerになりました。備忘録も兼ねて技術ネタを適当に。技術以外はこっち→http://mitsu9.hatenablog.com/

SwiftでFacebookのユーザー認証からデータ取得まで(Facebook SDK)

SNSとの連携ということで代表的なFacebookの認証からデータ取得まで実装しました。 FacebookはSocialFrameworkとFacebook SDKを使う2種類の方法があるのですが、今回はFacebook SDKを使って実装してみました。

SDKの導入

以下のリンクの内容に沿ってSDKを導入します。 行ったことは以下の5項目です。

  1. SDKのダウンロード
  2. Facebook Appの作成 (App ID等の取得)
  3. ProjectにFacebookSDKを追加
  4. plistファイルにApp IDなどの情報を追加
  5. Application Delegateに必要なコードを追加

Facebook SDK for iOS - Getting Started

ユーザー認証

  1. ログインボタンの設置

SDKの中にログインボタンがあるので、そちらを利用してボタンを設置します。 ボタンをViewに追加するだけで認証に関する処理は全てSDK側でしてくれるので非常に楽です。

カスタムで作りたい方はボタンタップ後の処理を数行書く必要があります。

// 認証に必要なSDKをimportする
import FBSDKCoreKit
import FBSDKLoginKit

class ViewController: UIViewController, FBSDKLoginButtonDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        let loginButton = FBSDKLoginButton() // ボタンの作成
        loginButton.center = self.view.center // 位置をcenterに設定
        loginButton.delegate = self // 認証後の処理のためにdelegateを設定
        loginButton.readPermissions = ["public_profile", "email", "user_friends"] // 欲しいデータに合わせてpermissionを設定
        self.view.addSubview(loginButton) // viewにボタンを追加
    }
}
  1. 認証後の処理

delegateを設定しておくことで認証後にメソッドが呼び出されます。 FBSDKLoginButtonDelegateプロトコルで定義されているメソッドは、

  • func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!)
  • func loginButtonDidLogOut(loginButton: FBSDKLoginButton!)

になります。 ログイン時とログアウト時に呼ばれる2種類のメソッドです。

これらはサンプルを利用して以下のように実装しました。

    func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!){
        println("User Logged In")
        
        if ((error) != nil)
        {
            // Process error
        }
        else if result.isCancelled {
            // Handle cancellations
        }
        else
        {
            // If you ask for multiple permissions at once, you
            // should check if specific permissions missing
            if result.grantedPermissions.contains("email")
            {
                // Do work
            }

            // ログイン後の処理はここに書く
        }
    }
    
    func loginButtonDidLogOut(loginButton: FBSDKLoginButton!) {
        println("User Logged Out")
    }

Facebook SDKを用いたとき認証に必要なコードはこれだけになります。 ほとんどサンプルのコピーなので、認証に関しては問題無く実装できると思います。

ユーザー情報の取得

ユーザーの情報を取得するにはFBSDKGraphRequestのインスタンスを作成し、データ取得後の処理を引数にstartWithCompletionHandlerを呼び出します。

FBSDKGraphRequest初期化時にparametersを指定することで必要な情報のみを取得することができます。 以下のコードでは(おそらく)全ての情報を取得しています。

取得できる情報は認証時にreadPermissionsに設定した項目によって変化します。 今回はreadPermissionsを["public_profile", "email", "user_friends"]に設定しているので、(おそらく)全ての情報がとれる権限になっています。

func returnUserData() {
    
    let graphRequest : FBSDKGraphRequest = FBSDKGraphRequest(graphPath: "me",
        parameters: ["fields": "id,email,gender,link,locale,name,timezone,updated_time,verified,last_name,first_name,middle_name"])
    graphRequest.startWithCompletionHandler({ (connection, result, error) -> Void in
        
        if ((error) != nil)
        {
            // Process error
            println("Error: \(error)")
        }
        else
        {
            println("fetched user: \(result)")

            // 個々の情報を取得したいときはこんな感じ
            let userName : NSString = result.valueForKey("name") as! NSString
            println("User Name is: \(userName)")
        }
    })
}

出力は以下のようになります。

fetched user: {
    email = "mail@mail";
    "first_name" = firstname;
    gender = male;
    id = randomnumber;
    "last_name" = lastname;
    link = "https://www.facebook.com/app_scoped_user_id/user_id/";
    locale = "ja";
    name = "firstname lastname";
    timezone = "-5";
    "updated_time" = "2015-07-18T03:34:22+0000";
    verified = 1;
}
User Name is: firstname lastname

友達のリストの取得

現在Facebookの友達のリストをとるAPIはないのですが、taggable_friendsというAPIを利用して友達の情報を取得することができます。

taggable_friendsを利用するとデフォルトでは25人分の情報と次の友達の情報を取得するための情報が返ってきます。(以下に例があります)

実際にアプリを開発する際はスクロールなどに合わせてAPIから情報を取得していくことになると思うのですが、今回は面倒なので全てのfriendsを一回で取得しようと思います。 そのためにパラメーターにlimitを追加して、友達の数をlimitに設定します。

この友達の数はfriendsというAPIから取得できるので、friendsで友達の数を取得し、その後全ての友達を取得するように実装しました。

ちなみに、実際に実行してみたところfriendsで取得した友達の数とtaggable_friendsで取得した友達の数に若干の差がありました(僕の場合877と859)。 taggable_friendsという名前からわかるようにtaggableでない友達は取得できないので、全ての友達は取得できません。 しかし、taggableでない友達は少ないと思うのであまり困ることはないかと思います。 (それにしてもtaggableでない友達ってどういう人?わかる方いたら教えてください。)

taggable_friendsのレスポンス↓

{
  "data": [
    {
      "id": "XXXXXXX",
      "name": "hoge hoge",
      "picture": {
        "data": {
          "is_silhouette": false,
          "url": "url"
        }
      }
    },
    {
      "id": "XXXXX",
      "name": "huga huga",
      "picture": {
        "data": {
          "is_silhouette": false,
          "url": "url"
        }
      }
    }
],
  "paging": {
    "cursors": {
      "before": "XXXXXXXXXX",
      "after": "XXXXXXXXXX"
    },
    "next": "https://graph.facebook.com/v2.4/854530921331223/taggable_friends?access_token=ACCESS_TOKEN&pretty=0&limit=25&after=XXXXXXXXXXXX"
  }
}

友達のリストを取得するメソッド

func friendsList() {
    
    let graphRequest : FBSDKGraphRequest = FBSDKGraphRequest(graphPath: "me/friends", parameters: nil)
    graphRequest.startWithCompletionHandler( { (connection, result, error) -> Void in
        
        if ((error) != nil)
        {
            // Process error
            println("Error: \(error)")
            return
        }
        
        //  友達の数を取得し次のリクエストのlimitに利用
        let summary = result.valueForKey("summary") as! NSDictionary
        let counts = summary.valueForKey("total_count") as! NSNumber
        
        let graphRequest : FBSDKGraphRequest = FBSDKGraphRequest(graphPath: "me/taggable_friends", parameters: ["fields": "id,name,picture", "limit": "\(counts)"])
        graphRequest.startWithCompletionHandler( { (connection, result, error) -> Void in
            
            if ((error) != nil)
            {
                // Process error
                println("Error: \(error)")
                return
            }
            else
            {
                // 友達を一人ずつ出力
                let friends = result.valueForKey("data") as! NSArray
                var count = 1
                if let array = friends as? [NSDictionary] {
                    for friend : NSDictionary in array {
                        let name = friend.valueForKey("name") as! NSString
                        println("\(count) \(name)")
                        count++
                    }
                }
            }  
        })  
    })
}

補足

Facebookの認証の権限はユーザー側で管理することができます。 具体的にはreadPermissionsを["public_profile", "email", "user_friends"]と設定していても、ユーザーによって実際は["public_profile"]だけの権限になっている場合があります。 このことによって、NSDictionaryで返ってくるデータに対してKeyが存在しないことがあり得るので、実際にアプリを開発する際はそのことを意識してOptional型を適切に利用する必要があります。 権限を持っているつもりでOptional Valueに対してforce wrappingしてしまうとアプリが落ちることになるので気をつけましょう。

(※上記のコードは一切このことについて気にしていません。)

最後に

認証は簡単にできたのですが、ドキュメントがよくわからずデータ取得に手こずりました。 今回はデータを取得することしかしていないので、次はポストやシェアができるようにしたいと思います。

全体のコードはGistにあるので参考にしたい方はどうぞ。

The demonstration of how to use Facebook SDK

参考サイト

Facebook SDK for iOS - Getting Started

Facebook API v2.0で、フレンド数やフレンド一覧を取得する方法 | Sunday In The Park

https://developers.facebook.com/tools/explorer/

Swiftで作るかっこいいwalkthrough(Airbnb風)

Airbnbのwalkthroughがかっこいいなーとおもったので同じようなものをつくってみました。

Airbnbの良いところは、

  • 横にスクロールすると今の画像がフェードアウトし、次の画像がフェードインする
  • 画像が常にアニメーションしている
  • 横スクロールに合わせて下のボタンの色がなめらかに変わる

です。

今回はこれらの項目を実装しました。

f:id:rayc5:20150622004404g:plain

1つめ | 横にスクロールすると今の画像がフェードアウトし、次の画像がフェードインする

今回はImageViewを乗せるためのimageViewContainerを用意し、そちらにImageViewを乗せるという方法で実装しました。

walkthroughの長さによってImageViewの数が変わってくるので、常に全てを表示するのではなく、今表示しているものと左右にスクロールしたときに出てくるものの計3枚のImageViewが一度にこのimageViewContainerにのっています。 左右にスクロールした時に出てくるビューはスクロールの開始時に追加され、終了時に取り除かれるので、スクロールしていないときはそのとき表示されているImageViewが表示されるようにしました。

ViewDidLoad

まずViewDidLoad内で下準備をします。

今回scrollViewはstoryBoardで実装し、imageViewContainerはコードで実装するという変なことをしたので、余計な部分がありますが気にしないでください。

基本的には、ImageViewを用意し配列に保存し、一枚目のImageViewだけimageViewContainerに追加し表示する処理をおこなっています。

class ViewController: UIViewController, UIScrollViewDelegate {

var imageViewContainer: UIView!
var imageViewArray = [UIImageView]()

override func viewDidLoad() {
    super.viewDidLoad()

    /** 関係ない部分は省略 */
        
    // imageViewContainer. scrollViewの後ろ側に追加
    imageViewContainer = UIView(frame: CGRectMake(0, 0, view.frame.width, view.frame.width))
    view.insertSubview(imageViewContainer, belowSubview: scrollView)
        
    // ImageViewの設定
    let frame = CGRectMake(0.0, 0.0, view.frame.size.width, view.frame.size.height)
     
    var imageView1 = UIImageView(image: UIImage(named: "image1.jpg"))
    imageView1.frame = frame
    imageViewArray.append(imageView1)
        
    var imageView2 = UIImageView(image: UIImage(named: "image2.jpg"))
    imageView2.frame = frame
    imageViewArray.append(imageView2)
        
    var imageView3 = UIImageView(image: UIImage(named: "image3.jpg"))
    imageView3.frame = frame
    imageViewArray.append(imageView3)
        
    // 最初は1枚目だけを追加. 後のビューはスクロールに合わせて追加・削除する.
    imageViewContainer.addSubview(imageViewArray[0])
}

UIScrollViewDelegate

アニメーションはscrollViewのdelegateメソッドを利用します。

まず初めにscrollViewWillBeginDraggingにて、左右のImageViewをimageViewContainerに追加します。

次にscrollViewDidScrollにて、ImageViewのalpha値を変更しフェードイン・アウトを実装します。

alpha値は現在のcontentOffsetと今のページのx座標の差分からスクロール量を計算し、それを元に計算します。progressがスクロールの進み具合を示す変数で、0~1の値をとるようになっているので、この変数を使ってalpha値を決めていきます。

func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    addBothSidesImageView()
}

func scrollViewDidScroll(scrollView: UIScrollView) {
    let pageWidth = scrollView.frame.size.width
    if Int(fmod(scrollView.contentOffset.x , pageWidth)) == 0 {
        pageControl.currentPage = Int(scrollView.contentOffset.x / pageWidth)
        imageViewArray[pageControl.currentPage].alpha = 1.0
        removeBothSidesImageView()
    } else {
        // ここで画像と色の変化を実装
        let currentView = imageViewArray[pageControl.currentPage]
        if let index = nextIndex {
            let def = pageWidth * CGFloat(pageControl.currentPage) - scrollView.contentOffset.x
            let progress = CGFloat(abs(def)) / pageWidth
            
            let nextView = imageViewArray[index]
            currentView.alpha = 1 - progress
            nextView.alpha = progress
            
            /** 関係ない部分は省略 */
        }
    }
}

private var nextIndex: Int? {
    get {
        var nextIndex: Int
        let currentPageX = CGFloat(pageControl.currentPage) * view.frame.width
        if scrollView.contentOffset.x < currentPageX { // 右スワイプ
            nextIndex = pageControl.currentPage - 1
        } else {
            nextIndex = pageControl.currentPage + 1
        }
            
        if nextIndex < 0 || pageCount <= nextIndex {
            return nil
        }
        return nextIndex
    }
}

2つめ | 画像が常にアニメーションしている

今回はimageViewContainerを作っているので、こちらを常にアニメーションすれば、ImageViewが常に変化するようになります。

アニメーションを永遠に繰り返すためにrepeatCountをHUGEにすることと、拡大縮小を繰り返すためにautoreversesをtrueにすることでこのようなアニメーションを実現できます。

override func viewDidLoad() {
    var animation = CABasicAnimation(keyPath: "transform.scale")
    animation.duration = 10.0
    animation.repeatCount = HUGE
    animation.autoreverses = true
    animation.fromValue = 1.0
    animation.toValue = 1.1
    imageViewContainer.layer.addAnimation(animation, forKey: "scale-layer")
}

3つめ | 横スクロールに合わせて下のボタンの色がなめらかに変わる

基本的には1つめの画像のフェードインと同じです。色の情報をarrayに格納しておき、その情報を元になめらかに色を変化させていきます。

画像はalpha値を変更したことに対し、今回は色のRGB値を変更することでなめらかに色を変化させていきます。

UIColorからRGB値を取得するためにはgetRed(red:UnsafeMutablePointer, green:UnsafeMutablePointer, blue:UnsafeMutablePointer, alpha:UnsafeMutablePointer)メソッドを使います。

これで今の色と次の色のRGB値を算出し、その差分をスクロールの量に合わせて追加することでなめらかに色を変化させることができます。

func scrollViewDidScroll(scrollView: UIScrollView) {
    let pageWidth = scrollView.frame.size.width
    if Int(fmod(scrollView.contentOffset.x , pageWidth)) == 0 {
        /** 関係ないところは省略 */
    } else {
        // ここで画像と色の変化を実装
        let currentView = imageViewArray[pageControl.currentPage]
        if let index = nextIndex {
            let def = pageWidth * CGFloat(pageControl.currentPage) - scrollView.contentOffset.x
            let progress = CGFloat(abs(def)) / pageWidth
            
            /** ImageViewの変更 - 略 - */
            
            let nextColor = colorArray[index]
            var currentColor = colorArray[pageControl.currentPage]
            var currentRGBColor = getRGB(currentColor)
            var defColors = defTwoColors(currentColor, second: nextColor)
            footerView.backgroundColor = UIColor(
                red: currentRGBColor.red + defColors.red * progress,
                green: currentRGBColor.green + defColors.green * progress,
                blue: currentRGBColor.blue + defColors.blue * progress,
                alpha: currentRGBColor.alpha + defColors.alpha * progress)
        }
    }
}

private func getRGB(color: UIColor) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
    var rgbValue = (red: CGFloat(0.0), green: CGFloat(0.0), blue: CGFloat(0.0), alpha: CGFloat(0.0))
    color.getRed(&rgbValue.red, green: &rgbValue.green, blue: &rgbValue.blue, alpha: &rgbValue.alpha)
    return (rgbValue.red, rgbValue.green, rgbValue.blue, rgbValue.alpha)
}

private func defTwoColors(first: UIColor, second: UIColor) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
    var currentColor = (red: CGFloat(0.0), green: CGFloat(0.0), blue: CGFloat(0.0), alpha: CGFloat(0.0))
    var nextColor = (red: CGFloat(0.0), green: CGFloat(0.0), blue: CGFloat(0.0), alpha: CGFloat(0.0))
    
    first.getRed(&currentColor.red, green: &currentColor.green, blue: &currentColor.blue, alpha: &currentColor.alpha)
    second.getRed(&nextColor.red, green: &nextColor.green, blue: &nextColor.blue, alpha: &nextColor.alpha)
    
    return (nextColor.red - currentColor.red, nextColor.green - currentColor.green, nextColor.blue - currentColor.blue, nextColor.alpha - currentColor.alpha)
}

最後に

全体像は以下のようになってます。

AirbnbWalkThrough

UIViewAnimationOptionsの定義から同時に指定できるオプションを理解する。

はじめに

UIViewAnimationOptionsについて

現在アニメーションについて勉強中なのですが、iOSにはUIViewAnimationOptionsという便利なものがあります。

これはOptionを指定するだけでかっこいいアニメーションを自動でしてくれるものです。

例えばTransitionCurlDownを指定するとremoveFromSuperview()に対して以下のようなアニメーションを付けてくれます。

f:id:rayc5:20150620062110g:plain

これは以下のように実装することで使えます。

import UIKit

class ViewController: UIViewController {
    
    var container: UIView!
    var redView: UIView!
    var blueView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // set container frame and add to the screen
        let frame = CGRect(x: 50, y: 50, width: 300, height: 300)
        container = UIView(frame: frame)
        container.backgroundColor = UIColor.grayColor()
        self.view.addSubview(container)
        
        // set redView and blueView; because blueView is added in animation, don't addSubview
        redView = UIView(frame: CGRectMake(0, 0, container.frame.width, container.frame.height))
        redView.backgroundColor = UIColor.redColor()
        container.addSubview(redView)
        
        blueView = UIView(frame: redView.frame)
        blueView.backgroundColor = UIColor.blueColor()
        
        // set button as a trigger of animation
        let button = UIButton(frame: CGRectMake(50, 400, 300, 30))
        button.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
        button.setTitle("start animation", forState: .Normal)
        self.view.addSubview(button)
        
        button.addTarget(self, action: "tapButton:", forControlEvents: .TouchUpInside)
    }
    
    func tapButton(sender: AnyObject) {
        var opt = UIViewAnimationOptions.TransitionCurlDown 
        
        var views = (frontView: redView, backView: blueView)
        
        UIView.transitionWithView(container, duration: 1.0, options: opt, animations: {
            // remove the front object...
            views.frontView.removeFromSuperview()
            
            // ... and add the other object
            self.container.addSubview(views.backView)
            
            }, completion: { finished in
                // any code...
        })
    }
}

UIViewAnimationOptionsの複数指定

このUIViewAnimationOptionsなのですが、以下のように複数指定することもできます。

var opt = UIViewAnimationOptions.TransitionCurlDown | .Repeat

ちなみにこの場合Repeatを設定したので、同様のアニメーションが永遠にリピートされます。

f:id:rayc5:20150620062848g:plain

本題

UIViewAnimationOptionsには横にflipする.TransitionFlipFromLeftと縦にflipする.TransitionFlipFromBottomのオプションがあります。

そこで僕は、この2つのオプションを指定すれば、縦と横が混ざって斜めにflipするようなかっこいいアニメーションが作れるのではと思ったんですねー。

そして少しの期待に胸をふくまらせながら動かしてみると、TransitionFlipFromBottomと全く同じ動きをしました。わかってはいましたがそう上手くいくわけはありません。この2つのオプションを同時に行うのは無理なのでどちらか一方ということでTransitionFlipFromBottomが選ばれたのでしょう。

そして次にしてみたことはTransitionCurlUpとTransitionCurlDownです。全く正反対のオプションを指定するとどうなるのか気になったんですね。

その結果は不思議なことにTransitionFlipFromBottomと同じ動きになったんです。TransitionCurlUpとTransitionCurlDownの2つのオプションから誰がTransitionFlipFromBottomを予想できるでしょうか。

この不思議な結果を受けて、詳細にUIViewAnimationOptionsについて調べて見ることにしました。

内容

UIView Class Referenceで詳細を調べました。

そこでは、UIViewAnimationOptionsはObjective-Cで以下のように実装されていることが書いてあります。

enum {
   UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
   UIViewAnimationOptionAllowUserInteraction      = 1 <<  1,
   UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2,
   UIViewAnimationOptionRepeat                    = 1 <<  3,
   UIViewAnimationOptionAutoreverse               = 1 <<  4,
   UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5,
   UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6,
   UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7,
   UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8,
   UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9,
   
   UIViewAnimationOptionCurveEaseInOut            = 0 << 16,
   UIViewAnimationOptionCurveEaseIn               = 1 << 16,
   UIViewAnimationOptionCurveEaseOut              = 2 << 16,
   UIViewAnimationOptionCurveLinear               = 3 << 16,
   
   UIViewAnimationOptionTransitionNone            = 0 << 20,
   UIViewAnimationOptionTransitionFlipFromLeft    = 1 << 20,
   UIViewAnimationOptionTransitionFlipFromRight   = 2 << 20,
   UIViewAnimationOptionTransitionCurlUp          = 3 << 20,
   UIViewAnimationOptionTransitionCurlDown        = 4 << 20,
   UIViewAnimationOptionTransitionCrossDissolve   = 5 << 20,
   UIViewAnimationOptionTransitionFlipFromTop     = 6 << 20,
   UIViewAnimationOptionTransitionFlipFromBottom  = 7 << 20,
};
typedef NSUInteger UIViewAnimationOptions;

複数指定の際に " | " を使っていたあたりから予想できる通り、ビット演算を利用して様々なオプションが管理されていますね。

UIViewAnimationOptionTransition~に関するものだけに焦点を当てて考えます。

これらのオプションは0~7までの数字を20ビット分左シフトしています。シフトのことは一旦おいておいて、0~7までの数字を2進数表示すると、以下のようになります。

オプション名 10進数 2進数
TransitionNone 0 000
TransitionFlipFromLeft 1 001
TransitionFlipFromRight 2 010
TransitionCurlUp 3 011
TransitionCurlDown 4 100
TransitionCrossDissolve 5 101
TransitionFlipFromTop 6 110
TransitionFlipFromBottom 7 111

これを見ると先ほどのアニメーションが納得いきますね。確かにTransitionCurlUp | TransitionCurlDown は 011 | 100 = 111 なので TransitionFlipFromBottomになっています。

また、これを見ると、TransitionFlipFromLeft | TransitionFlipFromRight = TransitionCurlUpになることなんかもわかります。この場合はFlipFromLeft,Rightという横の動きを足すことでCurlUpという縦の動きに変わるので、文字通りに考えると意味不明の動きをすることになりますね。

この実装からわかるように基本的にTransition系のオプションを2つ以上指定することは推奨されていません。オプションの複数指定は、RepeatとTransition系のオプションを指定するという様に、違う種類のオプションを指定するときに使いましょう。

複数指定して良いオプションについて

UIViewAnimationOptionsの詳細を理解したため、どのオプションが同時に指定できるのかが定義からわかります。

まず最初に書かれている10個のオプション(UIViewAnimationOptionLayoutSubviews ~ UIViewAnimationOptionOverrideInheritedOptions)は、2進数の1桁目から10桁目までのそれぞれの桁がそれぞれのオプションのフラグになっているので、それぞれ独立していることがわかります。したがって、複数のオプションを指定しても大丈夫です。

次に書かれているUIViewAnimationOptionCurveEaseInOut~UIViewAnimationOptionCurveLinearのオプションはTransition系のときと同様に複数指定すると想定外の動作をしてしまいます。

例えば、EaseInとEaseOutを指定するとLinearになってしまいますね。

(これに関してはLinearとEaseInOutを交換すると、EaseIn | EaseOut = EaseInOutになり、もし複数指定しても文字通りに動作するのでそうしたら良いのになと思いましたが、デフォルトがEaseInOutなので仕方ないみたいです。)

最後に

フラグが多いときはそれぞれのフラグを変数にするとフラグまみれになります。これは初心者のコードに多いパターンなのではないでしょうか。

このようなときは、UIViewAnimationOptionsのようにenumにしてビットで管理すると、一つの変数で管理できるのでコードをシンプルになりいいですね。

しかし、ビット演算は使いこなすと強力な武器になりますが、わからずに使うと今回取り上げたようなことが起こり、混乱のもとになります。

自作enumで同じようなことをするときには、実装する側も使う側も、オプションが同時に適用できるのか、片方しか適用できないのかを理解し実装に反映させることで思わぬバグを防ぐことができそうです。

PlaygroundでUIViewのアニメーションを簡単に確認

みなさんPlayground使ってますか? APIの確認などサクッと調べたいときに少ないコード量で調べられるので、活用すれば開発スピードがぐっと上がると思います。 そんな僕も今はまだ全然使いこなせていないので、これから便利な使い方を発見したときはまとめていきたいと思います。

今回はUIViewのアニメーションの確認です!

意外につまづきなかなかアニメーションしてくれなかったので、同じような状態の人は参考にしてもらえると良いと思います。

下準備

その1

View>Utilities>Show File Inspectorをクリック

右側にUtilityの画面が出てくるので、Playground SettingのRun in Full Simulatorにチェックを入れる。

チェックを入れた後は邪魔なので消しておく。View>Utilities>Hide Utilities

f:id:rayc5:20150617020919p:plain

その2

Assistant Editorを表示します。もし表示されていなかったら以下から表示できます。

View>Assistant Editor>Show Assistant Editor

実際にコードを書いてみる

今回僕はautoresizingMaskの動作がよくわからず調べてみたかったので、それを調べるようなコードを書いています。 ちなみにautoresizingMaskは親ビューのサイズが変更されたときに自分がどのように振る舞うかを決めるプロパティです。

ポイントはXCPlaygroundを使うところです。詳しくは以下で書きます。

import UIKit
import XCPlayground

var containerView = UIView(frame: CGRectMake(0, 0, 300, 300))
containerView.backgroundColor = UIColor.grayColor()
XCPShowView("Container View", containerView)

var subView = UIView(frame: CGRectMake(50, 50, 200, 200))
subView.backgroundColor = UIColor.blackColor()
containerView.addSubview(subView)

subView.autoresizingMask =
    .FlexibleHeight |
    .FlexibleBottomMargin |
    .FlexibleTopMargin |
    .FlexibleWidth |
    .FlexibleLeftMargin |
    .FlexibleRightMargin

UIView.animateWithDuration(2.0, animations: {
    containerView.frame = CGRectMake(0, 0, 400, 400)
})

XCPlaygroundについて

XCPlaygroundとは最初から用意されているユーティリティフレームワークです。 こちらをうまく利用することでPlaygroundで様々なことができるらしいです。

詳しくは公式ドキュメントを参照してください。(XCPlayground Module Reference)

使い方

XCPShowView(identifier: String, view: UIView)を利用することでViewを表示することができます。

こちらを利用することで、コードの変更時に自動でアニメーションをしてくれたり、タイムバーを利用してアニメーションの経過を詳細に見ることができます。

もしアニメーションしてくれないときは、Editor>Execute Playgroundからアニメーションさせることができると思います。

全体像はこんな感じ。コードと結果を一度に見ることができます。

f:id:rayc5:20150617074018p:plain

アニメーションはこんな感じでした。

f:id:rayc5:20150617063026g:plain

.FlexibleTopMarginと.FlexibleRightMarginだけにしたとき↓

f:id:rayc5:20150617074505g:plain

感想

変更をすぐに見ることができるので、アニメーションをテストしたり、細かく色を調節するときなんかに便利だと感じました。Viewの形を整えるときなんかにも良さそうですね。半径がどうだとか影がどうだとかいう処理をリアルタイムで確認できるのはメリットだと思います。

Playgroundで動作したら、そのコードをアプリに移植するように進めていくと開発スピードもあがりそうです。

SwiftでWunderlistのAPI使ってみた

Todoistに続き、WunderlistからもAPIを使ってデータを取ってみました。 これからはGitHubにサンプルも上げていこうと思うので、もしコードをみたい方がいればそちらも見てみてください。

GitHubのサンプルはこちら

全体的な流れはTodoistの時と基本は全く同じなのですが、Wunderlistの方がドキュメントはしっかりしており、パラメータも単にURLにつけるだけではなくHTTPのbodyやheaderにつけないといけなかったりしました。 しかし、パラメータのつけ方だけ気をつければ基本的には大丈夫だと思います!

参考サイト

Wunderlist API Documentation

事前準備

毎回のことですが、こちらからClient_IDとClient_Secretを取得します。

認証

認証もOAuthなので基本的にやることは同じです。

STEP1

リクエストを送信します。エンドポイントは以下のとおりです。 https://www.wunderlist.com/oauth/authorize?client_id=ID&redirect_uri=URL&state=RANDOM

パラメータは3つ

  • client_id : クライアントID
  • redirect_uri : リダイレクトURL
  • state : ランダムな文字列

Todoistのときはredirect_uriがoptionalでwebで設定できたのですが、Wunderlistではrequiredになっていました。

こちらは前回同様WebViewを利用してリクエストを送信しています。 以下のように認証用にWebViewを持つだけのシンプルなVCを作って実装しました。

class Wunderlist {
    private struct Info {
        static let client_id = "your_client_id"
        static let client_secret = "your_client_secret"
        static let redirect_uri = "https://localhost/"
        static let state = "state"
        
        static var code: String? = nil
        static var access_token: String? = nil {
            didSet {
                let ud = NSUserDefaults.standardUserDefaults()
                ud.setValue(access_token, forKey: Wunderlist.accessTokenPath)
            }
        }
    }

    private let authRequestURL = "https://www.wunderlist.com/oauth/authorize?client_id=\(Info.client_id)&redirect_uri=\(Info.redirect_uri)&state=\(Info.state)"

    // --<略>--
}

class WunderlistAuthController: UIViewController, UIWebViewDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
    
        webView = UIWebView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height))
        webView!.delegate = self
        view.addSubview(webView!)
        if let url = NSURL(string: Wunderlist.sharedInstance.authRequestURL) {
            webView!.loadRequest(NSURLRequest(URL: url))
        }
    }
}

STEP2

ユーザーが認証・許可をするとリダイレクトしてくれるので、WebViewでそれを拾って、access_tokenを取得するためのリクエストを送信します。

まず、URLからcodeを取得します。 shouldStartLoadWithRequestの中でリダイレクト先と一致していた場合、codeを取得するようにしました。

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    if let url = request.URL {
        if url.host == Wunderlist.sharedInstance.redirectHost {
            let query = url.query!
            // query forms "state=state&code=<code>"
            let code = query.substringFromIndex(advance(query.startIndex, 17))
            println("query: \(query),code: \(code)")
            Wunderlist.sharedInstance.getAccessToken(code)
            return true
        }
    }
    return true
}

次にリクエストをPOSTします。

パラメータは3つ

  • client_id: クライアントID
  • client_secret: クライアントsecret
  • code: 先ほど取得したcode

このパラメータなのですが、Todoistの時とは違い、HTTPBodyに記述します。 なのでコードは以下のような感じになります。

private let accessTokenRequestURL = "https://www.wunderlist.com/oauth/access_token"
private var accessTokenHTTPBody: String? {
    get {
        if let code = Info.code {
            return "client_id=\(Info.client_id)&client_secret=\(Info.client_secret)&code=\(code)"
        }
        return nil
    }
}

private func getAccessToken(code: String) {
    Info.code = code
    let request = NSMutableURLRequest(URL: NSURL(string: accessTokenRequestURL)!)
    request.HTTPMethod = "POST"
    var bodyData = accessTokenHTTPBody!
    request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
    NSURLConnection.sendAsynchronousRequest(request,
        queue: NSOperationQueue.mainQueue(),
        completionHandler: {
            (_, data, _) in
            let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
            let jsonResult = NSJSONSerialization.JSONObjectWithData(data,
                options: NSJSONReadingOptions.MutableContainers,
                error: nil) as! NSDictionary
            Info.access_token = jsonResult["access_token"] as? String
            self.delegate?.didAuthrization(true)
    })
}

これでリクエストを送信すると、無事access_tokenが取得できます。

データの取得

データ取得に関しての注意点

データを取得するためのリクエストにはaccess_tokenを添付する必要がありますが、wunderlistはHTTPのヘッダーの中に情報を格納するように要求しています。 また、X-Access-TokenとX-Client-IDというフィールドを作成する必要があります。 なので、リクエストを作成する際に、以下のようにしてヘッダーを追加します。

今回はエンドポイントを受け取りNSMutableURLRequestにして返す関数を作成しました。 その中でヘッダーにclient_idとaccess_tokenの情報を追加しています。

private func getRequest(endpoint: String) -> NSMutableURLRequest? {
    if let url = NSURL(string: endpoint) {
        let request = NSMutableURLRequest(URL: url)
   request.addValue(Info.access_token, forHTTPHeaderField: "X-Access-Token")
        request.addValue(Info.client_id, forHTTPHeaderField: "X-Client-ID")
        return request
    }
    return nil
}

Listの取得

taskを取得するためには、まずlistを取得してlist_idを知る必要があります。

listを取得するためのエンドポイントはhttps://a.wunderlist.com/api/v1/listsです。 パラメータは特に必要ないので、とりあえずリクエストをおくります。

WunderlistのAPIに関するクラスにgetメソッドを作り、引数としてenumで宣言しておいた何が欲しいかの情報を渡します。 返ってきたjsonからidやtitleなどの情報を取得します。

サンプルの中では、listを取得した際にidを利用してtaskを取得するようにしています。

Wunderlist.sharedInstance.get(.List) {
    if let array = $0 {
        for listObj in array {
            let list = listObj as! NSDictionary
            let id = list["id"] as! NSNumber
            let title = list["title"] as! String
            println("list: id=\(id), title=\(title)")
            self.getTasks(id.stringValue)
            self.lists.append(list)
        }
    }
}

Taskの取得

listを取得した後には、list_idを利用してtaskを取得します。

エンドポイントはhttps://a.wunderlist.com/api/v1/tasksで、必要なパラメータはlist_idです。 また、パラメータにcompletedを追加すると完了済み/未完了のタスクを指定して取得することもできます。

func getTasks(listID: String) {
    Wunderlist.sharedInstance.get(.Task, parameters: ["list_id":listID]) {
        if let array = $0 {
            for taskObj in array {
                let task = taskObj as! NSDictionary
                let id = task["id"] as! NSNumber
                let title = task["title"] as! String
                let completed = task["completed"] as! NSNumber
                let due = task["due_date"] as? String
                println("    task: id=\(id), title=\(title), due=\(due), completed=\(completed.boolValue)")
                self.tasks.append(task)
            }
        }
    }
}

SwiftからTodoistのTodoを取得する

TodoistとTodoistAPIで、毎朝のタスク報告をするヾ(‘ω’)ノを見てSwiftでTodoist使って何かできないかなーと思い、とりあえずAPI使ってみました。

参考サイト Todoist API Documentation

事前準備

こちらにてClient IDとClient Secretを取得する

認証

今回はWebViewを使って、認証をおこなえるようにしました。

STEP1

まずhttps://todoist.com/oauth/authorize?client_id=<CLIENT_ID>&scope=<SCOPE>&state=<STATE>&redirect_uri=<REDIRECT_URI>にリクエストを送ります。

パラメータは4つあります。

  • client_id: 事前準備で取得したClient IDです
  • scope: 権限の強さを指定します
    • task:add, data:read, data:read_write, data:delete, project:delete
  • state: 好きな文字列。CSRFから守るためとありました。
  • redirect_uri: リダイレクト先 (Optional, DeveloperサイトからDefaultを設定することができる)

STEP2

リクエストを送信すると、https://todoist.com/Users/showLogin?<PARAMETERS> が返ってきます。これは以下のようなGoogleかemailでログインすることができるビューです。 このビューをwebViewに表示しユーザーがログインできるようにします。 f:id:rayc5:20150601055724p:plain

STEP3

ユーザーがログインしたら、以下の「同意する」のビューになります。 ここで同意するをクリックすると、STEP1で指定したリダイレクト先にリダイレクトされます。

f:id:rayc5:20150601055719p:plain

このときに、パラメーターとしてstateとcodeの2つが添付されています。 このcodeが次に必要になるので、URLからcodeを取得します。

URLはhttps://<リダイレクト先>?state=<STATE>&code=<CODE>という形をしているので、urlからパラメータ部分を抽出し、stateの文字数がわかっているのでそれをもとにcodeの部分だけ抜き出しました。 (もうちょっとスマートにやりたいんですけど何かうまい方法無いのかな。。)

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        let url = request.URL!
        if url.host == "<リダイレクト先のホスト>" {
            let query = url.query!
            // stateの長さに依存する. 12+(stateの長さ). stateをstateに設定したので今回は17.
            let code = query.substringFromIndex(advance(query.startIndex, 17))
            
            // codeを利用してaccess_tokenを取得する処理
            // -- <略> --
        }
        return true
    }

STEP4

先ほど取得したcodeを利用してaccess_tokenを取得します。 https://todoist.com/oauth/access_token?client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&code=<CODE>にPOSTリクエストを送信します。

ここでのパラメータは3つです。

  • client_id: 事前準備で取得したClient ID
  • client_secret: 事前準備で取得したClient Secret
  • code: STEP3で取得したcode

これでaccess_tokenを取得できるので、大事に保存しましょう。

認証で引っかかった問題

STEP2でGoogleでログインしようとしたときにうまくリダイレクトしてくれませんでした。 メールならログインした後に以下の画面が表示されるのですが、Googleのときはローディングで一切進みません。

以下の画面を表示するために、Googleでログインが完了したタイミングでもう一度STEP1に戻ってリクエストを送信しました。 STEP1のリクエストですが、すでにログインしている場合はメールアドレス/パスワードを入力する画面ではなく、同意するかどうかの画面に飛びます。 なので、もう一度リクエストを送信して同意するかどうかの画面を表示するようにしました。

データ取得

タスクの取得

タスクのリストを取得するためにはhttps://todoist.com/API/v6/sync?token=<TOKEN>&seq_no=0&seq_no_global=0&resource_types=[\"items\"]にリクエストを送信します。

パラメータは以下の4つです。

  • token: アクセストークン
  • seq_no: Sequence number. 初めは0, 2回目以降は前回に受け取ったものを利用する.
  • seq_no_global: Global sequence number. 初めは0, 2回目以降は前回に受け取ったものを利用する.
  • resource_types: 取得したい情報を入力。リスト形式で複数渡すことが可能。
    • パラメータの種類は, projects, items, labels, notes, filters, reminders, locations, user, live_notifications, day_orders, allがある.

こんな感じで取得できました。

func getTasks() {
    let request = NSMutableURLRequest(URL: NSURL(string: getTasksURL)!)
    NSURLConnection.sendAsynchronousRequest(request,
        queue: NSOperationQueue.mainQueue(),
        completionHandler: {
            (res, data, error) in
            let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
            let jsonResult = NSJSONSerialization.JSONObjectWithData(data,
                options: NSJSONReadingOptions.MutableContainers,
                error: nil) as! NSDictionary
            
            let tasks = jsonResult["Items"] as! NSArray
            for task  in tasks {
                let task = task as! NSDictionary
                let id = task["id"] as! NSNumber
                let title = task["content"] as! String
                let date = task["due_date"] as! String
                println("id: \(id.stringValue), title: \(title), date: \(date)")
            }
    })
}
// 出力結果
id: 71504480, title: today's task 2, date: Mon 01 Jun 2015 04:59:59 +0000
id: 71504414, title: today's task 1, date: Mon 01 Jun 2015 04:59:59 +0000
id: 71504481, title: tomorrow's task 1, date: Tue 02 Jun 2015 04:59:59 +0000

まとめ

よくわからないところで引っかかり、ググってもイマイチ情報が出てこないので時間とられました。 Documentのサンプルがうまく動かないとかは止めて欲しいですね。

サンプルとして全体像を載せておきます。 適当すぎですが、とりあえずは動くはず。。

Todoist_API_sample

SwiftでGoogle Calendarの情報を取得してみた

SwiftからGoogleのCalendar APIを利用してカレンダー情報を取得してみました。 内容は認証の後、取得したカレンダーのデータをコンソールに出力するだけです。

参考サイト

Google Developer iOS Quickstart

事前準備

Google Developer iOS Quickstart のStep1に従ってClient IDを取得

CocoaPodでライブラリをインストール

Podfileに以下を追加し、pod install します。

pod 'Google-API-Client/Calendar', '~> 1.0'

Swiftから利用できるようにBridging-Headerに以下の情報を追加します。

#import "GTMOAuth2ViewControllerTouch.h"
#import "GTLCalendar.h"

カレンダーの情報を取得

import UIKit

class ViewController: UIViewController
{
    private var calendarService: GTLServiceCalendar? = nil
    
    private struct Info {
        static let KeychainItemName: String = "Google Calendar Quickstart"
        static let ClientID: String = "YOUR_CLIENT_ID_HERE"
        static let ClientSecret: String = "YOUR_CLIENT_SECRET_HERE"
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 端末に保存されている情報を読み込む
        calendarService = GTLServiceCalendar.init()
        calendarService?.authorizer = GTMOAuth2ViewControllerTouch.authForGoogleFromKeychainForName(
            Info.KeychainItemName,
            clientID: Info.ClientID,
            clientSecret: Info.ClientSecret)
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        // 認証がまだなら認証画面を出す. 認証済みならデータを取得
        if let canAuthorize = calendarService?.authorizer.canAuthorize {
            if !canAuthorize {
                self.presentViewController(createAuthController(), animated: true, completion: nil)
            } else {
                fetchEvents()
            }
        }
    }
    
    // 認証画面を作成して返す
    private func createAuthController() -> GTMOAuth2ViewControllerTouch{
        let authController = GTMOAuth2ViewControllerTouch(
            scope: kGTLAuthScopeCalendarReadonly,
            clientID: Info.ClientID,
            clientSecret: Info.ClientSecret,
            keychainItemName: Info.KeychainItemName,
            delegate: self,
            finishedSelector: "viewController:finishedWithAuth:errorOrNil:")
        return authController
    }
    
    // カレンダーのイベントを取得してコンソールに出力
    private func fetchEvents() {
        // クエリの設定.イベントは10個, 時間順にソート
        let query = GTLQueryCalendar.queryForEventsListWithCalendarId("primary") as! GTLQueryCalendar
        query.maxResults = 10
        query.timeMin = GTLDateTime(date: NSDate(), timeZone: NSTimeZone.localTimeZone())
        query.singleEvents = true
        query.orderBy = kGTLCalendarOrderByStartTime
        
        // クエリ実行. 完了後の処理はClosureで渡す
        calendarService?.executeQuery(query, completionHandler:{ (_, events, errorOrNil) -> Void in
            if let error = errorOrNil {
                self.showAlert("Error", message:"Unable to get calendar events.")
            } else {
                var result = ""
                if let items = events.items {
                    if items.count == 0 {
                        result = "No upcoming events found."
                    } else {
                        result = "Upcoming 10 events:\n"
                        for object in items {
                            if let event = object as? GTLCalendarEvent {
                                let start: GTLDateTime = event.start.dateTime ?? event.start.date
                                let startString = NSDateFormatter.localizedStringFromDate(
                                    start.date,
                                    dateStyle: NSDateFormatterStyle.ShortStyle,
                                    timeStyle: NSDateFormatterStyle.ShortStyle)
                                // イベントは開始時間とタイトルを出力
                                result += "\(startString) - \(event.summary)\n"
                            }
                        }
                    }
                } else {
                    result = "events.items() == nil\n"
                }
                // 結果の出力
                println(result)
            }
        })
    }
    
    // 認証後に呼ばれる関数.完了したら認証画面を閉じてイベントの取得.
    func viewController(viewController: GTMOAuth2ViewControllerTouch, finishedWithAuth authResult: GTMOAuth2Authentication, errorOrNil: NSError?) {
        if let error = errorOrNil {
            showAlert("Authentication Error", message: error.localizedDescription)
            calendarService?.authorizer = nil
        } else {
            calendarService?.authorizer = authResult;
            self.dismissViewControllerAnimated(true, completion: nil)
        }
    }
    
    // エラーを表示する
    private func showAlert(title: String, message: String) {
        let alert = UIAlertView(title: title, message: message, delegate: nil, cancelButtonTitle: "OK")
        alert.show()
    }
    
}

感想

SwiftからObjective-cのライブラリを利用するので、Optionalの扱いに気をつけないといけないですねー。

Xcodeはoption(alt)を押して変数をクリックすると型などの情報が表示されるので、それを使いながら確認して進めていきました。