前言
筆者是swift自學(xué)新手,希望借助閱讀別人開源項目提升自己swift水平耍共。文中將盡量使用文字描述來代替代碼的堆砌烫饼,建議讀者多參考源碼,以便更好理解項目试读。文中難免有錯誤之處杠纵,歡迎各路大牛留言指正。
項目信息
swift-2048 github地址
該項目可以說一個帶有實驗學(xué)習(xí)性質(zhì)的項目钩骇,其中部分功能沒有實現(xiàn)或不完整比藻。但2048游戲的基本功能均完整實現(xiàn)铝量。筆者將分3篇文章,分別按controller银亲、model慢叨、view的進(jìn)行介紹。
本篇是最后一篇群凶,將重點展開介紹view部分插爹。
以往文章:
第1篇-controller篇
第2篇-model篇
正文
本文將從以下2點展開說明:
- 文件結(jié)構(gòu)概括
- 游戲盤view
1.文件結(jié)構(gòu)概括
筆者喜歡先從文件結(jié)構(gòu)看起哄辣。該項目的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結(jié)構(gòu)來存儲TileView。Dictionary中的key当窗,是NSIndexPath够坐,實際運用中將位置坐標(biāo)x y(或raw col)轉(zhuǎn)成NSIndexPath,例如:
tiles[NSIndexPath(forRow: row, inSection: col)] = tile //設(shè)置位置坐標(biāo)為(row崖面,col)上的TileView
各位移步看TileView的內(nèi)部:
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不復(fù)雜元咙,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的結(jié)構(gòu)简识,有多少情況赶掖,只要預(yù)先設(shè)定好,即可:
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呢七扰?筆者猜想奢赂,這樣可以將顯示屬性(顏色、字體)相關(guān)的邏輯從TileView的邏輯中獨立出來颈走,便于統(tǒng)一修改和調(diào)試膳灶。
讀過上一篇model篇的讀者,會發(fā)現(xiàn)這里的GameboardView-TileView與model中的SquareGameboard-TileObject比較相似立由,但是2者還是有本質(zhì)上的差別:
- 相同點:2者都在各自領(lǐng)域(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>
...
}
為何是這樣?筆者認(rèn)為是model和view關(guān)注點不同
- model關(guān)注整個游戲的邏輯枣耀,即游戲盤中的每一個有效的位置都要被管理起來霉晕。TileObject不僅代表有數(shù)字的棋子庭再,也代表空棋子。所以牺堰,游戲盤中所有棋子(位置)是連續(xù)的拄轻,可以用數(shù)組表示。
- view關(guān)注顯示伟葫,即只關(guān)心游戲盤中有數(shù)字的棋子(移動恨搓、插入等)。TileView只表示有數(shù)字的格子筏养,不表示空棋子(空位置)斧抱。在游戲過程中,有數(shù)字的棋子數(shù)量小于游戲盤中棋子位置的總數(shù)量渐溶,而且棋子與棋子之間沒有連續(xù)的關(guān)系辉浦。故而使用數(shù)組就不適合了,而采用Dictionary茎辐,并用位置坐標(biāo)(NSIndexPath)做key就比較合理宪郊。
查看GameboardView的代碼,你會發(fā)現(xiàn)GameboardView沒有配套的委托協(xié)議拖陆。
沒有委托協(xié)議意味著:只存在viewController主動調(diào)用(通知)GameboardView弛槐,反過來GameboardView不需要通知viewController。進(jìn)一步講依啰,GameboardView只負(fù)責(zé)顯示乎串,不負(fù)責(zé)與用戶交互。(看過前面文章的讀者應(yīng)該會記得孔飒,該項目的用戶交互是由滑動手勢發(fā)起的)
筆者統(tǒng)計灌闺,GameboardView被controller調(diào)用的方法是以下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就只負(fù)責(zé)以簡單方式通知view(代碼上,就是直接調(diào)用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)// 直接調(diào)用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
//然后在字典中更新父腕,刪除舊的,在放進(jìn)新的
...(略青瀑,移動動畫和pop動畫璧亮,有興趣可查閱源碼)
}
用文字說明流程:
1》參數(shù)是坐標(biāo)萧诫,轉(zhuǎn)成NSIndexPath,在字典中找到棋子view
2》tiles字典更新枝嘶,將原來位置的棋子從字典原來位置刪除帘饶,覆蓋到新位置
3》原來位置的棋子view,計算新位置的frame群扶,使用動畫移動到新位置及刻。新位子原來的view刪除。如果是合并(新位置原來有格子)竞阐,需要pop動畫
另外2個方法也是差不多的套路:
moveTwoTiles 移動2個格子,過程與前面基本一致:
1》參數(shù)是坐標(biāo)缴饭,轉(zhuǎn)成NSIndexPath,找到棋子view(2個原來位置的棋子)
2》tiles字典更新:將一個原始棋子view從字典原來位置刪除馁菜,覆蓋到新位置茴扁。另外一個原始位置的棋子view從字典中刪除
3》計算新位置棋子的frame铃岔。然后2個原來位置的棋子汪疮,動畫移動到新的位置,然后刪除一個毁习,只保留一個智嚷。并顯示pop動畫
insertTile 新增格子
1》參數(shù)是坐標(biāo),轉(zhuǎn)成NSIndexPath纺且。
2》計算frame盏道,創(chuàng)建新的TileView。加入游戲盤view载碌,然后插入字典中
3》動畫顯示猜嘱。
總結(jié)
筆者經(jīng)過分析model時的邏輯洗禮,再分析view部分時嫁艇,頭腦就清晰多了朗伶。
處理model的委托時,基本就是2步:1》修改保存棋子view的字典步咪。2》移動棋子view
(該項目的view部分還有一些處理邏輯文中沒有提到(例如游戲盤背景的顯示论皆、棋子動畫,得分view等)猾漫,有興趣的讀者可自行查看源碼了解点晴。