iOS開發系列--UITableView全面解析

KenshinCui發表於2014-08-23

--UIKit之UITableView

概述

在iOS開發中UITableView可以說是使用最廣泛的控制元件,我們平時使用的軟體中到處都可以看到它的影子,類似於微信、QQ、新浪微博等軟體基本上隨處都是UITableView。當然它的廣泛使用自然離不開它強大的功能,今天這篇文章將針對UITableView重點展開討論。今天的主要內容包括:

  1. 基本介紹
  2. 資料來源
  3. 代理
  4. 效能優化
  5. UITableViewCell
  6. 常用操作
  7. UITableViewController
  8. MVC模式

基本介紹

UITableView有兩種風格:UITableViewStylePlain和UITableViewStyleGrouped。這兩者操作起來其實並沒有本質區別,只是後者按分組樣式顯示前者按照普通樣式顯示而已。大家先看一下兩者的應用:

1>分組樣式

UITableViewStyleGrouped1      UITableViewStyleGrouped2

2>不分組樣式

UITableViewStylePlain1       UITableViewStylePlain2

大家可以看到在UITableView中資料只有行的概念,並沒有列的概念,因為在手機作業系統中顯示多列是不利於操作的。UITableView中每行資料都是一個UITableViewCell,在這個控制元件中為了顯示更多的資訊,iOS已經在其內部設定好了多個子控制元件以供開發者使用。如果我們檢視UITableViewCell的宣告檔案可以發現在內部有一個UIView控制元件(contentView,作為其他元素的父控制元件)、兩個UILable控制元件(textLabel、detailTextLabel)、一個UIImage控制元件(imageView),分別用於容器、顯示內容、詳情和圖片。使用效果類似於微信、QQ資訊列表:

UITableViewCell1      UITableViewCell2

當然,這些子控制元件並不一定要全部使用,具體操作時可以通過UITableViewCellStyle進行設定,具體每個列舉表示的意思已經在程式碼中進行了註釋:

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
    UITableViewCellStyleDefault,    // 左側顯示textLabel(不顯示detailTextLabel),imageView可選(顯示在最左邊)
    UITableViewCellStyleValue1,        // 左側顯示textLabel、右側顯示detailTextLabel(預設藍色),imageView可選(顯示在最左邊)
    UITableViewCellStyleValue2,        // 左側依次顯示textLabel(預設藍色)和detailTextLabel,imageView可選(顯示在最左邊)
    UITableViewCellStyleSubtitle    // 左上方顯示textLabel,左下方顯示detailTextLabel(預設灰色),imageView可選(顯示在最左邊)
};

資料來源

由於iOS是遵循MVC模式設計的,很多操作都是通過代理和外界溝通的,但對於資料來源控制元件除了代理還有一個資料來源屬性,通過它和外界進行資料互動。 對於UITableView設定完dataSource後需要實現UITableViewDataSource協議,在這個協議中定義了多種 資料操作方法,下面通過建立一個簡單的聯絡人管理進行演示:

首先我們需要建立一個聯絡人模型KCContact

KCContact.h

//
//  Contact.h
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface KCContact : NSObject

#pragma mark 姓
@property (nonatomic,copy) NSString *firstName;
#pragma mark 名
@property (nonatomic,copy) NSString *lastName;
#pragma mark 手機號碼
@property (nonatomic,copy) NSString *phoneNumber;

#pragma mark 帶引數的建構函式
-(KCContact *)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName andPhoneNumber:(NSString *)phoneNumber;

#pragma mark 取得姓名
-(NSString *)getName;


#pragma mark 帶引數的靜態物件初始化方法
+(KCContact *)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName andPhoneNumber:(NSString *)phoneNumber;
@end

KCContact.m

//
//  Contact.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCContact.h"

@implementation KCContact

-(KCContact *)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName andPhoneNumber:(NSString *)phoneNumber{
    if(self=[super init]){
        self.firstName=firstName;
        self.lastName=lastName;
        self.phoneNumber=phoneNumber;
    }
    return self;
}

-(NSString *)getName{
    return [NSString stringWithFormat:@"%@ %@",_lastName,_firstName];
}

+(KCContact *)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName andPhoneNumber:(NSString *)phoneNumber{
    KCContact *contact1=[[KCContact alloc]initWithFirstName:firstName andLastName:lastName andPhoneNumber:phoneNumber];
    return contact1;
}

@end

為了演示分組顯示我們不妨將一組資料也抽象成模型KCContactGroup

KCContactGroup.h

//
//  KCContactGroup.h
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "KCContact.h"

@interface KCContactGroup : NSObject

#pragma mark 組名
@property (nonatomic,copy) NSString *name;

#pragma mark 分組描述
@property (nonatomic,copy) NSString *detail;

#pragma mark 聯絡人
@property (nonatomic,strong) NSMutableArray *contacts;

#pragma mark 帶引數個建構函式
-(KCContactGroup *)initWithName:(NSString *)name andDetail:(NSString *)detail andContacts:(NSMutableArray *)contacts;

#pragma mark 靜態初始化方法
+(KCContactGroup *)initWithName:(NSString *)name andDetail:(NSString *)detail andContacts:(NSMutableArray *)contacts;

@end

KCContactGroup.m

//
//  KCContactGroup.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCContactGroup.h"

@implementation KCContactGroup


-(KCContactGroup *)initWithName:(NSString *)name andDetail:(NSString *)detail andContacts:(NSMutableArray *)contacts{
    if (self=[super init]) {
        self.name=name;
        self.detail=detail;
        self.contacts=contacts;
    }
    return self;
}

+(KCContactGroup *)initWithName:(NSString *)name andDetail:(NSString *)detail andContacts:(NSMutableArray *)contacts{
    KCContactGroup *group1=[[KCContactGroup alloc]initWithName:name andDetail:detail andContacts:contacts];
    return group1;
}
@end

然後在viewDidLoad方法中建立一些模擬資料同時實現資料來源協議方法:

KCMainViewController.m

//
//  KCMainViewController.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCContact.h"
#import "KCContactGroup.h"

@interface KCMainViewController ()<UITableViewDataSource>{
    UITableView *_tableView;
    NSMutableArray *_contacts;//聯絡人模型
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化資料
    [self initData];
    
    //建立一個分組樣式的UITableView
    _tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
    
    //設定資料來源,注意必須實現對應的UITableViewDataSource協議
    _tableView.dataSource=self;
    
    [self.view addSubview:_tableView];
}

#pragma mark 載入資料
-(void)initData{
    _contacts=[[NSMutableArray alloc]init];
    
    KCContact *contact1=[KCContact initWithFirstName:@"Cui" andLastName:@"Kenshin" andPhoneNumber:@"18500131234"];
    KCContact *contact2=[KCContact initWithFirstName:@"Cui" andLastName:@"Tom" andPhoneNumber:@"18500131237"];
    KCContactGroup *group1=[KCContactGroup initWithName:@"C" andDetail:@"With names beginning with C" andContacts:[NSMutableArray arrayWithObjects:contact1,contact2, nil]];
    [_contacts addObject:group1];
    

    
    KCContact *contact3=[KCContact initWithFirstName:@"Lee" andLastName:@"Terry" andPhoneNumber:@"18500131238"];
    KCContact *contact4=[KCContact initWithFirstName:@"Lee" andLastName:@"Jack" andPhoneNumber:@"18500131239"];
    KCContact *contact5=[KCContact initWithFirstName:@"Lee" andLastName:@"Rose" andPhoneNumber:@"18500131240"];
    KCContactGroup *group2=[KCContactGroup initWithName:@"L" andDetail:@"With names beginning with L" andContacts:[NSMutableArray arrayWithObjects:contact3,contact4,contact5, nil]];
    [_contacts addObject:group2];
    
    
    
    KCContact *contact6=[KCContact initWithFirstName:@"Sun" andLastName:@"Kaoru" andPhoneNumber:@"18500131235"];
    KCContact *contact7=[KCContact initWithFirstName:@"Sun" andLastName:@"Rosa" andPhoneNumber:@"18500131236"];

    KCContactGroup *group3=[KCContactGroup initWithName:@"S" andDetail:@"With names beginning with S" andContacts:[NSMutableArray arrayWithObjects:contact6,contact7, nil]];
    [_contacts addObject:group3];
    
    
    KCContact *contact8=[KCContact initWithFirstName:@"Wang" andLastName:@"Stephone" andPhoneNumber:@"18500131241"];
    KCContact *contact9=[KCContact initWithFirstName:@"Wang" andLastName:@"Lucy" andPhoneNumber:@"18500131242"];
    KCContact *contact10=[KCContact initWithFirstName:@"Wang" andLastName:@"Lily" andPhoneNumber:@"18500131243"];
    KCContact *contact11=[KCContact initWithFirstName:@"Wang" andLastName:@"Emily" andPhoneNumber:@"18500131244"];
    KCContact *contact12=[KCContact initWithFirstName:@"Wang" andLastName:@"Andy" andPhoneNumber:@"18500131245"];
    KCContactGroup *group4=[KCContactGroup initWithName:@"W" andDetail:@"With names beginning with W" andContacts:[NSMutableArray arrayWithObjects:contact8,contact9,contact10,contact11,contact12, nil]];
    [_contacts addObject:group4];
    
    
    KCContact *contact13=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joy" andPhoneNumber:@"18500131246"];
    KCContact *contact14=[KCContact initWithFirstName:@"Zhang" andLastName:@"Vivan" andPhoneNumber:@"18500131247"];
    KCContact *contact15=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joyse" andPhoneNumber:@"18500131248"];
    KCContactGroup *group5=[KCContactGroup initWithName:@"Z" andDetail:@"With names beginning with Z" andContacts:[NSMutableArray arrayWithObjects:contact13,contact14,contact15, nil]];
    [_contacts addObject:group5];

}

#pragma mark - 資料來源方法
#pragma mark 返回分組數
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    NSLog(@"計算分組數");
    return _contacts.count;
}

#pragma mark 返回每組行數
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    NSLog(@"計算每組(組%i)行數",section);
    KCContactGroup *group1=_contacts[section];
    return group1.contacts.count;
}

