前言
在系统学习了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项目线程的问题。