【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之路任重道远,还需更加努力

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/209200.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

京东内部员工,爆料工资与公积金收入!

精彩回顾&#xff1a;进了央企&#xff0c;拿了户口&#xff0c;却感觉被困住了。 每个企业都有它的一套规则&#xff0c;哪些人适合加薪&#xff0c;哪些人适合拿奖金&#xff0c;哪些人适合给股票期权等等。但是说实话&#xff0c;很多人都只能拿底薪&#xff0c;这些福利啥的…

2018-2022年富时罗素 ESG评分数据

2018-2022年富时罗素 ESG评分数据 1、时间&#xff1a;2018-2022年 2、指标&#xff1a;证券代码、证券简称、富时罗素ESG评分、 3、说明&#xff1a; 富时罗素ESG评级体系评估了中国大陆、香港、欧洲以及美国等市场上1800家中国上市企业股票&#xff0c;评估了7200多种证券…

详解深度学习中的图神经网络GNN

引言 图神经网络GNN是深度学习的一个分支。 深度学习的四个分支对应了四种常见的数据格式&#xff0c;前馈神经网络FNN处理表格数据&#xff0c;表格数据可以是特征向量&#xff0c;卷积神经网络CNN处理图像数据&#xff0c;循环神经网络RNN处理时序数据&#xff0c;图神经网…

CSGO游戏搬砖市场下跌分析,是跑还是入?

CSGO市场下跌分析&#xff0c;是跑还是入&#xff1f; 以下所有都是阿阳本人最近几年观察市场和踩坑的一点经验&#xff0c;由于篇幅不长所以肯定会很浅薄&#xff0c;大伙下嘴轻点 。 首先现在真的是CSGO市场最低点吗&#xff1f;后续还会跌吗&#xff1f;我们究竟是该继续观…

C++三大特性——继承

目录 一.继承的概念及定义 1.1继承的概念 1.2 继承定义 1.2.1定义格式 1.2.2继承关系和访问限定符​编辑 1.2.3继承基类成员访问方式的变化 二.基类和派生类对象赋值转换 三.继承中的作用域 四.派生类的默认成员函数 五.继承与友元 六.继承与静态成员 一.继承的概念及…

stable-diffusion官方版本地复现手册(2023年4月10日)

stable diffusion v1.4官方版&#xff08;CompVis&#xff09;本地复现手册 主页&#xff1a;https://github.com/wjxpro 邮箱&#xff1a;804359553qq.com 文章目录 **stable diffusion v1.4官方版&#xff08;CompVis&#xff09;本地复现手册**〇、说明环境 一、准备1. [s…

leetcode:环形链表的入环点

题目描述 题目链接:力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目分析 我们假设起点到环的入口点的距离是L&#xff0c;入口点到相遇点的距离是X&#xff0c;环的长度是C 那么画图我们可以得知&#xff1a; 从开始到相遇时slow走的距离是LX从…

HarmonyOS ArkTS HTTP数据请求(九)

1 概述 日常生活中我们使用应用程序看新闻、发送消息等&#xff0c;都需要连接到互联网&#xff0c;从服务端获取数据。例如&#xff0c;新闻应用可以从新闻服务器中获取最新的热点新闻&#xff0c;从而给用户打造更加丰富、更加实用的体验。 那么要实现这样一种能实时从服务…

B033-Servlet交互 JSP

目录 ServletServlet的三大职责跳转&#xff1a;请求转发和重定向请求转发重定向汇总请求转发与重定向的区别用请求转发和重定向完善登录 JSP第一个JSP概述注释设置创建JSP文件默认字符编码集 JSP的java代码书写JSP的原理三大指令九大内置对象改造动态web工程进行示例内置对象名…

猫罐头多久喂一次?好用的猫罐头牌子推荐

猫爱吃猫罐头&#xff0c;包含各种美味&#xff0c;提供营养和口感。但喂猫吃罐头需技巧和耐心&#xff0c;以确保猫健康快乐成长。 作为一个从业宠物营养师7年的人&#xff0c;可以说对于猫咪的食物很有研究和猫罐头品牌选购上&#xff0c;我有自己的见解。 一、猫罐头多久喂…

第四代智能井盖传感器:智能井盖位移监测

当城市道路上的井盖出现异常时&#xff0c;可能会导致突发的交通事故或人员受伤事件。而传统的井盖监测往往依靠人力进行巡查&#xff0c;这种方式可能会因为监测不及时或不准确而带来问题。但是现在有了智能井盖传感器&#xff0c;它们成为了城市地下生命线的守护者。这种智能…

【数据库】数据库中的检查点Checkpoint,数据落盘的重要时刻

检查点(checkpoint) ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专栏会定…