#pragma mark返回每行的單元格
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    //NSIndexPath是一個結構體,記錄了組和行資訊
    NSLog(@"生成單元格(組:%i,行%i)",indexPath.section,indexPath.row);
    KCContactGroup *group=_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    UITableViewCell *cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
    cell.textLabel.text=[contact getName];
    cell.detailTextLabel.text=contact.phoneNumber;
    return cell;
}

#pragma mark 返回每組頭標題名稱
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    NSLog(@"生成組(組%i)名稱",section);
    KCContactGroup *group=_contacts[section];
    return group.name;
}

#pragma mark 返回每組尾部說明
-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section{
    NSLog(@"生成尾部(組%i)詳情",section);
    KCContactGroup *group=_contacts[section];
    return group.detail;
}
@end

執行可以看到如下效果:

Contact

大家在使用iPhone通訊錄時會發現右側可以按字母檢索,使用起來很方便,其實這個功能使用UITableView實現很簡單,只要實現資料來源協議的一個方法,構建一個分組標題的陣列即可實現。陣列元素的內容和組標題內容未必完全一致,UITableView是按照陣列元素的索引和每組資料索引順序來定位的而不是按內容查詢。

#pragma mark 返回每組標題索引
-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
    NSLog(@"生成組索引");
    NSMutableArray *indexs=[[NSMutableArray alloc]init];
    for(KCContactGroup *group in _contacts){
        [indexs addObject:group.name];
    }
    return indexs;
}

效果如下:

UITableViewIndex

需要注意的是上面幾個重點方法的執行順序,請看下圖:

image

值得指出的是生成單元格的方法並不是一次全部呼叫,而是隻會生產當前顯示在介面上的單元格,當使用者滾動操作時再顯示其他單元格。

注意:隨著我們的應用越來越複雜,可能經常需要除錯程式,在iOS中預設情況下不能定位到錯誤程式碼行,我們可以通過如下設定讓程式定位到出錯程式碼行:Show the Breakpoint  navigator—Add Exception breakpoint。

 

代理

上面我們已經看到通訊錄的簡單實現,但是我們發現單元格高度、分組標題高度以及尾部說明的高度都需要調整,此時就需要使用代理方法。UITableView代理方法有很多,例如監聽單元格顯示週期、監聽單元格選擇編輯操作、設定是否高亮顯示單元格、設定行高等。

1.設定行高

#pragma mark - 代理方法
#pragma mark 設定分組標題內容高度
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    if(section==0){
        return 50;
    }
    return 40;
}

#pragma mark 設定每行高度(每行高度可以不一樣)
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 45;
}

#pragma mark 設定尾部說明內容高度
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
    return 40;
}

2.監聽點選

在iOS中點選某聯絡個人就可以呼叫這個聯絡人,這時就需要監聽點選操作,這裡就不演示呼叫聯絡人操作了,我們演示一下修改人員資訊的操作。

KCMainViewContrller.m

//
//  KCMainViewController.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCContact.h"
#import "KCContactGroup.h"

@interface KCMainViewController ()<UITableViewDataSource,UITableViewDelegate,UIAlertViewDelegate>{
    UITableView *_tableView;
    NSMutableArray *_contacts;//聯絡人模型
    NSIndexPath *_selectedIndexPath;//當前選中的組和行
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化資料
    [self initData];
    
    //建立一個分組樣式的UITableView
    _tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
    
    //設定資料來源,注意必須實現對應的UITableViewDataSource協議
    _tableView.dataSource=self;
    //設定代理
    _tableView.delegate=self;
    
    [self.view addSubview:_tableView];
}

#pragma mark 載入資料
-(void)initData{
    _contacts=[[NSMutableArray alloc]init];
    
    KCContact *contact1=[KCContact initWithFirstName:@"Cui" andLastName:@"Kenshin" andPhoneNumber:@"18500131234"];
    KCContact *contact2=[KCContact initWithFirstName:@"Cui" andLastName:@"Tom" andPhoneNumber:@"18500131237"];
    KCContactGroup *group1=[KCContactGroup initWithName:@"C" andDetail:@"With names beginning with C" andContacts:[NSMutableArray arrayWithObjects:contact1,contact2, nil]];
    [_contacts addObject:group1];
    

    
    KCContact *contact3=[KCContact initWithFirstName:@"Lee" andLastName:@"Terry" andPhoneNumber:@"18500131238"];
    KCContact *contact4=[KCContact initWithFirstName:@"Lee" andLastName:@"Jack" andPhoneNumber:@"18500131239"];
    KCContact *contact5=[KCContact initWithFirstName:@"Lee" andLastName:@"Rose" andPhoneNumber:@"18500131240"];
    KCContactGroup *group2=[KCContactGroup initWithName:@"L" andDetail:@"With names beginning with L" andContacts:[NSMutableArray arrayWithObjects:contact3,contact4,contact5, nil]];
    [_contacts addObject:group2];
    
    
    
    KCContact *contact6=[KCContact initWithFirstName:@"Sun" andLastName:@"Kaoru" andPhoneNumber:@"18500131235"];
    KCContact *contact7=[KCContact initWithFirstName:@"Sun" andLastName:@"Rosa" andPhoneNumber:@"18500131236"];

    KCContactGroup *group3=[KCContactGroup initWithName:@"S" andDetail:@"With names beginning with S" andContacts:[NSMutableArray arrayWithObjects:contact6,contact7, nil]];
    [_contacts addObject:group3];
    
    
    KCContact *contact8=[KCContact initWithFirstName:@"Wang" andLastName:@"Stephone" andPhoneNumber:@"18500131241"];
    KCContact *contact9=[KCContact initWithFirstName:@"Wang" andLastName:@"Lucy" andPhoneNumber:@"18500131242"];
    KCContact *contact10=[KCContact initWithFirstName:@"Wang" andLastName:@"Lily" andPhoneNumber:@"18500131243"];
    KCContact *contact11=[KCContact initWithFirstName:@"Wang" andLastName:@"Emily" andPhoneNumber:@"18500131244"];
    KCContact *contact12=[KCContact initWithFirstName:@"Wang" andLastName:@"Andy" andPhoneNumber:@"18500131245"];
    KCContactGroup *group4=[KCContactGroup initWithName:@"W" andDetail:@"With names beginning with W" andContacts:[NSMutableArray arrayWithObjects:contact8,contact9,contact10,contact11,contact12, nil]];
    [_contacts addObject:group4];
    
    
    KCContact *contact13=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joy" andPhoneNumber:@"18500131246"];
    KCContact *contact14=[KCContact initWithFirstName:@"Zhang" andLastName:@"Vivan" andPhoneNumber:@"18500131247"];
    KCContact *contact15=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joyse" andPhoneNumber:@"18500131248"];
    KCContactGroup *group5=[KCContactGroup initWithName:@"Z" andDetail:@"With names beginning with Z" andContacts:[NSMutableArray arrayWithObjects:contact13,contact14,contact15, nil]];
    [_contacts addObject:group5];

}

#pragma mark - 資料來源方法
#pragma mark 返回分組數
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    NSLog(@"計算分組數");
    return _contacts.count;
}

#pragma mark 返回每組行數
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    NSLog(@"計算每組(組%i)行數",section);
    KCContactGroup *group1=_contacts[section];
    return group1.contacts.count;
}

#pragma mark返回每行的單元格
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    //NSIndexPath是一個物件,記錄了組和行資訊
    NSLog(@"生成單元格(組:%i,行%i)",indexPath.section,indexPath.row);
    KCContactGroup *group=_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    UITableViewCell *cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
    cell.textLabel.text=[contact getName];
    cell.detailTextLabel.text=contact.phoneNumber;
    return cell;
}

#pragma mark 返回每組頭標題名稱
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    NSLog(@"生成組(組%i)名稱",section);
    KCContactGroup *group=_contacts[section];
    return group.name;
}

#pragma mark 返回每組尾部說明
-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section{
    NSLog(@"生成尾部(組%i)詳情",section);
    KCContactGroup *group=_contacts[section];
    return group.detail;
}

#pragma mark 返回每組標題索引
-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
    NSLog(@"生成組索引");
    NSMutableArray *indexs=[[NSMutableArray alloc]init];
    for(KCContactGroup *group in _contacts){
        [indexs addObject:group.name];
    }
    return indexs;
}

#pragma mark - 代理方法
#pragma mark 設定分組標題內容高度
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    if(section==0){
        return 50;
    }
    return 40;
}

#pragma mark 設定每行高度(每行高度可以不一樣)
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 45;
}

#pragma mark 設定尾部說明內容高度
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
    return 40;
}

#pragma mark 點選行
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    _selectedIndexPath=indexPath;
    KCContactGroup *group=_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    //建立彈出視窗
    UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"System Info" message:[contact getName] delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
    alert.alertViewStyle=UIAlertViewStylePlainTextInput; //設定視窗內容樣式
    UITextField *textField= [alert textFieldAtIndex:0]; //取得文字框
    textField.text=contact.phoneNumber; //設定文字框內容
    [alert show]; //顯示視窗
}

#pragma mark 視窗的代理方法,使用者儲存資料
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    //當點選了第二個按鈕(OK)
    if (buttonIndex==1) {
        UITextField *textField= [alertView textFieldAtIndex:0];
        //修改模型資料
        KCContactGroup *group=_contacts[_selectedIndexPath.section];
        KCContact *contact=group.contacts[_selectedIndexPath.row];
        contact.phoneNumber=textField.text;
        //重新整理表格
        [_tableView reloadData];
    }
}

#pragma mark 重寫狀態樣式方法
-(UIStatusBarStyle)preferredStatusBarStyle{
    return UIStatusBarStyleLightContent;
}
@end

在上面的程式碼中我們通過修改模型來改變UI顯示,這種方式是經典的MVC應用,在後面的程式碼中會經常看到。當然UI的重新整理使用了UITableView的reloadData方法,該方法會重新呼叫資料來源方法,包括計算分組、計算每個分組的行數,生成單元格等重新整理整個UITableView。當然這種方式在實際開發中是不可取的,我們不可能因為修改了一個人的資訊就重新整理整個UITableViewView,此時我們需要採用區域性重新整理。區域性重新整理使用起來很簡單,只需要呼叫UITableView的另外一個方法:

