[Swift] RXSwift Refactoring in iOS

2024. 2. 13. 18:29ใ†Programming/Swift

 

 

 

 

 

๐Ÿ•บ

์˜ค๋Š˜ ๊ตฌํ˜„ํ•œ ๊ฒƒ 

hotfix๋กœ JWT ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ, refreshํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. 

https://github.com/GDSC-Wetox/Wetox-iOS/pull/16

 

[HOTFIX] Token Refresh ๊ตฌํ˜„ by SohyeonKim-dev · Pull Request #16 · GDSC-Wetox/Wetox-iOS

Summary & Issues Closes #15 Contents Token์ด ๋งŒ๋ฃŒ๋˜์–ด ๋„คํŠธ์›Œํฌ error๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ํ˜ธ์ถœ๋  handleTokenError func ๊ตฌํ˜„ ๋ฉ”์ธ ํ™”๋ฉด ๋ฐ ํ† ํฐ์„ ๊ธฐ๋ฐ˜์œผ๋กœ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ชจ๋“  ํ•จ์ˆ˜๋“ค์˜ error ์ฒ˜๋ฆฌ ๋ถ€๋ถ„์— ํ•ด๋‹น ํ•จ์ˆ˜

github.com

์ œ๊ฐ€ ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ ๊ณผ์ •์„ Escaping Closure๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์—ˆ๋Š”๋ฐ์š”.,

์ด๋ฒˆ ๊ธฐํšŒ์— RXSwift Observable๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (๋“œ.๋””.์–ด!)

ํ”„๋กœ์ ํŠธ ๋งˆ๊ฐ์ด 10์ผ ์ •๋„ ๋‚จ์•˜์Šต๋‹ˆ๋‹ค. (์ด๋ ‡๊ฒŒ ๊ฐœ๊ฐ•๋„ ๊ณง . . . ๐Ÿคฏ)

๋๊นŒ์ง€ ํ™”์ดํŒ…ํŒ…,, ๐Ÿ’ช

 

 

 

 

 

๐Ÿ“Œ ๊ธฐ์กด์˜ MVC + escaping closure ๊ธฐ๋ฐ˜์˜ ์ฝ”๋“œ 

//
//  AuthAPI.swift
//  Wetox-iOS
//
//  Created by ๊น€์†Œํ˜„ on 1/28/24.
//

import UIKit
import Moya

public class AuthAPI {
    static let shared = AuthAPI()
    var authProvider = MoyaProvider<AuthService>(plugins: [MoyaLoggerPlugin()])
    
    public init() { }
    
    // MARK: - ํšŒ์›๊ฐ€์ž…
    func register(registerRequest: RegisterRequest, profileImage: UIImage?, completion: @escaping (NetworkResult<Any>) -> Void) {
        authProvider.request(.register(registerRequest: registerRequest, profileImage: profileImage)) { (result) in
            switch result {
            case .success(let response):
                let statusCode = response.statusCode
                let data = response.data
                let networkResult = self.judgeRegisterStatus(by: statusCode, data)
                completion(networkResult)
                
            case .failure(let error):
                print("error: \(error)")
            }
        }
    }
    
    func login(tokenRequest: TokenRequest, completion: @escaping (NetworkResult<Any>) -> Void) {
        authProvider.request(.login(tokenRequest: tokenRequest)) { (result) in
            switch result {
            case .success(let response):
                let statusCode = response.statusCode
                let data = response.data
                let networkResult = self.judgeLoginStatus(by: statusCode, data)
                completion(networkResult)
                
            case .failure(let error):
                print("error: \(error)")
            }
        }
    }
    
    func logout(completion: @escaping (NetworkResult<Any>) -> Void) {
        authProvider.request(.logout) { (result) in
            switch result {
            case .success(let response):
                let statusCode = response.statusCode
                let data = response.data
                let networkResult = self.judgeLoginStatus(by: statusCode, data)
                completion(networkResult)
                
            case .failure(let error):
                print("error: \(error)")
            }
        }
    }
    
