일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- auto_assign
- 함수형 프로그래밍
- swift CI 적용
- XCTest
- 함수형 코딩
- Swift thread
- combine
- Apple Developer Academy @ POSTECH
- print 단점
- SwiftUI
- Swift
- 오픈소스
- 하드디스크 삭제 원리
- Firebase
- flutter
- SwiftUI VStack
- LGTM
- firestore
- IOS
- xcode
- 액션과 계산 데이터
- os_log
- github
- CI
- MVVM
- swift github action
- 2기화이팅
- ChatGPT
- unittest
- 쏙쏙 들어오는 함수형 코딩
- Today
- Total
개발공방
[애니또] MVVM + Combine - 리팩토링 (3) 본문
노션에 먼저 작성한 후 블로그로 옮기기 때문에 노션이 보기에 더 편합니다.
https://www.notion.so/MVVM-Combine-323ab64639374213b1b1734fcea257e7?pvs=4
[애니또] ViewController와 View의 분리 - 리팩토링 (1)
[애니또] ViewController와 View의 분리 - 리팩토링 (1)
현재 애니또 iOS팀이 직면한 문제 현재까지 개발하면서 많은 문제와 버그들을 만났다. 첫 문제들을 해결하는데는 어려움이 없었지만, 앱의 규모가 조금씩 커지면서 문제들이 서로 엮이기 시작했
dev-workplace.tistory.com
앞전에 작성한 글을 보면, UIViewController에서 View를 분리한 과정을 볼 수 있다. Delegate 패턴을 통해 데이터와 액션을 주고 받았다.
DetailWaitView를 MVC패턴에서 MVVM으로 변경하는 과정 중 먼저 한 작업은 네트워크 통신을 통해 방 정보를 불러와서 View에 바인딩하는 작업이었다.
기존의 방식과 크게 달라진 부분은 없지만, 핵심은 Publisher를 사용한 Stream으로 만들었다는 것이다.
이해를 돕기위해 기존 MVC 코드부터 살펴보자.
// 기존 코드
final class DetailWaitViewController: BaseViewController {
private var roomInformation: Room?
override func viewDidLoad() {
super.viewDidLoad()
self.fetchRoomData()
// 생략
}
// MARK: func
private func fetchRoomData() {
self.requestWaitRoomInfo() { [weak self] result in
switch result {
case .success(let room):
DispatchQueue.main.async {
self?.detailWaitView.updateDetailWaitView(room: room)
}
case .failure:
self?.makeAlert(title: TextLiteral.errorAlertTitle,
message: TextLiteral.detailWaitViewControllerLoadDataMessage)
}
}
}
// MARK: network
private func requestWaitRoomInfo(completionHandler: @escaping ((Result<Room, NetworkError>) -> Void)) {
Task {
do {
let data = try await self.detailWaitService.getWaitingRoomInfo(roomId: self.roomIndex.description)
if let roomInfo = data {
self.roomInformation = roomInfo
completionHandler(.success(roomInfo))
}
} catch NetworkError.serverError {
completionHandler(.failure(.serverError))
} catch NetworkError.clientError(let message) {
completionHandler(.failure(.clientError(message: message)))
}
}
}
}
간단하게 핵심은 총 3개다.
- ViewDidLoad단계에서 fetchRoomData 함수 실행
- Completion Handler를 통해 불러온 방 정보를 view에 업데이트
- roomInformation 변수에 데이터 할당
위 코드를 보면 ViewDidLoad 단계에서 fetchRoomData() 함수를 실행하고, 불러온 room 데이터를 self?.detailWaitView.updateDetailWaitView(room: room) 해당 뷰에 업데이트 시킨다.
그리고 방 정보를 self.roomInformation = roomInfo 변수에 저장한다. 해당 방 정보를 변수에 저장하는 이유는 초대코드를 복사하는 버튼을 눌렀을 때, 초대코드를 네트워크 통신을 통해 받아오는 것이 아니라 이미 받아왔던 데이터에서 초대 코드를 사용해 복사한다.
guard let invitationCode = self.roomInformation?.invitation?.code else { return }
이 코드 때문에 네트워크 통신으로 받아온 방 정보를 변수에 저장하고 있다.
위의 flow가 기존의 방 정보를 불러와서 View에 연결하는 코드였다.
그렇다면 이젠 MVVM으로 어떻게 변경했을까?
우선 DetailWaitViewModel을 만들었다.
// DetailWaitViewModel.swift
final class DetailWaitViewModel {
var roomInformation = CurrentValueSubject<Room?, Never>(nil)
func fetchRoomInformation() {
requestWaitRoomInfo { [weak self] result in
switch result {
case .success(let roominformation):
self?.roomInformation.send(roominformation)
case .failure(let error):
print(error)
}
}
}
// MARK: network
private func requestWaitRoomInfo(completionHandler: @escaping ((Result<Room, NetworkError>) -> Void)) {
Task {
do {
let data = try await self.detailWaitService.getWaitingRoomInfo(roomId: self.roomIndex.description)
if let roomInformation = data {
completionHandler(.success(roomInformation))
}
} catch NetworkError.serverError {
completionHandler(.failure(.serverError))
} catch NetworkError.clientError(let message) {
completionHandler(.failure(.clientError(message: message)))
}
}
}
}
우선 방 정보에 대한 Stream을 만들었다. var roomInformation = CurrentValueSubject<Room?, Never>(nil)
case .success(let roominformation):
self?.roomInformation.send(roominformation)
네트워크 통신이 정상적으로 이루어지면, send 를 통해 값을 Stream에 전달함으로서 이벤트 발생을 알린다.
그리고 ViewController에서는 뷰에 바인딩을 해준다.
// DetailWaitViewController.swift
final class DetailWaitViewController: BaseViewController {
// MARK: - property
private var cancellable = Set<AnyCancellable>()
private let detailWaitViewModel: DetailWaitViewModel
// MARK: - init
init(roomIndex: Int) {
self.detailWaitViewModel = DetailWaitViewModel(roomIndex: roomIndex, detailWaitService: DetailWaitAPI(apiService: APIService()))
super.init()
}
override func viewDidLoad() {
super.viewDidLoad()
self.setBind()
// 생략
}
// MARK: func
private func setBind() {
self.detailWaitViewModel.roomInformation
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] room in
if let room {
self?.detailWaitView.updateDetailWaitView(room: room)
}
})
.store(in: &self.cancellable)
}
}
이렇게만 하면 양방향 바인딩이 완료된다.
ViewModel에서는 이벤트가 발생했음을 Stream에 전달하는 역할에서 끝이고, ViewController는 해당 Stream에 이벤트가 발생했을 때 실행되는 함수를 연결함으로서 역할이 끝이다.
기존 코드에서는 네트워크 함수 호출 → 데이터 불러오기 시도 → UI 업데이트 이 세 역할이 하나의 flow로서 분리가 어려웠는데,
MVVM에서는 1. 네트워크 함수 호출 → 이벤트 발생 , 2. 이벤트가 발생했을 때 → UI업데이트 로써 각각의 역할이 분리되었다.
이번 게시글엔 대기중인 방의 정보를 불러와서 바인딩 하는 간단한 과정을 살펴보았다.
다음으론 각각의 액션(Input)과 결과(Output)을 어떻게 관리하고 처리했는지 살펴보겠다.
'Swift > UIKit' 카테고리의 다른 글
[애니또] MVVM + Combine Input & Output 구조 - 리팩토링 (4) (0) | 2023.07.14 |
---|---|
[애니또] MVVM을 선택한 이유 - 리팩토링 (2) (0) | 2023.06.07 |
[애니또] ViewController와 View의 분리 - 리팩토링 (1) (0) | 2023.04.19 |
Swift UIBezierPath 크리스마스 트리 (4) | 2022.12.25 |
Swift DispatchQueue 기본 원리 (8) | 2022.07.14 |