#pragma mark 視窗的代理方法,使用者儲存資料
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    //當點選了第二個按鈕(OK)
    if (buttonIndex==1) {
        UITextField *textField= [alertView textFieldAtIndex:0];
        //修改模型資料
        KCContactGroup *group=_contacts[_selectedIndexPath.section];
        KCContact *contact=group.contacts[_selectedIndexPath.row];
        contact.phoneNumber=textField.text;
        //重新整理表格
        NSArray *indexPaths=@[_selectedIndexPath];//需要區域性重新整理的單元格的組、行
        [_tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];//後面的引數代表更新時的動畫
    }
}

效能優化

前面已經說過UITableView中的單元格cell是在顯示到使用者可視區域後建立的,那麼如果使用者往下滾動就會繼續建立顯示在螢幕上的單元格,如果使用者向上滾動返回到檢視過的內容時同樣會重新建立之前已經建立過的單元格。如此一來即使UITableView的內容不是太多,如果使用者反覆的上下滾動,記憶體也會瞬間飆升,更何況很多時候UITableView的內容是很多的(例如微博展示列表,基本向下滾動是沒有底限的)。

前面一節中我們曾經提到過如何優化UIScrollView,當時就是利用有限的UIImageView動態切換其內容來儘可能減少資源佔用。同樣的,在UITableView中也可以採用類似的方式,只是這時我們不是在滾動到指定位置後更改滾動的位置而是要將當前沒有顯示的Cell重新顯示在將要顯示的Cell的位置然後更新其內容。原因就是UITableView中的Cell結構佈局可能是不同的,通過重新定位是不可取的,而是需要重用已經不再介面顯示的已建立過的Cell。

當然,聽起來這麼做比較複雜,其實實現起來很簡單,因為UITableView已經為我們實現了這種機制。在UITableView內部有一個快取池,初始化時使用initWithStyle:(UITableViewCellStyle) reuseIdentifier:(NSString *)方法指定一個可重用標識,就可以將這個cell放到快取池。然後在使用時使用指定的標識去快取池中取得對應的cell然後修改cell內容即可。

#pragma mark返回每行的單元格
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    //NSIndexPath是一個物件,記錄了組和行資訊
    NSLog(@"生成單元格(組:%i,行%i)",indexPath.section,indexPath.row);
    KCContactGroup *group=_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    
    //由於此方法呼叫十分頻繁,cell的標示宣告成靜態變數有利於效能優化
    static NSString *cellIdentifier=@"UITableViewCellIdentifierKey1";
    //首先根據標識去快取池取
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    //如果快取池沒有到則重新建立並放到快取池中
    if(!cell){
        cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    
    cell.textLabel.text=[contact getName];
    cell.detailTextLabel.text=contact.phoneNumber;
    NSLog(@"cell:%@",cell);
    return cell;
}

上面的程式碼中已經列印了cell的地址,如果大家執行測試上下滾動UITableView會發現滾動時建立的cell地址是初始化時已經建立的。

這裡再次給大家強調兩點:

  1. -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)方法呼叫很頻繁,無論是初始化、上下滾動、重新整理都會呼叫此方法,所有在這裡執行的操作一定要注意效能;
  2. 可重用標識可以有多個,如果在UITableView中有多類結構不同的Cell,可以通過這個標識進行快取和重新;

UITableViewCell

1.自帶的UITableViewCell

UITableViewCell是構建一個UITableView的基礎,在UITableViewCell內部有一個UIView控制元件作為其他內容的容器,它上面有一個UIImageView和兩個UILabel,通過UITableViewCellStyle屬性可以對其樣式進行控制。其結構如下:

UITableViewCellStuct

有時候我們會發現很多UITableViewCell右側可以顯示不同的圖示,在iOS中稱之為訪問器,點選可以觸發不同的事件,例如設定功能:

UITableViewCellAccesoryType

要設定這些圖示只需要設定UITableViewCell的accesoryType屬性,這是一個列舉型別具體含義如下:

typedef NS_ENUM(NSInteger, UITableViewCellAccessoryType) {
    UITableViewCellAccessoryNone,                   // 不顯示任何圖示
    UITableViewCellAccessoryDisclosureIndicator,    // 跳轉指示圖示
    UITableViewCellAccessoryDetailDisclosureButton, // 內容詳情圖示和跳轉指示圖示
    UITableViewCellAccessoryCheckmark,              // 勾選圖示
    UITableViewCellAccessoryDetailButton NS_ENUM_AVAILABLE_IOS(7_0) // 內容詳情圖示
};

例如在最近通話中我們通常設定為詳情圖示,點選可以檢視聯絡人詳情:

UITableViewCellAccessoryTypeDetaiButtonl

很明顯iOS設定中第一個accessoryType不在列舉之列,右側的訪問器型別是UISwitch控制元件,那麼如何顯示自定義的訪問器呢?其實只要設定UITableViewCell的accessoryView即可,它支援任何UIView控制元件。假設我們在通訊錄每組第一行放一個UISwitch,同時切換時可以輸出對應資訊:

//
//  KCMainViewController.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCContact.h"
#import "KCContactGroup.h"

@interface KCMainViewController ()<UITableViewDataSource,UITableViewDelegate,UIAlertViewDelegate>{
    UITableView *_tableView;
    NSMutableArray *_contacts;//聯絡人模型
    NSIndexPath *_selectedIndexPath;//當前選中的組和行
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化資料
    [self initData];
    
    //建立一個分組樣式的UITableView
    _tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
    
    //設定資料來源,注意必須實現對應的UITableViewDataSource協議
    _tableView.dataSource=self;
    //設定代理
    _tableView.delegate=self;
    
    [self.view addSubview:_tableView];
}

#pragma mark 載入資料
-(void)initData{
    _contacts=[[NSMutableArray alloc]init];
    
    KCContact *contact1=[KCContact initWithFirstName:@"Cui" andLastName:@"Kenshin" andPhoneNumber:@"18500131234"];
    KCContact *contact2=[KCContact initWithFirstName:@"Cui" andLastName:@"Tom" andPhoneNumber:@"18500131237"];
    KCContactGroup *group1=[KCContactGroup initWithName:@"C" andDetail:@"With names beginning with C" andContacts:[NSMutableArray arrayWithObjects:contact1,contact2, nil]];
    [_contacts addObject:group1];
    

    
    KCContact *contact3=[KCContact initWithFirstName:@"Lee" andLastName:@"Terry" andPhoneNumber:@"18500131238"];
    KCContact *contact4=[KCContact initWithFirstName:@"Lee" andLastName:@"Jack" andPhoneNumber:@"18500131239"];
    KCContact *contact5=[KCContact initWithFirstName:@"Lee" andLastName:@"Rose" andPhoneNumber:@"18500131240"];
    KCContactGroup *group2=[KCContactGroup initWithName:@"L" andDetail:@"With names beginning with L" andContacts:[NSMutableArray arrayWithObjects:contact3,contact4,contact5, nil]];
    [_contacts addObject:group2];
    
    
    
    KCContact *contact6=[KCContact initWithFirstName:@"Sun" andLastName:@"Kaoru" andPhoneNumber:@"18500131235"];
    KCContact *contact7=[KCContact initWithFirstName:@"Sun" andLastName:@"Rosa" andPhoneNumber:@"18500131236"];

    KCContactGroup *group3=[KCContactGroup initWithName:@"S" andDetail:@"With names beginning with S" andContacts:[NSMutableArray arrayWithObjects:contact6,contact7, nil]];
    [_contacts addObject:group3];
    
    
    KCContact *contact8=[KCContact initWithFirstName:@"Wang" andLastName:@"Stephone" andPhoneNumber:@"18500131241"];
    KCContact *contact9=[KCContact initWithFirstName:@"Wang" andLastName:@"Lucy" andPhoneNumber:@"18500131242"];
    KCContact *contact10=[KCContact initWithFirstName:@"Wang" andLastName:@"Lily" andPhoneNumber:@"18500131243"];
    KCContact *contact11=[KCContact initWithFirstName:@"Wang" andLastName:@"Emily" andPhoneNumber:@"18500131244"];
    KCContact *contact12=[KCContact initWithFirstName:@"Wang" andLastName:@"Andy" andPhoneNumber:@"18500131245"];
    KCContactGroup *group4=[KCContactGroup initWithName:@"W" andDetail:@"With names beginning with W" andContacts:[NSMutableArray arrayWithObjects:contact8,contact9,contact10,contact11,contact12, nil]];
    [_contacts addObject:group4];
    
    
    KCContact *contact13=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joy" andPhoneNumber:@"18500131246"];
    KCContact *contact14=[KCContact initWithFirstName:@"Zhang" andLastName:@"Vivan" andPhoneNumber:@"18500131247"];
    KCContact *contact15=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joyse" andPhoneNumber:@"18500131248"];
    KCContactGroup *group5=[KCContactGroup initWithName:@"Z" andDetail:@"With names beginning with Z" andContacts:[NSMutableArray arrayWithObjects:contact13,contact14,contact15, nil]];
    [_contacts addObject:group5];

}

#pragma mark - 資料來源方法
#pragma mark 返回分組數
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    NSLog(@"計算分組數");
    return _contacts.count;
}

#pragma mark 返回每組行數
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    NSLog(@"計算每組(組%i)行數",section);
    KCContactGroup *group1=_contacts[section];
    return group1.contacts.count;
}

#pragma mark返回每行的單元格
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    //NSIndexPath是一個物件,記錄了組和行資訊
    NSLog(@"生成單元格(組:%i,行%i)",indexPath.section,indexPath.row);
    KCContactGroup *group=_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    
    //由於此方法呼叫十分頻繁,cell的標示宣告成靜態變數有利於效能優化
    static NSString *cellIdentifier=@"UITableViewCellIdentifierKey1";
    static NSString *cellIdentifierForFirstRow=@"UITableViewCellIdentifierKeyWithSwitch";
    //首先根據標示去快取池取
    UITableViewCell *cell;
    if (indexPath.row==0) {
        cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifierForFirstRow];
    }else{
        cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    }
    //如果快取池沒有取到則重新建立並放到快取池中
    if(!cell){
        if (indexPath.row==0) {
            cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifierForFirstRow];
            UISwitch *sw=[[UISwitch alloc]init];
            [sw addTarget:self action:@selector(switchValueChange:) forControlEvents:UIControlEventValueChanged];
            cell.accessoryView=sw;

        }else{
            cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
            cell.accessoryType=UITableViewCellAccessoryDetailButton;
        }
    }
    
    if(indexPath.row==0){
        ((UISwitch *)cell.accessoryView).tag=indexPath.section;
    }
    
    cell.textLabel.text=[contact getName];
    cell.detailTextLabel.text=contact.phoneNumber;
    NSLog(@"cell:%@",cell);
    
    return cell;
}

