원문 보러가기 -> 런로그 위키
모든 데이터흐름은 Input/output으로만 처리하도록 하는게 맞음(구조보고 기대하는거랑 실제 로직이랑 차이가 생겨버림)
뷰모델-뷰컨 bind 시점 조정해라(init에서 부르지말것)
Input output 변수 있는경우는 struct가 나음(웬만해서 struct쓰자)
enum Input {
case loadData // 유저 데이터 호출
case menuItemSelected(Int) // 메뉴 항목 선택
}
struct Output {
/// 로딩 상태 제어
let stopLoading = CurrentValueSubject<Bool, Never>(false)
/// 프로필 정보 업데이트
let profileDataUpdated = CurrentValueSubject<UserInfoVO, Never>(
UserInfoVO(nickname: "RunLogger", totalDistance: 0.0, streakCount: 0, logCount: 0)
)
/// 이동할 뷰컨트롤러 이벤트 전달
let navigateToViewController = CurrentValueSubject<UIViewController?, Never>(nil)
}
이부분은 ViewModel의 생명주기를 알아야하는데, 우리는 Controller에서 뷰모델을 생성해주거나, ViewModel을 생성해서 ViewController를 생성할 때 주입해주기 때문에, ViewModel의 init()에서 bind를 해버리면, 아직 뷰가 존재하지 않는데 bind를 해버리는 상황이 발생할 수 있었다. 그래서 ViewModel의 bind()를 ViewController의 viewDidLoad()에서 부르는 로직으로 수정하였다.
ViewController파일
override func viewDidLoad() {
super.viewDidLoad()
setupUI() // 화면 구성
setupNavigationBar() // 네비게이션 바 설정
setupTableView() // 테이블뷰 바인딩
viewModel.bind() // ViewModel의 입력 수신 준비
bindViewModel() // ViewModel의 출력 바인딩
}
// ... 중략 ...
private func bindViewModel() {
// 로딩 상태 변경
viewModel.output.stopLoading
.receive(on: DispatchQueue.main)
.sink { [weak self] off in
off ? self?.stopLoading() : self?.startLoading()
}
.store(in: &cancellables)
// 프로필 데이터 업데이트
viewModel.output.profileDataUpdated
.receive(on: DispatchQueue.main)
.sink { [weak self] config in
self?.mypageView.configure(with: config)
}
.store(in: &cancellables)
// 화면 전환 처리
viewModel.output.navigateToViewController
.receive(on: DispatchQueue.main)
.sink { [weak self] viewController in
if let vc = viewController {
self?.navigationController?.pushViewController(vc, animated: true)
}
}
.store(in: &cancellables)
}
// MARK: - Bind (Input -> Output)
func bind() {
inputSubject
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
switch event {
case .loadData:
self?.fetchProfileData()
case .menuItemSelected(let index):
self?.handleMenuSelection(index)
}
}
.store(in: &cancellables)
}