본문 바로가기
Project/UIKit 업비트

[UIKit] Firebase, 애플 로그인 구현하기

by iOS_woo 2023. 5. 1.


Firebase에 애플 로그인을 구현해보았어요.

 

애플 ID 인증 > 파이어베이스 Sign In > 유저 데이터 확인
이런 흐름으로 진행되어요.

UI에 대해서는 생략할게요.

@escaping과 RxSwift, Combine 대신 async/await으로 비동기 처리를 해주었어요.
나름의 도전이었는데 개인적으로는 더 가독성이 높아진 것 같아요. 

1. Firebase 로그인 제공업체에 Apple을 추가해주세요. 

 

2. Signing & Capabilities에서 + Capability를 눌러 애플 로그인을 프로젝트에 추가해주세요. 

 

3. 애플 로그인 버튼을 눌렀을 시 작동할 함수를 생성해주세요.

@objc func appleSignInButtonTapped(_ sender: Any) {
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        
        let authrizationController = ASAuthorizationController(authorizationRequests: [request])
        authrizationController.delegate = self
        authrizationController.presentationContextProvider = self
        authrizationController.performRequests()
    }

 

4. 아래와 같은 함수들을 추가해주세요. 

extension SigninController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
    
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return self.view.window!
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization.credential {
            guard let appleIDToken = appleIDCredential.identityToken else { return }
            guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else { return }
            Task {
                let signin = await vm.appleSignin(withTokenId: idTokenString)
                switch signin {
                case .didFirstSignInWithApple:
                    print("애플 로그인이 처음이에요")
                    print("프로필 생성 페이지가 등장해야 합니다.")
                    self.delegate?.authenticationComlete()
                    self.dismiss(animated: true, completion: nil)
                case .didAlreadySignInWithApple:
                    self.delegate?.authenticationComlete()
                    self.dismiss(animated: true, completion: nil)
                case .didFailToSignInWithApple:
                    print("DEBUG: Failed to didCompleteWithAuthorization")
                }
            }
        }
    }
}

1. `presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor` : Apple ID 로그인 인증 컨트롤러의 프레젠테이션 컨텍스트를 제공하는 함수입니다. 이 함수에서는 현재 뷰 컨트롤러(`SigninController`)의 `view` 객체가 속한 윈도우(window) 객체를 반환합니다.

2. `authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization)` : Apple ID 로그인 인증이 완료되었을 때 호출되는 함수입니다. 이 함수에서는 `authorization` 매개변수에서 `ASAuthorizationAppleIDCredential` 객체를 추출하여 Apple ID 로그인에 성공한 경우, `vm.appleSignin(withTokenId: idTokenString)` 함수를 호출합니다. 이 함수는 Apple ID 토큰 ID(`idTokenString`)를 사용하여 Firebase Authentication에 로그인합니다. 그리고 로그인 성공 여부에 따라 `Output` 열거형 케이스 중 하나를 반환합니다. 반환된 케이스에 따라 로그인 성공 시 처리할 작업(예: 프로필 생성)을 수행합니다.

 

5. ViewModel을 아래와 같이 작성해주었어요.

import Firebase
import CryptoKit

class SigninViewModel {
    // MARK: - Properties
    
    typealias FirebaseUser = FirebaseAuth.User
    
    private var user: FirebaseUser?
    
    enum SignInType {
        case kakao
        case apple
    }
    
    enum Output {
        case didFirstSignInWithApple
        case didAlreadySignInWithApple
        case didFailToSignInWithApple
    }
    
    // MARK: Apple
    
    func appleSignin(withTokenId tokenId: String) async -> Output {
        do {
            let nonce = sha256(randomNonceString())
            let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: tokenId, rawNonce: nonce)
            let user = try await AuthService.signinUser(withCredential: credential)
            self.user = user
            let status = try await didUserAlreadyRegisterInFirestore(type: .apple)
            
            return status
        } catch {
            print("DEBUG: Failed to appleSignin\(error.localizedDescription)")
            return .didFailToSignInWithApple
        }
    }
    
    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
    }
    
    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
    }
    
    // MARK: - DidUserAlreadyRegisterInFirestore
    
    private func didUserAlreadyRegisterInFirestore(type: SignInType) async throws -> Output {
        do {
            guard let user = user else { return .didFailToSignInWithApple }
            
            let isExisted = try await FirebaseService.isUserAlreadyExisted(user: user)
            
            return isExisted ? .didAlreadySignInWithApple : .didFirstSignInWithApple
        } catch {
            print("DEBUG: Failed to didUserAlreadyRegisterInFirestore \(error.localizedDescription)")
            throw error
        }
    }
}

 

6. API 관련 코드들은 이렇게 작성하였어요. 

import Firebase

struct AuthService {
    static func signinUser(withCredential credential: OAuthCredential) async throws -> User {
        do {
            let result = try await Auth.auth().signIn(with: credential)
            return result.user
        } catch {
            print("DEBUG: Failed to signinUser\(error.localizedDescription)")
            throw error
        }
    }
}

import FirebaseFirestore

struct FirebaseService {
    static func isUserAlreadyExisted(user: User) async throws -> Bool {
        do {
            let document = try await Firestore.firestore().document("users/\(user.uid)").getDocument()
            return document.exists
        } catch {
            print("DEBUG: Failed to isUserAlreadyExisted\(error.localizedDescription)")
            throw error
        }
    }
}

 

완성!

흐름은 간략하게 다음과 같아요. 
애플 ID 인증 -> Firebase Sign In ->FireStrore에 UID 해당하는 자료가 있다면 로그인 페이지를 내리고, 없다면 프로필 생성화면으로 넘어가기 

에러처리나 async / await은 제 마음데로 작성을 해보았는데.. 만약 고칠 부분이 있다면 댓글로 알려주세요! 

전체코드(RxSwift 활용으로 수정했어요!):

 

GitHub - mwoo-git/RxSwiftUIkitMVVMPractice: RxSwift, UIkit, MVVM Practice

RxSwift, UIkit, MVVM Practice. Contribute to mwoo-git/RxSwiftUIkitMVVMPractice development by creating an account on GitHub.

github.com

 

댓글