forked from refactoring-challenge/reversi-ios
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BoardView.swift
177 lines (149 loc) · 7.17 KB
/
BoardView.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
import ReversiLogic
import UIKit
private let lineWidth: CGFloat = 2
public class BoardView: UIView {
private var cellViews: [CellView] = []
private var actions: [CellSelectionAction] = []
/// 盤の幅( `8` )を表します。
public let width: Int = 8
/// 盤の高さ( `8` )を返します。
public let height: Int = 8
/// 盤のセルの `x` の範囲( `0 ..< 8` )を返します。
public let xRange: Range<Int>
/// 盤のセルの `y` の範囲( `0 ..< 8` )を返します。
public let yRange: Range<Int>
/// セルがタップされたときの挙動を移譲するためのオブジェクトです。
public weak var delegate: BoardViewDelegate?
override public init(frame: CGRect) {
xRange = 0 ..< width
yRange = 0 ..< height
super.init(frame: frame)
setUp()
}
required public init?(coder: NSCoder) {
xRange = 0 ..< width
yRange = 0 ..< height
super.init(coder: coder)
setUp()
}
private func setUp() {
self.backgroundColor = UIColor(named: "DarkColor")!
let cellViews: [CellView] = (0 ..< (width * height)).map { _ in
let cellView = CellView()
cellView.translatesAutoresizingMaskIntoConstraints = false
return cellView
}
self.cellViews = cellViews
cellViews.forEach(self.addSubview(_:))
for i in cellViews.indices.dropFirst() {
NSLayoutConstraint.activate([
cellViews[0].widthAnchor.constraint(equalTo: cellViews[i].widthAnchor),
cellViews[0].heightAnchor.constraint(equalTo: cellViews[i].heightAnchor),
])
}
NSLayoutConstraint.activate([
cellViews[0].widthAnchor.constraint(equalTo: cellViews[0].heightAnchor),
])
for y in yRange {
for x in xRange {
let topNeighborAnchor: NSLayoutYAxisAnchor
if let cellView = cellViewAt(x: x, y: y - 1) {
topNeighborAnchor = cellView.bottomAnchor
} else {
topNeighborAnchor = self.topAnchor
}
let leftNeighborAnchor: NSLayoutXAxisAnchor
if let cellView = cellViewAt(x: x - 1, y: y) {
leftNeighborAnchor = cellView.rightAnchor
} else {
leftNeighborAnchor = self.leftAnchor
}
let cellView = cellViewAt(x: x, y: y)!
NSLayoutConstraint.activate([
cellView.topAnchor.constraint(equalTo: topNeighborAnchor, constant: lineWidth),
cellView.leftAnchor.constraint(equalTo: leftNeighborAnchor, constant: lineWidth),
])
if y == height - 1 {
NSLayoutConstraint.activate([
self.bottomAnchor.constraint(equalTo: cellView.bottomAnchor, constant: lineWidth),
])
}
if x == width - 1 {
NSLayoutConstraint.activate([
self.rightAnchor.constraint(equalTo: cellView.rightAnchor, constant: lineWidth),
])
}
}
}
reset()
for y in yRange {
for x in xRange {
let cellView: CellView = cellViewAt(x: x, y: y)!
let action = CellSelectionAction(boardView: self, x: x, y: y)
actions.append(action) // To retain the `action`
cellView.addTarget(action, action: #selector(action.selectCell), for: .touchUpInside)
}
}
}
/// 盤をゲーム開始時に状態に戻します。このメソッドはアニメーションを伴いません。
public func reset() {
for y in yRange {
for x in xRange {
setDisk(nil, atX: x, y: y, animated: false)
}
}
setDisk(.light, atX: width / 2 - 1, y: height / 2 - 1, animated: false)
setDisk(.dark, atX: width / 2, y: height / 2 - 1, animated: false)
setDisk(.dark, atX: width / 2 - 1, y: height / 2, animated: false)
setDisk(.light, atX: width / 2, y: height / 2, animated: false)
}
private func cellViewAt(x: Int, y: Int) -> CellView? {
guard xRange.contains(x) && yRange.contains(y) else { return nil }
return cellViews[y * width + x]
}
/// `x`, `y` で指定されたセルの状態を返します。
/// セルにディスクが置かれていない場合、 `nil` が返されます。
/// - Parameter x: セルの列です。
/// - Parameter y: セルの行です。
/// - Returns: セルにディスクが置かれている場合はそのディスクの値を、置かれていない場合は `nil` を返します。
public func diskAt(x: Int, y: Int) -> Disk? {
cellViewAt(x: x, y: y)?.disk
}
/// `x`, `y` で指定されたセルの状態を、与えられた `disk` に変更します。
/// `animated` が `true` の場合、アニメーションが実行されます。
/// アニメーションの完了通知は `completion` ハンドラーで受け取ることができます。
/// - Parameter disk: セルに設定される新しい状態です。 `nil` はディスクが置かれていない状態を表します。
/// - Parameter x: セルの列です。
/// - Parameter y: セルの行です。
/// - Parameter animated: セルの状態変更を表すアニメーションを表示するかどうかを指定します。
/// - Parameter completion: アニメーションの完了通知を受け取るハンドラーです。
/// `animated` に `false` が指定された場合は状態が変更された後で即座に同期的に呼び出されます。
/// ハンドラーが受け取る `Bool` 値は、 `UIView.animate()` 等に準じます。
public func setDisk(_ disk: Disk?, atX x: Int, y: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) {
guard let cellView = cellViewAt(x: x, y: y) else {
preconditionFailure() // FIXME: Add a message.
}
cellView.setDisk(disk, animated: animated, completion: completion)
}
}
public protocol BoardViewDelegate: AnyObject {
/// `boardView` の `x`, `y` で指定されるセルがタップされたときに呼ばれます。
/// - Parameter boardView: セルをタップされた `BoardView` インスタンスです。
/// - Parameter x: セルの列です。
/// - Parameter y: セルの行です。
func boardView(_ boardView: BoardView, didSelectCellAtX x: Int, y: Int)
}
private class CellSelectionAction: NSObject {
private weak var boardView: BoardView?
let x: Int
let y: Int
init(boardView: BoardView, x: Int, y: Int) {
self.boardView = boardView
self.x = x
self.y = y
}
@objc func selectCell() {
guard let boardView = boardView else { return }
boardView.delegate?.boardView(boardView, didSelectCellAtX: x, y: y)
}
}