Apple 로그인
원래라면
Apple Developer 사이트의 Certificates, Identifiers & Profiles → Identifiers 에 직접 들어가서 설정을 해야하지만,
Xcode에서 TARGETS → Signing & Capabilities에 개발자 계정을 설정한 상태에서
+ 버튼을 눌러 Sign in with Apple 추가하면
Apple Developer 사이트의 Certificates, Identifiers & Profiles → Identifiers에서 자동으로 설정이 추가가 된다.
Xcode가 자동으로 Apple Developer 계정에 반영해주는 것 같다.
소셜로그인 기능을 어댑터 패턴을 이용해 설계하면서,
다양한 로그인 방식을 유연하게 추가하는 것을 목표로 하는 프로젝트를 정리해놓은 글이다.
이제 애플 로그인을 추가할 차례이며,
공통 인터페이스를 기반으로 어댑터만 추가하면 다른 코드에 영향을 주지 않고 깔끔하게 확장할 수 있다.
SwiftUI에서는 SignInWithAppleButton을 통해 간편하게 애플 로그인 기능을 구현 가능하다.
성공 시 ASAuthorization 객체를 반환받아 결과를 파싱하는 방식으로 손쉽게 처리 가능
ASAuthorization는 Apple의 AuthenticationServices 프레임워크에서 인증 요청 결과를 담아주는 객체
애플 로그인을 성공하면 ASAuthorization 객체 안에 ASAuthorizationAppleIDCredential라는 credential이 담기게 됨
이 credential에는 사용자 ID, 이름, 이메일 등 인증에 필요한 정보가 들어 있음
SignInWithAppleButton
Sign in with Apple on a SwiftUI application
Learn how to add Sign in with Apple to a SwiftUI project using the Authentication Services framework.
www.createwithswift.com
import SwiftUI
import AuthenticationServices
struct SignInView: View {
var body: some View {
SignInWithAppleButton(.signUp) { request in
request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
switch result {
case .success(let authorization):
handleSuccessfulLogin(with: authorization)
case .failure(let error):
handleLoginError(with: error)
}
}
.frame(height: 50)
.padding()
}
private func handleSuccessfulLogin(with authorization: ASAuthorization) {
if let userCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
print(userCredential.user)
if userCredential.authorizedScopes.contains(.fullName) {
print(userCredential.fullName?.givenName ?? "No given name")
}
if userCredential.authorizedScopes.contains(.email) {
print(userCredential.email ?? "No email")
}
}
}
private func handleLoginError(with error: Error) {
print("Could not authenticate: \\(error.localizedDescription)")
}
}
그런데 현재 나의 프로젝트 구조에서 소셜 로그인의 유지보수와 확장성을 고려해서 어댑터 패턴을 도입하였다.
이 방식에서는 모든 소셜 로그인 로직이 공통 인터페이스를 따르도록 설계되어 있기 때문에,
결과적으로 ASAuthorization 와 같은 구체적인 객체를 직접 전달받을 수 없다.
따라서, ASAuthorizationControllerDelegate 기반의 콜백 API를 통해 로그인 결과를 처리하도록 설계하도록 결정하였다.
이 방식은 코드가 길어지는 단점이 있으나, 로그인 플로우 전체를 캡슐화하기 때문에
어댑터 패턴을 독립적으로 일관되게 관리할 수 있다.
그리고 뷰는 단순히 어댑터의 login만 호출하면 되기 때문에
2개의 클로저가 필요하는 SignInWithAppleButton를 사용하지 않고,
커스텀 버튼으로 대체하기로 결정함.
import Foundation
import AuthenticationServices
final class AppleLoginAdapter: NSObject, SocialLoginService {
private var continuation: CheckedContinuation<UserInfo, Error>?
@MainActor
func login() async throws -> UserInfo {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
}
@MainActor
func logout() async throws {}
func getServiceName() -> String {
return "Apple Auth"
}
}
extension AppleLoginAdapter: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
let userId = appleIDCredential.user
let fullName = appleIDCredential.fullName?.givenName ?? "Unknown"
let email = appleIDCredential.email ?? "Unknown"
let userInfo = UserInfo(id: userId, name: fullName, email: email)
continuation?.resume(returning: userInfo)
} else {
continuation?.resume(throwing: AuthError.userInfoNotFound(service: .apple))
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
continuation?.resume(throwing: error)
}
}
extension AppleLoginAdapter: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
if let windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) {
return keyWindow
}
return UIWindow()
}
}
결과
GitHub - joho2022/LoginPractice
Contribute to joho2022/LoginPractice development by creating an account on GitHub.
github.com