用户体验优化:商品局部刷新策略与实现

一、需求描述

在快速变动的电商类APP中需要刷新商品信息尤其是数据,以保证商品信息的准确性。在保证数据刷新的同时,因用户的阅览习惯,在刷新的数据时还需要保证浏览位置不变动。因此,我们决定讨论如何能更用户更友好的进行局部刷新。

让我们分析先来这一类商品的共同点与要求:

  • 商品一般为快速消费: 变动速度快.商品信息进度往往在几分钟内变动数次甚至下架。
  • 商品信息变化大:及时性要求高,需要对用户做出快速反馈
  • 一般收到受到客观限制:商品打折时间,库存等物理或活动上的限制
  • 刷新不能过于频繁,要权衡服务器压力
淘抢购界面(场景例图)

因此,我们需要平衡刷新的时效性与流量限制,抉择刷新策略和机制。

一、刷新机制的策略讨论

经过一些方案与谈论,我们有了初始的两种策略。
基于两种游标的刷新策略:


刷新策略
1.页标游标刷新 (左)

使用页标与游标刷新:以当前页面为页标为记录,接入上拉与下拉加载功能。用以保证上拉与下拉后的数据为最新。

优势:能保证数据始终最新,能获得最新的商品信息。时效性及时。
劣势:处理逻辑复杂,需要加上拉加载功能并和上拉刷新兼容,工作量巨大。需要处理因数据页码变动产生的商品重复。本屏刷新策略与上下屏不一致,逻辑判断情况多。

2.唯一标识刷新 (右)

使用唯一标识刷新:保持已有的原数据与商品列表,通过唯一标识,只抓取商品的最新数据。进行时间,库存的刷新。(新数据的同步和抓取交于另一个定时器,数据刷新只负责取最新数据)

优势:最小化原则,职能单一,逻辑实现简单。要求简单,单个或多个键值唯一确定一个商品数据即可。最大的优势是用户体验好,感知度较低。既能实时刷新数据,又不会影响使用的直观性。因数据不会变动,位置回滚操作也不需要额外的工作。
劣势:不能即时的获取新上架的商品信息。如果商品为组合类商品,不容易通过唯一标示查找不能使用。请求次数较零散与频繁。且若该商品已下架需要通过标识告诉用户结果,以免误认为发生bug。

对比了以上两种方案,我们最终选择了处理逻辑统一,用户使用不太突兀的方案(2),并对初始方案进行了优化,以减少请求量,平衡服务器压力。因刷新场景的常见性,我们做成了SDK的形式希望在能多个项目中使用。

二、缓存策略

触发流程:

  • 刷新机制: 用户滑动停止触发
    • 获取刷新id数组与位置(IndexPath)
    • 过滤数据,返回需要刷新的数组id
    • 通过代理返回id数组及位置等信息
  • -> 控制器发送请求
    • -> 刷新数据更新时间

方案文案如下:

缓存策略

三、系统优化

1.处理用户拖动误触发,进行函数防抖

    // 请求,是否对过滤数据
    private func pullShopItemsData(filter: Bool = false){
        // 0.5秒函数防抖, 合并请求再发送
        self.timer?.invalidate()
        self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(refreshOperaction(timer:)), userInfo: ["refreshRightNow":filter], repeats: false)
    }

