基於MVVM,用於快速搭建設定頁,個人資訊頁的框架

J_Knight_發表於2017-03-21

更新記錄:

2017.4.23:新增支援資料來源完全依賴網路請求的情況。 ** 2017.4.22:新增支援請求新資料後重新整理表格。** ** 2017.4.21: 新增CocoaPods支援:pod 'SJStaticTableView', '~> 1.2.0'。**


寫一個小小輪子~

寫UITableView的時候,我們經常遇到的是完全依賴於網路請求,需要自定義的動態cell的需求(比如微博帖子列表)。但是同時,大多數app裡面幾乎也都有設定頁,個人頁等其他以靜態表格為主的頁面。

而且這些頁面的共性比較多:

  1. 大多數情況下在進入頁面之前就已經拿到所有資料。
  2. cell樣式單一,自定義cell出現的機率比較小(幾乎都是高度為44的cell)。
  3. 多數都分組。

因為自己非常想寫一個開源的東西出來(也可以暴露自己的不足),同時又受限於水平,所以就打算寫這麼一個比較簡單,又具有通用性的框架:一個定製性比較高的適合在個人頁和設定頁使用的UITableView

在真正寫之前,看了幾篇類似的文章,挑出三篇自己覺得比較好的:

  1. Clean Table View Code
  2. 如何寫好一個UITableView
  3. 利用MVVM設計快速開發個人中心、設定等模組

看完總結之後,利用上週3天的業餘時間寫好了這個框架,為了它實用性,我仿照了微信客戶端的發現頁,個人頁和設定頁寫了一個Demo,來看一下效果圖:

發現頁 | 個人頁 | 個人資訊頁 | 設定頁

專案所用資源來自:GitHub:zhengwenming/WeChat Demo地址:GitHub: knightsj/SJStaticTableView

為了體現出這個框架的定製性,我自己也在裡面新增了兩個頁面,入口在設定頁裡面:

分組定製 | 同組定製

先不要糾結分組定製和同組定製的具體意思,在後面講到定製性的時候我會詳細說明。現在只是讓大家看一下效果。

在大概瞭解了功能之後,開始詳細介紹這個框架。寫這篇介紹的原因倒不是希望有多少人來用,而是表達一下我自己的思路而已。各位覺得不好的地方請多批評。

在正式講解之前,先介紹一下本篇的基本目錄:

  1. 用到的技術點。
  2. 功能說明。
  3. 使用方法。
  4. 定製性介紹。
  5. 新增支援重新整理功能。
  6. 新增支援資料來源完全依賴網路請求。

1. 用到的技術點


框架整體來說還是比較簡單的,主要還是基於蘋果的UITableView元件,為了解耦和責任分離,主要運用了以下技術點:

  • MVVM:採用MVVM架構,將每一行“純粹”的資料交給一個單獨的ViewModel,讓其持有每個cell的資料(行高,cell型別,文字寬度,圖片高度等等)。而且每一個section也對應一個ViewModel,它持有當前section的配置資料(title,header和footer的高度等等)。
  • 輕UIViewController:分離UITableViewDataSourceUIViewController,讓單獨一個類來實現UITableViewDataSource的職能。
  • block:使用block來呼叫cell的繪製方法。
  • 分類:使用分類來定義每一種不同的cell的繪製方法。

知道了主要運用的技術點以後,給大家詳細介紹一下該框架的功能。

2. 功能介紹


這個框架可以用來快速搭建設定頁,個人資訊頁能靜態表格頁面,使用者只需要給tableView的DataSource傳入元素是viewModel的陣列就可以了。

雖說這類頁面的佈局還是比較單一的,但是還是會有幾種不同的情況(cell的佈局型別),我對比較常見的cell佈局做了封裝,使用者可以直接使用。

我在定義這些cell的型別的時候,大致劃分了兩類:

  1. 第一類是系統風格的cell,大多數情況下,cell高度為44;在cell左側會有一張圖,一個label,也可以只存在一種(但是隻存在圖片的情況很少);在cell右側一般都有一個向右的箭頭,而且有時這個箭頭的左側還可能有label,image,也可以兩個都有。
  2. 第二類就是自定義的cell了,它的高度不一定是44,而且佈局和系統風格的cell很不一樣,需要使用者自己新增。

基於這兩大類,再細分了幾種情況,可以由下面這張圖來直觀看一下:

既然是cell的型別,那麼就型別的列舉就需要定義在cell的viewModel裡面:

typedef NS_ENUM(NSInteger, SJStaticCellType) {
    
    //系統風格的各種cell型別,已封裝好,可以直接用
    SJStaticCellTypeSystemLogout,                          //退出登入cell
    SJStaticCellTypeSystemAccessoryNone,                   //右側沒有任何控制元件
    SJStaticCellTypeSystemAccessorySwitch,                 //右側是開關
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    //右側是三角箭頭(箭頭左側可以有一個image或者一個label,或者二者都有,根據傳入的引數決定)
    
    //需要使用者自己新增的自定義cell型別
    SJStaticCellTypeMeAvatar,                              //個人頁“我”cell    
};
複製程式碼

來一張圖直觀得體會一下:

支援cell型別

