Bootstrap

【iOS】知乎日报总结

前言

  在系统学习了OC语言和UI控件后,知乎日报是第一个比较大的项目,耗时一个多月时间,里面使用到了YYModel、Masonry、AFNetworking、FMDB等多个第三方库。项目总体采用MVC大框架,下面是对这段时间知乎日报项目的总结。

首页

  首页主要是由一个轮播图和一个TableViewCell构成的,其中都要通过知乎日报的API对顶部轮播图和下面的实时文章进行网络请求。
在这里插入图片描述

网络请求

  在这里进行网络请求时,笔者将所有的网络请求封装到了一个manager类里,在需要时可直接通过manager单例进行方法调用。这里可以参考【iOS】使用AFNetworking进行网络请求
【iOS】使用一个单例通过AFNetworking来实现网络请求
该项目中使用到的开源API可参考:知乎日报 API 分析(如何规范api设计)

这里以对顶部的文章进行网络请求为例:

//Manager.h
+ (instancetype)sharedManager;

- (void)requestTopStoriesData: (LatestStoriesBlock)success failure: (ErrorBlock)failure;

//Manager.m
static id manager = nil;

+ (instancetype)sharedManager {
    if (!manager) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            manager = [[Manager alloc] init];
        });
    }
    return manager;
}

- (void)requestTopStoriesData:(LatestStoriesBlock)success failure:(ErrorBlock)failure {
    [[AFHTTPSessionManager manager] GET:@"https://news-at.zhihu.com/api/4/news/latest" parameters: nil progress: nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        LatestStoriesModel* latestStoriesModel = [[LatestStoriesModel alloc] initWithDictionary: responseObject error: nil];
        success(latestStoriesModel);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        failure(error);
    }];
}

对当天推送内容和过往内容的申请类似,就不多赘述了。

轮播图

至于顶部的轮播图,跟之前ZARA和网易云仿写项目的轮播图有异曲同工之妙。

//MainViewController.h
@property (nonatomic, strong)NSMutableArray* imageViewArray;
@property (nonatomic, strong)NSMutableArray* titleArray;
@property (nonatomic, strong)NSMutableArray* userNameArray;

//MainViewController.m
- (void) topStroies {
    self.imageViewArray = [[NSMutableArray alloc] init];
    self.titleArray = [[NSMutableArray alloc] init];
    self.userNameArray = [[NSMutableArray alloc] init];
    
    UIImageView* firstImageView = [[UIImageView alloc] init];
    Top_Stories* top_stories = self.latestStoriesModel.top_stories[4];
    [Manager setImage: firstImageView WithString: top_stories.image];
    [self.imageViewArray addObject: firstImageView];
    
    for (Top_Stories* top_stories in self.latestStoriesModel.top_stories) {
        UIImageView* imageView = [[UIImageView alloc] init];
        
        [Manager setImage: imageView WithString: top_stories.image];
        
        [self.imageViewArray addObject: imageView];
    
        [self.titleArray addObject: top_stories.title];
        [self.userNameArray addObject: top_stories.hint];
    }
    
    UIImageView* lastImageView = [[UIImageView alloc] init];
    top_stories = self.latestStoriesModel.top_stories[0];
    [Manager setImage: lastImageView WithString: top_stories.image];
    [self.imageViewArray addObject: lastImageView];
    self.mainView.imageViewArray = self.imageViewArray;
    self.mainView.titleArray = self.titleArray;
    self.mainView.userNameArray = self.userNameArray;
}

上滑加载

对于过往文章的上滑加载,我们只需要进行通知传值,然后请求数据后更新主页面的tableView即可。

- (void)viewDidLoad {
    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(loadBeforeStories) name: @"requestBeforeStories" object: nil];
}

- (void)requestBeforeStories {
    StoriesModel* storiesModel = [self.beforeStoriesModelArray lastObject];
    
    NSString* before1Date = storiesModel.date;
    [self.manager requestBeforeDate: before1Date beforeStoriesData:^(StoriesModel * _Nonnull beforeStoriesModel) {
            [self.beforeStoriesModelArray addObject: beforeStoriesModel];
            [self sendViewStories: beforeStoriesModel];
            
        NSString* before2Date = [DateModel getBeforeDateWithTimeString: beforeDate];
        [self.manager requestBeforeDate: before2Date beforeStoriesData:^(StoriesModel * _Nonnull beforeStoriesModel) {
                    [self.beforeStoriesModelArray addObject: beforeStoriesModel];
                    [self sendViewStories: beforeStoriesModel];
            
            NSString* before3ForeDate = [DateModel getBeforeDateWithTimeString: foreForeDate];
            [self.manager requestBeforeDate: before3Date beforeStoriesData:^(StoriesModel * _Nonnull beforeStoriesModel) {
                            [self.beforeStoriesModelArray addObject: beforeStoriesModel];
                            [self sendViewStories: beforeStoriesModel];
                            dispatch_async(dispatch_get_main_queue(), ^{
                                self.mainView.isLoading = NO;
                                [self.mainView.tableView reloadData];
                            });
                        } failure:^(NSError * _Nonnull error) {
                            NSLog(@"请求前三天消息失败");
                        }];
                } failure:^(NSError * _Nonnull error) {
                    NSLog(@"请求前两天消息失败");
                }];
        } failure:^(NSError * _Nonnull error) {
            NSLog(@"请求前一天消息失败");
        }];
}

