讀源碼系列(swift2048)-view篇

前言

筆者是swift自學(xué)新手,希望借助閱讀別人開源項(xiàng)目提升自己swift水平。文中將盡量使用文字描述來代替代碼的堆砌,建議讀者多參考源碼,以便更好理解項(xiàng)目。文中難免有錯(cuò)誤之處,歡迎各路大牛留言指正。

項(xiàng)目信息

swift-2048 github地址

項(xiàng)目主界面

該項(xiàng)目可以說一個(gè)帶有實(shí)驗(yàn)學(xué)習(xí)性質(zhì)的項(xiàng)目,其中部分功能沒有實(shí)現(xiàn)或不完整。但2048游戲的基本功能均完整實(shí)現(xiàn)。筆者將分3篇文章,分別按controller、model、view的進(jìn)行介紹。

本篇是最后一篇,將重點(diǎn)展開介紹view部分。
以往文章:
第1篇-controller篇
第2篇-model篇

正文

本文將從以下2點(diǎn)展開說明:

  1. 文件結(jié)構(gòu)概括
  2. 游戲盤view

1.文件結(jié)構(gòu)概括

筆者喜歡先從文件結(jié)構(gòu)看起。該項(xiàng)目的view部分,有下面4個(gè)文件組成:

views/AccessoryViews.swift //輔助的views.里面含顯示得分ScoreView和用來控制作用的ControlView(未實(shí)現(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)來存儲(chǔ)TileView。Dictionary中的key,是NSIndexPath,實(shí)際運(yùn)用中將位置坐標(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中是一個(gè)UILabel。然后數(shù)字和棋子背景因value不同而不同。顯示的規(guī)則來自AppearanceProviderProtocol。

protocol AppearanceProviderProtocol: class {
  func tileColor(value: Int) -> UIColor //根據(jù)值,返回棋子的背景顏色
  func numberColor(value: Int) -> UIColor//根據(jù)值,返回棋子的數(shù)字顏色
  func fontForNumbers() -> UIFont //返回?cái)?shù)字使用的字體
}

該協(xié)議的的實(shí)現(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的邏輯中獨(dú)立出來,便于統(tǒng)一修改和調(diào)試。


讀過上一篇model篇的讀者,會(huì)發(fā)現(xiàn)這里的GameboardView-TileView與model中的SquareGameboard-TileObject比較相似,但是2者還是有本質(zhì)上的差別:

  • 相同點(diǎn):2者都在各自領(lǐng)域(view和model)中,表示游戲盤和棋子
  • 不同點(diǎn):在model中,SquareGameboard組織TileObject的方式是數(shù)組。
struct SquareGameboard<T> {
    ...
    var boardArray : [T]//實(shí)際使用過程中,泛型使用TileObject代替
    ...
}

在view中,GameboardView組織TileView的方式是Dictionary

class GameboardView : UIView {
    ...
    var tiles: Dictionary<NSIndexPath, TileView>
    ...
}

為何是這樣?筆者認(rèn)為是model和view關(guān)注點(diǎn)不同

  • model關(guān)注整個(gè)游戲的邏輯,即游戲盤中的每一個(gè)有效的位置都要被管理起來。TileObject不僅代表有數(shù)字的棋子,也代表空棋子。所以,游戲盤中所有棋子(位置)是連續(xù)的,可以用數(shù)組表示。
  • view關(guān)注顯示,即只關(guān)心游戲盤中有數(shù)字的棋子(移動(dòng)、插入等)。TileView只表示有數(shù)字的格子,不表示空棋子(空位置)。在游戲過程中,有數(shù)字的棋子數(shù)量小于游戲盤中棋子位置的總數(shù)量,而且棋子與棋子之間沒有連續(xù)的關(guān)系。故而使用數(shù)組就不適合了,而采用Dictionary,并用位置坐標(biāo)(NSIndexPath)做key就比較合理。

查看GameboardView的代碼,你會(huì)發(fā)現(xiàn)GameboardView沒有配套的委托協(xié)議。
沒有委托協(xié)議意味著:只存在viewController主動(dòng)調(diào)用(通知)GameboardView,反過來GameboardView不需要通知viewController。進(jìn)一步講,GameboardView只負(fù)責(zé)顯示,不負(fù)責(zé)與用戶交互。(看過前面文章的讀者應(yīng)該會(huì)記得,該項(xiàng)目的用戶交互是由滑動(dòng)手勢(shì)發(fā)起的)


筆者統(tǒng)計(jì),GameboardView被controller調(diào)用的方法是以下4個(gè):

b.reset()
b.moveOneTile(from, to: to, value: value)
b.moveTwoTiles(from, to: to, value: value)
b.insertTile(location, value: value)

除了reset之外,其他3個(gè)方法,基本和model的協(xié)議是一樣的。因?yàn)閏ontroller就只負(fù)責(zé)以簡(jiǎn)單方式通知view(代碼上,就是直接調(diào)用view的方法)。以移動(dòng)一個(gè)棋子的委托方法為例(model委托的分析,請(qǐng)參見model部分的文章)

  func moveOneTile(from: (Int, Int), to: (Int, Int), value: Int) {//controller實(shí)現(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")
    }//得到舊位置的棋子(舊位置上一定會(huì)有棋子)
    let endTile = tiles[toKey]//得到新位置的棋子,可能不存在(單棋子移動(dòng)分成移動(dòng)和合并2種情況。只有合并情況才有新位置上的棋子。具體見model篇)

    var finalFrame = tile.frame
    finalFrame.origin.x = tilePadding + CGFloat(toCol)*(tileWidth + tilePadding)
    finalFrame.origin.y = tilePadding + CGFloat(toRow)*(tileWidth + tilePadding)
    //舊位置的frame,計(jì)算成新位置的frame

    tiles.removeValueForKey(fromKey)
    tiles[toKey] = tile
    //然后在字典中更新,刪除舊的,在放進(jìn)新的

    ...(略,移動(dòng)動(dòng)畫和pop動(dòng)畫,有興趣可查閱源碼)
  }

