Bootstrap

【iOS】知乎日报总结

前言

前五个礼拜完成了知乎日报的仿写,新学习了几个第三方库以及解决了一些网络异步的问题,本篇博客是对这个仿写项目的一些总结。两周前进行过一次总结【iOS】知乎日报前三周总结,这次主要内容是这几周的仿写的总结。

详情页

这个页面在上次进行过一次的总结,这次主要讲一下前期还没有解决的问题,在左右滑动时进行一个多个页面的滑动。这里我的思路是在- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView这个函数中来进行判断申请新的一天的数据,同时进行一个提前的申请,从而防止出现滑动到一个新的页面但是还没有数据来进行网络申请获得webView的情况出现。

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    CGFloat endWidth = self.mainview.scroll.contentOffset.x;
    NSInteger count = endWidth / 393;
    NSLog(@"滚动结束后:%ld", count);
    NSInteger webAPICount = _strurl.count;
    _countright = webAPICount - count;
    if(_countright <= 4 && !_Right) {
        _Right = YES;
        [self gaindate];
    }//申请新的一天的数据
    _numFMDB = count;
    if(count < _strurl.count)
        [self gainextra:count];//加载下面工具栏中的数据
    NSString *urlString = _strurl[_numFMDB];
    if (![_exitURL containsObject:urlString]) {
        _webview = [[WKWebView alloc] initWithFrame:CGRectMake(endWidth, 0, 393, 719)];
        _webview.navigationDelegate = self;
        NSLog(@"URL String: %@", urlString);
        [self gainWebview:urlString];
    }//加载这一页的webView
}

评论区

评论区中主要的要点是tableView的自适应高度已经使用UITextView实现长评论的展开与收缩。

tableView的自适应高度

tableView的自适应高度主要使用第三方库Masorny来实现,通过控制每个空间之间的距离,让UILabel以及UITextView的文字自己撑开Cell的高度,从而让整个页面中的cell都实现自适应高度。
在这里插入图片描述
如上图所示,每个Cell中展示的文字多少不同,Cell的高度都不相同。

首先我们要设置tableView的这两个属性:

self.commentview.tableview.estimatedRowHeight = 50;
_commentview.tableview.rowHeight = UITableViewAutomaticDimension;

而后通过对于自定义Cell中的控件的位置设置从而让文字撑开cell,做到自适应高度。

[_profile mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.contentView).offset(10);
        make.top.equalTo(self.contentView).offset(10);
        make.width.and.height.equalTo(@36);
    }];
    
    [_extra mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_profile.mas_top);
        make.right.equalTo(self.contentView).offset(-20);
        make.width.and.height.equalTo(@16);
    }];
    
    [_name mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.contentView).offset(55);
        make.top.equalTo(self.contentView).offset(10);
        make.width.equalTo(@150);
        make.height.mas_offset(25);
    }];
    
    [_main_comment mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(_name.mas_left);
        make.top.equalTo(_profile.mas_bottom).offset(-10);
        make.right.equalTo(self.contentView).offset(-20);
    }];
    
    [_time mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(_name.mas_left);
        make.top.equalTo(_aux_comment.mas_bottom).offset(10);
        make.width.mas_offset(80);
        make.bottom.equalTo(self.contentView.mas_bottom).offset(-10);
    }];
    
    [_comment mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_aux_comment.mas_bottom);
        make.right.equalTo(self.contentView).offset(-20);
        make.width.and.height.equalTo(@18);
    }];
    
    [_good mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_aux_comment.mas_bottom);
        make.right.equalTo(self.contentView).offset(-100);
        make.width.mas_offset(50);
        make.height.mas_offset(18);
    }];
    
    [_aux_comment mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(_main_comment.mas_left);
        make.top.equalTo(_main_comment.mas_bottom);
        make.right.equalTo(_main_comment.mas_right);
    }];

评论区的展开与收缩

