👋

Google Sign-In 6.0.0をSwiftUIで使う

7 min read

先日Google Sign-Inのバージョンが6.0.0に上がりSwift Package Managerに対応した。AdMobのSDKをCocoaPodを使いたくなくてバイナリを直でダウンロードして使っていた自分には、こうしてどんどんSwift Package Managerに対応してくれるのは朗報。
というわけで、早速Google Sign-In 6.0.0をSwiftUIで試してみた。

Swift Package MangerでGoogle Sign-In 6.0.0を使う

XcodeでSwift Package Managerを使ってパッケージをプロジェクトに加える方法は以下のページに詳しく載っている。

https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app
今回はGithubからGoogle Sign-In 6.0.0をダウンロードして使用する。
  1. Xcodeでプロジェクトを作成したら File->Swift Packages->Add Package Dependency…を選択
  2. 表示されるダイアログでGoogle Sign-In 6.0.0のアドレスhttps://github.com/google/GoogleSignIn-iOSを入力
  3. RulesがUp to Next Major 6.0.0になっているのを確認して先へ進む
  4. GoogleSignInが表示されるのを確認してFinish。
  5. プロジェクトのSwift Package Dependenciesにパッケージが表示される。
  6. 最後にXcodeのTARGETS->InfoからURL Typesで新しくURL Schemesを加える。
    入力するURL Schemeはclient IDを逆順にしたもの。(client ID作成時にダウンロードしたplistのREVERSED_CLIENT_IDをコピペするのが楽。)
  7. 後はコードの中でimport GoogleSignInすれば使用できる。
import GoogleSignIn

Google Sign-In 6.0.0の新しい部分

Google Sign-In 6.0.0の詳細は以下のページに詳しく書いてある。

https://github.com/google/GoogleSignIn-iOS/releases/tag/6.0.0
https://developers.google.com/identity/sign-in/ios/reference/Classes
色々と変更はあるが自分の既存のコードを書き換える必要がある場所は大体以下の通り。
  1. どの操作もdelegateを使わずにcallbackを使うようになった
    今まではdelegateを設定してそこで各種操作後の処理を行なっていたが、これからはdelegateなしで操作を行う場合にcallbackを渡すことになる。
  2. SignIn時のconfigurationはGIDConfigurationにまとめられた
  3. GIDSignInButtonが内部でGIDSignInを呼ばなくなった
    クリックしてもサインインしてくれないただのボタンになったのでIBActionなりなんなりで自分で signInWithConfiguration:presentingViewController:callback: を呼ぶ必要がある。また、SwiftUIでGIDSignInButtonを使う場合はIBActionで繋げることはできないので UIViewRepresentable を使うことになる。

SwiftUIで使ってみる

前に作ったXcodeでGoogleSignInを使うのサンプルをGoogle Sign-In 6.0.0に書き換えてみた。

基本的にはGet started with Google Sign-In for iOSのコードをそのまま使用して書き換えている。
以下、前のサンプルからの変更点を幾つか。

GIDSignInButtonの処理

まずGIDSignInButtonをSwiftUIで使うためにUIViewRepresentableプロトコルに対応した新しいクラスGoogleSignInBtnを作成。makeView(context:)の中でGIDSignInButtonのインスタンスを作成して見た目と挙動を設定している。

struct GoogleSignInBtn: UIViewRepresentable {
	typealias UIViewType = GIDSignInButton
	func makeView(context: Context) -> GIDSignInButton {
		let button = GIDSignInButton()
		// 見た目設定は省略
		button.addAction(.init(handler: { _ in
		    guard let presentingViewController = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController else {return}
		    
		    GIDSignIn.sharedInstance.signIn(
		        with: self.userData.signInConfig,
		        presenting: presentingViewController,
		        callback: { user, error in
		            if let user = user {
		                self.userData.signIn(user: user)
		            }
		            else{
		                if let error = error {
		                    print("error: \(error.localizedDescription)")
		                }
		            }
		        })
		}), for: .touchUpInside)
	}
	func updateView(_ uiView: GIDSignInButton, context: Context) {
	}
}

