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 활용으로 수정했어요!):
'Project > UIKit 업비트' 카테고리의 다른 글
[UIKit] Firebase, 카카오 로그인 구현하기 (0) | 2023.05.02 |
---|---|
[UIKit] UIMenu로 메뉴를 만들어보자 (0) | 2023.04.27 |
[UIKit] TableView에서 insetGrouped 스타일로 섹션을 나눠보자 (0) | 2023.04.22 |
[UIKit] TableView 위에 Header를 만들어보자 (0) | 2023.04.22 |
[UIKit] UISearchController, 검색 기능 구현하기 (1) | 2023.04.17 |
댓글