微信通讯录页面UITableView细节探究

写在前面的

最近做了一个城市选择列表页的需求,大概是一个页面,内容是中国城市的列表(UITableView 实现),用户可以选择切换城市。要求按拼音首字母分组(section),右侧有首字母索引,可以快速切换到以某一个字母开始的分组。从功能及页面结构上,跟微信的通讯录页及系统的通讯录页面其实是一个意思。

最开始是用系统的方式实现的,已经满足需求。但是还是仔细对比了一下微信的通讯录,发现微信做了很多的细节,是系统的通讯录里所没有实现的。本文要探究的是这些细节中的一个点的一小部分。

我们知道UITableView的style设置为UITableViewStyle.plain,其分组的头部视图(即 Section Header View)会是吸顶的效果,并且如果继续往上滑,会有下面的sectionHeader_B将原来吸在顶部的sectionHeader_A给顶出去,然后sectionHeader_B吸在顶部。仔细观察微信通讯录的细节,发现吸在顶部的ectionHeadertitle的颜色是高亮的绿色,而不再顶部的颜色是灰色,并且如果发生如上面所述的顶出去的效果的过程中,两个title的颜色还会跟随着位置的改变有渐变的效果。

就是这个细节引发了我的思考,如果我们也要做这个效果,改怎么做?

解决问题的过程

又仔细查了一遍UITableView的API,可以获取到section header的是下面的几个方法:

- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section NS_AVAILABLE_IOS(6_0);
- (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section NS_AVAILABLE_IOS(6_0);
- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;
- (nullable UITableViewHeaderFooterView *)headerViewForSection:(NSInteger)section NS_AVAILABLE_IOS(6_0);

发现苹果并没有提供相关的事件回调,说明我们要想知道相关的事件,就需要自己动手了。

Section Header在UITableView滚动时的行为

为了搞清楚这个问题,写了如下一个简单的子类

class YYHeaderView: UITableViewHeaderFooterView {
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        print("----------------------- \(String(describing: self.superview))")
    }
    
    override var frame: CGRect {
        didSet (newValue) {
            print("----------------------- \(newValue.origin)")
        }
    }
}

当装载到UITableView滚动时,发现其didMoveToSuperview时机与willDisplayHeaderViewdidEndDisplayingHeaderView时机相对应,其加到UITableView上的之后就不会发生改变,没有其他的探究空间了。
但是发现其frame这个方法一直在重新赋值,分两种情况:

  1. 当没有吸顶也就是自由滚动的时候,重新赋值的frame都是一样的,这也比较容易理解,因为其随着一起滚动,而滚动是UITableViewcontentOffset在改变,而SectionHeaderframe就不需要改变了。
  2. 当吸顶的时候,其frame随着滚动而发生改变,原因是contentOffset一直在改变,而SectionHeader要相对屏幕不变,其frame.origin.y就需要改变了。

将屏幕上所有的SectionHeader找出来

发现并没有跟可以返回所有可显示的cell类似的接口,只有一个接口- (nullable UITableViewHeaderFooterView *)headerViewForSection:(NSInteger)section是返回具体某个Section的Header。其实通过这个接口一个一个查也行,但是如何知道屏幕上当前显示的有哪几个section呢?

寻寻觅觅,找打一个open var indexPathsForVisibleRows: [IndexPath]? { get }接口,可以返回屏幕上所有可见的IndexPath,而IndexPath里面其实是包含section信息的,所以我们可以通过如下方法实现:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard scrollView === self.contentTableView else {
            return
        }
        let tableView = self.contentTableView
        
        guard let indexPaths = tableView.indexPathsForVisibleRows, !indexPaths.isEmpty else {
            return
        }
        
        let headers: [UITableViewHeaderFooterView] = {
            let range = indexPaths.first!.section...indexPaths.last!.section
            var headers: [UITableViewHeaderFooterView] = []
            for i in range {
                if let header = tableView.headerView(forSection: i) {
                    headers.append(header)
                }
            }
            return headers
        }()
        // 这里的headers 就是屏幕上所有的header了
    }

这里注意一点,如果某一个section下面如果有0个cell,indexPaths里就不会包含那个section的IndexPath,因为这个接口查询的是可现实的row,既cell,但是这个section及时没有一个cell,但是其Secton Header还是可以存在的(但是这个header其实没有任何存在的意义,这样的组其实应该被踢除出数据源),我们用的方法是把第一个IndexPath的section到最后一个IndexPath的section这个范围里面的所有都需要查询一下。也就是let range = indexPaths.first!.section...indexPaths.last!.section 的原因,也可以使用 map、去重、之后再排序的做法,不考虑没有意义的情况发生

        let dupSections = indexPaths.map { $0.section }
        let set = Set(dupSections)
        let sections = Array(set).sorted()

