FirebaseAuth+RxSwift

Sunday, April 16, 2017

Firebase Authでのサインイン・サインアウトの処理をRxSwiftを使ってイベントとして流せるようにしました。
また、RxSwift3.3.0から Single, Maybe, Completable が使えるようになったので、それらを使って実装してみます。

  • Swift 3.1
  • RxSwift 3.4.0

サインインの処理をSingleを使って

サインインの処理は Single を使って実装してみます。
Single だと1回だけ success(next)かerrorが流れるのを保証できます。

extension Reactive where Base: FIRAuth {
    private static func parseUserResponse(_ observer: @escaping (SingleEvent<FIRUser>) -> Void) -> (FIRUser?, Error?) -> Void {
        return { user, error in
            if let user = user {
                observer(.success(user))
            } else if let error = error {
                observer(.error(error))
            } else {
                fatalError()
            }
        }
    }

    public static func signIn(withEmail email: String, password: String) -> Single<FIRUser> {
        return Single.create { observer in
            if let auth = FIRAuth.auth() {
                auth.signIn(withEmail: email, password: password, completion: parseUserResponse(observer))
            } else {
                observer(.error(FIRAuth.AuthError.notInitialized))
            }
            return Disposables.create()
        }
    }

    public static func signIn(withCustomToken customToken: String) -> Single<FIRUser> {
        return Single.create { observer in
            if let auth = FIRAuth.auth() {
                auth.signIn(withCustomToken: customToken, completion: parseUserResponse(observer))
            } else {
                observer(.error(FIRAuth.AuthError.notInitialized))
            }
            return Disposables.create()
        }
    }

    public static func signInAnonymously() -> Single<FIRUser> {
        return Single.create { observer in
            if let auth = FIRAuth.auth() {
                auth.signInAnonymously(completion: parseUserResponse(observer))
            } else {
                observer(.error(FIRAuth.AuthError.notInitialized))
            }
            return Disposables.create()
        }
    }
}

extension FIRAuth {
    public enum AuthError: Error {
        case notInitialized
    }
}

Reactiveのextensionとして実装します。サインインの処理は様々あるのですが、その処理が完了した時のcallbackの引数はどれも同じなので、parseUserResponseという関数を用意して、userがあれば success を、errorがあれば error を流すように実装します。 また、funcではなくてstatic func としているのは、 FIRAuth.auth()FIRAuth? 型で、イベントを組む時に意識しないといけないため、それを関数内で処理するようにしています。
基本的にはFIRApp.configureで正常にsetupできていればnilになることは少ないですが。nilであった場合は、FIRAuth.AuthError.notInitializedを流すようにしています。

使用例はこのようになります。

FIRAuth.rx.signInAnonymously().subscribe(onSuccess: { user in
    print(user)
}, onError: { error in
    print(error)
}).addDisposableTo(disposeBag)

そういえば3.4.0からは Singlesubscribe(onSuccess:onError) が追加されたのでsubscribe時の処理が書きやすくなりました。

FIRAuthCredential を使ったサインイン処理は一旦スキップして後ほど。

サインアウトの処理をCompletableを使って

サインアウトの処理は成功時に値を流す必要はなく、完了したかエラーだったかを返せれば良いので Completable を使います。

public static func signOut() -> Completable {
    return Completable.create { observer in
        if let auth = FIRAuth.auth() {
            do {
                try auth.signOut()
                observer(.completed)
            } catch let error {
                observer(.error(error))
            }
        } else {
            observer(.error(FIRAuth.AuthError.notInitialized))
        }
        return Disposables.create()
    }
}


使用例はこのようになります。

FIRAuth.rx.signOut().subscribe(onCompleted: {
    print("signed out")
}, onError: { error in
    print(error)
}).addDisposableTo(disposeBag)

FIRAuthCredentialを用いたサインイン処理

最後にFIRAuthCredentialを用いたサインイン処理ですが、Google,Twitter,Github等の AuthProvider があるので、それらを纏めてより簡単にサインイン処理が行えるようにしてみます。

extension FIRAuth {

    public enum AuthProvider {
        case emailPassword(String, String) // email, password
        case facebook(String) // accessToken
        case google(String, String) // IDToken, accessToken
        case twitter(String, String) // token, secret
        case github(String) // token

        var credential: FIRAuthCredential {
            switch self {
            case .emailPassword(let email, let password):
                return FIREmailPasswordAuthProvider.credential(withEmail: email, password: password)
            case .facebook(let accessToken):
                return FIRFacebookAuthProvider.credential(withAccessToken: accessToken)
            case .google(let idToken, let accessToken):
                return FIRGoogleAuthProvider.credential(withIDToken: idToken, accessToken: accessToken)
            case .twitter(let token, let secret):
                return FIRTwitterAuthProvider.credential(withToken: token, secret: secret)
            case .github(let token):
                return FIRGitHubAuthProvider.credential(withToken: token)
            }
        }
    }
}

extension Reactive where Base: FIRAuth {
    public static func signIn(authProvider: FIRAuth.AuthProvider) -> Single<FIRUser> {
        return Single.create { observer in
            if let auth = FIRAuth.auth() {
                auth.signIn(with: authProvider.credential, completion: parseUserResponse(observer))
            } else {
                observer(.error(FIRAuth.AuthError.notInitialized))
            }
            return Disposables.create()
        }
    }
}

FIRAuthの下にAuthProviderというenumを作り、それぞれのプロバイダに必要な情報を渡してcredentialを作成する処理を実装します。
そのFIRAuth.AuthProviderを使ってサインインする処理を Single を使って実装すれば完了です。
使用例はこのようになります。

GithubOAuth.authorize()
    .flatMap { accessToken -> Single<FIRUser> in
        FIRAuth.rx.signIn(authProvider: .github(accessToken))
    }.subscribe(onSuccess: { user in
        print(user)
    }, onError: { error in
        print(error)
    }).addDisposableTo(disposeBag)

さいごに

新しく増えたSingle, Maybe, Completableの勉強がてら、FirebaseAuthでの処理をrx化してみました。
ソース(全容)は Gistに置いておきます。

techRxSwiftFirebase

RxJavaのcomposeっぽいものをRxSwiftで

iOS10で通知済みのローカル通知を削除する