티스토리 뷰
근데 외주사 디자이너는 당연히 hig도 안봤고 고쳐달라고 하면 그것도 한세월이라 그냥 진행해본다...
1. Signing& Capabilities에서
Sign in with Apple 추가.
2. 여전히 SwiftUI로 제공하지 않으므로 UIViewRepresentable로 해야한다...
import SwiftUI
import AuthenticationServices
// 1
final class SignInWithApple: UIViewRepresentable {
// 2
func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
// 3
return ASAuthorizationAppleIDButton()
}
// 4
func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
}
}
SignInWithApple()
.frame(width: 280, height: 60)
!! 근데 이걸로 만들면 custom icon을 못쓴다..!!
그냥 custom view를 만들어서, 이걸 붙여준다.
.onTapGesture(perform: showAppleLogin)
showAppleLogin함수는 다음과 같다.
private func showAppleLogin() {
// 1
let request = ASAuthorizationAppleIDProvider().createRequest()
// 2
request.requestedScopes = [.fullName, .email]
// 3
let controller = ASAuthorizationController(authorizationRequests: [request])
}
Delegate를 만들어 준다.
authorization 함수는 다음과 같이 두개가 있다.
authorizationController(controller:didCompleteWithAuthorization:) authorizationController(controller:didCompleteWithError:)
"Note: Apple will only provide you the requested details on the firstauthentication."
didCompletionWithAuthorization 함수는 "처음 auth일때만 정보를 준다." 따라서 우리는 이 경우에 꼭 정보를 저장하고 다시 묻지 않아야 한다. Sign In with Apple이 어려운 이유중에 하나이다. 이걸 이용해서 처음 로그인인지도 판단한다.
// appleIdCredential에서 정보가 들어있으면 register, 아니면 sign In
// 1
if let _ = appleIdCredential.email, let _ = appleIdCredential.fullName {
// 2
registerNewAccount(credential: appleIdCredential)
} else {
// 3
signInWithExistingAccount(credential: appleIdCredential)
}
로그인 시 ASAuthorizationAppleIDCredential 객체를 리턴한다.
만약 로그인이 처음이라서 회원가입 함수를 콜 한다고 하면, 다음과 같이 코드를 짤 수 있다.
private func registerNewAccount(credential: ASAuthorizationAppleIDCredential) {
// 1
let userData = UserData(email: credential.email!,
name: credential.fullName!,
identifier: credential.user)
// 2
let keychain = UserDataKeychain()
do {
try keychain.store(userData)
} catch {
self.signInSucceeded(false)
}
// 3
do {
let success = try WebApi.Register(
user: userData,
identityToken: credential.identityToken,
authorizationCode: credential.authorizationCode
)
self.signInSucceeded(success)
} catch {
self.signInSucceeded(false)
}
}
왜 키체인에다가 데이터를 저장하는지, 왜 WebApi.Register을 콜하는지 궁금했는데 이유는 다음과 같다.
didCompletionWithAuthorization 함수는 아까 말했듯이 처음 로그인시만 정보를 주는데, 앱 삭제하고 새로 로그인 해도 정보를 주지 않는다... 그래서 UserDefaults에 저장하면 안된다.
1) 키체인에 저장하는 이유
=> 안전해서!!
+) 내 뇌피셜이지만 UserDefaults는 앱 삭제시 사라져서 안되는 이유도 있는 듯 하다.
출처: https://adora-y.tistory.com/entry/iOS-KeyChain이란-Swift코드를-통해-살펴보기
2) WebApi.register??
enum WebApi {
static func Register(user: UserData, identityToken: Data?, authorizationCode: Data?) throws -> Bool {
return true
}
}
레이아저씨가 만든 함수였을 뿐... 여기다가 가입시 처리해야하는 서버 코드들 넣으면 된다.
// 키체인 프로토콜을 보자
import Foundation
enum KeychainError: Error {
case secCallFailed(OSStatus)
case notFound
case badData
case archiveFailure(Error)
}
protocol Keychain {
associatedtype DataType: Codable
var account: String { get set }
var service: String { get set }
func remove() throws
func retrieve() throws -> DataType
func store(_ data: DataType) throws
}
extension Keychain {
func remove() throws {
let status = SecItemDelete(keychainQuery() as CFDictionary)
guard status == noErr || status == errSecItemNotFound else {
throw KeychainError.secCallFailed(status)
}
}
func retrieve() throws -> DataType {
var query = self.keychainQuery()
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnAttributes as String] = kCFBooleanTrue
query[kSecReturnData as String] = kCFBooleanTrue
var result: AnyObject?
let status = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
guard status != errSecItemNotFound else { throw KeychainError.notFound }
guard status == noErr else { throw KeychainError.secCallFailed(status) }
do {
guard
let dict = result as? [String: AnyObject],
let data = dict[kSecAttrGeneric as String] as? Data,
let userData = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? DataType
else {
throw KeychainError.badData
}
return userData
} catch {
throw KeychainError.archiveFailure(error)
}
}
func store(_ data: DataType) throws {
var query = self.keychainQuery()
let archived: AnyObject
do {
archived = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true) as AnyObject
} catch {
throw KeychainError.archiveFailure(error)
}
let status: OSStatus
do {
// If doesn't already exist, this will throw a KeychainError.notFound,
// causing the catch block to add it.
_ = try self.retrieve()
let updates = [
String(kSecAttrGeneric): archived
]
status = SecItemUpdate(query as CFDictionary, updates as CFDictionary)
} catch KeychainError.notFound {
query[kSecAttrGeneric as String] = archived
status = SecItemAdd(query as CFDictionary, nil)
}
guard status == noErr else {
throw KeychainError.secCallFailed(status)
}
}
private func keychainQuery() -> [String: AnyObject] {
var query: [String: AnyObject] = [:]
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrService as String] = service as AnyObject
query[kSecAttrAccount as String] = account as AnyObject
return query
}
}
3. AppleUserData 객체랑 UserDataKeychain 선언하기
// 유저 데이터를 보관하기 위한 struct를 만들어 준다.
/// Represents the details about the user which were provided during initial registration.
struct UserData: Codable {
/// The email address to use for user communications. Remember it might be a relay!
let email: String
/// The components which make up the user's name. See `displayName(style:)`
let name: PersonNameComponents
/// The team scoped identifier Apple provided to represent this user.
let identifier: String
/// Returns the localized name for the person
/// - Parameter style: The `PersonNameComponentsFormatter.Style` to use for the display.
func displayName(style: PersonNameComponentsFormatter.Style = .default) -> String {
PersonNameComponentsFormatter.localizedString(from: name, style: style)
}
}
//
struct UserDataKeychain: Keychain {
// Make sure the account name doesn't match the bundle identifier!
var account = "com.raywenderlich.SignInWithApple.Details"
var service = "userIdentifier"
typealias DataType = UserData
}
registerNewAccount() 함수를 보면, credential.user를 사용해서 identifier로 삼는 것을 볼 수 있다. 이 값이 애플에서 사용자를 특정하라고 준 값이다. 이 값은 사용자가 가진 모든 애플 기기에서 동일하다!! (아이폰, 아이패드 등), 그리고 Team Id가 같은 앱들에서도 모두 동일하게 사용된다.
credential.identityToken 이랑 credential.authorizationCode는 필요할수도 있고 아닐수도 있다.
(애플은 OAuth에서 public key를 만들기 위해서 필요한 정보들을 주고있다. 특히 Json Web Key도 주고 있다고 한다. 이 부분은 필요 없어서 생략)
이미 있는 계정으로 로그인 할 때,
private func signInWithExistingAccount(credential: ASAuthorizationAppleIDCredential) {
// You *should* have a fully registered account here. If you get back an error
// from your server that the account doesn't exist, you can look in the keychain
// for the credentials and rerun setup
// if (WebAPI.login(credential.user,
// credential.identityToken,
// credential.authorizationCode)) {
// ...
// }
self.signInSucceeded(true)
}
회원가입이 안되어있다는 정보가 서버에서 오면, 키체인에서 retrieve() 함수를 써서 정보를 다시 받아와서 재가입 시도를 하면 된다.
그리고 유저가 이미 iCloud keychain에 있는 정보를 사용해서 로그인하려고 시도하면 (? 어느경우인지는 모르겠다)
signInWithUserAndPassword(credential: passwordCredential)
함수를 콜해주면 된다.
private func signInWithUserAndPassword(credential: ASPasswordCredential) {
// if (WebAPI.login(credential.user, credential.password)) {
// ...
// }
self.signInSucceeded(true)
}
여기서도 서버 로그인 함수를 콜 해주는데, 만약 서버에서 유저 정보가 없다는 리스폰스가 오면, 가입부터 다시 진행해주면 된다.
delegate를 보관하기 위한 변수도 만들어준다. 나는 Scene이 아니라 ViewModel에서 쓸거라서 @Published로 선언해줬다.
@State var appleSignInDelegates: SignInWithAppleDelegates! = nil
// 1
appleSignInDelegates = SignInWithAppleDelegates() { success in
if success {
// update UI
} else {
// show the user an error
}
}
// 2
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = appleSignInDelegates
// 3
controller.performRequests()
자동 로그인에 대한 얘기도 Raywenderich에 나와있지만 나에게는 불필요한 기능이라 구현하지 않았다.
앱에서 사용하는 웹사이트가 있으면 web credential도 저장할 수 있지만 이것 역시 나에게는 불필요한 기능이라 구현하지 않았다.
런타임시 확인하기
이 페이지는 Apple ID Account Setting에서 사용자가 Stop Using Apple ID 눌렀을 때도 대응해줘야 한다... 나는 해당 사항이 없어서 안해줬다.
[참고]
https://www.raywenderlich.com/4875322-sign-in-with-apple-using-swiftui#toc-anchor-004
'macOS, iOS' 카테고리의 다른 글
[iOS] 인스타그램에서 열기 (0) | 2021.06.01 |
---|---|
[iOS] 앱 푸시 동의 여부 받아오기 (0) | 2021.06.01 |
[iOS] Facebook 로그인 - SwiftUI (0) | 2021.05.28 |
[iOS] UserDefaults에서 값 지울때 (0) | 2021.05.26 |
[Swift] 딕셔너리는 같은 키 가질 수 없다... (0) | 2021.05.26 |