解法1.0

弄清楚了在UITableView滚动时,SectionHeaderframe的变化,以及可以找出所有的SectionHeader,结合SectionHeader在吸顶或者不吸顶时的行为,想到一个办法是拿第一个SectionHeaderframeminYUITableViewcontentOffsety比较,1.如果这两个值是相等的,则说明正好是在顶部的位置,是在吸顶了;2.如果小于的关系,说明在被顶出去的过程中;3.如果被顶出去了,就跟被顶出去的这个没有什么关系了,因为每次都是拿第一个找到的header来比较的;4,如果是大于的关系,说明第一个没有吸顶,应该是tableHeaderView还在展示。

有了规则其实就可以开始写代码了,但是实现之后发现,在UITableView滑动并且SectionHeader吸顶的过程中,其两个值是不相等的,并且滑动速度越快,相差越大,所以规律没办法控制,此方法失败。

但是为什么会相差呢?我只想说在scrollViewDidScroll回调的时机,contentOffset已经被设置为了将来想去的地方,而SectionHeaderframe还是当前的值,这也就解释了其差值为什么跟滑动速度有关。不过这只是我自己的猜测理解,暂时是这么理解的。

解法2.0

思路是既然SectionHeaderframecontentOffset设置不同步,那SectionHeader只见总应该是同步的吧,所以就想利用子视图之间的关系来做:

  1. 如果屏幕上有两个及以上的header,如果第二个header与第一个header挨着,其实是被顶出去的过程中,才会发生这种情况(不考虑某个section的cell为0)
  2. 如果不是第一种情况,那我们可以判断第一个SectionHeader下面挨着的是不是本组的第一个cell,如果紧挨着本组第一个cell,可以认为不吸顶
  3. 剩下的就是单个吸顶的情况

注意:没有考虑某个分组没有cell,也没有考虑有FooterView的情况。

代码实现:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard scrollView === self.contentTableView else {
            return
        }
        let tableView = self.contentTableView
        
        guard let indexPaths = tableView.indexPathsForVisibleRows, !indexPaths.isEmpty else {
            return
        }
        
        let headers: [UITableViewHeaderFooterView] = {
            let range = indexPaths.first!.section...indexPaths.last!.section
            var headers: [UITableViewHeaderFooterView] = []
            for i in range {
                if let header = tableView.headerView(forSection: i) {
                    headers.append(header)
                }
            }
            return headers
        }()
        
        if headers.isEmpty {
            return
        }
        
        let firstHeader = headers.first!
        if headers.count > 1 {
            // 2个以上 看第二个跟第一个是不是挨着
            let secondHeader = headers[1]
            let delta = secondHeader.frame.minY - firstHeader.frame.maxY
            if abs(delta) <= 1 {
                // 正在交换的那个 可以给两个section 根据位置设置渐变色
                firstHeader.textLabel?.textColor = UIColor.green
                return
            }
        }
        // 不是正在交换 跟cell 比
        if let firstIndex = indexPaths.first,
            firstIndex.row == 0,
            let cell = tableView.cellForRow(at: firstIndex) {
            let delta = cell.frame.minY - firstHeader.frame.maxY
            if abs(delta) <= 1 {
                // 不吸顶 有tableHeaderView还在显示
                firstHeader.textLabel?.textColor = UIColor.gray
                return
            }
        }
        
        // 是吸顶 第一个吸顶的情况
        firstHeader.textLabel?.textColor = UIColor.red
    }

通过上述方法,还可以知道当前处于哪个section,相应的设置右侧的索引(前提是索引是自定义的,系统的不支持高亮)

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

推荐阅读更多精彩内容

  • 一、简介 <<UITableView(或简单地说,表视图)的一个实例是用于显示和编辑分层列出的信息的一种手段 <<...
    无邪8阅读 10,596评论 3 3
  • 概述在iOS开发中UITableView可以说是使用最广泛的控件,我们平时使用的软件中到处都可以看到它的影子,类似...
    liudhkk阅读 9,025评论 3 38
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 1.nav1.navigationBar.barStyle=UIBarStyleBlack; //改变导航栏背景颜...
    SadMine阅读 1,576评论 1 4
  • 元旦就要来了 2017的余额已经快要用尽了 这一年的六月 我经历了高考 一场决定几千万人命运的考试 一...
    n清风烈酒阅读 555评论 0 1