寫給iOS小白的MVVM教程(一): 從MVC到MVVM之一個典型的MVC應用場景

前言

本着實踐爲主的原則,此係列文章不作過多的概念性的闡述和討論;更多的代碼和篇幅用來展現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"
    }
]

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 優化頁面加載時的進度提示;

思路分析

  • 博客分類列表頁面:

    1. 在前一頁面指定博客分類;

    2. 頁面加載時自動發起網絡請求獲取對應分類的數據;

    3. 獲取數據成功後,自動刷新視圖;獲取失敗,則給出錯誤提示;

    4. 點擊某一條數據,可跳轉到博客詳情頁.

  • 博客詳情頁面:

    1. 在前一頁面指定博客id;

    2. 頁面加載時自動發起網絡請求獲取id的博客詳情;

    3. 獲取成功後,自動刷新視圖;獲取失敗,則給出錯誤提示.

博客列表頁面

博客列表效果圖

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 重構這個示例的核心目的之一!

相關文章
相關標籤/搜索