#pragma mark 返回每組頭標題名稱
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    NSLog(@"生成組(組%i)名稱",section);
    KCContactGroup *group=_contacts[section];
    return group.name;
}

#pragma mark 返回每組尾部說明
-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section{
    NSLog(@"生成尾部(組%i)詳情",section);
    KCContactGroup *group=_contacts[section];
    return group.detail;
}

#pragma mark 返回每組標題索引
-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
    NSLog(@"生成組索引");
    NSMutableArray *indexs=[[NSMutableArray alloc]init];
    for(KCContactGroup *group in _contacts){
        [indexs addObject:group.name];
    }
    return indexs;
}

#pragma mark - 代理方法
#pragma mark 設定分組標題內容高度
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    if(section==0){
        return 50;
    }
    return 40;
}

#pragma mark 設定每行高度(每行高度可以不一樣)
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 45;
}

#pragma mark 設定尾部說明內容高度
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
    return 40;
}

#pragma mark 點選行
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    _selectedIndexPath=indexPath;
    KCContactGroup *group=_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    //建立彈出視窗
    UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"System Info" message:[contact getName] delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
    alert.alertViewStyle=UIAlertViewStylePlainTextInput; //設定視窗內容樣式
    UITextField *textField= [alert textFieldAtIndex:0]; //取得文字框
    textField.text=contact.phoneNumber; //設定文字框內容
    [alert show]; //顯示視窗
}

#pragma mark 視窗的代理方法,使用者儲存資料
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    //當點選了第二個按鈕(OK)
    if (buttonIndex==1) {
        UITextField *textField= [alertView textFieldAtIndex:0];
        //修改模型資料
        KCContactGroup *group=_contacts[_selectedIndexPath.section];
        KCContact *contact=group.contacts[_selectedIndexPath.row];
        contact.phoneNumber=textField.text;
        //重新整理表格
        NSArray *indexPaths=@[_selectedIndexPath];//需要區域性重新整理的單元格的組、行
        [_tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];//後面的引數程式碼更新時的動畫
    }
}


#pragma mark 重寫狀態樣式方法
-(UIStatusBarStyle)preferredStatusBarStyle{
    return UIStatusBarStyleLightContent;
}


#pragma mark 切換開關轉化事件
-(void)switchValueChange:(UISwitch *)sw{
    NSLog(@"section:%i,switch:%i",sw.tag, sw.on);
}
@end

最終執行效果:

Run

注意:

  1. 由於此時我們需要兩種UITableViewCell樣式,考慮到效能我們需要在快取池快取兩種Cell。
  2. UISwitch繼承於UIControl而不是UIView(當然UIControl最終也是繼承於UIView),繼承於UIControl的控制元件使用addTarget新增對應事件而不是代理,同時有“是否可用”、“是否高亮”、“是否選中”等屬性;
  3. 上面程式碼中如果有些UITableViewCell的UISwitch設定為on當其他控制元件重用時狀態也是on,解決這個問題可以在模型中設定對應的屬性記錄其狀態,在生成cell時設定當前狀態(為了儘可能簡化上面的程式碼這裡就不再修復這個問題);

2.自定義UITableViewCell

雖然系統自帶的UITableViewCell已經夠強大了,但是很多時候這並不能滿足我們的需求。例如新浪微博的Cell就沒有那麼簡單:

UITableViewCell

沒錯,這個介面佈局也是UITableView實現的,其中的內容就是UITableViewCell,只是這個UITableViewCell是使用者自定義實現的。當然要實現上面的UITableViewCell三言兩語我們是說不完的,這裡我們實現一個簡化版本,介面原型如下:

UITableViewCellPrototypeDesign

我們對具體控制元件進行拆分:

UITableViewCellPrototypeDesign2

在這個介面中有2個UIImageView控制元件和4個UILabel,整個介面顯示效果類似於新浪微博的訊息內容介面,但是又在新浪微博基礎上進行了精簡以至於利用現有知識能夠順利開發出來。

在前面的內容中我們的資料都是手動構建的,在實際開發中自然不會這麼做,這裡我們不妨將微博資料儲存到plist檔案中然後從plist檔案讀取資料構建模型物件(實際開發微博當然需要進行網路資料請求,這裡只是進行模擬就不再演示網路請求的內容)。假設plist檔案內容如下:

StatusInfoPlist

接下來就定義一個KCStatusTableViewCell實現UITableViewCell,一般實現自定義UITableViewCell需要分為兩步:第一初始化控制元件;第二設定資料,重新設定控制元件frame。原因就是自定義Cell一般無法固定高度,很多時候高度需要隨著內容改變。此外由於在單元格內部是無法控制單元格高度的,因此一般會定義一個高度屬性用於在UITableView的代理事件中設定每個單元格高度。

1.首先看一下微博模型KCStatus,這個模型主要的方法就是根據plist字典內容生成微博物件:

KCStatus.h

//
//  KCStatus.h
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface KCStatus : NSObject

#pragma mark - 屬性
@property (nonatomic,assign) long long Id;//微博id
@property (nonatomic,copy) NSString *profileImageUrl;//頭像
@property (nonatomic,copy) NSString *userName;//傳送使用者
@property (nonatomic,copy) NSString *mbtype;//會員型別
@property (nonatomic,copy) NSString *createdAt;//建立時間
@property (nonatomic,copy) NSString *source;//裝置來源
@property (nonatomic,copy) NSString *text;//微博內容



#pragma mark - 方法
#pragma mark 根據字典初始化微博物件
-(KCStatus *)initWithDictionary:(NSDictionary *)dic;

#pragma mark 初始化微博物件(靜態方法)
+(KCStatus *)statusWithDictionary:(NSDictionary *)dic;
@end

KCStatus.m

//
//  KCStatus.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCStatus.h"

@implementation KCStatus

#pragma mark 根據字典初始化微博物件
-(KCStatus *)initWithDictionary:(NSDictionary *)dic{
    if(self=[super init]){
        self.Id=[dic[@"Id"] longLongValue];
        self.profileImageUrl=dic[@"profileImageUrl"];
        self.userName=dic[@"userName"];
        self.mbtype=dic[@"mbtype"];
        self.createdAt=dic[@"createdAt"];
        self.source=dic[@"source"];
        self.text=dic[@"text"];
    }
    return self;
}

#pragma mark 初始化微博物件(靜態方法)
+(KCStatus *)statusWithDictionary:(NSDictionary *)dic{
    KCStatus *status=[[KCStatus alloc]initWithDictionary:dic];
    return status;
}

-(NSString *)source{
    return [NSString stringWithFormat:@"來自 %@",_source];
}
@end

2.然後看一下自定義的Cell

KCStatusTableViewCell.h

//
//  KCStatusTableViewCell.h
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <UIKit/UIKit.h>
@class KCStatus;

@interface KCStatusTableViewCell : UITableViewCell

#pragma mark 微博物件
@property (nonatomic,strong) KCStatus *status;

#pragma mark 單元格高度
@property (assign,nonatomic) CGFloat height;

@end

KCStatusTableViewCell.m

//
//  KCStatusTableViewCell.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCStatusTableViewCell.h"
#import "KCStatus.h"
#define KCColor(r,g,b) [UIColor colorWithHue:r/255.0 saturation:g/255.0 brightness:b/255.0 alpha:1] //顏色巨集定義
#define kStatusTableViewCellControlSpacing 10 //控制元件間距
#define kStatusTableViewCellBackgroundColor KCColor(251,251,251)
#define kStatusGrayColor KCColor(50,50,50)
#define kStatusLightGrayColor KCColor(120,120,120)

#define kStatusTableViewCellAvatarWidth 40 //頭像寬度
#define kStatusTableViewCellAvatarHeight kStatusTableViewCellAvatarWidth
#define kStatusTableViewCellUserNameFontSize 14
#define kStatusTableViewCellMbTypeWidth 13 //會員圖示寬度
#define kStatusTableViewCellMbTypeHeight kStatusTableViewCellMbTypeWidth
#define kStatusTableViewCellCreateAtFontSize 12
#define kStatusTableViewCellSourceFontSize 12
#define kStatusTableViewCellTextFontSize 14


@interface KCStatusTableViewCell(){
    UIImageView *_avatar;//頭像
    UIImageView *_mbType;//會員型別
    UILabel *_userName;
    UILabel *_createAt;
    UILabel *_source;
    UILabel *_text;
}

@end

@implementation KCStatusTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self initSubView];
    }
    return self;
}

#pragma mark 初始化檢視
-(void)initSubView{
    //頭像控制元件
    _avatar=[[UIImageView alloc]init];
    [self.contentView addSubview:_avatar];
    //使用者名稱
    _userName=[[UILabel alloc]init];
    _userName.textColor=kStatusGrayColor;
    _userName.font=[UIFont systemFontOfSize:kStatusTableViewCellUserNameFontSize];
    [self.contentView addSubview:_userName];
    //會員型別
    _mbType=[[UIImageView alloc]init];
    [self.contentView addSubview:_mbType];
    //日期
    _createAt=[[UILabel alloc]init];
    _createAt.textColor=kStatusLightGrayColor;
    _createAt.font=[UIFont systemFontOfSize:kStatusTableViewCellCreateAtFontSize];
    [self.contentView addSubview:_createAt];
    //裝置
    _source=[[UILabel alloc]init];
    _source.textColor=kStatusLightGrayColor;
    _source.font=[UIFont systemFontOfSize:kStatusTableViewCellSourceFontSize];
    [self.contentView addSubview:_source];
    //內容
    _text=[[UILabel alloc]init];
    _text.textColor=kStatusGrayColor;
    _text.font=[UIFont systemFontOfSize:kStatusTableViewCellTextFontSize];
    _text.numberOfLines=0;
//    _text.lineBreakMode=NSLineBreakByWordWrapping;
    [self.contentView addSubview:_text];
}

