はじめに
お疲れ様です。かむいです。
この記事はiOS Advent Calendar 2019の17日目の投稿となります。
前回は@codelynxさんの名前付き UIView と後付けストアドプロパティもどきでした。
先月Firebase AuthenticationでSign in with Appleが利用できるとの発表がありました。
firebase.googleblog.com
それまでは@fromkkさんが書かれていたようなCloud Functionsを利用したやり方でも近い仕組みを実現できていましたが、やはりFirebase Authentication単体で処理が実現できるのは嬉しい限りです。 qiita.com
今回はこのFirebase Authを利用した実装の紹介だけでなく、実装の中で登場するDelegateメソッドをRx化し、RxSwiftを利用しているケースを想定した値の取り方をやってみようかと思います。
サンプルコードはgithubにあげておりますので、宜しければそちらもご覧ください。
環境
- Xcode: ver 11.1
- Swift: ver 5.0
- CocoaPods: ver 1.8.4
- Firebase/Authentication: ver 6.13.0
注意
- サンプルコードは私個人の環境で用意したFirebaseのプロジェクト, Developer Portal上で設定したAppIDの連携により動作検証したものとなります。
- 直接クローン, ビルドしただけでは動きの確認はできませんのでご注意ください。
- 2019/12/17現在、Firebase Auth上のApple項目はBeta版となっております。今後仕様に変更があり、その結果この記事の実装だとうまく動作しなくなることがあるかもしれないのでご注意ください。
- FirebaseをiOSプロジェクトに導入するまでの手順は割愛しています。Firebaseの導入手順については公式ドキュメントにまとまっておりますのでそちらをご参照ください firebase.google.com
サンプルコードの画面構成
ログイン画面とログイン後の画面の2画面構成です。ログイン済みか否かを判定し、結果によって遷移先を変更します。
- 未ログイン: ログイン画面
- ログイン済: ログイン後画面
Firebaseの設定
- 使用したいプロジェクトを選択
- 画面左の項目から
開発/Authenticationを選択
- Authentication画面の
ログイン方法タブを選択 ログインプロバイダ内にあるApple(Beta)の右側の鉛筆アイコンを押下
有効にするスイッチをONにし、保存ボタンを押下
ログインプロバイダのApple(Beta)の項目が有効に変わったことを確認
Developer Portal
- Certificates, Identifiers & Profiles を選択
Identifiersを選択- アプリ上で利用しているAppIDを選択
Capabilitiesの項目からSign in with AppleにチェックをつけるAutomatically manage signingを使用しXcode上でCapabilitiesを選択した場合(後述)、ここは自動でチェックがつく
Xcode
TARGETS/アプリのMainTargetを選択しSigning & Capabilitiesを選択+ Capabilityを選択しSign in with Appleを選択
実装
ログインボタン
@available(iOS 13.0, *)
private func setupSignInWithApple() {
// ASAuthorizationAppleIDButtonの親クラスはUIControlなので注意
let signInWithAppleButton = ASAuthorizationAppleIDButton(
authorizationButtonType: .signIn,
authorizationButtonStyle: .black
)
signInWithAppleButton.addTarget(
self,
action: #selector(didTapSignInWithApple),
for: .touchUpInside
)
// 公式サンプルに倣いUIStackViewに追加
stackView.addArrangedSubview(signInWithAppleButton)
}
// MARK: - Action
@objc func didTapSignInWithApple() {
viewModel?.inputs.didTapSignInWithApple(context: self)
}
備考
- ASAuthorizationAppleIDButton
- 要: iOS13以上
- 公式のサンプル
- カスタマイズも可能だがデザインガイドライン沿う必要があるので注意
ログイン処理その1
import AuthenticationService
import CryptoKit
@available(iOS 13.0, *)
private func initAuthorizationController(with context: UIViewController?) -> ASAuthorizationController {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let nonce = randomNonceString()
self.nonce = nonce
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
// ノンス設定
request.nonce = sha256(nonce)
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
if let controller = context as? ASAuthorizationControllerPresentationContextProviding {
// ログイン画面上でSignInWithAppleのモーダル表示を行いたいためログイン画面のインスタンスを設定
// ログイン画面のViewControllerはASAuthorizationControllerPresentationContextProvidingに準拠している
authorizationController.presentationContextProvider = controller
}
// 許可フロー開始
authorizationController.performRequests()
return authorizationController
}
extension LoginServiceImpl {
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: Array<Character> =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
}
return random
}
randoms.forEach { random in
if length == 0 { return }
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
@available(iOS 13.0, *)
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
return String(format: "%02x", $0)
}.joined()
return hashString
}
}
備考
- AuthenticationServices
- 要: iOS13以上
- CryptoKit
- 要: iOS13以上
- ノンスとは
ログイン処理その2
上記ログイン処理で ASAuthorizationController のインスタンスを作成し許可フローを開始しましたが、その後にコールバックされる処理が以下のDelegateメソッド群です。
extension HogeViewController: ASAuthorizationControllerDelegate {
func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
// 成功時の処理
}
func authorizationController(controller _: ASAuthorizationController, didCompleteWithError error: Error) {
// 失敗時の処理
}
}
私は普段の開発でRxSwiftを利用しているため、できればDelegateメソッドをそのまま使うのではなく、1つの Signinストリーム という非同期処理の括りでSign in with Appleを実現したいと思いました。
サンプルコードでは以下の箇所がそれに当たります。
func login(context: UIViewController?) -> Completable {
let authorizationController = initAuthorizationController(with: context)
return authorizationController
.rx
.didComplete
.flatMap({ [weak self] (authorization, error) -> Completable in
if let error = error {
return .error(error)
}
guard
let self = self,
let credential = self.initCredential(with: authorization)
else {
throw AuthenticationError.failedToCreateCredential
}
return self.signIn(with: credential)
})
.asCompletable()
}
authorizationController変数からrxが生え、didComplete を呼び出すと、返り値として Delegateの didCompleteWithAuthorization か didCompleteWithError メソッドの値を返します。
(成功か失敗かでどちらかのDelegateメソッドの値を取得するため、成功時にはerrorはnilに、失敗時にはauthorizationはnilになっている感じです。 )
この処理を実現しているのが、サンプルコードの RxASAuthorizationControllerDelegateProxy です。
これらの実装方法はこちらの記事を参考にさせて頂きました。
// DelegateProxyType, DelegateProxyクラスに準拠したクラスを作成
@available(iOS 13.0, *)
public class RxASAuthorizationControllerDelegateProxy: DelegateProxy<ASAuthorizationController, ASAuthorizationControllerDelegate>,
DelegateProxyType,
ASAuthorizationControllerDelegate {
// 成功時と失敗時の値を入れるため、PublishSubjectのジェネリクスの型をタプルで用意
internal lazy var didComplete = PublishSubject<(ASAuthorization?, Error?)>()
public func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
_forwardToDelegate?.authorizationController(
controller: controller,
didCompleteWithAuthorization: authorization
)
// タプルで取得しない値の方にnilを設定
didComplete.onNext((authorization, nil))
}
public func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: Error
) {
_forwardToDelegate?.authorizationController(
controller: controller,
didCompleteWithError: error
)
// タプルで取得しない値の方にnilを設定
didComplete.onNext((nil, error))
}
deinit { self.didComplete.on(.completed) }
}
@available(iOS 13.0, *)
extension Reactive where Base: ASAuthorizationController {
// 定義したDelegateProxyの型のdelegateラッパー生成
public var delegate: DelegateProxy<ASAuthorizationController, ASAuthorizationControllerDelegate> {
return RxASAuthorizationControllerDelegateProxy.proxy(for: base)
}
// Delegateメソッドに対応したラッパープロパティの生成
public var didComplete: Observable<(ASAuthorization?, Error?)> {
return RxASAuthorizationControllerDelegateProxy
.proxy(for: base)
.didComplete
.asObservable()
}
}
備考
- ジェネリッククラスの
DelegateProxyには下の2つ値をパラメータに設定- Rxに対応させたいクラス
- そのクラスのDelegate
実行結果
ログイン画面で Sing In With Apple ボタンを押下しログインを完了すると、Authentication画面上に登録されたユーザーの情報が表示されます
まとめ
サンプルコードで簡単なSign in with Appleの実装をまとめてみました。
RxSwiftの対応も意識した作りとなっているため、これから試してみよう, 導入してみようという方のお力になれれば幸いです。
そいえば僕はクリスマスイブが誕生日だったりするのですが、プレゼント代わりにはてなスターとかブックマークとかしてくれると嬉しいです。
ではではメリークリスマスー(・ω・)ノ