์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- ํ๋ก๊ทธ๋๋จธ์ค
- Swfit
- designpattern
- arkit
- XCTest
- tableView
- Kuring
- Flutter
- rxcocoa
- ํจ์คํธ์บ ํผ์ค
- combine
- RxSwift
- UIKit
- reactorkit
- Xcode
- ios
- raywenderlich
- CollectionView
- Lv2
- ๋ฐฑ์ค
- realm
- SnapKit
- BFS
- BOJ
- TCA
- swift
- SwiftUI
- visionOS
- node.js
- MVVM
- Today
- Total
lgvv98
[ReactorKit] ReactorKit ๊ณต๋ถํ๊ธฐ #5 RxTodo ๋ฐ๋ผ์ก๊ธฐ (3) ๋ณธ๋ฌธ
[ReactorKit] ReactorKit ๊ณต๋ถํ๊ธฐ #5 RxTodo ๋ฐ๋ผ์ก๊ธฐ (3)
๐ฅ ์บ๋ฟ๋งจ 2022. 9. 12. 18:38ReactorKit ๊ณต๋ถํ๊ธฐ #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))
}
}
'apple > ๐ฆ UIKit & ReactiveX' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[ReactorKit] ReactorKit ๊ณต๋ถํ๊ธฐ #7 View (programming) (0) | 2022.10.01 |
---|---|
[ReactorKit] ReactorKit ๊ณต๋ถํ๊ธฐ #6 transform (0) | 2022.09.22 |
[ReactorKit] ReactorKit ๊ณต๋ถํ๊ธฐ #4 RxTodo ๋ฐ๋ผ์ก๊ธฐ (2) (0) | 2022.09.08 |
[ReactorKit] ReactorKit ๊ณต๋ถํ๊ธฐ #3 RxTodo ๋ฐ๋ผ์ก๊ธฐ (1) (0) | 2022.09.07 |
[ReactorKit] ReactorKit ๊ณต๋ถํ๊ธฐ #2 (0) | 2022.07.24 |