MVVM-2:深度学习
在这里,我们又回到了MVVM系列的第二部分。 如果您不熟悉MVVM,则希望阅读本系列的第一个博客MVVM — 1:一般讨论,以使MVVM更加清晰。 为什么只留下一个简单的MVVM演示? 如所承诺的那样,这将是在MVVM公园中进行的详细游览,因此是一个大博客;)。 所以等我直到最后。
你会学到什么?
- 在实时iOS应用程序开发场景中使用MVVM。
- 建立复杂的UI尊重MVVM概念。
- MVVM中遇到的常见问题并巧妙地解决了这些问题。
激动……
MVVM的缺点:
在探讨主要概念之前,让我们回顾一下MVVM的常见缺点:
- 难以在嵌套视图和复杂UI中管理视图模型及其状态。
- 各种MVVM组件和数据绑定之间的通信可能很痛苦。
- 视图和视图模型的代码可重用性很困难。
- 初学者的MVVM很难使用。
我们要建造什么?
我们将构建一个应用程序,该应用程序将获取您的当前位置并显示附近的自动取款机,咖啡馆,夜总会和您周围的餐馆之类的地方,因此,如果您想出去玩,这对您来说真的很容易:)。 没有比NEARBY更好的名字了。
该应用程序的外观如下:
MVVM的基本经验法则:
- 视图模型归视图所有 , 模型归视图模型所有 。
- View Model仅负责处理输入到输出以及驱动UI所需的逻辑。
- 视图模型不应修改UI。
- 该视图应仅负责UI处理。
- 视图不应与数据模型交互,反之亦然。
在构建我们的应用程序时,我们将尝试维护MVVM的这些基本规则。
因此,不浪费时间,让我们进入竞技场。
样例代码:
您可以下载示例代码,并在MVVM-2文件夹下打开“ Nearby”项目。
代码流:
该项目包含三个页面:主页,位置列表和位置详细信息页面。 为了便于理解,每个页面都被视为一个单独的模块。
对于此讨论,我们将分析HomePage模块。 其他模块的设计类似。
主页模块
通过分析HomePage设计,我们可以将页面中的视图分离为主页视图,分页单元格和带有集合视图的表格单元格。 此外,分页单元格具有水平滚动视图,该滚动视图具有显示位置细节的视图。
如果您考虑如何管理所有视图模型并在组件之间划分职责,您会发现MVVM使情况变得更加糟糕,特别是对于初学者而言。
可能有多种方法来构建下面的UI,但是对于我们的讨论,让我们根据下图分离视图 :
闻到腥味……。 是的,对于MVVM的初学者而言,上述隔离可能是一个错误和时间杀手。 那么,MVVM将导致哪些问题:
- 管理多个嵌套视图和视图模型 。
- 通过驱动UI的不同业务逻辑查看可重用性。 (例如,单个CollectionTableCell用于具有不同业务逻辑的多个单元。)
- 不同组件之间的通信。
我们用于视图模型 , 视图和模型的代码架构如下:
流程图说明:
我们的整个应用及其所有观点均基于上述隔离原则,因此了解这一点非常重要。
- HomeView(即HomeViewController )拥有HomeViewModel(即HomeViewModel ),因此我们在家庭控制器中创建了其私有属性。 它有助于我们确保除拥有的视图之外,没有人会搞乱视图模型 。
私人var viewModel = HomeViewModel()
- 当通知视图加载或刷新操作时, HomeViewModel获取应用程序数据,配置输出并准备表数据源数组。
viewDidLoad = {[弱自我]
self..getAppData(completion:{
自我?.prepareTableDataSource()
自我?.reloadTable()
})
}
在HomeViewModel实现中需要注意的几个指针:
- 我们称闭包为self 。 reloadTable()通知HomeViewController重新加载表,从而维持视图更新UI组件的责任。
- 应用程序的数据获取和数据保存逻辑应写在视图模型中。
- 视图模型发出API请求,从而节省了视图模型执行业务逻辑的责任。
- 如您所见,我们使用闭包进行通信,因为它们很小,可以轻松地在代码中移动,并具有许多其他好处。 但是正如Ben叔叔所言: “ 强大的力量带来巨大的责任。” ,在使用闭包时,我们应该格外小心,因为闭包可能会很混乱并且会导致线程问题。 有一个很棒的博客,解释了为什么我们应该使用回调而不是委托。
- 在此使用的API是Google Places API。 您可以在Google Places API中阅读有关Places API的更多信息。
- 您会发现一种有趣的方法,我们已经使用它来安排表格数据源数组。
/// tableDataSource数组定义
私人var tableDataSource:[HomeTableCellType] = [HomeTableCellType]()
- “ tableDataSource ”是“ HomeTableCellType ”枚举的集合。 特定的HomeTableCellType与其自己的viewModel相关联。
///枚举以区分不同的家庭单元格类型
列举HomeTableCellType {
案例pageingCell(型号:PaginationCellVM)
案例类别Cell(模型:TableCollectionCellVMRepresentable)
caseplaceCell(model:TableCollectionCellVMRepresentable)
}
等等…! TableCollectionCellVMRepresentable有什么用? 这似乎不是视图模型。 好吧,这是一种简化视图代码可重用性的方法,我们将在本博客的后面部分讨论。
- 由于我们知道单元格的数量及其类型,因此我们可以很容易地配置tableDataSource ,如下所示:
私人功能prepareTableDataSource(){
tableDataSource.append(cellTypeForPagingCell())
tableDataSource.append(cellTypeForCategoriesCell())
tableDataSource.append(contentsOf:cellTypeForPlaces())
numberOfRows = tableDataSource.count
}
- 让我们看一下cellType提取方法的实现。
私有函数cellTypeForPlaces()-> [HomeTableCellType] {
var cellTypes = [HomeTableCellType]()
让allPlaceTypes = PlaceType.allPlaceType()
用于allPlaceTypes {
let topPlaces = Helper.getTopPlace(paceType:type,topPlacesCount:3)
让placeCellVM = PlacesTableCollectionCellVM(dataModel:PlacesTableCollectionCellModel(places:topPlaces,title:type.homeCellTitleText()))
placeCellVM.cellSelected = {[弱自我] indexPath在
self?.placeSelected(topPlaces [indexPath.item])
}
如果topPlaces.count> 0 {
cellTypes.append(HomeTableCellType.placesCell(model:placeCellVM))
}
}
返回cellTypes
}
这是怎么回事? :
对于每种场所类型,我们都有一个带有嵌入式集合视图的表格单元。 因此,我们一一遍历所有场所类型,并使用关联的视图模型准备相应的像元类型 。 每个子视图模型事件都在HomeViewModel本身中被观察到,因此HomeViewModel会采取与之对应的必要操作,从而维护我们的所有权层次结构。
那么我们将如何在主页的tableView中使用此dataSource?
让我们检查HomeViewController的“ func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)”:
func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)-> UITableViewCell {
让cellType = viewModel.cellType(forIndex:indexPath)
切换cellType {
案例.pagingCell(let model):
返回cellForPagingCell(indexPath:indexPath,viewModel:model)
案例.categoriesCell(model:let model):
返回cellForCategoriesCell(indexPath:indexPath,viewModel:model)
案例.placesCell(model:let model):
返回cellForPlacesCell(indexPath:indexPath,viewModel:model)
}
}
这是怎么回事? :
我们正在获取cellType并对其进行迭代,并要求各个方法返回与该indexPath相对应的单元格,并将模型作为参数。
让我们看一下其中一种单元格获取方法:
私有函数cellForCategoriesCell(indexPath:IndexPath,viewModel:CategoriesTableCollectionCellVM)-> CollectionTableCell {
让cell = tableView.dequeueReusableCell(withIdentifier:CollectionTableCell.reuseIdentifier,for:indexPath)为! CollectionTableCell
cell.prepareCell(viewModel:viewModel)
返回单元
}
这是怎么回事? :
我们首先为类别初始化一个单元格,并准备为其提供视图模型的单元格。
坚持,稍等!! 如何渲染单元格?
单元格的呈现方式与HomePage UI通过HomeViewModel的输出呈现方式相似。 快速浏览“ CategoriesTableCollectionCellVM ”的init()方法,我们看到:
在里面() {
prepareDataSource()
configureOutput()
}
这是怎么回事? :
我们首先为“ CategoriesCell ”内的collectionView准备数据源。 数据源是“ ImageAndLabelCollectionCellVM ”的数组,因为collectionView的 dataSource显然是其单元格视图模型的集合。 然后,我们配置视图模型的输出,在本例中是其“ numberOfItems”和“ title”属性。
配置了视图模型后 ,我们通过调用“ setTableUI()”方法在“ CollectionTableCell”中设置UI。
花式的……不是吗? 因此,您可以在代码中观察到,我们已经非常有效地解决了管理嵌套视图及其视图模型的问题。 视图模型的层次结构类似于我们的UI层次结构,因此可以非常轻松地管理对依赖性的理解。
带走点:
- 视图模型保存UI的状态和输出。
- 视图模型相互通信,然后通过各自的视图将输出通知给每个视图 。
- 视图是视图模型的唯一所有者, 视图模型是数据模型的唯一所有者。
视图的代码可重用性:
现在,我们来谈谈MVVM带来的另一个障碍,即代码可重用性。 如果您看到我们的视图隔离图像,您会发现我们计划将单个CollectionTableCell用于多个单元格,即用于显示倾向于具有不同业务逻辑的类别和位置。 那么我们该如何处理呢?
常见的解决方案是根据单元格类型选择if-else条件,并修改业务逻辑。 但这又会使我们的视图模型变得笨重,如果将视图用于其他许多地方,我们将陷入一片混乱。
那么,为什么不对同一视图使用不同的视图模型呢?
听起来复杂吗?
协议是使确认类实现一组可选的和必需的属性和方法的绝妙方法。 在我们的情况下,我们将为与视图关联的每个视图模型创建一个通用协议。
如前所述, 视图模型将输入转换为输出。 因此,为视图提供的视图模型是要在UI上呈现的数据以及用于UI更新的回调。 即使我们具有不同的业务逻辑,我们用于特定UI呈现的输出也始终相同。
还没得到吗……? 没关系,让我们举个例子。
无论逻辑如何, CollectionTableCell都需要以下输出,输入和事件:
//单元格的输出
var标题:字符串
var numberOfItems:Int
func viewModelForCell(indexPath:IndexPath)-> ImageAndLabelCollectionCellVM
//输入到viewModel通知已选择单元格。
func cellSelected(indexPath:IndexPath)
//提供单元格回调选择的事件
var cellSelected:(IndexPath)->()
因此,为什么不创建一个包含所有这些协议的协议,并使每个视图模型都符合该协议。 因此,在代码中,我们准备了一个TableCollectionCellVMRepresentable和两个视图模型,每个模型分别用于一个单元格以显示位置和类别。 每个视图模型都对TableCollectionCellVMRepresentable进行确认。
协议TableCollectionCellVMRepresentable {
//输出
var title:字符串{get}
var numberOfItems:Int {get}
func viewModelForCell(indexPath:IndexPath)-> ImageAndLabelCollectionCellVM
//输入
func cellSelected(indexPath:IndexPath)
//事件
var cellSelected:(IndexPath)->(){get}
}
PlacesTableCollectionCellVM:
class PlacesTableCollectionCellVM:TableCollectionCellVMRepresentable {
var numberOfItems:Int = 0
var title:字符串=“”
var cellSelected:(IndexPath)->()= {_ in}
私人var dataModel:PlacesTableCollectionCellModel!
私有var数据源:[ImageAndLabelCollectionCellVM] = [ImageAndLabelCollectionCellVM]()
init(dataModel:PlacesTableCollectionCellModel){
self.dataModel =数据模型
prepareCollectionDataSource()
configureOutput()
}
私人功能configureOutput(){
标题= dataModel.title
numberOfItems = dataSource.count
}
私人功能prepareCollectionDataSource(){
//准备收集数据源
{.....}
}
func viewModelForCell(indexPath:IndexPath)-> ImageAndLabelCollectionCellVM {
返回dataSource [indexPath.row]
}
func cellSelected(indexPath:IndexPath){
cellSelected(indexPath)
}
}
CategoriesTableCollectionCellVM:
类CategoriesTableCollectionCellVM:TableCollectionCellVMRepresentable {
//输出
var title:字符串=“”
var numberOfItems:Int = 0
//活动
var cellSelected:(IndexPath)->()= {_ in}
私有var数据源:[ImageAndLabelCollectionCellVM] = [ImageAndLabelCollectionCellVM]()
在里面() {
prepareDataSource()
configureOutput()
}
私人功能prepareDataSource(){
//准备数据源
{....}
}
私人功能configureOutput(){
title =“想要更具体”
numberOfItems = dataSource.count
}
func viewModelForCell(indexPath:IndexPath)-> ImageAndLabelCollectionCellVM {
返回dataSource [indexPath.item]
}
func cellSelected(indexPath:IndexPath){
cellSelected(indexPath)
}
}
因此,我们有两个具有不同逻辑的不同视图模型 ,这些逻辑用于配置输出,但具有相同的一组输出和事件。
我们学到了什么?
- 我们如何管理状态以及查看嵌套UI的模型配置。
- 具有多个视图模型的重用视图 。
- 在实时应用构建环境中了解MVVM。
示例代码: GitHub链接
我很想听到你的消息
您可以通过以下渠道与我联系,以获取任何查询,反馈或只是想进行讨论:
Twitter — @G_ABHISEK
领英
堆栈溢出
邮件
abhisekbunty94@gmail.com
为了立即连接
SkypeId — gabhisekbunty
请随时与您的其他开发人员分享。