🪶
SwiftUI FirebaseAuth Apple SignIn
Apple Sign Inを実装する
SwiftUIにApple Sign Inを追加するには、AppleDeveloperAccountが必要です。それとxcodeにも設定が必要です。
公式だと詳しく書いてない
プロジェクトを作成したら、AppleDeveloperのサイトで、Bundle Indetifierの設定をする。そこで、Apple SignInができるように設定を追加する。
こちらを参考に設定を追加
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