点击上方“
程序员大咖
”,选择“置顶公众号”
关键时刻,第一时间送达!
当工期比较紧的时候,项目开发中会经常出现移动端等待后端接口数据的情形,不但耽误项目进度,更让人有种无奈的绝望。所以在开发中,我们常常自己做些假数据,以方便开发和UI调试。然而做假数据方法不同,效率和安全性都各不同,有时稍有不慎,还会产生很大的bug。因此本文拟结合我在贝聊的开发经验,讲一讲我们组在iOS开发中曾经用过的做假数据的方法及其优劣。
为方便下文的说明,本文主要以贝聊家长版app发现首页的热门帖子列表的实现为例。热门帖子列表的样式如下图:
这是比较常见的列表,用常用的UITableView实现即可,但需要自定义一个的UITableViewCell的子类ExploreTableViewCell。项目中,ExploreTableViewCell并没有用xib实现(因为xib日后不好修改,且代码复用性差),而是通过SnapKit用纯代码布的局,具体的布局代码大致如下:
import UIKit
import SnapKit
class ExploreTableViewCell: UITableViewCell {
let thumbnailImageView: UIImageView
let titleLabel: UILabel
let avatarImageView: UIImageView
let authorNameLabel: UILabel
let viewCountLabel: UILabel
let commentCountLabel: UILabel
//...其他属性
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
//创建view
thumbnailImageView = UIImageView()
titleLabel = UILabel()
avatarImageView = UIImageView()
authorNameLabel = UILabel()
viewCountLabel = UILabel()
commentCountLabel = UILabel()
//...其他view的创建
super.init(style: style, reuseIdentifier: reuseIdentifier)
//设置view
titleLabel.numberOfLines = 2
titleLabel.textColor = UIColor.black
titleLabel.snp.makeConstraints { (make) -> Void in
make.left.equalTo(thumbnailImageView.snp.right).offset(15)
make.right.equalTo(contentView.snp.right)
make.top.equalTo(contentView.snp.top)
}
//...其他view的设置
}
//...其他业务代码
}
源码中写死数据是最便捷的假数据做法,项目很赶时,为最快速的看到UI效果,一般都会采取这种假数据方式。比如在上述热门帖子列表示例项目中,为查看整个ExploreTableViewCell的布局效果,在titleLabel等subview的设置位置,直接写死假数据。
//...其他代码
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
//...其他初始化代码
//写死的假数据代码
titleLabel.text = "这是一个标题这是一个标题这是一个标题这是一个标题这"
thumbnailImageView.image = UIImage(named:"sampleImage")
avatarImageView.image = UIImage(named:"sampleImage")
authorNameLabel.text = "作者名"
viewCountLabel.text = "1000"
commentCountLabel.text = "1000"
//...其他初始化代码
}
//...其他代码
源码中写死假数据虽然方便,但稍有不慎就容易直接上线上环境(因为测试在测试时一般都会有数据,假数据被遮盖了),演变成一个有可能非常严重也有可能很轻的bug(贝聊就切实出现过这样的bug,而且还是个影响广泛的大bug),为安全起见,所有写死的假数据都应该包在条件编译宏内。
//...其他代码
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
//...其他初始化代码
//写死的假数据代码,包裹在条件编译宏内
#if DEBUG
titleLabel.text = "这是一个标题这是一个标题这是一个标题这是一个标题这"
thumbnailImageView.image = UIImage(named:"sampleImage")
avatarImageView.image = UIImage(named:"sampleImage")
authorNameLabel.text = "作者名"
viewCountLabel.text = "1000"
commentCountLabel.text = "1000"
#endif
//...其他初始化代码
}
//...其他代码
包在条件编译宏内,就可以保证不污染正式环境的代码,从而保证安全性。
在源码中写死假数据,有以下三个缺点:
假数据写在源代码中,即使用宏包裹起来,只是保证了一定的安全性,但依然污染了源代码,如果上线前忘了把假数据代码移除,它一直会残留在源代码中,而且还会一直影响DEBUG环境的调试。
本文示例代码的假数据虽然写在一块,但在实际开发中,并不是所有的UI代码都在一个文件中。即使在一个文件内,往往各个属性的初始化和设置也不在一个方法内。代码一多,基本很难管理。
正确的数据产生方式,应该是发一个网络请求,然后把请求回来的数据转成model,最后通过model给各个UI组件填充数据。而在源代码中写死假数据,直接打乱了数据的正确流通,这会使得整个开发的逻辑是颠倒的,不但使开发更容易出bug,而且逻辑流的切换带来的开发效率和开发感受都很差。
较好的假数据方式,应该尽可能的不污染源代码,不扰乱正常的数据流通,而且能集中管理。在研究单元测试时,我无意中发现stub某个页面请求数据的网络请求即可达到这种完美的假数据效果。
首先按正常的流程开发整个功能,(在开发中,我总是倾下于先创建Model,而不是先写UI)
-
创建Model
-
创建ViewController
-
创建View等UI元素
-
在ViewController中完成网络请求的发起,并完成从网络数据到Model的转换
-
应用Model填充UI
整个功能开发按照有真实网络请求进行,但事实上并没有网络请求,因为后台并未搭好,没关系,先按照后台给出的接口和数据格式定义,创建一个本地JSON文件。对于本文的示例(假定只有列表数据)来说,文件名暂为hotTopics.json,内容大致如下(贝聊发现首页实际上有很多其他元素,网络请求返回的JSON也比这个复杂的多):
{
hotTopics: [
{
"title": "这是一个标题这是一个标题这是一个标题",
"thumbnail": "https://api.beiliao.com/explore/image/fdlafjlfp34523.jpg",
"author": "小黄老师",
"authorAvatar": "https://api.beiliao.com/explore/image/fdlafjlfp34523.jpg",
"commentCount": 1000,
"viewCount": 3000
},
{
"title": "这是另外一个标题这是另外一个标题这是另外一个标题",
"thumbnail": "https://api.beiliao.com/explore/image/fdla32131fjlfp34523.jpg",
"author": "小李老师",
"authorAvatar": "https://api.beiliao.com/explore/image/fdl232afjlfp34523.jpg",
"commentCount": 1030,
"viewCount": 3400
}
]
}
然后在ViewController中stub本ViewController中所有的网络请求,我在开发中用的是OHHTTPStubs,大致的代码如下:
class ExploreViewController: UITableViewController {
//...其他代码
override func viewDidLoad() {
super.viewDidLoad()
//...其他代码
#if DEBUG
stubRequests()
#endif
//...其他代码
}
//...其他代码
func stubRequests() {
stub(isPath("/explore/hotTopics")) { _ in
let stubPath = OHPathForFile("hotTopics.json", type(of: self))
return fixture(filePath: stubPath!, headers: ["Content-Type":"application/json"])
}
}
}
注意所创建的JSON文件一定要加到项目目录中。添加完上述代码后,path为/explore/hotTopics的网络请求将被stub,返回的数据将是所指定JSON文件中的数据,这样就跟真实的网络请求没有任何的区别了。而且利用OHHTTPStubs还可以模拟网络请求失败、网络请求超时以及throttle等各种网络请求状态,从而更全面的调试UI和整个功能。
利用stub做假数据可以真实的做到基本不污染代码、集中管理和完全真实的数据流通流程,与在源码中写死这种方式相比,近乎完美。然而当你真正用过一段时间后,你会发现,这种方式还是有一个致命的缺点和一个不那么重要的缺点。
因为每改动一次数据,都需要重新编译,而iOS编译是很慢的,尤其是Swift。而要想做UI调试,频繁的改动数据,查看各种边界条件下的UI是必然的。
虽然相较上一种方法,污染非常小,但或多或少还是有污染的,有强迫症的人是受不了的,而且有时测试说是个bug(测试包一般是BETA环境),你build一下发现数据是假数据,不是网络请求的数据,还需要找到stub网络请求的位置,然后把代码注释了,也是极其的烦人的。
如果能做到每改动一下数据,然后刷新一下就可以看到了,像网页一样,而且真的一点都不污染代码,那就是完美的解决方案。
如果只是想做到,每改动一下数据,然后刷新以下就可以看到了,像网页一样,Xcode的动态注入是可以的,现在比较流行的是 injectionforxcode和dyci-main两个库。利用单元测试的网络请求stub做假数据,然后结合动态注入,理论上应该可以做到实时刷新,但事实上injectionforxcode和dyci-main的体验都是很糟糕的,时灵时不灵,我用过两次后,基本就不想碰了,我宁愿编译慢一点,当然我从来没有用动态注入去做假数据的实时刷新,但我觉得应该是个方案。
但这个方案即使可行,也还是会污染代码,并不算特别彻底的方案。真正彻底的方案,与用stub拦截网络请求的思路相同,只是要将网络请求的拦截放到整个APP外,有两个方案可行。
第一种就是本地自己搭个服务器,然后把开发时需要拦截的网络请求地址改为自己搭建的服务器地址,然后返回自己自定义的JSON数据。但这种方式也有三个缺点:
虽然搭建服务器是很简单的事,并不是所有人都会,也是需要一定的学习成本的。
这虽然已经把源码污染降到最低了,但毕竟还是有。
综合起来这种方案性价比并不高,但确实有一定的趣味性,毕竟自己折腾东西嘛。
第二种就是利用现有的网络代理软件,直接拦截对应的网络请求,然后返回本地写好的JSON数据。我最终采用的这种方案(因为我嫌配置服务器麻烦)。将APP中所有的网络请求都代理给网络代理,然后指定特定的网络请求返回本地JSON数据。这种方案的好处不言而喻,