关于评论区的长评论,这次我使用的是UITextView来实现,这里笔者借鉴了博客这里粘贴出来:iOS开发 多行文本的展开/全文和收起(UITextView),个人认为这篇博客讲的还算详细,这里笔者也简单概述一下自己的思路。

  1. 首先我们需要在Model中增加几个属性,即判断是否展开的isOpen、实际的文本高度以及文本的最大高度。
- (void)jisuan:(NSString *)content {
    NSLog(@"content = %@", content);
    _content = content;
    if (content == nil || [content isEqualToString:@""]) {
        self.titleActualH = 0;
        self.titleMaxH = 0;
    } else {
        NSUInteger numCount = 2; //这是cell收起状态下期望展示的最大行数
        NSString *str = @"这是一行用来计算高度的文本";
        //这行文本也可以为一个字,但不能太长
        CGFloat W =  318 - 30; //这里是文本展示的宽度
        self.titleActualH = [content boundingRectWithSize:CGSizeMake(W, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil].size.height;
        self.titleMaxH = [str boundingRectWithSize:CGSizeMake(W, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil].size.height*numCount;
    }
}
  1. 我们需要在cell中判断是否已经开展,以及通过添加给富文本事件来让实现展开与收缩。
- (void)setupCellData:(Modelcomment_reply_to *)model {
    
    NSString *suffixStr = @""; //添加的后缀按钮文本(收起或展开)
    NSString *contentStr = model.content;
    CGFloat H = model.titleActualH; //文本的高度,默认为实际高度
    
    if (model.titleActualH > model.titleMaxH) {
        //文本实际高度>最大高度,需要添加后缀
        if (model.isOpen) {
            //文本已经展开,此时后缀为“收起”按钮
            suffixStr = @"收起";
            contentStr = [NSString stringWithFormat:@"%@ %@", contentStr, suffixStr];
            H = model.titleActualH;
        } else {
            NSLog(@"就是NO");
            //文本已经收起,此时后缀为“展开/全文”按钮
            //需要对文本进行截取,将“...展开”添加到我们限制的展示文字的末尾
            NSUInteger numCount = 2; //这是cell收起状态下期望展示的最大行数
            CGFloat W = 318 - 30; //这里是文本展示的宽度
            NSString *tempStr = [self stringByTruncatingString:contentStr suffixStr:@"...展开" font:[UIFont systemFontOfSize:16] forLength:W*numCount];
            NSLog(@"tempStr = %@", tempStr);
            contentStr = tempStr;
            suffixStr = @"展开";
            H = model.titleMaxH;
        }
    }

    NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:contentStr attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]}];
    self.aux_comment.linkTextAttributes = @{};
    
    //给富文本的后缀添加点击事件
    if (suffixStr != nil && suffixStr.length > 0){
        NSRange range3 = [contentStr rangeOfString:suffixStr];
        [attStr addAttribute:NSForegroundColorAttributeName value:[UIColor systemBlueColor]range:range3];//[UIColor colorWithHexString:@"#000000"]
        NSString *valueString3 = [[NSString stringWithFormat:@"didOpenClose://%@", suffixStr] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
        [attStr addAttribute:NSLinkAttributeName value:valueString3 range:range3];
    }
    self.aux_comment.attributedText = attStr;
}


