관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 3. ReactorKit - `TaskList 구현`, 템플릿 (template), 비동기 처리 transform(mutation:) 본문

Architecture (swift)/ReactorKit

[iOS - swift] 3. ReactorKit - `TaskList 구현`, 템플릿 (template), 비동기 처리 transform(mutation:)

jake-kim 2021. 12. 2. 22:21

1. ReactorKit - 개념

2. ReactorKit - 테스트 방법 (Storyboard 사용, IBOutlet 테스트 방법)

3. ReactorKit - `TaskList 구현`, 템플릿 (template), 비동기 처리 transform(mutation:)

4. ReactorKit - `TaskEdit 구현`, 화면전환, 데이터 전달


ReactorKit 구현 방향

  1. View, Reactor 생성
  2. View의 storyboard에 UI 생성, IBOutlet 입력
  3. Reactor의 Action 정의, Action에 해당하는 Mutation, State 정의
  4. Reactor에서 필요한 service 정의
  5. Reactor의 mutate, reduce 정의

ReactorKit 템플릿

$ ./set_template.sh
  • 코드 생성 "Reactor Template" 검색

"TaskList"라는 이름으로 생성하면 3가지 파일 생성

CheckMark 기능 구현

  • Model 정의
    • Codable 프로토콜 - Task 구조체 인스턴스를 UserDefaults에 저장할때 unarchive, archive하기 위해 선언
    • Identifiable, Equatable 프로토콜 - RxDataSources의 Item에 사용되기 위해 필수로 준수해야하는 프로토콜
    • isSelected 프로퍼티 - checkMark를 표출 여부를 알기 위해 필요한 플래그값
    • TaskListSection 타입에서 사용되는 TaskCellReactor는 아래에서 계속 정의
// Task.swift

import ReactorKit
import Differentiator

typealias TaskListSection = SectionModel<Void, TaskCellReactor>

struct Task: Codable, Identifiable, Equatable {
    var id = UUID().uuidString
    var title: String
    var isSelected: Bool = false
    
    init(title: String) {
        self.title = title
    }
}
  • TaskCellReactor 정의 - 위 Task를 Cell에서 그대로 사용하지 않고, Cell에서는 Reactor로 감싼 Task를 사용
    • reactor.currentState로 task 접근
// TaskCellReactor.swift

class TaskCellReactor: Reactor {
    typealias Action = NoAction
    
    let initialState: Task
    
    init(task: Task) {
        self.initialState = task
    }
}
  • TaskTableViewCell 정의
    • xib도 같이 정의
  • TaskTableViewCell 구현 (RxDataSources를 이용하여 Cell, tableView에 적용 방법은 이곳 참고)
final class TaskTableViewCell: UITableViewCell, StoryboardView {
    
    typealias Reactor = TaskCellReactor
    var disposeBag = DisposeBag()
    
    // MARK: Constants
    
    struct LabelConstant {
        static let maximumNumberOfLines = 5
        static let font = UIFont.systemFont(ofSize: 16)
    }
    
    struct Metric {
        static let cellPadding = 16.0
    }
    
    
    // MARK: UI
    
    @IBOutlet weak var titleLabel: UILabel!
    
    
    // MARK: Binding
    
    func bind(reactor: TaskCellReactor) {
        titleLabel.text = reactor.currentState.title
        accessoryType = reactor.currentState.isSelected ? .checkmark : .none
    }
    
    
    // MARK: Layout
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        setupDynamicLayout()
    }
    
    private func setupDynamicLayout() {
        titleLabel.numberOfLines = LabelConstant.maximumNumberOfLines
        titleLabel.font = LabelConstant.font
        titleLabel.layer.frame.origin.y = Metric.cellPadding
        titleLabel.layer.frame.origin.x = Metric.cellPadding
        titleLabel.layer.frame.size.width = contentView.layer.frame.size.width - Metric.cellPadding * 2
        titleLabel.sizeToFit()
    }
    
    
    // MARK: Cell Height
    
    class func height(width: CGFloat, reactor: Reactor) -> CGFloat {
        let titleState = reactor.currentState.title
        
        let contentWidth = width - Metric.cellPadding * 2
        let heightContents = titleState.getCalculatedHeight(contentWidth: contentWidth,
                                                            font: LabelConstant.font,
                                                            maximumNumberOfLines: LabelConstant.maximumNumberOfLines)
        let heightWithPadding = heightContents + Metric.cellPadding * 2
        return heightWithPadding
    }
    
}
  • Reactor Template을 통해 View, Reactor 생성
  • 비즈니스 로직에서 사용될 TaskService 컴포넌트 생성

  • TaskService 아이디어
    • service 컴포넌트에서 tasks들을 가져오고, 저장하는 등의 로직 수행
    • service 컴포넌트에서 어떤 작업 수행 이후에 UI업데이트가 필요한 경우가 있으므로, 내부에 PublishSubject<내부에서 정의한 타입>으로 따로 두어서 이 타입을 Reactor에서 subscribe하여 mutate 시키는 로직이 존재
    • Reactor에서 servce의 PublishSubject에 관한 구독은, transfrom(mutation:) 메소드에서 수행

