读源码系列(swift2048)-view篇

前言

笔者是swift自学新手,希望借助阅读别人开源项目提升自己swift水平。文中将尽量使用文字描述来代替代码的堆砌,建议读者多参考源码,以便更好理解项目。文中难免有错误之处,欢迎各路大牛留言指正。

项目信息

swift-2048 github地址

项目主界面

该项目可以说一个带有实验学习性质的项目,其中部分功能没有实现或不完整。但2048游戏的基本功能均完整实现。笔者将分3篇文章,分别按controller、model、view的进行介绍。

本篇是最后一篇,将重点展开介绍view部分。
以往文章:
第1篇-controller篇
第2篇-model篇

正文

本文将从以下2点展开说明:

  1. 文件结构概括
  2. 游戏盘view

1.文件结构概括

笔者喜欢先从文件结构看起。该项目的view部分,有下面4个文件组成:

views/AccessoryViews.swift //辅助的views.里面含显示得分ScoreView和用来控制作用的ControlView(未实现也未使用)
views/GameboardView.swift //游戏盘view
views/TileView.swift //棋子view
AppearanceProvider.swift //外观的提供者,按规则显示提供颜色和字体大小

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//显示数字的label
    var value : Int = 0 {
    didSet {
        backgroundColor = delegate.tileColor(value)//棋子的背景
        numberLabel.textColor = delegate.numberColor(value)//数字的颜色
        numberLabel.text = "\(value)"//值
        }
    }
    unowned let delegate : AppearanceProviderProtocol //显示信息委托
    ...
}

TileView不复杂,UIView中是一个UILabel。然后数字和棋子背景因value不同而不同。显示的规则来自AppearanceProviderProtocol。

protocol AppearanceProviderProtocol: class {
  func tileColor(value: Int) -> UIColor //根据值,返回棋子的背景颜色
  func numberColor(value: Int) -> UIColor//根据值,返回棋子的数字颜色
  func fontForNumbers() -> UIFont //返回数字使用的字体
}

该协议的的实现,按典型的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)
        ...
    }
  }
  ...
}

那开发者为何使用AppearanceProviderProtocol呢?笔者猜想,这样可以将显示属性(颜色、字体)相关的逻辑从TileView的逻辑中独立出来,便于统一修改和调试。


读过上一篇model篇的读者,会发现这里的GameboardView-TileView与model中的SquareGameboard-TileObject比较相似,但是2者还是有本质上的差别:

  • 相同点:2者都在各自领域(view和model)中,表示游戏盘和棋子
  • 不同点:在model中,SquareGameboard组织TileObject的方式是数组。
struct SquareGameboard<T> {
    ...
    var boardArray : [T]//实际使用过程中,泛型使用TileObject代替
    ...
}

在view中,GameboardView组织TileView的方式是Dictionary

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

为何是这样?笔者认为是model和view关注点不同

  • model关注整个游戏的逻辑,即游戏盘中的每一个有效的位置都要被管理起来。TileObject不仅代表有数字的棋子,也代表空棋子。所以,游戏盘中所有棋子(位置)是连续的,可以用数组表示。
  • view关注显示,即只关心游戏盘中有数字的棋子(移动、插入等)。TileView只表示有数字的格子,不表示空棋子(空位置)。在游戏过程中,有数字的棋子数量小于游戏盘中棋子位置的总数量,而且棋子与棋子之间没有连续的关系。故而使用数组就不适合了,而采用Dictionary,并用位置坐标(NSIndexPath)做key就比较合理。

查看GameboardView的代码,你会发现GameboardView没有配套的委托协议。
没有委托协议意味着:只存在viewController主动调用(通知)GameboardView,反过来GameboardView不需要通知viewController。进一步讲,GameboardView只负责显示,不负责与用户交互。(看过前面文章的读者应该会记得,该项目的用户交互是由滑动手势发起的)


笔者统计,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的协议是一样的。因为controller就只负责以简单方式通知view(代码上,就是直接调用view的方法)。以移动一个棋子的委托方法为例(model委托的分析,请参见model部分的文章)

  func moveOneTile(from: (Int, Int), to: (Int, Int), value: Int) {//controller实现的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) {
    ...(略,检查参数代码)
    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》参数是坐标,转成NSIndexPath,在字典中找到棋子view
2》tiles字典更新,将原来位置的棋子从字典原来位置删除,覆盖到新位置
3》原来位置的棋子view,计算新位置的frame,使用动画移动到新位置。新位子原来的view删除。如果是合并(新位置原来有格子),需要pop动画

另外2个方法也是差不多的套路:
moveTwoTiles 移动2个格子,过程与前面基本一致:

1》参数是坐标,转成NSIndexPath,找到棋子view(2个原来位置的棋子)
2》tiles字典更新:将一个原始棋子view从字典原来位置删除,覆盖到新位置。另外一个原始位置的棋子view从字典中删除
3》计算新位置棋子的frame。然后2个原来位置的棋子,动画移动到新的位置,然后删除一个,只保留一个。并显示pop动画

insertTile 新增格子

1》参数是坐标,转成NSIndexPath。
2》计算frame,创建新的TileView。加入游戏盘view,然后插入字典中
3》动画显示。


总结

笔者经过分析model时的逻辑洗礼,再分析view部分时,头脑就清晰多了。
处理model的委托时,基本就是2步:1》修改保存棋子view的字典。2》移动棋子view

(该项目的view部分还有一些处理逻辑文中没有提到(例如游戏盘背景的显示、棋子动画,得分view等),有兴趣的读者可自行查看源码了解。

非常感谢您的阅读!您的留言、打赏、点赞、关注、分享,对笔者最大的鼓励:P

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 前言 笔者是swift自学新手,希望借助阅读别人开源项目提升自己swift水平。文中将尽量使用文字描述来代替代码的...
    安静的猫咪先生阅读 1,023评论 3 4
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,446评论 25 707
  • 今年国庆对于我来说却有不同! 前天女儿过了周岁生日,哺乳期间多一个小时的哺乳假结束了!从今天开始我中午...
    芝芝1981阅读 176评论 4 2
  • 小学二年级的时候,我就离开了老家的安乐窝——村校,转到乡镇的小学去读书了,同时也写下了我学生生涯的唯一光环—...
    alexader狗阅读 263评论 0 0