- (NSString *)stringByTruncatingString:(NSString *)str suffixStr:(NSString *)suffixStr font:(UIFont *)font forLength:(CGFloat)length {
    if (!str) return nil; // 如果 str 为 nil,返回 nil
    if (![str isKindOfClass:[NSString class]]) return nil; // 确保 str 是 NSString 类型

    // 从字符串的长度开始,逐步截断
    for (NSInteger i = str.length; i > 0; i--) {
        NSString *tempStr = [str substringToIndex:i];
        CGSize size = [tempStr sizeWithAttributes:@{NSFontAttributeName: font}];
        
        // 检查截断后的字符串宽度
        if (size.width < length) {
            // 添加 suffixStr
            NSString *resultStr = [tempStr stringByAppendingString:suffixStr];
            CGSize resultSize = [resultStr sizeWithAttributes:@{NSFontAttributeName: font}];

            // 如果加上后缀后的宽度也在限制内
            if (resultSize.width < length) {
                return resultStr; // 返回最终字符串
            }
        }
    }
    
    // 如果没有找到合适的截断位置,返回原字符串或处理为空的情况
    return suffixStr; // 如果所有情况都不满足,返回后缀
}//(这里笔者对于两行文本的判断还是有些问题,无法在第一行或者第二行空白时实现一个文本的展开和收缩,因为笔者这个方法中对于文字的截断判断存在一点问题)
  1. 最后在Controller中调用Cell中的方法,来实现即可
