TIL
🌱 난 오늘 무엇을 공부했을까?
📌 MVVM + Clean Architecture Refactoring 후기
📍 Data Layer
protocol ProjectRepositoryProtocol {
mutating func create(data: ProjectModel)
func read() -> [ProjectModel]
mutating func update(id: String,
data: ProjectModel)
mutating func delete(id: String)
}
struct TemporaryProjectRepository: ProjectRepositoryProtocol {
private var temporaryStore: [ProjectModel]
init() {
temporaryStore = [ProjectModel]()
}
mutating func create(data: ProjectModel) {
temporaryStore.append(data)
}
func read() -> [ProjectModel] {
return temporaryStore
}
mutating func update(id: String,
data: ProjectModel) {
temporaryStore.indices.forEach {
temporaryStore[$0] = temporaryStore[$0].id == id ? data : temporaryStore[$0]
}
}
mutating func delete(id: String) {
temporaryStore.removeAll(where: { $0.id == id })
}
}
📍 Domain Layer
🔗 Entity
struct ProjectModel: Decodable {
let id: String
let title: String
let body: String
let date: Date
let workState: ProjectState
}
enum ProjectState: String, Decodable, CaseIterable {
case todo, doing, done
var name: String {
switch self {
case .todo:
return "TODO"
case .doing:
return "DOING"
case .done:
return "DONE"
}
}
var actionTitles: (first: String,
second: String) {
switch self {
case .todo:
return ("Move To DOING", "Move To DONE")
case .doing:
return ("Move To TODO", "Move To DONE")
case .done:
return ("Move To TODO", "Move To DOING")
}
}
}
- LocalDB, RemoteDB에서 데이터를 모델로 변환하기 위한 Entity Type
🔗 UseCase
protocol ProjectUseCaseProtocol {
var repository: ProjectRepositoryProtocol { get }
mutating func create(data: ProjectViewModel)
func read() -> [ProjectViewModel]
mutating func update(id: String,
data: ProjectViewModel)
mutating func delete(id: String)
}
struct ProjectUseCase: ProjectUseCaseProtocol, ProjectTranslater {
var repository: ProjectRepositoryProtocol
init() {
repository = TemporaryProjectRepository()
}
mutating func create(data: ProjectViewModel) {
repository.create(data: translateToProjectModel(with: data))
}
func read() -> [ProjectViewModel] {
let models = repository.read().map {
translateToProjectViewModel(with: $0)
}
return models
}
mutating func update(id: String,
data: ProjectViewModel) {
repository.update(id: id,
data: translateToProjectModel(with: data))
}
mutating func delete(id: String) {
repository.delete(id: id)
}
}
struct ProjectViewModel {
let id: String
let title: String
let body: String
let date: String
var workState: ProjectState
}
protocol ProjectTranslater {
func translateToProjectModel(with data: ProjectViewModel) -> ProjectModel
func translateToProjectViewModel(with data: ProjectModel) -> ProjectViewModel
}
extension ProjectTranslater {
func translateToProjectModel(with data: ProjectViewModel) -> ProjectModel {
let model = ProjectModel(id: data.id,
title: data.title,
body: data.body,
date: data.date.toDate(),
workState: data.workState)
return model
}
func translateToProjectViewModel(with data: ProjectModel) -> ProjectViewModel {
let model = ProjectViewModel(id: data.id,
title: data.title,
body: data.body,
date: data.date.convertLocalization(),
workState: data.workState)
return model
}
}
📍 Presentation Layer
🔗 View Model
final class ProjectListViewModel {
private var useCase: ProjectUseCaseProtocol
private var todoListObserver: Observable<[ProjectViewModel]>
private var doingListObserver: Observable<[ProjectViewModel]>
private var doneListObserver: Observable<[ProjectViewModel]>
init() {
useCase = ProjectUseCase()
todoListObserver = Observable([ProjectViewModel]())
doingListObserver = Observable([ProjectViewModel]())
doneListObserver = Observable([ProjectViewModel]())
}
private func fetch() {
todoListObserver.value = read().filter {
$0.workState == .todo
}
doingListObserver.value = read().filter {
$0.workState == .doing
}
doneListObserver.value = read().filter {
$0.workState == .done
}
}
func todoListObserverBind(closure: @escaping ([ProjectViewModel]) -> Void) {
todoListObserver.bind(closure)
}
func doingListObserverBind(closure: @escaping ([ProjectViewModel]) -> Void) {
doingListObserver.bind(closure)
}
func doneListObserverBind(closure: @escaping ([ProjectViewModel]) -> Void) {
doneListObserver.bind(closure)
}
private func retrieveDateLabelColor(data stringDate: String) -> UIColor {
let date = stringDate.toDate()
let currentDate = Date()
if stringDate != currentDate.convertLocalization() && date < currentDate {
return .systemRed
}
return .black
}
func retrieveItems(state: ProjectState) -> [ProjectViewModel] {
switch state {
case .todo:
return todoListObserver.value
case .doing:
return doingListObserver.value
case .done:
return doneListObserver.value
}
}
func configureNumberOfRow(state: ProjectState) -> Int {
return retrieveItems(state: state).count
}
func configureCellItem(cell: ProjectTableViewCell,
state: ProjectState,
indexPath: IndexPath) {
let item = retrieveItems(state: state)[indexPath.row]
cell.setItems(title: item.title,
body: item.body,
date: item.date,
dateColor: retrieveDateLabelColor(data: item.date))
}
func makeTableHaederView(state: ProjectState) -> ProjectTableHeaderView {
let items = retrieveItems(state: state)
let view = ProjectTableHeaderView()
ProjectState.allCases.filter {
$0 == state
}.forEach {
view.setItems(title: $0.name,
count: items.count.description)
}
return view
}
func makeSwipeActions(state: ProjectState,
indexPath: IndexPath) -> [UIContextualAction] {
let item = retrieveItems(state: state)[indexPath.row]
let deleteSwipeAction = UIContextualAction(style: .destructive,
title: Design.deleteSwipeActionTitle,
handler: { [weak self] _, _, completionHaldler in
self?.delete(id: item.id)
completionHaldler(true)
})
return [deleteSwipeAction]
}
func makeAlertContoller(tableView: UITableView,
indexPath: IndexPath,
state: ProjectState) -> UIAlertController {
let alertController = UIAlertController(title: Design.alertControllerDefaultTitle,
message: nil,
preferredStyle: .actionSheet)
makeAlertAction(state: state,
indexPath: indexPath).forEach {
alertController.addAction($0)
}
alertController.popoverPresentationController?.sourceView = tableView
alertController.popoverPresentationController?.sourceRect = tableView.rectForRow(at: indexPath)
alertController.popoverPresentationController?.permittedArrowDirections = [.up]
return alertController
}
private func makeAlertAction(state: ProjectState,
indexPath: IndexPath) -> [UIAlertAction] {
let item = retrieveItems(state: state)[indexPath.row]
let actionHandlers = makeActionHandlers(item: item)
guard let firstActionHandler = actionHandlers.first,
let lastActionHandler = actionHandlers.last
else { return [UIAlertAction]() }
let firstAction = UIAlertAction(title: item.workState.actionTitles.first,
style: .default,
handler: firstActionHandler)
let lastAction = UIAlertAction(title: item.workState.actionTitles.second,
style: .default,
handler: lastActionHandler)
return [firstAction, lastAction]
}
private func makeActionHandlers(item: ProjectViewModel) -> [((UIAlertAction) -> Void)?] {
let actionHandlers = ProjectState.allCases.filter {
$0 != item.workState
}.map {
makeHandler(item: item,
state: $0)
}
return actionHandlers
}
private func makeHandler(item: ProjectViewModel,
state: ProjectState) -> ((UIAlertAction) -> Void)? {
let handler: ((UIAlertAction) -> Void) = { [self] ( _: UIAlertAction) in
changeState(item: item, state: state)
}
return handler
}
func create(data: ProjectViewModel) {
useCase.create(data: data)
fetch()
}
private func read() -> [ProjectViewModel] {
return useCase.read()
}
func update(id: String,
data: ProjectViewModel) {
useCase.update(id: id,
data: data)
fetch()
}
private func delete(id: String) {
useCase.delete(id: id)
fetch()
}
private func changeState(item: ProjectViewModel,
state: ProjectState) {
let newItem = ProjectViewModel(id: item.id,
title: item.title,
body: item.body,
date: item.date,
workState: state)
update(id: item.id,
data: newItem)
}
}
final class Observable<T> {
var value: T {
didSet {
listener?(value)
}
}
private var listener: ((T) -> Void)?
init(_ value: T) {
self.value = value
}
func bind(_ closure: @escaping (T) -> Void) {
closure(value)
listener = closure
}
}
📌 Class Diagram
📌 느낀점
ViewModel이 어떤 부분까지 처리해야 할 지 처음부터 정하고 리팩토링을 시작했다면 좋았을 것 같다.
- 단순하게 Controller의 일을 대신 처리한다고 생각하니 막막하고 리팩토링 과정이 많이 어려웠다. Model의 데이터를 접근하는(CRUD) input, output 이벤트들을 모두 View Model에서 처리하고 View에 결과를 던져줘야 한다고 생각했다.. 이게 맞는지는 MVVM을 더욱 공부해봐야지..
RxSwift
- UIKit 방법으로 바인딩을 하려고 하니 View Model도 엄청 커지고 Controller도 엄청 비대해지는데 이걸 RxSwift로 했다면 달라졌을까? 라는 생각이 들어서 RxSwift도 꼭 공부해보고 presentation 영역을 리팩토링 해봐야겠다.
Delegate Data Binding
- 현재 Observer 방식으로 Binding을 했는데 Delegate로 구현하는 방법과 어떤 차이가 있을지도 궁금증이 생겼다.
View Model Protocol
- Clean Architecture 설명에 View Model도 추상화를 해줘야 한다고 했는데, 어떤 부분들을 추상화 해줘야 하는지 몰라서 하지 못 했었다. 여러가지 예시를 보면 알 수 있을지도?
Desigin Pattern
- 이번 리팩토링에서 내가 사용한 Desigin Pattern이 뭐가 있을까 궁금해졌는데 Desigin Pattern에 아직 익숙하지 않아서 뭐가 뭔지 모르겠다. 그래도 전략 패턴을 사용해서 repository나 usecase를 변경하기 쉽도록 했다고 느껴진다.