通过朱利安Tejera-Frias

在OkCupid工作,188bet金宝搏官网我学到了很多关于约会的科学知识。但是,每当我要去约会的时候,我总是在纠结该穿什么。也许是纯色t恤配卡其裤,也许是深色牛仔裤配正装衬衫?.关键是,组织数据并使用正确的自定义布局来显示它是很困难的。

在您的约会生活中可能没有所有答案。但是,我可以提供一些关于如何编写优雅,可维护和可测试的指导UICollectionView.代码 (哪个比室内造型师更便宜).为了实现这一目标,我们将使用命令模式要创建一个将允许我们出发,配置和处理单元格的抽象而不知道其具体类型。

UICollectionView.是一个令人难以置信的灵活的工具uikit..苹果公司将其定义为

一个对象,用于管理数据项的有序集合,并使用可定制的布局显示它们。

只要您以同样的方式表示数据,它们可以相当简单。UICollectionViewDataSource.UICollectionViewDelegate在注册多个时,复杂性成长UICollectionViewCell,它们需要以自己独特的方式进行配置。If you've been working with iOS for a while, I'm pretty sure you have witnessed horrifying ( ) implementations of-> UICollectionViewCell . func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: indexPath

在Ok188bet金宝搏官网cupid,我们不断探索新的写作优雅,可维护和可测试的方式UICollectionView.代码。为了实现这一目标,我们将使用命令模式要创建一个将允许我们出发,配置和处理单元格的抽象而不知道其具体类型。

用户提要

在我们的示例项目这是当今许多开发商所面临的一个相当普遍的情况。

feed中显示的异构内容如下:

  • 将显示普通用户快照细胞(见绕口令).它有一个背景图像,头像和用户名的标签。
  • 功能的用户将显示使用圆形细胞(见书呆子).它只有一个头像和用户名的标签。
  • 广告将与我们的用户交织在一起。对于MVP,我们只会包括图像广告

让我们开始工作☺️!!

分离细胞

为了解耦集合视图单元格和数据源/委托的配置,我们需要使用下面详细介绍的抽象。

命令模式

该行为设计模式将请求封装到对象中,以便从调用仪中解耦具体实现。

此模式是简化计算单元配置、取消或任何其他特定请求的关键。

