apple/RxSwift, ReactorKit

[ReactorKit] ReactorKit 공부하기 #5 RxTodo 따라잡기 (3)

lgvv 2022. 9. 12. 18:38

ReactorKit 공부하기 #5 RxTodo 따라잡기 (3)

목표: Service 도입을 위해 RxTodo 코드 분석하기

 

RxTodo Service쪽 코드 분석

ServiceProvider.swift

  • ServiceProvider는 ServiceProviderType을 상속받아서 provider의 self로 주어 구현하는 ServiceProvider를 채택할 클래스에서 구현
protocol ServiceProviderType: class {
  var userDefaultsService: UserDefaultsServiceType { get }
  var alertService: AlertServiceType { get }
  var taskService: TaskServiceType { get }
}

final class ServiceProvider: ServiceProviderType {
  lazy var userDefaultsService: UserDefaultsServiceType = UserDefaultsService(provider: self)
  lazy var alertService: AlertServiceType = AlertService(provider: self)
  lazy var taskService: TaskServiceType = TaskService(provider: self)
}

 

 

UserDefaultsService.swift

  • 해당 형태로 구현하는 부분은 처음보는 방법인데, 객체지향적으로 더 좋은 방법으로 생각한다 배우기.
import Foundation

extension UserDefaultsKey {
  static var tasks: Key<[[String: Any]]> { return "tasks" }
}

protocol UserDefaultsServiceType {
  func value<T>(forKey key: UserDefaultsKey<T>) -> T?
  func set<T>(value: T?, forKey key: UserDefaultsKey<T>)
}

final class UserDefaultsService: BaseService, UserDefaultsServiceType {

  private var defaults: UserDefaults {
    return UserDefaults.standard
  }

  func value<T>(forKey key: UserDefaultsKey<T>) -> T? {
    return self.defaults.value(forKey: key.key) as? T
  }

  func set<T>(value: T?, forKey key: UserDefaultsKey<T>) {
    self.defaults.set(value, forKey: key.key)
    self.defaults.synchronize()
  }

}

 

 

기존에 내가 사용하던 방식

  • 객체지향적으로 부족한 점이 많아보임
  • 수정할 때마다 모듈이 수정되어야 하기 때문에 ㅠ
// ✅ 내가 기존 프로젝트에서 사용했던 방식

extension StringSet { 
    public struct UserDefaultKey { 
         private static let `default` = "project.bundle.id"
         
         public static let tasks = "\(`default`).task"
         public static let tempTask = "\(`default`).temp.task"
    }
}

class UserDefaultManager { 
    // NOTE: - ✅ 기본형
    static var tasks: [Task] { 
    	get { UserDefaults.standard.object(forKey: StringSet.UserDefaultKey.tasks) }
        set { UserDefaults.standard.set(newValue, forKey: StringSet.UserDefaultKey.tasks) }
    }
    
    // NOTE: - ✅ 프로퍼티 래퍼 사용 - 인코등 디코딩 제공 (주의: 모델이 Codable 상속 필수)
    @UserDefault(key: StringSet.UserDefaultKey.tasks, defaultValue: [], storage: .standard)
    static var tasks: [Task]
    
}

@propertyWrapper
struct UserDefault<T: Codable> {
    let key: String
    let defaultValue: T
    let storage: UserDefaults

    var wrappedValue: T {
        get {
            guard let data = self.storage.object(forKey: self.key) as? Data else { return defaultValue }
            return (try? PropertyListDecoder().decode(T.self, from: data)) ?? self.defaultValue
        }
        set {
            let encodedData = try? PropertyListEncoder().encode(newValue)
            self.storage.set(encodedData, forKey: self.key)
        }
    }

    init(key: String, defaultValue: T, storage: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.storage = storage
    }
}

 

 

UserDefaultKey<T>.Swift

  • RxTodo의 코드 분석
/// A generic key for `UserDefaults`. Extend this type to define custom user defaults keys.
///
/// ```
/// extension UserDefaultsKey {
///   static var myKey: Key<String> {
///     return "myKey"
///   }
///
///   static var anotherKey: Key<Int> {
///     return "anotherKey"
///   }
/// }
/// ```
struct UserDefaultsKey<T> {
  typealias Key<T> = UserDefaultsKey<T>
  let key: String
}

extension UserDefaultsKey: ExpressibleByStringLiteral {
  public init(unicodeScalarLiteral value: StringLiteralType) {
    self.init(key: value)
  }

