【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.mzph.cn/news/159191.shtml

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

相关文章

python中的exec()、eval()以及complie()

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 1.eval函数 函数的作用&#xff1a; 计算指定表达式的值。 也就是说它要执行的python代码只能是单个表达式&#xff08;注意eval不支持任何形式的赋值操作&…

git命令 cherry-pick

参考&#xff1a;https://blog.csdn.net/weixin_42585386/article/details/128256149 https://blog.csdn.net/weixin_44799217/article/details/128279250 merge和cherry-pick的区别&#xff1a; merge&#xff1a;是把某一个代码分支完全合并到当前的代码分支。完全合并的意…

TensorFlow实战教程(一)-TensorFlow环境部署

从本篇文章开始,作者正式开始研究Python深度学习、神经网络及人工智能相关知识。第一篇文章主要讲解神经网络基础概念,同时讲解TensorFlow2.0的安装过程及基础用法,主要结合作者之前的博客和"莫烦大神"的视频介绍,后面随着深入会讲解具体的项目及应用。基础性文章…

代码随想录刷题】Day17 二叉树04

文章目录 1.【110】平衡二叉树&#xff08;优先掌握递归&#xff09;1.1 题目描述1.2 解题思路1.3 java代码实现 2.【257】二叉树的所有路径&#xff08;优先掌握递归&#xff09;2.1 题目描述2.2 解题思路2.3 java代码实现 3.【404】左叶子之和&#xff08;优先掌握递归&#…

Spring 配置

配置文件最主要的目的 : 解决硬编码的问题(代码写死) SpringBoot 的配置文件,有三种格式 1.properties 2.yaml 3.yml(是 yaml 的简写) SpringBoot 只支持三个文件 1.application.properties 2.application.yaml 3.application.yml yaml 和 yml 是一样的,学会一个就行…

一文让你上手Linux常用命令(考前十分钟快速突击+零基础阅读)

文章目录 前言Linux 常用命令1. 基本操作lscdpwd 2. 对文件的操作touchcatechovim 3. 对目录的操作mkdirrm 4. 移动文件 / 目录的操作cpmv 5. 总结基本操作6. 必不可少的实用操作mangreppsnetstat 总结 前言 本文内容为 Linux 的一些超常用命令, 内容不多且十分实用, 这些命令…

三、Keil安装芯片包、下载固件库、建立STM32工程模板

目录 一、首先在Keil软件上安装好芯片包 二、下载官方固件库 三、建立基于固件库的Keil5工程模板 一、首先在Keil软件上安装好芯片包 STM32有很多系列的芯片&#xff0c;我们平常用的最多的是STM32F1系列的&#xff0c;因此安装F1系列的芯片包在我们初学时&#xff0c;只按照…

设计模式系列:三、责任链设计模式

一、概述 责任链模式是一种行为设计模式&#xff0c;它允许多个对象处理一个请求&#xff0c;从而避免了请求的发送者和接收者之间的耦合关系。 优点是把任务划分为一个一个的节点&#xff0c;然后按照节点之间的业务要求、顺序&#xff0c;把一个个节点串联起来&#xff0c;…

如何使用浏览器自动化框架Playwright开发“万媒易发”实现多平台自动发布文章?

作为一名程序员和开发者&#xff0c;我深知在多个媒体平台手动发布和管理文章的痛苦。因此&#xff0c;我决定使用Playwright浏览器自动化框架&#xff0c;开发一款名为“万媒易发”的工具&#xff0c;实现多平台自动发布文章。下面我将分享这款工具的开发过程和成果&#xff0…

浅析教学型数控车床使用案例

教学型数控车床是一种专为教学和培训设计的机床&#xff0c;它具有小型化、高精度和灵活性的特点&#xff0c;可以作为学校和技术学院的培训机器。下面是一个使用案例&#xff0c;以展示教学型数控车床在教学实训中的应用。 案例背景&#xff1a; 某职业技术学院的机械工程专业…

Socket通信之网络协议基本原理

一台机器将自己想要表达的内容&#xff0c;按照某种约定好的格式发送出去&#xff0c;当另外一台机器收到这些信息后&#xff0c;也能够按照约定好的格式解析出来&#xff0c;从而准确、可靠地获得发送方想要表达的内容。这种约定好的格式就是网络协议&#xff08;Networking P…

VR全景航拍要注意什么,航拍图片如何处理

引言: VR全景航拍技术是当前摄影和航拍领域的新潮流。它采用虚拟现实技术&#xff0c;通过360度全景镜头捕捉画面&#xff0c;可以为观众提供身临其境的视觉体验。在宣传展示中&#xff0c;利用VR全景航拍技术可以为品牌宣传带来更加生动、震撼的视觉效果。 一、航拍注意事项 …

【追求卓越01】数据结构--数组

引导 这一章节开始&#xff0c;正式进入数据结构与算法的学习过程中。由简到难&#xff0c;先开始学习最基础的数据结构--数组。 我相信对于数组&#xff0c;大家肯定是不陌生&#xff0c;因为数组在大多数的语言中都有&#xff0c;也是大家在编程中常常会接触到的。我不会说数…

jQuery实现横版手风琴效果

一、实现效果 当鼠标滑过方块的时候&#xff0c;方块的状态就会发生如下图所示的变化&#xff0c;同理当鼠标滑到其他的方块也会发生同样的效果&#xff0c;不仅大小会改变同时方块的颜色也会跟着发生变化&#xff1a; 二、代码实现 <!DOCTYPE html> <html><h…

笔记58:Encoder-Decoder 架构

本地笔记地址&#xff1a;D:\work_file\&#xff08;4&#xff09;DeepLearning_Learning\03_个人笔记\3.循环神经网络\第9章&#xff1a;动手学深度学习~现代循环神经网络 a a a a a a a a a

SpringBoot 导入其他配置文件

默认情况下&#xff0c;springboot 初始的项目中都有一个 application.yml 或者 application.properties 文件&#xff0c;如果我们希望再定义一个独立的配置文件用来配置特定业务数据&#xff0c;而不希望把这些配置内容都堆积在 application 配置文件中&#xff0c;实现这个需…

python命令行交互 引导用户选择宠物

代码 以下代码将在命令行中&#xff0c;引导用户选择一个或者多个宠物&#xff0c;并反馈用户选择的宠物 # -*- coding:UTF-8 -*- """ author: dyy contact: douyaoyuan126.com time: 2023/11/22 15:19 file: 在命令行中引导用户选择宠物.py desc: xxxxxx &qu…

好题分享(2023.11.12——2023.11.18)

目录 ​ 前情回顾&#xff1a; 前言&#xff1a; 题目一&#xff1a;《有效括号》 思路&#xff1a; 总结&#xff1a; 题目二&#xff1a;《用队列实现栈》 思路&#xff1a; 总结&#xff1a; 题目三&#xff1a;《用栈实现队列》 思路&#xff1a; 总结 &#x…

WPF实战项目十五(客户端):RestSharp的使用

1、在WPF项目中添加Nuget包&#xff0c;搜索RestSharp安装 2、新建Service文件夹&#xff0c;新建基础通用请求类BaseRequest.cs public class BaseRequest{public Method Method { get; set; }public string Route { get; set; }public string ContenType { get; set; } &quo…

Node.js之http模块

http模块是什么&#xff1f; http 模块是 Node,js 官方提供的、用来创建 web 服务器的模块。通过 http 模块提供的 http.createServer() 方法&#xff0c;就能方便的把一台普通的电脑&#xff0c;变成一台Web 服务器&#xff0c;从而对外提供 Web 资源服务。 如果我们想在node…