前のサンプルではdelegateを使ってサインイン時の処理を行なっていたが、今回はGIDSignInsignIn:with:presenting:callbackを使って行うことになる。この処理はボタンの挙動をaddActionで設定している部分で行なっている。

UserDataで情報アップデート

ビューの書き換えはUserDataを介して行う。
前のサンプルではUserDataがdelegateになってここで各種処理を行なっていたが今回は各種処理がなくなってデータの保持が主な仕事。
サインイン/アウトしたユーザはsignIn(user:)@Published var user: GIDGoogleUser?に保持され画面が書き変わる。画像データはネットワークアクセスが入るのでDispatchQueueで非同期にアップデートしている。
GIDConfigurationはここに作成している。UserDataenvironmentObjectでビューに渡されサインイン時に参照される。

fileprivate let clientID = "<clientID>"
fileprivate let grayAvatorImage: UIImage = UIImage(named: "PlaceholderAvatar")!

class UserData: NSObject, ObservableObject {

    @Published var user: GIDGoogleUser?
    @Published var avatorImage: UIImage = grayAvatorImage
    
    var signInConfig = GIDConfiguration.init(clientID: clientID)
    
    var isSignedIn: Bool {
        return GIDSignIn.sharedInstance.currentUser?.authentication != nil
    }

    var userName: String {
        if isSignedIn, let profile = user?.profile {
            return profile.name
        }
        else {
            return "< User name >"
        }
    }
    
    var email: String {
        if isSignedIn, let profile = user?.profile {
            return profile.email
        }
        else {
            return "< Email address >"
        }
    }    
    
    func signIn(user: GIDGoogleUser?) {
        self.user = user
        
        guard let user = user, let imageURL = user.profile?.imageURL(withDimension: 50) else {
            self.avatorImage = grayAvatorImage
            return
        }
        
        DispatchQueue.global().async {
            let data = try? Data(contentsOf: imageURL)
            DispatchQueue.main.async {
                guard let data = data, let imageData = UIImage(data: data) else {
                    self.avatorImage = grayAvatorImage
                    return
                }
                
                self.avatorImage = imageData
            }
        }
        
    }
}

既にサインインしている場合の処理

GIDSignIn.sharedInstance.restorePreviousSignIn(callback:)でアプリ起動時に以前のサインインの状態を復元できるので、これをAppDelegateapplication(application:launchOptions:)で行なっている。

class AppDelegate: UIResponder, UIApplicationDelegate {
    var userData = UserData()
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        GIDSignIn.sharedInstance.restorePreviousSignIn(callback: { user, error in
            if error != nil || user == nil {
                // Show the app's signed-out state.
                print("Not sign-in")
            }
            else {
                // Show the app's signed-in state.
                if let user=user, let userID = user.userID {
                    self.userData.signIn(user: user)
                    print("Already signed-in as :\(userID)")
                }
            }
        })
        return true
    }
    // その他省略
}

Googleの使い方のページのコードそのままなのだがSwiftUIならSceneDelegateでやっても良かったのかもしれない。

サンプルコード

https://github.com/paraches/GoogleSignIn6_SwiftUI
client IDは入っていないのでUserData.swiftのclientIDを編集してからでないとSign inボタンを押したら落ちます。
fileprivate let clientID = "<clientID>"    // https://console.cloud.google.com/ API -> Credential

最後に

Google Sign-Inが6.0.0でSwift Package Managerに対応したので早速テストサンプルを書いてみたのだけど、SwiftUIが全然書けない…。リリースされた当時に少しチュートリアルをやってみたりしてそこそこ分かった気になっていたのだけど、今回こんな簡単なサンプルを書くのでも思い切り忘れていて驚いた。
多分もう一度チュートリアルからやり直した方が良いレベルなので、コードのおかしなところとかあったら教えてください!