  public init(extendedGraphemeClusterLiteral value: StringLiteralType) {
    self.init(key: value)
  }

  public init(stringLiteral value: StringLiteralType) {
    self.init(key: value)
  }
}

 


BaseService.swift

  • Service도 BaseService class를 선언해서 제공
class BaseService {
  unowned let provider: ServiceProviderType

  init(provider: ServiceProviderType) {
    self.provider = provider
  }
}

 

 

TaskService.swift

이게 좀 핵심인 것 같은데, 구조를 꼼꼼하게 분석해보고자 함. 코드 설명은 주석으로.

import RxSwift

// 1. TaskEvent를 정의
enum TaskEvent {
  case create(Task)
  case update(Task)
  case delete(id: String)
  case move(id: String, to: Int)
  case markAsDone(id: String)
  case markAsUndone(id: String)
}

// 2. TaskServiceType을 정의
protocol TaskServiceType {
  var event: PublishSubject<TaskEvent> { get } // 이벤트를 받을 Subject
  func fetchTasks() -> Observable<[Task]> // tasks를 fetch할 메소드

  @discardableResult // 결과를 사용하지 않아도 warning이 나타나지 않음
  func saveTasks(_ tasks: [Task]) -> Observable<Void> 

  func create(title: String, memo: String?) -> Observable<Task>
  func update(taskID: String, title: String, memo: String?) -> Observable<Task>
  func delete(taskID: String) -> Observable<Task>
  func move(taskID: String, to: Int) -> Observable<Task>
  func markAsDone(taskID: String) -> Observable<Task>
  func markAsUndone(taskID: String) -> Observable<Task>
}

// 3. TaskService를 구현
final class TaskService: BaseService, TaskServiceType {
  // 3-1. 이벤트를 받아들일 서브젝트
  let event = PublishSubject<TaskEvent>()

  // 3-2. task fetch
  func fetchTasks() -> Observable<[Task]> {
    // 저장된 데이터가 있다면 값 가져오기
    if let savedTaskDictionaries = self.provider.userDefaultsService.value(forKey: .tasks) {
      let tasks = savedTaskDictionaries.compactMap(Task.init)
      return .just(tasks) // 값이 있다면 리턴해서 defaultTask를 거치지 않게 된다.
    }
    
    // 저장된 값이 없다면 기본 task 값으로 설정
    let defaultTasks: [Task] = [
      Task(title: "Go to https://github.com/devxoul"),
      Task(title: "Star repositories I am intersted in"),
      Task(title: "Make a pull request"),
    ]
    
    // 디폴트 값으로 딕셔너리 만들기
    let defaultTaskDictionaries = defaultTasks.map { $0.asDictionary() }
    // 디폴트 값 저장
    self.provider.userDefaultsService.set(value: defaultTaskDictionaries, forKey: .tasks)
    // 디폴트 값으로 리턴
    return .just(defaultTasks) 
  }

  @discardableResult
  func saveTasks(_ tasks: [Task]) -> Observable<Void> {
    let dicts = tasks.map { $0.asDictionary() } // task를 dictionary 형태로 변경
    self.provider.userDefaultsService.set(value: dicts, forKey: .tasks) // 저장
    return .just(Void()) // 빈값 내보내기
  }
  
  func create(title: String, memo: String?) -> Observable<Task> { 
  // 생성의 경우에는 생성 후 UI업데이트가 일어나야 하므로 이렇게 처리
    return self.fetchTasks() 
      .flatMap { [weak self] tasks -> Observable<Task> in // task를 옵저버블 형태로 방출
        guard let `self` = self else { return .empty() } 
        let newTask = Task(title: title, memo: memo) // 새로운 task 값을 생성
        return self.saveTasks(tasks + [newTask]).map { newTask } // 새로운 태스크 값 저장 후 map해서 방출
      }
      .do(onNext: { task in
        self.event.onNext(.create(task)) // 이벤트에 생성이라고 하나 내보냄
      })
  }
  
  func update(taskID: String, title: String, memo: String?) -> Observable<Task> {
    // 업데이트도 똑같이 싱크를 맞추기 위해 fetch
    return self.fetchTasks()
      .flatMap { [weak self] tasks -> Observable<Task> in
        guard let `self` = self else { return .empty() }
        // 같은 id가 있는지 찾아서 있는 경우에만 진행
        guard let index = tasks.index(where: { $0.id == taskID }) else { return .empty() }
        
        var tasks = tasks
        // with의 경우에는 Then 라이브러리에서 선언되어 있음
        let newTask = tasks[index].with { 
          $0.title = title
          $0.memo = memo
        }
        tasks[index] = newTask
        return self.saveTasks(tasks).map { newTask }
      }
      .do(onNext: { task in
        self.event.onNext(.update(task)) // 새로운 태스크를 이벤트로 방출
      })
  }

