炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Facebook, Twitter, Line の SNS 連携を取りまとめる LoginModel を作る


Swift2.2, Xcode7

最近は SNS ログインが増えてきたので Facebook, Twitter, Line の各 SNS のログイン処理をプロトコルを使って共通化して書く方法をご紹介します。

  1. Swift でのポリモーフィズムのような実装を Protocol で行う
  2. 各 SNS の認証処理を行う Model を作成する
  3. 各 SNSログインの処理を管理する LoginModel を作成する

Facebook, Twitter, Line の SDK をアプリへの設置が完了していることを前提に書いていますので、SDK の導入方法は各公式サイトを参考にしてください。Facebook と Twitter は Acounts.Framework ではなく、SDK の利用例です。

1. Swift でのポリモーフィズムのような実装を Protocol で行う

SNS共通で行う処理を定義するプロトコルを作成します。今回は以下のようにインターフェースを設計しました。

SNSAuthenticateProtocol.swift

/**
 SNS連携を管理する Model で採用するプロトコル
*/
protocol SNSAuthenticateProtocol {
    // 各 SNS 固有の uid を保持する
    var snsID: String { get }
    
    // 各 SNS の認証で取得した token を保持する
    var snsToken: String { get }
    
    // 各 SNS サービスの認証結果を処理する
    func authenticate(fromViewController: UIViewController)
    
    // 各 SNS サービスのユーザー情報を取得する
    func fetchUserData()
}

今回の各 SNS を enum で定義します。

LoginType.swift

enum LoginType {
    case Line
    case Facebook
    case Twitter
    
    var snsModel: SNSAuthenticateProtocol {
        switch self {
        case .Line:
            return LineModel.sharedInstance
        case .Facebook:
            return FacebookModel.sharedInstance
        case .Twitter:
            return TwitterModel.sharedInstance
        }
    }

    var snsID: String {
        return self.snsModel.snsID
    }

    var snsToken: String {
        return self.snsModel.snsToken
    }
}

SNSAuthenticateProtocol は FacebookModel, TwitterModel, LineModel で採用します。採用したクラスでは snsID や authenticate が実装されることが保証されるので、以下のような感じで SNS の type をそこまで意識せずに認証処理を実行することができます。

func request(loginType: LoginType, fromViewController: UIViewController) {
    self.loginType = loginType
    loginType.snsModel.authenticate(fromViewController)
}

func fetchUserData(loginType: LoginType) {
    loginType.snsModel.fetchUserData()
}

2. 各 SNS の認証処理を行う Model を作成する

各SNS の認証処理をそれぞれ実装します。その際 SNSAuthenticateProtocol を採用します。

FacebookModel.swift

import FBSDKLoginKit

class FacebookModel: NSObject, SNSAuthenticateProtocol {
    static let sharedInstance = FacebookModel()
    var snsID = ""
    var snsToken = ""
    
    // MARK: SNSAuthenticateProtocol
    func authenticate(fromViewController:UIViewController) {
        guard FBSDKAccessToken.currentAccessToken() == nil else {
            self.setLoginInformation(FBSDKAccessToken.currentAccessToken())
            // ログイン済みの時の処理とか書く
            return
        }
        
        let manager = FBSDKLoginManager()
        manager.loginBehavior = .SystemAccount
        manager.logInWithReadPermissions(["public_profile"], fromViewController: fromViewController) {
            (result: FBSDKLoginManagerLoginResult?, error: NSError?) -> Void in
            guard let loginResult = result else {
                return
            }
            guard error == nil else {
                return
            }
            if loginResult.isCancelled {
                return
            }
            self.setLoginInformation(loginResult.token)
            // ログイン後の処理とか書く
        }
    }
    
    func fetchUserData() {
        let graphRequest = FBSDKGraphRequest(graphPath: "me", parameters: ["fields": "name"])
        graphRequest.startWithCompletionHandler({ (connection, result: AnyObject?, error) -> Void in
            guard error == nil else {
                return
            }
            guard let user = result else {
                return
            }
            // ユーザー情報取得後の処理とか書く
        })
    }
    
    // MARK: private method
    private func setLoginInformation(accessToken: FBSDKAccessToken) {
        self.snsID = accessToken.userID
        self.snsToken = accessToken.tokenString
    }    
}

TwitterModel.swift

import TwitterKit