    private func judgeRegisterStatus(by statusCode: Int, _ data: Data) -> NetworkResult<Any> {
        let decoder = JSONDecoder()
        guard let decodedData = try? decoder.decode(RegisterResponse.self, from: data) else { return .pathError }
        
        switch statusCode {
        case 200:
            return .success(decodedData)
        case 400..<500:
            return .requestError
        case 500:
            return .serverError
        default:
            return .networkFail
        }
    }
    
    private func judgeLoginStatus(by statusCode: Int, _ data: Data) -> NetworkResult<Any> {
        let decoder = JSONDecoder()
        guard let decodedData = try? decoder.decode(TokenResponse.self, from: data) else { return .pathError }
        
        switch statusCode {
        case 200:
            return .success(decodedData)
        case 400..<500:
            return .requestError
        case 500:
            return .serverError
        default:
            return .networkFail
        }
    }
    
    private func judgeLogoutStatus(by statusCode: Int, _ data: Data) -> NetworkResult<Any> {
        let decoder = JSONDecoder()
        guard let decodedData = try? decoder.decode(String.self, from: data) else { return .pathError }
        
        switch statusCode {
        case 200:
            return .success(decodedData)
        case 400..<500:
            return .requestError
        case 500:
            return .serverError
        default:
            return .networkFail
        }
    }
}

 

 

 

๐Ÿ“Œ ํ˜ธ์ถœ๋˜๋Š” ๋ถ€๋ถ„

//
//  LoginViewController.swift
//  Wetox-iOS
//
//  Created by ๊น€์†Œํ˜„ on 1/24/24.
//

import UIKit
import SnapKit

import KakaoSDKUser
import KakaoSDKAuth
import AuthenticationServices

// ์ƒ๋žต

extension LoginViewController {
    func loginWithAPI(tokenRequest: TokenRequest) {
        AuthAPI.shared.login(tokenRequest: tokenRequest) { response in
            switch response {
                case .success(let loginData):
                    if let data = loginData as? TokenResponse {
                        UserDefaults.standard.set(tokenRequest.oauthProvider, forKey: Const.UserDefaultsKey.oauthProvider)
                        UserDefaults.standard.set(data.accessToken, forKey: Const.UserDefaultsKey.accessToken)
                        UserDefaults.standard.set(Date(), forKey: Const.UserDefaultsKey.updatedAt)
                        UserDefaults.standard.set(true, forKey: Const.UserDefaultsKey.isLogin)
                    }
                case .requestError:
                    print("loginWithAPI - requestError")
                case .pathError:
                    print("loginWithAPI - pathError")
                case .serverError:
                    print("loginWithAPI - serverError")
                case .networkFail:
                    print("loginWithAPI - networkFail")
            }
        }
    }
}

 

 

 

 

 

 

๐Ÿ“Œ new RXSwift ๊ธฐ๋ฐ˜์˜ ์ฝ”๋“œ

  • ์•„์ง MVVM ํ˜•์‹์œผ๋กœ ๋ทฐ๋ชจ๋ธ๊นŒ์ง€๋Š” ๋ถ„๋ฆฌ๋ฅผ ๋ชปํ–ˆ๋‹ค ๐Ÿฅฒ
  • ๊ธฐ์กด์— ํด๋กœ์ €๋กœ response๋ฅผ ๊ฐ€์ ธ์˜จ ๋กœ์ง์„ rx Observable๋กœ ๊ฐ€์ ธ์˜ค๋„๋ก ๋ฐ”๋€Œ์—ˆ์Šต๋‹ˆ๋‹ค. 
  • ๋‚˜์•„๊ฐ€ expired token์œผ๋กœ ๋ฐœ์ƒํ•  ์˜ค๋ฅ˜์— ๋Œ€ํ•ด, ์žฌ๋ฐœ๊ธ‰ request๋ฅผ ๋‚ ๋ฆด ์ˆ˜ ์žˆ๋„๋ก ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. 
  • RXSwift ์ฒ˜์Œ ์จ๋ณด๋Š”๋ฐ์š”, ์‹ ๊ธฐํ•˜๋„ค์š” . . ๋‹ค์Œ๋ฒˆ์—๋Š” ๋” ์ž˜ ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„์š” :)  
