知乎日报——第四周

news/2024/12/20 22:32:28/

「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/news/1556749.html

相关文章

使用Python从阿里云物联网平台获取STM32温度数据

在物联网&#xff08;IoT&#xff09;应用中&#xff0c;设备数据的采集与监控至关重要。本文将详细介绍如何使用Python从阿里云物联网平台获取STM32设备的温度数据。我们将从已有的Java代码出发&#xff0c;逐步将其转换为Python&#xff0c;并处理在过程中遇到的问题&#xf…

OELOVE 6.0城市列表模板

研究了好久OELOVE6.0源码&#xff0c;一直想将城市列表给单独整出来&#xff0c;做地区排名&#xff0c;但是PHP程序都是加密的&#xff0c;非常难搞&#xff0c;做二开都是要命的处理不了&#xff0c;在这里有一个简单方法可以处理城市列表&#xff0c;并且可以自定义TDK&…

使用ElasticSearch实现全文检索

文章目录 全文检索任务描述技术难点任务目标实现过程1. java读取Json文件&#xff0c;并导入MySQL数据库中2. 利用Logstah完成MySQL到ES的数据同步3. 开始编写功能接口3.1 全文检索接口3.2 查询详情 4. 前端调用 全文检索 任务描述 在获取到数据之后如何在ES中进行数据建模&a…

圣乔ERP系统downloadFile.action存在任意文件读取漏洞

免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…

Jupyter Notebook 适合做机器学习开发吗?

现在很多机器学习项目都是在Jupyter notebook中开发、训练、调试和演示的&#xff0c;比如openai、deepmind等&#xff0c;kaggle比赛中的原生环境就是Jupyter notebook&#xff0c;几乎任何机器学习的开发都可以在上面进行。 对于问题中的困扰&#xff0c;想要写py文件&#x…

树莓派3B+驱动开发(8)- i2c控制PCF8591

github主页&#xff1a;https://github.com/snqx-lqh 本项目github地址&#xff1a;https://github.com/snqx-lqh/RaspberryPiDriver 本项目硬件地址&#xff1a;https://oshwhub.com/from_zero/shu-mei-pai-kuo-zhan-ban 欢迎交流 笔记说明 这一节&#xff0c;主要是设备树有…

如何实现单例模式?

什么是单例模式&#xff1f; 单例模式是一种创建型的设计模式&#xff0c;它确保一个类只有一个实例&#xff0c;并提供一个全局访问点来访问这个实例 单例模式在整个程序运行期间只创建一个对象&#xff0c;常用于管理全局资源&#xff0c;实现日志系统等场景 将构造函数私…

从零开始:PHP基础教程系列-第11篇:使用Composer管理依赖

从零开始&#xff1a;PHP基础教程系列 第11篇&#xff1a;使用Composer管理依赖 一、什么是Composer&#xff1f; Composer是PHP的依赖管理工具。它允许开发者轻松地管理项目中的库和依赖项&#xff0c;自动下载和更新所需的包&#xff0c;以及处理版本冲突。Composer使得在…