简介:
从服务器代码一直到前端用户界面,本文将详细介绍构建 iPhone 聊天应用程序的整个过程
架构一个 iPhone 聊天应用程序
常用缩写词
DOM:文档对象模型
IDE:集成开发环境
SAX:XML 简单 API
SQL:结构化查询语言
UI:用户界面
W3C:万维网联盟
XIB:Xml 界面生成器
XML:可扩展标记语言
目前已有 4000 万台 iPhones 在用,您无疑对编写 iOS 应用程序感兴趣。但是从何着手呢?大多数应用程序都会连接网络,那么一个跨越两端的项目(比如说聊天应用程序)又是如何呢?本文将向您介绍如何利用服务器和客户端组件构建一个聊天应用程序。从本文可以学到编写
iOS 应用程序的整个流程。学完本文之后,我保证您会想要编写一个这样的应用程序。
构建应用程序从架构解决方案开始。图 1 中的架构展示了 iOS 设备(这里是 iPhone)如何通过两个
PHP 页面连接到服务器。
图 1. Chat App 客户端/服务器架构
这两个 PHP 页面(add.php 和 messages.php)都连接到数据库,分别用于发布和检索消息。在我提供的代码中,数据库是
MySQL,但是您可以使用 DB2 或者您喜欢的任何其他数据库。
我使用的协议是 XML。add.php 页面返回一个 XML 消息,指出消息发布是否成功。messages.php
页面返回发布到服务器的最新消息。
在您开始之前,我想要介绍一下您将从本文学到的内容。
- 数据库访问。我将向您介绍如何使用 PHP 向数据库添加行和检索行。
- XML 编码。服务器代码演示如何将消息打包成 XML。
- 构建 iOS 界面。我将详细介绍如何为应用程序构建用户界面。
- 查询服务器。Objective-C 代码向 messages.php 页面发出 GET 请求,以得到最新的聊天消息。
- 解析 XML。使用对 iOS 开发人员可用的 XML 解析器,您可以解析从 messages.php
返回的 XML。
- 显示消息。应用程序使用一个定制列表项显示聊天消息;这一方法可以让您了解到如何定制自己的 iOS
应用程序的外观。
- 发布消息。应用程序通过 add.php 将数据发布到服务器,add.php 将指导您完成发布过程。
- 定时器。定时器任务用于周期性地轮询 messages.php,看何时来了新的聊天项目。
对于一个例子来说,这些内容太多了,应该为您开发您想要构建的任何类型的客户端/服务器
iOS 应用程序提供一组适当的工具。
构建服务器脚本
从创建数据库开始。我将我的数据库叫做 "chat",您可以给您的数据库随便取个您喜欢的名字。您只需要确保在
PHP 中更改连接字符串,以匹配数据库的名称。用来为应用程序构建单个表的 SQL 脚本在清单 1 中。
清单 1. chat.sql
DROP TABLE IF EXISTS chatitems; CREATE TABLE chatitems ( id BIGINT NOT NULL PRIMARY KEY auto_increment, added TIMESTAMP NOT NULL, user VARCHAR(64) NOT NULL, message VARCHAR(255) NOT NULL ); |
这个简单的单表数据库只有 4 个字段:
- 行的 id,这是一个自动递增的整数
- 添加消息的日期
- 添加消息的用户
- 消息本身的文本
您可以更改这些字段的大小,以适应您的内容。
在生产系统中,您很可能还想要有一个带有姓名和密码字段的用户表,还有一个用户登录界面。对于本例来说,我想要让数据库尽量简单,所以数据库中只有一个表。
您想要构建的第一部分代码是清单 2 中的 add.php 脚本。
清单 2. add.php
<?php header( 'Content-type: text/xml' ); mysql_connect( 'localhost:/tmp/mysql.sock', 'root', '' ); mysql_select_db( 'chat' ); mysql_query( "INSERT INTO chatitems VALUES ( null, null, '". mysql_real_escape_string( $_REQUEST['user'] ). "', '". mysql_real_escape_string( $_REQUEST['message'] ). "')" ); ?> <success />
|
该脚本连接到数据库,并使用已发布的 user 和 message 字段存储消息。就是在简单的 INSERT
语句中,两个值被转义,以解决任何含义不确定的字符,比如说可能会扰乱 SQL 语法的单引号。
为了测试 add 脚本,您创建一个 test.html 页面,如清单 3 所示,它只是将字段张贴到 add.php
脚本。
清单 3. test.html
<html> <head> <title>Chat Message Test Form</title> </head> <body> <form action="add.php" method="POST"> User: <input name="user" /><br /> Message: <input name="message" /><br /> <input type="submit" /> </form> </body> </html>
|
这个简单的页面只有一个表单(指向 add.php)和两个文本字段(分别用于用户和消息)。然后还有一个
Submit 按钮,用于执行张贴。
test.html 页面安装好之后,您就可以测试 add.php 脚本了。在浏览器中打开测试页面,结果类似于
图 2,User 字段中显示有值 "jack",Message 字段中有值 "This
is a test",下面是一个 Submit Query 按钮。
图 2. 消息发布测试页面
从这里,您添加一些值并单击 Submit Query 按钮。如果一切正常,您会看到类似于图 3 的画面。
图 3. 成功的消息发布
否则,您可能会得到一个 PHP 堆栈跟踪,告诉您数据库连接失败或者 INSERT 语句不工作。
消息添加脚本能够工作,下面应该构建 messages.php 脚本了,它返回消息列表。该脚本展示在清单
4 中。
清单 4. messages.php
<?php header( 'Content-type: text/xml' ); mysql_connect( 'localhost:/tmp/mysql.sock', 'root', '' ); mysql_select_db( 'chat' ); if ( $_REQUEST['past'] ) { $result = mysql_query('SELECT * FROM chatitems WHERE id > '. mysql_real_escape_string( $_REQUEST['past'] ). ' ORDER BY added LIMIT 50'); } else { $result = mysql_query('SELECT * FROM chatitems ORDER BY added LIMIT 50' ); } ?> <chat> <?php while ($row = mysql_fetch_assoc($result)) { ?> <message added="<?php echo( $row['added'] ) ?>" id="<?php echo( $row['id'] ) ?>"> <user><?php echo( htmlentities( $row['user'] ) ) ?></user> <text><?php echo( htmlentities( $row['message'] ) ) ?></text> </message> <?php } mysql_free_result($result); ?> </chat>
|
这个脚本稍微有点复杂。它做的第一件事是完成查询。这里有两种可能:
- 如果提供了 past 参数,那么脚本只返回超过指定 ID 的消息。
- 如果没有指定 past 参数,那么返回所有消息。
使用 past 参数的原因是,您想要客户端是智能的。您想要客户端记住它已经看到过的消息,只寻找那些超过它已经具有的消息。客户端逻辑足够简单,它只保留它找到的最高值
ID,并作为 past 参数发送它。在开始时,它可以发送 0 作为值,相当于根本就不指定任何内容。
脚本的第二部分从查询结果集中检索记录,并将它们编码成 XML。如果这一部分脚本能够工作,那么您在浏览器中打开这一页面时,会看到类似图
4 的效果。
图 4. 聊天消息列表
服务器脚本就算完成了。当然,您可以添加您想要的任何逻辑,额外的通道、用户验证和登录,等等。对于这个实验性的聊天应用程序,这个脚本已经工作得很好了。现在您可以构建将会使用这个服务器脚本的
iOS 应用程序了。
构建客户端代码
iOS IDE 叫做 XCode。如果您还没有这个 IDE,那么需要从 Apple Developer
Site(参见 参考资料)下载它。最新生产版本是 XCode 3,我这里的屏幕截图使用的就是这个版本。现在已经有了一个更新的版本,叫做
XCode 4,它在 IDE 中集成了 User Interface 编辑器,但是该版本目前还处于预览模式。
XCode 安装好之后,现在就该使用图 5 所示的 New Project 向导构建应用程序了。
图 5. 构建一个基于视图的 iPhone 应用程序
开始最容易的应用程序类型是基于视图的应用程序。这种应用程序允许您在您选择的地方放置控件,并为您完成大多数
UI 设计。选择控件之后,再选择 iPhone 或 iPad。这一选择关系到您将在什么样的设备上进行模拟。您可以编写代码,以便在
iPhone 或 iPad 或者 Apple 即将推出的任何其他 i-设备上运行。
单击 Choose 之后,您会被要求给应用程序命名。我将我的应用程序取名为 “iOSChatClient”,但是您可以随便给自己的应用程序取一个您喜欢的名字。您给应用程序命名之后,XCode
IDE 会构建核心应用程序文件。然后,编译并启动它,确保一切正常。
创建用户界面
创建应用程序之后,您就可以开发界面了。从视图控制器 XIB 文件开始,该文件位于 Resources
文件夹中。通过双击该文件夹,可以打开 Interface Builder,这是 UI 工具箱。
图 6. 界面布局
图 6 展示了我如何布局三个控件。顶部是文本框,用于输入您想要发送的消息。文本框的右边是 Send
按钮。下面是 UITableView 对象,其中展示了所有聊天记录。
我会详细介绍如何在 Interface Builder 中完成这一切,但是我建议您下载项目代码,自己试验一下。尽管放心将该项目用作您自己的应用程序的模板。
创建视图控制器
用户界面这就完成了。下一个任务是回到 XCode IDE,向视图控制器类定义添加一些成员变量、属性和方法,如清单
5 所示。
清单 5. iOSChatClientViewController.h
#import <UIKit/UIKit.h>
@interface iOSChatClientViewController : UIViewController
<UITableViewDataSource,UITableViewDelegate>
{
IBOutlet UITextField *messageText;
IBOutlet UIButton *sendButton;
IBOutlet UITableView *messageList;
NSMutableData *receivedData;
NSMutableArray *messages;
int lastId;
NSTimer *timer;
NSXMLParser *chatParser;
NSString *msgAdded;
NSMutableString *msgUser;
NSMutableString *msgText;
int msgId;
Boolean inText;
Boolean inUser;
}
@property (nonatomic,retain) UITextField *messageText;
@property (nonatomic,retain) UIButton *sendButton;
@property (nonatomic,retain) UITableView *messageList;
- (IBAction)sendClicked:(id)sender;
@end |
从顶部开始,我向类定义添加了 UITableViewDataSource 和 UITableViewDelegate。该代码用于驱动消息显示。类中有一些方法可以被回调,以便向表视图提供数据和布局信息。
实例变量分为五组。顶部是对各种 UI 元素的对象引用、要发送的消息的文本字段、发送按钮和消息列表。
下面是一些缓冲区,用于存储返回的 XML、消息列表和看到的最新 ID。lastID 从 0 开始,但是被设置为您看到的任何消息的最大
ID 值。它然后作为 past 参数的值被发送回服务器。
定时器每几秒钟触发一次,以查找来自服务器的新消息。最后一部分代码包含解析 XML 所需的所有成员变量。存在很多成员变量,这是因为
XML 解析器是一个基于回调的解析器,这表示它在类中保留有很多状态。
成员变量下面是属性和单击处理程序。它们由 Interface Builder 用来将界面元素连接到这个控制器类。事实上,视图控制器中有了这些元素之后,就可以回到
Interface Builder,使用连接器控件将消息文本、发送按钮和消息列表连接到它们对应的属性,将
Touch Inside 事件连接到 sendClicked 方法。
构建视图控制器代码
在这一节,可以开始深入项目正题,实现视图控制器。虽然代码都在一个文件中,但是我把它们分成好几个清单,以便解释每一部分时更简单一点。
第一部分,清单 6,介绍应用程序开始部分和视图控制器的初始化。
清单 6. iOSChatClientViewController.m – 开始
#import "iOSChatClientViewController.h"
@implementation iOSChatClientViewController
@synthesize messageText, sendButton, messageList;
- (id)initWithNibName:(NSString *)nibNameOrNil
bundle:(NSBundle *)nibBundleOrNil {
if ((self = [super initWithNibName:nibNameOrNil
bundle:nibBundleOrNil])) {
lastId = 0;
chatParser = NULL;
}
return self;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)viewDidUnload {
}
- (void)dealloc {
[super dealloc];
}
|
这是标准的 iOS 代码。代码中有一些对可变的系统事件(比如说内存警告和存储单元分配)的回调。在生产应用程序中,您想要完美地处理这些事件,但是对于这个示例应用程序来说,我不想让事情过于复杂。
第一个真正的任务是对 messages.php 脚本发出 GET 请求。清单 7 展示了此任务的代码。
清单 7. iOSChatClientViewController.m – 得到消息
- (void)getNewMessages { NSString *url = [NSString stringWithFormat: @"http://localhost/chat/messages.php?past=%ld&t=%ld", lastId, time(0) ];
NSMutableURLRequest *request = [[[NSMutableURLRequest
alloc] init] autorelease];
[request setURL:[NSURL URLWithString:url]];
[request setHTTPMethod:@"GET"];
NSURLConnection *conn=[[NSURLConnection alloc]
initWithRequest:request delegate:self];
if (conn)
{
receivedData = [[NSMutableData data] retain];
}
else
{
}
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection
*)connection
{
if (chatParser)
[chatParser release];
if ( messages == nil )
messages = [[NSMutableArray alloc] init];
chatParser = [[NSXMLParser alloc] initWithData:receivedData];
[chatParser setDelegate:self];
[chatParser parse];
[receivedData release];
[messageList reloadData];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector: @selector(timerCallback)]];
[invocation setTarget:self];
[invocation setSelector:@selector(timerCallback)];
timer = [NSTimer scheduledTimerWithTimeInterval:5.0
invocation:invocation repeats:NO];
}
- (void)timerCallback {
[timer release];
[self getNewMessages];
}
|
代码开始是 getNewMessages 方法。该方法创建请求,并通过构建一个 NSURLConnection
而开始这个请求。它还创建了用于存储响应数据的数据缓冲区。三个事件处理程序 didReceieveResponse、didReceiveData
和 connectionDidFinishLoading 都处理加载数据的各个阶段。
connectionDidFinishLoading 方法是最重要的,因为它启动读取数据并挑出消息的 XML
解析器。
这里的最后一个方法是 timerCallback,由定时器用来启动新消息请求。当定时器超时时,getNewMessages
方法被调用,这将再次启动定时过程,最后将创建一个新的定时器,这个定时器超时时,会再次启动消息检索过程,等等。
下一部分,清单 8,处理 XML 的解析。
清单 8. iOSChatClientViewController.m – 解析消息
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { if ( [elementName isEqualToString:@"message"] ) { msgAdded = [[attributeDict objectForKey:@"added"] retain]; msgId = [[attributeDict objectForKey:@"id"] intValue]; msgUser = [[NSMutableString alloc] init]; msgText = [[NSMutableString alloc] init]; inUser = NO; inText = NO; } if ( [elementName isEqualToString:@"user"] ) { inUser = YES; } if ( [elementName isEqualToString:@"text"] ) { inText = YES; } }
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString
*)string {
if ( inUser ) {
[msgUser appendString:string];
}
if ( inText ) {
[msgText appendString:string];
}
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString
*)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString
*)qName {
if ( [elementName isEqualToString:@"message"]
) {
[messages addObject:[NSDictionary dictionaryWithObjectsAndKeys:msgAdded,
@"added",msgUser,@"user",msgText,@"text",nil]];
lastId = msgId;
[msgAdded release];
[msgUser release];
[msgText release];
}
if ( [elementName isEqualToString:@"user"]
) {
inUser = NO;
}
if ( [elementName isEqualToString:@"text"]
) {
inText = NO;
}
}
|
了解 SAX 解析的人应该都熟悉这个 XML 解析器。您给它一些 XML,当标签打开或关闭时,当找到文本时,它都会向您的代码发送事件。它是一个基于事件的解析器,而不是基于
DOM 的解析器。事件解析器的优点是,内存占用少。但是缺点是比较难以使用,因为在解析期间,所有的状态都需要存储在主机对象中。
过程开始时,所有成员变量(比如 msgAdded、msgUser、inUser 和 inText)都被初始化为一个空字符串或
false。然后,随着每个标签在 didStartElement 方法中完成初始处理,代码查看标签名,并设置适当的
inUser 或 inText Boolean 值。这里,foundCharacters 方法处理向适当的字符串添加文本数据。didEndElement
方法然后处理标签的结束,即在发现 <message> 结束标签时将已解析的消息添加到消息列表。
现在您需要编写代码来显示消息。代码展示在清单 9 中。
清单 9. iOSChatClientViewController.m – 显示消息
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
- (NSInteger)tableView:(UITableView *)myTableView
numberOfRowsInSection:
(NSInteger)section {
return ( messages == nil ) ? 0 : [messages count];
}
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:
(NSIndexPath *)indexPath {
return 75;
}
- (UITableViewCell *)tableView:(UITableView *)myTableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = (UITableViewCell *)[self.messageList
dequeueReusableCellWithIdentifier:@"ChatListItem"];
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"ChatListItem"
owner:self options:nil];
cell = (UITableViewCell *)[nib objectAtIndex:0];
}
NSDictionary *itemAtIndex = (NSDictionary *)[messages
objectAtIndex:indexPath.row];
UILabel *textLabel = (UILabel *)[cell viewWithTag:1];
textLabel.text = [itemAtIndex objectForKey:@"text"];
UILabel *userLabel = (UILabel *)[cell viewWithTag:2];
userLabel.text = [itemAtIndex objectForKey:@"user"];
return cell;
}
|
这些就是 UITableViewDataSource 和 UITableViewDelegate 接口定义的所有方法。最重要的一个是
cellForRowAtIndexPath 方法,它为列表项创建一个定制的 UI,并将它的文本字段设置为这个消息的适当文本。
这个定制的列表项定义在新的 ChatListItem.xib 文件中,您需要将这个文件创建在 Resources
文件夹中。在这个文件中,您创建一个新的 UITableViewCell 条目,其中有两个标签,分别标注为
1 和 2。这个文件以及所有其他代码都可从可下载的项目中得到(参见 下载)。
cellForRowAtIndexPath 方法中的代码分配这些 ChatListItem 单元格中的一个,然后将标签的文本字段设置为我们看到的这个消息的文本和用户值。
我知道要考虑的事项太多,但是已经快结束了。您已经完成了启动视图、获得消息 XML、解析消息和显示消息的代码。惟一剩下要做的事情是编写发送消息的代码。
构建此代码的第一件事是为用户名创建一个设置。iOS 应用程序可以定义进入 Settings 控制面板的定制设置。要创建一个设置,您需要使用
New File 向导在 Resources 文件夹中创建一个设置包。然后您使用图 7 中的 settings
编辑器,将它删除成单个设置。
图 7. 设置 settings
然后您确定,您想要此设置的标题为 User,并具有键 user_preference。然后,您就可以为清单
10 中的消息发送代码使用这个首选项来得到用户名了。
清单 10. iOSChatClientViewController.m – 发送消息
- (IBAction)sendClicked:(id)sender { [messageText resignFirstResponder]; if ( [messageText.text length] > 0 ) { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *url = [NSString stringWithFormat:
@"http://localhost/chat/add.php"];
NSMutableURLRequest *request = [[[NSMutableURLRequest
alloc]
init] autorelease];
[request setURL:[NSURL URLWithString:url]];
[request setHTTPMethod:@"POST"];
NSMutableData *body = [NSMutableData data];
[body appendData:[[NSString stringWithFormat:@"user=%@&message=%@",
[defaults stringForKey:@"user_preference"],
messageText.text] dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPBody:body];
NSHTTPURLResponse *response = nil;
NSError *error = [[[NSError alloc] init] autorelease];
[NSURLConnection sendSynchronousRequest:request
returningResponse:&response error:&error];
[self getNewMessages];
}
messageText.text = @"";
}
- (void)viewDidLoad {
[super viewDidLoad];
messageList.dataSource = self;
messageList.delegate = self;
[self getNewMessages];
}
@end
|
这是 Send Message 按钮的单击处理程序代码。它创建一个 NSMutableURLRequest,该请求具有
add.php 脚本的 URL。它然后将消息主体设置为一个字符串,该字符串的用户和消息数据被编码为 POST
格式。它然后使用一个 NSURLConnection 同步地向服务器发送消息数据,并使用 getNewMessages
启动一次消息检索。
该文件底部的 viewDidLoad 方法是视图加载时调用的方法。它开始消息检索过程,并将消息列表与该对象连接,以便消息列表知道从哪里得到数据。
所有这些都编写好之后,现在就该测试应用程序了。首先是在图 8 所示的 Settings 页面中设置用户名。
图 8. Settings 页面
单击 iOSChatClient 应用程序,会显示图 9 所示的 settings 页面。
图 9. 设置用户名
然后就像使用手机一样回到应用程序,并像图 10 中一样使用键盘输入一条消息。
图 10. 输入新消息
然后按下 send 按钮,我们看到消息被发送并发布到服务器,并从 messages.php 返回,就像您可以从图
11 中看到的一样。
图 11. 完成的聊天应用程序
您会从代码中看到,send 按钮和消息列表之间没有直接连接。所以消息进入消息列表的惟一方式是,通过服务器成功地将数据插入到数据库中。然后
message.php 代码成功地返回消息列表中的消息用于显示。
结束语
这篇文章无疑让您受益匪浅。您在后台对 XML 数据完成了一些数据库操作。构建了一个带有定制用户界面的
iOS 应用程序,它向服务器发送数据,从服务器检索数据。您使用 XML 解析器解析从服务器返回的响应 XML。您还构建了一个定制列表
UI,以便消息看起来更为美观。
下一步该怎么走完全取决于您自己。Apple 已经为您在 iPhone 或 iPad 上实现您的愿景提供了工具。本文给您构建自己的支持网络的应用程序提供了路线图。我鼓励您亲自动手试一试。如果您确实构建了比较酷的应用程序,请告诉我,我会帮助您将它提交到
App Store。
参考资料
- iOS Reference Library:访问这个优秀的网站,这里提供学习对您可用的所有 iOS
工具集的在线资源。
- PHP 网站:探究可用的 PHP 的最佳参考资料。
- W3C:访问这个优秀的标准网站,尤其是跟本文相关的 XML 标准。
- Apple documentation on the Objective-C language:复习
Objective-C 语言的独特语法。
- 本文作者的更多文章(Jack Herrington,developerWorks,2005 年
3 月至今):阅读关于 Ajax、JSON、PHP、XML 和其他技术的文章。
- developerWorks XML 专区:在 XML 专区获取提高您的专业技能所需的资源。
- My developerWorks 中文社区:个性化您的 developerWorks 体验。
- IBM XML 认证:了解如何才能成为一名 IBM 认证的 XML 和相关技术的开发人员。
- XML 技术库:访问 developerWorks XML 专区,获得广泛的技术文章和技巧、教程、标准和
IBM 红皮书。此外,阅读更多的 XML 技巧。
- developerWorks 技术活动 和 网络广播:随时关注这些活动中的技术。
- developerWorks 播客:收听面向软件开发人员的有趣访谈和讨论。
- developerWorks 按需演示:包括面向初学者的产品安装和设置演示,以及为经验丰富的开发人员提供的高级功能。
|