forked from refactoring-challenge/reversi-ios
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathViewController.swift
209 lines (176 loc) · 7.58 KB
/
ViewController.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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import UIKit
import Combine
class ViewController: UIViewController {
@IBOutlet private var boardView: BoardView!
@IBOutlet private var messageDiskView: DiskView!
@IBOutlet private var messageLabel: UILabel!
@IBOutlet private var messageDiskSizeConstraint: NSLayoutConstraint!
@IBOutlet private var playerControls: [UISegmentedControl]!
@IBOutlet private var countLabels: [UILabel]!
@IBOutlet private var playerActivityIndicators: [UIActivityIndicatorView]!
private var viewModel: ViewModel!
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
boardView.delegate = self
viewModel = ViewModel(savedData: try? loadGame())
bindViewModel(messageDiskSize: messageDiskSizeConstraint.constant)
}
private var viewHasAppeared: Bool = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if viewHasAppeared { return }
viewHasAppeared = true
viewModel.viewDidFirstAppear()
}
}
// MARK: Binding
extension ViewController {
private func bindViewModel(messageDiskSize: CGFloat) {
// メッセージ表示の中のディスクの表示
let messageDiskSizeConstraint: NSLayoutConstraint = self.messageDiskSizeConstraint
let messageDiskView: DiskView = self.messageDiskView
viewModel.messageDiskViewDisk
.sink { disk in
if let disk = disk {
messageDiskSizeConstraint.constant = messageDiskSize
messageDiskView.disk = disk
} else {
messageDiskSizeConstraint.constant = 0
}
}
.store(in: &cancellables)
// メッセージ表示の中のテキスト
viewModel.messageLabelText
.assign(to: \.text, on: messageLabel)
.store(in: &cancellables)
// プレイヤーモードの選択値
for (viewModel, view) in zip(viewModel.playerControlSelectedIndices, playerControls) {
viewModel
.assign(to: \.selectedSegmentIndex, on: view)
.store(in: &cancellables)
}
// ディスクカウント
for (viewModel, view) in zip(viewModel.countLabelTexts, countLabels) {
viewModel
.assign(to: \.text, on: view)
.store(in: &cancellables)
}
// ぐるぐる
for (viewModel, view) in zip(viewModel.playerActivityIndicatorAnimateds, playerActivityIndicators) {
viewModel
.sink { isAnimated in
if isAnimated {
view.startAnimating()
} else {
view.stopAnimating()
}
}
.store(in: &cancellables)
}
// リバーシ盤表示の更新
viewModel.boardViewUpdate
.sink { [weak self] request in
self?.handleBoardViewUpdateRequest(request)
}
.store(in: &cancellables)
// アラート表示
viewModel.showAlert
.sink { [weak self] request in
self?.handleShowAlertRequest(request)
}
.store(in: &cancellables)
// セーブ
viewModel.save
.sink { [weak self] request in
self?.handleSaveRequest(request)
}
.store(in: &cancellables)
}
}
// MARK: Handling Request
extension ViewController {
/// リバーシ盤の表示更新のリクエストをハンドリングします。
/// - Parameter request: リクエスト
private func handleBoardViewUpdateRequest(_ request: DetailedRequest<BoardUpdate>) {
switch request.detail {
case .withAnimation(let cellChange):
boardView.setDisk(cellChange.disk, atX: cellChange.x, y: cellChange.y, animated: true) { [weak self] result in
self?.viewModel.boardViewUpdateCompleted(requestId: request.requestId)
}
case .withoutAnimation(let cellChanges):
for change in cellChanges {
boardView.setDisk(change.disk, atX: change.x, y: change.y, animated: false)
}
viewModel.boardViewUpdateCompleted(requestId: request.requestId)
}
}
/// アラート表示のリクエストをハンドリングします。
/// - Parameter request: リクエスト
private func handleShowAlertRequest(_ request: ViewModel.AlertRequest) {
let alertController = UIAlertController(title: request.title, message: request.message, preferredStyle: .alert)
for action in request.actions.enumerated() {
alertController.addAction(UIAlertAction(title: action.element.title, style: action.element.style) { [weak self] _ in
self?.viewModel.alertActionSelected(requestId: request.requestId, selectedIndex: action.offset)
})
}
present(alertController, animated: true)
}
/// セーブリクエストをハンドリングします。
/// - Parameter request: リクエスト
private func handleSaveRequest(_ request: DetailedRequest<String>) {
try? saveGame(data: request.detail)
viewModel.saveCompleted(requestId: request.requestId)
}
}
// MARK: Inputs
extension ViewController {
/// リセットボタンが押された場合に呼ばれるハンドラーです。
/// アラートを表示して、ゲームを初期化して良いか確認し、
/// "OK" が選択された場合ゲームを初期化します。
@IBAction func pressResetButton(_ sender: UIButton) {
viewModel.pressResetButton()
}
/// プレイヤーのモードが変更された場合に呼ばれるハンドラーです。
@IBAction func changePlayerControlSegment(_ sender: UISegmentedControl) {
let side = playerControls.firstIndex(of: sender)!
viewModel.changePlayerControlSegment(side: side, selectedIndex: sender.selectedSegmentIndex)
}
}
extension ViewController: BoardViewDelegate {
/// `boardView` の `x`, `y` で指定されるセルがタップされたときに呼ばれます。
/// - Parameter boardView: セルをタップされた `BoardView` インスタンスです。
/// - Parameter x: セルの列です。
/// - Parameter y: セルの行です。
func boardView(_ boardView: BoardView, didSelectCellAtX x: Int, y: Int) {
viewModel.boardViewDidSelectCell(x: x, y: y)
}
}
// MARK: Save and Load
extension ViewController {
private var path: String {
(NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first! as NSString).appendingPathComponent("Game")
}
/// ゲームの状態をファイルに書き出し、保存します。
func saveGame(data: String) throws {
do {
try data.write(toFile: path, atomically: true, encoding: .utf8)
} catch let error {
throw FileIOError.write(path: path, cause: error)
}
}
/// ゲームの状態をファイルから読み込み、復元します。
func loadGame() throws -> String {
let input: String
do {
input = try String(contentsOfFile: path, encoding: .utf8)
} catch let error {
throw FileIOError.read(path: path, cause: error)
}
return input
}
enum FileIOError: Error {
case write(path: String, cause: Error?)
case read(path: String, cause: Error?)
}
}