  func delete(taskID: String) -> Observable<Task> {
    // 이것도 똑같이 fetch해야해
    return self.fetchTasks()
      .flatMap { [weak self] tasks -> Observable<Task> in
        guard let `self` = self else { return .empty() }
        guard let index = tasks.index(where: { $0.id == taskID }) else { return .empty() }
        var tasks = tasks
        let deletedTask = tasks.remove(at: index) // 해당 인덱스 삭제하면 끝
        return self.saveTasks(tasks).map { deletedTask } // 값 저장 후에 방출
      }
      .do(onNext: { task in
        self.event.onNext(.delete(id: task.id)) // 삭제된 id 이벤트로 전달
      })
  }

  func move(taskID: String, to destinationIndex: Int) -> Observable<Task> {
    return self.fetchTasks()
      .flatMap { [weak self] tasks -> Observable<Task> in
        guard let `self` = self else { return .empty() }
        guard let sourceIndex = tasks.index(where: { $0.id == taskID }) else { return .empty() }
        var tasks = tasks
        let task = tasks.remove(at: sourceIndex) // 삭제하고
        tasks.insert(task, at: destinationIndex) // 이동된 위치에 삽입
        return self.saveTasks(tasks).map { task }
      }
      .do(onNext: { task in
        self.event.onNext(.move(id: task.id, to: destinationIndex)) // 아이디 값이랑 이동된 위치 반환
      })
  }

  func markAsDone(taskID: String) -> Observable<Task> {
    // UI옆에 체크표시 여부
    return self.fetchTasks()
      .flatMap { [weak self] tasks -> Observable<Task> in
        guard let `self` = self else { return .empty() }
        guard let index = tasks.index(where: { $0.id == taskID }) else { return .empty() }
        var tasks = tasks
        let newTask = tasks[index].with {
          $0.isDone = true
          return
        }
        tasks[index] = newTask
        return self.saveTasks(tasks).map { newTask }
      }
      .do(onNext: { task in
        self.event.onNext(.markAsDone(id: task.id)) // 완료되었다고 id를 이벤트로 전달
      })
  }

  func markAsUndone(taskID: String) -> Observable<Task> {
    return self.fetchTasks()
      .flatMap { [weak self] tasks -> Observable<Task> in
        guard let `self` = self else { return .empty() }
        guard let index = tasks.index(where: { $0.id == taskID }) else { return .empty() }
        var tasks = tasks
        let newTask = tasks[index].with {
          $0.isDone = false
          return
        }
        tasks[index] = newTask
        return self.saveTasks(tasks).map { newTask }
      }
      .do(onNext: { task in
        self.event.onNext(.markAsUndone(id: task.id))
      })
  }


}

 

TaskListReactor 살펴보기

import ReactorKit
import RxCocoa
import RxDataSources
import RxSwift

typealias TaskListSection = SectionModel<Void, TaskCellReactor>

final class TaskListViewReactor: Reactor {

  enum Action {
    case refresh
    case toggleEditing
    case toggleTaskDone(IndexPath)
    case deleteTask(IndexPath)
    case moveTask(IndexPath, IndexPath)
  }

  enum Mutation {
    case toggleEditing
    case setSections([TaskListSection])
    case insertSectionItem(IndexPath, TaskListSection.Item)
    case updateSectionItem(IndexPath, TaskListSection.Item)
    case deleteSectionItem(IndexPath)
    case moveSectionItem(IndexPath, IndexPath)
  }

  struct State {
    var isEditing: Bool
    var sections: [TaskListSection]
  }

  // 🌼 1. Properties 선언
  let provider: ServiceProviderType
  let initialState: State
  
  // 🌼 2. init의 파라미터로 전달하여 초기화
  init(provider: ServiceProviderType) {
    self.provider = provider 
    self.initialState = State(
      isEditing: false,
      sections: [TaskListSection(model: Void(), items: [])]
    )
  }
  
  // 액션에 따른 변화 
  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case .refresh:
      return self.provider.taskService.fetchTasks() // 🌼 3. Service를 이렇게 사용
        .map { tasks in
          let sectionItems = tasks.map(TaskCellReactor.init)
          let section = TaskListSection(model: Void(), items: sectionItems)
          return .setSections([section])
        }

