본문 바로가기
IOS

[iOS] ReactorKit 프로젝트 Drrrible 뜯어보기(2)

by eigen96 2022. 10. 13.
728x90

리액터킷 적용 예제를 살펴보았습니다.

지난 글에서 로그인 액션이 발생하였을 때 setLoading(true) Mutation을 발생시키며 현재 로딩중임을 알리고

authorize()라는 인증하는 비즈니스 로직을 호출하는 흐름을 쭉 훑어보았습니다.

해당 인증 로직은 서비스 레이어로 넘긴 것을 알 수 있었죠.

 

서비스 레이어를 살펴보도록 하겠습니다.

비즈니스 로직이 있는 리액터(ViewModel)에 필요한 데이터들을 불러올때

추상화 시킨 데이터 리포지토리(Network디렉토리)를 통해 데이터 소스(local, remote 등등)에 상관없이 서비스 레이어에서 하나의 인터페이스로 데이터를 불러 사용할 수 있도록 구성해두었다는 것을 알 수 있습니다.

 

final class UserService: UserServiceType {
  fileprivate let networking: DrrribleNetworking

  init(networking: DrrribleNetworking) {
    self.networking = networking
  }

  fileprivate let userSubject = ReplaySubject<User?>.create(bufferSize: 1)
  lazy var currentUser: Observable<User?> = self.userSubject.asObservable()
    .startWith(nil)
    .share(replay: 1)

  func fetchMe() -> Single<Void> {
    return self.networking.request(.me)
      .map(User.self)
      .do(onSuccess: { [weak self] user in
        self?.userSubject.onNext(user)
      })
      .map { _ in }
  }
}

 

 

final class AuthService: AuthServiceType {

  fileprivate let clientID = "130182af71afe5247b857ef622bd344ca5f1c6144c8fa33c932628ac31c5ad78"
  fileprivate let clientSecret = "bbebedc51c2301049c2cb57953efefc30dc305523b8fdfadb9e9a25cb81efa1e"

  fileprivate var currentViewController: UIViewController?
  fileprivate let callbackSubject = PublishSubject<String>()

  fileprivate let keychain = Keychain(service: "com.drrrible.ios")
  private(set) var currentAccessToken: AccessToken?

  private let navigator: NavigatorType

  init(navigator: NavigatorType) {
    self.navigator = navigator
    self.currentAccessToken = self.loadAccessToken()
    log.debug("currentAccessToken exists: \(self.currentAccessToken != nil)")
  }

  func authorize() -> Observable<Void> {
    let parameters: [String: String] = [
      "client_id": self.clientID,
      "scope": "public+write+comment+upload",
    ]
    let parameterString = parameters.map { "\($0)=\($1)" }.joined(separator: "&")
    let url = URL(string: "https://dribbble.com/oauth/authorize?\(parameterString)")!

    // Default animation of presenting SFSafariViewController is similar to 'push' animation
    // (from right to left). To use 'modal' animation (from bottom to top), we have to wrap
    // SFSafariViewController with UINavigationController and set navigation bar hidden.
    let safariViewController = SFSafariViewController(url: url)
    let navigationController = UINavigationController(rootViewController: safariViewController)
    navigationController.isNavigationBarHidden = true
    self.navigator.present(navigationController)
    self.currentViewController = navigationController

    return self.callbackSubject
      .flatMap(self.accessToken)
      .do(onNext: { [weak self] accessToken in
        try self?.saveAccessToken(accessToken)
        self?.currentAccessToken = accessToken
      })
      .map { _ in }
  }

  func callback(code: String) {
    self.callbackSubject.onNext(code)
    self.currentViewController?.dismiss(animated: true, completion: nil)
    self.currentViewController = nil
  }

  func logout() {
    self.currentAccessToken = nil
    self.deleteAccessToken()
  }

  fileprivate func accessToken(code: String) -> Single<AccessToken> {
    let urlString = "https://dribbble.com/oauth/token"
    let parameters: Parameters = [
      "client_id": self.clientID,
      "client_secret": self.clientSecret,
      "code": code,
    ]
    return Single.create { observer in
      let request = AF
        .request(urlString, method: .post, parameters: parameters)
        .responseData { response in
          switch response.result {
          case let .success(jsonData):
            do {
              let accessToken = try JSONDecoder().decode(AccessToken.self, from: jsonData)
              observer(.success(accessToken))
            } catch let error {
              observer(.error(error))
            }

          case let .failure(error):
            observer(.error(error))
          }
        }
      return Disposables.create {
        request.cancel()
      }
    }
  }

  fileprivate func saveAccessToken(_ accessToken: AccessToken) throws {
    try self.keychain.set(accessToken.accessToken, key: "access_token")
    try self.keychain.set(accessToken.tokenType, key: "token_type")
    try self.keychain.set(accessToken.scope, key: "scope")
  }

  fileprivate func loadAccessToken() -> AccessToken? {
    guard let accessToken = self.keychain["access_token"],
      let tokenType = self.keychain["token_type"],
      let scope = self.keychain["scope"]
    else { return nil }
    return AccessToken(accessToken: accessToken, tokenType: tokenType, scope: scope)
  }

  fileprivate func deleteAccessToken() {
    try? self.keychain.remove("access_token")
    try? self.keychain.remove("token_type")
    try? self.keychain.remove("scope")
  }

}

 

 

원래는 더 자세하게 코드 한줄한줄 살펴보고자 하였으나 일단 여기서 멈추게 되었습니다.

 

Drrrible과 달리 현재 Bidit의 레파지토리에서 데이터를 받아오는 수단으로 Apolo를 사용하고 있고

Rx로 콜백을 처리하지 않고 Completion 구조로 받아와서 그 콜백 내에서 옵저버로 콜을 해주고 있기 때문에
현재 코드에 더 집중해보고자 합니다.

728x90

댓글