知乎日报——第四周

ops/2024/12/16 14:06:15/

「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的架构去写一个稍微完整的项目,学习了许多新的知识以及第三方库。当然过程之中仍然暴露了许多之前从未想过到的问题,还是认识到了自身的不足,还是希望自己能够在学习路上越来越精进自己。


http://www.ppmy.cn/ops/142383.html

相关文章

Vue3+Element-Plus使用el-form和el-table嵌套实现表格编辑并提交表单校验

el-form和el-table嵌套说明 ① :model"formData" 给表单绑定数据&#xff0c;formData是表单的数据对象 ② 表单数据对象formData中定义的tableData是表单内嵌套的表格显示数据 ③ rules为表单绑定的校验规则 ④ :prop"tableData.${$index}.name" 绑定传入…

【unity】从零开始制作平台跳跃游戏--界面的认识,添加第一个角色!

在上一篇文章中&#xff0c;我们已经完成了unity的环境配置与安装⬇️ 【Unity】环境配置与安装-CSDN博客 接下来&#xff0c;让我们开始新建一个项目吧&#xff01; 新建项目 首先进入unityHub的项目页面&#xff0c;点击“新项目”&#xff1a; 我们这个系列将会以2D平台…

The Annotated Transformer

The Annotated Transformer 外网链接&#xff1a;源码解析Transformer The Annotated Transformer

【roadMap】我转行软件测试的经历

软件测试这行咋样&#xff1f; 如果你简单了解过「软件测试工程师」这个岗位&#xff0c;就会知道它的基本特点&#xff1a; 待遇比开发低&#xff0c;比其他行业高入门丝滑&#xff0c;算是技术岗最简单的一类测试行业有细分领域&#xff1a;功能、性能、自动化… 每个行业…

第一章 计算机网络概论

1.1小节&#xff1a;计算机网络的形成和发展&#xff08;重要知识点&#xff09; 早期计算机网络&#xff1a; 计算机技术与通信技术结合&#xff1a; 起步于1951年麻省理工学院林肯实验室开发的SAGE系统&#xff0c;被视为计算机与通信技术整合的先驱。民用首次应用&#xf…

100个python经典面试题详解(新版)

应老粉要求,每晚加餐一个最新面试题 包括Python面试中常见的问题,涵盖列表、元组、字符串插值、比较操作符、装饰器、类与对象、函数调用方式、数据结构操作、序列化、数据处理函数等多个方面。 旨在帮助数据科学家和软件工程师准备面试或提升Python技能。 19、字符串乘法是…

【NumPy进阶】:内存视图、性能优化与高级线性代数

目录 1. 深入理解 NumPy 的内存视图与拷贝1.1 内存视图&#xff08;View&#xff09;1.1.1 创建视图1.1.2 视图的特点 1.2 数组拷贝&#xff08;Copy&#xff09;1.2.1 创建拷贝1.2.2 拷贝的特点 1.3 视图与拷贝的选择 2. NumPy 的优化与性能提升技巧2.1 向量化操作示例&#x…

Python 命令搭建 Https的服务器

要使用Python命令行搭建HTTPS服务器&#xff0c;您可以使用http.server模块&#xff08;在Python 3.x中可用&#xff09;&#xff0c;并结合ssl模块来创建安全的HTTPS连接。以下是一个简单的步骤指南&#xff1a; 准备证书&#xff1a; 在搭建HTTPS服务器之前&#xff0c;您需要…