简单可用的未读通知提醒设计
注意事项: 任何设计都需要考虑项目需求(包括明面上以及隐含的需求(限制))。本文的一个重要前提是没有长连接,后端无法直接向移动端推送消息。
原始需求
个人界面的右上角添加一个进入消息中心(用于展示一些推送新闻)的按钮。当有未读消息时,可以显示一个小红点提醒用户进入未读消息界面。
思考流程
个人界面的停留时长是多少?
在当前的项目中,因为个人界面不是用户的主要使用场景,所以用户在个人界面停留时间很短。如果简单的设计为进入个人界面再调用接口获取数据会导致错过一次宝贵的能够提醒用户查看消息的机会。所以需要预加载数据。
消息的推送频率有多高?
近期 ( 3 - 6 个月)内,推送频率会在 1 - 5 条/天。
是否要求信息推送后,用户立即看到消息提醒
不需要实时推送。只需要尽可能的保证用户看到消息即可。
设计
根据上面的思考流程,客户端设计为 轮询 + 用户特定操作触发 的方式调用接口。
未读通知脑图
未读通知脑图.png在第一版设计中,进入消息中心界面 和 销毁消息中心界面 会触发 是否显示小红点 变量 **重置为 false ** 的操作。该操作的目的是为了防止在消息中心界面时,获取到新的数据时会导致 是否显示小红点 变量更新为 true 的情况。
今天写本文时,修改为: 进入消息中心界面 时 监听 消息列表 变量,当 消息列表 变量发生变化时,刷新消息中心界面 并将 是否显示小红点 变量重置为 false。
修改后,可以及时 更新 消息中心界面,并且将 是否显示小红点 变量 **重置为 false ** 的操作防止到一处
下面是脑通中使用的相关变量+常量
-
变量:
- 消息列表:记录消息中心的数据(可以持久化到数据库)
- 本地最新消息id:差量更新数据
- 是否显示小红点:是否显示小红点
-
常量:
- 初次轮询延迟 :启动APP后延迟触发,可以设置为 0s 或者 30s
- 轮询间隔 :控制轮询的频率,一般为 10分钟 至 30分钟
- 宽容度时间间隔:防止用户频繁进出个人信息界面,导致频繁获取数据。一般为 3分钟 至 5分钟
在 iOS 中,默认的超时为60s;当网络差或者数据较多时,单个请求可能会持续2分钟以上。所以当宽容度时间间隔 设置为小于某个临街值(比如3分钟)时,需要添加额外的变量避免同时有多个请求
iOS 版本的实现
因为文章长度的限制,下面只给出了关键代码并省略了持久化存储的部分。
下面的代码使用了自己对
NSTimer
添加的分类NSTimer+SunTask
。
源码地址:https:// github.com/sunbohong/NSTimer-SunTask
您可以通过在 podfile 文件中添加下面的代码使用该库。
pod 'NSTimer-SunTask', '~> 0.2.0'
Manager
#import <Foundation/Foundation.h>
#import "SUNNotificationCenterModel.h"
@interface SUNNotificationCenterManager : NSObject
@property (nonatomic, assign, readonly) BOOL showTip;
/**
* 消息中心title,默认为“消息中心”,由服务器返回
*/
@property (nonatomic, copy, readonly) NSString *title;
+ (instancetype)sharedManager;
/**
* 清除消息铃铛的小红点
*/
- (void)clearTips;
/**
* 强制立即刷新数据
*/
- (void)updateMsg;
/**
* 强制刷新数据,并在刷新请求结束后,调用block
*
* @param block 刷新请求结束后被调用的block
*/
- (void)updateMsgWithCompletionBlock:(dispatch_block_t)block;
/**
* get SQLite all SUNNotificationCenterModel object
*
* @return all SUNNotificationCenterModel object
*/
- (NSMutableArray<SUNNotificationCenterModel *> *)allobjects;
/**
* 添加对消息中心数据变化的观察,添加时会 **立即** 调用该block,并在状态更新时再次进行回调
*
* @param block 当有新的数据变化时的回调
*/
- (void)addObserverBlock:(dispatch_block_t)block;
/**
* 移除block
*
* @param block 被移除的block
*/
- (void)removeBlock:(dispatch_block_t)block;
@end
#import "SUNNotificationCenterManager.h"
static NSString *kShowTips = @"kShowTips";
@import NSTimer_SunTask;
@interface SUNNotificationCenterManager ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) NSMutableSet<dispatch_block_t> *observerBlocks;
@end
@implementation SUNNotificationCenterManager
+ (void)load {
/**
* 初始化
*/
[SUNNotificationCenterManager sharedManager];
}
+ (instancetype)sharedManager {
static SUNNotificationCenterManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [SUNNotificationCenterManager new];
[sharedManager createTable];
});
return sharedManager;
}
- (id)init {
self = [super init];
if(self) {
_title = @"消息中心";
_observerBlocks = [NSMutableSet set];
_timer = [NSTimer sun_scheduleAfter:1 repeatingEvery:10*60 action:^{
[[SUNNotificationCenterManager sharedManager] updateMsg];
}];
}
return self;
}
- (void)clearTips {
if([self showTip]) {
[self setShowTip:NO];
}
}
- (BOOL)showTip {
return [[NSUserDefaults standardUserDefaults] boolForKey:kShowTips];
}
- (void)updateMsg {
[self updateMsgWithCompletionBlock:NULL];
}
- (void)updateMsgWithCompletionBlock:(dispatch_block_t)block {
[self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:10*60]];
// 调用接口,并在更新结束后
[[SUNHttpManager manager] updateMessageCenterWithSuccess:^(NSURLSessionDataTask *_Nonnull task, SUNHttpResponse *_Nonnull httpResponse) {
[self setValuesForKeysWithDictionary:httpResponse.res];
if(block) {
block();
}
} failure:^(NSURLSessionDataTask *_Nullable task, NSError *_Nullable error) {
if(block) {
block();
}
}];
}
// 返回数据,可以是内存缓存或者从数据库中获取
- (NSMutableArray *)allobjects {
// ...
}
- (void)addObserverBlock:(dispatch_block_t)block {
if(!block) {
return;
}
/**
* 直接执行一次block
*/
block();
[self.observerBlocks addObject:[block copy]];
}
- (void)removeBlock:(dispatch_block_t)block {
[self.observerBlocks removeObject:block];
}
#pragma mark - private
/**
* 当前版本设计为服务器返回消息中心的title,本类通过 NSKeyValueCoding 进行数据解析操作。
为了防止将来添加新的字段导致NSObject类默认抛出异常,所以添加本方法
*
*/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"未定义的key = %@", key);
}
- (void)setValue:(id)value forKey:(NSString *)key {
if([key isEqualToString:@"list"]) {
// 这里需要注意,当前项目是增量更新。
// 不要用 `[self setShowTip:([value count] == 0)]` 的写法。该写法会导致无法正常提示用户。
// 比如,在第15分钟获取到了增量数据,第30分钟再次获取时,没有新的数据。但是会导致隐藏小红点。
if([value count] > 0) [self setShowTip:YES];
// 缓存数据
// ...
} else{
[super setValue:value forKey:key];
}
}
/**
* 返回数据库本地存储地址
*
* @return 数据库本地存储地址
*/
+ (NSString *)filePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *ducumentsDirectory = [paths objectAtIndex:0];
NSString *str = [[NSString alloc] initWithFormat:@"%@/NotificationCenter.sqlite", ducumentsDirectory];
return str;
}
/**
* 创建表
*/
- (void)createTable {
// ...
}
/**
* 更新是否显示小红点的入口,并通知观察者
*
* @param showTip 是否显示小红点
*/
- (void)setShowTip:(BOOL)showTip {
[[NSUserDefaults standardUserDefaults] setBool:showTip forKey:kShowTips];
for(dispatch_block_t block in self.observerBlocks) {
block();
}
}
@end
业务方
#import "SunNotificationCenterViewController.h"
#import "SunNotificationCenterManager.h"
@implementation SunNotificationCenterViewController {
dispatch_block_t observerBlock;
}
- (void)viewDidLoad {
[super viewDidLoad];
/**
* 消息中心获取数据后,需要更新数据,并清除小红点。这里没有使用`strongSelf`的原因是,防止引用循环。
*/
__weak typeof(self) weakSelf = self;
self->observerBlock = ^(){
[[SunNotificationCenterManager sharedManager] clearTips];
[weakSelf updateViews];
};
[[SunNotificationCenterManager sharedManager] addObserverBlock:self->observerBlock];
}
// 移除监测block
- (void)dealloc {
[[SunNotificationCenterManager sharedManager] removeBlock:self->observerBlock];
}
// 进入时,触发下拉刷新,可以根据自己的项目需求定制
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.tableView.mj_header beginRefreshing];
}
// 下拉刷新触发的方法,这里的回调只需要停止下拉即可。更新界面是另外的操作。
- (void)updateMsg {
[[SunNotificationCenterManager sharedManager] updateMsgWithCompletionBlock:^{
[self.tableView.mj_header endRefreshing];
}];
}
// 更新界面
- (void)updateViews {
self.title = [SunNotificationCenterManager sharedManager].title;
[self.tableView reloadData];
}
@end