Swift开发黑科技:还在争论MVC和MVVM?现在你应该试试MVP!
本人原创,长文慎入,但此文绝对不会让你失望。 模型代码: struct Event {
var date = ""
var eventTitle = ""
init(date:String,title:String){
self.date = date
self.eventTitle = title
}
}
struct Festival {
var date = ""
var festivalName = ""
init(date:String,name:String){
self.date = date
self.festivalName = name
}
}
为了简单我都使用了String类型的数据,至于为什么要使用struct而不使用class,大家可以参考WWDC2015的414号视频,讲的非常清楚,我自己的项目中的数据模型已经全部转成struct了,我会在后面专门写博文讲解struct,这里就不赘述了。这里需要啰嗦一下,注意创建的时候使用的是字面量的方法,而不是可选型,我一直认为使用字面量的方法是更好的选择,可选型很容易被当做语法糖滥用。尤其是数据的初始化中,你确定你真的需要一个空值?拿一个空值能做什么?做某种标志位么?请和你的后台开发人员商议,让他给你一个Bool类型的标志位,而不是一个空值。在可能的情况下,给你的数据模型的属性赋一个语义明确的字面量初始值,比如这里我们使用空字符串作为初始值。如果你的数据只是做展示的不会存在修改情况,你也可以使用如下的方法做初始化,以达到效率的最大化: struct Event {
let date:String
let eventTitle:String
init(date:String = "",eventTitle:String = ""){
self.date = date
self.eventTitle = eventTitle
}
}
在Swift1.2版本之后,let定义的数据也支持延迟加载了,这里使用了默认参数值做非空的保障。 class ShowedTableViewCell: UITableViewCell {
//用来展示事件主题或节日名称的Label
@IBOutlet weak var MixLabel: UILabel!
//用来展示日期的Label
@IBOutlet weak var dateLabel: UILabel!
}
MVC架构: var eventList = [Event]()
var festivalList = [Festival]()
let loadedEventList = [Event(date: "2月14",eventTitle: "送礼物")]
let loadedFestivalList = [Festival(date: "1月1日",festivalName: "元旦"),Festival(date: "2月14",festivalName: "情人节")]
这里使用了struct的默认构造器构造对象,有两个节日提醒:元旦节和情人节,元旦节没什么事情做,情人节那天有个事件提醒”送礼物“,我们使用GCD去模拟数据刷新,整个控制器的代码如下: import UIKit
let cellReusedID = "ShowedTableViewCell"
class ShowedTableViewController: UITableViewController {
var eventList = [Event]()
var festivalList = [Festival]()
let loadedEventList = [Event(date: "2月14",eventTitle: "送礼物")]
let loadedFestivalList = [Festival(date: "1月1日",Festival(date: "2月14",festivalName: "情人节")]
override func viewDidLoad() {
super.viewDidLoad()
let delayInSeconds = 2.0
let popTime = dispatch_time(DISPATCH_TIME_NOW,Int64(delayInSeconds * Double(NSEC_PER_SEC)))
dispatch_after(popTime,dispatch_get_main_queue()) { () -> Void in
self.eventList = self.loadedEventList
self.festivalList = self.loadedFestivalList
self.tableView.reloadData()
}
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation,return the number of sections
return 1
}
override func tableView(tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation,return the number of rows
return eventList.count + festivalList.count
}
override func tableView(tableView: UITableView,cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID,forIndexPath: indexPath) as! ShowedTableViewCell
//传统的MVC,你需要在这里处理数据本身的同构与异构情况,还得处理数据与视图的逻辑关系
//这里我们把事件提醒放在节日的前面展示
if indexPath.row > eventList.count - 1{
cell.MixLabel.text = festivalList[indexPath.row - eventList.count].festivalName
cell.dateLabel.text = festivalList[indexPath.row - eventList.count].date
cell.backgroundColor = UIColor.whiteColor()
return cell
} else {
cell.MixLabel.text = eventList[indexPath.row].eventTitle
cell.dateLabel.text = eventList[indexPath.row].date
cell.backgroundColor = UIColor.redColor()
return cell
}
}
}
运行一下看看: 似乎还不错,我们把两个不同的数据结构展现在一张页面上了,并且复用了cell,但是设置cell的代理方法中的代码似乎有点多,而且如果我需要按照时间去排序,那么两个同构的数组作为数据源不好排序,那么重构首先从把同构变成异构开始。由于struct没有继承,按照Swift2.0的精神,此时我们需要提炼两个数据模型的共性,方法是利用protocol,观察到Event和Festival都有date属性,所以写一个协议: protocol hasDate{
var date:String {get}
}
这里这个协议只有一个属性date,Swift协议中定义的属性只有声明,遵守协议的对象必须实现这个属性,但是不限于存储属性还是计算属性。协议中定义的属性必须指定最低的访问级别,这里的date必须是可读的,至于可写的权限取决于实现该协议的数据类型中的定义。由于我们的Event和Festival都具有了date属性,直接让二者遵守hasDate协议,不要用扩展的方式让二者遵守协议,编译器报错的,很怪0 0. import UIKit
let cellReusedID = "ShowedTableViewCell"
class ShowedTableViewController: UITableViewController {
var dataList = [hasDate]()
var loadeddataList:[hasDate] = [Event(date: "2月14",eventTitle: "送礼物"),Festival(date: "1月1日",festivalName: "情人节")]
override func viewDidLoad() {
super.viewDidLoad()
let delayInSeconds = 2.0
let popTime = dispatch_time(DISPATCH_TIME_NOW,dispatch_get_main_queue()) { () -> Void in
//注意这里,我故意把loadeddataList中的数据打乱了,为了实现异构数据的按照某个公共类型的属性的排序,使用了Swift内置的sort函数,String遵守了Compareable协议,这里为了简单吧date指定为String类型,如果是NSDate,你可以在sort的闭包中指定合适的排序规则。
self.dataList = self.loadeddataList.sort{$0.date < $1.date}
self.tableView.reloadData()
}
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
return dataList.count
}
override func tableView(tableView: UITableView,cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID,forIndexPath: indexPath) as! ShowedTableViewCell
//注意这里,通过可选绑定进行异构数据的类型控制
if let event = dataList[indexPath.row] as? Event{
cell.MixLabel.text = event.eventTitle
cell.dateLabel.text = event.date
cell.backgroundColor = UIColor.redColor()
return cell
} else if let festival = dataList[indexPath.row] as? Festival{
cell.MixLabel.text = festival.festivalName
cell.dateLabel.text = festival.date
cell.backgroundColor = UIColor.whiteColor()
return cell
} else {
return cell
}
}
}
运行一下: protocol CellPresentable{
var mixLabelData:String {get set}
var dateLabelData:String {get set}
var color: UIColor {get set}
func updateCell(cell:ShowedTableViewCell)
}
这个协议的思想是显示地声明一个更新cell的方法,并根据cell需要的数据声明两个属性,我们并不关心mixLabel和dateLabel的数据从哪里来,叫什么名字,但他们的功能是确定的,Swift2.0之后可以扩展协议,下面通过协议扩展给这个协议增加默认的实现,这样在绑定数据时可以减少代码量: extension CellPresentable{
func updateCell(cell:ShowedTableViewCell){
cell.MixLabel.text = mixLabelData
cell.dateLabel.text = dateLabelData
cell.backgroundColor = color
}
}
好了,我们写好了,下一步我们要修改cell的代码,增加一个方法接受一个CellPresentable: class ShowedTableViewCell: UITableViewCell {
//用来展示事件主题或节日名称的Label
@IBOutlet weak var MixLabel: UILabel!
//用来展示日期的Label
@IBOutlet weak var dateLabel: UILabel!
func updateWithPresenter(presenter: CellPresentable) {
presenter.updateCell(self)
}
}
这里也做了一些改进,李信洁前辈的示例中是针对每一个控件去定义方法的,其实对一个View的所有IBOutlet做更新不就是更新它自己么,所以这里我的写法是直接传入self。然后(我也不想多说然后,但是步骤就是这么多)为了绑定异构的Model和View你还需要定义一个ViewModel,并且通过定义不同的init实现数据绑定: struct ViewModel:CellPresentable{
var dateLabelData = ""
var mixLabelData = ""
var color = UIColor.whiteColor()
init(modal:Event){
self.dateLabelData = modal.date
self.mixLabelData = modal.eventTitle
self.color = UIColor.redColor()
}
init(modal:Festival){
self.dateLabelData = modal.date
self.mixLabelData = modal.festivalName
self.color = UIColor.whiteColor()
}
}
最后我们终于可以去修改我们的控制器了,控制器中需要更改的是与cell有关的datasource方法: override func tableView(tableView: UITableView,forIndexPath: indexPath) as! ShowedTableViewCell
if let event = dataList[indexPath.row] as? Event{
let viewModel = ViewModel(modal: event)
cell.updateWithPresenter(viewModel)
return cell
} else if let festival = dataList[indexPath.row] as? Festival{
let viewModel = ViewModel(modal: festival)
cell.updateWithPresenter(viewModel)
return cell
} else {
return cell
}
}
}
这段代码写的我满头大汗,编译运行,幸运的是运行的结果是正确的: 我在想MVVM模式的意义是什么?我在使用MVVM之前甚至需要考虑一下值不值得花时间去写成MVVM的模样,因为MVVM需要给所有的view提供协议,并且将所有的数据模型的绑定过程写进一个新的数据结构ViewModal中,但其实这个ViewModel的价值非常之小,除了数据绑定,没有其他作用了,里面甚至只有空洞的init构造器,我想我已经决定放弃这个思路了。 //视图使用的协议
protocol RenderContext{
func renderText(texts:String...)
func renderImage(images:UIImage...)
}
//数据使用的协议
protocol ViewModelType{
func renderInContext(context:RenderContext)
}
上面是大会上傅若愚前辈的原版,在介绍这个协议的用法之前,我觉得应该先做一点点改进,ViewModalType应该改成: protocol ViewModelType{
func renderInContext<R:RenderContext>(context:R)
}
这两个版本都可以通过编译,差别在运行的效率上,下面我在playground中展示一个示例,这个示例来源于《Advanced Swift》这本书,其实苹果的WWDC2015 408号视频中也明确表述了不要把协议当做参数类型,而写成泛型的约束,但是没有详细讲解为什么,下面是示例: func takesProtocol(x: CustomStringConvertible) { //
print ( sizeofValue(x))
}
func takesPlaceholder<T: CustomStringConvertible>(x: T) {
print ( sizeofValue(x))
}
两个方法,前者使用协议作为参数的类型,后者使用协议作为泛型的约束条件,两个方法都会打印参数的长度,调用一下试试: takesProtocol(1 as Int16) takesPlaceholder(1 as Int16)
打印结果: protocol RenderContext{
func renderText(texts:String...)
func renderImage(images:UIImage...)
}
extension RenderContext{
func renderText(texts:String...){
}
func renderImage(images:UIImage...){
}
}
现在你的模型应该是下面这样: struct Event:hasDate,ViewModelType{
var date = ""
var eventTitle = ""
func renderInContext<R : RenderContext>(context: R) {
context.renderText(date,eventTitle)
}
}
struct Festival:hasDate,ViewModelType{
var date = ""
var festivalName = ""
func renderInContext<R : RenderContext>(context: R) {
context.renderText(date,festivalName)
}
}
视图的代码应该是这样的: class ShowedTableViewCell: UITableViewCell,RenderContext {
//用来展示事件主题或节日名称的Label
@IBOutlet weak var MixLabel: UILabel!
//用来展示日期的Label
@IBOutlet weak var dateLabel: UILabel!
func renderText(texts: String...) {
dateLabel.text = texts[0]
MixLabel.text = texts[1]
}
}
由于遵守了多个协议,所以控制器中原本的异构类型不合适了,此时可以给多个协议类型写一个别名方便使用,记得顺便更新一下你的Model,提高可读性: typealias DateViewModel = protocol<hasDate,ViewModelType>
现在控制器中的数据源可以使用新的异构类型了: var dataList = [DateViewModel]()
var loadeddataList:[DateViewModel] = [Event(date: "2月14",Festival(date: "1月1日",festivalName: "情人节")]
然后更新cell的代理方法: override func tableView(tableView: UITableView,cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID,forIndexPath: indexPath) as! ShowedTableViewCell
dataList[indexPath.row].renderInContext(cell)
return cell
}
不错,代码简洁了很多,运行一下: struct Event:DateViewModel{
var date = ""
var eventTitle = ""
func renderInContext<R : RenderContext>(context: R) {
context.renderText(date,eventTitle,"red")
}
}
这样在检验的时候就看最后一个参数就好了: class ShowedTableViewCell: UITableViewCell,RenderContext {
//用来展示事件主题或节日名称的Label
@IBOutlet weak var MixLabel: UILabel!
//用来展示日期的Label
@IBOutlet weak var dateLabel: UILabel!
func renderText(texts: String...) {
dateLabel.text = texts[0]
MixLabel.text = texts[1]
if texts[2] == "red"{
backgroundColor = UIColor.redColor()
}
}
}
这里有个语法糖,可变参数的方法,在取参时不会发生越界,因为Festival的renderText方法只传了两个值,运行结果又正常了。那么如果我粗心把参数写错顺序了呢?结果成了这样: //视图使用的协议
protocol ViewType{
func getData<M:ModelType>(data:M)
}
//数据使用的协议
protocol ModelType{
func giveData<V:ViewType>(context:V)
}
不需要在扩展中写默认实现,因为传值是相互且确定的,所以方法一定会被实现。 typealias DateViewModel = protocol<hasDate,ModelType>
struct Festival:DateViewModel{
var date = ""
var festivalName = ""
func giveData<V : ViewType>(context: V) {
context.getData(self)
}
}
struct Event:DateViewModel{
var date = ""
var eventTitle = ""
func giveData<V : ViewType>(context: V) {
context.getData(self)
}
}
视图: class ShowedTableViewCell: UITableViewCell,ViewType {
//用来展示事件主题或节日名称的Label
@IBOutlet weak var MixLabel: UILabel!
//用来展示日期的Label
@IBOutlet weak var dateLabel: UILabel!
func getData<M : ModelType>(data: M) {
if let event = data as? Event{
MixLabel.text = event.eventTitle
dateLabel.text = event.date
backgroundColor = UIColor.redColor()
} else if let festival = data as? Festival{
MixLabel.text = festival.festivalName
dateLabel.text = festival.date
}
}
}
再次用苹果官方给出的异构判断方法解决异构,协议不同于类,没有那么多继承上的检查,所以使用as?是很高效的,最后只要给控制器中的代码换个名字就够了: override func tableView(tableView: UITableView,forIndexPath: indexPath) as! ShowedTableViewCell
dataList[indexPath.row].giveData(cell)
return cell
}
完成,运行效果: 写在后面: 关于博主本人: 《Swift开发手册:技巧与实战》作者。国内计算机领域的某名校毕业,学习不差,初入社会,曾只身离校北漂妄图以退学抗议畸形的研究生教育,后心疼父母返校完成学业。从2014年底开始接触Swift后一发不可收拾,至今保持狂热,小人物大梦想,孜孜不倦致力于改善iOS编程体验。欢迎大家留言交流,力所能及之处,必倾囊相授。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |