apple/UIKit & ReactiveX
[ReactorKit] ReactorKit 공부하기 #5 RxTodo 따라잡기 (3)
lgvv
2022. 9. 12. 18:38
ReactorKit 공부하기 #5 RxTodo 따라잡기 (3)
✅ 목표: Service 도입을 위해 RxTodo 코드 분석하기
✅ 내 프로젝트에서 서비스 도입한 결과 코드
🚨 Realm에서 tableView.rx.itemMoved시, 레코드 순서 어떻게 바꿔야할지 모르겠음.
Realm에 대한 이해가 부족해서 앱이 crash 나는 경우가 많은데, 이를 학습해서 보완해야함.
Realm에 Token을 사용해서 변경된 데이터의 싱크를 다른 View에서 맞출 수 있음.
코드를 분석하면서 Realm을 완벽하게 적용하여 만들지는 못했지만, BaseViewController랑 Service가 왜 필요한지 확실하게 느낀 시간이었음. 다음 스텝으로 Realm을 더 학습해서 완벽하게 적용해보자.
✅ RxTodo Service쪽 코드 분석 ✅
1. RxTodo의 서비스 폴더 구조
✅ 1. ServiceProvider.swift
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)
}
ServiceProvider는 ServiceProviderType을 상속받아서 provider의 self로 주어 구현하는 ServiceProvider를 채택할 클래스에서 구현하고자 함.
✅ 2. 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
}
}
✅ 2-1. 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)
}
}
✅ 3. BaseService.swift
class BaseService {
unowned let provider: ServiceProviderType
init(provider: ServiceProviderType) {
self.provider = provider
}
}
Service도 BaseService class를 선언해서 제공한다.
✅ 4. 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))
}
}