文章目录
- 前言
- 一、首页
- 1.网络的异步请求
- 2.避免同一网络请求执行多次
- 3.下拉刷新与上拉加载的实现
- 下拉刷新
- 上拉加载
- 二、网页
- 1.webView的实现
- 2.webView的滑动加载
- 3.网页与首页内容的同步更新
- 三、评论区
- Masonory实现行高自适应
- 四、收藏中心
- 通过FMDB实现数据持久化
- 1.创建或打开数据库
- 2.数据库操作
- 建表操作
- 增加数据
- 删除数据
- 查询数据
- 修改数据库字段
- 总结
前言
近期耗时一个月完成了第一个项目知乎日报,用到了AFNetworking
,JSONModel
,Masonry
,FMDB
等一系列第三方库以及其他新学的知识点,整体采用了MVC框架进行编写,特此撰写博客总结
一、首页
实现效果:
我们通过我们的视图层级图展开我们首页功能的分析:
1.网络的异步请求
我们从上述层级示意图以及动画效果展示,我们可以看出这个页面主要是将轮播图
与Tableviewcell
共同添加到一个TableView
上实现的页面。
在这里我们用到了知乎日报提供的开源接口实现我们对图片以及数据的请求——知乎日报 API 分析(如何规范api设计)
在这里笔者是使用AFNetworking对API进行请求的。详见【iOS】使用单例封装通过AFNetworking实现的网络请求
在刚开始请求数据来构建页面时,笔者请求到的数据总是为空,因此View中总是无法显示请求的图片以及数据,或是请求到了数据,但并没有在View中显示出来,细分析其原因,是因为网络请求是一个异步且耗时的过程。笔者给出一个例子:
- (void)getCurrentModel {[[Manager shareManager] NetWorkGetWithRecentData:^(CurrentModel *model) {self.modelDictionary = [model toDictionary];for (int i = 0; i < 5; i++) {[self->_topURLArray addObject:self.modelDictionary[@"top_stories"][i][@"url"]];[self->_topIDArray addObject:self.modelDictionary[@"top_stories"][i][@"id"]];[self.topPageTitleArray addObject:self.modelDictionary[@"top_stories"][i][@"title"]];[self.topPageImageURLArray addObject:self.modelDictionary[@"top_stories"][i][@"image"]];}[self.topArray addObject:[model toDictionary]];[self.allArray addObject:[model toDictionary]];// 异步执行任务创建方法dispatch_async(dispatch_get_main_queue(), ^{[self.homeView.allArray addObject:[model toDictionary]];});} andError:^(NSError * _Nullable error) {NSLog(@"失败");}];NSLog(@"%@", _topArray);NSLog(@"%@", _allArray);[self initHomeView];}
在这段代码中我们先用[self initHomeView]
对我们的视图进行初始化,再去请求我们的数据对视图进行赋值,结果就会出现这样的页面
可以看到我们打印的结果为空,并且View中的控件没有被赋值,这就是涉及到了异步请求的知识。简单的来说就是当我们使用NetWorkGetWithRecentData:
方法时,我们的网络请求被放在了后台线程进行执行,但这并不会阻塞我们的主线程,我们的程序的主线程会继续执行。
也就是说即使我们的网络请求还没有请求成功,我们的程序也会继续向下执行,这就导致了我们的View的空白
使用
AFNetworking
等网络请求库时,它们通常会处理底层的多线程操作,将网络请求放在后台线程
中执行,以确保不会阻塞主线程
。这是为了避免在主线程中执行网络请求导致的界面卡顿和不流畅的用户体验。
现在我们利用代码来验证一下这个结论:
- (void)getCurrentModel {NSLog(@"111");[[Manager shareManager] NetWorkGetWithRecentData:^(CurrentModel *model) {self.modelDictionary = [model toDictionary];for (int i = 0; i < 5; i++) {[self->_topURLArray addObject:self.modelDictionary[@"top_stories"][i][@"url"]];[self->_topIDArray addObject:self.modelDictionary[@"top_stories"][i][@"id"]];[self.topPageTitleArray addObject:self.modelDictionary[@"top_stories"][i][@"title"]];[self.topPageImageURLArray addObject:self.modelDictionary[@"top_stories"][i][@"image"]];}[self.topArray addObject:[model toDictionary]];[self.allArray addObject:[model toDictionary]];// 异步执行任务创建方法dispatch_async(dispatch_get_main_queue(), ^{NSLog(@"222");[self initHomeView];[self.homeView.allArray addObject:[model toDictionary]];});} andError:^(NSError * _Nullable error) {NSLog(@"失败");}];NSLog(@"333");}
假如我们这段代码都在一个线程中执行,那么1,2,3会相继输出
但是结果并非如此
在网络请求中的NSLog(@"222");
是最后才输出的,这也证明了我们的网络请求是一个异步的过程。
说完了问题,我们该如何解决这个问题呢,这里就需要使用到我们的GCD—— dispatch_async(dispatch_get_main_queue(), ^{ });
使用这个方法,能在其他线程中调用我们的主线程,也就是当后台线程的进程执行完之后,返回我们的主线程进行任务执行。
这就保证了当我们的网络请求请求完之后我们才会对我们的UI进行布局,防止了UI出现空白的情况。
关于GCD的知识笔者还不甚了解,后续学到会加以补充,现在只懂简单的使用与其基本原理
2.避免同一网络请求执行多次
在进行下拉加载数据时,笔者使用了- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
协议来监听tableView.contentOffset
以此来判断是否需要获取新数据。
但是网络请求是一个耗时的过程,假如我们连续滑动页面,就会出现同一网络请求被执行多次的情况,我们的程序会因为线程阻塞而崩溃。为了避免这种情况,我们就需要对网络请求进行锁定。
笔者在代码中使用了一个BOOL变量对网络请求进行锁定,当监听到需要加载数据的时候,我们的BOOL变量就为YES,请求成功数据后再变为NO,在BOOL变量为YES时无法进行相同的网络请求,这就避免了同一网络请求执行多次。
- (void)getPastModel:(NSString *)date {if (self.isLoadingMoreData) {return;}self.isLoadingMoreData = YES; // 设置加载标志pastData++;numbersOfLoad++;date = [self.homeModel pastDateForJson:numbersOfLoad];[[Manager shareManager] NetWorkGetWithPastData:^(PastModel * _Nonnull model) {self.pastModelDictionary = [model toDictionary];
// [self.homeView.pastArray addObject:self.pastModelDictionary];[self.homeView.pastTimeArray addObject:[self.homeModel pastDate:numbersOfLoad]];[self.homeView.allArray addObject:[model toDictionary]];[self.allArray addObject:[model toDictionary]];NSLog(@"%@", self.allArray);dispatch_async(dispatch_get_main_queue(), ^{[self.homeView hideLoadMoreView];[self.homeView.tableView reloadData];self.isLoadingMoreData = NO; // 重置加载标志});} andError:^(NSError * _Nullable error) {NSLog(@"失败: %@", error); // 添加日志} andDate:date];}
3.下拉刷新与上拉加载的实现
通过首页的动画演示,我们可以看到在笔者上拉与下拉时都有对应的菊花控件显示,在这里笔者分别用了两种方法去使用菊花控件
下拉刷新
笔者使用了MJRefresh
第三方库进行菊花控件的创建,使用第三方库的创建十分简单,只有一行代码 self.homeView.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
随后我们使用loadNewData
方法模拟加载效果:
- (void)loadNewData {// 模拟网络请求加载新数据dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{// 加载完成后结束下拉刷新[self.homeView.tableView.mj_header endRefreshing];// 刷新 UITableView 的数据
// [self.homeView.tableView reloadData];});
}
上拉加载
对于上拉加载我们同样也可以使用MJRefresh,但是笔者在这里使用了UIKit
自带的控件UIActivityIndicatorView
首先在上拉时创建并展现菊花控件
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {// 用户结束拖拽手势if (self.tableView.contentOffset.y + self.tableView.frame.size.height >= self.tableView.contentSize.height) {
// self.backgroundColor = [UIColor whiteColor];[self showLoadMoreView];[[NSNotificationCenter defaultCenter] postNotificationName:@"reNew" object:nil userInfo:nil];
// NSLog(@"%d", ([self.viewModelDictionary[@"stories"] count] + 1) * pastData);
// [_tableView reloadData];}
}- (void)showLoadMoreView {// 创建和配置加载动画视图self.activityIndicator.frame = CGRectMake(0, 0, 320, 33);self.activityIndicator.tag = 123; // 设置一个标记以便后续移除[self.activityIndicator startAnimating];// 将加载动画视图添加到UIScrollView的底部self.tableView.tableFooterView = self.activityIndicator;
}- (void)hideLoadMoreView {// 停止加载动画UIActivityIndicatorView *activityIndicator = [self.tableView.tableFooterView viewWithTag:123];[activityIndicator stopAnimating];// 隐藏加载动画视图self.tableView.tableFooterView = nil;
}
随后在网络请求完成后在主线程移除菊花控件的展现
- (void)getPastModel:(NSString *)date {if (self.isLoadingMoreData) {return;}self.isLoadingMoreData = YES; // 设置加载标志pastData++;numbersOfLoad++;date = [self.homeModel pastDateForJson:numbersOfLoad];[[Manager shareManager] NetWorkGetWithPastData:^(PastModel * _Nonnull model) {self.pastModelDictionary = [model toDictionary];
// [self.homeView.pastArray addObject:self.pastModelDictionary];[self.homeView.pastTimeArray addObject:[self.homeModel pastDate:numbersOfLoad]];[self.homeView.allArray addObject:[model toDictionary]];[self.allArray addObject:[model toDictionary]];NSLog(@"%@", self.allArray);dispatch_async(dispatch_get_main_queue(), ^{[self.homeView hideLoadMoreView];[self.homeView.tableView reloadData];self.isLoadingMoreData = NO; // 重置加载标志});} andError:^(NSError * _Nullable error) {NSLog(@"失败: %@", error); // 添加日志} andDate:date];}
二、网页
网页效果实现
1.webView的实现
在我们的知乎日报API中,其给我们提供了webView的URL,我们可以通过使用WKWebView来加载我们的网页
根据MVC的原则,我们会在首页的Viewcontroller中获取我们的URL然后将其传到我们网页的Viewcontroller中,最后在View中加载我们的网页
步骤1:
导入#import <UIKit/UIKit.h>
步骤2:
_webView = [[WKWebView alloc] init];NSURL* urlWeb = [NSURL URLWithString:_allArray[(_nowPage - 5) / 5][@"stories"][_nowPage % 5][@"url"]];NSURLRequest* webRequest = [[NSURLRequest alloc] initWithURL:urlWeb];[_webView loadRequest:webRequest];
这样我们就完成了我们webView的加载
2.webView的滑动加载
同时通过动画我们可以看到每向后滑动一页,我们的webView才会进行加载,这就节约了我们的内存,减少了用户等待的时间。
笔者这里的思路是通过协议监听当前scrollerView的页数,当当前网页没有加载过便用通知传值通知View层进行Webview的加载
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {NSInteger currentPage = (scrollView.contentOffset.x / Width) + 0.5;_pageNumber = [NSNumber numberWithInteger:currentPage + 5];//滑动cell只加载当前webview,不加载多余webViewif (scrollView.tag == 77) {// 当滚动视图向右滚动且快接近画布右边缘时,触发加载数据的操作if (scrollView.contentOffset.x > ([_allArray count] * 5 * Width - Width) && self.isLoadingMoreData == NO) {self.isLoadingMoreData = YES;[self loadMoreData];} else {if (currentPage != (_webView.nowPage - 5) && isLoadingWebView == NO && self.isLoadingMoreData == NO && ![self.nowPageSet containsObject:_pageNumber]) {NSLog(@"%ld", (long)currentPage);isLoadingWebView = YES;_webView.nowPage = currentPage + 5;[self.nowPageSet addObject:_pageNumber];NSLog(@"%ld..", (long)_webView.nowPage);[[NSNotificationCenter defaultCenter] postNotificationName:@"newPage" object:nil userInfo:nil];}}}}- (void)addView {
// NSLog(@"%@111",_allArray);if (_isCollectionWebView == YES) {_scrollView.tag = 55;_scrollView.contentOffset = CGPointMake(Width * _nowPage, 0);self.scrollView.contentSize = CGSizeMake(Width * [_webURLArray count], 0);NSLog(@"%@111", _webURLArray);for (int i = 0; i < [_webURLArray count]; i++) {_webView = [[WKWebView alloc] init];NSURL* urlWeb = [NSURL URLWithString:_webURLArray[i]];NSURLRequest* webRequest = [[NSURLRequest alloc] initWithURL:urlWeb];[_webView loadRequest:webRequest];_webView.frame = CGRectMake(Width * i, 0, Width, Height * 0.92);[self.scrollView addSubview:_webView];}} else {if (_nowPage < 5) {_scrollView.tag = 66;_scrollView.contentOffset = CGPointMake(Width * _nowPage, 0);self.scrollView.contentSize = CGSizeMake(Width * 5, 0);for (int i = 0; i < 5; i++) {_webView = [[WKWebView alloc] init];NSURL* urlWeb = [NSURL URLWithString:_allArray[0][@"top_stories"][i][@"url"]];NSURLRequest* webRequest = [[NSURLRequest alloc] initWithURL:urlWeb];[_webView loadRequest:webRequest];_webView.frame = CGRectMake(Width * i, 0, Width, Height * 0.92);[self.scrollView addSubview:_webView];}} else {_scrollView.tag = 77;self.scrollView.contentSize = CGSizeMake(Width * [self.allArray count] * 5 + Width, 0);_scrollView.contentOffset = CGPointMake(Width * (_nowPage - 5), 0);_webView = [[WKWebView alloc] init];NSURL* urlWeb = [NSURL URLWithString:_allArray[(_nowPage - 5) / 5][@"stories"][_nowPage % 5][@"url"]];NSURLRequest* webRequest = [[NSURLRequest alloc] initWithURL:urlWeb];[_webView loadRequest:webRequest];_webView.frame = CGRectMake(Width * (_nowPage - 5), 0, Width, Height * 0.875);[self.scrollView addSubview:_webView];}}}
同时笔者为了实现webView的流畅滑动,在滑动到最后一页需要请求新数据时,笔者将 self.scrollView.contentSize
设为了CGSizeMake(Width * [self.allArray count] * 5 + Width, 0);
[self.allArray count]在这里也会实时更新,请求到新数据之后新数据会添加到[self.allArray count]中
在这里+Width
的意义就是当我们滑动到最后一页时,我们需要时间去等待数据的加载,如果没有多处一页空白,那么只有当数据加载完成后我们的scrollerView
才会滑动到下一页。
而+Width
则实现了无需等待,我们可以直接滑动到下一页,数据加载完成后会自动呈现在我们的scrollerView
上
- (void)layoutNewScrollView {self.scrollView.contentSize = CGSizeMake(Width * [self.allArray count] * 5 + Width, 0);
}
3.网页与首页内容的同步更新
当我们在网页页面滑动获取新数据的同时,我们返回首页时数据也需要同步更新。
这里笔者的思路是通过设置全局变量与通知传值来告诉首页我们网页的数据已经更新,同时将网页的数组同步给首页的数组,从而实现数据的同步更新。
##网页
- (void)pressButtonReturn {if (_isCollectionWebView == NO) {NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];userInfo[@"key1"] = self.allArray;userInfo[@"key2"] = self.pastTimeArray;[self.navigationController popViewControllerAnimated:YES];[[NSNotificationCenter defaultCenter] postNotificationName:@"update" object:nil userInfo:userInfo];}else {[self.navigationController popViewControllerAnimated:YES];}
}##首页
- (void)update:(NSNotification *)send {NSArray *newDataArray = send.userInfo[@"key1"];NSArray *newPastTimeArray = send.userInfo[@"key2"];if (![self.homeView.allArray isEqualToArray:newDataArray] || ![self.homeView.pastTimeArray isEqualToArray:newPastTimeArray]) {self.homeView.allArray = [newDataArray mutableCopy];self.allArray = [newDataArray mutableCopy];self.homeView.pastTimeArray = [newPastTimeArray mutableCopy];[self.homeView.tableView reloadData];}// self.homeView.allArray = send.userInfo[@"key1"];// self.allArray = send.userInfo[@"key1"];// self.homeView.pastTimeArray = send.userInfo[@"key2"];//原来的这段代码让self.homeView.allArray与self.allArray同时指向了send.userInfo[@"key1"],也就是说我之后在此对self.allArray进行修改self.homeView.allArray也会同步修改,反之亦然
}
对于如何获取全局变量可以参考以下博客:【iOS】浅析static,const,extern关键字
三、评论区
实现效果:
Masonory实现行高自适应
这个知识点笔者会专门再写一篇博客讲述,后面会将博客补上
四、收藏中心
实现效果:
通过FMDB实现数据持久化
在使用FMDB之前,我们简单了解一下什么是FMDB
FMDB 是 iOS 平台的 SQLite 数据库框架
FMDB 以 OC 的方式封装了 SQLite 的 C 语言 API
更通俗的讲,FMDB就是iOS的数据库,它支持SQL语句来执行操作
核心类 FMDB有三个主要的类
FMDatabase
一个FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句。
FMResultSet
使用FMDatabase执行查询后的结果集
FMDatabaseQueue
用于在多线程中执行多个查询或更新,它是线程安全的
笔者在这里简单介绍一下FMDB的使用步骤:
1.创建或打开数据库
- (void)createDataBase {// 获取数据库文件的路径NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];NSString *path = [docPath stringByAppendingPathComponent:@"DailyNews.sqlite"];FMDatabase *db = [FMDatabase databaseWithPath:path];if ([db open]) {NSLog(@"Open database Success");} else {NSLog(@"fail to open database");}
}
在这里我们的path会有三种情况:
具体文件路径
如果不存在会自动创建,(使用绝对路径),就像我们的此时的path为[docPath stringByAppendingPathComponent:@"DailyNews.sqlite"];
- 空字符串
@""
会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除。 nil
会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁
2.数据库操作
在FMDB中,除查询以外的所有操作,都称为“更新”
- create
- drop
- insert
- update
- delete
建表操作
语法:create table table_name (field1 type1, field2 type2, field3 type3...);
在SQLite3中可以不指定字段的数据类型,SQLite3会自动推断类型。常用的格式有:
文本: Text
整形: Integer
二进制数据: Blob
浮点型: Real Float、Double
布尔型: Boolean
时间型: Time
日期型: Date
时间戳: TimeStamp
代码示例:
if ([db open]) {BOOL result = [db executeUpdate:@"CREATE TABLE 't_DailyNews' ('webPageID' TEXT, 'webPageTitle' TEXT, 'webPageImageURL' TEXT, 'webURL' TEXT)"];if (result) {NSLog(@"创表成功");}NSLog(@"Open database Success");} else {NSLog(@"fail to open database");}
增加数据
语法:INSERT INTO table_name (field1, field2...) VALUES (var1, var2...);
代码示例:
- (void)insertDataBase {if ([_dataBase open]) {NSString *insertSql = @"insert into 't_DailyNews'(webPageID,webPageTitle,webPageImageURL, webURL) values(?,?,?,?)";BOOL result = [_dataBase executeUpdate:insertSql, _webPageID, _webPageTitle, _webPageImageURL, _webURL];if (result) {NSLog(@"添加数据成功");} else {NSLog(@"添加数据失败");}}
}
删除数据
语法:NSString *deleteSQL = @"DELETE FROM your_table WHERE condition = ?";
删除所有符合条件的数据,不设置条件时清空所有数据。
代码示例:
- (void)deleteDataBase {NSString *sql = @"delete from t_DailyNews where (webPageID) = (?) and (webPageTitle) = (?) and (webURL) = (?)";if ([_dataBase open]) {BOOL result = [_dataBase executeUpdate:sql, _webPageID, _webPageTitle, _webURL];if (result) {NSLog(@"删除数据成功");} else {NSLog(@"删除数据失败");}}
}
查询数据
语法:NSString *querySQL = @"SELECT * FROM your_table WHERE condition = ?";
代码示例:
- (void)traverseDataBase {[_dataBase open];NSString *selectSql = @"select * from t_DailyNews";FMResultSet *rs = [_dataBase executeQuery:selectSql];//FMResultSet专门用来进行查询操作while ([rs next]) {NSString *webPageID = [rs stringForColumn:@"webPageID"];NSString *webPageTitle = [rs stringForColumn:@"webPageTitle"];NSString *webURL = [rs stringForColumn:@"webURL"];if ([webPageID isEqualToString:_webPageID] && [webPageTitle isEqualToString:_webPageTitle] && [webURL isEqualToString:_webURL]) {_webView.buttonCollection.selected = YES;[_dataBase close];return;}}_webView.buttonCollection.selected = NO;[_dataBase close];return;
}
修改数据库字段
添加列:
ALTER TABLE your_table
ADD COLUMN new_column_name INTEGER;
删除列:
ALTER TABLE your_table
DROP COLUMN column_to_be_deleted;
重命名列:
ALTER TABLE your_table
RENAME COLUMN old_column_name TO new_column_name;
笔者这里仅给出添加列的示例:
- (void)xiugai {// 获取数据库文件的路径NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];NSString *path = [docPath stringByAppendingPathComponent:@"DailyNewsOfGood.sqlite"];FMDatabase *db = [FMDatabase databaseWithPath:path];// Assuming db is your FMDatabase instanceif ([db open]) {NSString *alterTableSQL = @"ALTER TABLE 't_DailyNewsOfGood' ADD COLUMN countsOfGood TEXT";BOOL success = [db executeUpdate:alterTableSQL];if (success) {NSLog(@"Table altered successfully");} else {NSLog(@"Error altering table: %@", [db lastErrorMessage]);}[db close];} else {NSLog(@"Error opening database: %@", [db lastErrorMessage]);}}
这里需要注意的是如果我们需要修改数据库中表的字段,不能在创表的代码上进行修改,因为此时数据库已经创建成功。
讲完了数据库的基本操作,我们来讲讲我们的收藏效果是如何实现的
当我们点击我们的收藏按钮时,就将当前页面的webView与ID等一系列标志添加到数据库中,当我们打开收藏中心时我们的程序首先会读取数据库中的内容,从而将我们收藏的内容展现在我们UI上。
反之删除也是同理,我们取消收藏时就以当前页面的webView与ID等一系列标志与数据库中的数据进行匹配,如果匹配成功则成功删除数据库中对应的数据。从而实现了收藏的取消
同时我们在滑动我们的webView时还会通过遍历数据库实时对当前webView是否存在于我们的数据库中进行判断
- (void)judgeCollectionInOrOut {[_dataBase open];NSString *selectSql = @"select * from t_DailyNews";FMResultSet *rs = [_dataBase executeQuery:selectSql];//FMResultSet专门用来进行查询操作while ([rs next]) {NSString *webPageID = [rs stringForColumn:@"webPageID"];NSString *webPageTitle = [rs stringForColumn:@"webPageTitle"];NSString *webURL = [rs stringForColumn:@"webURL"];if ([webPageID isEqualToString:_webPageID] && [webPageTitle isEqualToString:_webPageTitle] && [webURL isEqualToString:_webURL]) {_webView.buttonCollection.selected = YES;[_dataBase close];return;}}_webView.buttonCollection.selected = NO;[_dataBase close];return;
}
总结
知乎日报耗时一个月,在此期间碰到了许多问题,例如网络请求的异步加载问题,Masonry实现行高自适应的控件约束问题,单元格内按钮的复用问题,tableview的优化问题以及使用MVC框架中各个层的职责问题,总结便是学习iOS之路任重道远,还需更加努力