用文字說明流程:

1》參數(shù)是坐標(biāo),轉(zhuǎn)成NSIndexPath,在字典中找到棋子view
2》tiles字典更新,將原來位置的棋子從字典原來位置刪除,覆蓋到新位置
3》原來位置的棋子view,計(jì)算新位置的frame,使用動(dòng)畫移動(dòng)到新位置。新位子原來的view刪除。如果是合并(新位置原來有格子),需要pop動(dòng)畫

另外2個(gè)方法也是差不多的套路:
moveTwoTiles 移動(dòng)2個(gè)格子,過程與前面基本一致:

1》參數(shù)是坐標(biāo),轉(zhuǎn)成NSIndexPath,找到棋子view(2個(gè)原來位置的棋子)
2》tiles字典更新:將一個(gè)原始棋子view從字典原來位置刪除,覆蓋到新位置。另外一個(gè)原始位置的棋子view從字典中刪除
3》計(jì)算新位置棋子的frame。然后2個(gè)原來位置的棋子,動(dòng)畫移動(dòng)到新的位置,然后刪除一個(gè),只保留一個(gè)。并顯示pop動(dòng)畫

insertTile 新增格子

1》參數(shù)是坐標(biāo),轉(zhuǎn)成NSIndexPath。
2》計(jì)算frame,創(chuàng)建新的TileView。加入游戲盤view,然后插入字典中
3》動(dòng)畫顯示。


總結(jié)

筆者經(jīng)過分析model時(shí)的邏輯洗禮,再分析view部分時(shí),頭腦就清晰多了。
處理model的委托時(shí),基本就是2步:1》修改保存棋子view的字典。2》移動(dòng)棋子view

(該項(xiàng)目的view部分還有一些處理邏輯文中沒有提到(例如游戲盤背景的顯示、棋子動(dòng)畫,得分view等),有興趣的讀者可自行查看源碼了解。

非常感謝您的閱讀!您的留言、打賞、點(diǎn)贊、關(guān)注、分享,對(duì)筆者最大的鼓勵(lì):P

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,305評(píng)論 4 61
  • 前言 筆者是swift自學(xué)新手,希望借助閱讀別人開源項(xiàng)目提升自己swift水平。文中將盡量使用文字描述來代替代碼的...
    安靜的貓咪先生閱讀 1,142評(píng)論 3 4
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,034評(píng)論 25 709
  • 今年國(guó)慶對(duì)于我來說卻有不同! 前天女兒過了周歲生日,哺乳期間多一個(gè)小時(shí)的哺乳假結(jié)束了!從今天開始我中午...
    芝芝1981閱讀 207評(píng)論 4 2
  • 小學(xué)二年級(jí)的時(shí)候,我就離開了老家的安樂窩——村校,轉(zhuǎn)到鄉(xiāng)鎮(zhèn)的小學(xué)去讀書了,同時(shí)也寫下了我學(xué)生生涯的唯一光環(huán)—...
    alexader狗閱讀 295評(píng)論 0 0

友情鏈接更多精彩內(nèi)容