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

【iOS】知乎日报

文章目录

  • 前言
  • 一、首页
    • 1.网络的异步请求
    • 2.避免同一网络请求执行多次
    • 3.下拉刷新与上拉加载的实现
      • 下拉刷新
      • 上拉加载
  • 二、网页
    • 1.webView的实现
    • 2.webView的滑动加载
    • 3.网页与首页内容的同步更新
  • 三、评论区
    • Masonory实现行高自适应
  • 四、收藏中心
    • 通过FMDB实现数据持久化
      • 1.创建或打开数据库
      • 2.数据库操作
        • 建表操作
        • 增加数据
        • 删除数据
        • 查询数据
        • 修改数据库字段
  • 总结


前言

近期耗时一个月完成了第一个项目知乎日报,用到了AFNetworkingJSONModelMasonryFMDB等一系列第三方库以及其他新学的知识点,整体采用了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之路任重道远,还需更加努力

相关文章:

【iOS】知乎日报

文章目录 前言一、首页1.网络的异步请求2.避免同一网络请求执行多次3.下拉刷新与上拉加载的实现下拉刷新上拉加载 二、网页1.webView的实现2.webView的滑动加载3.网页与首页内容的同步更新 三、评论区Masonory实现行高自适应 四、收藏中心通过FMDB实现数据持久化1.创建或打开数…...

python实现自动刷平台学时

背景 前一阵子有个朋友让我帮给小忙&#xff0c;因为他每学期都要看视频刷学时&#xff0c;一门平均需要刷500分钟&#xff0c;一学期有3-4门需要刷的。 如果是手动刷的话&#xff0c;比较麻烦&#xff0c;能否帮他做成自动化的。搞成功的话请我吃饭。为了这顿饭&#xff0c;咱…...

Vue3-pnpm包管理器创建项目

一些优势&#xff1a;比同类工具快2倍左右、节省磁盘空间 官网&#xff1a;pnpm - 速度快、节省磁盘空间的软件包管理器 | pnpm中文文档 | pnpm中文网 npm升级到yarn再升级到pnpm&#xff08;速度更快&#xff09; 安装方式&#xff1a;npm install -g pnpm 创建项目&#…...

Centos上安装Docker和DockerCompose

安装Docker Docker可以运行在MAC&#xff0c;Windows&#xff0c;CtenOS,UBUNTU等操作系统上。目前主流的版本有Docker CE和Docker EE&#xff0c;CE是免费的开源Docker版本&#xff0c;适用于开发人员和小型团队&#xff0c;EE是适用于企业的容器化解决方案。它基于Docker CE…...

视频文件+EasyDarwin做摄像机模拟器模拟RTSP流很方便,还能做成系统服务,方法与流程

之前我看到过一家人工智能做算法的企业&#xff0c;用EasyDarwinFFMPEG做了一个摄像机的模拟器&#xff0c;方法大概是&#xff1a; 用ffmpeg读取mp4等类型的视频文件&#xff08;当然ffmpeg啥都能读取&#xff09;&#xff0c;再以RTSP协议的形式推送给EasyDarwin&#xff1b…...

修改Linux系统的网络参数

修改Linux系统的网络参数 接收缓冲区是用来存储从网络接口接收到的数据的一块内存区域。通过增大接收缓冲区的大小&#xff0c;可以提高网络传输的性能&#xff0c;特别是在处理大量数据或高负载情况下。 sudo sysctl -w net.core.rmem_max2097152 sudo sysctl -w net.core.r…...

virtualList 封装使用 虚拟列表 列表优化

虚拟列表 列表优化 virtualList 组件封装 virtualList 组件封装 本虚拟列表 要求一次性加载完所有数据 不适合分页 新建一个select.vue 组件页面 <template><div> <el-select transfer"true" :popper-append-to-body"true"popper-class…...

HCIP-九、路由控制

九、路由控制 实验拓扑实验需求及解法1.企业生产网运行 OSPF&#xff0c;完成以下需求&#xff1a;2.数据中心运行 ISIS3.路由引入4.路由策略5.策略路由6.ISP 过滤私网路由 实验拓扑 实验需求及解法 1.企业生产网运行 OSPF&#xff0c;完成以下需求&#xff1a; 1.1 OSPF 进程…...

Vue3水印(Watermark)

APIs 参数说明类型默认值必传width水印的宽度&#xff0c;默认值为 content 自身的宽度numberundefinedfalseheight水印的高度&#xff0c;默认值为 content 自身的高度numberundefinedfalserotate水印绘制时&#xff0c;旋转的角度&#xff0c;单位 number-22falsezIndex追加…...

redis的性能管理、主从复制和哨兵模式

一、redis的性能管理 redis的数据时缓存在内存中的 查看系统内存情况 info memory used_memory:853688 redis中数据占用的内存 used_memory_rss:10522624 redis向操作系统申请的内存 used_memory_peak:853688 redis使用内存的峰值 系统巡检&#xff1a;硬件巡检、数据库 n…...

排序算法:归并排序、快速排序、堆排序

归并排序 要将一个数组排序&#xff0c;可以先将它分成两半分别排序&#xff0c;然后再将结果合并&#xff08;归并&#xff09;起来。这里的分成的两半&#xff0c;每部分可以使用其他排序算法&#xff0c;也可以仍然使用归并排序&#xff08;递归&#xff09;。 我看《算法》…...

