A Pattern Carved With Tastes
🎥 "정말 감명깊게 본 영화는 무늬처럼 우리 몸안에 남더라고"
실시간 영화 순위를 살펴보고, 궁금했던 영화를 검색, 메모와 함께 기억하고싶은 영화들을 소중하게 저장할 수 있는 앱
- 날마다의 영화 순위, 관객 수를 살펴볼 수 있는 메인화면
- 줄거리와 등장인물 정보 등을 확인할 수 있는 상세화면
- Youtube와 연동되어 제공되는 영화 예고편
- 장르별 개봉 영화 탐색, 개봉일 순, 관객 기대 순위 순 정렬 기능
- TMDB 데이터베이스를 이용한 영화 검색 / 정렬 / 저장 기능
- 원하는 영화를 메모와 함께 나의 폴더별 무늬 저장소에 저장 가능
- 개발 인원
- iOS개발 1명
- 개발 기간
- 2024.07 - 2024.08 (1개월)
- iOS 최소 버전
- iOS 16.0+
-
활용기술 및 키워드
- iOS : swift 5.10, xcode 15.4, UIKit
- Network : RxSwift, Alamofire
- UI : CodeBaseUI, Snapkit, Then, Hero
-
라이브러리
라이브러리 | 사용 목적 | Version |
---|---|---|
Kingfisher | 이미지 처리 | 7.0 |
SkeletonView | 로딩 이미지 처리 | 1.31 |
Realm | 앱 내 파일 저장소 | 10.52.1 |
Input / Output + MVVM Architecture
- RxSwift Input / Output Pattern을 통한 양방향 데이터 바인딩으로 프로젝트 데이터 흐름 일원화
- Router Pattern을 통한 반복되는 네트워크 작업 추상화, RxSwift Single Traits를 통한 에러 핸들링
- Generic 형식의 Base Class 상속과 Protocol 채택을 통한 프로젝트 구조의 명시적 정의
onNext, onComplete가 아닌 onError를 받게 되면 FlatMap에서 Mapping한 함수일지라도 Stream 전체가 끊겨버린다.
- Request와 Decodable 리턴 형식만 다른 반복되는 네트워크 통신을 추상화하기 위해, Alamofire를 Router Pattern과 함께 적용
- RxSwift의 Single Traits를 사용하여 Observable의 이벤트 방출, 완료를 단순화하여 처리하려 하였으나, Error Case시에 전체 Stream까지 모두 종료되는 이슈가 발생
- Result 형태를 맵핑하는 Single을 반환값으로 설정하고, Error Case 또한 Single에서가 아닌 Result 형태에서 처리하는 형태로 문제 해결
Network Manager
//Network Request with RxSwift
func rxNetworkRequest<T: Decodable>(router: Network, type: T.Type) -> Single<T> {
//Create Observable
let observable = Single<T>.create { single in
//Mapping Alamofire
AF.request(router.endPoint, method: router.method, parameters: router.parameters).responseDecodable(of: T.self) { response in
switch response.result {
case .success(let value):
single(.success(value))
case .failure(let error):
single(.failure(error))
}
}
//Return Disposable
return Disposables.create()
}
return observable
}
ViewModel
button.rx.tap
.throttle(.seconds(1), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { NetworkManager.shared.networkRequest(router: Network.poster(id: id), type: PosterResult.self) }
.subscribe(onNext: { value in
switch value {
case .success(let poster):
print(poster)
case .failure(let error):
print(error)
}
}, onError: { error in
print(error)
}, onCompleted: {
print("Completed")
}, onDisposed: {
print("Disposed")
})
.disposed(by: disposeBag)
loadView, Input, Output, transform 등의 작업을 Class 생성 시마다 매 번 반복을 해 주어야 한다.
- MVVM 구조에서 반복적으로 수행되는 init, loadView, configure 등의 작업들을 자동화 해주기 위해 Generic 형식의 Base Class 상속을 활용
- ViewModel에서 Input \ Output 구조체, 구조체를 바인딩해주는 transform메소드의 구현을 명시적으로 정의해주기 위해 Protocol의 AssociatedType 사용
Base View Controller
class BaseViewController<View: BaseView, ViewModel: BaseViewModel>: UIViewController {
let baseView: View
let viewModel: ViewModel
init(view: View, viewModel: ViewModel) {
self.baseView = view
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = baseView
}
override func viewDidLoad() {
super.viewDidLoad()
configureView()
configureRx()
}
func configureView() { }
func configureRx() { }
}
View Model Protocol
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}