# 프로젝트 관리 앱 [STEP2-1] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP1 PR 보냅니다! 요번 프로젝트 잘 부탁드립니다 :)
저희가 찾아서 정리해본 각각의 장단점 들 입니다!
# STEP1 로컬과 리모트(클라우드)의 데이터 동기화 및 로컬 데이터 저장 기술 을 선택하자!
|분류 |기술스택 |
|------ |------------ |
|의존성 관리도구|Swift Package Manager |
|UI |RxCocoa |
|비동기 처리 |RxSwift, RxRelay |
|로컬 DB |Realm |
|원격 DB |FireBase Realtime DataBase|
|App 아키텍쳐 |MVVM |
## 결론
Remote데이터 동기화는 Firebase 를 선택했으며 Local 데이터 DB는 Realm을 선택했습니다!
빠른 개발과 많은 문서들이 존재하는 Firebase를 선택하는 것이 적절하다고 판단합니다. 또한 많은 개발자들이 사용하는 이유가 있을 것이라고 생각하기에 사용해보고 싶은 생각이 듭니다.
Realm은 한번 공부해서 사용해보자 라는 마음이 들지만 Realm은 대용량 데이터를 저장하는데 적합하다고 하여 이번 프로젝트에서 적합한것인지 궁금합니다.
### 로컬과 리모트(클라우드)의 데이터 동기화 및 로컬 데이터 저장 기술의 종류
- SQLite
- 버전 호환성
- iOS 5.0 이상
- 장점:
- iOS 내부에 있으므로 라이브러리 사용할 필요 없다.
- 단점:
- write 경우 테이블이 아닌 DB를 lock를 걸어서 성능이 나빠진다.
- Date Time 같은 필드가 존재하지 않음
- CoreData
- 버전 호환성
- iOS 3.0 이상
- 장점:
- Sqlite는 내장된 라이브러리라서 가볍게 사용하기 좋다.
- CoreData는 기본으로 제공되는 데이터 저장용 프레임워크로, 객체 형태로 데이터를 관리할 수 있다
- 단점:
- XCode를 통해서 Entity를 생성하고, 코드로 데이터를 Read, Write하는 과정이 직관적이지 않고 사용이 불편하다.
- iCloud
- 버전 호환성
- iOS 8.0 이상
- 장점:
- remote 저장소에 저장하여 여러 device에서 접근가능하다.
- 단점:
- 객체 로컬 저장 불가능
- CloudKit 사용하려면 애플 구독 아이디 필요하다(비용 발생)
- Dropbox
클라우드 스토리지 서비스.
원드라이브, 구글 드라이브와 함께 클라우드 스토리지 빅3 중하나.
무료버젼은 용량이적다.
객체 로컬 저장 불가능
- 버전 호환성:
- iOS 13.1 이상
- 장점:
- 안정성(업로드 속도는 많이 빠르진 않지만 누락되는거 없고 에러도 없이 어떻게든 동기화를 다 시켜버린다
- 백업(사용자가 변경하거나 지운 파일 복구기능)
- 실시간 동기화(delay없이 업로드되는 즉시 다른 기기에서 이어서 작업이가능하다.)
- 단점:
- 저장장치 용량을 넘어가는 양은 동기화 불가능 (스마트 동기화 없는 개인 계정인 경우)
- 메모용 플랫폼으로는 적합하지 않음
- Firebase
모바일 서버를 개발하기 위해서는 인증, 데이터베이스, 푸시 알람, 스토리지, API 등 모든 것을 개발해야 한다.
FireBase는 이 모든 플렛폼을 프로젝트 구축 시 자동적으로 만들어 준다.
- 버전 호환성
- Xcode 13.3.1 이상
- iOS 10
https://firebase.google.com/docs/ios/setup?hl=ko
- 장점:
- 서버 구축없이 빠른 개발이 가능하다.
- NoSQl 기반의 3세대 데이터베이스(관계형 DB보다 빠름)
- 단점:
- 종종 서버의 응답속도가 느려진다.(서버가 해외에있음)
- Firebase의 데이터베이스인 FireStore(신버전)나 RealTimeDatabase(구버전) 모두 쿼리가 매우 빈약하다.
- 데이터 검색이 어렵다.(모든데이터를 받아와 앱에서 필터링하여 사용해야 한다.)
- Realm
대용량 DB
- 버전 호환성
- iOS 8 이상
- Xcode 7.3 이상
- Xcode 14 이상 사용시 ios 11이상
- 장점:
- SQLite와 CoreData보다 작업 속도가 빠르다.
- Realm Studio를 통해서 DB 상태를 편하게 확인할 수 있다.
- 직관적인 코드로 작업할 수 있다.
- Rx를 지원하는 RxRealm이 존재한다.
- 높은 용량을 요구하고 보통 대용량 데이터를 다룰 때 사용한다고 한다.
- 단점:
- 바이너리 용량이 늘어남
- main thread 이용하고 있는데 다른 thread 접근하면 에러남. 그래서 사용할 때 스레드 지정해줘야 함
- 다양한 쿼리를 지원하지 않음
- iOS8부터 지원가능
- MongoDB
- 버전 호환성:
- 버전 확인 불가능(찾지못했습니다)
- 장점:
- 다양한 형태의 데이터 저장 가능
- 데이터 모델의 유연한 변화 가능(데이터 모델 변경, 필드 확장 용이)
- Read/Write 성능이 뛰어남
- 많은 데이터 저장이 가능
- 장비 확장이 간단함
- JSON 구조 : 데이터를 직관적으로 이해 가능
- 사용 방법이 쉽고, 개발이 편리함
- 단점:
- 데이터 업데이트 중 장애 발생 시, 데이터 손실 가능
- 많은 인덱스 사용 시, 충분한 메모리 확보 필요
- 데이터 공간 소모가 RDBMS에 비해 많은(비효율적인 Key 중복 입력)
- 복잡한 JOIN 사용시 성능 제약이 따름
# 아래의 고민 포인트를 토대로 고민해봅시다.
## 하위 버전 호환성에는 문제가 없는가?
프로젝트 타겟 버젼은 14.1 이므로 위의 선택 사항모두 최소 13.3.1 이후 부터 사용 가능하다.
## 안정적으로 운용 가능한가?
MongoDB는 데이터 업데이트 중 장애 발생 시 데이터 손실이 가능하므로 안정적이지 못하다고 본다.
DropBox도 저장용량이 넘어가면 동기화를 못하므로 안정적이지 못하다고 본다.
그 외에는 사용 시 안정적으로 운용할 수 있다고 판단한다.
## 미래 지속가능성이 있는가?
Firebase는 현재 가장 많은 사람들이 사용하는 remote 저장 방법이므로 데이터 검증이 되었다고 판단된다. 또한 서버 구축 없이 빠른 개발이 가능하다는 장점이 있다.
Realm는 Rx를 지원한다. 또한 CoreData, SQLite보다 작업 속도가 빠르고 DB의 상태를 편하게 볼 수 있는 장점이 존재한다.
## 리스크를 최소화 할 수 있는가? 알고있는 리스크는 무엇인가?
Realm 사용 시 main thread가 아닌 다른 thread 접근하면 에러가 나는 리스크가 있다.
해당 리스크를 해결하기 위해 Realm 접근및 사용시 main thread에서 작업하도록 지정 해주어야한다.
Firebase는 데이터베이스에 접근할 때 서버 응답 시간이 조금 지연되는 경우 가 발생한다.(서버가 해외에 구축되어 있기 때문이다.)
## 어떤 의존성 관리도구를 사용하여 관리할 수 있는가?
CocoaPods, SwiftPackageManager
## 이 앱의 요구기능에 적절한 선택인가?
빠른 개발과 많은 문서들이 존재하는 Firebase를 선택하는 것이 적절하다고 판단한다. 또한 많은 개발자들이 사용하는 이유가 있을 것이라고 생각하기에 사용해보고 싶은 생각이 든다.
Realm이 CoreData보다 작업 속도가 빠르고 Rx를 지원하기 때문에 CoreData와 얼마나 다른지 확인도 해보고 새로운 것을 사용해보고 싶은 생각이 든다.
모델구현
뷰 구현
안녕하세요 루카스! 저희 PR를 총 3번에 걸쳐서 보낼 예정입니다.
각 PR마다 구현할 기능들에 대한 계획과 예정 날짜를 보내드립니다.
확인해주시고 수정이 필요하다고 생각하시면 수정해보겠습니다!
@에디🍻 안녕하세요~ 저는 혹시 가능하다면 더 작게 나누는 경험을 해보셨으면 좋겠는데요,
예컨대 2-1에서 네비게이션바 구현이랑 할일 리스트 쪽은 구분해도 될 것 같아요.
2-2도 마찬가지 관점에서 각자 다른 기능이라면 더 나누시면 좋을 것 같구요.
2-3도 마찬가지로 Swipe 구현과 Popover, 토스트 등 전부 각기 다른 기능이라서 더 나눌 수 있는 부분이 있다면 작게 만들어 보시면 좋을 것 같아요
위 부분은 어디까지나 제안이기에, 제가 얘기드린 부분 참고해서 어디까지 나눌지 아니면 그대로 가고 싶으신지 편하게 말씀 주세요
# STEP2 진행순서
말씀해주신 피드백 바탕으로 세부적으로 각 성격에 맞게 나누어서 구분해보았습니다.
이 계획대로 진행하려고 하는데 괜찮을까요?
⭐️ 표시는 각 스텝별 제목입니다.
2-1 (화요일)
⭐️앱 초기 세팅
네비게이션바 구현
2-2 (목요일)
⭐️할일 리스트 UI 구성
할일 리스트에서 필요한 모델 구현
할일 리스트 테이블뷰 구현
2-3 (월요일)
⭐️Toast View UI 구성
"+" ButtonTap -> ToastView 보이도록 구현
할일 생성/편집 toast view 구현
toast view shadow 적용
2-4 (화요일)
⭐️Toast View 기능 구현
ToastView에서 편집 기능 구현
ToastView 행동에 따른 할일 리스트 데이터 적용 구현
2-5 (수요일)
⭐️할일 리스트 Swipe 구현
테이블 뷰 Swipe에 따른 셀 삭제 적용
2-6 (수요일)
⭐️Popover 구현
PopOver에 따른 테이블 뷰 셀 이동 구현 및 테이블 뷰 header count 적용
2-7 (목요일)
⭐️localization 구현
데이터피커, 테이블 뷰 date 적용
date 초과 날짜에 따른 색상 적용
안녕하세요 @innocarpe
2-1 PR 보냅니다! 감사합니다
### 초기 세팅
|분류 |기술스택 |
|------ |------------ |
|의존성 관리도구|Swift Package Manager |
|UI |RxCocoa |
|비동기 처리 |RxSwift, RxRelay |
|로컬 DB |Realm |
|원격 DB |FireBase Realtime DataBase|
|App 아키텍쳐 |MVVM |
### 폴더 구조
폴더 구조는 아래와 같습니다.
<img src = "https://i.imgur.com/U1TMwNZ.png" width = "200" height ="217"> <img src= "https://i.imgur.com/m24FCIS.png" width= "265">
폴더구조를 각 화면별로 그룹을 나누어 진행하려고 합니다!
### 네비바
아래와 같이 만들었습니다!
<img src = "https://user-images.githubusercontent.com/52434820/177318994-26572262-d9de-491b-93ea-381cbf4cf5b7.png" width = "250">
## STEP2-2
기능 요구서의 예시 이미지

구현 하려하는 이미지

## 배경
- 프로젝트에 필요한 라이브러리 설치
- UI: RxCocoa
- 비동기 처리: Rxswift, RxRelay
- 로컬 DB: Realm
- 원격 DB: FireBase Realtime DataBase
- TodoList View에서 NavigationBar 구현
- 각 파일 폴더로 구분
## 작업 내용
- AppDelegate, SceneDelegate 주석 제거
- 각 Scene 별로 파일 분리
<img src = "https://i.imgur.com/U1TMwNZ.png" width = "200" height ="217"> <img src= "https://i.imgur.com/m24FCIS.png" width= "265">
- TodoListViewControlle 네비게이션바 구현
네비게이션바에 사용될 title과 button 추가
## 테스트 방법
SPM(Swift Package Manager)으로 설치했으므로 따로 라이브러리 설치가 필요하지 않습니다.
시뮬레이터를 iPAD로 설정해서 실행하시면 됩니다.
## 리뷰 노트
Scene에 따라 폴더 분리를 하면 각 화면에 필요한 요소를 폴더로 관리할 수 있어 좋다고 생각합니다. 이 방식이 괜찮은 방향일까요?
## 영상
TodoListViewController 네비게이션바 구현 영상
<img src= "https://i.imgur.com/o4iSfiz.gif" width= "250">
## 문제 해결을 위한 생각들
1. 셀타입을 3개 만든다면 Todo -> Doing 로 이동시키면 TodoCell이 가지고있는 정보를 가지고 DoingCell 을 만들어 넣어준다.
2. 셀이 상태값을 갖는다. 상태값은 Todo, Doing, Done
## 질문 리스트
1. 도메인 구조로 나누는 것에 대한 기준
2. rx header 임의 설정하는거.. 안나온다 어케하죠?
## RxdataSource
1. 헤더 폰트 설정 못함
2. 헤더 색상 변경 못함
```swift=
struct Todo {
let title: String
let description: String
let date: String
}
extension Todo: IdentifiableType, Equatable {
typealias Identity = String
var identity: String {
return UUID().uuidString
}
}
final class TodoListViewModel {
let tableViewData: BehaviorSubject<[TodoListSection]>?
let mockData = [
Todo(title: "책상정리", description: "집중이 안되요", date: "2021.03.06"),
Todo(title: "고구마", description: "감자", date: "오늘")
]
init() {
self.tableViewData = BehaviorSubject(value: [TodoListSection(header: "헤더!", items: mockData)])
}
}
struct TodoListSection {
var header: String
var items: [Todo]
}
extension TodoListSection: AnimatableSectionModelType {
typealias Identity = String
init(original: TodoListSection, items: [Todo]) {
self = original
self.items = items
}
var identity: String {
return header
}
}
typealias TodoListSectionDataSource = RxTableViewSectionedReloadDataSource<TodoListSection>
private func bind() {
//데이터소스 초기화
let dataSource = TodoListSectionDataSource(
configureCell: { dataSource,
tableView,
indexPath,
item in
guard let cell = tableView.dequeueReusableCell(
withIdentifier: TodoListCell.identifier,
for: indexPath
) as? TodoListCell else {
return UITableViewCell()
}
cell.configure(item)
return cell
})
dataSource.titleForHeaderInSection = { dataSource, index in
return dataSource.sectionModels[index].header
}
//테이블뷰 바인딩 작업
self.viewModel.tableViewData?
.bind(to: todoView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
```
# 프로젝트 관리 앱 [STEP2-2] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP2-2 PR 보냅니다!
## 배경
- 테이블 뷰 UI 구현
- 각 셀에는 [제목 / 설명 / 기한]이 표시됩니다
- 제목은 한 줄이며, 길면 잘라서 마지막 부분을 …로 표시합니다
- 설명이 세 줄 이상이면 세 줄 까지만 표시합니다
- 설명이 세 줄 이하라면, 설명글의 높이에 맞게 셀의 높이가 맞춰집니다
## 작업 내용
**3개의 테이블뷰(TODO, DOING, DONE) UI 구현합니다.**
- Model에 mode, identifier, title, description, date 프로퍼티를 담고 있는 Todo 타입 구현
- TodoList 에 titleLabel, descriptionLabel, dateLabel 프로퍼티를 담고 있는 TodoListCell 타입 구현
- TodoList 에 mode, tabelView, viewModl, disposeBag 프로퍼티를 담고 있는 ListView 타입 구현
- TodoList 에 tabelViewData 프로퍼티를 담고 있는 TodoListViewModel 타입 구현
- TodoList 에 todoView, doingView, doneView 프로퍼티를 TodoListViewController 타입 구현
**각 셀에는 [제목 / 설명 / 기한]이 표시됩니다**
- titleLabel, descriptionLabel, dateLabel를 contentsStackView에 넣어서 관리 했습니다.
```swift
private let contentsStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.alignment = .fill
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .title3)
return label
}()
private let descriptionLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 3
label.font = .preferredFont(forTextStyle: .body)
label.textColor = .systemGray
label.lineBreakMode = .byTruncatingTail
return label
}()
private let dateLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .body)
return label
}()
```
**설명이 세 줄 이상이면 세 줄 까지만 표시합니다**
**설명이 세 줄 이하라면, 설명글의 높이에 맞게 셀의 높이가 맞춰집니다**
- descriptionLabel에 lineBreakMode를 사용하여 3줄이 넘어가면 끝 부분으로 자르도록 했습니다.
```swift
private let descriptionLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 3
label.font = .preferredFont(forTextStyle: .body)
label.textColor = .systemGray
label.lineBreakMode = .byTruncatingTail
return label
}()
```
**각 셀 사이의 inset separator를 통해 구분선 만들기**
기능 요구서의 사진과 동일하게 만들기 위해 Cell 내부에 layoutSubviews() 메서드를 override 하여 contentView.fram을 inset을 추가한 크기로 재설정 하도록 하였습니다.
```swift
override func layoutSubviews() {
super.layoutSubviews()
self.contentView.frame = self.contentView.frame.inset(by: UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0))
}
```
## 테스트 방법
- iPAD 시뮬레이터를 실행하면 TodoList TableView를 볼 수 있습니다.
## 리뷰 노트
- 처음 HeaderView 부분을 Rxswift에서 TableView의 header 관련한 Delegate method를 제공하지 않을까 생각하고 찾아보았습니다. 하지만 찾아보았을 떄, 관련 메서드가 없었고 단순히 Header에 값을 뿌려주는 방식만 찾게 되었습니다. 그리고 UIKit의 Delegate를 사용하는 예시들만 보았습니다. 그래서 결국 tableView에 header를 추가 하는 방식이 아닌 View를 추가 하는 방식을 채택하게 되었는데, Rxswift에는 TableView의 header의 색깔, 사이즈를 조절하는 Delegate method가 없는 것일까요?
- UI 구현 후 테이블 뷰 셀 사이의 구분선을 만들기 위해 inset를 주었습니다.
하지만 height가 애매하는 레이아웃 이슈가 존재해서 cell에 요소를 감싸는 stackView의 distribution를 fill에서 .fillProportionally 로 바꾸며 해결했습니다.
레이아웃 이슈는 해결되었지만 일부 tableView cell이 잘리는 현상이 발생되었습니다.
모든 셀이 아니라 일부 셀만 적용되는데 그 이유가 무엇일까요?
- 처음 설계에서 ListView에서 UIViewController를 상속받아서 이곳에서 TableView Delegate method를 해결해주려고 했습니다. 예를 들어, 테이블 뷰의 셀을 터치하는 등의 동작 처리가 있습니다.
하지만 이보다 더 상위뷰인 TodoListViewController에서 모든 뷰의 이벤트 처리를 하는 것이 더 옳다고 판단해서 ListView가 UIview를 상속하도록 바꾸었습니다. 괜찮은 설계 방향일까요?
**Cell의 크기가 깨지는 이미지**

**View hierarchy에서 보았을 때의 height Layout issus**

## 영상
<img src= "https://i.imgur.com/D15l7hM.gif" width= "700">
안녕하세요 루카스!
저희가 PR 보내고 추가 작업을 해서 커밋을 더 올렸습니다!
작업한 내용은 Mode에 따라 filter를 통해 Cell에 보여지도록 구현했습니다.
또한 HeaderView에 CountLabel에도 이 숫자가 적용되도록 했습니다.
추가 확인해주시면 감사하겠습니다😄
<img src = "https://i.imgur.com/rcNfCf9.gif" width="700">
1. MockData 가져오는거 프로토콜 사용해서 read, save, delete, update 할수있도록 변경하고싶음
2. 테이블뷰 디퍼블? 나중에 쓸수 있으면 하면 좋겠다.
# 프로젝트 관리 앱 [STEP2-3] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP2-3 PR 보냅니다!🌟
## 배경
- 프로젝트 할일 리스트 UI
- 할일 추가 기능
- 오른쪽 위의 + 버튼을 선택하면 새로운 할일을 추가하는 작성양식이 표시됩니다
- 작성한 할일은 [할일(TODO)] 영역에 추가합니다
## 작업 내용
**지난 PR에서 반영해야할 부분**
1. Status → TodoListItemStatus로 네이밍 변경했습니다.
2. CurrentDateFormatter -> DateFormatter가 내부 라이브러리로 존재하여 Formatter로 변경했습니다.
3. UIEdgeInset, Date, StackView + Extension -> 폴더명 + Sugar를 사용하여 명확한 목적 갖는 네이밍으로 변경했습니다.
4. TodoListViewModel+Fixture 프로젝트 파일 및 파일들 -> TodoListViewModel+Temp로 임시로 사용할 프로젝트 파일로 네이밍 변경했습니다.
5. ViewModel 기능 추가에 대한 역할 이야기 -> ListViewModel는 화면에 보여주기 위해 dataBase로 부터 가져온 데이터를 각각의 테이블뷰에 맞게 filter를 통해 넣어주는 역할을 합니다.(추후 편집기능도 추가할 예정입니다)
DetailViewModel는 현재 새 할일 목록을 생성하기 위해 donButton이 눌렀을 때 이벤트를 감지해서 save가 되도록 하는 역할을 합니다.
6. MockDatabase 네이밍 -> 임시로 사용할 것이기에 TempDataBase로 네이밍 변경했습니다.
**STEP 2-3 작업 부분**
1. Coordinator를 활용한 화면 전환
2. ToastView UI 구현
3. ToastView에서 할일 생성 기능 구현
4. Cell 터치시 ToastView 보이도록 구현(누른 Cell 정보 보이는 것은 미구현 - 편집기능)
**STEP2-2 PR 이후 반영하지 못한 부분**
1. header
2. Inset
Inset관련 부분은 리뷰 노트에 질문사항을 작성하도록 하였습니다!
header부분은 아직 해결하지 못하여 조금 더 찾아보도록 하겠습니다!
## 테스트 방법
IPAD 시뮬레이터를 실행한 후 '+' 버튼을 누르면 Toast View를 볼 수 있습니다.
이곳에서 Title, Date, Description를 입력 후 Done를 누르면 TODO 테이블뷰 셀에 만들어지는 것을 볼 수 있습니다.
## 리뷰 노트
**1. Inset 관련 질문**
Inset을 추가함으로써 Cell의 높이가 컨텐츠를 표시하기에 충분하지 않게되는 문제가 발생합니다! 해당 문제를 해결하기 위해 Cell의 높이(height)를 최소 값을 주기위해 tabelView.rowHeigh, tableView.estimatedRowHeight를 사용해 해결해보려 했으나 실패했습니다..!
```swift
table.rowHeight = 200
table.estimatedRowHeight = UITableViewAutomaticDimension
```
그래서 Inset을 추가하는 방법말고 border를 추가하는 방법을 생각해보았습니다!
편법이라고 생각이 들기는 하지만..Cell 컨텐츠뷰에 border를 추가하는 방법으로 Cell 간의 간격을 표시하는 방법을 생각해 보았는데 이런방법으로도 보통 사용하는지 궁금합니다..!
```swift
extension CALayer {
func addTopBorder() {
let border = CALayer()
border.frame = CGRect.init(x: frame.height, y: .zero, width: frame.width, height: 5)
border.backgroundColor = UIColor.systemGray6.cgColor
self.addSublayer(border)
}
}
```
Cell 의 contentsView border의 left, right spacing 부분을 수정해야 하지만 아래의 사진과 같이 Cell간의 간격을 표시해줄수 있게 됩니다..!

**2. UIView, UIViewController 상속문제**
화면전환을 ListView에서 시켜주어야 할지 TodoListViewController에서 시켜주어야할지 고민했던 부분입니다!
ListView가 UIView를 상속받아야 하는지 UIViewController 를 상속받아야 하는지에 대한 고민은 Coordinator 패턴을 적용함으로써 UIView를 상속받도록 하였습니다.
화면전환을 담당하는 역할을 구현함으로써 ListView와 TodoListViewController는 화면전환에 필요한 중복코드를 해결할수 있으며 ListView가 화면전환 기능이 필요없다면 UIViewController를 상속받아야 할 이유가 사라졌다고 판단했습니다.
Coordinator 패턴을 사용함으로써 ViewController에 필요한 의존성을 주입할수 있도록 수정하였습니다.
코디네이터 패턴을 처음 적용해보았는데 View의 갯수가 많지 않기 때문에 childCoordinator는 구현하지 않았습니다! 처음 적용해본것이기 때문에 수정할부분은있는지 어떤 부분을 개선하면 좋을지 조언을 부탁드립니다!
**3. TempDataBase 강제 언래핑**
TempDataBase를 통해 데이터가 잘 들어가는지 확인하기 위해 Temp 데이터를 넣었습니다.
Todo를 만드는 과정에서 강제언래핑을 활용해서 했는데, 저희는 데이터가 들어있는 것을 확인하고 그것이 있는 것을 이미 알고 있으므로 강제 언래핑을 썼는데 이부분에서는 괜찮을지 궁금합니다.
```swift
let tempTodoData = [
Todo(todoListItemStatus: TodoListItemStatus(rawValue: tempTodoStatus!)!, title: tempTodoTitle!, description: tempTodoDescription!, date: Date()),
Todo(todoListItemStatus: TodoListItemStatus(rawValue: tempDoneStatus!)!, title: tempDoneTitle!, description: tempDoneDescription!, date: Date()),
Todo(todoListItemStatus: TodoListItemStatus(rawValue: tempDoingStatus!)!, title: tempDoingTitle!, description: tempDoingDescription!, date: Date())
]
```
## 영상
<img src= "https://i.imgur.com/RPZN9xm.gif" width= "700">
2-4 (화요일)
⭐️Toast View 기능 구현
ToastView에서 편집 기능 구현
ToastView 행동에 따른 할일 리스트 데이터 적용 구현
2-5 (수요일)
⭐️할일 리스트 Swipe 구현
테이블 뷰 Swipe에 따른 셀 삭제 적용
# 프로젝트 관리 앱 [STEP2-4] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP2-4, 2-5 PR 보냅니다!🌟
## 배경
- ToastView에서 편집 기능 구현
- ToastView 행동에 따른 할일 리스트 데이터 적용 구현
- 테이블 뷰 Swipe에 따른 셀 삭제 적용
## 작업 내용
- ToastView 편집, 생성 기능에 따라 버튼이 다르게 보이도록 구현
- ToastView 편집 시 Edit 눌러야만 편집 가능하도록 구현
- ToastView 생성 시 버튼이 바뀌지 않도록 구현
- ToastView Cancel 버튼 시 update없이 dismiss만 되도록 구현
- 테이블 뷰 Swipe시 셀 삭제하도록 구현
## 테스트 방법
- iPAD 시뮬레이터를 실행한후 TodoListView의 Cell을 tap하여 DetailView의 Edit버튼을 tap 하면 수정 기능을 사용할수 있습니다.
- iPAD 시뮬레이터를 실행하면 TodoListView의 TableView Cell Swipe Gesture기능을 사용할수 있습니다.
## 리뷰 노트
### 삼항연산자
이번 PR 내용에는 DetailViewController 에서 삼항 연산자를 활용한 부분이 여럿 존재합니다.
```swift
private let selectedData: Todo?
self.selectedData == nil ? true : false
```
위의 코드의 selectedData프로퍼티는 Cell을 tap하여 DetailView가 띄워졌는지, TodoListVIew에서 우측상단의 + 버튼을 tap하여 DetailView가 띄워졌는지를 구분합니다.
selectedData프로퍼티가 nil이 아니라면 선택한 Cell의 data를 갖는 Todo타입을 소유하게됩니다.
DetailViewController를 Cell을 Edit할때와 Create할때 모두 사용하기 때문에 구분할수 있게 구현해주었습니다.
그렇다 보니 selectedData 프로퍼티를 기준으로 navigation의 title, Button의 title 혹은 datePicker, textField, textView 등등 속성이 다르게 적용되야 하기 때문에 삼항 연산자를 활용해 처리하였는데 괜찮은 방법인지 더좋은 방법이 있는지 조언을 받고 싶습니다!
```swift
textField.isEnabled = self.selectedData == nil ? true : false
datePicker.isEnabled = self.selectedData == nil ? true : false
textView.isEditable = self.selectedData == nil ? true : false
title: self.selectedData == nil ? "Cancle" : "Edit",
```
### Domain단위 폴더 구분

위의 스크린샷 방식으로 분리를 했습니다.
도메인 방식으로 폴더를 분리하는 것이 이와 같은 방식일까요?
## 영상
**DetailView Edit기능 추가**

**TableView Cell Swipe Gesture 제거 기능 추가**

# PR
네임스페이스 고민!!!!!!!!!!!물어보기, 내부외부
### 2번쨰 README.md 빠지게된 내용들
### TodoList 생성, 편집, 삭제 기능
TodoList 생성과 편집은 DetailView에서 todoData의 유무로 판단하여 각 기능이 동작하도록 했다.
동일한 화면에서 하나의 분기로 해결해줄 수 있고 내부 로직도 비슷하므로 한번에 해결해줄 수 있다.
doneButton 눌렀을 시 todoData 유무에 따라 데이터베이스에서 TodoListData를 생성하거나 업데이트한다.
```swift
// DetailViewModel.swift
func doneButtonTapEvent(
todo: Todo?,
selectedData: Todo? = nil,
completion: @escaping () -> Void
) {
guard let todo = todo else {
return
}
if let _ = selectedData {
self.dataBase.update(todo: todo)
completion()
} else {
self.dataBase.create(todoListData: [todo])
completion()
}
}
```
Done버튼을 눌렀을 시 todoData를 생성해주고 coordinator를 활용해서 dismiss해주도록 한다
왼쪽 버튼 눌렀을 때 todoData가 없다면 dismiss해주고 todoData가 있다면 편집기능모드로 바꿔준다
```swift
// DetailViewController.swift
private func bind() {
rightBarButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.detailViewModel.doneButtonTapEvent(
todo: self?.createTodo(),
selectedTodo: self?.selectedTodo,
completion: { [weak self] in
self?.coordinator?.dismiss()
})
})
.disposed(by: self.disposeBag)
leftBarButton.rx.tap
.subscribe(onNext: { [weak self] in
if self?.selectedTodo == nil {
self?.coordinator?.dismiss()
} else {
self?.changeAttribute()
}
})
.disposed(by: self.disposeBag)
}
```
### Popover를 통해 TodoList Status 변경
longPressGesture를 감지하고 TableView에서 어떤 Cell인지 감지할 수 있는 로직을 구현하여
TableView에서 이를 사용하도록 한다.
```swift
// Reactive+Sugar.swift
extension Reactive where Base: UITableView {
func modelLongPressed<T>(_ modelType: T.Type) -> ControlEvent<(UITableViewCell, T)> {
let longPressGesture: UILongPressGestureRecognizer = {
let gesture = UILongPressGestureRecognizer(target: nil, action: nil)
gesture.minimumPressDuration = 0.5
return gesture
}()
base.addGestureRecognizer(longPressGesture)
let source = longPressGesture.rx.event
.filter { $0.state == .began }
.map { base.indexPathForRow(at: $0.location(in: base)) }
.flatMap { [weak tableView = base as UITableView] indexPath -> Observable<(UITableViewCell, T)> in
guard let tableView = tableView,
let indexPath = indexPath,
let cell = tableView.cellForRow(at: indexPath) else { return Observable.empty() }
return Observable.zip(
Observable.just(cell),
Observable.just(try tableView.rx.model(at: indexPath))
)
}
return ControlEvent(events: source)
}
}
```
TableView에서 longPressGesture가 있을 시 선택한 것 외 다른 2개의 status를 찾아서 Popover에 표현하도록 구현한다
```swift
// ListView.swift
self.tableView.rx.modelLongPressed(Todo.self)
.subscribe(onNext: { [weak self] selectedCell, selectedData in
guard let (firstStatus, secondStatus) = self?.listViewModel.distinguishMenuType(of: selectedData) else {
return
}
self?.coordinator?.showPopOver(
sourceView: selectedCell,
firstTitle: "Move To \(firstStatus.value)",
secondTitle: "Move To \(secondStatus.value)",
firstAction: {
self?.listViewModel.moveDifferentSection(to: firstStatus, selectedCell: selectedData)
},
secondAction: {
self?.listViewModel.moveDifferentSection(to: secondStatus, selectedCell: selectedData)
})
})
.disposed(by: self.disposeBag)
```
Popover에 자신의 status 제외한 나머지를 status를 보여주도록 한다
그리고 Popover에서 선택한 Section으로 status를 바꾸게 되면 변경에 대해 선언형으로 구현했으므로 자동으로 이동되도록 구현한다
```swift
// TodoListViewModel.swift
func distinguishMenuType(of todo: Todo) -> (TodoListItemStatus, TodoListItemStatus) {
switch todo.todoListItemStatus {
case .todo: return (.doing, .done)
case .doing: return (.todo, .done)
case .done: return (.todo, .doing)
}
}
func moveDifferentSection(to: TodoListItemStatus, selectedCell: Todo) {
var selectedTodo = selectedCell
guard let newStatus = TodoListItemStatus(rawValue: to.rawValue) else {
return
}
selectedTodo.todoListItemStatus = newStatus
self.dataBase.update(todo: selectedTodo)
}
```
# 프로젝트 관리 앱 [STEP2-6, STEP2-7] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP2-6, 2-7 PR 보냅니다!🌟
## 배경
- 할일을 길게 터치하여 팝오버 메뉴를 표시합니다
- 팝오버에 표시하는 메뉴는 상황에 따라 달라집니다
- TODO의 할일 : Move to DOING / Move to DONE
- DOING의 할일 : Move to TODO / Move to DONE
- DONE의 할일 : Move to TODO / Move to DOING
- 본인의 판단에 따라 팝오버는 액션시트로 대체할 수 있습니다
- 현재시간 대비 기한이 지날 시 DateLabel이 빨간색으로 변하도록 합니다
## 작업 내용
- longPressGesture시 Popover창이 띄워지며 메뉴에 따라 원하는 Status로 가도록 구현
- 현재 시간 대비 할일의 기한이 지날 시 DateLabel(TODO, DOING Status에서만)의 색깔이 빨간색으로 변하도록 구현
## 테스트 방법
- iPAD 시뮬레이터를 실행한후 TodoListView의 Cell을 longPressGesture시 popover가 뜨며 선택된 메뉴 Status로 이동할 수 있습니다.
- iPAD 시뮬레이터를 실행하고 '+'버튼 누른 후 기한을 현재시간보다 늦추면 DateLabel이 빨간색으로 변하게 됩니다.
## 리뷰 노트
### 네임스페이스
네임스페이스를 사용하면 한곳에 사용하는 하드코딩을 방지할 수 있다는 장점이 있습니다.
이를 전역에서 사용하지 않고 사용처 타입 에서만 접근할수있도록 구현하기는 하지만 네임스페이스가 무엇인지 확인하기 위해 위아래로 옮겨가며 확인해야하는 불편함이 존재할 것이라고 생각합니다.
하지만 팀원 제의에서 enum외부에서(네임스페이스 사용처) 읽고 어떠한 값이 들어있는지 유추할수 있도록 enum 속 enum를 만들어서 네임스페이스 내부에서 어떤것을 사용하는지 알수있게끔 하면 괜찮지 않을까라는 의견이 있었습니다.
이에 대한 예시는 아래와 같습니다.
**예시코드1**
```swift
enum Image {
static let quality = //
}
```
위의 예시코드처럼 사용하게 되면`Image.quality`와 같이 사용하게되는데 이렇게 작성하게되면 어떠한 값이 사용되고있는지 사용처에서는 알수 없기 때문에 확인하기 위해서는 네임스페이스를 확인해야하는 작업이 추가 됩니다.
**예시코드2**
```swift
enum Image {
enum Quality {
static let low = //
static let high = //
}
enum Size {
static let big = //
static let small = //
}
}
```
네임스페이스를 위의 예시코드처럼 작성하게되면 네임스페이스 사용처에서 `Image.Qualiyt.high`이러한 방식으로 사용할수 있게됩니다!
이렇게 작성한다면 정확한 값은 알수없지만 외부(사용처)에서 읽기만 하더라도 어떤값이 들어갔는지 조금은 유추할수 있게되는데 이러한 방식은 외부에서 읽기 전용으로 네임스페이스를 구현하는 느낌이 강해집니다.
팀원과 이야기했을 때 네임스페이스에 대한 사용에 대한 의견이 갈려서 루카스의 의견을 듣고싶어서 말씀드립니다!
네임스페이스는 언제 사용하면 좋을까요? 그리고 어떻게 사용해야 좋을까요!?
## 영상
**등록된 Todo, Doing 의 data가 현재날짜를 초과할경우 dateLabel 색상 변경**
<img src= "https://i.imgur.com/aIJnhZC.gif" width= "700">
**Cell Longpress기능구현, Popover기능구현 및 Cell이동 기능 구현**
<img src= "https://i.imgur.com/2u58U0S.gif" width= "700">
# 프로젝트관리앱II STEP 1 진행순서
1-1 (화요일)
⭐️로컬 디스크 연동
Realm 활용하여 로컬 디스크 연동 및 기능 구현
1-2 (수요일)
⭐️리모트 디스크 연동
Firebase 활용하여 로컬과 리모트 동기화
네트워크 연결안되었을 시 유저에게 보여주는 UI
1-3 (목요일)
⭐️History Popover 구현
# 프로젝트 관리 앱 II [3-1] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP 3-1 PR 보냅니다!🌟
## 배경
- 로컬 디스크 캐시를 구현하여 네트워크에 연결하지 않아도 기존의 데이터를 사용자에게 표시할 수 있습니다
- Realm를 이용하여 데이터를 저장, 삭제, 편집 기능을 구현합니다.
## 작업 내용
- 로컬 디스크인 Realm를 활용해 데이터를 저장, 삭제, 편집 기능 구현
- 데이터를 저장 후 다시 실행하더라도 데이터가 남아있도록 구현
## 테스트 방법
- iPAD 시뮬레이터를 실행해서 '+' 버튼을 눌러 새로운 할일 목록을 만들 수 있습니다.
- 기존의 할일 목록의 cell를 클릭 시 편집 화면에서 편집할 수 있습니다.
- 스와이프를 통해 할일 목록을 삭제할 수 있습니다.
- Long press Button으로 나온 Popover를 통해 할일 목록을 옮길 수 있습니다.
## 리뷰 노트
처음으로 Realm 라이브러리를 공부하여 CRUD를 구현하고 적용해보았는데요!
RealmDatabase타입은 UI에 데이터를 전달하고, 로컬 디스크에 데이터를 저장하게됩니다!
처음 사용하다 보니 개선할부분이 있는지 좀더 좋은 방향이있었을지 조언을 부탁드립니다!
## 영상
**Todo 생성및 수정**
<img src= "https://i.imgur.com/IJKYB6Y.gif" width= "700">
**Todo Popover및 이동 기능 구현**
<img src= "https://i.imgur.com/QK3qaHL.gif" width= "700">
**Todo Swipe Action을 활용해 제거 기능 구현**
<img src= "https://i.imgur.com/pKcpK9J.gif" width= "700">
STEP3 구현 세부 사항
//1. 앱이켜진다. -> Realm의 데이터를 fireBase동기화를 시켜주고
//2. Todo 추가되거나 변경될때 Realm 에 데이터를 저장하고 fireBase에 동기화 시켜준다.
//firebase의 목적은 백업및 추후 공유 기능?
# 프로젝트 관리 앱 II [3-2] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP 3-2 PR 보냅니다!🌟
잘부탁드립니다! 😁
## 배경
- 리모트 디스크를 구현하여 로컬 디스크에 저장된 데이터를 리모트 디스크에 동기화 합니다.
- Firebase를 이용하여 데이터를 저장, 삭제, 편집, 복구 기능을 구현합니다.
- 리모트 디스크의 연결상태를 유저에게 알려줄수있는 UI를 구현합니다.
## 작업 내용
- 앱을 실행할경우 로컬디스크에 저장된 데이터를 리모트 디스크와 동기화 기능 수행 하도록 구현
- 로컬 디스크에 데이터를 저장, 삭제, 편집 할경우 리모트 디스크도 동일한 작업을 수행하도록 구현
- 유저가 리모트 디스크와 연결되어있는지 유무를 알수있도록 UI 이미지를 구현
## 테스트 방법
- iPAD 시뮬레이터를 실행하면 로컬 리모트 에 저장된 데이터들이 리모트 디스크 Firebase 웹에서 데이터가 추가되었는지 확인할 수 있습니다.
- iPAD 시뮬레이터에서 로컬 디스크에 데이터를 저장, 삭제, 편집 기능을 사용할경우 Firebase 웹에서 데이터가 저장및 수정 되었는지 확인할 수 있습니다.
- iPAD 시뮬레이터를 실행하면 우측 상단에 와이파이 이미지를 통해 유저가 리모트 디스크에 연결되어있는지 유무를 확인할 수 있습니다.
## 리뷰 노트
### 로컬 디스크와 리모트 디스크의 동기화 방법
로컬 디스크와 리모트 디스크의 동기화 기능을 수행하는 시점은 다음과 같습니다!
- 앱 처음 실행 시 리모트 디스크와 동기화하도록 합니다.
- 앱 실행 중에 로컬 디스크 CRUD 기능 동작 시 리모트 디스크와 실시간 동기화하도록 합니다.
리모트 디스크가 로컬 디스크와 동기화 기능을 수행하기위해 리모트 디스크에도 CRUD 기능을 구현하였는데요 유저가 로컬 디스크에 데이터를 저장, 수정, 삭제 기능을 사용하게 되면 로컬 디스크에 데이터를 저장한후 로컬 리모트에서 해당 데이터를 저장, 수정, 삭제 기능을 수행하게 됩니다.
아래의 코드와 같은 방식으로 동기화 기능을 수행하게 됩니다!
```swfit
func create(todoData: Todo) {
self.realm.create(todoData: todoData) { todoData in
self.firebase.create(todoData: todoData)
}
self.todoListBehaviorRelay.accept(self.todoListBehaviorRelay.value + [todoData])
}
```
위의 방식대로 리모트 디스크에 새로운 값을 추가하게되는데 이러한 방법을 동기화 작업을 구현했다고 표현할수 있는지 궁금합니다.
그저 로컬 디스크에 데이터를 저장하고 리모트 디스크에도 같은 값을 저장한것이라고 표현해야할까요!?
위의 방식이 동기화가 아니라고 한다면 리모트 디스크는 로컬디스크의 값이 변경되는지 감지하여 변경된다면 리모트 디스크에 값을 추가, 제거, 수정 하도록 하는것이 동기화 작업이라고 표현할수 있는걸까요~?
일반적으로 어떠한 방식으로 로컬 디스크와 리모트 디스크를 동기화 하는지 궁금합니다!
## 영상
**Local에서 할일 목록 생성 시 Firebase에서도 생성 동기화**
<img src ="https://i.imgur.com/vxzECBZ.gif" width ="700">
**Local에서 할일 목록 업데이트 시 Firebase에서도 업데이트 동기화**
<img src ="https://i.imgur.com/tK3atw7.gif" width ="700">
**Local에서 할일 목록 삭제 시 Firebase에서도 삭제 동기화**
<img src ="https://i.imgur.com/SboR0rR.gif" width ="700">
**네트워크 연결 유무에 따라 Wifi 모양 변경**
<img src ="https://i.imgur.com/Qi6eIhJ.gif" width ="700">
RealmDatabase 의 DB 변경감지 메서드 입니다.
TodoDTO 모델을 observe한 NotificationToken을 Realm에서 제공하고 있습니다.
이 Token를 통해 CRUD를 모두 감지하게 되어서 실제 switch문에서 변화가 감지되면 각 case 구문을 실행하는 것을 확인할 수 있었습니다.(각 case로 분기처리되어있는 곳이 break point로 멈추는것을 확인했습니다.)
그래서 저희 생각은 변경이 감지되면 RealmDB에 저장된 데이터를 completion 클로져를 사용해 외부로 전달하여 dataSync 메서드를 호출하는 DatabaseManager에서 firebase 와 동기화 하도록 로직을 구현하면 되겠다고 생각했습니다.
```swift
//RealmDatabase
func dataSync(completion: @escaping (Results<TodoDTO>) -> Void) {
self.token = realm?.objects(TodoDTO.self).observe { changes in
switch changes {
case let .initial(todoList):
completion(todoList)
case let .update(todoList, deletions: _, insertions: _, modifications: _):
completion(todoList)
case .error(_):
return
}
}
}
```
dataSync메서드는 RealmDB의 상위 레이어인 DatabaseManager가 초기화될 시점에 호출하게됩니다.
firebase와 동기화 작업을 하기위해서는 [Todo] 타입으로 데이터를 전달해주어야합니다.
completion 으로 전달할 todoList 타입이 `Results<TodoDTO>` 이기 때문에 저희는 [Todo] 배열로 변경하는 과정이 필요했습니다.
firebase와 동기화 작업을 하기위해서는 sync()메서드를 호출하게 되는데 인자값으로 [Todo] 타입을 전달해주어야합니다.
```swift
//FirebaseDatabase
private let firebase: DatabaseReference?
func sync(todoData: [Todo]) {
todoData.forEach {
let todoListReference = self.firebase?.child("TodoList/\($0.identifier.uuidString)")
todoListReference?.setValue($0.dictionary)
}
}
```
dataSync에서 completion으로 받아온 todoList를 map를 통해 `Result<TodoDTO>` 내부로 접근해서 convertTodo() method를 호출하면 변환과 원하는 값을 얻을 수 있다고 생각했습니다.
```swift
todoList.map { $0.convertTodo() }
```
하지만 반환 값이 `lazyMapSequence<Result<TodoDTO>, Todo>` 와 같이 나와서 Todo 배열로 되지 않는 것을 확인했습니다.
그래서 todoArray 배열을 만들어서 todoList를 반복문을 돌리며 값을 넣어주니 원하는 [Todo] 배열로 들어갈 수 있었습니다.
이를 활용해 만들어둔 sync method를 호출해서 동기화를 진행하고 화면에도 적용시키는 로직을 구현했습니다.
```swift
final class DatabaseManager: DatabaseManagerProtocol {
var todoListBehaviorRelay = BehaviorRelay<[Todo]>(value: [])
private let realm = RealmDatabase()
private let firebase = FirebaseDatabase()
init() {
self.realm.dataSync { [weak self] todoList in
var todoArray: [Todo] = []
todoList.forEach { todoArray.append($0.convertTodo()) }
self?.firebase.sync(todoData: todoArray)
self?.todoListBehaviorRelay.accept(todoArray)
}
}
}
```
init부분을 보게되면 dataSync의 completion 클로져 구문을 위와같이 작성해주었습니다!
하지만 delete 부분만 동기화 작동이 되지 않고 있습니다.
저희의 추측으로는 Firebase에게 전달될 RealmDB 데이터, todoList에는 RealmDB에서 제거되고 남겨진 모든 데이터를 Firebase에 전달하게 됩니다.
Firebase가 동기화 작업을하는 방식은 전달된 데이터를 Firebase에 저장된 데이터와 비교하여 추가된것이 있는지, 변경된것이 있는지를 확인하여 동기화 하게됩니다.
Firebase의 동기화 과정은 Realm의 모든 데이터를 기반으로 덮어씌우며 동기화하는 것이 아니라 Realm의 Data기반으로 업데이트가 되었는지 확인 후 Realm의 Data에 대해서만 변경을 감지하는 것 같습니다.
그래서 만약 Realm에서 1,2,3 데이터가 있고 firebase에서도 1,2,3이 있는 상황에서 Realm에서 3을 제거하고 동기화를 하게되면 Realm에 남아있는 1,2 를 firebase에게 전달되어 1,2를 기반으로 업데이트 유무를 확인하는 것 같습니다.
그리하여 firebase에서는 3이 사라졌는지 여부를 파악하지 못하고 있어서 동기화를 하지 않습니다.
그러므로 Delete기능이 원활하게 동작하지 않는 것으로 추측됩니다.
그래서 저희가 생각한 방안은 아래 3가지가 있습니다.
1. firebase가 동기화 작업을 할때 firebase가 가지고있던 데이터를 모두 제거한후 RealmDB 에서 전달받은 데이터를 동기화 하도록 하는 방법입니다.
2. Realm에서 데이터를 지울 때 동시에 firebase에서도 지워주는 방식이 있습니다. 이는 동기화 방식이 아니라 같은 곳에서 realm.delete method와 firebase.delete 메서드를 호출하는 것입니다.
3. Realm의 dataSync메서드는 데이터가 제거되었을때도 감지하고 있기 때문에 제거되는 데이터 정보를 escaping closure 로 전달해주어 firebase가 delete기능을 수행하도록 하는 방법 입니다.
- 위의 방법으로 구현하게되면 dataSync 는 총 2개의 escaping closure를 가지게 됩니다.
현재 생각나는 방식은 3가지가 있습니다.
1번과 2번 방식은 delete를 동기화로 처리하는 느낌이 들지 않습니다. delete를 동기화하는 것처럼 보이게 만드는 방식같습니다.
3번 같은 경우에는 2개의 completion이 존재합니다. 1곳은 제거할 데이터에 대한 completion, 다른 곳은 제거할 데이터를 제외한 모든 데이터의 completion 입니다.
이렇게 따로 구분해서 보낸 후 sync 메서드 내에서 firebase.delete 메서드를 호출해야 합니다. 그래도 sync 메서드 내부에서 지워야할 것과 남겨두어야 할 것으로 구분될 수 있어서 동기화느낌이 나는 것 같습니다.
3번 방식을 선택할경우 아래와같은 모습으로 사용될것 같습니다.
```swift
init() {
self.realm.dataSync { [weak self] todoList in
var todoArray: [Todo] = []
todoList.forEach { todoArray.append($0.convertTodo()) }
self?.firebase.sync(todoData: todoArray)
} { [weak self] deleteData in
self?.firebase.delete(todoID: deleteData.identifier)
}
}
```
그리하여 3가지 방식을 고안했지만 결정하지 못하고 있습니다. 그래서 루카스의 의견을 듣고싶습니다!
Firebase에 전달된 RealmDB데이터, todoList는 제거해야할 정보를 전달하는것이아닌 제거되고 남은 데이터를 Firebase에게 전달하는것 이기 때문에 Firebase는 전달된 데이터에 포함된 데이터 만을 자신의 DB와 비교하게 됩니다.
RealmDB에서 전달된 데이터를 Firebase DB에 덮어씌우는 방식이 아니기 때문에 이러한 문제가 있는것으로 추측됩니다.
동기화 시점
렘[1, 2, 4] 파이어베이스는 [1, 2, 3, 4]
# 프로젝트 관리 앱 II [3-3] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP 3-3 PR 보냅니다!🌟
잘부탁드립니다! 😁
## 배경
- 화면 왼쪽 위의 ‘History’ 버튼을 선택하면 변경 내역을 팝오버로 볼 수 있습니다
- 추가/이동/삭제
## 작업 내용
- 할일 목록을 추가, 이동, 삭제, 편집 했을 시 History popover에서 내역이 보여지도록 구현
- popover에서 할일 목록을 최신 순으로 보여지도록 구현
- popover에서 DateLabel에 Time도 보이도록 변경
## 테스트 방법
- 할일 목록을 추가, 이동, 삭제, 편집한 후 History 버튼을 눌렀을 시 popover로 내역을 볼 수 있습니다.
## 리뷰 노트
### DateLabel 표시하는 메서드의 범용성과 사용성 관점
저희 기존 코드에서는 Todo타입이 Date타입을 프로퍼티로 보유하고 있습니다.
이는 언제까지 해야할일(Todo)의 마감기한을 나타냅니다.
그리고 Todo의 Date타입을 화면에 보여주기위해 String값으로 변환하여 화면에 보여주게 됩니다.
이번 PR에는 History popover기능을 구현했는데 기존에는 timeStyle은 none으로 표기하였으나 History popoverView 에서는 기능 요구서상 시간을 표기하여야 했습니다.
그래서 이를 구현해야 할지에 대해 의견이 달라서 질문을 드릴려고 합니다.
저희는 메서드를 하나로 통합하는 범용성있게 사용하는 것과 사용처에서 제한을 주어야 한다는 사용성 관점의 차이가 존재했습니다.
#### 범용성 관점
먼저 범용성 있게 사용하자 라는 관점으로 보게되면 하나의 함수로 인자값을 받아 여러곳에서 사용할수 있게되는 코드를 구현하게 됩니다.
중복 코드를 줄이고 비슷한 기능을 하나의 메서드로 동작하도록 할수 있다는 것이 장점이라고 생각합니다.
아래의 코드는 기존 Date타입을 화면에 보여주기위해 String 타입으로 변경하는 메서드 입니다.
```swift
extension Date {
func convertToString() -> String {
guard let dateString = Formatter.date.string(for: self) else { return "" }
return dateString
}
}
```
앞서 말씀드린대로 History Popover기능을 구현하게되어 Popover View에서는 시간을 표기해주어야 하는 상황이 생겼고 이를 해결하기위해 다음과 같이 시간을 표기할 것인지 말 것인지를 인자값으로 받아 사용처에서 선택할수 있도록 하는 메서드를 구현하였습니다.
```swift
func convertToString(isMarkTheTime: Bool = false) -> String {
Formatter.date.timeStyle = isMarkTheTime == false ? .none : .medium
guard let dateString = Formatter.date.string(for: self) else { return "" }
return dateString
}
```
위와 같이 구현할경우 convertToString(isMarkTheTime:) 함수 하나로 여러곳에서 사용할수 있게됩니다.
#### 사용성 관점
사용성 관점에서는 유저에게 파라미터로 선택권을 주어야 할 필요가 있냐였습니다.
하나의 범용적 메서드를 활용하면 중복코드를 줄일 수 있는 장점이 존재합니다.
하지만 현재 작성한 방식으로 한다면 파라미터로 다른 사람의 입력을 받아야 하는데 먼저 그 부분이 과연 사용처에서 결정해야할 것인지에 의문이 들었습니다.
또한 이로인해 휴먼오류를 발생할 수 있다고 생각했습니다.
그래서 나온 코드는 2가지 메서드로 아래와 같습니다.
```swift
func convertToString() -> String {
guard let dateString = Formatter.date.string(for: self) else { return "" }
return dateString
}
func convertToHistoryString() -> String {
Formatter.date.timeStyle = .medium
guard let dateString = Formatter.date.string(for: self) else { return "" }
return dateString
}
```
저의 관점에서는 범용성보다는 사용자에게 선택을 주지 않고 제한된 호출이 적절한거 같다고 판단했습니다.
결론적으로 두 관점이 각각 일리가 있어서 현재 선택을 하지 못하고 2가지 코드를 남겨두었습니다.
이처럼 범용성과 사용성을 고려해야할 때 어떤 것을 더 중점적으로 보고 선택해야할까요?
또한 현재같은 상황에서 각각 주장이 확실해서 결정이 어려운데 만약 현업에서 이런 상황이 발생하면 어떻게 소통하여 해결할 수 있을까요?
## 영상
**Todo 생성시 History 내역 추가 기능**
<img src ="https://i.imgur.com/pQMi7sa.gif" width ="700">
**Todo 이동시 History 내역 추가 기능**
<img src ="https://i.imgur.com/Fc8UmEM.gif" width ="700">
**Todo 수정시 History 내역 추가 기능**
<img src ="https://i.imgur.com/B8U3tgz.gif" width ="700">
**Todo 삭제시 History 내역 추가 기능**
<img src ="https://i.imgur.com/4T2jwhd.gif" width ="700">
현재(PR)상의 코드는 트리거 혹은 전파 구조로 되어있습니다.
이를 sync(동기화)방식으로 변경하기 위해 새로운 브랜치에 작업하여 해결되지 않던 문제를 루카스와 이야기 하던것이었는데요!
말씀하신것 처럼 delete에 문제가 있다면 sync방식으로 변경하지 않고(수정하지 않고) 현재방식이 트리거 혹은 전파 방식이기때문에 그대로 사용해도 괜찮을것 같다는 생각이 들었습니다.
**먼저 PublishRelay 를 사용한 이유에 대한 답변입니다!**
History 내역을 구현하기위해 저희가 상의한 기준은 다음과 같습니다!
1. history내역은 로컬디스크, 리모트디스크에 저장하지 않습니다.
2. 앱을 재실할경우 History내역은 제거됩니다.
위와 같이 History를 디스크에 저장하지 않기때문에 앱을 실행할경우 초기값이 필요없을것이라 판단했습니다!
**Relay를 구분한것 에 대한 답변입니다!**
조언해주신 enum으로 구분하는것에대해 정확히 이해하지못한것 같습니다!
현재 이해한 상태로는 Relay하나로 방출하고 DatabaseManager의 bind() 메서드 내부에서는 필터링하여케이스별로 처리할수 있을것 같은데요...!
이해하지 못한 부분은 enum에서 assocated value를 사용하는 방법을 모르는것같슴다.
>Lucas(루카스)
@malrang 🌽 네 코멘트 달고 저도 계속 간간히 생각을 해 봤는데요, 우선 수정하지 않고 그대로 가는 것도 방법이긴 할 것 같구요, 제가 코멘트로 남긴 부분은 그대로 가지 않고 다르게 바꾸었을 때 어떻게 해 볼 것이냐에 대한 부분이긴 했어요.
>
>현재의 경우 Realm 에서 전파를 한다기 보다는 completion 을 통해서 상위 레이어에서 받아서 처리하고 있죠. 모든 CRUD 에서 전부 그렇게 하고 있구요.
>
>이렇게 해도 되기는 하겠지만, 저는 Realm 내부에 외부 전파를 위한 이벤트 스트림을 만들고 (각 케이스를 enum 타입으로 만들어서), 그걸 상위에서 받아서 Firebase 쪽으로 스트림으로 전달하는 방식을 생각했어요. 그러면 Firebsae 내부에서 바인딩을 통해서 동기화라고 해야 할까요 로컬 액션에 대한 처리를 할 수 있게 됩니다.
그러면 상위 레이어가 하는 역할은 한 곳에서 스트림을 받아 다른 곳으로 넘겨주는 역할만 하면 되겠죠. 일종의 컨트롤러 처럼요
>Lucas(루카스)
>@malrang 🌽 아하 네 기본적인 방향성은 저도 그렇게 생각했습니다!! 다만 각 Relay 를 구분할 필요가 있으려나요? 각 케이스를 enum 타입으로 하고, 필요한 정보들은 각 케이스의 assocated value 로 받도록 해서 해당 케이스가 발생했을 때 하나의 Relay 로 방출하도록 하면 되지 않을까요? 그러면 스트림 바인딩 하나만 하고 그 내부에서 케이스별로 처리해주시면 될 것 같아서요..!
그리고, 혹시 PublishRelay 를 쓰신 이유도 알려주실 수 있을까요~?
저번 조언해주셨을때 enum을 활용해 케이스 별로 나누어 전달하는 방법에대해 떠오르는 구현방법이 없어 아래의 코드처럼 Relay를 3개 만들어 방출하도록 PR하였습니다.
```swift
let createDataPublishRelay = PublishRelay<Todo>()
let updateDataPublishRelay = PublishRelay<Todo>()
let deleteDataPublishRelay = PublishRelay<UUID>()
```
어떻게 enum을 활용할 수있을까 enum 내부에 static let 을 사용하라고 조언해주신 것일까 고민했었는데요! 이번에 DM으로 조언해주신 enum assocated value를 활용하라고 말씀해주신걸 보고 루카스가 어떤 조언을 해주신것인지 이해했습니다..!
아래와 같이 enum case별로 각각 나눈후 하나의 Relay로 방출하게 되는구조로 수정하였습니다!
```swift
enum CRUDType {
case create(at: Todo)
case update(at: Todo)
case delete(at: UUID)
case read(at: [Todo])
}
```
```swift
final class RealmDatabase {
let dataBehaviorRelay = BehaviorRelay<CRUDType>(value: .read(at: []))
}
```
```swift
final class DatabaseManager: DatabaseManagerProtocol {
private func bind() {
self.realm.dataBehaviorRelay.subscribe(onNext: { CRUDType in
switch CRUDType {
case .create(at: let todoData):
self.firebase.create(todoData: todoData)
case .update(at: let selectedTodoData):
self.firebase.update(selectedTodo: selectedTodoData)
case .delete(at: let selectedTodoData):
self.firebase.delete(todoID: selectedTodoData)
case .read(at: let todoDataList):
self.firebase.sync(todoData: todoDataList)
self.todoListBehaviorRelay.accept(todoDataList)
}
})
.disposed(by: self.disposeBag)
}
}
```
CRUDType이라는 네이밍을 지었는데, 이 부분이 BehaviorRelay의 타입에 들어가기 떄문에 타입이 들어가야겠다고 생각했습니다.
하지만 계속 고민해보았을 때 이 4가지의 타입을 한번에 아우르는 네이밍이 적절하게 떠오르지않아 결국 CRUDType이라고 정하게 되었습니다. 네이밍이 적절하지 않다면 말씀해주시면 더 고민해보겠습니다.
PublishRealy를 처음 썼던 이유는 Create, Update, Delete 동작은 DatabaseManager에서 구독을 한후 동작하기 때문에 초기값이 필요 없다고 생각하여 Behavior 가 아닌 Publish를 선택하였습니다.
그후 Create, Update, Delete와 같이 Read 기능도 동일하게 방출해줄수는 없을까?? 고민하게 되었고 구현에 실패하여 Read 기능은 직접 호출하는 방식을 사용했습니다.
실패했던 이유는 당시 찾지 못했으나 이번 루카스의 조언대로 수정해보며 이번에 왜 실패하는지 원인을 분석해 보았습니다.
PublishRealy와 BehaviorRealy 의 차이점은 앞서 말씀드린 초기값의 유무 차이점도 있지만
PublishRelay는 구독한 이후의 값만 받아오기 때문에 되지 않았던 것이였습니다. 그래서 초기값으로 세팅 해주어야 하는 Read의 성격을 파악하지 못했었던 것같습니다. 결론적으로 BehaviorRelay로 바꾼 이후 모두 동작이 잘되는 것을 확인했습니다.
이번 조언과 수정으로 CRUD 기능 모두 Realm을 구독 하여 동기화하는 방식으로 수정할수 있었습니다!
1. 테스트코드
2. AppCoordinator
3. DetailViewController - edit, add
- 뷰, edit, add
4. 에러처리
5. 히스토리 내역 한번에 지우는 기능
6. 동기화 작업후 동기화 기능을 수행했다는 UI구현
7. 클린아키텍쳐 그래도가능은한거지? 아니면 처음부터 다시 짜야되는 수준인건지??
개인 앱 준비는 조금늦게
포폴 쥬스메이커랑 오픈마켓, 프로젝트 매니저
# 프로젝트 관리 앱 II [4] malrang, Eddy
안녕하세요 루카스! @innocarpe
STEP4 PR 보냅니다!🌟
잘부탁드립니다! 😁
## 배경
- 마감일 오전 9시에 로컬 노티피케이션으로 마감일을 알립니다
- 마감일이 변경되면 알림의 날짜를 변경합니다
- 할일을 완료하면 설정해 두었던 알림을 해제합니다
- 화면 상단의 버튼을 통해 되돌리기 기능을 수행할 수 있습니다
- 수행할 내용이 없으면 버튼을 비활성화 합니다
## 작업 내용
- 로컬 노티피케이션 생성, 삭제, 업데이트 기능 구현
- 마감일 오전 9시에 알람 오도록 설정
- 할일 목록이 Done에 있을 시 알림 해제 기능 구현
- Undo, Redo 기능 구현
## 리뷰 노트
### Notification update 기능
노티피케이션 update 기능을 구현헀는데 저희가 구현한 방식은 아래와 같습니다.
```swift
func updateNotification(todoData: Todo) {
self.deleteNotification(todoIdentifier: todoData.identifier.uuidString)
guard todoData.todoListItemStatus == .done else {
return self.setNotification(todoData: todoData)
}
}
```
이 방식을 사용하면 실제로는 delete를 한 후 다시 만들어주는 방식입니다.
저희가 처음 생각한 방식은 dateComponent만 갈아끼워주면 된다고 생각했습니다.
그래서 todoData의 identifier를 찾은 후 이것의 dateComponent나 trigger를 찾아서 바꾸려고 했습니다.
하지만 dateComponent와 trigger를 갈아끼우려고 했는데 get-only 방식이라서 갈아끼워지지 않았습니다.
결론적으로 이 부분에 대해 구글링을 해보며 방법을 찾아보았지만 적절한 방법이 나오지 않아서 결국 위의 방식으로 구현하게 되었습니다.
### Undo, Redo 기능 구현을 위한 설계
되돌리기(Undo)기능과 되돌리기 취소(Redo)기능을 구현하기 위해 설계부분을 조언받고 싶습니다!
저희가 구현하고자 했던 Undo기능과 Redo기능은 다음과 같습니다!
1. History 내역이 존재할경우 HistoryButton, UndoButton이 활성화 됩니다. 내역이 존재하지 않는다면 비활성화 됩니다.
**비활성화된 HistoryButton, UndoButton**

**활성화된 HistoryButton, UndoButton**

2. Undo내역이 존재할경우 RedoButton이 활성화 됩니다. 내역이 존재하지 않는다면 비활성화 됩니다.
**활성화된 RedoButton**

3. 앱을 재실행할경우 기존의 History내역이 모두 제거됩니다.
위와 같이 Button활성화 비활성화를 이용해 사용자에게 되돌리기(Undo), 되돌리기취소(Redo) 기능을 사용할수 있는지 여부를 나타내었습니다.
그리고 Undo, Redo 기능을 구현하기위해 DatabaseManager가 Relay를 추가적으로 가지게 되었습니다.
```swift
final class DatabaseManager: DatabaseManagerProtocol {
let todoListBehaviorRelay = BehaviorRelay<[Todo]>(value: [])
let historyBehaviorRelay = BehaviorRelay<[History]>(value: [])
let undoBehaviorRelay = BehaviorRelay<[History]>(value: [])
}
```
아래의 그림은 Stream이 어떻게 이동하는지를 나타내는 그림입니다!
그림의 상황은 새로운 Todo를 추가하고 Undo 버튼을 Tap하고 Redo버튼을 Tap했을때 발생되는 Stream 입니다.

Todo를 생성하거나 제거, 수정 하게될 경우 todoListBehaviorRelay, historyBehaviorRelay에 accept기능을 수행하게되고 historyBehaviorRelay는 HistoryViewModel과 연결되며 HistoryView에 어떤 것을 그려줘야하는지 데이터를 방출하게됩니다.
<img src ="https://i.imgur.com/GJ8ht2q.gif" width ="450">
historyBehaviorRelay에 방출된 값이 있는지 유무로 HistoryButton, UndoButton이 활성화, 비활성화를 결정합니다.
Undo(되돌리기) 버튼이 활성화 되었을 때 Undo버튼을 탭하게되면 historyBehaviorRelay의 마지막으로 방출된 데이터와 반대의 기능을 수행합니다.
예를 들면 create기능으로 새로운 Todo를 생성했다면 Undo 버튼을 탭할경우 delete기능을 수행합니다.
그후 historyBehaviorRelay에 저장된 처음 수행했던 기능 create내역과 Undo 버튼을 탭하여 수행한 Delete 내역을 historyBehaviorRelay에서 제거하고 undoBehaviorRelay에 create내역을 추가합니다.
Undo버튼을 한번이라도 탭했다면 Redo버튼이 활성화 됩니다.
Undo버튼을 탭할 경우 Undo기능을 수행하기전 위의 예시로 들자면 create 내역을 undoBehaviorRelay로 전달하게되고 undoBehaviorRelay에 전달된 값이 있다면 Redo 버튼이 활성화 됩니다.
Redo버튼을 탭할경우 undoBehaviorRelay의 마지막 작업 create 기능을 수행하도록 합니다.
그후 undoBehaviorRelay의 마지막 내역을 제거합니다.
이렇게 History로그를 이용해 Undo, Redo 기능을 구현하였는데요. 생성과 제거는 문제가 없지만 이동할 경우와 수정 기능에서 문제가 발생했습니다.
수정 기능을 Undo, Redo 하기위해서는 History모델을 저장할때 수정전 데이터와 수정후 데이터를 모두 소유해야하는 문제가 발생했습니다.
이동기능은 enum을 활용하여 이동하기전 TodoListItemStatus와 이동후 TodoListItemStatus를 연관값으로 갖도록하여 문제를 해결할수 있었으나 똑같은 방법으로 title, description, date 모두 수정전 데이터와 수정후 데이터를 소유해야하는 상황인데요! 좋은 방법이라는 생각이 들지않으며 현재의 구조가 이해하기 복잡한것 같아 변경해야 할것같습니다..! 어떤 방법으로 Undo, Redo를 구현하는것이 좋은 방법이었을지 조언을 받고 싶습니다..!
## 영상
**UserNotification 사용자권한 요청 기능**
<img src ="https://i.imgur.com/VPUK7PH.gif" width ="700">
**UserNotification alert알림 기능**
<img src ="https://i.imgur.com/JYGpdju.gif" width ="700">
**Todo 추가후 Undo, Redo 기능**
<img src ="https://i.imgur.com/e7YR06Q.gif" width ="700">
**Todo 추가및 이동후 Undo, Redo 기능**
<img src ="https://i.imgur.com/9oJLcKg.gif" width ="700">
@malrang-malrang @kimkyunghun3 말씀대로 설계 변경이 필요할 것 같습니다. 코드는 아직 보지 않았고, PR 본문에 있는
let historyBehaviorRelay = BehaviorRelay<[History]>(value: [])
let undoBehaviorRelay = BehaviorRelay<[History]>(value: [])
부분부터 먼저 보고 의견을 드리는데요,
Undo 와 Redo 는 액션 에 대해서 수행하는 것입니다. 그러면 만약 undo 나 redo 를 스트림으로 외부에 방출해서 그걸 바인딩한다고 하면 스트림에서 방출되어야 하는 정보는 RedoableAction 이나 UndoableAction 과 같은 성질의 것이어야 합니다. 예를 들어 보면
어떤 Todo 를 추가한다.
어떤 Todo 를 삭제한다.
어떤 Todo 를 어디에서 어디로 이동한다.
이런 것들이 UndoableAction 이겠죠. 포토샵을 생각해 보시면 쉬울 것 같습니다. 그러면 이러한 개념들이 추상화가 되어 표현이 되어야 할 것이고(타입으로 선언하든), 이 추상화된 개념을 가지고 프로젝트 내에서 구현을 해야 하는 것입니다.
그러면 어떤 UndoableAction 이 있는데, 화면에서 유저가 Undo 버튼을 눌러서 실제로 그걸 데이터 + UI에 반영해야 한다면 어떻게 할지 생각을 해 본다면
UndoableAction 을 Undo 했을 때 어떻게 처리를 해야 하는지 구현한다. 예컨대
어떤 Todo 를 추가한다. -> Undo 하면 어떤 Todo 를 삭제한다.
어떤 Todo 를 A 에서 B 로 이동한다. -> Undo 하면 어떤 Todo 를 B 에서 A 로 이동한다
와 같은 식이 될 것이다.
정도의 생각이 듭니다. 그리고 반대로 RedoableAction 의 경우엔 Undo 와 다를 것 같은 것이 어떤 Todo 를 추가한다. 액션이었다면 다시 그걸 수행해 주면 되겠죠. 이 두 액션을 명시적으로 나누어야만 할 지는 저도 확신은 없습니다만 (하나의 타입만 두고 내부에서 redo 처리와 undo 처리를 따로 명시한다던가) 방향성은 이렇게 잡아볼 것 같네요.
그러면 개념은 이렇게 추상화를 했고, 개념과 실제 데이터 + UI 단 사이를 중계하는 클래스가 하나 필요할 것 같기도 하네요. UndoableAction 타입 내부에 로직을 만들어 보는 방향도 먼저 고려는 해 볼 것 같은데, 타입은 타입으로만 두고 중계하는 클래스에서 액션에 대해 데이터를 어떻게 핸들링할 지를 구현하는 방향도 있을 것 같구요.
그래서 결과적으로 액션에 따른 데이터가 변경이 되면, 데이터 바인딩은 이미 되어 있을 테니 결과물을 UI 로 렌더링하게 될 것 같구요.
Undo/Redo 에 대해서는 우선 이 정도 생각이 드는데 두 분께서 위 코멘트에 참고해 반영을 해 보시거나 아니면 그 전에라도 어떤 생각이 드시는지 얘기해주셔도 좋을 것 같습니다!
안녕하세요 루카스! 답변이 늦었습니다!
루카스의 조언대로 구조를 수정해보았습니다!
말씀해주신 Undo와 Redo는 Action에대해 수행하는것으로 undo, redo를 스트림으로 외부에 방출해서 바인딩할때 UndoableAction, RedoableAction 과 같이 방출하라고 말씀해주셨는데요..!
아래와 같이 History를 수정하였습니다!
```swift
struct History: Equatable {
let action: HistoryAction
let nextTodo: Todo
let previousTodo: Todo?
}
```
이전과 달리 History는 2개의 Todo를 소유하게되며 수정전Todo와 수정후 Todo를 소유하게됩니다!
```swift
enum HistoryAction: String {
case moved = "Moved"
case added = "Added"
case edited = "Edited"
case removed = "Removed"
var description: String {
return self.rawValue
}
}
```
History가 어떠한 액션으로 만들어진것인지 를 포함하게 됩니다!
이를 이용하여 undo, redo의 행동을 정해주었습니다!
루카스의 조언과는 조금 다른 형태로 구현되었습니다..!
그리고 undo,redo 기능을 처리해줄 UndoRedoManager 를 구현하여 이곳에서는 undo, redo 작업만 하도록 구현하였습니다.
```swift
// UndoRedoManager.swift
final class UndoRedoManager {
let undoRelay = PublishRelay<Void>()
let redoRelay = PublishRelay<Void>()
private var undoDataStack: [History] = []
private var redoDataStack: [History] = []
private let disposeBag = DisposeBag()
private let database: DatabaseManagerProtocol
private let undoRedoActionAble: UndoRedoActionAble
}
```
historyBehaviorRelay에 변화가 생길 경우 undoDataStack에 히스토리를 넣어주고 있습니다.
undoRealy의 경우에는 undoDataStack의 마지막꺼를 제거하고 이를 redoDataStack 배열에 넣어주고 있습니다.
그리고 redoRealy의 경우에는 redoDataStack의 마지막꺼를 제거하고 있습니다.
```swift
// UndoRedoManager.swift
func bind() {
self.database.historyBehaviorRelay
.subscribe(onNext: { [weak self] history in
self?.undoDataStack = history
})
.disposed(by: self.disposeBag)
self.undoRelay
.subscribe(onNext: { [weak self] _ in
guard let lastHistory = self?.undoDataStack.removeLast() else { return }
self?.undoRedoActionAble.undoTapEvent(history: lastHistory)
self?.redoDataStack.append(lastHistory)
})
.disposed(by: self.disposeBag)
self.redoRelay
.subscribe(onNext: { [weak self] _ in
guard let lastHistory = self?.redoDataStack.removeLast() else { return }
self?.undoRedoActionAble.redoTapEvent(history: lastHistory)
})
.disposed(by: self.disposeBag)
}
```
isRedoEmpty 메서드는 undoRelay, redoRelay를 하나로 합쳐서 redoDataStack의 count가 0인 경우 observer로 false를 내보재서 Redo가 없다는 것을 표현하도록 했습니다. 있는 경우에는 true를 내보내도록 했습니다.
```swift
// UndoRedoManager.swift
func isRedoEmpty() -> Observable<Bool> {
return Observable.create { observer in
let _ = Observable.of(self.undoRelay, self.redoRelay)
.merge()
.subscribe(onNext: { [weak self] _ in
if self?.redoDataStack.count == 0 {
observer.onNext(false)
} else {
observer.onNext(true)
}
})
return Disposables.create()
}
}
```