主页面这里在程序运行加载view时可能会出现view空白的情况,查询资料了解到可能是线程的问题,我们是在Controller中调用的网络请求,请求下来的数据会赋值给View的各种属性,然后UI就会将这些数据展现出来,但请求是需要时间的,而UI的布局会比网络请求快,所以如果在viewDidLoad里面先初始化view,那么数据还没请求下来,UI就已经布局,导致我们的View里的UI控件是空白的,为解决这一问题,这里需要用到GCD的一个方法:

dispatch_async(dispatch_get_main_queue(), ^{
    [self sendViewStories: latestStoriesModel];
    [self topStories];
    [self setViewAndModel];
    [self requestBeforeStoriesWithDate: self.latestStoriesModel.date];
});

dispatch_async(dispatch_get_main_queue(), ^{ … }):这个函数调用将一个代码块提交到主队列(主线程)上异步执行。这通常用于确保UI更新和其他需要在主线程执行的操作能够正确运行,特别是在从后台线程返回结果时。

这里笔者对GCD的学习还较为浅显,只是能用于解决UI更新冲突的问题,至于具体的原理和GCD更多的使用方法,笔者后期还会再进行学习。

图片请求

在对图片进行网络请求时,请求到的图片内容都是url,此时我们没法直接将其转化为图片形式,所以就要使用一个第三方库——SDWebImage库,这个库可以将我们请求到的url转为图片,其用法如下:
首先,我们在podfile文件中导入该库,然后使用终端命令导入项目,跟之前Masonry库的导入步骤一样,然后在我们需要获取图片的文件里导入SDWebImage的头文件,之后获取我们通过网络请求到的图片的url,使用 SDWebImage 中的 sd_setImageWithURL: 方法将网络图片加载到 UIImageView 中。

NSString *string = [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

NSURL* url = [NSURL URLWithString:string];
    
[self.imageView sd_setImageWithURL: url placeholderImage: [UIImage imageNamed: @"placeholder.png"]];

文章详情页

  在文章详情页主要实现一个文章内容的布局,文章底部工具栏的设置和文章的点赞、收藏功能及其的持久化,还有文章评论区页面的展开。
在这里插入图片描述

WKWebView的使用

WKWebView 是苹果在 iOS 8 及以后版本中引入的一个用于加载和显示网页内容的类,它是 WebKit 框架的一部分,用于替代旧的 UIWebView。WKWebView 提供了更好的性能、更强的功能和更高的安全性。
要在iOS应用中使用 WKWebView,需要导入 WebKit 框架,并创建 WKWebView 的实例。

#import <WebKit/WebKit.h>

// 创建一个 WKWebView 实例
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame];

