Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- combine
- clean architecture
- 리펙토링
- 애니메이션
- swift documentation
- rxswift
- ios
- Protocol
- 클린 코드
- map
- Clean Code
- SWIFT
- Xcode
- ribs
- RxCocoa
- Human interface guide
- tableView
- HIG
- 리팩토링
- 스위프트
- swiftUI
- MVVM
- UICollectionView
- 리펙터링
- Refactoring
- uitableview
- collectionview
- UITextView
- uiscrollview
- Observable
Archives
- Today
- Total
김종권의 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:212. ReactorKit - 테스트 방법 (Storyboard 사용, IBOutlet 테스트 방법)
3. ReactorKit - `TaskList 구현`, 템플릿 (template), 비동기 처리 transform(mutation:)
4. ReactorKit - `TaskEdit 구현`, 화면전환, 데이터 전달
ReactorKit 구현 방향
- View, Reactor 생성
- View의 storyboard에 UI 생성, IBOutlet 입력
- Reactor의 Action 정의, Action에 해당하는 Mutation, State 정의
- Reactor에서 필요한 service 정의
- Reactor의 mutate, reduce 정의
ReactorKit 템플릿
- 구현 방법은 이곳 참고: https://ios-development.tistory.com/760
- 구현된 내용 - 구현된 코드가 있는 git repository를 clone 후 프로젝트 경로로 terminal 이동하여 쉘 실행
$ ./set_template.sh
- 코드 생성 "Reactor Template" 검색
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도 같이 정의
- 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
* 참고
'Architecture (swift) > ReactorKit' 카테고리의 다른 글
[iOS - swift] 4. ReactorKit - `TaskEdit 구현`, 화면전환, 데이터 전달 (0) | 2021.12.03 |
---|---|
[iOS - swift] 2. ReactorKit - 테스트 방법 (Storyboard 사용, IBOutlet 테스트 방법) (4) | 2021.11.30 |
[iOS - swift] 1. ReactorKit - 개념 (4) | 2021.11.30 |
Comments