ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ReactorKit#5] Example : RxTodo 구조 분석[미완]
    iOS Dev/ReactorKit 2020. 6. 21. 02:35
    반응형

    이번에 따라해볼 코드는 https://github.com/devxoul/RxTodo

     

    devxoul/RxTodo

    iOS Todo Application using RxSwift and ReactorKit. Contribute to devxoul/RxTodo development by creating an account on GitHub.

    github.com

    RxTodo이다!

    간단히 설명을 하자면 +버튼을 누르면 테이블 뷰 최상단에 셀이 하나 삽입되게, Edit버튼을 누르면 셀 삭제, 셀 위치 이동, 셀 편집을 할 수 있게 하는 것이다. 셀을 클릭하면 체크표시가 나타난다.

    우선 코드를 보기 전에 프로젝트 구성이 교과서적으로 깔끔하게 되어있어서 한번 자세히 보고 넘어가려 한다.

    최상단 폴더인 RxTodo에는 세가지 폴더 [Sources], [Support], [Resources]가 있다. 
    [Sources] : .swift 파일들, 즉 코드들이 모두 여기에 위치하게 된다.
    [Support] : Info.plist가 안에 들어있다.
    [Resources] : 에셋, 스토리보드 파일들이 들어있다.

    Sources폴더는 main.swift, AppDelegate.swift, [Types], [Services], [Models], [ViewControllers], [Views], [Rx], [Utils] 으로 이루어져있다.
    main.swift : UIApplication쪽 일을 처리하는 파일이다. 아직 정확히 뭘 하는건지는 잘 모르겠다.
    AppDelegate.swift : 최초의 window를 정의한다. 나는 SceneDelegate에서 할 것이다.
    [Types] : 제네릭을 이용해서 UserDefaultsKey를 구현하는 UserDefaultsKey.swift를 담고 있다.
    [Services] : 여러 서비스들이 들어간다. UserDefaultsService, TaskService, AlertService가 눈에 띈다.
    [Models] : 모델이 정의되어있다.
    [ViewControllers] : ViewController과 ViewController에 대응되는 Reactor가 있다.
    [Views] : 셀과 셀에 대응되는 Reactor가 있다.
    [Rx] : RxSwift에 필요한 파일들?이 있는 것 같다. 
    [Utils] : 기타 extension, utils가 있다.

    쭉 훑어보니 이걸 모두 이해하고 그대로 내 프로젝트에 옮기는건 너무 비효율적일 것 같아서 구조 분석만 해봐야겠다.
    처음보는 코드(문법)들이 너무 많아서 시간이 오래 걸릴 것 같다. 나중에 실력이 더 쌓이면 제대로 뜯어봐야겠다.

     

    본격적인 구조 분석 시작.

    TaskListViewReactor 구조 :

    Action : (View에서 이벤트를 받고 Mutation으로 액션 전달. mutate()에서 구현.)
     refresh - 새로고침 액션 전달
     toggleEditing - Edit 뷰로 넘어가겠다는 액션 전달
     toggleTaskDone(IndexPath) - TaskDone, 즉 체크표시를 IndexPath위치에 하겠다는 액션 전달
     deleteTask(IndexPath) - IndexPath 위치의 Task를 삭제하겠다는 액션 전달
     moveTask(IndexPath, IndexPath) - IndexPath의 위치에서 IndexPath의 위치로 Task를 옮기겠다는 액션 전달

    Mutation : (Action에서 액션을 받아서 State를 갱신하는 명령 전달. reduce()에서 구현.)
     toggleEditing - Edit 뷰로 넘어가라는 명령 전달
     setSections([TaskListSection]) - TaskListSection 데이터를 세팅하라는 명령 전달.
     insertSectionItem(IndexPath, TaskListSection.Item) - IndexPath 위치에 TaskListSection.Item을 삽입.
     updateSectionItem(IndexPath, TaskListSection.Item) - IndexPath 위치에 TaskListSection.Item을 갱신.
     deleteSectionItem(IndexPath) - IndexPath 위치에 있는 Item을 삭제.
     moveSectionItem(IndexPath, IndexPath) - IndexPath위치에 있던 Item을 다른 IndexPath위치로 이동.

    State : (Mutation에서 명령을 받아서 View 갱신.)
     isEditing: Bool - Edit 상태 판별
     sections: [TaskListSection] - 뷰에 표시될 데이터들.

    mutate() : 
     .refresh - task들을 불러와서 .setSections([TaskListSection])을 리턴.
     .toggleEditing - 단순히 Edit뷰로 넘어가는 것 이므로 따로 인자 없이 .toggleEditing을 리턴.
     .toggleTaskDone(indexPath) - 현재 indexPath가 체크(mark)가 없으면 taskService의 markAsDone을 실행. 있으면 taskService의 markAsUndone을 실행.
     .deleteTask(indexPath) - taskService의 delete를 실행.
     .moveTask(sourceIndexPath, destinationIndexPath) - taskService의 move를 실행.

    추가 설명 : .toggleTaskDone에서 return할 때

    return self.provider.taskService.markAsDone(taskID: task.id).flatMap { _ in Observable.empty() }

    이렇게 되어있는데,

    return self.provider.taskService.markAsDone(taskID: task.id).flatMap { (Task) -> Observable<Mutation> in
    	return Observable.empty()
    }

    이 문장이랑 같다. 즉 markAsDone을 실행한 후 Observable.empty()를 리턴한다.
    그럼 markAsDone을 한번 뜯어보자. 

    func markAsDone(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 = true
    			return
    		}
    		tasks[index] = newTask
    		return self.saveTasks(tasks).map { newTask }
    	}
    	.do(onNext: { task in
    	self.event.onNext(.markAsDone(id: task.id))
    	})
    }

    self.fetchTasks()는 task 목록을 불러오는 것이고, flatMap은 데이터 가공 후 Observable형식으로 리턴. 아무튼 현재 리스트를 가져와서 인자로 받은 taskID를 갖고 있는 task를 찾아서, 그 친구의 isDone을 true로 바꿔주고, saveTasks를 실행시켜준다.

    return self.saveTasks(tasks).map { newTask } 는 다음과 같은 코드다.

    return self.saveTasks(tasks).map { () -> Task in
    	return newTask
    }

     

    결국 self.saveTasks(tasks)가 Observable<Void>를 리턴하기 때문에 이 한 문장은 task하나를 수정하고, save를 한 다음 수정한 그 task를 Observable<Task> 형태로 리턴하는 역할을 수행하고 있다.

    그럼 맨 아래에 있는 do는 뭐냐. reactivex.io/documentation/operators/do.html 여기서 볼 수 있다.
    위에서 수정된 task 하나가 Observable에 싸여진 채로 내려왔고, PublishSubject타입인 self.event의 onNext에 TaskEvent의 .markAsDone을 보낸다.

    이렇게 .toggleTaskDone은 Observable.empty()를 리턴하고, 우리는 이제 transform을 봐야 한다.
    왜냐하면 우리는 task에 mark표시를 했지만 뷰는 아직 업데이트가 안되어있기 때문이다. 여기서 끝나면 안되고 state의 업데이트까지 플로우를 연결시켜주어야 한다.
    transform(mutation:)에 대해 조금 부연 설명을 하면, 위에 mutate에서 event에 onNext()를 보내는 행동을 하면, 그 event를 Observable<Mutation>으로 적절하게 바꾸어서 위에 mutate에서 나온 값과 함께 reduce()로 보낸다.

    예를 들면, 위에 mutate(action:)에서 .refresh 액션을 한다고 하면, .setSections([section])을 리턴 할 것이다. 그리고 transform(mutation:)으로 간다. 이 mutation에 들어가는 인자는 .setSections([section])의 값이다.
    let taskEventMutation에서 self.provider.taskService.event의 값을 가져와야 하는데, 우리는 이 event 값을 준 적이 없으므로 .empty()가 나올 것이다. 즉, transform()을 하고 나온 값은 .setSections([section])과 .empty()를 합친 값이다.

    또 다른 예를 들면, 위에 mutate(action:)에서 .deleteTask를 한다고 하면, self.provider.taskService.event의 값을 .delete(id)의 값으로 넣어주고 .empty()를 리턴 할 것이다. 그리고 transform(mutation:)으로 간다. 이 mutation에 들어가는 인자는 .empty() 이다.
    let taskEventMutation에서 self.provider.taskService.event의 값을 가져와서 아래에 있는 또 다른 mutate(taskEvent:) 에 전달하고, .deleteSectionItem을 받는다. 즉, transform()을 하고 나온 값은 .empty()와 .deleteSectionItem을 합친 값이다.

     

    이렇게 mutate() -> transform()을 하고 reduce()에 갈 차례이다.

    reduce() :
     .setSections(sections) - state.sections에 sections를 세팅.
     .toggleEditing - state.isEditing에 기존 state.isEditing의 반대 값을 세팅.
     .insertSectionItem(indexPath, sectionItem) - state.sections의 indexPath 위치에 sectionItem을 세팅.
     .updateSectionItem(indexPath, sectionItem) - state.sections의 indexPath 위치에 sectionItem을 세팅.
     .deleteSectionItem(indexPath) - state.sections의 indexPath위치를 삭제.
     .moveSectionItem(sourceIndexPath, destinationIndexPath) - state.sections의 sourceIndexPath의 아이템을 삭제하고, 그 아이템을 state.sections의 destinationIndexPath의 아이템에 삽입.

     

    TaskListViewController 구조 :

    dataSource - 테이블의 데이터소스
    addButtonItem - +버튼
    tableView - 테이블 뷰

    bind :

    self.rx.viewDidLoad - Reactor.Action.refresh
    self.editButtonItem.rx.tap - Reactor.Action.toggleEditing
    self.tableView.rx.itemSelected - Reactor.Action.toggleTaskDone
    self.tableview.rx.itemDeleted - Reactor.Action.deleteTask
    self.tableView.rx.itemMoved - Reactor.Action.moveTask
    self.addButtonItem.rx.tap - reactor.reactorForCreastingTask (TaskEditViewReactor)
    self.tableView.rx.modelSelected - reactor.reactorForEditingTask

    state :

    reactor.state에 datasource와 setEditing 연결.

     

     

    TaskEditViewReactor 구조 분석 :

    Action :
     updateTaskTitle(String) - Task의 Title을 갱신하는 액션
     cancel - 취소 액션
     submit - 확인 액션 (새로운 task면 self.provider.taskService.create, 원래 있던 task면 self.provider.taskService.update)

    Mutation :
     updateTaskTitle(String) - task의 title을 String으로 바꾸는 명령 전달
     dismiss - 취소 명령 전달

    State :
     title: String
     taskTitle: String
     canSubmit: Bool
     shouldConfirmCancel: Bool
     isDismissed: Bool

    mutate() :
     .updateTaskTitle(taskTitle) - taskTitle을 불러와서 .updateTaskTitle(taskTitle) 전달
     .submit - 저장 후 .dismiss 전달
     .cancel - 취소 후 .dismiss 전달

    reduce() :
     .updateTaskTitle(taskTitle) - 기존 State에서 taskTitle을 새로 설정하고, taskTitle이 비어있다면 canSubmit을 False, 아니면 True, souldConfirmCancel의 값은 taskTitle != self.initialState.taskTitle 값으로.
     .dismiss - state.isDismissed = true

     

    TaskEditViewController 구조 :

    bind:
     cancelButtonItem을 누르면 .cancel
     doneButtonItem을 누르면 .submit
     titleInput의 text는 .updateTaskTitle로

     (reactor.state의) .title은 self.navigationItem의 title로
     .taskTitle은 titleInput의 text로
     .canSubmit은 doneButtonItem의 활성화 상태로
     .isDismissed는 self?.dismiss를 할지 말지 결정

     

    TaskCell, TaskCellReactor 구조 :

    TaskListViewController의 dataSource, 그리고 TaskListViewReactor의 mutate()에서 TaskCell과 TaskCellReactor을 불러오게 된다.
    TaskCell에서는 titleLabel과 accessoryType을 bind시켜주고, 
    TaskCellReactor에는 그냥 init 코드만 있다.

    반응형

    댓글

Designed by Tistory.