2.预测用户浏览轨迹,进行页面上下半屏预刷新

    if (maxRow != -1 && minRow != listData.count+1) {
        // 上下两屏幕
        NSInteger rest = (10 - refreshIdList.count) > 0 ?(10 - refreshIdList.count):0;
        NSInteger beforeNum = floor(rest/2.0);
        // 前半屏
        NSInteger startNum = (minRow - beforeNum>0?minRow - beforeNum:0);
        for (NSInteger i = startNum; i<minRow; i++) {
            NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
            ShopItemsDataSourceModel *model =  [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
            [refreshIdList addObject:model];
        }
        // 后半屏
        NSInteger afterNum = rest - beforeNum;
        NSInteger endNum = maxRow+afterNum < listData.count-1 ? maxRow+afterNum : listData.count-1;
        
        for (NSInteger i = maxRow+1; i<=endNum; i++) {
            NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
            ShopItemsDataSourceModel *model =  [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
            [refreshIdList addObject:model];
        }
    
    }
    return refreshIdList;

3.减少请求数方差,使用旧数据填充

保证及时性,发送请求前进行最小请求量合并,进行节流。

    // 请求数不足,填充老数据
    private func itemsWithFillOldData(originalRefresItems: RefreshItemsInfoStruct, toNum minNum: Int) -> RefreshItemsInfoStruct {
        var filledItems: RefreshItemsInfoStruct = originalRefresItems
        let minRequestNum = minNum - originalRefresItems.count;
        let sortedArray = self.refreshInfoDict
            .filter({return !originalRefresItems.keys.contains($0.key)}) // 除去已确定要刷新元素
            .sorted(by: {$0.value.lastRefreshStamp < $1.value.lastRefreshStamp}) // 根据刷新时间排序
        
        let maxIndex = minRequestNum < (sortedArray.count)
            ? minRequestNum - 1
            : (sortedArray.count) - 1
        
        if ((sortedArray.count) > 0) {
            // 切割数组
            let sortedArray = sortedArray[0...maxIndex]
            for infoTuple in sortedArray {
                filledItems.updateValue(infoTuple.value, forKey: infoTuple.key)
            }
        }
        return filledItems;
    }

4.定时器周期性刷新

四、SDK 类图

刷新机制类图.png

使用代理模式, 传入想要刷新的cell信息,代理者只负责判断是否数据过期,返回需要刷新的列表,具体刷新操作由使用者去做。

五、接入说明

1.初始化代理,遵循 ShopItemsRefreshProxyProtocol 协议
// 初始化商品刷新代理
self.proxy = [[ShopItemsRefreshProxy alloc] initWithRefreshMaster:self];
2.实现两个 数据来源 与 请求操作 两个代理方法
// 数据来源
- (NSArray<ShopItemsPrepareInfoModel *> *)shopItemsPrepareToRefresh{
    NSMutableArray<ShopItemsPrepareInfoModel *> *refreshIdList = [NSMutableArray array];
    refreshIdList = //视野内你能见到的cell
    return refreshIdList; // 传入所有你想刷新的cell,是否该刷新由代理决定
}
 
// 数据操作
- (void)shopItemsRefreshOperation:(ShopItemsRefreshProxy *)refreshProxy
                   itemsToRefresh:(NSDictionary<NSString *,ShopItemsInfoModel *> *)itemsToRefresh
                  allItemsInfoDic:(NSDictionary<NSString *,ShopItemsInfoModel *> *)allItemsInfoDic {
    __weak typeof(self) weakSelf = self;
     
    [self showHomeLoadView]; // 刷新动画
// 发送请求服务
    [refreshItemsService refreshHomeCellWithRefreshIdDic:itemsToRefresh
                                          completeHandle:^(NSDictionary<NSString *,id> * _Nullable respond, NSError * _Nullable error) {
        // ...请求成功,刷新数据源
        [refreshProxy updateRefreshTimeWithItems:refreshItems];// 刷新数据的跟新时间
        [self hideHomeLoadView];// 停止刷新动画
    }
}

3.进行刷新

// 在拖拽结束方法进行调用
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    if (self.proxy) {
        [self.proxy pullIntelligent]; // 进行刷新
    }
}
注: 商品刷新SDK 现因仅在我们的应用中使用,暂未集成到cocoapods。如果有类似的使用需求,在评论区@我即可。我会后续集成进去。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 4,977评论 0 9
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,074评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,654评论 2 59
  • 占位,待更新 大年30的晚上,太忙碌了,都没有及时更新打卡,占个位置还占出来一个坑啊。 大年30晚上我们在家里吃好...
    巧巧姐阅读 147评论 0 0
  • Android 5.0中新增了ripple类型,即波纹效果这里要注意,波纹效果只在5.0以上的设备生效,要实现此种...
    DevWang阅读 8,545评论 7 52