前言
本着實踐為主的原則,此系列文章不做過多的概念性的闡述和討論;更多的代碼和篇幅用來展示MVC和MVVC下的基礎代碼結構與具體實現,來展示各自優劣.這篇文章,更多的在於發掘MVC與MVVC的共性,以期為那些對MVVC感興趣的iOS開發者,找到一種平滑的過渡與重構代碼的方式.如果對MVVC感興趣,可以直接將本文的大部分代碼引用到自己的項目中,畢竟代碼是寫出來的!開篇之前,你可以先到這里下載本文的示例工程: https://github.com/ios122/ios122
在這一篇章里,我會分別使用我所理解的MVC與MVVC兩種模式來完成同一個應用場景,以期幫助那些熟悉傳統MVC模式代碼的iOS攻城獅,能更好理解MVVC.限於篇幅,將MVC和MVVM拆分為兩個部分,今天要說的是一個典型的MVC的應用場景,為基於MVC的MVVM重構做個基礎.這篇文章着重進行了接口准備,必須的知識點的說明等內容.
預設場景:按分類請求一組博客,點擊獲取博客詳情
我們選取最常見的一組場景: 根據某種規則獲取一組數據,點擊某一條數據,可以跳轉到下一界面獲取數據詳情.這里我會根據分類請求此分類下的博客列表,點擊某一條信息,可跳轉到博客詳情頁.簡單說,其實我們真正需要實現的只有兩個頁面: 博客分類列表頁 與 博客詳情頁.
數據接口准備
我們至少需要兩個接口,一個可以根據分類來獲取博客列表,一個用來根據id獲取博客詳情.
使用預定義的接口
如果你沒有自己的服務器或者對服務器開發不熟悉,可以使用我准備的這兩個測試接口:
博客列表接口
http://www.ios122.com/find_php/index.php?viewController=YFPostListViewController&model[category]=ui&model[page]=2
ui
分類名稱,目前預定義支持:ui
,network
,tool
,autolayout
四個分類.2
,獲取第幾頁的數據,從0開始計數,指請求此分類下第幾頁的數據.預定義每個分類下有100條數據,每20條數據一頁.- 返回示例:
[
{
"id": "ui_40",
"title": "title_ui_40",
"desc": "desc_ui_40"
},
{
"id": "ui_41",
"title": "title_ui_41",
"desc": "desc_ui_41"
},
{
"id": "ui_42",
"title": "title_ui_42",
"desc": "desc_ui_42"
},
{
"id": "ui_43",
"title": "title_ui_43",
"desc": "desc_ui_43"
},
{
"id": "ui_44",
"title": "title_ui_44",
"desc": "desc_ui_44"
},
{
"id": "ui_45",
"title": "title_ui_45",
"desc": "desc_ui_45"
},
{
"id": "ui_46",
"title": "title_ui_46",
"desc": "desc_ui_46"
},
{
"id": "ui_47",
"title": "title_ui_47",
"desc": "desc_ui_47"
},
{
"id": "ui_48",
"title": "title_ui_48",
"desc": "desc_ui_48"
},
{
"id": "ui_49",
"title": "title_ui_49",
"desc": "desc_ui_49"
},
{
"id": "ui_50",
"title": "title_ui_50",
"desc": "desc_ui_50"
},
{
"id": "ui_51",
"title": "title_ui_51",
"desc": "desc_ui_51"
},
{
"id": "ui_52",
"title": "title_ui_52",
"desc": "desc_ui_52"
},
{
"id": "ui_53",
"title": "title_ui_53",
"desc": "desc_ui_53"
},
{
"id": "ui_54",
"title": "title_ui_54",
"desc": "desc_ui_54"
},
{
"id": "ui_55",
"title": "title_ui_55",
"desc": "desc_ui_55"
},
{
"id": "ui_56",
"title": "title_ui_56",
"desc": "desc_ui_56"
},
{
"id": "ui_57",
"title": "title_ui_57",
"desc": "desc_ui_57"
},
{
"id": "ui_58",
"title": "title_ui_58",
"desc": "desc_ui_58"
},
{
"id": "ui_59",
"title": "title_ui_59",
"desc": "desc_ui_59"
}
]
2.博客詳情接口
http://www.ios122.com/find_php/index.php?viewController=YFPostViewController&model[id]=ui_0
ui_0
表示博客唯一標識.其應為分類博客列表返回的一個有效id.- 返回示例:
{
"title": "title of ui_0",
"body": "<h2>Hello iOS122</h2> Scann To Join Us <br /> <image alt=\"qq\" src=\"https://raw.githubusercontent.com/ios122/ios122/master/1443002712802.png\" />"
}
自定義接口
如果你有自己的服務器接口,直接使用即可;但是下面的oc代碼,你可能也要對應變換下;如果你對服務器接口開發不是很了解,可以先閱讀下這篇文章: iOS程序猿如何快速掌握 PHP,化身”全棧攻城獅”?.
假定,你已經閱讀並領會了 << iOS程序猿如何快速掌握 PHP,化身”全棧攻城獅”? >>,這篇文章,新建問及那,並把下面的代碼復制到對應文件中,然后根據自己的需要更改即可:
博客列表接口源文件
<?php // YFPostListViewController.php
class YFPostListViewController
{
public $model = array(); //!< 傳入的數據.
private $countOfPerPage = 20; //!< 每頁數據條數.
/* 獲取內容,用於輸出顯示. */
protected function getContent()
{
/* 預定義一組數據 */
$datasource = array();
$categorys = array('ui', 'network', 'tool', 'autolayout');
for ($i=0; $i < count($categorys); $i++) {
$categoryName = $categorys[$i];
$categoryData = array();
for ($j=0; $j < 100; $j++) {
$item = array(
'id' => "{$categoryName}_{$j}",
'title' => "title_{$categoryName}_{$j}",
'desc' => "desc_{$categoryName}_{$j}"
);
$categoryData[$j] = $item;
}
$datasource[$categoryName] = $categoryData;
}
$queryCategoryName = $this->model['category'];
$queryPage = $this->model['page'];
$targetCategoryData = $datasource[$queryCategoryName];
$content = array();
for ($i = $this->countOfPerPage * $queryPage ; $i < $this->countOfPerPage * ($queryPage + 1); $i ++ ) {
$content[] = $targetCategoryData[$i];
}
$content = json_encode($content);
return $content;
}
public function show()
{
$content = $this->getContent();
header("Content-type: application/json");
echo $content;
}
}
博客詳情接口源文件
<?php // YFPostViewController.php
class YFPostViewController
{
public $model = array(); //!< 傳入的數據.
/* 獲取內容,用於輸出顯示. */
protected function getContent()
{
$id = $this->model['id'];
$content = array(
'title' => "title of {$id}",
'body' => '<h2>Hello iOS122</h2> Scann To Join Us <br /> <image alt="qq" src="https://raw.githubusercontent.com/ios122/ios122/master/1443002712802.png" />'
);
$content = json_encode($content);
return $content;
}
public function show()
{
$content = $this->getContent();
header("Content-type: application/json");
echo $content;
}
}
MVC 版本實現: 類似的代碼,你不知道敲過了多少遍
技術要點
下面列出將要用到的技術點,如有你不熟悉的,可點擊對應鏈接訪問:
- 使用 AFNetworking 來處理網絡請求;
- 使用 MJExtension實現JSON到數據模型的自動轉換;
- 使用 MJRefresh 實現下拉刷新與上拉加載更多的效果;
- 使用 Masonry 進行AutoLayout布局;
- 使用 MBProgressHUD 優化頁面加載時的進度提示;
思路分析
-
博客分類列表頁面:
- 在前一頁面指定博客分類;
- 頁面加載時自動發起網絡請求獲取對應分類的數據;
- 獲取數據成功后,自動刷新視圖;獲取失敗,則給出錯誤提示;
- 點擊某一條數據,可跳轉到博客詳情頁.
-
博客詳情頁面:
- 在前一頁面指定博客id;
- 頁面加載時自動發起網絡請求獲取id的博客詳情;
- 獲取成功后,自動刷新視圖;獲取失敗,則給出錯誤提示.
博客列表頁面
1. 在前一頁面指定博客分類;
這一步,大家肯定都會:
YFMVCPostListViewController * mvcPostListVC = [[YFMVCPostListViewController alloc] init];
mvcPostListVC.categoryName = @"ui";
[self.navigationController pushViewController: mvcPostListVC animated: YES];
2. 頁面加載時自動發起網絡請求獲取對應分類的數據;
為了保證每次都能進入列表頁,都能自動刷新數據,建議在 viewWillAppear:
方法刷新數據:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear: animated];
[self updateData];
}
updateData
方法進行數據的更新:
- (void)updateData
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSString * urlStr = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostListViewController&model[category]=%@&model[page]=0", self.categoryName];
[manager GET: urlStr parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"JSON: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"Error: %@", error);
}];
}
此處使用的是預定義接口,數據請求成功后,控制台輸入如下:
JSON: (
{
desc = "desc_ui_0";
id = "ui_0";
title = "title_ui_0";
},
{
desc = "desc_ui_1";
id = "ui_1";
title = "title_ui_1";
},
{
desc = "desc_ui_2";
id = "ui_2";
title = "title_ui_2";
},
{
desc = "desc_ui_3";
id = "ui_3";
title = "title_ui_3";
},
{
desc = "desc_ui_4";
id = "ui_4";
title = "title_ui_4";
},
{
desc = "desc_ui_5";
id = "ui_5";
title = "title_ui_5";
},
{
desc = "desc_ui_6";
id = "ui_6";
title = "title_ui_6";
},
{
desc = "desc_ui_7";
id = "ui_7";
title = "title_ui_7";
},
{
desc = "desc_ui_8";
id = "ui_8";
title = "title_ui_8";
},
{
desc = "desc_ui_9";
id = "ui_9";
title = "title_ui_9";
},
{
desc = "desc_ui_10";
id = "ui_10";
title = "title_ui_10";
},
{
desc = "desc_ui_11";
id = "ui_11";
title = "title_ui_11";
},
{
desc = "desc_ui_12";
id = "ui_12";
title = "title_ui_12";
},
{
desc = "desc_ui_13";
id = "ui_13";
title = "title_ui_13";
},
{
desc = "desc_ui_14";
id = "ui_14";
title = "title_ui_14";
},
{
desc = "desc_ui_15";
id = "ui_15";
title = "title_ui_15";
},
{
desc = "desc_ui_16";
id = "ui_16";
title = "title_ui_16";
},
{
desc = "desc_ui_17";
id = "ui_17";
title = "title_ui_17";
},
{
desc = "desc_ui_18";
id = "ui_18";
title = "title_ui_18";
},
{
desc = "desc_ui_19";
id = "ui_19";
title = "title_ui_19";
}
)
3. 獲取數據成功后,自動刷新視圖;獲取失敗,則給出錯誤提示;
這一部分,涉及的變動較多,我就直接貼代碼了.你會注意到View和數據已經交叉進行了,很亂的感覺.而這也是我們想要使用MVVM重構代碼的重要原因之一.
//
// YFMVCPostListViewController.m
// iOS122
//
// Created by 顏風 on 15/10/14.
// Copyright (c) 2015年 iOS122. All rights reserved.
//
#import "YFMVCPostListViewController.h"
#import "YFArticleModel.h"
#import <AFNetworking.h>
#import <MJRefresh.h>
#import <MBProgressHUD.h>
@interface YFMVCPostListViewController ()<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView * tableView;
@property (nonatomic, strong) NSMutableArray * articles; //!< 文章數組,內部存儲AFArticleModel類型.
@property (assign, nonatomic) NSInteger page; //!< 數據頁數.表示下次請求第幾頁的數據.
@end
@implementation YFMVCPostListViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (NSMutableArray *)articles
{
if (nil == _articles) {
_articles = [NSMutableArray arrayWithCapacity: 42];
}
return _articles;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear: animated];
// 馬上進入刷新狀態
[self.tableView.header beginRefreshing];
}
- (UITableView *)tableView
{
if (nil == _tableView) {
_tableView = [[UITableView alloc] init];
[self.view addSubview: _tableView];
[_tableView makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(UIEdgeInsetsMake(0, 0, 0, 0));
}];
_tableView.delegate = self;
_tableView.dataSource = self;
NSString * cellReuseIdentifier = NSStringFromClass([UITableViewCell class]);
[_tableView registerClass: NSClassFromString(cellReuseIdentifier) forCellReuseIdentifier:cellReuseIdentifier];
_tableView.header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
self.page = 0;
[self updateData];
}];
_tableView.footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
[self updateData];
}];
}
return _tableView;
}
/**
* 更新視圖.
*/
- (void) updateView
{
[self.tableView reloadData];
}
/**
* 更新數據.
*
* 數據更新后,會自動更新視圖.
*/
- (void)updateData
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSString * urlStr = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostListViewController&model[category]=%@&model[page]=%ld", self.categoryName, (long)self.page ++];
[manager GET: urlStr parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self.tableView.header endRefreshing];
[self.tableView.footer endRefreshing];
if (1 == self.page) { // 說明是在重新請求數據.
self.articles = nil;
}
NSArray * responseArticles = [YFArticleModel objectArrayWithKeyValuesArray: responseObject];
[self.articles addObjectsFromArray: responseArticles];
[self updateView];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self.tableView.header endRefreshing];
[self.tableView.footer endRefreshing];
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.mode = MBProgressHUDModeText;
hud.labelText = @"您的網絡不給力!";
[hud hide: YES afterDelay: 2];
}];
}
# pragma mark - tabelView代理方法.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger number = self.articles.count;
return number;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString * cellReuseIdentifier = NSStringFromClass([UITableViewCell class]);
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: cellReuseIdentifier forIndexPath:indexPath];
YFArticleModel * model = self.articles[indexPath.row];
NSString * content = [NSString stringWithFormat: @"標題:%@ 內容:%@", model.title, model.desc];
cell.textLabel.text = content;
return cell;
}
@end
4. 點擊某一條數據,可跳轉到博客詳情頁.
只需要再額外實現下 -tableView: didSelectRowAtIndexPath:
方法即可:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 跳轉到博客詳情.
YFArticleModel * articleModel = self.articles[indexPath.row];
YFMVCPostViewController * postVC = [[YFMVCPostViewController alloc] init];
postVC.articleID = articleModel.id;
[self.navigationController pushViewController: postVC animated: YES];
}
博客詳情頁面
1. 在前一頁面指定博客id;
這里其實就是博客列表的控制器的那幾句:
// 跳轉到博客詳情.
YFArticleModel * articleModel = self.articles[indexPath.row];
YFMVCPostViewController * postVC = [[YFMVCPostViewController alloc] init];
postVC.articleID = articleModel.id;
[self.navigationController pushViewController: postVC animated: YES];
2. 頁面加載時自動發起網絡請求獲取id的博客詳情;
此處為了方便,我們依然使用預定義的博客詳情接口:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSString * urlStr = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostViewController&model[id]=%@", self.articleID];
[manager GET: urlStr parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"%@", responseObject);
[self updateView];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.mode = MBProgressHUDModeText;
hud.labelText = @"您的網絡不給力!";
[hud hide: YES afterDelay: 2];
}];
請求的輸入,Xcode控制台打印輸出,類似於:
{
body = "<h2>Hello iOS122</h2> Scann To Join Us <br /> <image alt=\"qq\" src=\"https://raw.githubusercontent.com/ios122/ios122/master/1443002712802.png\" />";
title = "title of ui_0";
}
3. 獲取成功后,自動刷新視圖;獲取失敗,則給出錯誤提示.
你會注意到,我們在上一步獲取的數據,body部分內部是HTML字符串,所以我們要使用webView來顯示博客詳情.這和最近炒得很火的的混合開發模式有些像,但是目前主流的博客應用,幾乎都是這么做的.完整代碼如下:
//
// YFMVCPostViewController.m
// iOS122
//
// Created by 顏風 on 15/10/16.
// Copyright (c) 2015年 iOS122. All rights reserved.
//
#import "YFMVCPostViewController.h"
#import "YFArticleModel.h"
#import <AFNetworking.h>
#import <MBProgressHUD.h>
@interface YFMVCPostViewController ()<UIWebViewDelegate>
@property (strong, nonatomic) UIWebView * webView;
@property (strong, nonatomic) YFArticleModel * article;
@end
@implementation YFMVCPostViewController
- (UIWebView *)webView
{
if (nil == _webView) {
_webView = [[UIWebView alloc] init];
[self.view addSubview: _webView];
[_webView makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(UIEdgeInsetsMake(64, 0, 0, 0));
}];
}
return _webView;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear: animated];
[self updateData];
}
/**
* 更新視圖.
*/
- (void) updateView
{
[self.webView loadHTMLString: self.article.body baseURL:nil];
}
/**
* 更新數據.
*
* 數據更新后,會自動更新視圖.
*/
- (void)updateData
{
[MBProgressHUD showHUDAddedTo:self.view animated: YES];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSString * urlStr = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostViewController&model[id]=%@", self.articleID];
[manager GET: urlStr parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
self.article = [YFArticleModel objectWithKeyValues: responseObject];
[self updateView];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.mode = MBProgressHUDModeText;
hud.labelText = @"您的網絡不給力!";
[hud hide: YES afterDelay: 2];
}];
}
@end
小結
此篇主要展示了一個典型的列表-->詳情場景的MVC實現,相關技術代碼可以直接用於自己的項目中.盡管這是簡化的場景,但依然可以很明顯地看出來數據,網絡請求與視圖間的相互調用,使代碼整體的可復用性大大降低! 而這,也是我們下次要用 MVVC 重構這個示例的核心目的之一!