啥?iOS长列表还可以这么写

一般说,iOS界面的一些长列表,比如首页,活动页,长的会比较长,那么写起来总感觉没有那么优雅,那么如何才能做到优雅呢?
我在实践工作利用swift枚举的关联值和自定义组模型方法来实现了

  • 下面是gif图效果


    image

可以看到,有些组是杂乱无章的排列着,而且运营那边要求,他们可以在后台自定义这些组的顺序
这可怎么办!🥺
下面看我的实现方式

定义一个组模型枚举

  • 包含可能的定义,每个枚举关联当前组需要显示的数据模型,有可能是一个对象数组,也有可能是一个对象
/// 新版首页组cell的类型
enum OriginGroupCellType {
    case marquee(list: [MarqueeModel]) // 跑马灯
    case beltAndRoad(list: [GlobalAdModel]) // 一带一路广告位
    case shoppingCarnival(list: [GlobalAdModel]) // 购物狂欢节
    case walletCard(smallWelfare: WelfareSmallResutlModel) // 钱包卡片
    case wallet(list: [HomeNavigationModel]) // 钱包cell
    case otc(list: [GlobalAdModel]) // OTC
    case hxPrefecture(list: [GlobalAdModel]) // HX商品专区
    case middleNav(list: [HomeNavigationModel]) // 中部导航
    case bottomNav(list: [HomeNavigationModel]) // 底部导航
//    case otherMall(list: [OriginOtherMallModel]) // 其它商家cell
//    case otherService(list: [OriginServiceModel]) // 其它服务cell
    case broadcast(topSale: HomeNavigationModel, hot: OriginBroadcastModel, choiceness: OriginBroadcastModel) // 直播cell
    case middleAd(list: [GlobalAdModel]) // 中间广告cell
    case localService(list: [LocalServiceModel]) // 本地服务cell
    case bottomFloat(headerList: [OriginBottomFloatHeaderModel]) // 底部悬停cell
}
  • 考虑到要下拉刷新等问题,可以这些枚举都得遵守Equatable协议
  extension OriginGroupCellType: Equatable {
    public static func == (lhs: OriginGroupCellType, rhs: OriginGroupCellType) -> Bool {
        switch (lhs, rhs) {
        case (.marquee, .marquee): return true
        case (.beltAndRoad, .beltAndRoad): return true
        case (.shoppingCarnival, .shoppingCarnival): return true
        case (.walletCard, .walletCard): return true
        case (.wallet, .wallet): return true
        case (.otc, .otc): return true
        case (.hxPrefecture, .hxPrefecture): return true
        case (.middleNav, .middleNav): return true
        case (.bottomNav, .bottomNav): return true
//        case (.otherMall, .otherMall): return true
//        case (.otherService, .otherService): return true
        case (.broadcast, .broadcast): return true
        case (.middleAd, .middleAd): return true
        case (.localService, .localService): return true
        case (.bottomFloat, .bottomFloat): return true
        default:
            return false
        }
    }
}

接下来就是组模型的定义

  • 同时我抽取一个协议GroupProvider,方便复用
protocol GroupProvider {
    /// 占位
    associatedtype GroupModel where GroupModel: Equatable
    
    /// 是否需要往组模型列表中添加当前组模型
    func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool
    /// 获取当前组模型在组模型列表的下标
    func index(with current: GroupModel, listMs: [GroupModel]) -> Int
}

extension GroupProvider {
    func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool {
        return !listMs.contains(current)
    }
    
    func index(with current: GroupModel, listMs: [GroupModel]) -> Int {
        return listMs.firstIndex(of: current) ?? 0
    }
}

  • OriginGroupModel,同样也遵守Equatable协议,防止重复添加
func addTo(listMs: inout [OriginGroupModel]) 
  • 这个方法是方便于下拉刷新时,替换最新数据所用
public struct OriginGroupModel: GroupProvider {
    typealias GroupModel = OriginGroupModel
    
    /// 组模型的类型
    var cellType: OriginGroupCellType
    /// 排序
    var sortIndex: Int

    /// 把groupModel添加或替换到listMs中
    func addTo(listMs: inout [OriginGroupModel]) {
        if isNeedAppend(with: self, listMs: listMs) {
            listMs.append(self)
        } else {
            let index = self.index(with: self, listMs: listMs)
            listMs[index] = self
        }
    }
}

extension OriginGroupModel: Equatable {
    public static func == (lhs: OriginGroupModel, rhs: OriginGroupModel) -> Bool {
        return lhs.cellType == rhs.cellType
    }
}
  • 考虑要自定义顺序,所以需要定义一个排序的实体
// MARK: - 新版首页组模型的排序规则模型
struct OriginGroupSortModel {
    /// 搜索历史的排序
    var marqueeIndex: Int
    var beltAndRoadIndex: Int
    var shoppingCarnivalIndex: Int
    var walletCardIndex: Int
    var walletIndex: Int
    var otcIndex: Int
    var hxPrefectureIndex: Int
    var middleNavIndex: Int
    var bottomNavIndex: Int
    var otherMallIndex: Int
    var otherServiceIndex: Int
    var broadcastIndex: Int
    var middleAdIndex: Int
    var localServiceIndex: Int
    var bottomFloatIndex: Int