在這裡有三點需要說一下:

  1. 這裡面除了自定義的cell以外,其他型別的cell都不需要開發者自己佈局,都已經被我封裝好,只需要在cell的ViewModel裡面傳入相應的型別和資料(文字,圖片)即可。
  2. 因為左側的兩個控制元件(圖片和文字)是至少存在一個而且左右順序固定(圖片永遠在最左側),所以該框架通過開發者傳入的左側需要顯示的圖片和文字,可以自己進行cell的佈局。所以型別的判斷主要作用於cell的右側。
  3. 值得一提的是,在"最右側是一個箭頭"子分支的五個型別其實都屬於一個型別,只需要傳入文字和圖片,以及文字圖片的顯示順序引數(這個引數只在同時存在圖片和文字的時候有效)就可以自行判斷佈局。

在瞭解了該框架的功能之後,我們先看一下如何使用這個框架:

3. 使用方法


整合方法:

  1. 靜態:手動將SJStaticTableViewComponent資料夾拖入到工程中。
  2. 動態:CocoaPods:pod 'SJStaticTableView', '~> 1.1.2

具體的方法先用文字說明一下:

  1. 將要開發的頁面的ViewController繼承SJStaticTableViewController
  2. 在新ViewController裡實現createDataSource方法,將viewModel陣列傳給控制器的dataSource屬性。
  3. 根據不同的cell型別,呼叫不同的cell繪製方法。
  4. 如果需要接受cell的點選,需要實現didSelectViewModel方法。

可能感覺比較抽象,我拿設定頁來具體說明一下:

先看一下設定頁的佈局:

設定頁

然後我們看一下設定的ViewController的程式碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"設定";
}


- (void)createDataSource
{
    self.dataSource = [[SJStaticTableViewDataSource alloc] initWithViewModelsArray:[Factory settingPageData] configureBlock:^(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel *viewModel) {
        
        switch (viewModel.staticCellType)
        {
            case SJStaticCellTypeSystemAccessoryDisclosureIndicator:
            {
                [cell configureAccessoryDisclosureIndicatorCellWithViewModel:viewModel];
            }
                break;
                
            case SJStaticCellTypeSystemAccessorySwitch:
            {
                [cell configureAccessorySwitchCellWithViewModel:viewModel];
            }
                break;
                
            case SJStaticCellTypeSystemLogout:
            {
                [cell configureLogoutTableViewCellWithViewModel:viewModel];
            }
                break;
                
            case SJStaticCellTypeSystemAccessoryNone:
            {
                [cell configureAccessoryNoneCellWithViewModel:viewModel];
            }
                break;
                
            default:
                break;
        }
    }];
}