#pragma mark 設定微博
-(void)setStatus:(KCStatus *)status{
    //設定頭像大小和位置
    CGFloat avatarX=10,avatarY=10;
    CGRect avatarRect=CGRectMake(avatarX, avatarY, kStatusTableViewCellAvatarWidth, kStatusTableViewCellAvatarHeight);
    _avatar.image=[UIImage imageNamed:status.profileImageUrl];
    _avatar.frame=avatarRect;
    
    
    //設定會員圖示大小和位置
    CGFloat userNameX= CGRectGetMaxX(_avatar.frame)+kStatusTableViewCellControlSpacing ;
    CGFloat userNameY=avatarY;
    //根據文字內容取得文字佔用空間大小
    CGSize userNameSize=[status.userName sizeWithAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kStatusTableViewCellUserNameFontSize]}];
    CGRect userNameRect=CGRectMake(userNameX, userNameY, userNameSize.width,userNameSize.height);
    _userName.text=status.userName;
    _userName.frame=userNameRect;
    
    
    //設定會員圖示大小和位置
    CGFloat mbTypeX=CGRectGetMaxX(_userName.frame)+kStatusTableViewCellControlSpacing;
    CGFloat mbTypeY=avatarY;
    CGRect mbTypeRect=CGRectMake(mbTypeX, mbTypeY, kStatusTableViewCellMbTypeWidth, kStatusTableViewCellMbTypeHeight);
    _mbType.image=[UIImage imageNamed:status.mbtype];
    _mbType.frame=mbTypeRect;
    
    
    //設定釋出日期大小和位置
    CGSize createAtSize=[status.createdAt sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:kStatusTableViewCellCreateAtFontSize]}];
    CGFloat createAtX=userNameX;
    CGFloat createAtY=CGRectGetMaxY(_avatar.frame)-createAtSize.height;
    CGRect createAtRect=CGRectMake(createAtX, createAtY, createAtSize.width, createAtSize.height);
    _createAt.text=status.createdAt;
    _createAt.frame=createAtRect;
    
    
    //設定裝置資訊大小和位置
    CGSize sourceSize=[status.source sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:kStatusTableViewCellSourceFontSize]}];
    CGFloat sourceX=CGRectGetMaxX(_createAt.frame)+kStatusTableViewCellControlSpacing;
    CGFloat sourceY=createAtY;
    CGRect sourceRect=CGRectMake(sourceX, sourceY, sourceSize.width,sourceSize.height);
    _source.text=status.source;
    _source.frame=sourceRect;
    
    
    //設定微博內容大小和位置
    CGFloat textX=avatarX;
    CGFloat textY=CGRectGetMaxY(_avatar.frame)+kStatusTableViewCellControlSpacing;
    CGFloat textWidth=self.frame.size.width-kStatusTableViewCellControlSpacing*2;
    CGSize textSize=[status.text boundingRectWithSize:CGSizeMake(textWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kStatusTableViewCellTextFontSize]} context:nil].size;
    CGRect textRect=CGRectMake(textX, textY, textSize.width, textSize.height);
    _text.text=status.text;
    _text.frame=textRect;
    
    _height=CGRectGetMaxY(_text.frame)+kStatusTableViewCellControlSpacing;
}

#pragma mark 重寫選擇事件,取消選中
-(void)setSelected:(BOOL)selected animated:(BOOL)animated{
    
}
@end

這是我們自定義Cell這個例子的核心,自定義Cell分為兩個步驟:首先要進行各種控制元件的初始化工作,這個過程中只要將控制元件放到Cell的View中同時設定控制元件顯示內容的格式(字型大小、顏色等)即可;然後在資料物件設定方法中進行各個控制元件的佈局(大小、位置)。在程式碼中有幾點需要重點提示大家:

  • 對於單行文字資料的顯示呼叫- (CGSize)sizeWithAttributes:(NSDictionary *)attrs;方法來得到文字寬度和高度。
  • 對於多行文字資料的顯示呼叫- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(NSDictionary *)attributes context:(NSStringDrawingContext *)context ;方法來得到文字寬度和高度;同時注意在此之前需要設定文字控制元件的numberOfLines屬性為0。
  • 通常我們會在自定義Cell中設定一個高度屬性,用於外界方法呼叫,因為Cell內部設定Cell的高度是沒有用的,UITableViewCell在初始化時會重新設定高度。

3.最後我們看一下自定義Cell的使用過程:

KCStatusViewController.m

//
//  KCCutomCellViewController.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCStatusCellViewController.h"
#import "KCStatus.h"
#import "KCStatusTableViewCell.h"

@interface KCStatusCellViewController ()<UITableViewDataSource,UITableViewDelegate,UIAlertViewDelegate>{
    UITableView *_tableView;
    NSMutableArray *_status;
    NSMutableArray *_statusCells;//儲存cell,用於計算高度
}
@end

@implementation KCStatusCellViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化資料
    [self initData];
    
    //建立一個分組樣式的UITableView
    _tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
    
    //設定資料來源,注意必須實現對應的UITableViewDataSource協議
    _tableView.dataSource=self;
    //設定代理
    _tableView.delegate=self;
    
    [self.view addSubview:_tableView];
}

#pragma mark 載入資料
-(void)initData{
    NSString *path=[[NSBundle mainBundle] pathForResource:@"StatusInfo" ofType:@"plist"];
    NSArray *array=[NSArray arrayWithContentsOfFile:path];
    _status=[[NSMutableArray alloc]init];
    _statusCells=[[NSMutableArray alloc]init];
    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [_status addObject:[KCStatus statusWithDictionary:obj]];
        KCStatusTableViewCell *cell=[[KCStatusTableViewCell alloc]init];
        [_statusCells addObject:cell];
    }];
}
#pragma mark - 資料來源方法
#pragma mark 返回分組數
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 1;
}

#pragma mark 返回每組行數
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{

    return _status.count;
}

#pragma mark返回每行的單元格
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    static NSString *cellIdentifier=@"UITableViewCellIdentifierKey1";
    KCStatusTableViewCell *cell;
    cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if(!cell){
        cell=[[KCStatusTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    //在此設定微博,以便重新佈局
    KCStatus *status=_status[indexPath.row];
    cell.status=status;
    return cell;
}

#pragma mark - 代理方法
#pragma mark 重新設定單元格高度
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    //KCStatusTableViewCell *cell=[tableView cellForRowAtIndexPath:indexPath];
    KCStatusTableViewCell *cell= _statusCells[indexPath.row];
    cell.status=_status[indexPath.row];
    return cell.height;
}

#pragma mark 重寫狀態樣式方法
-(UIStatusBarStyle)preferredStatusBarStyle{
    return UIStatusBarStyleLightContent;
}
@end

這個類中需要重點強調一下:Cell的高度需要重新設定(前面說過無論Cell內部設定多高都沒有用,需要重新設定),這裡採用的方法是首先建立對應的Cell,然後在- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;方法中設定微博資料計算高度通知UITableView。

最後我們看一下執行的效果:

RunEffect2

常用操作

UITableView和UITableViewCell提供了強大的操作功能,這一節中會重點討論刪除、增加、排序等操作。為了方便演示我們還是在之前的通訊錄的基礎上演示,在此之前先來給檢視控制器新增一個工具條,在工具條左側放一個刪除按鈕,右側放一個新增按鈕:

#pragma mark 新增工具欄
-(void)addToolbar{
    CGRect frame=self.view.frame;
    _toolbar=[[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, frame.size.width, kContactToolbarHeight)];
    //    _toolbar.backgroundColor=[UIColor colorWithHue:246/255.0 saturation:246/255.0 brightness:246/255.0 alpha:1];
    [self.view addSubview:_toolbar];
    UIBarButtonItem *removeButton=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(remove)];
    UIBarButtonItem *flexibleButton=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    UIBarButtonItem *addButton=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add)];
    NSArray *buttonArray=[NSArray arrayWithObjects:removeButton,flexibleButton,addButton, nil];
    _toolbar.items=buttonArray;
}

1.刪除

在UITableView中無論是刪除操作還是新增操作都是通過修改UITableView的編輯狀態來改變的(除非你不用UITableView自帶的刪除功能)。在刪除按鈕中我們設定UITableView的編輯狀態:

#pragma mark 刪除
-(void)remove{
    //直接通過下面的方法設定編輯狀態沒有動畫
    //_tableView.editing=!_tableView.isEditing;
    
    [_tableView setEditing:!_tableView.isEditing animated:true];
}

點選刪除按鈕會在Cell的左側顯示刪除按鈕:

UITableViewCellRemoveStatus

此時點選左側刪除圖示右側出現刪除:

UITableViewCellDeleteStatus2

用過iOS的朋友都知道,一般這種Cell如果向左滑動右側就會出現刪除按鈕直接刪除就可以了。其實實現這個功能只要實現代理-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;方法,只要實現了此方法向左滑動就會顯示刪除按鈕。只要點選刪除按鈕這個方法就會呼叫,但是需要注意的是無論是刪除還是新增都是執行這個方法,只是第二個引數型別不同。下面看一下具體的刪除實現:

#pragma mark 刪除操作
//實現了此方法向左滑動就會顯示刪除按鈕
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
    KCContactGroup *group =_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    if (editingStyle==UITableViewCellEditingStyleDelete) {
        [group.contacts removeObject:contact];
        //考慮到效能這裡不建議使用reloadData
        //[tableView reloadData];
        //使用下面的方法既可以區域性重新整理又有動畫效果
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];
        
        //如果當前組中沒有資料則移除組重新整理整個表格
        if (group.contacts.count==0) {
            [_contacts removeObject:group];
            [tableView reloadData];
        }
    }
}

從這段程式碼我們再次看到了MVC的思想,要修改UI先修改資料。而且我們看到了另一個重新整理表格的方法- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;,使用這個方法可以再刪除之後重新整理對應的單元格。效果如下:

UITableViewCellDelete

