MVVM-2:深度学习

在这里,我们又回到了MVVM系列的第二部分。 如果您不熟悉MVVM,则希望阅读本系列的第一个博客MVVM — 1:一般讨论,以使MVVM更加清晰。 为什么只留下一个简单的MVVM演示? 如所承诺的那样,这将是在MVVM公园中进行的详细游览,因此是一个大博客;)。 所以等我直到最后。

你会学到什么?

  1. 在实时iOS应用程序开发场景中使用MVVM。
  2. 建立复杂的UI尊重MVVM概念。
  3. MVVM中遇到的常见问题并巧妙地解决了这些问题。

激动……

MVVM的缺点:

在探讨主要概念之前,让我们回顾一下MVVM的常见缺点:

  • 难以在嵌套视图和复杂UI中管理视图模型及其状态。
  • 各种MVVM组件和数据绑定之间的通信可能很痛苦。
  • 视图和视图模型的代码可重用性很困难。
  • 初学者的MVVM很难使用。

我们要建造什么?

我们将构建一个应用程序,该应用程序将获取您的当前位置并显示附近的自动取款机,咖啡馆,夜总会和您周围的餐馆之类的地方,因此,如果您想出去玩,这对您来说真的很容易:)。 没有比NEARBY更好的名字了。

该应用程序的外观如下:

MVVM的基本经验法则:

  1. 视图模型视图所有模型视图模型所有
  2. View Model仅负责处理输入到输出以及驱动UI所需的逻辑。
  3. 视图模型不应修改UI。
  4. 视图应仅负责UI处理。
  5. 视图不应与数据模型交互,反之亦然。

在构建我们的应用程序时,我们将尝试维护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实现中需要注意的几个指针:

  • 我们称闭包为selfreloadTable()通知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)
}

}

因此,我们有两个具有不同逻辑的不同视图模型 ,这些逻辑用于配置输出,但具有相同的一组输出和事件。

我们学到了什么?

  1. 我们如何管理状态以及查看嵌套UI的模型配置。
  2. 具有多个视图模型的重用视图
  3. 在实时应用构建环境中了解MVVM。

示例代码: GitHub链接

我很想听到你的消息

您可以通过以下渠道与我联系,以获取任何查询,反馈或只是想进行讨论:

Twitter — @G_ABHISEK

领英

堆栈溢出

邮件

abhisekbunty94@gmail.com

为了立即连接

SkypeId — gabhisekbunty

请随时与您的其他开发人员分享。