if([_arrLong[indexPath.row] objectForKey:@"reply_to"] != nil) {
                Modelcomment_reply_to* model = [Modelcomment_reply_to yy_modelWithDictionary:_arrLong[indexPath.row][@"reply_to"]];
                if([model isKindOfClass:[Modelcomment_reply_to class]]) {
                    NSLog(@"true");
                } else {
                    NSLog(@"model class = %@", [model class]);
                }
                NSMutableString *str = [NSMutableString stringWithString:@"//"]; // 使用 stringWithString 初始化
                [str appendFormat:@"%@", model.author]; // 追加作者
                [str appendFormat:@":"]; // 追加冒号
                [str appendFormat:@"%@", model.content];
                [model jisuan:str];
                NSLog(@"高度 = %f", model.titleActualH);
                NSLog(@"第%ld组,第%ld个", indexPath.section, indexPath.row);
                [cell setupCellData:model];
                [cell setOpenCloseBlock:^{
                    NSMutableDictionary *replyTo = self->_arrLong[indexPath.row][@"reply_to"];
                    // 获取当前的 isOpen 值
                    NSNumber *currentValue = replyTo[@"isOpen"];
                    BOOL isOpen = [currentValue boolValue]; // 转换为 BOOL
                    // 反转 isOpen 的值并存储回字典
                    replyTo[@"isOpen"] = @( !isOpen );
                    [self->_commentview.tableview beginUpdates];
                    [self->_commentview.tableview reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
                    [self->_commentview.tableview endUpdates];
                    
                }];
                
            } 

效果图:
在这里插入图片描述

收藏中心

收藏中心笔者这里使用了FMDB实现数据持久化来实现这个功能,这个功能主要就是将一些数据保存在自己的电脑中,在不同的控制器中实现一个数据的增删改查,具体来说就是在详情页中进行数据的保存和取消,在收藏中心中重新调用这些内容,让保存的数据进行网络申请展示保存的数据。
这里实现比较简单,故而笔者主要在这里介绍一下FMDB这个数据库。

FMDB 是一个用于 iOS 和 macOS 平台的 Objective-C/Swift 数据库框架,它对 SQLite 数据库进行了封装,提供了更加方便和安全的数据库操作方式。

FMDB数据库

  1. 数据库的创建
-(void) setupFMDB {
    //FMDB的创建
    NSString* doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSLog(@"doc = %@", doc);
    NSString* fileName = [doc stringByAppendingPathComponent:@"zhihuDaily.sqlite"];
    _dataBase = [FMDatabase databaseWithPath:fileName];
    if([self.dataBase open]) {
        if (result) {
            NSLog(@"创表成功");
        } else {
            NSLog(@"创表失败");
        }
    }
}
  • NSSearchPathForDirectoriesInDomains 是一个用于获取特定目录路径的函数。这里通过传入 NSDocumentDirectory(表示应用程序的文档目录)、NSUserDomainMask(表示搜索范围为当前用户)和 YES(表示展开路径中的波浪线,比如 ~/Documents 转换为实际路径)来获取应用程序文档目录的路径.
  • stringByAppendingPathComponent: 方法用于在一个路径字符串的末尾添加一个新的路径组件。这里在之前获取到的文档目录路径 doc 后面添加了 “zhihuDaily.sqlite”,构建出一个完整的数据库文件路径。这个路径表示要创建的 SQLite 数据库文件将存储在应用程序的文档目录下,文件名为 “zhihuDaily.sqlite”.

创表

BOOL result = [self.dataBase executeUpdate:@"CREATE TABLE IF NOT EXISTS collectionData (mainHeading text NOT NULL, webAPI text NOT NULL, imageURL text NOT NULL,newsID text NOT NULL);"];
        if (result) {
            NSLog(@"创表成功");
        } else {
            NSLog(@"创表失败");
        }

这里CREATE TABLE IF NOT EXISTS collectionData指的是如果collectionData这个表格不存在就创建一个名字为它的表格。后面括号中的内容是表格中所包含的数据名称,后期增删改查都需要使用到他们。

数据库的使用

这里我们主要涉及的就是数据库内容的增删改查,下面依次对这些内容进行一个说明解释。

  • 增加数据:
-(void) insertDate {
    if([_dateBase open]) {
        BOOL result = [_dateBase executeUpdate:@"INSERT INTO collectionData (mainHeading, webAPI, imageURL, newsID) VALUES(?, ?, ?, ?);", _strTitle[_numFMDB], _strurl[_numFMDB], _strImageURL[_numFMDB], _strid[_numFMDB]];
        if (!result) {
            NSLog(@"增加数据失败");
        }else{
            NSLog(@"增加数据成功");
        }
        [_dateBase close];
    }
}

这里使用的NSERT INTO collectionData 表示给collectionData这个表格中增加一行数据,后面的内容即为数据的一一对应的数据

  • 删除数据:
-(void) deleteData {
    if([_dateBase open]) {
        NSString *sql = @"delete from collectionData WHERE webAPI = ?";
        BOOL result = [_dateBase executeUpdate:sql, _strurl[_numFMDB]];
        if(result) {
            NSLog(@"删除成功");
        } else {
            NSLog(@"删除失败");
        }
        [_dateBase close];
    }
}

这里使用executeUpdate这个方法查找到所需要删除的那一行数据,从而对其进行一个删除的操作。上面字符串中的文字代表着要对这个表格执行的操作,即delete。

  • 查找数据:
-(BOOL) queryData {
    BOOL select = NO;
    if([_dateBase open]) {
        NSString* sql = @"SELECT * FROM collectionData WHERE webAPI = ?";
        FMResultSet *resultSet = [_dateBase executeQuery:sql, _strurl[_numFMDB]];
        if([resultSet next]) {
            select = YES;
        }
        [resultSet close];
        [_dateBase close];
    }
    return select;
}

这里字符串中的文字即是要给这个表格执行的操作SELECT 后面的这个WHERE webAPI则代表依据数据webAPI这一列来查找。

  • 更新数据:
// 更新数据
- (void)updateData {
    if ([self.collectionDatabase open]) {
        NSString *sql = @"UPDATE collectionData SET id = ? WHERE nameLabel = ?";
        BOOL result = [self.collectionDatabase executeUpdate:sql, @"1", @"hi world"];
        if (!result) {
            NSLog(@"数据修改失败");
        } else {
            NSLog(@"数据修改成功");
        }
        [self.collectionDatabase close];
    }
}

这里其实和上面的一样,先依据某一个数据找到这一行的数据,在对其进行一个更改的操作即可。

总结

上述即是笔者这两周进行知乎日报仿写中所遇的一些个人觉得比较重要的部分,其实我的项目中还是有一些地方需要改正,存在一些稍有不合理的地方需要继续改正,后期还会对项目进行一个完善,如果有觉得值得更改记录的地方再重新写博客进行一个记录。

;