Redis 面试题——持久化

目录 1.概述1.1.Redis 的持久化功能是指什么&#xff1f;1.2.Redis 有哪些持久化机制&#xff1f; 2.RDB2.1.什么是 RDB 持久化&#xff1f;2.2.Redis 中使用什么命令来生成 RDB 快照文件&#xff1f;2.3.如何在 Redis 的配置文件中对 RDB 进行配置&#xff1f;2.4.✨RDB 持久化…...

Linux使用固定ip地址

设置静态ip&#xff0c;我们就需要修改 /etc/sysconfig/network-scripts/ifcfg-ens33 配置文件。 vim /etc/sysconfig/network-scripts/ifcfg-ens33 //进入网卡ens33的配置页面 (1) 将 BOOTPROTO dhcp 改成 BOOTPROTO static 也就是将动态ip&#xff0c;改成静态i…...

ESP Multi-Room Music 方案:支持音频实时同步播放 实现音乐互联共享

项目背景 随着无线通信技术的发展&#xff0c;针对不同音频应用领域的无线音频产品正不断涌现。近日&#xff0c;乐鑫科技推出了基于 Wi-Fi 的多扬声器互联共享音乐通信协议——ESP Multi-Room Music 方案。该方案使用乐鑫自研的基于 Wi-Fi 局域网的音频同步播放技术&#xff…...

java分布式锁分布式锁

java分布式&锁&分布式锁 锁 锁的作用&#xff1a;有限资源的情况下&#xff0c;控制同一时间段&#xff0c;只有某些线程&#xff08;用户/服务器&#xff09;能访问到资源。 锁在java中的实现&#xff1a; synchronized关键字并发包的类 缺点&#xff1a;只对单个的…...

2. 流程控制|方法|数组|二维数组|递归

文章目录 流程控制代码块选择结构循环结构跳转控制关键字 方法方法的概述方法的重载Junit单元测试初识全限定类名 Debug 小技巧数组数组的基本概念数组的基本使用数组的声明数组的初始化 JVM内存模型什么是引用数据类型基本数据类型和引用数据类型的区别堆和栈中内容的区别 数组…...

22. 自动装配有哪些限制(需要注意)?

自动装配有哪些限制(需要注意)&#xff1f; 一定要声明set方法覆盖&#xff1a; 你仍可以用 < constructor-arg >和 < property > 配置来定义依赖&#xff0c;这些配置将始终覆盖自动注入。基本数据类型&#xff1a;不能自动装配简单的属性&#xff0c;如基本数据…...

14 网关实战:网关聚合API文档

上节课介绍了网关层的认证鉴权,今天这节介绍一下网关层如何聚合API接口文文档。 为什么需要聚合API接口文档? 大型微服务系统模块众多,木谷博客系统就有9个,如果这些服务的接口地址没有一个统一,那么客户端将要保存每个服务的接口地址,这个肯定是不现实。 先来看一下A…...

css 固定按钮到页面顶部或者底部的实现方式

实现方式 要将按钮固定到顶部或底部&#xff0c;可以使用CSS的定位属性来实现。下面是一种常用的方法&#xff1a; 创建一个包含按钮的HTML元素&#xff0c;例如一个<div>元素。确保给它添加一个唯一的id&#xff0c;以便在CSS中进行定位。 <div id"myButton&qu…...

【Java Spring】SpringBoot常用插件

文章目录 1、Lombok1.1 IDEA社区版安装Lombok1.2 IDEA专业版安装Lombok1.3 Lombok的基本使用 2、EditStarters2.1 IDEA安装EditStarters2.2 EditStarters基本使用方法 1、Lombok 是简化Java开发的一个必要工具&#xff0c;lombok的原理是编译过程中将lombok的注解给去掉并翻译…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

SciencePlots——绘制论文中的图片

文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了&#xff1a;一行…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

Neo4j 集群管理:原理、技术与最佳实践深度解析

Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

基于Docker Compose部署Java微服务项目

一. 创建根项目 根项目&#xff08;父项目&#xff09;主要用于依赖管理 一些需要注意的点&#xff1a; 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件&#xff0c;否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

【决胜公务员考试】求职OMG——见面课测验1

2025最新版&#xff01;&#xff01;&#xff01;6.8截至答题&#xff0c;大家注意呀&#xff01; 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:&#xff08; B &#xff09; A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

ElasticSearch搜索引擎之倒排索引及其底层算法

文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...

【JavaSE】绘图与事件入门学习笔记

-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角&#xff0c;以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向&#xff0c;距离坐标原点x个像素;第二个是y坐标&#xff0c;表示当前位置为垂直方向&#xff0c;距离坐标原点y个像素。 坐标体系-像素 …...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...

阿里云Ubuntu 22.04 64位搭建Flask流程(亲测)

cd /home 进入home盘 安装虚拟环境&#xff1a; 1、安装virtualenv pip install virtualenv 2.创建新的虚拟环境&#xff1a; virtualenv myenv 3、激活虚拟环境&#xff08;激活环境可以在当前环境下安装包&#xff09; source myenv/bin/activate 此时&#xff0c;终端…...