前言
筆者是swift自學新手,希望借助閱讀別人開源項目提升自己swift水平。文中將盡量使用文字描述來代替代碼的堆砌,建議讀者多參考源碼,以便更好理解項目。文中難免有錯誤之處,歡迎各路大牛留言指正。
項目信息
swift-2048 github地址
該項目可以說一個帶有實驗學習性質的項目,其中部分功能沒有實現(xiàn)或不完整。但2048游戲的基本功能均完整實現(xiàn)。筆者將分3篇文章,分別按controller、model、view的進行介紹。
本篇是最后一篇,將重點展開介紹view部分。
以往文章:
第1篇-controller篇
第2篇-model篇
正文
本文將從以下2點展開說明:
- 文件結構概括
- 游戲盤view
1.文件結構概括
筆者喜歡先從文件結構看起。該項目的view部分,有下面4個文件組成:
views/AccessoryViews.swift //輔助的views.里面含顯示得分ScoreView和用來控制作用的ControlView(未實現(xiàn)也未使用)
views/GameboardView.swift //游戲盤view
views/TileView.swift //棋子view
AppearanceProvider.swift //外觀的提供者,按規(guī)則顯示提供顏色和字體大小
2.游戲盤view(GameboardView)
GameboardView代表游戲盤,TileView代表棋子。
GameboardView源碼中
class GameboardView : UIView {
...
var tiles: Dictionary<NSIndexPath, TileView>
...
}
源碼可見,GameboardView是以Dictionary結構來存儲TileView。Dictionary中的key,是NSIndexPath,實際運用中將位置坐標x y(或raw col)轉成NSIndexPath,例如:
tiles[NSIndexPath(forRow: row, inSection: col)] = tile //設置位置坐標為(row,col)上的TileView
各位移步看TileView的內部:
class TileView : UIView {
...
let numberLabel : UILabel//顯示數(shù)字的label
var value : Int = 0 {
didSet {
backgroundColor = delegate.tileColor(value)//棋子的背景
numberLabel.textColor = delegate.numberColor(value)//數(shù)字的顏色
numberLabel.text = "\(value)"http://值
}
}
unowned let delegate : AppearanceProviderProtocol //顯示信息委托
...
}
TileView不復雜,UIView中是一個UILabel。然后數(shù)字和棋子背景因value不同而不同。顯示的規(guī)則來自AppearanceProviderProtocol。
protocol AppearanceProviderProtocol: class {
func tileColor(value: Int) -> UIColor //根據(jù)值,返回棋子的背景顏色
func numberColor(value: Int) -> UIColor//根據(jù)值,返回棋子的數(shù)字顏色
func fontForNumbers() -> UIFont //返回數(shù)字使用的字體
}
該協(xié)議的的實現(xiàn),按典型的switch-case的結構,有多少情況,只要預先設定好,即可:
class AppearanceProvider: AppearanceProviderProtocol {
func tileColor(value: Int) -> UIColor {
switch value {
case 2:
return UIColor(red: 238.0/255.0, green: 228.0/255.0, blue: 218.0/255.0, alpha: 1.0)
case 4:
return UIColor(red: 237.0/255.0, green: 224.0/255.0, blue: 200.0/255.0, alpha: 1.0)
...
}
}
...
}
那開發(fā)者為何使用AppearanceProviderProtocol呢?筆者猜想,這樣可以將顯示屬性(顏色、字體)相關的邏輯從TileView的邏輯中獨立出來,便于統(tǒng)一修改和調試。
讀過上一篇model篇的讀者,會發(fā)現(xiàn)這里的GameboardView-TileView與model中的SquareGameboard-TileObject比較相似,但是2者還是有本質上的差別:
- 相同點:2者都在各自領域(view和model)中,表示游戲盤和棋子
- 不同點:在model中,SquareGameboard組織TileObject的方式是數(shù)組。
struct SquareGameboard<T> {
...
var boardArray : [T]//實際使用過程中,泛型使用TileObject代替
...
}
在view中,GameboardView組織TileView的方式是Dictionary
class GameboardView : UIView {
...
var tiles: Dictionary<NSIndexPath, TileView>
...
}
為何是這樣?筆者認為是model和view關注點不同
- model關注整個游戲的邏輯,即游戲盤中的每一個有效的位置都要被管理起來。TileObject不僅代表有數(shù)字的棋子,也代表空棋子。所以,游戲盤中所有棋子(位置)是連續(xù)的,可以用數(shù)組表示。
- view關注顯示,即只關心游戲盤中有數(shù)字的棋子(移動、插入等)。TileView只表示有數(shù)字的格子,不表示空棋子(空位置)。在游戲過程中,有數(shù)字的棋子數(shù)量小于游戲盤中棋子位置的總數(shù)量,而且棋子與棋子之間沒有連續(xù)的關系。故而使用數(shù)組就不適合了,而采用Dictionary,并用位置坐標(NSIndexPath)做key就比較合理。
查看GameboardView的代碼,你會發(fā)現(xiàn)GameboardView沒有配套的委托協(xié)議。
沒有委托協(xié)議意味著:只存在viewController主動調用(通知)GameboardView,反過來GameboardView不需要通知viewController。進一步講,GameboardView只負責顯示,不負責與用戶交互。(看過前面文章的讀者應該會記得,該項目的用戶交互是由滑動手勢發(fā)起的)
筆者統(tǒng)計,GameboardView被controller調用的方法是以下4個:
b.reset()
b.moveOneTile(from, to: to, value: value)
b.moveTwoTiles(from, to: to, value: value)
b.insertTile(location, value: value)
除了reset之外,其他3個方法,基本和model的協(xié)議是一樣的。因為controller就只負責以簡單方式通知view(代碼上,就是直接調用view的方法)。以移動一個棋子的委托方法為例(model委托的分析,請參見model部分的文章)
func moveOneTile(from: (Int, Int), to: (Int, Int), value: Int) {//controller實現(xiàn)的model的委托
assert(board != nil)//board就是GameboardView
let b = board!
b.moveOneTile(from, to: to, value: value)// 直接調用view的方法
}
下面是moveOneTile方法:
func moveOneTile(from: (Int, Int), to: (Int, Int), value: Int) {
...(略,檢查參數(shù)代碼)
let (fromRow, fromCol) = from
let (toRow, toCol) = to
let fromKey = NSIndexPath(forRow: fromRow, inSection: fromCol)
let toKey = NSIndexPath(forRow: toRow, inSection: toCol)
//上面是得到棋子view的key
guard let tile = tiles[fromKey] else {
assert(false, "placeholder error")
}//得到舊位置的棋子(舊位置上一定會有棋子)
let endTile = tiles[toKey]//得到新位置的棋子,可能不存在(單棋子移動分成移動和合并2種情況。只有合并情況才有新位置上的棋子。具體見model篇)
var finalFrame = tile.frame
finalFrame.origin.x = tilePadding + CGFloat(toCol)*(tileWidth + tilePadding)
finalFrame.origin.y = tilePadding + CGFloat(toRow)*(tileWidth + tilePadding)
//舊位置的frame,計算成新位置的frame
tiles.removeValueForKey(fromKey)
tiles[toKey] = tile
//然后在字典中更新,刪除舊的,在放進新的
...(略,移動動畫和pop動畫,有興趣可查閱源碼)
}
用文字說明流程:
1》參數(shù)是坐標,轉成NSIndexPath,在字典中找到棋子view
2》tiles字典更新,將原來位置的棋子從字典原來位置刪除,覆蓋到新位置
3》原來位置的棋子view,計算新位置的frame,使用動畫移動到新位置。新位子原來的view刪除。如果是合并(新位置原來有格子),需要pop動畫
另外2個方法也是差不多的套路:
moveTwoTiles 移動2個格子,過程與前面基本一致:
1》參數(shù)是坐標,轉成NSIndexPath,找到棋子view(2個原來位置的棋子)
2》tiles字典更新:將一個原始棋子view從字典原來位置刪除,覆蓋到新位置。另外一個原始位置的棋子view從字典中刪除
3》計算新位置棋子的frame。然后2個原來位置的棋子,動畫移動到新的位置,然后刪除一個,只保留一個。并顯示pop動畫
insertTile 新增格子
1》參數(shù)是坐標,轉成NSIndexPath。
2》計算frame,創(chuàng)建新的TileView。加入游戲盤view,然后插入字典中
3》動畫顯示。
總結
筆者經(jīng)過分析model時的邏輯洗禮,再分析view部分時,頭腦就清晰多了。
處理model的委托時,基本就是2步:1》修改保存棋子view的字典。2》移動棋子view
(該項目的view部分還有一些處理邏輯文中沒有提到(例如游戲盤背景的顯示、棋子動畫,得分view等),有興趣的讀者可自行查看源碼了解。