通过朱利安Tejera-Frias

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

当涉及到你的约会生活时,我可能不知道所有的答案。然而,我可以提供一些关于如何编写优雅、可维护和可测试的代码的指导UICollectionView代码(哪个比室内设计师更便宜).为了实现这个目标,我们将使用命令模式为了创建一个抽象,使我们能够在不知道单元格的具体类型的情况下对它们进行队列、配置和处理。

UICollectionView提供了一个非常灵活的工具UIKit.苹果将其定义为

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

只要您以相同的方式表示数据,它们可以相当直接。UICollectionViewDataSourceUICollectionViewDelegate当我们注册多个时,复杂性会增加UICollectionViewCell,它们需要以自己独特的方式配置。If you've been working with iOS for a while, I'm pretty sure you have witnessed horrifying ( ) implementations offunc collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: indexPath) -> UICollectionViewCell

在Ok188bet金宝搏官网Cupid,我们一直在探索写出优雅、可维护和可测试的代码的新方法UICollectionView代码。为了实现这个目标,我们将使用命令模式为了创建一个抽象,使我们能够在不知道单元格的具体类型的情况下对它们进行队列、配置和处理。

用户提要

在我们的示例项目,我们将把用户和广告一起显示在feed中。这是当今许多开发者所面临的普遍情况。

提要中显示的异构内容如下:

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

让我们开始工作☺️!!

分离细胞

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

命令模式

这种行为设计模式将请求封装到一个对象中,以便将具体实现与调用者解耦。

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

协议CollectionViewCellCommand {
函数执行(细胞:UICollectionViewCell)

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

视图模型

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

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


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

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

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

命令使用方法:

它们真的很容易使用!CollectionViewCellCommand可以在任何上下文中执行,只要传入适当的参数。方法中指定的方法UICollectionViewDataSourceUICollectionViewDelegate协议。

配置命令

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

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: indexPath) -> UICollectionViewCell {let viewModel = viewModels[indexPath。// cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.); // cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.)viewModel.commands[.configuration]?. execute (cell: cell) return cell}

请注意,可选的链接安全处理失踪案命令.configuration关键。

选择命令

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

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


let viewModel = viewModels[indexPath.item]
viewModel.commands [.selection] ? .perform(细胞:细胞)

宏命令

在示例项目中,当用户单元格被选中时,我们推送一个视图控制器。如果我们也想要跟踪一个分析事件,我们可以把它们放在一个命令中,但这将打破[单一责任原则](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(user: user) -> CollectionViewCellViewModel {
let size = CGSize(width: 300, height: 200)
let configurationCommand = UserSnapshotCollectionViewCellConfigurationCommand(user: user, imageNetworkManager: imageNetworkManager ())
let命令:[CollectionViewCellCommandKey: CollectionViewCellCommand] = [
.configuration: configurationCommand
//附加的(CommandKey, Command)键值对可以在这里添加,以解决不同的场景(选择,取消选择,等等)

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


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

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

struct UserSnapshotCollectionViewCellConfigurationCommand: CollectionViewCellCommand {

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

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


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


cell.usernameLabel.text = user.username

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


如果让backgrounddurl = user。backgroundUrl {
_ = imageNetworkManager。请求(url: backgrounddurl){(图像)在
cell.backgroundImageView.image =图像





配置命令请求图像avatarImageViewbackgroundImageView通过imageNetworkManager.它还会更新usernameLabel

我们现在可以使用工厂在UsersViewController变换用户CollectionViewCellViewModel

类UsersViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

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



var viewModels = [CollectionViewCellViewModel]()

覆盖func viewDidLoad() {
super.viewDidLoad ()
setupViewModels ()


func setupViewModels () {
let userFactory = UserSnapshotCollectionViewCellViewModelFactory()
让userViewModels = User.all.map(userFactory.create)
视图模型。追加(contentsOf userViewModels):

广告单元

为了表示广告,我们将使用结构体包含一个contentUrl和一个clickthroughUrl

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

/ /测试数据
静态var all:[广告]{
返回(
广告(id: 1, contentUrl: URL(字符串:“https://cdn.okccdn.com/media/img/hub/mediakit/okcupid_darkbg.188bet金宝搏官网png”)!,点击通过URL: URL(字符串:“https://okcupid.com/h188bet金宝搏官网ome”)!类型:.image)




enum AdvertisementType {
案例图片
案例视频
情况下音频

图像广告将由视觉上的AdvertisementCollectionViewCell,其中只包含一个UIImageView.单击此单元格时,单击通过url将由UIApplication.的SelectionCommand要求UIApplication广告来执行。

进口UIKit

struct AdvertisementCollectionViewCellSelectionCommand: CollectionViewCellCommand {

private let application: UIApplication
私人出租广告:广告

init(advertisement: advertisement, application: UIApplication) {
自我。应用=应用
自我。广告=广告


func执行(cell: UICollectionViewCell) {

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



AdvertisementCollectionViewCellViewModelFactory是负责改造的一个广告成一个CollectionViewCellViewModel.就像在用户->视图模型例中,工厂定义大小标识符和支持命令

struct AdvertisementCollectionViewCellViewModelFactory {

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


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


将这些更改合并到UsersViewController,我们可以修改setupViewModels ()函数通过添加几行:

let advertisementFactory = AdvertisementCollectionViewCellViewModelFactory()
let ads = advertising .all.map{广告工厂。/ /创建一个应用程序
视图模型。insert(contentsOf: ads, at: viewModels.)数/ 2)

结论

通过应用命令模式抽象,通过将每个组件分离到服务于单一的目的.拥有只做一件事的类可以减少软件工程师对其进行推理的脑力负担,对于那些不熟悉代码库的人来说更是如此。由于低耦合,它还有助于包含单元测试模型视图控制器层。

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

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

这就是UserViewController在应用了所有的更改之后。

进口UIKit

类UsersViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

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



var viewModels = [CollectionViewCellViewModel]()

覆盖func viewDidLoad() {
super.viewDidLoad ()
setupViewModels ()


func setupViewModels () {
let userFactory = UsersCollectionViewCellViewModelFactory()
let advertisementFactory = AdvertisementCollectionViewCellViewModelFactory()

让userViewModels = User.all.map {userFactory。create(user: $0, viewController: self)}
视图模型。追加(contentsOf userViewModels):
let ads = advertising .all.map{广告工厂。/ /创建一个应用程序
视图模型。insert(contentsOf: ads, at: viewModels.)数/ 2)


// MARK: - UICollectionViewDataSource

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
返回viewModels.count


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

let viewModel = viewModels[indexPath.item]
// cell根据绑定到视图模型的标识符退出队列
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.);标识符:indexPath)
viewModel.commands [.configuration] ? .perform(细胞:细胞)

返回单元格


/ /马克:UICollectionViewDelegate

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


let viewModel = viewModels[indexPath.item]
viewModel.commands [.selection] ? .perform(细胞:细胞)


func collectionView(_ collectionView: UICollectionView, didenddisplays cell: UICollectionViewCell, forItemAt indexPath: indexPath) {
let viewModel = viewModels[indexPath.item]
viewModel.commands [.cancellation] ? .perform(细胞:细胞)


func collectionView(_ collectionView: UICollectionView,布局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科技博客上阅读他们的故事188bet金宝搏官网

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

188bet金宝搏官网OkCupid的工程团队负责每天为数百万人配对。在OkCupid科技博客上阅读他们的故事188bet金宝搏官网