2.新增

新增和刪除操作都是設定UITableView的編輯狀態,具體是新增還是刪除需要根據代理方法-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath;的返回值來確定。因此這裡我們定義一個變數來記錄點選了哪個按鈕,根據點選按鈕的不同在這個方法中返回不同的值。

#pragma mark 取得當前操作狀態,根據不同的狀態左側出現不同的操作按鈕
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{
    if (_isInsert) {
        return UITableViewCellEditingStyleInsert;
    }
    return UITableViewCellEditingStyleDelete;
}
#pragma mark 編輯操作(刪除或新增) //實現了此方法向左滑動就會顯示刪除(或新增)圖示 -(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{ KCContactGroup *group =_contacts[indexPath.section]; KCContact *contact=group.contacts[indexPath.row]; if (editingStyle==UITableViewCellEditingStyleDelete) { [group.contacts removeObject:contact]; //考慮到效能這裡不建議使用reloadData //[tableView reloadData]; //使用下面的方法既可以區域性重新整理又有動畫效果 [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom]; //如果當前組中沒有資料則移除組重新整理整個表格 if (group.contacts.count==0) { [_contacts removeObject:group]; [tableView reloadData]; } }else if(editingStyle==UITableViewCellEditingStyleInsert){ KCContact *newContact=[[KCContact alloc]init]; newContact.firstName=@"first"; newContact.lastName=@"last"; newContact.phoneNumber=@"12345678901"; [group.contacts insertObject:newContact atIndex:indexPath.row]; [tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];//注意這裡沒有使用reladData重新整理 } }

執行效果:

UITableViewCellInsert

3.排序

只要實現-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;代理方法當UITableView處於編輯狀態時就可以排序。

#pragma mark 排序
//只要實現這個方法在編輯狀態右側就有排序圖示
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
    KCContactGroup *sourceGroup =_contacts[sourceIndexPath.section];
    KCContact *sourceContact=sourceGroup.contacts[sourceIndexPath.row];
    KCContactGroup *destinationGroup =_contacts[destinationIndexPath.section];
    
    [sourceGroup.contacts removeObject:sourceContact];
    if(sourceGroup.contacts.count==0){
        [_contacts removeObject:sourceGroup];
        [tableView reloadData];
    }
    
    [destinationGroup.contacts insertObject:sourceContact atIndex:destinationIndexPath.row];
    
}

執行效果:

UITableViewCellMove

最後給大家附上上面幾種操作的完整程式碼:

//
//  KCContactViewController.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCContactViewController.h"
#import "KCContact.h"
#import "KCContactGroup.h"
#define kContactToolbarHeight 44

@interface KCContactViewController ()<UITableViewDataSource,UITableViewDelegate,UIAlertViewDelegate>{
    UITableView *_tableView;
    UIToolbar *_toolbar;
    NSMutableArray *_contacts;//聯絡人模型
    NSIndexPath *_selectedIndexPath;//當前選中的組和行
    BOOL _isInsert;//記錄是點選了插入還是刪除按鈕
}

@end

@implementation KCContactViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化資料
    [self initData];
    
    //建立一個分組樣式的UITableView
    _tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
    _tableView.contentInset=UIEdgeInsetsMake(kContactToolbarHeight, 0, 0, 0);
    [self.view addSubview:_tableView];
    
    //新增工具欄
    [self addToolbar];
    
    //設定資料來源,注意必須實現對應的UITableViewDataSource協議
    _tableView.dataSource=self;
    //設定代理
    _tableView.delegate=self;
    
    
}

#pragma mark 載入資料
-(void)initData{
    _contacts=[[NSMutableArray alloc]init];
    
    KCContact *contact1=[KCContact initWithFirstName:@"Cui" andLastName:@"Kenshin" andPhoneNumber:@"18500131234"];
    KCContact *contact2=[KCContact initWithFirstName:@"Cui" andLastName:@"Tom" andPhoneNumber:@"18500131237"];
    KCContactGroup *group1=[KCContactGroup initWithName:@"C" andDetail:@"With names beginning with C" andContacts:[NSMutableArray arrayWithObjects:contact1,contact2, nil]];
    [_contacts addObject:group1];
    
    
    
    KCContact *contact3=[KCContact initWithFirstName:@"Lee" andLastName:@"Terry" andPhoneNumber:@"18500131238"];
    KCContact *contact4=[KCContact initWithFirstName:@"Lee" andLastName:@"Jack" andPhoneNumber:@"18500131239"];
    KCContact *contact5=[KCContact initWithFirstName:@"Lee" andLastName:@"Rose" andPhoneNumber:@"18500131240"];
    KCContactGroup *group2=[KCContactGroup initWithName:@"L" andDetail:@"With names beginning with L" andContacts:[NSMutableArray arrayWithObjects:contact3,contact4,contact5, nil]];
    [_contacts addObject:group2];
    
    
    
    KCContact *contact6=[KCContact initWithFirstName:@"Sun" andLastName:@"Kaoru" andPhoneNumber:@"18500131235"];
    KCContact *contact7=[KCContact initWithFirstName:@"Sun" andLastName:@"Rosa" andPhoneNumber:@"18500131236"];
    
    KCContactGroup *group3=[KCContactGroup initWithName:@"S" andDetail:@"With names beginning with S" andContacts:[NSMutableArray arrayWithObjects:contact6,contact7, nil]];
    [_contacts addObject:group3];
    
    
    KCContact *contact8=[KCContact initWithFirstName:@"Wang" andLastName:@"Stephone" andPhoneNumber:@"18500131241"];
    KCContact *contact9=[KCContact initWithFirstName:@"Wang" andLastName:@"Lucy" andPhoneNumber:@"18500131242"];
    KCContact *contact10=[KCContact initWithFirstName:@"Wang" andLastName:@"Lily" andPhoneNumber:@"18500131243"];
    KCContact *contact11=[KCContact initWithFirstName:@"Wang" andLastName:@"Emily" andPhoneNumber:@"18500131244"];
    KCContact *contact12=[KCContact initWithFirstName:@"Wang" andLastName:@"Andy" andPhoneNumber:@"18500131245"];
    KCContactGroup *group4=[KCContactGroup initWithName:@"W" andDetail:@"With names beginning with W" andContacts:[NSMutableArray arrayWithObjects:contact8,contact9,contact10,contact11,contact12, nil]];
    [_contacts addObject:group4];
    
    
    KCContact *contact13=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joy" andPhoneNumber:@"18500131246"];
    KCContact *contact14=[KCContact initWithFirstName:@"Zhang" andLastName:@"Vivan" andPhoneNumber:@"18500131247"];
    KCContact *contact15=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joyse" andPhoneNumber:@"18500131248"];
    KCContactGroup *group5=[KCContactGroup initWithName:@"Z" andDetail:@"With names beginning with Z" andContacts:[NSMutableArray arrayWithObjects:contact13,contact14,contact15, nil]];
    [_contacts addObject:group5];
    
}

#pragma mark 新增工具欄
-(void)addToolbar{
    CGRect frame=self.view.frame;
    _toolbar=[[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, frame.size.width, kContactToolbarHeight)];
    //    _toolbar.backgroundColor=[UIColor colorWithHue:246/255.0 saturation:246/255.0 brightness:246/255.0 alpha:1];
    [self.view addSubview:_toolbar];
    UIBarButtonItem *removeButton=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(remove)];
    UIBarButtonItem *flexibleButton=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    UIBarButtonItem *addButton=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add)];
    NSArray *buttonArray=[NSArray arrayWithObjects:removeButton,flexibleButton,addButton, nil];
    _toolbar.items=buttonArray;
}
#pragma mark 刪除
-(void)remove{
    //直接通過下面的方法設定編輯狀態沒有動畫
    //_tableView.editing=!_tableView.isEditing;
    _isInsert=false;
    [_tableView setEditing:!_tableView.isEditing animated:true];
}
#pragma mark 新增
-(void)add{
    _isInsert=true;
    [_tableView setEditing:!_tableView.isEditing animated:true];
}

#pragma mark - 資料來源方法
#pragma mark 返回分組數
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return _contacts.count;
}

#pragma mark 返回每組行數
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    KCContactGroup *group1=_contacts[section];
    return group1.contacts.count;
}

#pragma mark返回每行的單元格
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    //NSIndexPath是一個物件,記錄了組和行資訊
    KCContactGroup *group=_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];

    static NSString *cellIdentifier=@"UITableViewCellIdentifierKey1";

    //首先根據標識去快取池取
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    //如果快取池沒有取到則重新建立並放到快取池中
    if(!cell){
        cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    
    cell.textLabel.text=[contact getName];
    cell.detailTextLabel.text=contact.phoneNumber;
    
    return cell;
}

#pragma mark - 代理方法
#pragma mark 設定分組標題
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    KCContactGroup *group=_contacts[section];
    return group.name;
}

#pragma mark 編輯操作(刪除或新增)
//實現了此方法向左滑動就會顯示刪除(或新增)圖示
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
    KCContactGroup *group =_contacts[indexPath.section];
    KCContact *contact=group.contacts[indexPath.row];
    if (editingStyle==UITableViewCellEditingStyleDelete) {
        [group.contacts removeObject:contact];
        //考慮到效能這裡不建議使用reloadData
        //[tableView reloadData];
        //使用下面的方法既可以區域性重新整理又有動畫效果
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];
        
        //如果當前組中沒有資料則移除組重新整理整個表格
        if (group.contacts.count==0) {
            [_contacts removeObject:group];
            [tableView reloadData];
        }
    }else if(editingStyle==UITableViewCellEditingStyleInsert){
        KCContact *newContact=[[KCContact alloc]init];
        newContact.firstName=@"first";
        newContact.lastName=@"last";
        newContact.phoneNumber=@"12345678901";
        [group.contacts insertObject:newContact atIndex:indexPath.row];
        [tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];//注意這裡沒有使用reladData重新整理
    }
}