class TwitterModel: NSObject, SNSAuthenticateProtocol {
    static let sharedInstance = TwitterModel()
    var snsID = ""
    var snsToken = ""
    var loginSession: TWTRSession?
    
    // MARK: SNSAuthenticateProtocol
    func authenticate(fromViewController: UIViewController) {
        Twitter.sharedInstance().logInWithCompletion { (session: TWTRSession?, error: NSError?) in
            if let error = error {
                if error.code == TWTRLogInErrorCode.Canceled.rawValue {
                    return
                }
                return
            }
            guard let user = session else {
                return
            }
            self.loginSession = session
            self.setLoginInformation(user)
            // ログイン後の処理を書く
        }
    }
    
    func fetchUserData() {
        // ユーザー情報の取得処理とか書く。
        // Facebook と違って Twitter はユーザー情報が session の中に含まれているので、
        // 再リクエストの必要はない。 self.loginSession にユーザー情報は入っている。
        print(self.loginSession)
    }
    
    private func setLoginInformation(user: TWTRSession) {
        self.snsID = user.userID
        self.snsToken = user.authToken
    }
}

LineModel.swift

class LineModel: NSObject, SNSAuthenticateProtocol {
    static let sharedInstance = LineModel()
    var snsID = ""
    var snsToken = ""
    var loginSession: AnyObject?
    private let lineAdapter = LineAdapter.adapterWithConfigFile()
    
    override init() {
        super.init()
        NSNotificationCenter.defaultCenter().addObserver(
            self,
            selector: #selector(self.lineAuthorizationDidChange(_:)),
            name: LineAdapterAuthorizationDidChangeNotification,
            object: nil)
    }
    
    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
    
    // MARK: SNSAuthenticateProtocol
    func authenticate(fromViewController: UIViewController) {
        guard self.lineAdapter.canAuthorizeUsingLineApp else {
            return
        }
        // 既に認証済みだったらログインを行う
        if self.lineAdapter.authorized {
            self.login()
        } else {
            self.lineAdapter.authorize()
        }
    }
    
    func fetchUserData() {
        // ユーザー情報の取得処理とか書く。
        // Facebook と違って Line はユーザー情報が session の中に含まれているので、
        // 再リクエストの必要はない。 self.loginSession にユーザー情報は入っている。
        print(self.loginSession)
    }
    
    /**
     Line認証後の判定処理
     - parameter notification:通知オブジェクト
     */
    func lineAuthorizationDidChange(notification: NSNotification) {
        guard notification.userInfo?["error"] == nil else {
            return
        }
        guard self.lineAdapter.authorized else {
            return
        }
        self.login()
    }
    
    // private method
    private func login() {
        self.lineAdapter.getLineApiClient().getMyProfileWithResultBlock {[unowned self] (session, error) -> Void in
            guard error == nil else {
                return
            }
            self.loginSession = session
            self.setLoginInformation(session)
            // ログイン処理とか書く
        }
    }
    
    private func setLoginInformation(session: AnyObject) {
        guard let id = session["mid"] as? String else {
            return
        }
        guard let token = self.lineAdapter.getLineApiClient().accessToken else {
            return
        }
        self.snsID = id
        self.snsToken = token
    }
}

3. 各 SNSログインの処理を管理する LoginModel を作成する

LoginModel.swift

class LoginModel: NSObject {
    static let sharedInstance = LoginModel()
    var loginType: LoginType?

    func request(loginType: LoginType, fromViewController: UIViewController) {
        self.loginType = loginType
        loginType.snsModel.authenticate(fromViewController)
    }

    func fetchUserData(loginType: LoginType) {
        loginType.snsModel.fetchUserData()
    }

    // ログイン成功を受けて self.fetchUserData(loginType) を実行したりする。
}

これで準備が完了です。実際にログインを行う LoginViewController の実装です。

LoginViewController.swift

class LoginViewController: UIViewController {    

    ... 省略

    // MARK: IBAction
    @IBAction func lineLogin() {
        LoginModel.sharedInstance.request(.Line, fromViewController: self)
    }

    @IBAction func facebookLogin() {
        LoginModel.sharedInstance.request(.Facebook, fromViewController: self)
    }

    @IBAction func twitterLogin() {
        LoginModel.sharedInstance.request(.Twitter, fromViewController: self)
    }
}

スッキリでいい感じですね!