    static var defaultSort: OriginGroupSortModel {
        return OriginGroupSortModel(
            marqueeIndex: 0,
            beltAndRoadIndex: 1,
            shoppingCarnivalIndex: 2,
            walletCardIndex: 3,
            walletIndex: 4,
            otcIndex: 5,
            hxPrefectureIndex: 6,
            middleNavIndex: 7,
            bottomNavIndex: 8,
            otherMallIndex: 9,
            otherServiceIndex: 10,
            broadcastIndex: 11,
            middleAdIndex: 12,
            localServiceIndex: 13,
            bottomFloatIndex: 99)
    }
}

控制器里定义一个 组模型数组

  • 这里有关键代码是
listMs.sort(by: { return $0.sortIndex < $1.sortIndex }) 
  • 所有的数据加载完毕后,会根据我们的自定义排序规则去排序
    /// 组模型数据
    public var listMs: [OriginGroupModel] = [] {
        didSet {
            listMs.sort(by: {
                return $0.sortIndex < $1.sortIndex
            })
            collectionView.reloadData()
        }
    }
    
    /// 组模型排序规则(可以由后台配置返回,在这里我们先给一个默认值)
    /// 需要做一个请求依赖,先请求排序接口,再请求各组的数据
    public lazy var sortModel: OriginGroupSortModel = OriginGroupSortModel.defaultSort

网络请求代码

func loadData(_ update: Bool = false, _ isUHead: Bool = false) {
        // 定义队列组
        let queue = DispatchQueue.init(label: "getOriginData")
        let group = DispatchGroup()
        
        // MARK: - 文字跑马灯
        group.enter()
        queue.async(group: group, execute: {
            HomeNetworkService.shared.getMarqueeList { [weak self] (state, message, data) in
                guard let `self` = self else { return }
                self.collectionView.uHead.endRefreshing()
                
                defer { group.leave() }
                let groupModel = OriginGroupModel(cellType: .marquee(list: data), sortIndex: self.sortModel.marqueeIndex)
                guard !data.isEmpty else { return }
                
                /// 把groupModel添加到listMs中
                groupModel.addTo(listMs: &self.listMs)
            }
        })
        
        /// .... 此处省略其它多个请求

        group.notify(queue: queue) {
            // 队列中线程全部结束,刷新UI
            DispatchQueue.main.sync { [weak self] in
                self?.collectionView.reloadData()
            }
        }
    }

collectionView的代理方法处理

func numberOfSections(in collectionView: UICollectionView) -> Int {
        return listMs.count
    }
    
    func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        let groupModel = listMs[section]
        switch groupModel.cellType {
        case .marquee, .beltAndRoad, .walletCard, .wallet, .otc, .hxPrefecture, .shoppingCarnival, .middleAd:
            return 1
        case .middleNav(let list):
            return list.count
        case .bottomNav(let list):
            return list.count
        case .broadcast:
            return 1
        case .localService(let list):
            return list.count
        case .bottomFloat:
            return 1
        }
    }
  • 同理,collectionView的代理方法中,都是先拿到 cellType 来判断,达到精准定位, 举个栗子
    /// Cell大小
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let groupModel = listMs[indexPath.section]
        let width = screenWidth - 2 * margin
        switch groupModel.cellType {
        case .marquee:
            return CGSize(width: screenWidth, height: 32)
        case .beltAndRoad:
            return CGSize(width: width, height: 46)
        case .walletCard:
            return CGSize(width: width, height: 85)
        case .wallet:
            return CGSize(width: width, height: OriginWalletCell.eachHeight * 2 + 10)
        case .otc, .hxPrefecture:
            return CGSize(width: width, height: 60)
        case .middleNav:
            let row: CGFloat = 5
            let totalWidth: CGFloat = 13 * (row - 1) + 2 * margin
            return CGSize(width: (screenWidth - totalWidth) / row, height: CGFloat(98.zh(80).vi(108)))
        case .bottomNav:
            let isFirstRow: Bool = indexPath.item < 2
            let row: CGFloat = isFirstRow ? 2 : 3
            let totalWidth: CGFloat = 4 * (row - 1) + 2 * margin
            let width = (screenWidth - totalWidth) / row
            return CGSize(width: floor(Double(width)), height: 70)
        case .shoppingCarnival:
            return CGSize(width: width, height: 150)
        case .broadcast:
            return CGSize(width: screenWidth - 20, height: 114)
        case .middleAd:
            return CGSize(width: width, height: 114)
        case .localService:
            let width = (82 * screenWidth) / 375
            return CGSize(width: width, height: 110)
        case .bottomFloat:
            let h = bottomCellHeight > OriginBottomH ? bottomCellHeight : OriginBottomH
            return CGSize(width: screenWidth, height: h)
        }
    }

总结一下这种写法的优势

  • 方便修改组和组之前的顺序问题,甚至可以由服务器下发顺序

  • 方便删减组,只要把数据的添加组注释掉

  • 用枚举的方式,定义每个组,更清晰,加上swift的关联值优势,可以不用在控制器里定义多个数组

  • 考虑到要下拉刷新,所以抽取了一个协议 GroupProvider,里面提供两个默认的实现方法

    • 方法一:获取当前cellType在listMs中的下标
    • 方法二:是否要添加到listMs中
  • 界面长什么样,全部由数据来驱动,这组没有数据,界面就对应的不显示(皮之不存,毛将焉附),有数据就按预先设计好的显示

源码地址(源码内容和gif图中有差异,但是思路是一致的)

https://github.com/XYXiaoYuan/GroupModelTest

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

推荐阅读更多精彩内容