#pragma mark 排序
//只要實現這個方法在編輯狀態右側就有排序圖示
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
    KCContactGroup *sourceGroup =_contacts[sourceIndexPath.section];
    KCContact *sourceContact=sourceGroup.contacts[sourceIndexPath.row];
    KCContactGroup *destinationGroup =_contacts[destinationIndexPath.section];
    
    [sourceGroup.contacts removeObject:sourceContact];
    [destinationGroup.contacts insertObject:sourceContact atIndex:destinationIndexPath.row];
    if(sourceGroup.contacts.count==0){
        [_contacts removeObject:sourceGroup];
        [tableView reloadData];
    }
}

#pragma mark 取得當前操作狀態,根據不同的狀態左側出現不同的操作按鈕
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{
    if (_isInsert) {
        return UITableViewCellEditingStyleInsert;
    }
    return UITableViewCellEditingStyleDelete;
}

#pragma mark 重寫狀態樣式方法
-(UIStatusBarStyle)preferredStatusBarStyle{
    return UIStatusBarStyleLightContent;
}

@end

通過前面的演示這裡簡單總結一些UITableView的重新整理方法:

- (void)reloadData;重新整理整個表格。

- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);重新整理指定的分組和行。

- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);重新整理指定的分組。

- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;刪除時重新整理指定的行資料。

- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;新增時重新整理指定的行資料。

UITableViewController

很多時候一個UIViewController中只有一個UITableView,因此蘋果官方為了方便大家開發直接提供了一個UITableViewController,這個控制器 UITableViewController實現了UITableView資料來源和代理協議,內部定義了一個tableView屬性供外部訪問,同時自動鋪滿整個螢幕、自動伸縮以方便我們的開發。當然UITableViewController也並不是簡單的幫我們定義完UITableView並且設定了資料來源、代理而已,它還有其他強大的功能,例如重新整理控制元件、滾動過程中固定分組標題等。

有時候一個表格中的資料特別多,檢索起來就顯得麻煩,這個時候可以實現一個搜尋功能幫助使用者查詢資料,其實搜尋的原理很簡單:修改模型、重新整理表格。下面使用UITableViewController簡單演示一下這個功能:

//
//  KCContactTableViewController.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCContactTableViewController.h"
#import "KCContact.h"
#import "KCContactGroup.h"
#define kSearchbarHeight 44

@interface KCContactTableViewController ()<UISearchBarDelegate>{
    UITableView *_tableView;
    UISearchBar *_searchBar;
    //UISearchDisplayController *_searchDisplayController;
    NSMutableArray *_contacts;//聯絡人模型
    NSMutableArray *_searchContacts;//符合條件的搜尋聯絡人
    BOOL _isSearching;
}
@end

@implementation KCContactTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化資料
    [self initData];
    
    //新增搜尋框
    [self addSearchBar];

}

#pragma mark - 資料來源方法

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    if (_isSearching) {
        return 1;
    }
    return _contacts.count;;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (_isSearching) {
        return _searchContacts.count;
    }
    KCContactGroup *group1=_contacts[section];
    return group1.contacts.count;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    KCContact *contact=nil;
    
    if (_isSearching) {
        contact=_searchContacts[indexPath.row];
    }else{
        KCContactGroup *group=_contacts[indexPath.section];
        contact=group.contacts[indexPath.row];
    }
    
    static NSString *cellIdentifier=@"UITableViewCellIdentifierKey1";
    
    //首先根據標識去快取池取
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    //如果快取池沒有取到則重新建立並放到快取池中
    if(!cell){
        cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    
    cell.textLabel.text=[contact getName];
    cell.detailTextLabel.text=contact.phoneNumber;
    
    return cell;
}

#pragma mark - 代理方法
#pragma mark 設定分組標題
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    KCContactGroup *group=_contacts[section];
    return group.name;
}


#pragma mark - 搜尋框代理
#pragma mark  取消搜尋
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar{
    _isSearching=NO;
    _searchBar.text=@"";
    [self.tableView reloadData];
}

#pragma mark 輸入搜尋關鍵字
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
    if([_searchBar.text isEqual:@""]){
        _isSearching=NO;
        [self.tableView reloadData];
        return;
    }
    [self searchDataWithKeyWord:_searchBar.text];
}

#pragma mark 點選虛擬鍵盤上的搜尋時
-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar{
    
    [self searchDataWithKeyWord:_searchBar.text];
    
    [_searchBar resignFirstResponder];//放棄第一響應者物件,關閉虛擬鍵盤
}




#pragma mark 重寫狀態樣式方法
-(UIStatusBarStyle)preferredStatusBarStyle{
    return UIStatusBarStyleLightContent;
}

#pragma mark 載入資料
-(void)initData{
    _contacts=[[NSMutableArray alloc]init];
    
    KCContact *contact1=[KCContact initWithFirstName:@"Cui" andLastName:@"Kenshin" andPhoneNumber:@"18500131234"];
    KCContact *contact2=[KCContact initWithFirstName:@"Cui" andLastName:@"Tom" andPhoneNumber:@"18500131237"];
    KCContactGroup *group1=[KCContactGroup initWithName:@"C" andDetail:@"With names beginning with C" andContacts:[NSMutableArray arrayWithObjects:contact1,contact2, nil]];
    [_contacts addObject:group1];
    
    
    
    KCContact *contact3=[KCContact initWithFirstName:@"Lee" andLastName:@"Terry" andPhoneNumber:@"18500131238"];
    KCContact *contact4=[KCContact initWithFirstName:@"Lee" andLastName:@"Jack" andPhoneNumber:@"18500131239"];
    KCContact *contact5=[KCContact initWithFirstName:@"Lee" andLastName:@"Rose" andPhoneNumber:@"18500131240"];
    KCContactGroup *group2=[KCContactGroup initWithName:@"L" andDetail:@"With names beginning with L" andContacts:[NSMutableArray arrayWithObjects:contact3,contact4,contact5, nil]];
    [_contacts addObject:group2];
    
    
    
    KCContact *contact6=[KCContact initWithFirstName:@"Sun" andLastName:@"Kaoru" andPhoneNumber:@"18500131235"];
    KCContact *contact7=[KCContact initWithFirstName:@"Sun" andLastName:@"Rosa" andPhoneNumber:@"18500131236"];
    
    KCContactGroup *group3=[KCContactGroup initWithName:@"S" andDetail:@"With names beginning with S" andContacts:[NSMutableArray arrayWithObjects:contact6,contact7, nil]];
    [_contacts addObject:group3];
    
    
    KCContact *contact8=[KCContact initWithFirstName:@"Wang" andLastName:@"Stephone" andPhoneNumber:@"18500131241"];
    KCContact *contact9=[KCContact initWithFirstName:@"Wang" andLastName:@"Lucy" andPhoneNumber:@"18500131242"];
    KCContact *contact10=[KCContact initWithFirstName:@"Wang" andLastName:@"Lily" andPhoneNumber:@"18500131243"];
    KCContact *contact11=[KCContact initWithFirstName:@"Wang" andLastName:@"Emily" andPhoneNumber:@"18500131244"];
    KCContact *contact12=[KCContact initWithFirstName:@"Wang" andLastName:@"Andy" andPhoneNumber:@"18500131245"];
    KCContactGroup *group4=[KCContactGroup initWithName:@"W" andDetail:@"With names beginning with W" andContacts:[NSMutableArray arrayWithObjects:contact8,contact9,contact10,contact11,contact12, nil]];
    [_contacts addObject:group4];
    
    
    KCContact *contact13=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joy" andPhoneNumber:@"18500131246"];
    KCContact *contact14=[KCContact initWithFirstName:@"Zhang" andLastName:@"Vivan" andPhoneNumber:@"18500131247"];
    KCContact *contact15=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joyse" andPhoneNumber:@"18500131248"];
    KCContactGroup *group5=[KCContactGroup initWithName:@"Z" andDetail:@"With names beginning with Z" andContacts:[NSMutableArray arrayWithObjects:contact13,contact14,contact15, nil]];
    [_contacts addObject:group5];
    
}

#pragma mark 搜尋形成新資料
-(void)searchDataWithKeyWord:(NSString *)keyWord{
    _isSearching=YES;
    _searchContacts=[NSMutableArray array];
    [_contacts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        KCContactGroup *group=obj;
        [group.contacts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            KCContact *contact=obj;
            if ([contact.firstName.uppercaseString containsString:keyWord.uppercaseString]||[contact.lastName.uppercaseString containsString:keyWord.uppercaseString]||[contact.phoneNumber containsString:keyWord]) {
                [_searchContacts addObject:contact];
            }
        }];
    }];
    
    //重新整理表格
    [self.tableView reloadData];
}

#pragma mark 新增搜尋欄
-(void)addSearchBar{
    CGRect searchBarRect=CGRectMake(0, 0, self.view.frame.size.width, kSearchbarHeight);
    _searchBar=[[UISearchBar alloc]initWithFrame:searchBarRect];
    _searchBar.placeholder=@"Please input key word...";
    //_searchBar.keyboardType=UIKeyboardTypeAlphabet;//鍵盤型別
    //_searchBar.autocorrectionType=UITextAutocorrectionTypeNo;//自動糾錯型別
    //_searchBar.autocapitalizationType=UITextAutocapitalizationTypeNone;//哪一次shitf被自動按下
    _searchBar.showsCancelButton=YES;//顯示取消按鈕
    //新增搜尋框到頁首位置
    _searchBar.delegate=self;
    self.tableView.tableHeaderView=_searchBar;
}

@end

執行效果:

UITableViewSearch

在上面的搜尋中除了使用一個_contacts變數去儲存聯絡人資料還專門定義了一個_searchContact變數用於儲存搜尋的結果。在輸入搜尋關鍵字時我們重新整理了表格,此時會呼叫表格的資料來源方法,在這個方法中我們根據定義的搜尋狀態去決定顯示原始資料還是搜尋結果。

我們發現每次搜尋完後都需要手動重新整理表格來顯示搜尋結果,而且當沒有搜尋關鍵字的時候還需要將當前的tableView重新設定為初始狀態。也就是這個過程中我們要用一個tableView顯示兩種狀態的不同資料,自然會提高程式邏輯複雜度。為了簡化這個過程,我們可以使用UISearchDisplayController,UISearchDisplayController內部也有一個UITableView型別的物件searchResultsTableView,如果我們設定它的資料來源代理為當前控制器,那麼它完全可以像UITableView一樣載入資料。同時它本身也有搜尋監聽的方法,我們不必在監聽UISearchBar輸入內容,直接使用它的方法即可自動重新整理其內部表格。為了和前面的方法對比在下面的程式碼中沒有直接刪除原來的方式而是註釋了對應程式碼大家可以對照學習:

//
//  KCContactTableViewController.m
//  UITableView
//
//  Created by Kenshin Cui on 14-3-1.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCContactTableViewControllerWithUISearchDisplayController.h"
#import "KCContact.h"
#import "KCContactGroup.h"
#define kSearchbarHeight 44

@interface KCContactTableViewControllerWithUISearchDisplayController ()<UISearchBarDelegate,UISearchDisplayDelegate>{
    UITableView *_tableView;
    UISearchBar *_searchBar;
    UISearchDisplayController *_searchDisplayController;
    NSMutableArray *_contacts;//聯絡人模型
    NSMutableArray *_searchContacts;//符合條件的搜尋聯絡人
    //BOOL _isSearching;
}
@end

@implementation KCContactTableViewControllerWithUISearchDisplayController

- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化資料
    [self initData];
    
    //新增搜尋框
    [self addSearchBar];

}

#pragma mark - 資料來源方法

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
//    if (_isSearching) {
//        return 1;
//    }
    //如果當前是UISearchDisplayController內部的tableView則不分組
    if (tableView==self.searchDisplayController.searchResultsTableView) {
        return 1;
    }
    return _contacts.count;;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//    if (_isSearching) {
//        return _searchContacts.count;
//    }
    //如果當前是UISearchDisplayController內部的tableView則使用搜尋資料
    if (tableView==self.searchDisplayController.searchResultsTableView) {
        return _searchContacts.count;
    }
    KCContactGroup *group1=_contacts[section];
    return group1.contacts.count;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    KCContact *contact=nil;
    
//    if (_isSearching) {
//        contact=_searchContacts[indexPath.row];
//    }else{
//        KCContactGroup *group=_contacts[indexPath.section];
//        contact=group.contacts[indexPath.row];
//    }
    //如果當前是UISearchDisplayController內部的tableView則使用搜尋資料
    if (tableView==self.searchDisplayController.searchResultsTableView) {
        contact=_searchContacts[indexPath.row];
    }else{
        KCContactGroup *group=_contacts[indexPath.section];
        contact=group.contacts[indexPath.row];
    }
    
    static NSString *cellIdentifier=@"UITableViewCellIdentifierKey1";
    
    //首先根據標識去快取池取
    UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    //如果快取池沒有取到則重新建立並放到快取池中
    if(!cell){
        cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    
    cell.textLabel.text=[contact getName];
    cell.detailTextLabel.text=contact.phoneNumber;
    
    return cell;
}

#pragma mark - 代理方法
#pragma mark 設定分組標題
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    if (tableView==self.searchDisplayController.searchResultsTableView) {
        return @"搜尋結果";
    }
    KCContactGroup *group=_contacts[section];
    return group.name;
}
#pragma mark 選中之前
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    [_searchBar resignFirstResponder];//退出鍵盤
    return indexPath;
}


#pragma mark - 搜尋框代理
//#pragma mark  取消搜尋
//-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar{
//    //_isSearching=NO;
//    _searchBar.text=@"";
//    //[self.tableView reloadData];
//    [_searchBar resignFirstResponder];
//}
//
//#pragma mark 輸入搜尋關鍵字
//-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
//    if([_searchBar.text isEqual:@""]){
//        //_isSearching=NO;
//        //[self.tableView reloadData];
//        return;
//    }
//    [self searchDataWithKeyWord:_searchBar.text];
//}

//#pragma mark 點選虛擬鍵盤上的搜尋時
//-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar{
//    
//    [self searchDataWithKeyWord:_searchBar.text];
//    
//    [_searchBar resignFirstResponder];//放棄第一響應者物件,關閉虛擬鍵盤
//}

#pragma mark - UISearchDisplayController代理方法
-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString{
    [self searchDataWithKeyWord:searchString];
    return YES;
}


#pragma mark 重寫狀態樣式方法
-(UIStatusBarStyle)preferredStatusBarStyle{
    return UIStatusBarStyleLightContent;
}

#pragma mark 載入資料
-(void)initData{
    _contacts=[[NSMutableArray alloc]init];
    
    KCContact *contact1=[KCContact initWithFirstName:@"Cui" andLastName:@"Kenshin" andPhoneNumber:@"18500131234"];
    KCContact *contact2=[KCContact initWithFirstName:@"Cui" andLastName:@"Tom" andPhoneNumber:@"18500131237"];
    KCContactGroup *group1=[KCContactGroup initWithName:@"C" andDetail:@"With names beginning with C" andContacts:[NSMutableArray arrayWithObjects:contact1,contact2, nil]];
    [_contacts addObject:group1];
    
    
    
    KCContact *contact3=[KCContact initWithFirstName:@"Lee" andLastName:@"Terry" andPhoneNumber:@"18500131238"];
    KCContact *contact4=[KCContact initWithFirstName:@"Lee" andLastName:@"Jack" andPhoneNumber:@"18500131239"];
    KCContact *contact5=[KCContact initWithFirstName:@"Lee" andLastName:@"Rose" andPhoneNumber:@"18500131240"];
    KCContactGroup *group2=[KCContactGroup initWithName:@"L" andDetail:@"With names beginning with L" andContacts:[NSMutableArray arrayWithObjects:contact3,contact4,contact5, nil]];
    [_contacts addObject:group2];
    
    
    
    KCContact *contact6=[KCContact initWithFirstName:@"Sun" andLastName:@"Kaoru" andPhoneNumber:@"18500131235"];
    KCContact *contact7=[KCContact initWithFirstName:@"Sun" andLastName:@"Rosa" andPhoneNumber:@"18500131236"];
    
    KCContactGroup *group3=[KCContactGroup initWithName:@"S" andDetail:@"With names beginning with S" andContacts:[NSMutableArray arrayWithObjects:contact6,contact7, nil]];
    [_contacts addObject:group3];
    
    
    KCContact *contact8=[KCContact initWithFirstName:@"Wang" andLastName:@"Stephone" andPhoneNumber:@"18500131241"];
    KCContact *contact9=[KCContact initWithFirstName:@"Wang" andLastName:@"Lucy" andPhoneNumber:@"18500131242"];
    KCContact *contact10=[KCContact initWithFirstName:@"Wang" andLastName:@"Lily" andPhoneNumber:@"18500131243"];
    KCContact *contact11=[KCContact initWithFirstName:@"Wang" andLastName:@"Emily" andPhoneNumber:@"18500131244"];
    KCContact *contact12=[KCContact initWithFirstName:@"Wang" andLastName:@"Andy" andPhoneNumber:@"18500131245"];
    KCContactGroup *group4=[KCContactGroup initWithName:@"W" andDetail:@"With names beginning with W" andContacts:[NSMutableArray arrayWithObjects:contact8,contact9,contact10,contact11,contact12, nil]];
    [_contacts addObject:group4];
    
    
    KCContact *contact13=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joy" andPhoneNumber:@"18500131246"];
    KCContact *contact14=[KCContact initWithFirstName:@"Zhang" andLastName:@"Vivan" andPhoneNumber:@"18500131247"];
    KCContact *contact15=[KCContact initWithFirstName:@"Zhang" andLastName:@"Joyse" andPhoneNumber:@"18500131248"];
    KCContactGroup *group5=[KCContactGroup initWithName:@"Z" andDetail:@"With names beginning with Z" andContacts:[NSMutableArray arrayWithObjects:contact13,contact14,contact15, nil]];
    [_contacts addObject:group5];
    
}

#pragma mark 搜尋形成新資料
-(void)searchDataWithKeyWord:(NSString *)keyWord{
    //_isSearching=YES;
    _searchContacts=[NSMutableArray array];
    [_contacts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        KCContactGroup *group=obj;
        [group.contacts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            KCContact *contact=obj;
            if ([contact.firstName.uppercaseString containsString:keyWord.uppercaseString]||[contact.lastName.uppercaseString containsString:keyWord.uppercaseString]||[contact.phoneNumber containsString:keyWord]) {
                [_searchContacts addObject:contact];
            }
        }];
    }];
    
    //重新整理表格
    //[self.tableView reloadData];
}

#pragma mark 新增搜尋欄
-(void)addSearchBar{
    _searchBar=[[UISearchBar alloc]init];
    [_searchBar sizeToFit];//大小自適應容器
    _searchBar.placeholder=@"Please input key word...";
    _searchBar.autocapitalizationType=UITextAutocapitalizationTypeNone;
    _searchBar.showsCancelButton=YES;//顯示取消按鈕
    //新增搜尋框到頁首位置
    _searchBar.delegate=self;
    self.tableView.tableHeaderView=_searchBar;

    _searchDisplayController=[[UISearchDisplayController alloc]initWithSearchBar:_searchBar contentsController:self];
    _searchDisplayController.delegate=self;
    _searchDisplayController.searchResultsDataSource=self;
    _searchDisplayController.searchResultsDelegate=self;
    [_searchDisplayController setActive:NO animated:YES];

}

@end

執行效果:

UITableViewSearch2

注意如果使用Storyboard或xib方式建立上述程式碼則無需定義UISearchDisplayController成員變數,因為每個UIViewController中已經有一個searchDisplayController物件。

MVC模式

通過UITableView的學習相信大家對於iOS的MVC已經有一個大致的瞭解,這裡簡單的分析一下iOS中MVC模式的設計方式。在iOS中多數資料來源檢視控制元件(View)都有一個dataSource屬性用於和控制器(Controller)互動,而資料來源我們一般會以資料模型(Model)的形式進行定義,View不直接和模型互動,而是通過Controller間接讀取資料。

就拿前面的聯絡人應用舉例,UITableView作為檢視(View)並不能直接訪問模型Contact,它要顯示聯絡人資訊只能通過控制器(Controller)來提供資料來源方法。同樣的控制器本身就擁有檢視控制元件,可以操作檢視,也就是說檢視和控制器之間可以互相訪問。而模型既不能訪問檢視也不能訪問控制器。具體依賴關係如下圖:

MVC

相關文章