開発中のプロジェクトでGoogle APIを使うことになり、アカウント管理を含めGoogleサインインを導入しました。
ただ、その際に公式のframeworkのまま使おうとした場合に、今回の要件上イケてないと感じた部分がいくつかあったため、
RxSwiftを用いた拡張フレームワークを作成しました。
今回の実装のサンプルコードはオープンソースで公開しました。
前提条件
- Swift 4.2
- Xcode 10.0
- 公式ドキュメント通りに動かすことはできてる前提( Cloud Console上の設定、ライブラリ導入など)
導入方法で参考にしたもの
イケてないポイント
今回取り組んだイケてないポイントは次の2点です。
1. DelegateじゃなくRxで非同期に処理を扱いたい
2. signInSilently() できない!?(扱いづらい)
1. DelegateじゃなくRxで非同期に処理を扱いたい
GoogleSignIn FrameworkはDelegateを利用し、ログインアクションの結果を受け取る仕様になっています。
しかし今回のアプリではRxSwiftを使用しMVVMで実装を行なっていたため、できればログインアクションをリアクティブに
行い、関連処理をVM内のメソッドチェーンでコンパクトに処理したいと思いました。
RxSwiftにはDelegateメソッドを扱うためのDelegateProxyというものがあったので今回 @tako3a の記事
とRxSwiftのGithub上に上がっていたIssueを参考にGIDSignInクラスのRxエクステンションを実装しました。
GIDSignInDelegateのDelegateProxyを実装
import GoogleSignIn
import RxSwift
import RxCocoa
class RxGIDSignInDelegateProxy: DelegateProxy<GIDSignIn, GIDSignInDelegate>,GIDSignInDelegate {
public weak private(set) var gidSignIn: GIDSignIn?
var signInSubject = PublishSubject<GIDGoogleUser>()
init(gidSignIn: ParentObject) {
self.gidSignIn = gidSignIn
super.init(parentObject: gidSignIn, delegateProxy: RxGIDSignInDelegateProxy.self)
}
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if let u = user {
signInSubject.on(.next(u))
} else if let e = error {
signInSubject.on(.error(e))
}
_forwardToDelegate?.sign(signIn, didSignInFor:user, withError: error)
}
deinit {
signInSubject.on(.completed)
}
}
extension RxGIDSignInDelegateProxy :DelegateProxyType {
static func registerKnownImplementations() {
register { RxGIDSignInDelegateProxy(gidSignIn: $0) }
}
static func currentDelegate(for object: GIDSignIn) -> GIDSignInDelegate? {
return object.delegate
}
static func setCurrentDelegate(_ delegate: GIDSignInDelegate?, to object: GIDSignIn) {
object.delegate = delegate
}
}
extension Reactive where Base: GIDSignIn {
public var delegate: DelegateProxy<GIDSignIn, GIDSignInDelegate> {
return self.gidSignInDelegate
}
var signIn: Observable<GIDGoogleUser> {
let proxy = self.gidSignInDelegate
proxy.signInSubject = PublishSubject<GIDGoogleUser>()
return proxy.signInSubject
.asObservable()
.do(onSubscribed: {
proxy.gidSignIn?.signIn()
})
.take(1)
.asObservable()
}
}
細かい説明は省きますが、DelegateProxyとしてRxGIDSignInDelegateProxy
を定義し、
signInSubject:PublishSubject<GIDGoogleUser>
を変数として持たせます。
Reactive Extention上で
signIn: Observable<GIDGoogleUser>
としてそれをラップした上でdoメソッドとしてGIDSignInクラスのsignIn()を呼んでいます。
利用する側としては
let login = GIDSignIn.sharedInstance().rx.login
.flatMap({ (user:GIDUser) in
//GIDUserを使ってなんか処理
})
.share(replay: 1)
のようにDelegateの実装なしに晴れてメソッドチェーンを繋げていくことができます!
2. signInSilently() できない!?(扱いづらい)
無事にスマートなログイン処理が実装できたのですが、この後も問題が発覚しました。
ログインを終えたユーザにはGIDSignInクラスにはsignInSilentlyというメソッドが用意されています。
// Attempts to sign in a previously authenticated user without interaction. The delegate will be
// called at the end of this process indicating success or failure.
- (void)signInSilently;
ところが下記のコードではユーザ情報が取得できませんでした。
import UIKit
import GoogleSignIn
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GIDSignIn.sharedInstance().clientID = "your client ID"
GIDSignIn.sharedInstance().scopes = ["your scopes"]
GIDSignIn.sharedInstance().signInSilently()
//ログインを1度した後再度起動するとtrueが返ってくる
if GIDSignIn.sharedInstance().hasAuthInKeychain() {
print(GIDSignIn.sharedInstance()?.currentUser)
//なぜか常にnil。。
}
return true
}
currentUserを呼ぶタイミングを遅延させたりいろいろと試してみましたが常にnilが返ってきてしまいます。
この現象は下記のStackOverFlowでも報告されていました。
GIDSignIn.sharedInstance().currentUser nil when relaunching app
解決策
類似のStackOverFlowを探っていくと以下の情報を見つけました
Swift+GIDSignIn - hasAuthInKeyChain returns true but currentUser is still nil after calling signInSilently
I had a similar issue. First I thought it was a timing issue. By invoking signIn.signInSilently() after fully loading the root viewController it did work. I am now 99% sure I solved it by executing signIn.signInSilently() on the main thread through
どうやら メインスレッドで動作させた方が良さそうだと言っています。
また、 仕様で書かれている通りDelegateメソッドで結果を受け取った方が確実だと考えました。
ということで、signin()メソッドと同様に、ViewController上からRxエクステンションとして実装しました。
GIDSignInDelegateのDelegateProxyを実装
extension Reactive where Base: GIDSignIn {
public var delegate: DelegateProxy<GIDSignIn, GIDSignInDelegate> {
return self.gidSignInDelegate
}
var signIn: Observable<GIDGoogleUser> {
let proxy = self.gidSignInDelegate
proxy.signInSubject = PublishSubject<GIDGoogleUser>()
return proxy.signInSubject
.asObservable()
.do(onSubscribed: {
proxy.gidSignIn?.signIn()
})
.take(1)
.asObservable()
}
//下記を追加実装
var signInSilent: Observable<GIDGoogleUser> {
let proxy = self.gidSignInDelegate
proxy.signInSubject = PublishSubject<GIDGoogleUser>()
return proxy.signInSubject
.asObservable()
.do(onSubscribed: {
proxy.gidSignIn?.signInSilently()
})
.take(1)
.asObservable()
}
}
これでユーザ情報が必要な場面でsignIn()と同様に呼び出すことでGIDUserを取得することができるようになりました。
今回の実装のサンプルコード
https://github.com/kazumanagano/GIDSignIn-Rx