    case .toggleEditing:
      return .just(.toggleEditing)

    case let .toggleTaskDone(indexPath):
      let task = self.currentState.sections[indexPath].currentState // 현재 state의 값을 가져오기
      if !task.isDone {
        return self.provider.taskService.markAsDone(taskID: task.id).flatMap { _ in Observable.empty() }
      } else {
        return self.provider.taskService.markAsUndone(taskID: task.id).flatMap { _ in Observable.empty() }
      }

    case let .deleteTask(indexPath):
      let task = self.currentState.sections[indexPath].currentState
      return self.provider.taskService.delete(taskID: task.id).flatMap { _ in Observable.empty() }

    case let .moveTask(sourceIndexPath, destinationIndexPath):
      let task = self.currentState.sections[sourceIndexPath].currentState
      return self.provider.taskService.move(taskID: task.id, to: destinationIndexPath.item)
        .flatMap { _ in Observable.empty() }
    }
  }

  func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
    let taskEventMutation = self.provider.taskService.event
      .flatMap { [weak self] taskEvent -> Observable<Mutation> in
        self?.mutate(taskEvent: taskEvent) ?? .empty()
      }
    return Observable.of(mutation, taskEventMutation).merge()
  }

  private func mutate(taskEvent: TaskEvent) -> Observable<Mutation> {
    let state = self.currentState
    switch taskEvent {
    case let .create(task):
      let indexPath = IndexPath(item: 0, section: 0)
      let reactor = TaskCellReactor(task: task)
      return .just(.insertSectionItem(indexPath, reactor))

    case let .update(task):
      guard let indexPath = self.indexPath(forTaskID: task.id, from: state) else { return .empty() }
      let reactor = TaskCellReactor(task: task)
      return .just(.updateSectionItem(indexPath, reactor))

    case let .delete(id):
      guard let indexPath = self.indexPath(forTaskID: id, from: state) else { return .empty() }
      return .just(.deleteSectionItem(indexPath))

    case let .move(id, index):
      guard let sourceIndexPath = self.indexPath(forTaskID: id, from: state) else { return .empty() }
      let destinationIndexPath = IndexPath(item: index, section: 0)
      return .just(.moveSectionItem(sourceIndexPath, destinationIndexPath))

    case let .markAsDone(id):
      guard let indexPath = self.indexPath(forTaskID: id, from: state) else { return .empty() }
      var task = state.sections[indexPath].currentState
      task.isDone = true
      let reactor = TaskCellReactor(task: task)
      return .just(.updateSectionItem(indexPath, reactor))

    case let .markAsUndone(id):
      guard let indexPath = self.indexPath(forTaskID: id, from: state) else { return .empty() }
      var task = state.sections[indexPath].currentState
      task.isDone = false
      let reactor = TaskCellReactor(task: task)
      return .just(.updateSectionItem(indexPath, reactor))
    }
  }

  func reduce(state: State, mutation: Mutation) -> State {
    var state = state
    switch mutation {
    case let .setSections(sections):
      state.sections = sections
      return state

    case .toggleEditing:
      state.isEditing = !state.isEditing
      return state

    case let .insertSectionItem(indexPath, sectionItem):
      state.sections.insert(sectionItem, at: indexPath)
      return state

    case let .updateSectionItem(indexPath, sectionItem):
      state.sections[indexPath] = sectionItem
      return state

    case let .deleteSectionItem(indexPath):
      state.sections.remove(at: indexPath)
      return state

    case let .moveSectionItem(sourceIndexPath, destinationIndexPath):
      let sectionItem = state.sections.remove(at: sourceIndexPath)
      state.sections.insert(sectionItem, at: destinationIndexPath)
      return state
    }
  }

  private func indexPath(forTaskID taskID: String, from state: State) -> IndexPath? {
    let section = 0
    let item = state.sections[section].items.index { reactor in reactor.currentState.id == taskID }
    if let item = item {
      return IndexPath(item: item, section: section)
    } else {
      return nil
    }
  }

  func reactorForCreatingTask() -> TaskEditViewReactor {
    return TaskEditViewReactor(provider: self.provider, mode: .new)
  }

  func reactorForEditingTask(_ taskCellReactor: TaskCellReactor) -> TaskEditViewReactor {
    let task = taskCellReactor.currentState
    return TaskEditViewReactor(provider: self.provider, mode: .edit(task))
  }

}