/
MovieDetailViewModel.swift
132 lines (112 loc) · 3.45 KB
/
MovieDetailViewModel.swift
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//
// MovieDetailViewModel.swift
// ModernMVVMList
//
// Created by Vadim Bulavin on 3/19/20.
// Copyright © 2020 Vadym Bulavin. All rights reserved.
//
import Foundation
import Combine
final class MovieDetailViewModel: ObservableObject {
@Published private(set) var state: State
private var bag = Set<AnyCancellable>()
private let input = PassthroughSubject<Event, Never>()
init(movieID: Int) {
state = .idle(movieID)
Publishers.system(
initial: state,
reduce: Self.reduce,
scheduler: RunLoop.main,
feedbacks: [
Self.whenLoading(),
Self.userInput(input: input.eraseToAnyPublisher())
]
)
.assign(to: \.state, on: self)
.store(in: &bag)
}
func send(event: Event) {
input.send(event)
}
}
// MARK: - Inner Types
extension MovieDetailViewModel {
enum State {
case idle(Int)
case loading(Int)
case loaded(MovieDetail)
case error(Error)
}
enum Event {
case onAppear
case onLoaded(MovieDetail)
case onFailedToLoad(Error)
}
struct MovieDetail {
let id: Int
let title: String
let overview: String?
let poster: URL?
let rating: Double?
let duration: String
let genres: [String]
let releasedAt: String
let language: String
init(movie: MovieDetailDTO) {
id = movie.id
title = movie.title
overview = movie.overview
poster = movie.poster
rating = movie.vote_average
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.minute, .hour]
duration = movie.runtime.flatMap { formatter.string(from: TimeInterval($0 * 60)) } ?? "N/A"
genres = movie.genres.map(\.name)
releasedAt = movie.release_date ?? "N/A"
language = movie.spoken_languages.first?.name ?? "N/A"
}
}
}
// MARK: - State Machine
extension MovieDetailViewModel {
static func reduce(_ state: State, _ event: Event) -> State {
switch state {
case .idle(let id):
switch event {
case .onAppear:
return .loading(id)
default:
return state
}
case .loading:
switch event {
case .onFailedToLoad(let error):
return .error(error)
case .onLoaded(let movie):
return .loaded(movie)
default:
return state
}
case .loaded:
return state
case .error:
return state
}
}
static func whenLoading() -> Feedback<State, Event> {
Feedback { (state: State) -> AnyPublisher<Event, Never> in
guard case .loading(let id) = state else { return Empty().eraseToAnyPublisher() }
return MoviesAPI.movieDetail(id: id)
.map(MovieDetail.init)
.map(Event.onLoaded)
.catch { Just(Event.onFailedToLoad($0)) }
.eraseToAnyPublisher()
}
}
static func userInput(input: AnyPublisher<Event, Never>) -> Feedback<State, Event> {
Feedback(run: { _ in
return input
})
}
}