- (void)didSelectViewModel:(SJStaticTableviewCellViewModel *)viewModel atIndexPath:(NSIndexPath *)indexPath
{
    
    switch (viewModel.identifier)
    {
            
        case 6:
        {
            NSLog(@"退出登入");
            [self showAlertWithMessage:@"真的要退出登入嘛?"];
        }
            break;
            
        case 8:
        {
            NSLog(@"清理快取");
        }
            break;
            
        case 9:
        {
            NSLog(@"跳轉到定製性cell展示頁面 - 分組");
            SJCustomCellsViewController *vc = [[SJCustomCellsViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;
            
        case 10:
        {
            NSLog(@"跳轉到定製性cell展示頁面 - 同組");
            SJCustomCellsOneSectionViewController *vc = [[SJCustomCellsOneSectionViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;
            
        default:
            break;
    }
}
複製程式碼

看到這裡,你可能會有這些疑問:

  1. UITableViewDataSource方法哪兒去了?
  2. viewModel陣列是如何設定的?
  3. cell的繪製方法是如何區分的?
  4. UITableViewDelegate的方法哪裡去了?

下面我會一一解答,看完了下面的解答,就能幾乎完全掌握這個框架的思路了:

問題1:UITableViewDataSource方法哪兒去了?

我自己封裝了一個類SJStaticTableViewDataSource專門作為資料來源,需要控制器給它一個viewModel陣列。

來看一下它的實現檔案:

//SJStaticTableViewDataSource.m
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return self.viewModelsArray.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.cellViewModelsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //獲取section的ViewModel
    SJStaticTableviewSectionViewModel *sectionViewModel = self.viewModelsArray[indexPath.section];
    //獲取cell的viewModel
    SJStaticTableviewCellViewModel *cellViewModel = sectionViewModel.cellViewModelsArray[indexPath.row];
    
    SJStaticTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellViewModel.cellID];
    if (!cell) {
        cell = [[SJStaticTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellViewModel.cellID];
    }
    self.cellConfigureBlock(cell,cellViewModel);
    
    return cell;
    
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionHeaderTitle;  
}

- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionFooterTitle;
}
複製程式碼

表格的cell和section都設定了與其對應的viewModel,用於封裝其對應的資料:

cell的viewModel(大致看一下即可,後面有詳細說明):

typedef NS_ENUM(NSInteger, SJStaticCellType) {
    
    //系統風格的各種cell型別,已封裝好,可以直接用
    SJStaticCellTypeSystemLogout,                          //退出登入cell(已封裝好)
    SJStaticCellTypeSystemAccessoryNone,                   //右側沒有任何控制元件
    SJStaticCellTypeSystemAccessorySwitch,                 //右側是開關
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    //右側是三角箭頭(箭頭左側可以有一個image或者一個label,或者二者都有,根據傳入的引數決定)
    
    //需要使用者自己新增的自定義cell型別
    SJStaticCellTypeMeAvatar,                              //個人頁“我”cell
    
};


typedef void(^SwitchValueChagedBlock)(BOOL isOn);           //switch開關切換時呼叫的block


@interface SJStaticTableviewCellViewModel : NSObject

@property (nonatomic, assign) SJStaticCellType staticCellType;                  //型別


@property (nonatomic, copy)   NSString *cellID;                                  //cell reuser identifier
@property (nonatomic, assign) NSInteger identifier;                              //區別每個cell,用於點選

// =============== 系統預設cell左側 =============== //
@property (nonatomic, strong) UIImage  *leftImage;                               //左側的image,按需傳入
@property (nonatomic, assign) CGSize leftImageSize;                              //左側image的大小,存在預設設定

@property (nonatomic, copy)   NSString *leftTitle;                               //cell主標題,按需傳入
@property (nonatomic, strong) UIColor *leftLabelTextColor;                       //當前組cell左側label裡文字的顏色
@property (nonatomic, strong) UIFont *leftLabelTextFont;                         //當前組cell左側label裡文字的字型

@property (nonatomic, assign) CGFloat leftImageAndLabelGap;                      //左側image和label的距離,存在預設值


// =============== 系統預設cell右側 =============== //
@property (nonatomic, copy)   NSString *indicatorLeftTitle;                      //右側箭頭左側的文字,按需傳入
@property (nonatomic, strong) UIColor *indicatorLeftLabelTextColor;              //右側文字的顏色,存在預設設定,也可以自定義
@property (nonatomic, strong) UIFont *indicatorLeftLabelTextFont;                //右側文字的字型,存在預設設定,也可以自定義
@property (nonatomic, strong) UIImage *indicatorLeftImage;                       //右側箭頭左側的image,按需傳入
@property (nonatomic, assign) CGSize indicatorLeftImageSize;                     //右側尖頭左側image大小,存在預設設定,也可以自定義

@property (nonatomic, assign, readonly)  BOOL hasIndicatorImageAndLabel;         //右側尖頭左側的文字和image是否同時存在,只能通過內部計算

@property (nonatomic, assign) CGFloat indicatorLeftImageAndLabelGap;             //右側尖頭左側image和label的距離,存在預設值
@property (nonatomic, assign) BOOL isImageFirst;                                 //右側尖頭左側的文字和image同時存在時,是否是image挨著箭頭,預設為YES
@property (nonatomic, copy) SwitchValueChagedBlock switchValueDidChangeBlock;    //切換switch開關的時候呼叫的block


// =============== 長寬資料 =============== //
@property (nonatomic, assign) CGFloat cellHeight;                                //cell高度,預設是44,可以設定
@property (nonatomic, assign) CGSize  leftTitleLabelSize;                        //左側預設Label的size,傳入text以後內部計算
@property (nonatomic, assign) CGSize  indicatorLeftLabelSize;                    //右側label的size


// =============== 自定義cell的資料放在這裡 =============== //
@property (nonatomic, strong) UIImage *avatarImage;
@property (nonatomic, strong) UIImage *codeImage;
@property (nonatomic, copy)   NSString *userName;
@property (nonatomic, copy)   NSString *userID;
複製程式碼

section的viewModel(大致看一下即可,後面有詳細說明):

@interface SJStaticTableviewSectionViewModel : NSObject

@property (nonatomic, copy)   NSString *sectionHeaderTitle;         //該section的標題
@property (nonatomic, copy)   NSString *sectionFooterTitle;         //該section的標題
@property (nonatomic, strong) NSArray  *cellViewModelsArray;        //該section的資料來源

@property (nonatomic, assign) CGFloat  sectionHeaderHeight;         //header的高度
@property (nonatomic, assign) CGFloat  sectionFooterHeight;         //footer的高度

@property (nonatomic, assign) CGSize leftImageSize;                 //當前組cell左側image的大小
@property (nonatomic, strong) UIColor *leftLabelTextColor;          //當前組cell左側label裡文字的顏色
@property (nonatomic, strong) UIFont *leftLabelTextFont;            //當前組cell左側label裡文字的字型
@property (nonatomic, assign) CGFloat leftImageAndLabelGap;         //當前組左側image和label的距離,存在預設值

@property (nonatomic, strong) UIColor *indicatorLeftLabelTextColor; //當前組cell右側label裡文字的顏色
@property (nonatomic, strong) UIFont *indicatorLeftLabelTextFont;   //當前組cell右側label裡文字的字型
@property (nonatomic, assign) CGSize indicatorLeftImageSize;        //當前組cell右側image的大小
@property (nonatomic, assign) CGFloat indicatorLeftImageAndLabelGap;//當前組cell右側image和label的距離,存在預設值


- (instancetype)initWithCellViewModelsArray:(NSArray *)cellViewModelsArray;
複製程式碼

你可能會覺得屬性太多了,但這些屬性的存在意義是為cell的定製性服務的,在後文會有解釋。

現在瞭解了我封裝好的資料來源,cell的viewModel,section的viewModel以後,我們看一下第二個問題:

問題2: viewModel陣列是如何設定的?

我們來看一下設定頁的viewModel陣列的設定:

+ (NSArray *)settingPageData
{
    // ========== section 0
    SJStaticTableviewCellViewModel *vm0 = [[SJStaticTableviewCellViewModel alloc] init];
    vm0.leftTitle = @"賬號與安全";
    vm0.identifier = 0;
    vm0.indicatorLeftTitle = @"已保護";
    vm0.indicatorLeftImage = [UIImage imageNamed:@"ProfileLockOn"];
    vm0.isImageFirst = NO;
    
    SJStaticTableviewSectionViewModel *section0 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm0]];
    
    

    // ========== section 1
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftTitle = @"新訊息通知";
    vm1.identifier = 1;
    
    //額外新增switch
    SJStaticTableviewCellViewModel *vm7 = [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftTitle = @"夜間模式";
    vm7.switchValueDidChangeBlock = ^(BOOL isON){
        NSString *message = isON?@"開啟夜間模式":@"關閉夜間模式";
        NSLog(@"%@",message);
    };
    vm7.staticCellType = SJStaticCellTypeSystemAccessorySwitch;
    vm7.identifier = 7;
    
    SJStaticTableviewCellViewModel *vm8 = [[SJStaticTableviewCellViewModel alloc] init];
    vm8.leftTitle = @"清理快取";
    vm8.indicatorLeftTitle = @"12.3M";
    vm8.identifier = 8;
    
    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftTitle = @"隱私";
    vm2.identifier = 2;
    
    
    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftTitle = @"通用";
    vm3.identifier = 3;
    
    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1,vm7,vm8,vm2,vm3]];
    



    // ========== section 2
    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftTitle = @"幫助與反饋";
    vm4.identifier = 4;
    
    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftTitle = @"關於微信";
    vm5.identifier = 5;
    
    SJStaticTableviewSectionViewModel *section2 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm4,vm5]];
    


      // ========== section 4
    SJStaticTableviewCellViewModel *vm9 = [[SJStaticTableviewCellViewModel alloc] init];
    vm9.leftTitle = @"定製性cell展示頁面 - 分組";
    vm9.identifier = 9;
    
    SJStaticTableviewCellViewModel *vm10 = [[SJStaticTableviewCellViewModel alloc] init];
    vm10.leftTitle = @"定製性cell展示頁面 - 同組";
    vm10.identifier = 10;
    
    SJStaticTableviewSectionViewModel *section4 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm9,vm10]];
    
    

    // ========== section 3
    SJStaticTableviewCellViewModel *vm6 = [[SJStaticTableviewCellViewModel alloc] init];
    vm6.staticCellType = SJStaticCellTypeSystemLogout;
    vm6.cellID = @"logout";
    vm6.identifier = 6;
   
    SJStaticTableviewSectionViewModel *section3 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm6]];
    
    return @[section0,section1,section2,section4,section3];
}
複製程式碼