//
//  AuthAPI.swift
//  Wetox-iOS
//
//  Created by ๊น€์†Œํ˜„ on 1/28/24.
//

import UIKit
import Moya
import RxSwift
import RxMoya

public class AuthAPI {
    static let authProvider = MoyaProvider<AuthService>(plugins: [MoyaLoggerPlugin()])
    
    static func register(registerRequest: RegisterRequest, profileImage: UIImage?) -> Observable<RegisterResponse> {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        
        return AuthAPI.authProvider.rx.request(.register(registerRequest: registerRequest, profileImage: profileImage))
            .map(RegisterResponse.self, using: decoder)
            .asObservable()
            .catch { error in
                if let moyaError = error as? MoyaError {
                    switch moyaError {
                    case .statusCode(let response):
                        // HTTP ์ƒํƒœ ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ์ฒ˜๋ฆฌ
                        print("HTTP Status Code: \(response.statusCode)")
                    case .jsonMapping(let response):
                        // JSON ๋งคํ•‘ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
                        print("JSON Mapping Error for Response: \(response)")
                    default:
                        // ๊ธฐํƒ€ Moya ์—๋Ÿฌ ์ฒ˜๋ฆฌ
                        print("Other MoyaError: \(moyaError.localizedDescription)")
                    }
                }
                return Observable.error(error)
            }
    }
    
    static func login(tokenRequest: TokenRequest) -> Observable<TokenResponse> {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        
        return AuthAPI.authProvider.rx.request(.login(tokenRequest: tokenRequest))
            .map(TokenResponse.self, using: decoder)
            .asObservable()
            .catch { error in
                handleTokenError(error: error, request: .login(tokenRequest: tokenRequest))
            }
    }
    
    static func handleTokenError(error: Error, request: AuthService) -> Observable<TokenResponse> {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        
        if let moyaError = error as? MoyaError, moyaError.response?.statusCode == 401 {
            print("๋งŒ๋ฃŒ๋œ ํ† ํฐ์— ๋Œ€ํ•˜์—ฌ ์žฌ๋ฐœ๊ธ‰์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.")
            
            let refreshTokenRequest = TokenRequest(oauthProvider: UserDefaults.standard.string(forKey: Const.UserDefaultsKey.oauthProvider) ?? String(), 
                                                    openId: UserDefaults.standard.string(forKey: Const.UserDefaultsKey.openId) ?? String())
            
            return AuthAPI.authProvider.rx.request(.login(tokenRequest: refreshTokenRequest))
                .map(TokenResponse.self, using: decoder)
                .asObservable()
        }
        else {
            return Observable.error(error)
        }
    }
}

 

 

 

๐Ÿ“Œ ํ˜ธ์ถœ๋˜๋Š” ๋ถ€๋ถ„

//
//  LoginViewController.swift
//  Wetox-iOS
//
//  Created by ๊น€์†Œํ˜„ on 1/24/24.
//

import UIKit
import SnapKit

import KakaoSDKUser
import KakaoSDKAuth
import AuthenticationServices
import RxSwift

// ์ƒ๋žต

extension LoginViewController {
    func loginWithAPI(tokenRequest: TokenRequest) {
        AuthAPI.login(tokenRequest: tokenRequest)
            .subscribe(onNext: { tokenResponse in
                UserDefaults.standard.set(tokenRequest.oauthProvider, forKey: Const.UserDefaultsKey.oauthProvider)
                UserDefaults.standard.set(tokenResponse.accessToken, forKey: Const.UserDefaultsKey.accessToken)
                UserDefaults.standard.set(Date(), forKey: Const.UserDefaultsKey.updatedAt)
                UserDefaults.standard.set(true, forKey: Const.UserDefaultsKey.isLogin)
                print("๋กœ๊ทธ์ธ ์„ฑ๊ณต: \(tokenResponse)")
            }, onError: { error in
                print("๋กœ๊ทธ์ธ ์‹คํŒจ: \(error.localizedDescription)")
            })
            .disposed(by: disposeBag)
    }
}