iOS 实现多样式列表

设计图.jpeg
  • 如图,我们在开发中经常需要完成这样的多样式的列表,特别是电商行业,不知道大家都是怎么实现的?接下来我来说说我的实现方式,不足之处或有好的想法的欢迎也分享我下,谢谢。 限于篇幅和保密问题,文章中会有些地方省略掉,这里主要讲的是思路。

  • 创建每个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 和 重构是有必要的。

PS:感觉写得不是很顺畅,希望能慢慢锻炼中得到改善O(∩_∩)O哈哈~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容