ukSeung iOS

[Swift] RxSwift + MVVM, FaceID & TouchID authentication 본문

iOS/Swift

[Swift] RxSwift + MVVM, FaceID & TouchID authentication

욱승 2023. 10. 5. 16:18

안녕하세염 욱승입니당

 

이번 포스팅에서는 앱에서 필수로 쓰이는 FaceID 및 TouchID 인증을 알아보겠읍니다

포스팅은 편의상 반말루 ㅋ

 

 

import

import LocalAuthentication

해당 라이브러리는 Apple 내장 라이브러리라 Cocoapods이나 Packages에 추가할 필요 음슴

 

info.plist

FaceID를 사용하는 기기면

Privacy - Face ID Usage Description를 추가해조야댐

 

구현(View)

//
//  LocalAuthenticationViewController.swift
//  SwiftPractice
//
//  Created by ukseung.dev on 2023/10/05.
//

import Foundation
import UIKit
import RxSwift
import RxCocoa

final class LocalAuthenticationViewController: UIViewController, UIViewControllerAttribute {
    let viewModel = LocalAuthenticationViewModel()
    let disposeBag = DisposeBag()
    var navTitle: String?
    
    
    lazy var authLabel = UILabel().then {
        $0.text = "인증 전"
        $0.textColor = .black
        $0.sizeToFit()
    }
    
    lazy var authButton = UIButton().then {
        $0.setTitle("LocalAuthentication", for: .normal)
        $0.backgroundColor = .systemBlue
        $0.setTitleColor(.white, for: .normal)
        $0.layer.cornerRadius = 10
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setNavigationBar()
        setUI()
        setAttributes()
        bindRx()
    }
    
    func setNavigationBar() {
        self.navigationItem.title = navTitle ?? ""
    }
    
    func setUI() {
        self.view.backgroundColor = .white
        
        self.view.addSubview(authLabel)
        self.view.addSubview(authButton)
    }
    
    func setAttributes() {
        authLabel.snp.makeConstraints {
            $0.bottom.equalTo(authButton.snp.top).offset(-40)
            $0.centerX.equalToSuperview()
        }
        
        authButton.snp.makeConstraints {
            $0.centerX.centerY.equalToSuperview()
            $0.width.equalTo(200)
            $0.height.equalTo(40)
        }
    }
    
    func bindRx() {
        viewModel.authenticationSubject
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] AuthType in
                guard let self = self else { return }
                
                switch AuthType {
                case AuthType.success :
                    self.authLabel.textColor = .systemGreen
                default:
                    self.authLabel.textColor = .systemRed
                }

                self.authLabel.text = AuthType.message
            })
            .disposed(by: disposeBag)
        
        authButton.rx.tap
            .subscribe(onNext: { [weak self] in
                guard let self = self else { return }
                
                self.viewModel.authenticateWithBiometrics()
            })
            .disposed(by: disposeBag)
    }
}

코드의 가독성을 위해 Snapkit과 Then을 사용!

 

구현(ViewModel)

//
//  LocalAuthenticationViewModel.swift
//  SwiftPractice
//
//  Created by ukseung.dev on 2023/10/05.
//

import Foundation
import RxSwift
import RxCocoa
import LocalAuthentication

enum AuthType {
    case appCancel
    case userCancel
    case biometryNotAvailable
    case biometryNotEnrolled
    case biometryLockout
    case authenticationFailed
    case success

    var message: String {
        switch self {
        case .appCancel:
            return "앱에 의해 인증이 취소되었습니다"
        case .userCancel:
            return "사용자에 의해 인증이 취소되었습니다"
        case .biometryNotAvailable:
            return "이 기기에서는 Face ID / Touch ID를 사용할 수 없습니다"
        case .biometryNotEnrolled:
            return "사용자가 Face ID / Touch ID를 등록하지 않았습니다"
        case .biometryLockout:
            return "Face ID / TouchID가 너무 많은 실패 시도로 잠겼습니다"
        case .authenticationFailed:
            return "Face ID / TouchID 인증에 실패했습니다"
        case .success:
            return "Face ID / TouchID 인증 성공"
        }
    }
}


final class LocalAuthenticationViewModel {
    let authenticationSubject = PublishSubject<AuthType>()
    let context = LAContext()
    var error: NSError?
    
    func authenticateWithBiometrics() {
        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Face ID / TouchID로 인증을 시도합니다") { success, error in
                if success {
                    // Face ID authentication succeeded
                    self.authenticationSubject.onNext(AuthType.success)
                } else {
                    // Face ID authentication failed
                    if let error = error as? LAError {
                        switch error.code {
                        case .appCancel:
                            self.authenticationSubject.onNext(AuthType.appCancel)
                        case .userCancel:
                            self.authenticationSubject.onNext(AuthType.userCancel)
                        case .biometryNotAvailable:
                            self.authenticationSubject.onNext(AuthType.biometryNotAvailable)
                        case .biometryNotEnrolled:
                            self.authenticationSubject.onNext(AuthType.biometryNotEnrolled)
                        case .biometryLockout:
                            self.authenticationSubject.onNext(AuthType.biometryLockout)
                        default:
                            print("Face ID authentication failed: \(error.localizedDescription)")
                        }
                    }
                }
            }
    }
}

 

 

결과

인증 전

 

Face ID 첫 시도의 경우 Info.plist에 적어놨던 문구가 노출

 

 

실패 case
성공 case

 

성공이면 인증 성공, 실패면 실패 사유를 return하게 구현 해놨다

 

 

결론

앱 개발시 거의 불가피하게 사용되는 기능이니 숙지해두자 😎

728x90
반응형