「OC」知乎日报——第四周(完)
文章目录
- 「OC」知乎日报——第四周(完)
- 本周总结
- 收藏界面
- 使用高度数组优化
- 设置缓存
- 总结
本周总结
本周使用FMDB完成了本地数据的创建,管理相关的点赞收藏信息,优化了tableView,使用高度数组存储动态cell的高度,并且在展开之后更新动态高度数组,使用抽屉视图写了收藏新闻的展示页面。完成了离线缓存功能,在网络请求受阻的情况下,可以使用缓存当中的信息,这样加载过的信息在没有网络的情况下也可以获取信息。
收藏界面
首先收藏界面使用的是抽屉视图,使用的是present模态视图的方法,具体实现有兴趣的读者可以通过「iOS」自定义Modal转场——抽屉视图的实现进行相关的学习。
我们讲一下其中实现本地化存储的FMDB的Manger之中的相关逻辑,首先是和网络请求的思路类似,先设置一个管理类单例来使用FMDB进行管理,这是它的头文件。
#import <Foundation/Foundation.h>
#import "fmdb/FMDB.h"
NS_ASSUME_NONNULL_BEGIN
@class extraInfo;
@interface DBTool : NSObject
-(void)createDB;
-(void)insertInfo:(extraInfo *)info;
- (NSArray<extraInfo *> *)fetchStarInfo;
- (extraInfo *)fetchInfoForNewsID:(NSString *)newsID;
- (void)deleteInfoForNewsID:(NSString *)newsID;
+ (instancetype)sharedManager;
@endNS_ASSUME_NONNULL_END
可以看到实现的内容也是数据库当中最基础的增删改查,由于不太难,我就直接把实现的方法代码贴出:
#import "DBTool.h"
#import "extraInfo.h"
@interface DBTool ()
@property (strong, nonatomic) FMDatabase *db;
@end@implementation DBTool+ (instancetype)sharedManager {static DBTool *manager;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{manager = [[DBTool alloc] init];[manager createDB];[manager createTab];});return manager;
}- (void)createDB {NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;NSString *fileName = [docPath stringByAppendingPathComponent:@"newsInfo.db"];NSLog(@"%@",docPath);self.db = [FMDatabase databaseWithPath:fileName];BOOL isSuccess = [self.db open];if (!isSuccess) {NSLog(@"打开数据库失败");}
}- (void)createTab {[self createDB];NSString *sql = @"CREATE TABLE IF NOT EXISTS newsInfo (""id INTEGER PRIMARY KEY AUTOINCREMENT, ""newsID TEXT UNIQUE, ""isLiked INTEGER, ""isFavorited INTEGER, ""newsTopic TEXT, ""newsIamge TEXT)";BOOL isSuccess = [self.db executeUpdate:sql];if (!isSuccess) {NSLog(@"数据表创建失败: %@", [self.db lastErrorMessage]);}[self.db close];
}- (void)insertInfo:(extraInfo *)info {[self createDB];NSString *sql = @"INSERT OR REPLACE INTO newsInfo ""(newsID, isLiked, isFavorited, newsTopic, newsIamge) ""VALUES (?, ?, ?, ?, ?)";BOOL isSuccess = [self.db executeUpdate:sql,info.newsID,@(info.isLiked),@(info.isFavorited),info.newsTopic,info.newsIamge];if (!isSuccess) {NSLog(@"数据插入或更新失败: %@", [self.db lastErrorMessage]);}[self.db close];
}- (extraInfo *)fetchInfoForNewsID:(NSString *)newsID {[self createDB];NSString *sql = @"SELECT isLiked, isFavorited, newsTopic, newsIamge FROM newsInfo WHERE newsID = ?";FMResultSet *result = [self.db executeQuery:sql, newsID];extraInfo *info = nil;if ([result next]) {info = [[extraInfo alloc] init];info.newsID = newsID;info.isLiked = [result boolForColumn:@"isLiked"];info.isFavorited = [result boolForColumn:@"isFavorited"];info.newsTopic = [result stringForColumn:@"newsTopic"];info.newsIamge = [result stringForColumn:@"newsIamge"];} else {NSLog(@"未找到新闻 ID 为 %@ 的记录", newsID);}[self.db close];return info;
}- (NSArray<extraInfo *> *)fetchStarInfo {[self createDB];NSString *sql = @"SELECT id, newsID, isLiked, isFavorited, newsTopic, newsIamge FROM newsInfo WHERE isFavorited = 1 ORDER BY id DESC";FMResultSet *result = [self.db executeQuery:sql];NSMutableArray<extraInfo *> *infoArray = [NSMutableArray array];while ([result next]) {extraInfo *info = [[extraInfo alloc] init];info.newsID = [result stringForColumn:@"newsID"];info.isLiked = [result boolForColumn:@"isLiked"];info.isFavorited = [result boolForColumn:@"isFavorited"];info.newsTopic = [result stringForColumn:@"newsTopic"];info.newsIamge = [result stringForColumn:@"newsIamge"];[infoArray addObject:info];}[self.db close];return infoArray;
}- (void)deleteInfoForNewsID:(NSString *)newsID {[self createDB];NSString *sql = @"DELETE FROM newsInfo WHERE newsID = ?";BOOL isSuccess = [self.db executeUpdate:sql, newsID];if (isSuccess) {NSLog(@"删除新闻 ID %@ 的记录成功", newsID);} else {NSLog(@"删除记录失败: %@", [self.db lastErrorMessage]);}[self.db close];
}@end#import "DBTool.h"
#import "extraInfo.h"
@interface DBTool ()
@property (strong, nonatomic) FMDatabase *db;
@end@implementation DBTool+ (instancetype)sharedManager {static DBTool *manager;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{manager = [[DBTool alloc] init];[manager createDB];[manager createTab];});return manager;
}- (void)createDB {NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;NSString *fileName = [docPath stringByAppendingPathComponent:@"newsInfo.db"];NSLog(@"%@",docPath);self.db = [FMDatabase databaseWithPath:fileName];BOOL isSuccess = [self.db open];if (!isSuccess) {NSLog(@"打开数据库失败");}
}- (void)createTab {[self createDB];NSString *sql = @"CREATE TABLE IF NOT EXISTS newsInfo (""id INTEGER PRIMARY KEY AUTOINCREMENT, ""newsID TEXT UNIQUE, ""isLiked INTEGER, ""isFavorited INTEGER, ""newsTopic TEXT, ""newsIamge TEXT)";BOOL isSuccess = [self.db executeUpdate:sql];if (!isSuccess) {NSLog(@"数据表创建失败: %@", [self.db lastErrorMessage]);}[self.db close];
}- (void)insertInfo:(extraInfo *)info {[self createDB];NSString *sql = @"INSERT OR REPLACE INTO newsInfo ""(newsID, isLiked, isFavorited, newsTopic, newsIamge) ""VALUES (?, ?, ?, ?, ?)";BOOL isSuccess = [self.db executeUpdate:sql,info.newsID,@(info.isLiked),@(info.isFavorited),info.newsTopic,info.newsIamge];if (!isSuccess) {NSLog(@"数据插入或更新失败: %@", [self.db lastErrorMessage]);}[self.db close];
}- (extraInfo *)fetchInfoForNewsID:(NSString *)newsID {[self createDB];NSString *sql = @"SELECT isLiked, isFavorited, newsTopic, newsIamge FROM newsInfo WHERE newsID = ?";FMResultSet *result = [self.db executeQuery:sql, newsID];extraInfo *info = nil;if ([result next]) {info = [[extraInfo alloc] init];info.newsID = newsID;info.isLiked = [result boolForColumn:@"isLiked"];info.isFavorited = [result boolForColumn:@"isFavorited"];info.newsTopic = [result stringForColumn:@"newsTopic"];info.newsIamge = [result stringForColumn:@"newsIamge"];} else {NSLog(@"未找到新闻 ID 为 %@ 的记录", newsID);}[self.db close];return info;
}- (NSArray<extraInfo *> *)fetchStarInfo {[self createDB];NSString *sql = @"SELECT id, newsID, isLiked, isFavorited, newsTopic, newsIamge FROM newsInfo WHERE isFavorited = 1 ORDER BY id DESC";FMResultSet *result = [self.db executeQuery:sql];NSMutableArray<extraInfo *> *infoArray = [NSMutableArray array];while ([result next]) {extraInfo *info = [[extraInfo alloc] init];info.newsID = [result stringForColumn:@"newsID"];info.isLiked = [result boolForColumn:@"isLiked"];info.isFavorited = [result boolForColumn:@"isFavorited"];info.newsTopic = [result stringForColumn:@"newsTopic"];info.newsIamge = [result stringForColumn:@"newsIamge"];[infoArray addObject:info];}[self.db close];return infoArray;
}- (void)deleteInfoForNewsID:(NSString *)newsID {[self createDB];NSString *sql = @"DELETE FROM newsInfo WHERE newsID = ?";BOOL isSuccess = [self.db executeUpdate:sql, newsID];if (isSuccess) {NSLog(@"删除新闻 ID %@ 的记录成功", newsID);} else {NSLog(@"删除记录失败: %@", [self.db lastErrorMessage]);}[self.db close];
}@end
我的存储结构就是,存储对应的序列ID,新闻ID,点赞收藏情况,新闻标题和新闻图片,由于点赞的数量其实需要实时申请,因此不做存储,在详情页的滚动视图到对应位置才进行申请。在详情页之中更新对应的BottomView的代码需要做出更改,内容大致如下:
if (index == self.currentPage) {extraInfo *cachedInfo = [self getCachedInfoForStory:story];// 先设置界面上的 UI 状态[self updateBottomViewWithInfo:cachedInfo];// 如果缓存中有 extraInfo,且不需要网络请求,则不发送网络请求if (![self isInfoStale:cachedInfo]) {return;}// 发送网络请求,获取最新的 extraInfo[[NetworkManager sharedManager] fetchNewsExtraInfo:story.id completion:^(extraInfo *info, NSError *error) {if (!error && info) {[self updateInfo:info forStory:story];[self updateBottomViewWithInfo:info];} else {NSLog(@"Error fetching extra info: %@", error);}}];
}- (extraInfo *)getCachedInfoForStory:(Story *)story {// 1️⃣ 优先从内存缓存中获取extraInfo *cachedInfo = self.extraInfoCache[story.id];if (!cachedInfo) {// 2️⃣ 如果内存缓存中没有,则从数据库中获取cachedInfo = [[DBTool sharedManager] fetchInfoForNewsID:story.id];}if (!cachedInfo) {// 3️⃣ 如果数据库中也没有,则创建默认的 extraInfocachedInfo = [[extraInfo alloc] init];cachedInfo.newsID = story.id;cachedInfo.newsTopic = story.title;cachedInfo.newsIamge = [story.images firstObject];cachedInfo.isLiked = NO;cachedInfo.isFavorited = NO;// ⚡️ 将新创建的缓存对象加入内存缓存,避免重复创建self.extraInfoCache[story.id] = cachedInfo;}return cachedInfo;
}- (void)updateBottomViewWithInfo:(extraInfo *)info {if (!info) return; // 防止空数据// 只更新 UI 一次,避免多次 setSelected[self.bottomView setInfo:info];[self.bottomView.like setSelected:info.isLiked];[self.bottomView.star setSelected:info.isFavorited];
}
这里拿获取详情页的状态的相关数据来举例,首先是是查找字典缓存之中的内容,如果找不到,其次再查找本地数据库之中的内容,如果都找不到就给点赞收藏状态给一个初始值NO。最后再在进行网络请求点赞评论数量时再进行赋值。
通过收藏界面进入的详情页和无限右滑的详情页的区别就是,收藏页的滚动页数是固定的,也没有太大的区别。
使用高度数组优化
我们在编写评论区的时候,由于我们的高度时动态变化的,由于cell的复用机制我们会不断的计算cell之中的高度,所以我们可以通过方法来计算cell的高度,然后将高度存储在C层的字典当中,当滑动到对应cell时可以根据index访问到cell对应的高度。
在这里我使用了一个类方法,去计算cell的高度
+ (CGFloat)heightForComment:(ShortComment *)comment {CGFloat height = 80;UIFont *usernameFont = [UIFont boldSystemFontOfSize:18];UIFont *commentFont = [UIFont systemFontOfSize:16];CGSize maxTextWidth = CGSizeMake(310, CGFLOAT_MAX);CGRect usernameRect = [comment.author boundingRectWithSize:maxTextWidthoptions:NSStringDrawingUsesLineFragmentOriginattributes:@{NSFontAttributeName: usernameFont}context:nil];height += usernameRect.size.height;CGRect commentTextRect = [comment.content boundingRectWithSize:maxTextWidthoptions:NSStringDrawingUsesLineFragmentOriginattributes:@{NSFontAttributeName: commentFont}context:nil];height += commentTextRect.size.height;if (comment.replyTo) {UIFont *replyFont = [UIFont systemFontOfSize:14];NSString *replyText = [NSString stringWithFormat:@"// %@:%@", comment.replyTo.author, comment.replyTo.content];if (comment.isExpanded) {CGRect replyTextRect = [replyText boundingRectWithSize:maxTextWidthoptions:NSStringDrawingUsesLineFragmentOriginattributes:@{NSFontAttributeName: replyFont}context:nil];height += replyTextRect.size.height;} else {CGFloat lineHeight = replyFont.lineHeight;CGFloat maxHeight = lineHeight * 3; CGRect replyTextRect = [replyText boundingRectWithSize:maxTextWidthoptions:NSStringDrawingUsesLineFragmentOriginattributes:@{NSFontAttributeName: replyFont}context:nil];height += MIN(replyTextRect.size.height, maxHeight);}}return height;
}
我将固定的高度累加起来,然后再去动态计算两个textView的高度,计算完后将对应高度存在C层当中,代码逻辑如下
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {NSNumber *commentID = nil;// 确定评论的 IDif (indexPath.section == 0 && self.longComments.count > 0) {commentID = @(self.longComments[indexPath.row].commentId);} else if ((indexPath.section == 1 || (indexPath.section == 0 && self.longComments.count == 0)) && self.shortComments.count > 0) {commentID = @(self.shortComments[indexPath.row].commentId);}NSNumber *cachedHeight = self.heightCache[commentID];if (cachedHeight) {return cachedHeight.floatValue;}CGFloat height = 0.0;if (indexPath.section == 0 && self.longComments.count > 0) {LongComment* comment = self.longComments[indexPath.row];height = [LongCommentTableViewCell heightForComment:comment];} else if ((indexPath.section == 1 || (indexPath.section == 0 && self.longComments.count == 0)) && self.shortComments.count > 0) {ShortComment *comment = self.shortComments[indexPath.row];height = [CommentTableViewCell heightForComment:comment];}self.heightCache[commentID] = @(height); // 缓存高度return height;
}
还有一个问题就是,我们要保存我们展开的状态,所以如果我们点击展开,其实cell的高度会随着变化,但是在我们C层当中的数组存储的其实还是未展开时的高度,在这里我使用一个通知传值,当我们点击展开的时候,我们的cell发送通知给C层,根据打包的数据找到对应的cell,将高度字典之中的高度进行一个更新操作,具体操作如下:
//更改数据源之中的isExpanded属性,以及移除高度数组进行重新计算
-(void)reloadCell1:(NSNotification *)notification {CommentTableViewCell* cell = notification.userInfo[@"cell"];NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];NSNumber *commentID = notification.userInfo[@"commentID"];ShortComment *commment = self.shortComments[indexPath.row];commment.isExpanded = !commment.isExpanded;commment.isLike = [notification.userInfo[@"isLike"] boolValue];[self.tableView beginUpdates];if (commentID) {[self.heightCache removeObjectForKey:commentID];}[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];[self.tableView endUpdates];
}-(void)reloadCell2:(NSNotification *)notification {LongCommentTableViewCell* cell = notification.userInfo[@"cell"];NSNumber *commentID = notification.userInfo[@"commentID"];NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];LongComment *commment = self.longComments[indexPath.row];commment.isLike = [notification.userInfo[@"isLike"] boolValue];commment.isExpanded = !commment.isExpanded;[self.tableView beginUpdates];if (commentID) {[self.heightCache removeObjectForKey:commentID];}[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];[self.tableView endUpdates];
}
设置缓存
由于关于UrlCache
之中的内容其实了解的不是很深,但所幸其实使用起来也不太难,只要对之前的AFN的manger之中的方法进行一点改修就可以了,接下来就介绍一下我实现的缓存机制
首先是在AppDelegate之中,我们给这个程序划分一个全局的内存,并给上对应的关键字,作为缓存
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {NSUInteger memoryCapacity = 50 * 1024 * 1024; // 50 MB 内存缓存NSUInteger diskCapacity = 200 * 1024 * 1024; // 200 MB 磁盘缓存NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:memoryCapacity diskCapacity:diskCapacity diskPath:@"networkCache"];[NSURLCache setSharedURLCache:urlCache];return YES;
}
紧接着在对应的网络请求方法之中,修改一下
- (void)fetchLatestNewsWithCompletion:(void (^)(News *response, NSError *error))completion {NSString *urlString = @"https://news-at.zhihu.com/api/4/stories/latest";NSURL *url = [NSURL URLWithString:urlString];NSURLRequest *request = [NSURLRequest requestWithURL:url];[self GET:urlString parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {NSDictionary *userInfo = @{@"cachedDate": [NSDate date]};NSData *data = [NSJSONSerialization dataWithJSONObject:responseObject options:0 error:nil];NSCachedURLResponse *newCache = [[NSCachedURLResponse alloc] initWithResponse:task.response data:data userInfo:userInfo storagePolicy:NSURLCacheStorageAllowed];[[NSURLCache sharedURLCache] storeCachedResponse:newCache forRequest:request];//当我在网络请求获取到信息的时候,就将对应信息存在缓存当中News *newsResponse = [News yy_modelWithJSON:responseObject];if (completion) {completion(newsResponse, nil);}} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {//在网络申请失败的时候,根据网络请求找到对应的缓存,将内容NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];if (cachedResponse) {NSDictionary *userInfo = cachedResponse.userInfo;NSDate *cachedDate = userInfo[@"cachedDate"];NSTimeInterval cacheAge = [[NSDate date] timeIntervalSinceDate:cachedDate];News *newsResponse = [News yy_modelWithJSON:cachedResponse.data];if (completion) {![请添加图片描述](https://i-blog.csdnimg.cn/direct/a2ffe5ed3fee425ca38b93e95a5b6904.gif)completion(newsResponse, nil);}return;}if (completion) {completion(nil, error);}}];
}
这样就算我们将网络关闭,程序还是会对我们的之前访问过的内容有一个缓存。
总结
到此为止,知乎日报的内容已经大致完成,剩下一些细节还需要慢慢的完善,通过知乎日报这个项目,确实逐步了解了如何用MVC的架构去写一个稍微完整的项目,学习了许多新的知识以及第三方库。当然过程之中仍然暴露了许多之前从未想过到的问题,还是认识到了自身的不足,还是希望自己能够在学习路上越来越精进自己。