Swift 实现多样式列表
-
如图,我们在开发中经常需要完成这样的多样式的列表,特别是电商行业,不知道大家都是怎么实现的?接下来我来说说我的实现方式,不足之处或有好的想法的欢迎也分享我下,谢谢。 限于篇幅和保密问题,文章中会有些地方省略掉,这里主要讲的是思路。
-
创建每个section布局结构协议 RecommentDataProtocol.swift
// 数据结构类型 enum RecommemtDataType { case banner // 轮播图 case shoseIcon // 图标选项 } // 表头信息 struct SectionHeader { var sectionTitle: String init(sectionTitle: String) { self.sectionTitle = sectionTitle } // 这里可以根据需求增加标题属性,比如 // var height: CGFloat { // return 10.0 // } } // 每个 section 对应的数据属性 protocol RecommentDataProtocol { var dataType: RecommemtDataType { get } var rowCount: Int { get set } // 每个 section 显示的行数,set 方法可以用 mutating func setRowCount(rowCount: Int) 代替 var size: CGSize { get } // 每一行的大小,用 UICollectionView 所以是 size var sectionHeader: SectionHeader { get } } // 设置默认值 extension RecommentDateProtocol { var rowCount: Int { get { return 1 } set { rowCount = newValue } } var size: CGSize { return CGSize(width: 0.0, height: 0.0) } }
-
创建 RecommemtDataType 对应的数据模型:BannerModel.swift、ShosenIconModel.swift,这里的代码没啥好说的,根据服务器返回的数据结构解析就OK
-
创建整个列表的数据模型 RecommentBaseModel.swift,这里包含了所有要显示的数据集合
class RecommentBaseModel: BaseModel { var banners: [BannerModel] = [BannerModel]() var shoseIcons: [ShosenIconModel] = [ShosenIconModel]() // 网络请求,这里使用的 MVVM 设计模式,我选择数据请求放在这里(model) func loadRecommentData(completeHandler: @escaping (_ message: String, _ isSuccess: Bool) -> Void) -> Void { // 解析数据得到 banners、shoseIcons 数据集 。。。。。。 } }
-
创建 viewModel 协议 RecommentViewModelProtocol.swift,关于面向协议编程的理解可以看这里
protocol RecommentViewModelProtocol { var items: [RecommentDataProtocol] { get set } var recommentModel: RecommentBaseModel { get set } }
-
创建 viewModel:RecommentBannerViewModel.swift、RecommentShosenIconViewModel.swift、RecommentViewModel.swift
/* * * RecommentBannerViewModel.swift、RecommentShosenIconViewModel.swift 要实现 RecommentDataProtocol 协议 */ // 轮播图 viewModel final class RecommentBannerViewModel: RecommentDataProtocol { var dataType: RecommemtDateType { return .banner } var size: CGSize { return CGSize(width: SYSTEMMACROS_SCREEN_WIDTH, height: FITSCREEN(f: 190.0)) } var sectionHeader: SectionHeader = SectionHeader(sectionTitle: "") var banners: [BannerModel] = [] } // icon 选项 viewModel final class RecommentShosenIconViewModel: RecommentDataProtocol { var dataType: RecommemtDateType { return .shoseIcon } var sectionHeader: SectionHeader = SectionHeader( sectionTitle: "") var size: CGSize { return CGSize(width: SYSTEMMACROS_SCREEN_WIDTH, height: FITSCREEN(f: 90.0)) } var shoseIcons: [ShosenIconModel] = [ShosenIconModel]() } // 推荐列表 viewModel final class RecommentViewModel: RecommentViewModelProtocol { var items: [RecommentDateProtocol] = [] var recommentModel: RecommentBaseModel = RecommentBaseModel() // viewModel 关联 Model func loadRecommentData(completeHandler: @escaping (_ message: String, _ isSuccess: Bool) -> Void) -> Void { // 加载数据 self.recommentModel.loadRecommentDate { (message: String, isSuccess: Bool) in self.items.removeAll() // 获取轮播图信息 let bannerViewModel: RecommentBannerViewModel = RecommentBannerViewModel() bannerViewModel.banners = self.recommentModel.banners self.items.append(bannerViewModel) // 获取选项信息 let shoseIconViewModel = RecommentShosenIconViewModel() shoseIconViewModel.shoseIcons = self.recommentModel.shoseIcons self.items.append(shoseIconViewModel) completeHandler(message, isSuccess) } } }
-
创建 banner 和 shoseIcon 要显示的 View
/** * 列表用的是 UICollectionView 所以这里的 View 都继承自 UICollectionViewCell */ // 轮播图界面 class RecommentBannerCollectionViewCell: UICollectionViewCell { // TODO:关于界面的实现细节这里就不写了 var bannerViewModel: RecommentBannerViewModel? { // 关联viewModel didSet { guard (bannerViewModel?.banners.count)! > 0 else { return } // TODO:给界面赋值刷新显示 } } // icon 选项界面 class ShoseIconsCollectionViewCell: UICollectionViewCell { // TODO:关于界面的实现细节这里就不写了 var shoseIconViewModel: RecommentShosenIconViewModel = RecommentShosenIconViewModel() { didSet { // TODO:给界面赋值刷新显示 } } }
-
创建 RecommentViewController.swift
fileprivate let kBannerCellIdentifier = "kBannerCellIdentifier" fileprivate let kShoseCellIdentifier = "kShoseCellIdentifier" // MARK: - life cyclic class RecommentViewController: BaseViewController { var viewModel: RecommentViewModel = RecommentViewModel() // 关联viewModel var recommentCollectionView: UICollectionView? // 实现细节省略。。。 override func viewDidLoad() { super.viewDidLoad() setupView() // 初始化添加 recommentCollectionView } func setupView() { initRecommentCollectionView() } // MARK: init subview private func initRecommentCollectionView() -> Void { let layout = UICollectionViewFlowLayout() recommentCollectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) recommentCollectionView?.backgroundColor = .white recommentCollectionView?.autoresizingMask = [.flexibleHeight, .flexibleWidth] recommentCollectionView?.showsVerticalScrollIndicator = false recommentCollectionView?.showsHorizontalScrollIndicator = false recommentCollectionView?.alwaysBounceVertical = true recommentCollectionView?.delegate = self recommentCollectionView?.dataSource = self recommentCollectionView?.register(RecommentBannerCollectionViewCell.self, forCellWithReuseIdentifier: kBannerCellIdentifier) recommentCollectionView?.register(ShoseIconsCollectionViewCell.self, forCellWithReuseIdentifier: kShoseCellIdentifier) recommentCollectionView?.es_addPullToRefresh { // 下拉刷新 ProgressHub.show() self.viewModel.loadRecommentData(completeHandler: { (message: String, isSuccess: Bool) in self.recommentCollectionView?.es_stopPullToRefresh() guard isSuccess else { ProgressHub.showStatus(statusString: message) return } ProgressHub.dismiss() self.recommentCollectionView?.reloadData() }) } recommentCollectionView?.es_startPullToRefresh() recommentCollectionView?.es_addInfiniteScrolling { // TODO:上拉加载更多(这里只加载推荐商品) self.recommentCollectionView?.es_stopLoadingMore() } view.addSubview(recommentCollectionView!) } } // 布局 extension RecommentViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { // 关键点,省略大量 if 或 switch let item: RecommentDateProtocol = viewModel.items[indexPath.section] return item.size } } // 实现代理 extension RecommentViewController: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { // 关键点,省略大量 if 或 switch return viewModel.items.count } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { // 关键点,省略大量 if 或 switch let item: RecommentDateProtocol = viewModel.items[section] return item.rowCount } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let item: RecommentDateProtocol = viewModel.items[indexPath.section] // 这里也可以用抽象类代替 switch 的实现,但考虑到 cell 可能存在的各种操作事件交互,增加数据与事件关联的复杂度,暂时选择 switch switch item.dateType { case .banner: let cell: RecommentBannerCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: kBannerCellIdentifier, for: indexPath) as! RecommentBannerCollectionViewCell cell.bannerViewModel = item as? RecommentBannerViewModel return cell case .shoseIcon: let cell: ShoseIconsCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: kShoseCellIdentifier, for: indexPath) as! ShoseIconsCollectionViewCell cell.shoseIconViewModel = item as! RecommentShosenIconViewModel return cell default: break } return UICollectionViewCell() } }
-
好啦,主要的过程已经实现完成,其实这个过程主要实现思想就是状态设计模式(statue pattern),大家可以去具体了解下该设计模式。任何时候抽象的目的都是解耦、易扩展,这里减少了数据与界面的耦合性,同时当需要增加新的类型的时候,只要在 RecommemtDataType 增加类型,实现对应的 viewModel 实现 RecommentDataProtocol 协议,然后再在 UICollectionViewDataSource 的代理中实现对应的 switch 分支即可,更易扩展。当然在抽象时也会增加文件量,需要维护更多的文件,所以我们在写代码过程中需要根据需求,自我衡量,选择适当的方式,同时定期 review 和 重构是有必要的。