1차 MVP를 MVC에서 MVVM으로 리팩토링해보았다.
PermissionViewController
(MVC)import UIKit
import SnapKit
class PermissionViewController: UIViewController {
private let permissionView = PermissionView()
private let familyControlsManager = FamilyControlsManager() // ✅ 직접 권한 요청을 처리하는 객체
override func viewDidLoad() {
super.viewDidLoad()
view = permissionView
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.navigationBar.isHidden = true
requestScreenTimePermission() // ✅ 권한 요청을 직접 호출
}
private func requestScreenTimePermission() {
familyControlsManager.requestAuthorization { [weak self] in
DispatchQueue.main.async {
self?.navigateToStarList() // ✅ 권한이 허용되면 다음 화면으로 이동
}
}
}
private func navigateToStarList() {
let starListViewController = StarListViewController()
navigationController?.pushViewController(starListViewController, animated: false)
}
}
ViewController가 너무 많은 역할을 함
requestScreenTimePermission()
에서 비즈니스 로직(권한 요청)과 화면 이동을 직접 수행UI 변경이 있을 때 비즈니스 로직도 영향을 받을 가능성이 큼
requestScreenTimePermission()
내부 로직도 수정해야 할 수도 있음PermissionViewModel.swift
(MVVM)//
// PermissionViewModel.swift
// star
//
// Created by Eden on 1/31/25.
//
import Foundation
import RxSwift
import RxCocoa
final class PermissionViewModel {
private let familyControlsManager: FamilyControlsManager
init(familyControlsManager: FamilyControlsManager = FamilyControlsManager()) {
self.familyControlsManager = familyControlsManager
}
}
extension PermissionViewModel {
struct Input {
let requestPermissionTrigger: Observable<Void>
}
struct Output {
let navigateToStarList: Observable<Void>
}
func transform(_ input: Input) -> Output {
let navigateToStarList = PublishRelay<Void>()
input.requestPermissionTrigger
.flatMapLatest { [weak self] _ -> Observable<Bool> in
guard let self = self else { return Observable.just(false) }
return self.requestScreenTimePermission()
}
.filter { $0 } // ✅ granted가 true일 때만 실행
.map { _ in } // ✅ Bool → Void 변환
.bind(to: navigateToStarList)
.disposed(by: DisposeBag())
return Output(
navigateToStarList: navigateToStarList.asObservable()
)
}
private func requestScreenTimePermission() -> Observable<Bool> {
return Observable.create { observer in
self.familyControlsManager.requestAuthorization {
observer.onNext(true)
observer.onCompleted()
}
return Disposables.create()
}
}
}
✅ ViewModel이 권한 요청을 처리하고, ViewController는 이를 구독하여 UI를 변경함.
✅ ViewModel에서 requestScreenTimePermission()
을 실행하여 권한 요청을 수행함.
✅ ViewController는 Output
을 구독하여 화면을 이동하는 역할만 담당함.
PermissionViewController.swift
(MVVM)//
// PermissionViewController.swift
// star
//
// Created by Eden on 1/24/25.
//
import UIKit
import SnapKit
import RxSwift
final class PermissionViewController: UIViewController {
private let permissionViewModel = PermissionViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.navigationBar.isHidden = true
}
private func bind() {
let input = PermissionViewModel.Input(
requestPermissionTrigger: Observable.just(())
)
let output = permissionViewModel.transform(input)
output.navigateToStarList
.subscribe(onNext: { [weak self] in
self?.navigateToStarList()
})
.disposed(by: disposeBag)
}
func navigateToStarList() {
let starListViewController = StarListViewController()
navigationController?.pushViewController(starListViewController, animated: false)
}
}
✅ ViewController는 UI 업데이트 역할만 수행하고, 비즈니스 로직을 담당하지 않음.
✅ ViewModel의 Output을 구독하여 화면 전환을 수행.
MVC (Before) | MVVM (After) | |
---|---|---|
권한 요청 로직 | ViewController에서 직접 실행 | ViewModel에서 관리 |
ViewController 역할 | UI 관리 + 권한 요청 처리 | UI 관리만 담당 |
비즈니스 로직 분리 | ViewController에 포함됨 | ViewModel로 이동하여 재사용 가능 |
테스트 가능성 | 낮음 (UI 코드와 결합) | 높음 (ViewModel 단위 테스트 가능) |