学习如何进阶使用UITableView,带给应用更高级的观感(look and feel)
● 学习如何开发自己定制的UITableView类,模仿iMessage应用的观感
● 为一个基于分组的UITableView实现下钻逻辑
在iOS应用中呈现数据时,UITableView可能是最经常使用的用户界面对象。在本章中,将学习到以超越标准实现的方式使用UITableView,并理解UITableView类的工作方式。你会创建一个聊天视图控制器,它支持定制的单元格和灵活的行高,以及下钻功能的实现,能够将多个对象的多个分类进行分组,从而生成一个高级的用户界面。最后,你会为表格视图的实现添加搜索功能。
2.1 理解UITableView
UITableView直接继承于UIScrollView类,从而给它带来直向(译者注:横向和纵向)滚动的能力。当想要使用UITableView时,必须首先创建UITableView类的实例,将它指向UIView控件而使其可见,并且建立一个datasource对象和一个负责与UITableView进行交互的delegate对象。
2.1.1 datasource和delegate
每一个UITableView都需要datasource和delegate这两个对象。datasource对象为UITableView提供数据。通常,datasource对象使用NSArray类或者NSDictionary类在内部存储数据,并且根据需要将数据提供给表视图。delegate对象必须实现UITableViewDelegate和UITableViewDataSource这两个协议。
UITableViewDelegate协议定义了几个方法,delegate对象需要实现其中至少三个方法。
delegate对象必须实现的方法有:
● tableview:numberOfRowsInSection:
● numberOfSectionsInTableView:
● tableview:cellForRowAtIndexPath:
启动Xcode开发环境,使用Single View ApplicationProject模板创建新项目,并使用如图2-1中所示的配置将其命名为PlainTable。
图1
使用Interface Builder工具打开YDViewController.xib文件,并将一个UITableView控件添加到该窗口中。使用Assistant Editor工具为这个UITableView控件创建一个属性。也需要设置Referencing Outlets一栏中的datasource和delegate指向UITableView对象。确保YDViewController.xib文件看起来如图2-2中所示。
图2
打开YDViewController.h文件,创建名为rowData的NSMutableArray对象充当datasource,如代码清单2-1中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-1 Chapter2/PlainTable/YDViewController.h
- #import <UIKit/UIKit.h>
- @interface YDViewController : UIViewController
- @property (weak, nonatomic) IBOutlet UITableView *mTableView;
- @property(nonatomic,strong) NSMutableArray* rowData;
- @end
- </span>
打开YDViewController.m文件,实现如代码清单2-2中所示的代码,关于这段代码,会在代码清单后详细说明。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-2 Chapter2/PlainTable/YDViewController.m
- #import "YDViewController.h"
- @interface YDViewController ()
- @end
- @implementation YDViewController
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- // Do any additional setup after loading the view, typically from a nib.
- [self loadData];
- }
- -(void)loadData
- {
- if (self.rowData!=nil)
- {
- [self.rowData removeAllObjects];
- self.rowData=nil;
- }
- self.rowData = [[NSMutableArray alloc] init];
- for (int i=0 ; i<100;i++)
- {
- [self.rowData addObject:[NSString stringWithFormat:@"Row: %i",i]];
- }
- //now my datasource if populated let's reload the tableview
- [self.mTableView reloadData];
- }
- #pragma mark UITableView delegate
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
- return 1;
- }
- - (NSInteger)tableView:(UITableView *)tableView
- numberOfRowsInSection:(NSInteger)section {
- return [self.rowData count];
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- static NSString *CellIdentifier = @"Cell";
- UITableViewCell *cell = (UITableViewCell *)[tableView
- dequeueReusableCellWithIdentifier:CellIdentifier];
- if (cell == nil) {
- cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
- reuseIdentifier:CellIdentifier];
- }
- cell.selectionStyle = UITableViewCellSelectionStyleNone;
- cell.textLabel.text = [self.rowData objectAtIndex:indexPath.row];
- return cell;
- }
- -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
- (NSIndexPath *)indexPath
- {
- [tableView deselectRowAtIndexPath:indexPath animated:YES];
- }
- - (void)didReceiveMemoryWarning
- {
- [super didReceiveMemoryWarning];
- // Dispose of any resources that can be recreated.
- }
- @end
- </span>
下面对这段代码进行分解,向你解释代码中各方法的作用。
在viewDidLoad方法中,调用了本地方法loadData,该方法创建了一个带有100个记录的NSMutableArray对象,并将reloadData消息发送给self.mTableView对象。
reloadData方法迫使mTableView对象通过调用delegate方法重新加载数据,并更新用户界面。
在#pragma mark UITableView delegate标记语句之后,需要实现表视图运行所必须的delegate对象的最小方法集合。
调用numberOfSectionsInTableView: 这个delegate方法来决定UITableView控件的section的数量。如果使用UITableViewStylePlain风格,UITableView控件的section数通常是1。后面将会学习到带有下钻功能的例子,如果使用例子中那种风格的section,则需要返回实际的section的数量。
当渲染单元格时,会调用tableview:cellForRowAtIndexPath:这个delegate方法。这个方法恰好是布局UITableViewCell的地方(UITableView中的一行)。现在,先简单地创建一个UITableviewCell,如果单元格仍然可用的话,试着在内存中重用它。
为了显示rowData数组中的正确的行,需要将[rowData objectAtIndex :indexPath.row];方法的返回值赋给cell.textLabel.text属性。
当用户以单击某行的方式选择该行时,会调用tableview:didSelectRowAtIndexPath:这个delegate方法。deselectRowAtIndexPath:animated:的delegate方法会取消这一行的选择,因此单元格不会保持高亮的状态。
如果想要保持选择状态仍然可见,那么请省略这行代码。
当应用运行时,结果如图2-3中所示。
2.1.2 滚动
由于UITableView对象继承于UIScrollView类,因此它本身拥有完全的滚动功能。然而,在某些情况下,例如在UITableView中添加一个新行,或者删除一行时,可能要直接滚动到UITableView中的某个位置。
可以通过调用UITableView的scrollToRowAtIndexPath:atScrollPosition:animated:方法,获得UITableView上基于代码的滚动效果。这个方法传入的第一个参数是NSIndexPath类型的对象。NSIndexPath对象表示到嵌套数组集合树上的某一特定节点的路径。这个路径称为索引路径。在iOS应用中,用NSIndexPath对象来确定到表格视图内的行和section的路径。调用NSIndexPath类的indexPathForRow:inSection:方法,传入行和section的索引数字,通过这种方式可以创建NSIndexPath的实例。
启动Xcode开发环境,使用SingleView Application Project模板创建一个新项目,并使用如图2-4中所示的配置,将其命名为ScrollingTable。
使用Interface Builder工具打开YDViewController.xib文件,创建一个用户界面,如图2-5中所示。
如代码清单2-3中所示,建立YDViewController.h文件。作为前一个例子的补充,为引入的两个UIButton添加两个动作。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-3 Chapter2/ScrollingTable/YDViewController.h
- #import <UIKit/UIKit.h>
- @interface YDViewController : UIViewController
- @property (weak, nonatomic) IBOutlet UITableView *mTableView;
- @property(nonatomic,strong) NSMutableArray* rowData;
- - (IBAction)scrollToTop:(UIButton *)sender;
- - (IBAction)scrollToBottom:(UIButton *)sender;
- @end
- </span>
YDViewController.m文件的实现与前面的代码清单2-2中的类似,唯一的区别在于此时scrollToTop:和scrollToBottom:这两个方法的实现如代码清单2-4中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-4 Chapter2/ScrollingTable/YDViewController.m
- #import "YDViewController.h"
- @interface YDViewController ()
- @end
- @implementation YDViewController
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- // Do any additional setup after loading the view, typically from a nib.
- [self loadData];
- }
- -(void)loadData
- {
- if (self.rowData!=nil)
- {
- [self.rowData removeAllObjects];
- self.rowData=nil;
- }
- self.rowData = [[NSMutableArray alloc] init];
- for (int i=0 ; i<100;i++)
- {
- [self.rowData addObject:[NSString stringWithFormat:@"Row: %i",i]];
- }
- //now my datasource if populated let's reload the tableview
- [self.mTableView reloadData];
- }
- #pragma mark UITableView delegates
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
- return 1;
- }
- - (NSInteger)tableView:(UITableView *)tableView
- numberOfRowsInSection:(NSInteger)section {
- return [self.rowData count];
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- static NSString *CellIdentifier = @"Cell";
- UITableViewCell *cell = (UITableViewCell *)[tableView
- dequeueReusableCellWithIdentifier:CellIdentifier];
- if (cell == nil) {
- cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
- reuseIdentifier:CellIdentifier];
- }
- cell.selectionStyle = UITableViewCellSelectionStyleNone;
- cell.textLabel.text = [self.rowData objectAtIndex:indexPath.row];
- return cell;
- }
- -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
- (NSIndexPath *)indexPath
- {
- [tableView deselectRowAtIndexPath:indexPath animated:YES];
- }
- - (void)didReceiveMemoryWarning
- {
- [super didReceiveMemoryWarning];
- // Dispose of any resources that can be recreated.
- }
- - (IBAction)scrollToTop:(UIButton *)sender
- {
- NSIndexPath *topRow = [NSIndexPath indexPathForRow:0 inSection:0];
- [self.mTableView scrollToRowAtIndexPath:topRow
- atScrollPosition:UITableViewScrollPositionTop animated:YES];
- }
- - (IBAction)scrollToBottom:(UIButton *)sender
- {
- NSIndexPath *bottomRow = [NSIndexPath indexPathForRow:
- [self.rowData count]-1 inSection:0];
- [self.mTableView scrollToRowAtIndexPath:bottomRow
- atScrollPosition:UITableViewScrollPositionBottom animated:YES];
- }
- @end
- </span>
在scrollToTop:方法中,创建一个NSIndexPath对象的实例,把indexPathForRow的值置为0,可以将表视图滚动至顶部。在scrollToBottom:方法中,使用[self.rowData count]-1的值创建NSIndexPath实例,可以将表视图滚动至底部。
用所创建的NSIndexPath对象调用scrollToRowAtIndexPath:atScrollPosition:animated:方法时,mTableView控件既可以滚动到表格的顶部,也可以滚动到表格的底部。
这一实现的结果如图2-6和图2-7中所示。
图6 图7
2.2 构建聊天视图控制器
在本节中,将开发一个聊天视图控制器模拟iMessage以及其他即时通信应用的行为。为此,将学习如何使用灵活的单元格高度和定制的单元格创建一个定制的UITableView的实例。
最终的应用看起来如图2-8所示。
启动Xcode开发环境,用Single View ApplicationProject模板创建一个新项目,使用如图2-9所示的配置,将其命名为YDChatApp。
本例中所使用的图片,可以从本章的下载文件中获得。
YDViewController类会呈现即将开发的定制的UITableView,而且不使用Interface Builder工具开发。所有的UI代码是YDViewController.m文件的一个组成部分。
2.2.1 构建datasource
你将不会使用标准的UITableView,但是为了支持各种不同的聊天泡泡和section,会创建带有特定行为的定制的UITableView对象,并且使用定制的单元格。基于这个原因,开始时会编码实现一个定制的datasource对象,并被挂接到定制的UITableView上。创建一个继承于NSObject类的新协议,将其命名为YDChatTableViewDataSource。协议的源代码如代码清单2-5中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-5 Chapter2/YDChatApp/YDChatTableViewDataSource.h
- #import <Foundation/Foundation.h>
- @class YDChatData;
- @class YDChatTableView;
- @protocol YDChatTableViewDataSource <NSObject>
- - (NSInteger)rowsForChatTable:(YDChatTableView *)tableView;
- - (YDChatData *)chatTableView:(YDChatTableView *)tableView
- dataForRow:(NSInteger)row;
- @end
- </span>
这个协议直接继承于NSObject类,其中定义了必须在YDViewController类里实现的两个方法,定义定制的UITableView的地方就是这里。
2.2.2 构建聊天数据对象
为了使生活更轻松,定义一个名为YDChatData的对象,用来保存一条聊天消息的相关信息。可以用聊天的用户、时间戳、文字或者图片来初始化这个对象。枚举类型YDChatType有两种可能的值,ChatTypeMine和ChatTypeSomeone,用来负责聊天消息在UITableView上的位置。创建一个继承于NSObject的新的Objective-C类,将其命名为YDChatData。
YDChatData.h文件的源代码如代码清单2-6中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-6 Chapter2/YDChatApp/YDChatData.h
- #import <Foundation/Foundation.h>
- @class YDChatUser;
- //enumerator to identify the chattype
- typedef enum _YDChatType
- {
- ChatTypeMine = 0,
- ChatTypeSomeone = 1
- } YDChatType;
- @interface YDChatData : NSObject
- @property (readonly, nonatomic) YDChatType type;
- @property (readonly, nonatomic, strong) NSDate *date;
- @property (readonly, nonatomic, strong) UIView *view;
- @property (readonly, nonatomic) UIEdgeInsets insets;
- @property (nonatomic,strong) YDChatUser *chatUser;
- //custom initializers
- + (id)dataWithText:(NSString *)text date:(NSDate *)date
- type:(YDChatType)type andUser:(YDChatUser *)_user;
- + (id)dataWithImage:(UIImage *)image date:(NSDate *)date
- type:(YDChatType)type andUser:(YDChatUser *)_user;
- + (id)dataWithView:(UIView *)view date:(NSDate *)date
- type:(YDChatType)type andUser:(YDChatUser *)_user
- insets:(UIEdgeInsets)insets;
- @end
- </span>
在类的实现中,实现几个不同的初始化方法,如代码清单2-7中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-7 Chapter2/YDChatApp/YDChatData.m
- import "YDChatData.h"
- #import <QuartzCore/QuartzCore.h>
- @implementation YDChatData
- //create some constant UIEdgeInsets to property align text and images
- const UIEdgeInsets textInsetsMine = {5, 10, 11, 17};
- const UIEdgeInsets textInsetsSomeone = {5, 15, 11, 10};
- const UIEdgeInsets imageInsetsMine = {11, 13, 16, 22};
- const UIEdgeInsets imageInsetsSomeone = {11, 18, 16, 14};
- #pragma initializers
- + (id)dataWithText:(NSString *)text date:(NSDate *)date type:(YDChatType)type
- andUser:(YDChatUser *)_user
- {
- return [[YDChatData alloc] initWithText:text date:date
- type:type andUser:_user];
- }
- •(id)initWithText:(NSString *)text date:(NSDate *)date type:(YDChatType)type andUser:(YDChatUser *)_user
- {
- UIFont* font = [UIFont boldSystemFontOfSize:12];
- int width = 225, height = 10000.0;
- NSMutableDictionary *atts = [[NSMutableDictionary alloc] init];
- [atts setObject:font forKey:NSFontAttributeName];
- CGRect size = [text boundingRectWithSize:CGSizeMake(width, height)
- options:NSStringDrawingUsesLineFragmentOrigin
- attributes:atts
- context:nil];
- UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, size.size.width, size.size.height)];
- label.numberOfLines = 0;
- label.lineBreakMode = NSLineBreakByWordWrapping;
- label.text = (text ? text : @"");
- label.font = font;
- label.backgroundColor = [UIColor clearColor];
- UIEdgeInsets insets = (type == ChatTypeMine ? textInsetsMine : textInsetsSomeone);
- return [self initWithView:label date:date type:type andUser:_user insets:insets];
- }
- •(id)initWithImage:(UIImage *)image date:(NSDate *)date type:(YDChatType)type
- andUser:(YDChatUser *)_user
- {
- CGSize size = image.size;
- if (size.width > 220)
- {
- size.height /= (size.width / 220);
- size.width = 220;
- }
- UIImageView *imageView = [[UIImageView alloc] initWithFrame:
- CGRectMake(0, 0, size.width, size.height)];
- imageView.image = image;
- imageView.layer.cornerRadius = 5.0;
- imageView.layer.masksToBounds = YES;
- UIEdgeInsets insets =
- (type == ChatTypeMine ? imageInsetsMine : imageInsetsSomeone);
- return [self initWithView:imageView date:date type:type andUser:_user
- insets:insets];
- }
- + (id)dataWithView:(UIView *)view date:(NSDate *)date type:(YDChatType)type
- andUser:(YDChatUser *)_user insets:(UIEdgeInsets)insets
- {
- return [[YDChatData alloc] initWithView:view date:date type:type
- andUser:_user insets:insets];
- }
- •(id)initWithView:(UIView *)view date:(NSDate *)date type:(YDChatType)type
- andUser:(YDChatUser *)_user insets:(UIEdgeInsets)insets
- {
- self = [super init];
- if (self)
- {
- _chatUser = _user;
- _view = view;
- _date = date;
- _type = type;
- _insets = insets;
- }
- return self;
- }
- @end
- </span>
2.2.3 构建定制的UITableView控件
创建一个名为YDChatTableView的新的Objective-C类,继承于UITableView类,并且实现了名为ChatBubbleTypingType的枚举类型和需要的属性,如代码清单2-8中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-8 Chapter2/YDChatApp/YDChatTableView.h
- #import <UIKit/UIKit.h>
- #import "YDChatTableViewDataSource.h"
- #import "YDChatTableViewCell.h"
- //enumerator to identify the bubble type
- typedef enum _ChatBubbleTypingType
- {
- ChatBubbleTypingTypeNobody = 0,
- ChatBubbleTypingTypeMe = 1,
- ChatBubbleTypingTypeSomebody = 2
- } ChatBubbleTypingType;
- @interface YDChatTableView : UITableView
- @property (nonatomic, assign) id<YDChatTableViewDataSource> chatDataSource;
- @property (nonatomic) NSTimeInterval snapInterval;
- @property (nonatomic) ChatBubbleTypingType typingBubble;
- @end
- </span>
在YDChatTableView类的实现中,私有接口遵从于UITableViewDelegate和UITable- ViewDataSource这两个协议,在这里还定义一个名为bubbleSection的属性。
初始化方法为UITableView设置了默认属性,例如背景颜色、delegate和datasource属性等。重写reloadData方法,并编写你自己的代码,从而在YDChatTableView中加载数据。
另外,必须重写numberOfSectionsInTableView、tableview:numberOfRowsInSection:、tableview:heightForRowAtIndexPath:和tableview:cellForRowAtIndexPath:这几个方法。tableview:cellForRowAtIndexPath:方法创建并返回一个YDChatHeaderTableViewCell对象,或者是一个YDChatTableViewCell对象。
如果正在显示的单元格是首行,那么tableview:heightForRowAtIndexPath:方法就会返回YDChatHeaderTableViewCell控件的高度,或者根据这一特定的数据行与之相关的YDChatData对象,计算出高度并返回。
完整的实现如代码清单2-9中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-9 Chapter2/YDChatApp/YDChatTableView.m
- #import "YDChatTableView.h"
- #import "YDChatData.h"
- #import "YDChatHeaderTableViewCell.h"
- @interface YDChatTableView ()<UITableViewDelegate, UITableViewDataSource>
- @property (nonatomic, retain) NSMutableArray *bubbleSection;
- @end
- @implementation YDChatTableView
- - (void)initializer
- {
- self.backgroundColor = [UIColor clearColor];
- self.separatorStyle = UITableViewCellSeparatorStyleNone;
- self.delegate = self;
- self.dataSource = self;
- //the snap interval in seconds implements a headerview to seperate chats
- self.snapInterval = 60 * 60 * 24; //one day
- self.typingBubble = ChatBubbleTypingTypeNobody;
- }
- - (id)init
- {
- self = [super init];
- if (self) [self initializer];
- return self;
- }
- - (id)initWithFrame:(CGRect)frame
- {
- self = [super initWithFrame:frame];
- if (self) [self initializer];
- return self;
- }
- - (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
- {
- self = [super initWithFrame:frame style:UITableViewStylePlain];
- if (self) [self initializer];
- return self;
- }
- #pragma mark - Override
- - (void)reloadData
- {
- self.showsVerticalScrollIndicator = NO;
- self.showsHorizontalScrollIndicator = NO;
- self.bubbleSection = nil;
- int count = 0;
- self.bubbleSection = [[NSMutableArray alloc] init];
- if (self.chatDataSource && (count = [self.chatDataSource
- rowsForChatTable:self]) > 0)
- {
- NSMutableArray *bubbleData = [[NSMutableArray alloc]
- initWithCapacity:count];
- for (int i = 0; i < count; i++)
- {
- NSObject *object = [self.chatDataSource
- chatTableView:self dataForRow:i];
- assert([object isKindOfClass:[YDChatData class]]);
- [bubbleData addObject:object];
- }
- [bubbleData sortUsingComparator:^NSComparisonResult(id obj1, id obj2)
- {
- YDChatData *bubbleData1 = (YDChatData *)obj1;
- YDChatData *bubbleData2 = (YDChatData *)obj2;
- return [bubbleData1.date compare:bubbleData2.date];
- }];
- NSDate *last = [NSDate dateWithTimeIntervalSince1970:0];
- NSMutableArray *currentSection = nil;
- for (int i = 0; i < count; i++)
- {
- YDChatData *data = (YDChatData *)[bubbleData objectAtIndex:i];
- if ([data.date timeIntervalSinceDate:last] > self.snapInterval)
- {
- currentSection = [[NSMutableArray alloc] init];
- [self.bubbleSection addObject:currentSection];
- }
- [currentSection addObject:data];
- last = data.date;
- }
- }
- [super reloadData];
- }
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- {
- int result = [self.bubbleSection count];
- if (self.typingBubble != ChatBubbleTypingTypeNobody) result++;
- return result;
- }
- •(NSInteger)tableView:(UITableView *)tableView
- numberOfRowsInSection:(NSInteger)section
- {
- if (section >= [self.bubbleSection count]) return 1;
- return [[self.bubbleSection objectAtIndex:section] count] + 1;
- }
- •(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:
- (NSIndexPath *)indexPath
- {
- // Header
- if (indexPath.row == 0)
- {
- return [YDChatHeaderTableViewCell height];
- }
- YDChatData *data = [[self.bubbleSection objectAtIndex:indexPath.section]
- objectAtIndex:indexPath.row - 1];
- return MAX(data.insets.top + data.view.frame.size.height +
- data.insets.bottom, 52);
- }
- •(UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- // Header based on snapInterval
- if (indexPath.row == 0)
- {
- static NSString *cellId = @"HeaderCell";
- YDChatHeaderTableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:cellId];
- YDChatData *data = [[self.bubbleSection objectAtIndex:indexPath.section]
- objectAtIndex:0];
- if (cell == nil) cell = [[YDChatHeaderTableViewCell alloc] init];
- cell.date = data.date;
- return cell;
- }
- // Standard
- static NSString *cellId = @"ChatCell";
- YDChatTableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:cellId];
- YDChatData *data = [[self.bubbleSection objectAtIndex:indexPath.section]
- objectAtIndex:indexPath.row - 1];
- if (cell == nil) cell = [[YDChatTableViewCell alloc] init];
- cell.data = data;
- return cell;
- }
- @end
- </span>
2.2.4 灵活的单元格高度
由于聊天消息具有图片和文本,这会使每个单元格的宽度和高度发生变化,因此表格可以是不相同的,并且需要根据单元格的内容进行计算。在重写的tableview:heightForRow- AtIndexPath:方法中,存在一个判断条件,如果当前的单元格是标题单元格,那么就返回标题单元格的高度;如果这个单元格是一个普通的聊天单元格,那么就获取与该单元格相连的YDChatData对象,并计算用作显示单元格的相关UIEdgeInset变量的最大值。下面的代码片段在YDChatTableView类中实现,负责灵活地返回单元格的高度。
- <span style="font-family:Microsoft YaHei;font-size:14px;">(float)tableView:(UITableView *)tableView
- heightForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- // Header
- if (indexPath.row == 0)
- {
- return [YDChatHeaderTableViewCell height];
- }
- YDChatData *data = [[self.bubbleSection objectAtIndex:indexPath.section]
- objectAtIndex:indexPath.row - 1];
- return MAX(data.insets.top + data.view.frame.size.height +
- data.insets.bottom, 52);
- }
- </span>
2.2.5 开发定制的单元格
为了正确显示聊天数据,需要两种不同的定制的UITableViewCell对象,它们都继承于UITableViewCell类。
一种用以显示标题头,在这种情况下,就是显示与snapInterval属性相关的日期和时间编组。另外一种用以显示YDChatData对象中保存的聊天消息。前一种表格对象有一个名为height返回值类型为CGFloat的静态方法,返回这个UITableViewCell的高度,还有一个日期类型的属性,因而日期和时间可以从snapInterval属性中获得。创建一个名为YDChatTableViewHeaderCell的新的Objective-C类,打开YDChatTableViewHeaderCell.h文件,应用如代码清单2-10中所示的代码。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-10 Chapter2/YDChatApp/YDChatTableViewHeaderCell.h
- #import <UIKit/UIKit.h>
- @interface YDChatHeaderTableViewCell : UITableViewCell
- + (CGFloat)height;
- @property (nonatomic, strong) NSDate *date;
- @end
- </span>
- <span style="font-family:Microsoft YaHei;font-size:14px;">
- </span>
YDChatTableViewHeaderCell类的实现简单地返回30.0作为height方法的返回值。setDate方法接收一个日期对象,并创建UILabel控件,将其添加到视图上,用以显示section的日期-时间戳。实现如代码清单2-11中所示的代码。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-11 Chapter2/YDChatApp/YDChatTableViewHeaderCell.m
- #import "YDChatHeaderTableViewCell.h"
- @interface YDChatHeaderTableViewCell ()
- @property (nonatomic, retain) UILabel *label;
- @end
- @implementation YDChatHeaderTableViewCell
- + (CGFloat)height
- {
- return 30.0;
- }
- - (void)setDate:(NSDate *)value
- {
- NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
- [dateFormatter setDateStyle:NSDateFormatterMediumStyle];
- [dateFormatter setTimeStyle:NSDateFormatterShortStyle];
- NSString *text = [dateFormatter stringFromDate:value];
- if (self.label)
- {
- self.label.text = text;
- return;
- }
- self.selectionStyle = UITableViewCellSelectionStyleNone;
- self.label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0,
- self.frame.size.width, [YDChatHeaderTableViewCell height])];
- self.label.text = text;
- self.label.font = [UIFont boldSystemFontOfSize:12];
- self.label.textAlignment = NSTextAlignmentCenter;
- self.label.shadowOffset = CGSizeMake(0, 1);
- self.label.shadowColor = [UIColor whiteColor];
- self.label.textColor = [UIColor darkGrayColor];
- self.label.backgroundColor = [UIColor clearColor];
- [self addSubview:self.label];
- }
- @end
- </span>
既然已经为HeaderCell创建了类,那么也需要为ChatCell创建一个定制的类,用来显示真实的聊天消息。创建一个继承于UITableViewCell的新的Objective-C类,将其命名为YDChatTableViewCell。为这个类添加YDChatData类型的唯一的一个属性,用以显示真实的聊天消息,并将单元格作为定制的UITableViewCell对象返回。
在YDChatTableViewCell.h文件中实现如代码清单2-12中所示的代码。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-12 Chapter2/YDChatApp/YDChatTableViewCell.h
- #import <UIKit/UIKit.h>
- #import "YDChatData.h"
- @interface YDChatTableViewCell : UITableViewCell
- @property (nonatomic, strong) YDChatData *data;
- -(void)setData(YDChatData*)data;
- @end</span>
setData:方法接受YDChatData对象,将它赋值给data属性。下一步,它会调用rebuild- UserInterface方法,如果该方法之前没有创建过bubbleImage,那么就会创建这个对象。如果YDChatData对象有代表一个用户的值,那么就会使用该聊天用户的头像,作为子视图添加到界面上。
YDChatTableViewCell.m文件的实现代码如代码清单2-13中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-13 Chapter2/YDChatApp/YDChatTableViewCell.m
- #import <QuartzCore/QuartzCore.h>
- #import "YDChatTableViewCell.h"
- #import "YDChatData.h"
- #import "YDChatUser.h"
- @interface YDChatTableViewCell ()
- //declare properties
- @property (nonatomic, retain) UIView *customView;
- @property (nonatomic, retain) UIImageView *bubbleImage;
- @property (nonatomic, retain) UIImageView *avatarImage;
- - (void) setupInternalData;
- @end
- @implementation YDChatTableViewCell
- @synthesize data=_data;
- - (void)setData:(YDChatData *)data
- {
- _data = data;
- [self rebuildUserInterface];
- }
- - (void) rebuildUserInterface
- {
- self.selectionStyle = UITableViewCellSelectionStyleNone;
- if (!self.bubbleImage)
- {
- self.bubbleImage = [[UIImageView alloc] init];
- [self addSubview:self.bubbleImage];
- }
- YDChatType type = self.data.type;
- CGFloat width = self.data.view.frame.size.width;
- CGFloat height = self.data.view.frame.size.height;
- CGFloat x = (type == ChatTypeSomeone) ? 0 :
- self.frame.size.width -
- width -
- self.data.insets.left -
- self.data.insets.right;
- CGFloat y = 0;
- //if we have a chatUser show the avatar of the YDChatUser property
- if (self.data.chatUser)
- {
- YDChatUser *thisUser = self.data.chatUser;
- [self.avatarImage removeFromSuperview];
- self.avatarImage = [[UIImageView alloc] initWithImage:(thisUser.avatar ?
- thisUser.avatar : [UIImage imageNamed:@"noAvatar.png"])];
- self.avatarImage.layer.cornerRadius = 9.0;
- self.avatarImage.layer.masksToBounds = YES;
- self.avatarImage.layer.borderColor =
- [UIColor colorWithWhite:0.0 alpha:0.2].CGColor;
- self.avatarImage.layer.borderWidth = 1.0;
- //calculate the x position
- CGFloat avatarX = (type == ChatTypeSomeone) ? 2 :
- self.frame.size.width - 52;
- CGFloat avatarY = self.frame.size.height - 50;
- //set the frame correctly
- self.avatarImage.frame = CGRectMake(avatarX, avatarY, 50, 50);
- [self addSubview:self.avatarImage];
- CGFloat delta = self.frame.size.height -
- (self.data.insets.top + self.data.insets.bottom +
- self.data.view.frame.size.height);
- if (delta > 0) y = delta;
- if (type == ChatTypeSomeone) x += 54;
- if (type == ChatTypeMine) x -= 54;
- }
- [self.customView removeFromSuperview];
- self.customView = self.data.view;
- self.customView.frame =
- CGRectMake(x + self.data.insets.left,
- y + self.data.insets.top, width, height);
- [self.contentView addSubview:self.customView];
- //depending on the ChatType a bubble image on the left or right
- if (type == ChatTypeSomeone)
- {
- self.bubbleImage.image = [[UIImage imageNamed:@"yoububble.png"]
- stretchableImageWithLeftCapWidth:21 topCapHeight:14];
- }
- else {
- self.bubbleImage.image = [[UIImage imageNamed:@"mebubble.png"]
- stretchableImageWithLeftCapWidth:15 topCapHeight:14];
- }
- self.bubbleImage.frame =
- CGRectMake(x, y, width + self.data.insets.left +
- self.data.insets.right, height +
- self.data.insets.top + self.data.insets.bottom);
- }
- - (void)setFrame:(CGRect)frame
- {
- [super setFrame:frame];
- [self rebuildUserInterface];
- }
- @end
- </span>
2.2.6 创建聊天用户对象
创建一个新的名为YDChatUser的类,具有两个属性:用户名和头像,它们会被显示在刚刚创建的YDChatTableViewCell中。设计YDChatUser类用来设置用户对象的用户名和头像图片,这样可以关联到YDChatData对象上。
创建一个继承于NSObject的新的Objective-C类,将其命名为YDChatUser。YDChatUser.h文件如代码清单2-14中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-14 Chapter2/YDChatApp/YDChatUser.h
- #import <Foundation/Foundation.h>
- @interface YDChatUser : NSObject
- @property (nonatomic, strong) NSString *username;
- @property (nonatomic, strong) UIImage *avatar;
- - (id)initWithUsername:(NSString *)user avatarImage:(UIImage *)image;
- @end
- </span>
实现定制的构造方法,并且将传入的参数值赋给属性,如代码清单2-15中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-15 Chapter2/YDChatApp/YDChatUser.m
- #import "YDChatUser.h"
- @implementation YDChatUser
- @synthesize avatar = _avatar;
- @synthesize username = _username;
- - (id)initWithUsername:(NSString *)user avatarImage:(UIImage *)image
- {
- self = [super init];
- if (self)
- {
- self.avatar = [image copy];
- self.username = [user copy];
- }
- return self;
- }
- @end</span>
2.2.7 融会贯通
既然已经开发了所有独立的组件,那么就可以编写YDViewController类,使用聊天消息来显示YDChatTableView了。
YDViewController.h文件不需要任何编码工作。
YDViewController.m文件导入所需的头文件,以此为开始并遵从YDChatTableViewDataSource和UITextViewDelegate协议。在viewDidLoad方法的开头,以编程方式创建了用户界面元素。在这个方法的结尾,创建了YDChatUser类型的两个对象,如下面这段代码示例所示:
me =[[YDChatUser alloc] initWithUsername:@"Peter"
avatarImage:[UIImage imageNamed:@"me.png"]];
you =[[YDChatUser alloc] initWithUsername:@"You"
avatarImage:[UIImageimageNamed:@"noavatar.png"]];
最终,在viewDidLoad方法中,一些YDChatData记录被创建并添加到Chats数组中,作为YDChatTableView控件的datasource对象。
YDChatData *first = [YDChatData dataWithText:
@"Hey, how are you doing? I'm inParis take a look at this picture."
date:[NSDatedateWithTimeIntervalSinceNow:-600]
type:ChatTypeMine andUser:me];
YDChatData *second = [YDChatDatadataWithImage:
[UIImage imageNamed:@"eiffeltower.jpg"]
date:[NSDatedateWithTimeIntervalSinceNow:-290]
type:ChatTypeMine andUser:me];
YDChatData *third = [YDChatDatadataWithText:
@"Wow.. Really cool pictureout there. Wish I could be with you"
date:[NSDatedateWithTimeIntervalSinceNow:-5]
type:ChatTypeSomeone andUser:you];
YDChatData *forth = [YDChatDatadataWithText:
@"Maybe next time you can comewith me."
date:[NSDatedateWithTimeIntervalSinceNow:+0]
type:ChatTypeMine andUser:me];
//Initialize the Chats array with thecreated YDChatData objects
Chats = [[NSMutableArray alloc]
initWithObjects:first, second,third,forth, nil];
sendMessage方法创建了YDChatData对象,使用从msgText控件中得到的文本来初始化这个对象,将其加入到Chats数组中,并调用chatTable对象的reloadData方法。
当选中UITextView,开始在这个控件内输入文字时,会触发textView:shouldChange- TextInRange:replacementText:、textViewDidBeginEditing:和textViewDidChange:这三个方法,用来操控用户界面。shortenTableView和showTableView方法用来控制YDChatTableView的高度。
完整的实现方式如代码清单2-16中所示。
- <span style="font-family:Microsoft YaHei;font-size:14px;">代码清单2-16 Chapter2/YDChatApp/YDViewController.m
- #import "YDChatUser.h"
- #import "YDChatTableViewDataSource.h"
- #import "YDViewController.h"
- #import <QuartzCore/QuartzCore.h>
- #import "YDChatTableView.h"
- #import "YDChatTableViewDataSource.h"
- #import "YDChatData.h"
- #import "YDChatUser.h"
- #define lineHeight 16.0f
- @interface YDViewController ()<YDChatTableViewDataSource,UITextViewDelegate>
- {
- YDChatTableView *chatTable;
- UIView *textInputView;
- UITextField *textField;
- NSMutableArray *Chats;
- UIView* sendView;
- UIButton* sendButton;
- UITextView* msgText;
- BOOL composing;
- float prevLines;
- YDChatUser* me ;
- YDChatUser* you ;
- }
- @end
- @implementation YDViewController
- CGRect appFrame;
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- self.view.backgroundColor=[UIColor lightGrayColor];
- //create your instance of YDChatTableView
- self.chatTable=[[YDChatTableView alloc] initWithFrame:
- CGRectMake(0,40,[[UIScreen mainScreen] bounds].size.width,
- [[UIScreen mainScreen] bounds].size.height -40) style:UITableViewStylePlain];
- chatTable.backgroundColor=[UIColor whiteColor];
- [self.view addSubview:chatTable];
- appFrame= [[UIScreen mainScreen] applicationFrame];
- sendView = [[UIView alloc] initWithFrame:
- CGRectMake(0,appFrame.size.height-56,320,56)];
- sendView.backgroundColor=[UIColor blueColor];
- sendView.alpha=0.9;
- msgText = [[UITextView alloc] initWithFrame:CGRectMake(7,10,225,36)];
- msgText.backgroundColor = [UIColor whiteColor];
- msgText.textColor=[UIColor blackColor];
- msgText.font=[UIFont boldSystemFontOfSize:12];
- msgText.autoresizingMask =
- UIViewAutoresizingFlexibleHeight |
- UIViewAutoresizingFlexibleTopMargin;
- msgText.layer.cornerRadius = 10.0f;
- msgText.returnKeyType=UIReturnKeySend;
- msgText.showsHorizontalScrollIndicator=NO;
- msgText.showsVerticalScrollIndicator=NO;
- //Set the delegate so you can respond to user input
- msgText.delegate=self;
- [sendView addSubview:msgText];
- msgText.contentInset = UIEdgeInsetsMake(0,0,0,0);
- [self.view addSubview:sendView];
- sendButton = [[UIButton alloc] initWithFrame:CGRectMake(235,10,77,36)];
- sendButton.backgroundColor=[UIColor lightGrayColor];
- [sendButton addTarget:self action:@selector(sendMessage)
- forControlEvents:UIControlEventTouchUpInside];
- sendButton.autoresizingMask=UIViewAutoresizingFlexibleTopMargin;
- sendButton.layer.cornerRadius=6.0f;
- [sendButton setTitle:@"Send" forState:UIControlStateNormal];
- [sendView addSubview:sendButton];
- //create two YDChatUser object one representing me and one
- representing the other party
- me = [[YDChatUser alloc] initWithUsername:@"Peter"
- avatarImage:[UIImage imageNamed:@"me.png"]];
- you =[[YDChatUser alloc] initWithUsername:@"You"
- avatarImage:[UIImage imageNamed:@"noavatar.png"]];
- //Create some YDChatData objects here
- YDChatData *first = [YDChatData dataWithText:
- @"Hey, how are you doing? I'm in Paris take a look at this picture."
- date:[NSDate dateWithTimeIntervalSinceNow:-600]
- type:ChatTypeMine andUser:me];
- YDChatData *second = [YDChatData dataWithImage:
- [UIImage imageNamed:@"eiffeltower.jpg"]
- date:[NSDate dateWithTimeIntervalSinceNow:-290]
- type:ChatTypeMine andUser:me];
- YDChatData *third = [YDChatData dataWithText:
- @"Wow.. Really cool picture out there. Wish I could be with you"
- date:[NSDate dateWithTimeIntervalSinceNow:-5]
- type:ChatTypeSomeone andUser:you];
- YDChatData *forth = [YDChatData dataWithText:
- @"Maybe next time you can come with me."
- date:[NSDate dateWithTimeIntervalSinceNow:+0]
- type:ChatTypeMine andUser:me];
- //Initialize the Chats array with the created YDChatData objects
- Chats = [[NSMutableArray alloc]
- initWithObjects:first, second, third,forth, nil];
- //set the chatDataSource
- chatTable.chatDataSource = self;
- //call the reloadData, this is actually calling your override method
- [chatTable reloadData];
- }
- -(void)sendMessage
- {
- composing=NO;
- YDChatData *thisChat = [YDChatData dataWithText:msgText.text
- date:[NSDate date] type:ChatTypeMine andUser:me];
- [Chats addObject:thisChat];
- [chatTable reloadData];
- [self showTableView];
- [msgText resignFirstResponder];
- msgText.text=@"";
- sendView.frame=CGRectMake(0,appFrame.size.height-56,320,56);
- NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
- [chatTable scrollToRowAtIndexPath:indexPath
- atScrollPosition:UITableViewScrollPositionBottom
- animated:YES];
- }
- #pragma UITextViewDelegate
- //if user presses enter consider as end of message and send it
- -(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
- replacementText:(NSString *)text
- {
- if([text isEqualToString:@"\n"]) {
- [self sendMessage];
- return NO;
- }
- return YES;
- }
- // this function returns the height of the entered text in the msgText field
- -(CGFloat )textY
- {
- UIFont* systemFont = [UIFont boldSystemFontOfSize:12];
- int width = 225.0, height = 10000.0;
- NSMutableDictionary *atts = [[NSMutableDictionary alloc] init];
- [atts setObject:systemFont forKey:NSFontAttributeName];
- CGRect size = [msgText.text boundingRectWithSize:CGSizeMake(width, height)
- options:NSStringDrawingUsesLineFragmentOrigin
- attributes:atts
- context:nil];
- float textHeight = size.size.height;
- float lines = textHeight / lineHeight;
- if (lines >=4)
- lines=4;
- if ([msgText.text length]==0)
- lines=0.9375f;
- return 190 - (lines * lineHeight) + lineHeight;
- }
- -(void)textViewDidChange:(UITextView *)textView
- {
- UIFont* systemFont = [UIFont boldSystemFontOfSize:12];
- int width = 225.0, height = 10000.0;
- NSMutableDictionary *atts = [[NSMutableDictionary alloc] init];
- [atts setObject:systemFont forKey:NSFontAttributeName];
- CGRect size = [msgText.text boundingRectWithSize:CGSizeMake(width, height)
- options:NSStringDrawingUsesLineFragmentOrigin
- attributes:atts
- context:nil];
- float textHeight = size.size.height;
- float lines = textHeight / lineHeight;
- if (lines >=4)
- lines=4;
- composing=YES;
- msgText.contentInset = UIEdgeInsetsMake(0,0,0,0);
- sendView.frame = CGRectMake(0,appFrame.size.height-270 - (lines * lineHeight) + lineHeight ,320,56 + (lines * lineHeight)-lineHeight);
- if (prevLines!=lines)
- [self shortenTableView];
- prevLines=lines;
- }
- prevLines=lines;
- }
- //let's change the frame of the chatTable so we can see the bottom
- -(void)shortenTableView
- { [UIView beginAnimations:@"moveView" context:nil];
- [UIView setAnimationDuration:0.1];
- chatTable.frame=CGRectMake(0, 0, 320, [self textY] );
- [UIView commitAnimations];
- prevLines=1;
- }
- // show the chatTable as it was
- -(void)showTableView
- {
- [UIView beginAnimations:@"moveView" context:nil];
- [UIView setAnimationDuration:0.1];
- chatTable.frame=CGRectMake(0,0,320,460 - 56);
- [UIView commitAnimations];
- }
- //when user starts typing change the frame position and shorten the chatTable
- -(void)textViewDidBeginEditing:(UITextView *)textView
- { [UIView beginAnimations:@"moveView" context:nil];
- [UIView setAnimationDuration:0.3];
- sendView.frame = CGRectMake(0,appFrame.size.height-270,320,56);
- [UIView commitAnimations];
- [self shortenTableView];
- [msgText becomeFirstResponder];
- }
- - (BOOL)shouldAutorotateToInterfaceOrientation:
- (UIInterfaceOrientation)interfaceOrientation
- {
- return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
- }
- #pragma mark - YDChatTableView implementation
- //here are the required implementation from your YDChatTableViewDataSource
- - (NSInteger)rowsForChatTable:(YDChatTableView *)tableView
- {
- return [Chats count];
- }
- - (YDChatData *)chatTableView:(YDChatTableView *)tableView
- dataForRow:(NSInteger)row
- {
- return [Chats objectAtIndex:row];
- }
- @end
- </span>
《iOS 高级编程》试读电子书,免费提供,有需要的留下邮箱,一有空即发送给大家。 别忘啦顶哦!
购书地址:
京东:http://item.jd.com/11573064.html
当当:http://product.dangdang.com/23596918.html
互动:http://product.china-pub.com/3770647
亚马逊:http://www.amazon.cn/dp/B00P7NO4K2