当前位置: 首页 > article >正文

知乎日报——第四周

「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;
@end

NS_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️⃣ 如果数据库中也没有,则创建默认的 extraInfo
        cachedInfo = [[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:maxTextWidth
                                                       options:NSStringDrawingUsesLineFragmentOrigin
                                                    attributes:@{NSFontAttributeName: usernameFont}
                                                       context:nil];
    height += usernameRect.size.height;
    CGRect commentTextRect = [comment.content boundingRectWithSize:maxTextWidth
                                                           options:NSStringDrawingUsesLineFragmentOrigin
                                                        attributes:@{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:maxTextWidth
                                                           options:NSStringDrawingUsesLineFragmentOrigin
                                                        attributes:@{NSFontAttributeName: replyFont}
                                                           context:nil];
            height += replyTextRect.size.height;
        } else {
            CGFloat lineHeight = replyFont.lineHeight;
            CGFloat maxHeight = lineHeight * 3; 
            CGRect replyTextRect = [replyText boundingRectWithSize:maxTextWidth
                                                           options:NSStringDrawingUsesLineFragmentOrigin
                                                        attributes:@{NSFontAttributeName: replyFont}
                                                           context:nil];
            height += MIN(replyTextRect.size.height, maxHeight);
        }
    }
    return height;
}

我将固定的高度累加起来,然后再去动态计算两个textView的高度,计算完后将对应高度存在C层当中,代码逻辑如下

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *commentID = nil;

    // 确定评论的 ID
    if (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.kler.cn/a/442573.html

相关文章:

  • java集合面试题
  • Mac玩Steam游戏秘籍!
  • 【AIGC-ChatGPT进阶提示词指令】智慧母婴:打造基于成长树的儿童发展引导系统
  • RPC实现原理,怎么跟调用本地一样
  • 【端云一体化】云函数的使用
  • 大数据技术Kafka详解 ⑤ | Kafka中的CAP机制
  • BEVFusion论文阅读
  • 在Linux中使用`scp`进行远程目录文件复制
  • fpga系列 HDL:Quartus II 时序约束 静态时序分析 (STA) test.out.sdc的文件结构
  • C语言实现八大排序算法
  • thinkphp8自带分页bootstrap
  • Android OpenGLES2.0开发(九):图片滤镜
  • Docker Compose 缓存Redis主从节点的搭建 分布式搭建
  • GIN
  • Rust安装字节源
  • 异步将用户信息存入 Redis 缓存
  • 计算机网络——期末复习(2)1-3章考试重点
  • 智能算法驱动:中阳科技量化交易模型的革新之路
  • matlab绘图时设置左、右坐标轴为不同颜色
  • SCAU期末笔记 - Linux系统应用与开发教程样卷解析(2024版)
  • WPF 布局控件
  • Python什么是动态调用方法?What is Dynamic Method Invocation? (中英双语)
  • OpenCV中的边缘检测和轮廓处理
  • OSLC助力系统工程的全生命周期整合 (转)
  • GEE+本地XGboot分类
  • 智慧商城:首页静态结构,封装首页请求接口,轮播和导航和商品基于请求回来的数据进行渲染