🪶

SwiftUI FirebaseAuth Apple SignIn

2024/11/26に公開

Apple Sign Inを実装する

SwiftUIにApple Sign Inを追加するには、AppleDeveloperAccountが必要です。それとxcodeにも設定が必要です。

公式だと詳しく書いてない

プロジェクトを作成したら、AppleDeveloperのサイトで、Bundle Indetifierの設定をする。そこで、Apple SignInができるように設定を追加する。

こちらを参考に設定を追加
https://firebase.google.com/docs/auth/ios/apple?hl=ja
https://medium.com/firebase-developers/firebase-authentication-in-swiftui-part-3-80be99dbc63d
https://appdev-room.com/swift-firebase-authentication-apple

Firebaseの設定を追加する

import SwiftUI
import SwiftData
import FirebaseCore

class AppDelegate: NSObject, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    FirebaseApp.configure()

    return true
  }
}

@main
struct CoffeeLifeApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
//            ContentView()
            SignInView()
        }
    }
}

SignIn

カスタムボタンを作成したのでこちらを使用。違うボタンを使っても良い。

import SwiftUI

struct CustomButton: View {
    var iconName: String
    var title: String
    var action: () -> Void
    
    var body: some View {
        Button(action: action) {
            HStack(spacing: 12) { // spacing を明示的に設定
                Image(systemName: iconName)
                    .frame(width: 24)
                
                Text(title)
                    .frame(maxWidth: .infinity)
                
                // 右側の余白を確保するための透明なスペーサー
                Image(systemName: iconName)
                    .frame(width: 24)
                    .opacity(0) // 透明にすることで、左のアイコンとバランスを取る
            }
            .padding(.horizontal, 16)
            .foregroundColor(.black)
            .font(.system(size: 16, weight: .semibold))
            .frame(maxWidth: 300, minHeight: 52)
            .background(.white)
            .cornerRadius(15)
        }
    }
}

ロジックを実装

AppleSignInに必要なロジックはコード長いですが、このように書けば動きました。今回はまだログインの維持する機能はないです。

import SwiftUI
import FirebaseAuth
import AuthenticationServices
import CryptoKit

// ビューモデル
class SignInViewModel: NSObject, ObservableObject {
    @Published var isSignedIn = false
    @Published var errorMessage: String?
    
    // 現在の nonce を保持
    private var currentNonce: String?
    
    // ランダムな nonce を生成
    private func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        let charset: [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 remainingLength == 0 {
                    return
                }
                
                if random < charset.count {
                    result.append(charset[Int(random)])
                    remainingLength -= 1
                }
            }
        }
        return result
    }
    
    // SHA256でハッシュ化
    private func sha256(_ input: String) -> String {
        let inputData = Data(input.utf8)
        let hashedData = SHA256.hash(data: inputData)
        let hashString = hashedData.compactMap {
            String(format: "%02x", $0)
        }.joined()
        
        return hashString
    }
    
    // Apple Sign In の処理
    func handleSignInWithApple() {
        let nonce = randomNonceString()
        currentNonce = nonce
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        request.nonce = sha256(nonce)
        
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }
    
    // Firebase認証
    private func authenticateWithFirebase(credential: AuthCredential) {
        Auth.auth().signIn(with: credential) { [weak self] (result, error) in
            if let error = error {
                self?.errorMessage = error.localizedDescription
                return
            }
            self?.isSignedIn = true
            
            // ユーザー情報の保存やその他の処理をここに追加
        }
    }
}

// Apple Sign In のデリゲート
extension SignInViewModel: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            guard let nonce = currentNonce else {
                fatalError("Invalid state: A login callback was received, but no login request was sent.")
            }
            guard let appleIDToken = appleIDCredential.identityToken else {
                print("Unable to fetch identity token")
                return
            }
            guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
                print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
                return
            }
            
            // Firebase credential を作成
            let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                    idToken: idTokenString,
                                                    rawNonce: nonce)
            
            // Firebase で認証
            authenticateWithFirebase(credential: credential)
        }
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        self.errorMessage = error.localizedDescription
    }
}

// プレゼンテーションコンテキストプロバイダー
extension SignInViewModel: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        guard let window = UIApplication.shared.windows.first else {
            fatalError("No window found")
        }
        return window
    }
}

ログインとログイン後のモーダルを実装する。私は自作した画像を使っているのでみなさんはImageを別物に変更するか削除して使ってみてください。

import SwiftUI

// メインビュー
struct SignInView: View {
    @StateObject private var viewModel = SignInViewModel()
    @State private var showNextView = false
    
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea(.all)
            
            VStack {
                Text("CoffeeLife")
                    .fontWeight(.medium)
                    .foregroundColor(Color.brown)
                    .font(.system(size: 50))
                
                Image("mag")
                    .padding(.bottom, 40)
                
                CustomButton(iconName: "apple.logo", title: "Sign in with Apple") {
                    viewModel.handleSignInWithApple()
                }
                .padding([.leading, .trailing, .bottom], 20)
                
                if let url = URL(string: "https://ios-docs.dev") {
                    Link("利用規約", destination: url)
                        .foregroundColor(.blue)
                }
                
                if let url = URL(string: "https://ios-docs.dev") {
                    Link("プライバシーポリシー", destination: url)
                        .foregroundColor(.blue)
                }
            }
        }
        .alert("エラー", isPresented: Binding(
            get: { viewModel.errorMessage != nil },
            set: { if !$0 { viewModel.errorMessage = nil } }
        )) {
            Button("OK") {
                viewModel.errorMessage = nil
            }
        } message: {
            Text(viewModel.errorMessage ?? "")
        }
        .onChange(of: viewModel.isSignedIn) { isSignedIn in
            if isSignedIn {
                showNextView = true
            }
        }
        .fullScreenCover(isPresented: $showNextView) {
            // サインイン後に表示するビューを指定
            Text("サインイン成功!")
                .font(.title)
        }
    }
}

// プレビュー
#Preview {
    SignInView()
}

AppleSignInを使うときは、実機でやった方がいいと思うので、iPhoneのOSのバージョンを合わせて試してみてください。成功するとアカウント情報がFirebaseAuthに保存されます。

Discussion