本着實踐爲主的原則,此係列文章不作過多的概念性的闡述和討論;更多的代碼和篇幅用來展現MVC和MVVC下的基礎代碼結構與具體實現,來展現各自優劣.這篇文章,更多的在於發掘MVC與MVVC的共性,以期爲那些對MVVC感興趣的iOS開發者,找到一種平滑的過渡與重構代碼的方式.若是對MVVC感興趣,能夠直接將本文的大部分代碼引用到本身的項目中,畢竟代碼是寫出來的!開篇以前,你能夠先到這裏下載本文的示例工程: https://github.com/ios122/ios122php
在這一篇章裏,我會分別使用我所理解的MVC與MVVC兩種模式來完成同一個應用場景,以期幫助那些熟悉傳統MVC模式代碼的iOS攻城獅,能更好理解MVVC.限於篇幅,將MVC和MVVM拆分爲兩個部分,今天要說的是一個典型的MVC的應用場景,爲基於MVC的MVVM重構作個基礎.這篇文章着重進行了接口準備,必須的知識點的說明等內容.ios
咱們選取最多見的一組場景: 根據某種規則獲取一組數據,點擊某一條數據,能夠跳轉到下一界面獲取數據詳情.這裏我會根據分類請求此分類下的博客列表,點擊某一條信息,可跳轉到博客詳情頁.簡單說,其實咱們真正須要實現的只有兩個頁面: 博客分類列表頁 與 博客詳情頁.git
咱們至少須要兩個接口,一個能夠根據分類來獲取博客列表,一個用來根據id獲取博客詳情.github
若是你沒有本身的服務器或者對服務器開發不熟悉,可使用我準備的這兩個測試接口:web
http://www.ios122.com/find_php/index.php?viewController=YFPostListViewController&model[category]=ui&model[page]=2
ui
分類名稱,目前預約義支持: ui
, network
, tool
,autolayout
四個分類.json
2
,獲取第幾頁的數據,從0開始計數,指請求此分類下第幾頁的數據.預約義每一個分類下有100條數據,每20條數據一頁.數組
返回示例:ruby
[ { "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" } ]
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; } }
下面列出將要用到的技術點,若有你不熟悉的,可點擊對應連接訪問:
使用 AFNetworking 來處理網絡請求;
使用 MJExtension實現JSON到數據模型的自動轉換;
使用 MJRefresh 實現下拉刷新與上拉加載更多的效果;
使用 Masonry 進行AutoLayout佈局;
使用 MBProgressHUD 優化頁面加載時的進度提示;
博客分類列表頁面:
在前一頁面指定博客分類;
頁面加載時自動發起網絡請求獲取對應分類的數據;
獲取數據成功後,自動刷新視圖;獲取失敗,則給出錯誤提示;
點擊某一條數據,可跳轉到博客詳情頁.
博客詳情頁面:
在前一頁面指定博客id;
頁面加載時自動發起網絡請求獲取id的博客詳情;
獲取成功後,自動刷新視圖;獲取失敗,則給出錯誤提示.
這一步,你們確定都會:
YFMVCPostListViewController * mvcPostListVC = [[YFMVCPostListViewController alloc] init]; mvcPostListVC.categoryName = @"ui"; [self.navigationController pushViewController: mvcPostListVC animated: YES];
爲了保證每次都能進入列表頁,都能自動刷新數據,建議在 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"; } )
這一部分,涉及的變更較多,我就直接貼代碼了.你會注意到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
只須要再額外實現下 -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]; }
這裏其實就是博客列表的控制器的那幾句:
// 跳轉到博客詳情. YFArticleModel * articleModel = self.articles[indexPath.row]; YFMVCPostViewController * postVC = [[YFMVCPostViewController alloc] init]; postVC.articleID = articleModel.id; [self.navigationController pushViewController: postVC 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) { 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"; }
你會注意到,咱們在上一步獲取的數據,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 重構這個示例的核心目的之一!