我們可以看到,交給dataSource的陣列是一個二維陣列:

  • 第一維是section陣列,元素是每一個section對應的viewModel:SJStaticTableviewSectionViewModel
  • 第二維是cell陣列,元素是每一個cell對應的viewModel:SJStaticTableviewCellViewModel

有幾個SJStaticTableviewCellViewModel的屬性需要強調一下:

  1. isImageFirst:因為該頁面第一組的cell右側的箭頭左邊同時存在一個image和一個label,所以需要額外設定二者的順序。因為預設緊挨著箭頭的是圖片,所以我們需要重新設定它為NO,作用是讓label緊挨著箭頭。
  2. identifier:這個屬性是一個整數,它用來標記每個cell,用於在使用者點選cell的時候進行判斷。我沒有將使用者的點選與cell的index相關聯,是因為有的時候因為需求我們可能會更改cell的順序或者刪除某個cell,所以依賴cell的index是不妥的,容易出錯。
  3. cellID:這個屬性用來cell的複用。因為總是有個別cell的佈局是不同的:在這裡出現了一個退出登入的cell,所以需要和其他的cell區別開來(cellID可以不用設定,有預設值,用來標記最常用的cell型別)。

顯然,Factory類屬於Model,它將“純資料”交給了dataSource使用的兩個viewModel。這個類是我自己定義的,讀者在使用這個框架的時候可以根據需求自己定義。

現在知道了資料來源的設定方法,我們看一下第三個問題:

問題3:cell的繪製方法是如何區分的?

心細的同學會發現,在dataSource的cellForRow:方法裡,我用了block方法來繪製了cell。

先看一下這個block的定義:

typedef void(^SJStaticCellConfigureBlock)(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel * viewModel);
複製程式碼

這個block在控制器裡面回撥,通過判斷cell的型別來繪製不同的cell。

那麼不同型別的cell是如何區分的呢? --- 我用的是分類。

有分類,就一定有一個被分類的類: SJStaticTableViewCell

看一下它的標頭檔案:

//所有cell都是這個類的分類

@interface SJStaticTableViewCell : UITableViewCell

@property (nonatomic, strong) SJStaticTableviewCellViewModel *viewModel;

// =============== 系統風格cell的所有控制元件 =============== //

//左半部分
@property (nonatomic, strong) UIImageView *leftImageView;               //左側的ImageView
@property (nonatomic, strong) UILabel *leftTitleLabel;                  //左側的Label