ex) View에서 cell 탭 > Reactor에서 service에게 업데이트 요청 > service에서 업데이트 후 내부 PublishSubject에 onNext 발생 > Reactor에서 구독하고 있다가, 오면 mutate를 시킨 후 reduce 실행

 

- 정보를 저장할 UserDefaultsManager 모듈 구현: https://ios-development.tistory.com/702

- TaskService 구현 (TaskEvent는 내부 로직 수행 후 PublishSubject에 방출할 이벤트 타입이고, 구독은 Reactor에서 진행)

enum TaskEvent {
    case checkMark(id: String)
    case checkUnMark(id: String)
}

protocol TaskService {
    var event: PublishSubject<TaskEvent> { get }
    
    func fetchTasks() -> Observable<[Task]>
    func setCheckMark(taskId: String, isCheckNow: Bool) -> Observable<Task>
}

final class TaskServiceImpl: TaskService {
    
    let event = PublishSubject<TaskEvent>()
    
    func fetchTasks() -> Observable<[Task]> {
        if let savedTasks = UserDefaultsManager.tasks {
            return .just(savedTasks)
        }
        let defaultTasks = [Task(title: "iOS 앱"),
                            Task(title: "iOS 앱 개발 알아가기"),
                            Task(title: "Jake의 iOS 앱 개발 알아가기")]
        UserDefaultsManager.tasks = defaultTasks
        return .just(defaultTasks)
    }
    
    func setCheckMark(taskId: String, isCheckNow: Bool) -> Observable<Task> {
        return fetchTasks()
            .flatMap { [weak self] tasks -> Observable<Task> in
                guard let `self` = self else { return .empty() }
                guard let index = tasks.firstIndex(where: { $0.id == taskId }) else { return .empty() }
                var tasks = tasks
                tasks[index].isSelected = !isCheckNow
                return self.saveTasks(tasks).map { tasks[index] }
            }.do(onNext: { [weak self] task in
                if isCheckNow {
                    self?.event.onNext(.checkMark(id: task.id))
                } else {
                    self?.event.onNext(.checkUnMark(id: task.id))
                }
            })
    }
    
    @discardableResult
    private func saveTasks(_ tasks: [Task]) -> Observable<Void> {
        UserDefaultsManager.tasks = tasks
        return .just(Void())
    }
}

- PublishSubject 구독은 Reactor의 transfrom(mutation:)에서 수행 (아래에서 계속)

  • TaskViewController와 TaskListReactor 구현
    • 핵심은 TaskListReactor의 transfrom(mutation:) 부분이고, 이 함수가 존재하기 때문에 mutate 메소드가 두 개인것을 주의
func mutate(action: Action) -> Observable<Mutation> { ... }

/// 원래 있던 mutate()에서 발생한 이벤트와 TaskService().event에 있는 것을 같이 방출하는 메소드
func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
    let taskEventMutation = 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> { ... }

func reduce(state: State, mutation: Mutation) -> State { ... }

다른 방법) transform을 사용해도 되지만, 각 service에 Observable을 반환하게하여 해당 값을 transform없이 바로 사용하면 더욱 간편

 

* 전체 소스 코드: https://github.com/JK0369/ExTaskUsingReactorKit

 

*  참고

- https://github.com/devxoul/RxTodo

Comments