// 加载一个网页
NSURL *url = [NSURL URLWithString:@"https://www.example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];

在编写文章详情页时,刚开始会出现UI加载空白即WKWebView网页加载不出来的情况,查询资料并检查代码之后,发现是App Transport Security Settings(ATS)设置问题:
如果没有在 Info.plist 文件中设置 App Transport Security Settings,加载 https 链接可能会失败。需要在 Info.plist 中添加 NSAppTransportSecurity 字典,并将 NSAllowsArbitraryLoads 设置为 YES 以允许连接到任何域。

在这里插入图片描述

App Transport Security (ATS) 是苹果公司在其移动操作系统 iOS 9 和 OS X 10.11 El Capitan 及更高版本中引入的一项安全特性。ATS 的目的是提高应用程序的网络安全性,确保所有的网络通信都通过安全的方式进行。
强制使用 HTTPS:
ATS 要求应用程序的所有网络请求都必须通过 HTTPS 发送,以保护用户数据免受中间人攻击和窃听。
数据加密:
ATS 强制使用强加密标准,包括 TLS 1.2 或更高版本,并要求使用前向保密(Forward Secrecy)和强加密套件。
限制明文请求:
ATS 限制了对明文 HTTP、FTP 或非加密的 WebSocket 连接的请求,以防止敏感信息在传输过程中被截获。
允许自定义:
开发者可以在 Info.plist 文件中配置 ATS 设置,以适应特定的网络通信需求,例如允许特定的非安全连接或为特定域名设置例外。
内容安全策略:
ATS 鼓励开发者使用内容安全策略(CSP)来减少跨站脚本(XSS)攻击的风险。
网络代理:
ATS 支持网络代理的使用,允许应用程序通过代理服务器进行网络通信。
应用审核:
苹果在应用审核过程中会检查 ATS 的合规性,如果开发者需要为应用启用非安全的网络连接,需要在应用审核时提供充分的理由。
隐私保护:
ATS 有助于保护用户的隐私,因为它限制了第三方跟踪用户网络行为的能力。
逐步淘汰:
从 iOS 11 和 macOS 10.13 High Sierra 开始,苹果鼓励开发者使用更现代的网络技术,如 WKWebView,这些技术默认启用 ATS 并提供更好的性能和安全性。

点赞、收藏持久化——FMDB的使用

使用FMDB库之前,先通过 Cocoapods 引入到项目中,使用 pod ‘FMDB’ 命令进行安装。
关于FMDB的具体用法见:【iOS】iOS的轻量级数据库——FMDB

这里以对收藏功能实现持久化为例:

- (void)createStoriesCollect {
    NSString* doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString* filename = [doc stringByAppendingPathComponent: @"collectDatabase.sqlite"];
    
    self.collectDatabase = [FMDatabase databaseWithPath: filename];
    
    if ([self.collectDatabase open]) {
        BOOL result = [self.collectDatabase executeUpdate: @"CREATE TABLE IF NOT EXISTS collectDatabase (idLabel text NOT NULL)"];
        
        if (result) {
            FMResultSet* resultSet = [self.collectDatabase executeQuery: @"SELECT * FROM collectDatabase"];
            while ([resultSet next]) {
                [self.storiesCollect addObject: [resultSet stringForColumn: @"idLabel"]];
            }
            NSLog(@"create succeed");
        } else {
            NSLog(@"create error");
        }
        [self.collectDatabase close];
    }
}

- (void)saveStoriesCollect {
    if ([self.collectDatabase open]) {
        for (NSString* ID in self.storiesCollectSet) {
            FMResultSet* resultSet = [self.collectDatabase executeQuery: @"SELECT * FROM collectDatabase WHERE idLabel = ?", ID];
            if (![resultSet next]) {
                BOOL result = [self.collectDatabase executeUpdate: @"INSERT INTO collectDatabase (idLabel) VALUES (?)", ID];
                if (result) {
                    NSLog(@"insert succeed");
                } else {
                    NSLog(@"insert error");
                }
            }
        }
        [self.collectDatabase close];
    }
}

- (void)deleteCollectWithID:(NSString*)ID {
    if ([self.collectDatabase open]) {
        BOOL result = [self.collectDatabase executeUpdate: @"delete from collectDatabase WHERE idLabel = ?", ID];
        if (result) {
            NSLog(@"delete succeed");
        } else {
            NSLog(@"delete error");
        }
    }
}

其他问题

沙盒问题

刚开始写的时候运行项目会Failed,然后出现这样的报错:Sandbox: rsync.samba(27013) deny(1) file-write-create /Users /…

查询完资料说是:

在 iOS 项目中,特别是在使用 Xcode 时,沙盒环境阻止了应用写入不被允许的文件或目录。

解决方法:在 Xcode 中,在项目的 Build Settings 中搜索 User Script Sandboxing 并将其值设置为 NO 。
在这里插入图片描述

单元格点击

单元格点击后自动取消选中需要使用deselectRowAtIndexPath: animated:方法。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == 1) {
        [[NSNotificationCenter defaultCenter] postNotificationName: @"didselectCollection" object: nil userInfo: nil];
    }
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

其他

xcode SDK does not contain ‘libarclite‘问题解决方法:
在这里插入图片描述
Framework ‘Pods_______’ not found问题解决参考:
framework not found Pods_xxx xcode打包错误解决方法

Linker command failed with exit code 1 (use -v to see invocation)问题解决参考:
Xcode出现( linker command failed with exit code 1)错误总结

总结

  知乎日报是第一个比较大的项目,耗时一个多月时间,在完成过程中遇到了很多问题,也学到了很多,比如YYModel、AFNetworking、SDWebImage、FMDB等的用法,后续还会继续学习GCD等关于iOS项目线程的问题。

;