//右半部分
@property (nonatomic, strong) UIImageView *indicatorArrow;              //右側的箭頭
@property (nonatomic, strong) UIImageView *indicatorLeftImageView;      //右側的箭頭的左邊的imageview
@property (nonatomic, strong) UILabel *indicatorLeftLabel;              //右側的箭頭的左邊的Label
@property (nonatomic, strong) UISwitch *indicatorSwitch;                //右側的箭頭的左邊的開關
@property (nonatomic, strong) UILabel *logoutLabel;                     //退出登入的label

// =============== 使用者自定義的cell裡面的控制元件 =============== //

//MeViewController裡面的頭像cell
@property (nonatomic, strong) UIImageView *avatarImageView;
@property (nonatomic, strong) UIImageView *codeImageView;
@property (nonatomic, strong) UIImageView *avatarIndicatorImageView;
@property (nonatomic, strong) UILabel *userNameLabel;
@property (nonatomic, strong) UILabel *userIdLabel;


//統一的,佈局cell左側部分的內容(標題 / 圖片 + 標題),所有系統風格的cell都要呼叫這個方法
- (void)layoutLeftPartSubViewsWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;

@end
複製程式碼

在這裡我定義了所有的控制元件和一個佈局cell左側的控制元件的方法。因為幾乎所有的分類的左側幾乎都是類似的,所以將它抽取出來。

那麼究竟有幾個分類呢?(可以參考上面cellViewModel標頭檔案裡的列舉型別)

//右側有剪頭的cell(最常見)
@interface SJStaticTableViewCell (AccessoryDisclosureIndicator)
- (void)configureAccessoryDisclosureIndicatorCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
複製程式碼
//右側沒有控制元件的cell
@interface SJStaticTableViewCell (AccessoryNone)
- (void)configureAccessoryNoneCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
複製程式碼
//右側是開關的 cell
@interface SJStaticTableViewCell (AccessorySwitch)
- (void)configureAccessorySwitchCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
複製程式碼
//退出登入cell
@interface SJStaticTableViewCell (Logout)
- (void)configureLogoutTableViewCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
複製程式碼
//一個自定義的cell(在個人頁的第一排)
@interface SJStaticTableViewCell (MeAvatar)
- (void)configureMeAvatarTableViewCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
複製程式碼

在使用這個框架的時候,如果遇到不滿足當前需求的情況,可以自己新增分類。

問題4:UITableViewDelegate的方法哪裡去了?

說到UITableViewDelegate的代理方法,我們最熟悉的莫過於didSelectRowAtIndexPath:了。

但是我在寫這個框架的時候,自己定義了一個繼承於UITableViewDelegate的代理:SJStaticTableViewDelegate,並給它新增了一個代理方法: ``

@protocol SJStaticTableViewDelegate <UITableViewDelegate>

@optional

- (void)didSelectViewModel: (SJStaticTableviewCellViewModel *)viewModel atIndexPath:(NSIndexPath *)indexPath;

@end
複製程式碼

這個方法返回的是當前點選的cell對應的viewModel,弱化了indexPath的作用。

為什麼要這麼做?

想一想原來點選cell的代理方法:didSelectRowAtIndexPath:。我們通過這個點選方法,拿到的是cell對應的indexPath,然後再通過這個indexPath,就可以在資料來源裡面查詢對應的模型(viewModel或者model)。

因此,我定義的這個方法直接返回了被點選cell對應的viewModel,等於說幫使用者節省了一個步驟。當然如果要使用的話也可以使用系統原來的didSelectRowAtIndexPath:方法。

來看一下這個新的代理方法是如何實現的:

//SJStaticTableView.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    if ((self.sjDelegate) && [self.sjDelegate respondsToSelector:@selector(didSelectViewModel:atIndexPath:)]) {
        
        SJStaticTableviewCellViewModel *cellViewModel = [self.sjDataSource tableView:tableView cellViewModelAtIndexPath:indexPath];
        [self.sjDelegate didSelectViewModel:cellViewModel atIndexPath:indexPath];
        
    }else if((self.sjDelegate)&& [self.sjDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]){
        
        [self.sjDelegate tableView:tableView didSelectRowAtIndexPath:indexPath];
        
    }
}
複製程式碼

現在讀者應該大致瞭解了這個框架的實現思路,現在我講一下這個框架的定製性。

4. 定製性


這個框架有一個配置檔案:SJConst.h,它定義了這個框架的所有預設資料和預設配置,比如cell左側lable的字型,顏色;左側label和image的距離;右側label的字型和顏色,右側圖片的預設大小等等。來看一下程式碼:

#ifndef SJConst_h
#define SJConst_h

//distance
#define SJScreenWidth      [UIScreen mainScreen].bounds.size.width
#define SJScreenHeight     [UIScreen mainScreen].bounds.size.height

#define SJTopGap 8               //same as bottom gap
#define SJLeftGap 12             //same as right gap
#define SJLeftMiddleGap 10       //in left  part: the gap between image and label
#define SJRightMiddleGap 6       //in right part: the gap between image and label
#define SJImgWidth 30            //default width and height
#define SJTitleWidthLimit 180    //limt width of left and right labels

//image
#define SJIndicatorArrow @"arrow"