协议CollectionViewCellCommand {
Func执行(Cell:UICollectionViewCell)

考虑到UICollectionViewCellS是可重用的,命令需要接受一个单元格作为参数。

视图模型

CollectionViewCellViewModel包含出队列、显示和与单元格交互所需的所有信息。

struct CollectionViewCellViewModel {
让标识符:字符串
让大小:CGSize
让命令:[CollectionViewCellCommandKey: CollectionViewCellCommand]


enum CollectionViewCellCommandKey {
案例配置
情况下取消
病例选择
//你可以添加更多的案例来处理取消选择或任何其他交互

操作的所有具体细节都保存在命令,允许我们的视图模型用于使用任何UICollectionView.与任何类型的基础模型(低耦合不是很酷吗?)它意味着视图模型是独立的任何或者行动

异构数据是条件分支语句的主要驱动因素,因为由于业务需求或仅仅是数据不兼容,每种情况都需要以不同的方式处理。CollectionViewCellViewModel成为事实上的标准代表任何数据或者行为需要在视觉上通过UICollectionView.

如何使用这些命令:

他们真的很容易使用!CollectionViewCellCommand只要传递了适当的参数,就可以在任何上下文中执行。它们通常对应于所指定的方法UICollectionViewDataSource.或者UICollectionViewDelegate协议。

配置命令

标识符CollectionViewCellModel用于将适当的单元格从UICollectionView..视图模型中的特定命令可以通过使用CollectionViewCellCommandKey,在这种情况下.configuration钥匙。

Func CollectionView(_ collectionView:uicollectionView,CellforiteMat IndexPath:IndexPath) - > UICollectionViewCell {exteviemodel = ViewModels [IndexPath.Item] //基于与视图模型相关的标识符排队,Let Cell = CollectiveView.dequeuereusablecell(withreuseidentifer:ViewModel.identifier,for:IndexPath)ViewModel.commands [.configuration]?support(cell:cell)返回小区}

请注意,可选的链接安全地处理失踪的情况命令为了.configuration钥匙。

选择命令

选择命令的过程包括获取视图模型对于一个IndexPath然后使用合适的CollectionViewCellCommandKey.的。选择键对应DidselectItemat

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: indexPath) {
guard让cell = collectionView。cellForItem(at: indexPath) else {
返回


让ViewModel = ViewModels [IndexPath.Item]
viewModel.commands [.selection] ? .perform(细胞:细胞)

宏观命令

在示例项目中,当用户单元格被选中时,我们push了一个视图控制器。如果我们也想跟踪一个分析事件,我们可以把它放在一个单独的命令,但这将打破[单一责任原则](https://en.wikipedia.org/wiki/SOLID_ (object-oriented_design).为了避免这个陷阱,我们可以创建一个宏茂包含多个命令数组。

struct collectionviewcellmacrocommand:collectionViewCellCommand {

private let命令:[CollectionViewCellCommand]

init(命令:[collectionViewCellCommand]){
self.commands =命令


func执行(cell: UICollectionViewCell) {
命令。forEach{$ 0。执行(细胞:细胞)}

用户快照细胞

一个工厂作为理想的抽象来为我们创建一个视图模型快照细胞.在它里面,我们可以指定大小标识符细胞以及任何命令

struct UserSnapshotCollectionViewCellViewModelFactory {

func create(用户:用户) - > collectionViewCellViewModel {
让尺寸= CGSIZE(宽度:300,高度:200)
Let ConfigurationCommand = UsersnapshotCollectionViewCellConfigurationCommand(用户:用户,ImageNetWorkManager:ImageNetWorkManager())
让命令:[CollectionViewCellCommandKey: CollectionViewCellCommand] = [
.configuration:configurationCommand.
//其他(commandkey,命令)可以在此处添加键值对以解决不同的方案(选择,取消选择等)

返回CollectionViewCellViewModel(标识符:"UserSnapshotCollectionViewCell",大小:大小,命令:命令)


要配置用户快照细胞,我们需要创建一个遵守的类型CollectionViewCellCommand协议。命令通常使用状态需要执行请求,这意味着我们需要将所有依赖项注入到它们中。

⚠️==考虑潜在的保留周期时注射对象进命令.记住要使用适当时==⚠️

struct UserSnapshotCollectionViewCellConfigurationCommand: CollectionViewCellCommand {

//执行命令所需的内部状态
private let user:用户
private让imageNetworkManager: ImageNetworkManagerProtocol

init(user: user, imageNetworkManager: ImageNetworkManagerProtocol = imageNetworkManager ()) {
自我。用户=用户
自我。imageNetworkManager = imageNetworkManager


//这就是奇迹发生的地方
func执行(cell: UICollectionViewCell) {
Guard let cell = cell as?其他UserSnapshotCollectionViewCell {
返回


cell.usernameLabel.text = user.username

_ = imageNetworkManager。request(url: user.avatarUrl){(图像)在
cell.avatarImageView.image =图像


如果让backgroundurel = user.backoundurl {
_ = imageNetworkManager。request(url: backgrounddurl){(图像)在
cell.backgroundImageView.image =图像





Configuration命令请求图像avatarimageview.backgroundImageView通过imageNetworkManager.它还会更新UsernameLabel.

我们现在可以使用位于UsersViewController转变用户到CollectionViewCellViewModel

Class UsiversViewController:UIViewController,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {

@IBOutlet弱var collectionView: UICollectionView!{
didSet {
collectionView.delegate = self.
collectionView。数据Source = self



var viewModels = [CollectionViewCellViewModel]()

覆盖Func ViewDidLoad(){
super.viewDidLoad()
setupViewModels ()


func setupViewModels () {
让userFactory = UserSnapshotCollectionViewCellViewModelFactory()
让userViewModels = User.all.map(userFactory.create)
ViewModels.Append(Containsof:UserviewModels)

广告细胞

要代表广告,我们将使用一个塑造包含一个contentull.A.clickthroughUrl

struct广告{
让id: Int
让contentUrl: URL
让clickthroughUrl: URL
让类型:AdvertisementType

/ /测试数据
static var all:[广告]{
返回(
广告(id: 1, contentUrl: URL(字符串:“https://cdn.okccdn.com/media/img/hub/mediakit/okcupid_darkbg.188bet金宝搏官网png”)!, clickthroughUrl: URL(字符串:“https://okcu188bet金宝搏官网pid.com/home”)!类型:.image)




enum AdvertisementType {
案例图像
案例视频
案例音频

图像广告将在视觉上由AdvertisementCollectionViewCell,它只包括anUIImageView..当我们点击这个单元格时,点击链接将由UIApplication.的SelectionCommand要求UIApplication广告来执行。

进口UIKit

struct AdvertisementCollectionViewCellSelectionCommand:

private let application: UIApplication
private let advertisement:广告

init(广告:广告,应用程序:UIApplication){
自我。应用=应用
自我。广告=广告


func执行(cell: UICollectionViewCell) {

如果application.canOpenURL (advertisement.clickthroughUrl) {
application.open(广告。clickthroughUrl,options: [:], completionHandler: nil)



AdvertisementCollectionViewCellViewModelFactory负责转型广告进入A.CollectionViewCellViewModel.就像在用户- >viewmodel.实例时,工厂定义大小标识符和支持命令

struct AdvertisementCollectionViewCellViewModelFactory {

func create(广告:广告,应用程序:UIApplication) -> CollectionViewCellViewModel {
让size = CGSize(width: 220, height: 220)
let configurationCommand = AdvertisementCollectionViewCellConfigurationCommand(广告:广告,imageNetworkManager: imageNetworkManager ()))
让SelectionCommand = AdvertisementCollectionViewCellSelectionCommand(广告:广告,应用程序:应用程序)
让命令:[CollectionViewCellCommandKey: CollectionViewCellCommand] = [
.configuration: configurationCommand,
.selection: selectionCommand


返回CollectionViewCellViewModel(标识符:"AdvertisementCollectionViewCell",大小:大小,命令:命令)


把这些变化合并到UsersViewController,我们可以修改setupViewModels ()函数,添加几行:

let advertisementFactory = AdvertisementCollectionViewCellViewModelFactory()
let ads = advertising .all.map {advertisementFactory。创建(广告:$0,应用:.shared)}
视图模型。insert(contentsOf: ads, at: viewModels.)数/ 2)

结论

通过应用命令模式抽象,通过将每个组件划分为服务于单一的目的.拥有一件事的课程使其不太精神上征收软件工程师,甚至更多,对于不熟悉Codebase的任何人来说,甚至更有。它还有助于包括单位测试,因为耦合低模型视图控制器层。

你可能已经注意到,添加不同类型的细胞几乎没有改变ui.的命令模式也为我们打开了创造的大门可撤销的操作,如果集合视图具有支持清除挂起更改的可修改输入字段的单元格,那么这将非常方便。

闭包变得越来越流行,函数式编程的采用比以往任何时候都要高。然而,命令是回调的面向对象替代品,它们是可以操作和扩展的一级对象。

这就是UserviewController.看起来在应用了所有的变化之后。

进口UIKit

Class UsiversViewController:UIViewController,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {

@IBOutlet弱var collectionView: UICollectionView!{
didSet {
collectionView.delegate = self.
collectionView。数据Source = self



var viewModels = [CollectionViewCellViewModel]()

覆盖Func ViewDidLoad(){
super.viewDidLoad()
setupViewModels ()


func setupViewModels () {
让useructory = UsersCollectionViewCellViewModelfactory()
let advertisementFactory = AdvertisementCollectionViewCellViewModelFactory()

let userViewModels = User.all.map {userFactory。创建用户:$0,viewController: self}
ViewModels.Append(Containsof:UserviewModels)
let ads = advertising .all.map {advertisementFactory。创建(广告:$0,应用:.shared)}
视图模型。insert(contentsOf: ads, at: viewModels.)数/ 2)


// MARK: - UICollectionViewDataSource

Func CollectionView(_ CollectionView:UICollectionView,Numberofitemsection部分:Int) - > int {
返回viewModels.count


func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: indexPath) -> UICollectionViewCell {

让ViewModel = ViewModels [IndexPath.Item]
//单元格基于绑定到视图模型的标识符退出队列
让cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel。标识符:indexPath)
viewModel.commands [.configuration] ? .perform(细胞:细胞)

返回单元格


/ /马克:UICollectionViewDelegate

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: indexPath) {
guard让cell = collectionView。cellForItem(at: indexPath) else {
返回


让ViewModel = ViewModels [IndexPath.Item]
viewModel.commands [.selection] ? .perform(细胞:细胞)


Func CollectionView(_ collectionView:uicollectionView,didenddisplaying cell:uicollectionViewCell,foriTemat IndexPath:IndexPath){
让ViewModel = ViewModels [IndexPath.Item]
viewModel.commands [.cancellation] ? .perform(细胞:细胞)


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: indexPath) -> CGSize {
//由视图模型定义适当的大小
返回视图模型indexPath.item .size


你觉得怎么样命令模式?你会在你的下一个项目中使用它吗?加入我们的对话reddit !

188bet金宝搏官网OkCupid正在招聘iOS用户点击这里了解更多

最初发表在https://tech.188bet金宝搏官网okcupid.com2017年10月23日。

188bet金宝搏官网OkCupid科技博客

阅读来自OkCupid工程团队的故事,每天连188bet金宝搏官网接着数百万人

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网Okcupid的工程团队负责每天匹配数百万人。阅读Okcupid Tech Blog上的故事188bet金宝搏官网

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网Okcupid的工程团队负责每天匹配数百万人。阅读Okcupid Tech Blog上的故事188bet金宝搏官网