Login-RxSwift-MVVM
Firebase + Google + Apple 로그인 구현
목적
RxSwift + MVVM 기능 학습
SnapKit + Then 기능 학습
SnapKit + Then 조합으로 UI구현
- 텍스트와 텍스트크기를 프로퍼티로 따로 관리해 준다.
- UI객체는 Then으로 구현해 준다
- AutoLayout은 SnapKit으로 구현해 준다.
- Text와 Size를 구조체에 별도로 생성하고 관리해 준다.
struct DV {
enum TextSize {
static let loginTextViewHeight: CGFloat = 48 }
enum LabelText {
static let emailInfoLabel = "이메일주소 또는 전화번호"
static let passwordInfoLabel = "비밀번호"
static let passwordSecureButton = "표시"
static let loginButton = "로그인"
static let joinButton = "회원가입"
static let passwordResetButton = "비밀번호 재설정"
}
}
- SnapKit과 Then으로 View를 생성해 준다
lazy var emailInfoLabel = UILabel().then { $0.text = DV.LabelText.emailInfoLabel $0.font = UIFont.systemFont(ofSize: 18) $0.textColor = .kakaoLightBrown }`
emailInfoLabel.snp.makeConstraints {
$0.leading.equalTo(emailTextFieldView).offset(8)
$0.trailing.equalTo(emailTextFieldView).offset(-8)
self.emailInfoLabelCenterYConstraint = $0.centerY.equalTo(emailTextFieldView).offset(-3).constraint
}
RxSwift + MVVM
- TextLabel을 선택하면 PlaceHolderLabel이 애니메이션 효과와 함께 작아진다.
- email과 password 양식 검사를 통해 PlaceHolderLabel에 표시해 준다.
- email과 password가 맞으면 다음 화면으로 넘어간다.
- 로그인 여부는 Auth.auth().currentUser?를 통해 확인한다.
로직
- LoginViewController & LoginViewModel
- 화면을 터치하면 편집을 종료한다.
- 텍스트필드를 터치를 인식할 수 있게 한다.
- 텍스트필드와 ViewModel의 email프로퍼티와 바인딩한다.
- ViewModel에서 email양식을 검사한다.
- 양식이 맞으면 플레이스홀더의 색상을 변경한다.
LoginViewController
//LoginViewController
private let viewModel = LoginViewModel()
private let disposeBag = DisposeBag()
private lazy var textFields = [loginView.emailTextField, loginView.passwordTextField]
let tapGesture = UITapGestureRecognizer()
view.addGestureRecognizer(tapGesture)
tapGesture.rx.event
.subscribe(onNext: { [weak self] _ in
self?.view.endEditing(true)
})
.disposed(by: disposeBag)
textFields.forEach { textField in
textField.rx.controlEvent([.editingDidBegin,.editingChanged])
.subscribe(onNext: { [weak self] in
self?.textFieldDidBeginEditing(textField)
})
.disposed(by: disposeBag)
}
loginView.emailTextField.rx.text.orEmpty
.bind(to: viewModel.email)
.disposed(by: disposeBag)
viewModel.emailValid
.map { $0 ? UIColor.systemGreen : UIColor.kakaoLightBrown}
.bind(to: loginView.emailInfoLabel.rx.textColor)
.disposed(by: disposeBag)
- 텍스트필드를 편집하기 시작하면 플레이스홀더의 위치와 크기를 변경해 준다
//LoginViewController
// MARK: - 텍스트필드 델리게이트
//텍스트필드 편집 시작할때의 설정 - 문구가 위로올라가면서 크기 작아지고, 오토레이아웃 업데이트
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == loginView.emailTextField {
loginView.emailTextFieldView.backgroundColor = .systemGray6
loginView.emailInfoLabel.font = UIFont.systemFont(ofSize: 11)
// 오토레이아웃 업데이트
loginView.emailInfoLabelCenterYConstraint?.update(offset: -13)
}
}
LoginViewModel
- email의 양식을 검사하고 검사한 값을 emailValid와 바인딩해준다.
//LoginViewModel
let email = BehaviorRelay<String>(value: "")
let emailValid = BehaviorRelay<Bool>(value: false)
private let disposeBag = DisposeBag()
email
.map { $0.isValidEmail() }
.bind(to: emailValid)
.disposed(by: disposeBag)
UserDefault + PropertyWrapper
- 소셜로그인을 플러그인 하기 전에 사용했던 로그인 상태 확인 방법으로 사용
- 프로퍼티래퍼로 UserDefault 구조체를 감싸서 사용하기 쉽게 만들어준다.
viewModel.loginSuccess
.subscribe(onNext: { [weak self] in
IsLogin.launchedBefore = true
self?.dismiss(animated: true)
})
.disposed(by: disposeBag)
@propertyWrapper
struct UserDefault<T> {
private let key: String
private let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
struct IsLogin {
@UserDefault(key: keyEnum.isLogin.rawValue, defaultValue: false)
static var launchedBefore: Bool
}
enum keyEnum: String {
case isLogin = "isLogin"
}
Google Login
- Firebase를 활용해 구글 계정 연동
)
- 로그인 성공 시 LoginViewController를 dismiss 한다.
loginView.googleButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.handleGoogleLogin()
})
.disposed(by: disposeBag)
private func handleGoogleLogin() {
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
GIDSignIn.sharedInstance.configuration = config
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(withPresenting: self) { [unowned self] result, error in
guard error == nil else {
// ...
return
}
guard let user = result?.user,
let idToken = user.idToken?.tokenString
else {
// ...
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: user.accessToken.tokenString)
// ...
Auth.auth().signIn(with: credential) { _,_ in
self.dismiss(animated: true)
}
}
}
Apple Login
- 애플 Certificates를 활용해 로그인
- Apple Developer에 접속해 Services IDs애 등록해 준다.
- Xcode의 Project에서 +Capability를 클릭해 sign in with Apple을 추가해 준다
- 아래 코드를 추가해 주면 완성
import AuthenticationServices
import Crypto
// Unhashed nonce.
fileprivate var currentNonce: String?
@available(iOS 13.0, *)
extension LoginViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.view.window!
}
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
}
func startSignInWithAppleFlow() {
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()
}
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
var randomBytes = [UInt8](repeating: 0, count: length)
let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
if errorCode != errSecSuccess {
fatalError(
"Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
)
}
let charset: [Character] =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
let nonce = randomBytes.map { byte in
// Pick a random character from the set, wrapping around if needed.
charset[Int(byte) % charset.count]
}
return String(nonce)
}
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
}
// Initialize a Firebase credential, including the user's full name.
let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
rawNonce: nonce,
fullName: appleIDCredential.fullName)
// Sign in with Firebase.
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
// Error. If error.code == .MissingOrInvalidNonce, make sure
// you're sending the SHA256-hashed nonce as a hex string with
// your request to Apple.
print(error.localizedDescription)
return
}
// User is signed in to Firebase with Apple.
// ...
self.dismiss(animated: true)
}
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
// Handle error.
print("Sign in with Apple errored: \(error)")
}
}
전체코드는 Github에 올려뒀습니다.
https://github.com/Jimmy-Jung/Login_RxSwift_MVVM_Study.git