//font
#define SJLeftTitleTextFont               [UIFont systemFontOfSize:15]
#define SJLogoutButtonFont                [UIFont systemFontOfSize:16]
#define SJIndicatorLeftTitleTextFont      [UIFont systemFontOfSize:13]

//color
#define SJColorWithRGB(R,G,B,A)           [UIColor colorWithRed:R/255.0 green:G/255.0 blue:B/255.0 alpha:A]
#define SJLeftTitleTextColor              [UIColor blackColor]
#define SJIndicatorLeftTitleTextColor     SJColorWithRGB(136,136,136,1)

#endif /* SJConst_h */
複製程式碼

這裡定義的預設配置在cellViewModel和sectionViewModel初始化的時候使用:

cell的viewModel:

//SJStaticTableviewCellViewModel.m
- (instancetype)init
{
    self = [super init];
    if (self) {        
        _cellHeight = 44;
        _cellID = @"defaultCell";
        _staticCellType = SJStaticCellTypeSystemAccessoryDisclosureIndicator;//預設是存在三角箭頭的cell
        _isImageFirst = YES;
        
        //都是預設配置
        _leftLabelTextFont = SJLeftTitleTextFont;
        _leftLabelTextColor = SJLeftTitleTextColor;
        _leftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _leftImageAndLabelGap = SJLeftMiddleGap;
        _indicatorLeftLabelTextFont = SJIndicatorLeftTitleTextFont;
        _indicatorLeftLabelTextColor = SJIndicatorLeftTitleTextColor;
        _indicatorLeftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _indicatorLeftImageAndLabelGap = SJRightMiddleGap;
    }
    return self;
}
複製程式碼

section的viewModel:

- (instancetype)initWithCellViewModelsArray:(NSArray *)cellViewModelsArray
{
    self = [super init];
    if (self) {
        _sectionHeaderHeight = 10;
        _sectionFooterHeight = 10;
        _leftLabelTextFont = SJLeftTitleTextFont;
        _leftLabelTextColor = SJLeftTitleTextColor;
        _leftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _leftImageAndLabelGap = SJLeftMiddleGap;
        _indicatorLeftLabelTextFont = SJIndicatorLeftTitleTextFont;
        _indicatorLeftLabelTextColor = SJIndicatorLeftTitleTextColor;
        _indicatorLeftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _indicatorLeftImageAndLabelGap = SJRightMiddleGap;
        _cellViewModelsArray = cellViewModelsArray;        
    }
    return self;
}
複製程式碼

顯然,這個預設配置只有一組,但是可能一個app裡面同時存在一個設定頁和一個個人頁。而這兩個頁面的風格也可能是不一樣的,所以這個預設配置只能給其中一個頁面,另一個頁面需要另外配置,於是就有了定製性的功能。

再來看一下展示定製性效果的圖:

分組定製 | 同組定製

參照這個效果圖,我們看一下這兩個頁面的資料來源是如何設定的:

分組頁面:

+ (NSArray *)customCellsPageData
{
    //預設配置
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm1.leftTitle = @"全部預設配置,用於對照";
    vm1.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm1.indicatorLeftTitle = @"王者榮耀!";
    
    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1]];
    
    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm2.leftTitle = @"左側圖片變小";
    vm2.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm2.indicatorLeftTitle = @"王者榮耀!";
    
    SJStaticTableviewSectionViewModel *section2 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm2]];
    section2.leftImageSize = CGSizeMake(20, 20);
    
    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm3.leftTitle = @"字型變小變紅";
    vm3.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm3.indicatorLeftTitle = @"王者榮耀!";
    
    SJStaticTableviewSectionViewModel *section3 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm3]];
    section3.leftLabelTextFont = [UIFont systemFontOfSize:8];
    section3.leftLabelTextColor = [UIColor redColor];
    
    
    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm4.leftTitle = @"左側兩個控制元件距離變大";
    vm4.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm4.indicatorLeftTitle = @"王者榮耀!";
    
    SJStaticTableviewSectionViewModel *section4 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm4]];
    section4.leftImageAndLabelGap = 20;
    
    
    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm5.leftTitle = @"右側圖片變小";
    vm5.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm5.indicatorLeftTitle = @"王者榮耀!";
    
    SJStaticTableviewSectionViewModel *section5 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm5]];
    section5.indicatorLeftImageSize = CGSizeMake(15, 15);
    
    
    SJStaticTableviewCellViewModel *vm6= [[SJStaticTableviewCellViewModel alloc] init];
    vm6.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm6.leftTitle = @"右側字型變大變藍";
    vm6.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm6.indicatorLeftTitle = @"王者榮耀!";
    
    SJStaticTableviewSectionViewModel *section6 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm6]];
    section6.indicatorLeftLabelTextFont = [UIFont systemFontOfSize:18];
    section6.indicatorLeftLabelTextColor = [UIColor blueColor];
    
    
    SJStaticTableviewCellViewModel *vm7= [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm7.leftTitle = @"右側兩個控制元件距離變大";
    vm7.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm7.indicatorLeftTitle = @"王者榮耀!";
    
    SJStaticTableviewSectionViewModel *section7 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm7]];
    section7.indicatorLeftImageAndLabelGap = 18;
    
    
    return @[section1,section2,section3,section4,section5,section6,section7];
    
}
複製程式碼

我們可以看到,定製的程式碼都作用於section的viewModel。

同組頁面:

+ (NSArray *)customCellsOneSectionPageData
{
    //預設配置
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm1.leftTitle = @"全部預設配置,用於對照";
    vm1.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm1.indicatorLeftTitle = @"王者榮耀!";
    
    
    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm2.leftTitle = @"左側圖片變小";
    vm2.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm2.indicatorLeftTitle = @"王者榮耀!";
    vm2.leftImageSize = CGSizeMake(20, 20);
    
    
    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm3.leftTitle = @"字型變小變紅";
    vm3.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm3.indicatorLeftTitle = @"王者榮耀!";
    vm3.leftLabelTextFont = [UIFont systemFontOfSize:8];
    vm3.leftLabelTextColor = [UIColor redColor];
    
    
    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm4.leftTitle = @"左側兩個控制元件距離變大";
    vm4.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm4.indicatorLeftTitle = @"王者榮耀!";
    vm4.leftImageAndLabelGap = 20;
    
    
    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm5.leftTitle = @"右側圖片變小";
    vm5.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm5.indicatorLeftTitle = @"王者榮耀!";
    vm5.indicatorLeftImageSize = CGSizeMake(15, 15);
    
    
    SJStaticTableviewCellViewModel *vm6= [[SJStaticTableviewCellViewModel alloc] init];
    vm6.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm6.leftTitle = @"右側字型變大變藍";
    vm6.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm6.indicatorLeftTitle = @"王者榮耀!";
    vm6.indicatorLeftLabelTextFont = [UIFont systemFontOfSize:18];
    vm6.indicatorLeftLabelTextColor = [UIColor blueColor];
    
    
    SJStaticTableviewCellViewModel *vm7= [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm7.leftTitle = @"右側兩個控制元件距離變大";
    vm7.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm7.indicatorLeftTitle = @"王者榮耀!";
    vm7.indicatorLeftImageAndLabelGap = 18;
    
    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1,vm2,vm3,vm4,vm5,vm6,vm7]];
    
    return @[section1];
}
複製程式碼

為了方便比較,同組頁面的定製和分組是一致的。我們可以看到,定製程式碼都作用於cell的viewModel上了。

為什麼要有同組和分組展示?

同組和分組展示的目的,是為了展示這個框架的兩種定製性。

  • 分組頁面所展示的是section級的定製性:cell的配置任務交給section層的viewModel。一旦設定,該section裡面的所有cell都能保持這一配置。

  • 同組頁面所展示的是cell級的定製性:cell的配置任務交給cell層的viewModel。一旦設定,只有當前cell具有這個配置,不影響其他cell。

其實為了省事,只在section層的viewModel上配置即可(如果給每個cell都給設定相同的配置太不優雅了),因為從設計角度來看,一個section裡面的cell的風格不一致的情況比較少見(我覺得不符合設計):比如在一個section裡面,不太可能兩個cell裡面的圖片大小是不一樣的,或者字型大小也不一樣。

還是看一下section級的定製程式碼吧:

//重新設定了該組全部cell裡面左側label的字型
- (void)setLeftLabelTextFont:(UIFont *)leftLabelTextFont
{
    if (_leftLabelTextFont != leftLabelTextFont) {
        
        if (![self font1:_leftLabelTextFont hasSameFontSizeOfFont2:leftLabelTextFont]) {
            
            _leftLabelTextFont = leftLabelTextFont;
            
            //如果新的寬度大於原來的寬度,需要重新設定,否則不需要
            [_cellViewModelsArray enumerateObjectsUsingBlock:^(SJStaticTableviewCellViewModel * viewModel, NSUInteger idx, BOOL * _Nonnull stop) {
                viewModel.leftLabelTextFont = _leftLabelTextFont;
                CGSize size = [self sizeForTitle:viewModel.leftTitle withFont:_leftLabelTextFont];
                if (size.width > viewModel.leftTitleLabelSize.width) {
                    viewModel.leftTitleLabelSize = size;
                }
            }];
            
        }
    }
}

//重新設定了該組全部cell裡面左側label的字的顏色
- (void)setLeftLabelTextColor:(UIColor *)leftLabelTextColor
{
    if (![self color1:_leftLabelTextColor hasTheSameRGBAOfColor2:leftLabelTextColor]) {
         _leftLabelTextColor = leftLabelTextColor;
        [_cellViewModelsArray makeObjectsPerformSelector:@selector(setLeftLabelTextColor:) withObject:_leftLabelTextColor];
    }
}

//重新設定了該組全部cell裡面左側圖片等大小
- (void)setLeftImageSize:(CGSize)leftImageSize
{
    SJStaticTableviewCellViewModel *viewMoel = _cellViewModelsArray.firstObject;
    
    CGFloat cellHeight = viewMoel.cellHeight;
    if ( (!CGSizeEqualToSize(_leftImageSize, leftImageSize)) && (leftImageSize.height < cellHeight)) {
        _leftImageSize = leftImageSize;
        [_cellViewModelsArray enumerateObjectsUsingBlock:^(SJStaticTableviewCellViewModel *viewModel, NSUInteger idx, BOOL * _Nonnull stop)
        {
            viewMoel.leftImageSize = _leftImageSize;
        }];
    }
}
複製程式碼

因為每個section都持有它內部的所有cell的viewModel,所以在set方法裡面,如果發現傳進來的配置與當前配置不一致,就需要更新所有cell的viewModel對應的屬性。

