使用 Facebook 在 iOS Swift 中自动登录 & Google

Auto login in iOS Swift using Facebook & Google

我知道这是一个设计问题,并不是真正的技术问题,但它听起来很常见,所以我认为它也适用于此。

我有一个 iOS 应用程序(用 Swift 编写)应该支持 3 种登录方法:Facebook、Google 和使用电子邮件和密码注册。

一旦用户第一次登录,我想记住他使用的是哪种方法,并在下次应用程序启动时自动登录(当然假设它没有留在后台)。

对于Google我可以使用下面的代码:

let googleSignIn = GPPSignIn.sharedInstance()
if googleSignIn.trySilentAuthentication() {
    // was able to login automatically using google
    // no need to show login screen
}

此代码是 UI 独立的,这意味着我可以从 AppDelegate 调用它并提前知道我是应该显示登录屏幕还是自动跳到应用程序的主屏幕。

看起来 Facebook 中的等效功能是 ViewController 实现 FBLoginViewDelegate 协议,具有 FBLoginView 按钮,并将其自身指定为 FBLoginView 的委托:

class LoginViewController: UIViewController, FBLoginViewDelegate, GPPSignInDelegate {

    @IBOutlet weak var fbLoginView: FBLoginView!

    override func viewDidLoad() {
        self.fbLoginView.delegate = self

     }
 ...

对于 Facebook 自动登录,我可以在此视图控制器中使用以下代码:

func loginViewFetchedUserInfo(loginView : FBLoginView!, user: FBGraphUser) {
      // logged in automatically using facebook, open main page
 }

然而,这段代码是 UI-dependent,这意味着我必须实例化 ViewController,否则 loginViewFetchedUserInfo 回调将不会触发,我不会'不知道用户使用 Facebook 自动登录。

当然,如果我想在用户使用电子邮件和密码注册后处理自动登录,我需要自己管理它,在这种情况下我可以做的只是使用 NSUserDefaults 保存凭据。

基本上我也可以使用相同的机制来记住用户使用的登录方法,但我想对于 Facebook 和 Google 我仍然应该调用他们适当的 API 来实际执行自动登录。

我遇到的问题是 Facebook 没有 UI-independent API 像 Google,我必须 "pass through" 登录屏幕,尽管我最终可能不需要它。

另一个问题是 Facebook 事件是异步触发的,所以我需要 "suspend" 登录按钮演示直到它被触发,虽然我可以很容易地知道用户过去已经登录到 Facebook,通过使用 NSUserDefaults 保持指示,就像我在注册案例中所做的那样,无论如何我都想知道我应该使用哪个 API 来验证自动登录。

关于Google登录,不清楚trySilentAuthentication返回的值是否足以验证登录,还是我还需要等待GPPSignInDelegate.finishedWithAuth触发,所以需要实例化LoginView 也在这种情况下。

这原来是一个很长的问题,但同样,这听起来像是一个很常见的场景,所以希望我能得到一些见解。

万一有人遇到类似问题,我缺少的 Facebook API 是 FBSession.openActiveSessionWithAllowLoginUI

我的代码是这样的:

class LoginHelper : NSObject, GPPSignInDelegate {

    private let googleClientID = "myClientID"
    private let userDefaultsLoginMethodKey = "LoginMethod"
    private let userDefaultsAppTokenKey = "AppToken"

    var googleSignIn: GPPSignIn

    // google auto login is async, need to save handlers
    var autoLoginCompletionHandler : (() -> Void)?
    var autoLoginErrorHandler : ((err: String) -> Void)?

    class var sharedInstance : LoginHelper {
        struct Singleton {
            static let instance : LoginHelper = LoginHelper()
        }
        return Singleton.instance
    }

    enum LoginMethod : String {

        case NotLoggedIn = "NotLoggedIn"
        case Facebook = "facebook"
        case Google = "google"
        case MyApp = "MyApp"

        func token() -> String? {
            switch (self) {
            case .Google:
                if LoginHelper.sharedInstance.googleSignIn.authentication != nil {
                    return LoginHelper.sharedInstance.googleSignIn.authentication.accessToken
                }
            case .Facebook:
                if FBSession.activeSession().isOpen {
                    if let tokenData = FBSession.activeSession().accessTokenData {
                        return tokenData.accessToken
                    }
                }
            default:
                return nil
            }
            return nil
        }
    }

    var loginMethod : LoginMethod {

        get {
            let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
            if let method = defaults.stringForKey(userDefaultsLoginMethodKey) {
                if let loginMethod = LoginMethod(rawValue: method) {
                    return loginMethod
                }
            }
            return LoginMethod.NotLoggedIn
        }
        set {
            let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
            defaults.setValue(newValue.rawValue, forKey: userDefaultsLoginMethodKey)
        }
    }

    var accessToken : String? {

        get {
            let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
            if let token = defaults.stringForKey(userDefaultsAppTokenKey) {
                return token
            }
            return nil
        }
        set {
            let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
            defaults.setValue(newValue, forKey: userDefaultsAppTokenKey)
        }
    }

    private override init() {
        googleSignIn = GPPSignIn.sharedInstance()
        googleSignIn.clientID = googleClientID
        googleSignIn.shouldFetchGooglePlusUser = true
        googleSignIn.shouldFetchGoogleUserEmail = true
        googleSignIn.scopes = [kGTLAuthScopePlusLogin, kGTLAuthScopePlusUserinfoEmail]
    }

    func loginToServer(method : LoginMethod, completionHandler : (() -> Void)? = nil, errorHandler : ((err : String) -> Void)? = nil) {
        if let token = loginMethod.token() {
            // login to server with token
        }
    }

    func autoLogin(completionHandler : () -> Void, errorHandler : (err : String) -> Void) {

        switch (loginMethod) {

        case .Google:

            googleSignIn.delegate = self
            if googleSignIn.trySilentAuthentication() {
                // actual login will occur when finishedWithAuth is triggered, 
                // keep completionHandler & errorHandler to be used later
                autoLoginCompletionHandler = completionHandler
                autoLoginErrorHandler = errorHandler
            } else {
                errorHandler(err: "silent authentication failed for Google")
            }

        case .Facebook:
            if FBSession.openActiveSessionWithAllowLoginUI(false) {
                loginToServer(loginMethod, completionHandler: completionHandler, errorHandler: errorHandler)
            } else {
                errorHandler(err: "failed to login automaticlly to Facebook")
            }
        default:
            Logger.debug("Skip server login for \(loginMethod.rawValue)")
            completionHandler()
        }
    }

    func finishedWithAuth(auth: GTMOAuth2Authentication,  error: NSError! ) -> Void {
        if let err = error {
            Logger.error("Failed to login with Google")
            autoLoginErrorHandler?(err: "failed to login automaticlly to Google")
        } else {
            // login to server
            loginToServer(LoginMethod.Google, completionHandler: autoLoginCompletionHandler, errorHandler: autoLoginErrorHandler)
        }
    }

    func didDisconnectWithError(error: NSError!) -> Void{
    }

}

请注意,LoginHelper 必须 扩展 NSObject,如 this post 中所述。