注意事項(xiàng): 任何設(shè)計(jì)都需要考慮項(xiàng)目需求(包括明面上以及隱含的需求(限制))。本文的一個(gè)重要前提是沒有長(zhǎng)連接,后端無法直接向移動(dòng)端推送消息。
原始需求
個(gè)人界面的右上角添加一個(gè)進(jìn)入消息中心(用于展示一些推送新聞)的按鈕。當(dāng)有未讀消息時(shí),可以顯示一個(gè)小紅點(diǎn)提醒用戶進(jìn)入未讀消息界面。
思考流程
個(gè)人界面的停留時(shí)長(zhǎng)是多少?
在當(dāng)前的項(xiàng)目中,因?yàn)閭€(gè)人界面不是用戶的主要使用場(chǎng)景,所以用戶在個(gè)人界面停留時(shí)間很短。如果簡(jiǎn)單的設(shè)計(jì)為進(jìn)入個(gè)人界面再調(diào)用接口獲取數(shù)據(jù)會(huì)導(dǎo)致錯(cuò)過一次寶貴的能夠提醒用戶查看消息的機(jī)會(huì)。所以需要預(yù)加載數(shù)據(jù)。
消息的推送頻率有多高?
近期 ( 3 - 6 個(gè)月)內(nèi),推送頻率會(huì)在 1 - 5 條/天。
是否要求信息推送后,用戶立即看到消息提醒
不需要實(shí)時(shí)推送。只需要盡可能的保證用戶看到消息即可。
設(shè)計(jì)
根據(jù)上面的思考流程,客戶端設(shè)計(jì)為 輪詢 + 用戶特定操作觸發(fā) 的方式調(diào)用接口。
未讀通知腦圖
在第一版設(shè)計(jì)中,進(jìn)入消息中心界面 和 銷毀消息中心界面 會(huì)觸發(fā) 是否顯示小紅點(diǎn) 變量 **重置為 false ** 的操作。該操作的目的是為了防止在消息中心界面時(shí),獲取到新的數(shù)據(jù)時(shí)會(huì)導(dǎo)致 是否顯示小紅點(diǎn) 變量更新為 true 的情況。
今天寫本文時(shí),修改為: 進(jìn)入消息中心界面 時(shí) 監(jiān)聽 消息列表 變量,當(dāng) 消息列表 變量發(fā)生變化時(shí),刷新消息中心界面 并將 是否顯示小紅點(diǎn) 變量重置為 false。
修改后,可以及時(shí) 更新 消息中心界面,并且將 是否顯示小紅點(diǎn) 變量 **重置為 false ** 的操作防止到一處
下面是腦通中使用的相關(guān)變量+常量
-
變量:
- 消息列表:記錄消息中心的數(shù)據(jù)(可以持久化到數(shù)據(jù)庫(kù))
- 本地最新消息id:差量更新數(shù)據(jù)
- 是否顯示小紅點(diǎn):是否顯示小紅點(diǎn)
-
常量:
- 初次輪詢延遲 :?jiǎn)?dòng)APP后延遲觸發(fā),可以設(shè)置為 0s 或者 30s
- 輪詢間隔 :控制輪詢的頻率,一般為 10分鐘 至 30分鐘
- 寬容度時(shí)間間隔:防止用戶頻繁進(jìn)出個(gè)人信息界面,導(dǎo)致頻繁獲取數(shù)據(jù)。一般為 3分鐘 至 5分鐘
在 iOS 中,默認(rèn)的超時(shí)為60s;當(dāng)網(wǎng)絡(luò)差或者數(shù)據(jù)較多時(shí),單個(gè)請(qǐng)求可能會(huì)持續(xù)2分鐘以上。所以當(dāng)寬容度時(shí)間間隔 設(shè)置為小于某個(gè)臨街值(比如3分鐘)時(shí),需要添加額外的變量避免同時(shí)有多個(gè)請(qǐng)求
iOS 版本的實(shí)現(xiàn)
因?yàn)槲恼麻L(zhǎng)度的限制,下面只給出了關(guān)鍵代碼并省略了持久化存儲(chǔ)的部分。
下面的代碼使用了自己對(duì)
NSTimer
添加的分類NSTimer+SunTask
。
源碼地址:https:// github.com/sunbohong/NSTimer-SunTask
您可以通過在 podfile 文件中添加下面的代碼使用該庫(kù)。
pod 'NSTimer-SunTask', '~> 0.2.0'
Manager
#import <Foundation/Foundation.h>
#import "SUNNotificationCenterModel.h"
@interface SUNNotificationCenterManager : NSObject
@property (nonatomic, assign, readonly) BOOL showTip;
/**
* 消息中心title,默認(rèn)為“消息中心”,由服務(wù)器返回
*/
@property (nonatomic, copy, readonly) NSString *title;
+ (instancetype)sharedManager;
/**
* 清除消息鈴鐺的小紅點(diǎn)
*/
- (void)clearTips;
/**
* 強(qiáng)制立即刷新數(shù)據(jù)
*/
- (void)updateMsg;
/**
* 強(qiáng)制刷新數(shù)據(jù),并在刷新請(qǐng)求結(jié)束后,調(diào)用block
*
* @param block 刷新請(qǐng)求結(jié)束后被調(diào)用的block
*/
- (void)updateMsgWithCompletionBlock:(dispatch_block_t)block;
/**
* get SQLite all SUNNotificationCenterModel object
*
* @return all SUNNotificationCenterModel object
*/
- (NSMutableArray<SUNNotificationCenterModel *> *)allobjects;
/**
* 添加對(duì)消息中心數(shù)據(jù)變化的觀察,添加時(shí)會(huì) **立即** 調(diào)用該block,并在狀態(tài)更新時(shí)再次進(jìn)行回調(diào)
*
* @param block 當(dāng)有新的數(shù)據(jù)變化時(shí)的回調(diào)
*/
- (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]];
// 調(diào)用接口,并在更新結(jié)束后
[[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();
}
}];
}
// 返回?cái)?shù)據(jù),可以是內(nèi)存緩存或者從數(shù)據(jù)庫(kù)中獲取
- (NSMutableArray *)allobjects {
// ...
}
- (void)addObserverBlock:(dispatch_block_t)block {
if(!block) {
return;
}
/**
* 直接執(zhí)行一次block
*/
block();
[self.observerBlocks addObject:[block copy]];
}
- (void)removeBlock:(dispatch_block_t)block {
[self.observerBlocks removeObject:block];
}
#pragma mark - private
/**
* 當(dāng)前版本設(shè)計(jì)為服務(wù)器返回消息中心的title,本類通過 NSKeyValueCoding 進(jìn)行數(shù)據(jù)解析操作。
為了防止將來添加新的字段導(dǎo)致NSObject類默認(rèn)拋出異常,所以添加本方法
*
*/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"未定義的key = %@", key);
}
- (void)setValue:(id)value forKey:(NSString *)key {
if([key isEqualToString:@"list"]) {
// 這里需要注意,當(dāng)前項(xiàng)目是增量更新。
// 不要用 `[self setShowTip:([value count] == 0)]` 的寫法。該寫法會(huì)導(dǎo)致無法正常提示用戶。
// 比如,在第15分鐘獲取到了增量數(shù)據(jù),第30分鐘再次獲取時(shí),沒有新的數(shù)據(jù)。但是會(huì)導(dǎo)致隱藏小紅點(diǎn)。
if([value count] > 0) [self setShowTip:YES];
// 緩存數(shù)據(jù)
// ...
} else{
[super setValue:value forKey:key];
}
}
/**
* 返回?cái)?shù)據(jù)庫(kù)本地存儲(chǔ)地址
*
* @return 數(shù)據(jù)庫(kù)本地存儲(chǔ)地址
*/
+ (NSString *)filePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *ducumentsDirectory = [paths objectAtIndex:0];
NSString *str = [[NSString alloc] initWithFormat:@"%@/NotificationCenter.sqlite", ducumentsDirectory];
return str;
}
/**
* 創(chuàng)建表
*/
- (void)createTable {
// ...
}
/**
* 更新是否顯示小紅點(diǎn)的入口,并通知觀察者
*
* @param showTip 是否顯示小紅點(diǎn)
*/
- (void)setShowTip:(BOOL)showTip {
[[NSUserDefaults standardUserDefaults] setBool:showTip forKey:kShowTips];
for(dispatch_block_t block in self.observerBlocks) {
block();
}
}
@end
業(yè)務(wù)方
#import "SunNotificationCenterViewController.h"
#import "SunNotificationCenterManager.h"
@implementation SunNotificationCenterViewController {
dispatch_block_t observerBlock;
}
- (void)viewDidLoad {
[super viewDidLoad];
/**
* 消息中心獲取數(shù)據(jù)后,需要更新數(shù)據(jù),并清除小紅點(diǎn)。這里沒有使用`strongSelf`的原因是,防止引用循環(huán)。
*/
__weak typeof(self) weakSelf = self;
self->observerBlock = ^(){
[[SunNotificationCenterManager sharedManager] clearTips];
[weakSelf updateViews];
};
[[SunNotificationCenterManager sharedManager] addObserverBlock:self->observerBlock];
}
// 移除監(jiān)測(cè)block
- (void)dealloc {
[[SunNotificationCenterManager sharedManager] removeBlock:self->observerBlock];
}
// 進(jìn)入時(shí),觸發(fā)下拉刷新,可以根據(jù)自己的項(xiàng)目需求定制
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.tableView.mj_header beginRefreshing];
}
// 下拉刷新觸發(fā)的方法,這里的回調(diào)只需要停止下拉即可。更新界面是另外的操作。
- (void)updateMsg {
[[SunNotificationCenterManager sharedManager] updateMsgWithCompletionBlock:^{
[self.tableView.mj_header endRefreshing];
}];
}
// 更新界面
- (void)updateViews {
self.title = [SunNotificationCenterManager sharedManager].title;
[self.tableView reloadData];
}
@end