既然section的ViewModel能做這些,為什麼還要有一個cell層的配置呢?

-- 只是為了提高配置的自由度罷了,萬一突然來個需求需要某個cell很獨特呢?(大家應該知道我說的神麼意思 ^^)

cell的viewModel屬性的set方法的實現和section的一致,這裡就不上程式碼了。

5. 新增支援重新整理功能

在1.1.2版本支援了:在更新資料來源後,重新整理資料來源。 舉個例子:在發現頁模擬網路請求,在請求結束後更新某個cell的viewmodel:

//模擬網路請求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        //請求成功x
        NSDictionary *responseDict = @{@"title_info":@"新遊戲上架啦",
                                       @"title_icon":@"game_1",
                                       @"game_info":@"一起來玩鬥地主呀!",
                                       @"game_icon":@"doudizhu"
                                       };
        //將要重新整理cell的indexPath
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:3];
        
        //獲取cell對應的viewModel
        SJStaticTableviewCellViewModel *viewModel = [self.dataSource tableView:self.tableView cellViewModelAtIndexPath:indexPath];
        
        if (viewModel) {
            //更新viewModel
            viewModel.leftTitle = responseDict[@"title_info"];
            viewModel.leftImage = [UIImage imageNamed:responseDict[@"title_icon"]];
            viewModel.indicatorLeftImage = [UIImage imageNamed:responseDict[@"game_icon"]];
            viewModel.indicatorLeftTitle = responseDict[@"game_info"];
            
            //重新整理tableview
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
 });
複製程式碼

效果圖:

更新資料來源後重新整理表格

6. 新增支援資料來源完全依賴網路請求

在1.2.0版本支援了:資料來源完全依賴網路請求的情況。

現在的最新版本里,SJStaticViewController在建立的時候分為兩種情況:

  1. SJDefaultDataTypeExist:在表格生成之前就存在資料,可以是表格的全部資料,也可以是表格的預設資料(後來通過網路請求來更新部分資料,參考上一節)。
  2. SJDefaultDataTypeNone:意味著當前沒有任何的預設資料可以使用,也就是無法生成tableview,需要在網路請求拿到資料後,再手動呼叫生成資料來源,生成表格的方法。
//SJStaticTableViewController.h
typedef enum : NSUInteger {
    
    SJDefaultDataTypeExist,    //在表格生成之前就有資料(1. 完全不依賴網路請求,有現成的完整資料 2. 先生成預設資料,然後通過網路請求來更新資料並重新整理表格)
    SJDefaultDataTypeNone,     //無法生成預設資料,需要完全依賴網路請求,在拿到資料後,生成表格
    
}SJDefaultDataType;

- (instancetype)initWithDefaultDataType:(SJDefaultDataType)defualtDataType;
複製程式碼
//SJStaticTableViewController.m
- (instancetype)initWithDefaultDataType:(SJDefaultDataType)defualtDataType
{
    self = [super init];
    if (self) {
        self.defualtDataType = defualtDataType;
    }
    return self;
}

- (instancetype)init
{
    self = [self initWithDefaultDataType:SJDefaultDataTypeExist];//預設是SJDefaultDataTypeExist
    return self;
}

- (void)viewDidLoad {
    
    [super viewDidLoad];
    [self configureNav];
    
    //在能夠提供給tableivew全部,或者部分資料來源的情況下,可以先構造出tableview;
    //否則,需要在網路請求結束後,手動呼叫configureTableView方法
    if (self.defualtDataType == SJDefaultDataTypeExist) {
        [self configureTableView];
    }
}

//只有在SJDefaultDataTypeExist的時候才會自動呼叫,否則需要手動呼叫
- (void)configureTableView
{
    [self createDataSource];//生成資料來源
    [self createTableView];//生成表格
}
複製程式碼

看一個例子,我們將表情頁設定為SJDefaultDataTypeNone,那麼就意味著我們需要手動呼叫configureTableView方法:

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
     self.navigationItem.title = @"表情";
    [self networkRequest];
}


- (void)networkRequest
{
    [MBProgressHUD showHUDAddedTo: self.view animated:YES];
    
    //模擬網路請求
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        [MBProgressHUD hideHUDForView: self.view animated:YES];
         self.modelsArray = [Factory emoticonPage];//網路請求後,將資料儲存在self.modelsArray裡面
        [self configureTableView];//手動呼叫
        
    });
}

- (void)createDataSource
{
    self.dataSource = [[SJStaticTableViewDataSource alloc] initWithViewModelsArray:self.modelsArray configureBlock:^(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel *viewModel) {
        
        switch (viewModel.staticCellType) {
                
            case SJStaticCellTypeSystemAccessoryDisclosureIndicator:
            {
                [cell configureAccessoryDisclosureIndicatorCellWithViewModel:viewModel];
            }
                break;
                
            default:
                break;
        }
    }];
}
複製程式碼

看一下效果圖:

基於MVVM,用於快速搭建設定頁,個人資訊頁的框架
好了,到這裡就講差不多了,程式碼量雖然不多,但是都說清楚還是感覺挺需要時間想的。

希望如果各位覺得哪裡不好,可以給出您的寶貴意見~

本篇已同步到個人部落格:傳送門

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章