目前已有 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
1 2 3 4 5 6 7 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
1 2 3 4 5 6 7 8 9 10 11 <?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
1 2 3 4 5 6 7 8 9 10 11 12 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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 参数发送它。在开始时,它可以发送 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #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 – 开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #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 – 得到消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 - (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 – 解析消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 - (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 – 显示消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 - (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 – 发送消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 - (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。
下载本文源代码
更多关于PHP 的详细信息,或者下载地址请点这里
转载自 IBM Developers