目前已有 4000 万台 iPhones 在用,您无疑对编写 iOS 应用程序感兴趣。但是从何着手呢?大多数应用程序都会连接网络,那么一个跨越两端的项目(比如说聊天应用程序)又是如何呢?本文将向您介绍如何利用服务器 和客户端组件构建一个聊天应用程序。从本文可以学到编写 iOS 应用程序的整个流程。学完本文之后,我保证您会想要编写一个这样的应用程序。

构建应用程序从架构解决方案开始。图 1 中的架构展示了 iOS 设备(这里是 iPhone)如何通过两个 PHP 页面连接到服务器。

图 1. Chat App 客户端/服务器架构
iPhone 客户端图,带有两个连接到数据库的页面 add.php 和 messages.php

这两个 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 />

该脚本连接到数据库,并使用已发布的 usermessage 字段存储消息。就是在简单的 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. 消息发布测试页面

带有 User 和 Message 字段及 Submit Query 按钮的页面的屏幕截图

从这里,您添加一些值并单击 Submit Query 按钮。如果一切正常,您会看到类似于图 3 的画面。

图 3. 成功的消息发布

在浏览器中查看的 XML 文件的屏幕截图,带有一个 <success/> 元素

否则,您可能会得到一个 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. 聊天消息列表

编码为 XML 的简短聊天消息的屏幕截图,包含时间戳、ID、用户和消息文本

服务器脚本就算完成了。当然,您可以添加您想要的任何逻辑,额外的通道、用户验证和登录,等等。对于这个实验性的聊天应用程序,这个脚本已经工作得很好了。现在您可以构建将会使用这个服务器脚本的 iOS 应用程序了。

构建客户端代码

iOS IDE 叫做 XCode。如果您还没有这个 IDE,那么需要从 Apple Developer Site(参见 参考资料)下载它。最新生产版本是 XCode 3,我这里的屏幕截图使用的就是这个版本。现在已经有了一个更新的版本,叫做 XCode 4,它在 IDE 中集成了 User Interface 编辑器,但是该版本目前还处于预览模式。

XCode 安装好之后,现在就该使用图 5 所示的 New Project 向导构建应用程序了。

图 5. 构建一个基于视图的 iPhone 应用程序

选择的模板选项的屏幕截图:iPhone OS > Application and Product > iPhone > View-based Application

开始最容易的应用程序类型是基于视图的应用程序。这种应用程序允许您在您选择的地方放置控件,并为您完成大多数 UI 设计。选择控件之后,再选择 iPhone 或 iPad。这一选择关系到您将在什么样的设备上进行模拟。您可以编写代码,以便在 iPhone 或 iPad 或者 Apple 即将推出的任何其他 i-设备上运行。

单击 Choose 之后,您会被要求给应用程序命名。我将我的应用程序取名为 “iOSChatClient”,但是您可以随便给自己的应用程序取一个您喜欢的名字。您给应用程序命名之后,XCode IDE 会构建核心应用程序文件。然后,编译并启动它,确保一切正常。

创建用户界面

创建应用程序之后,您就可以开发界面了。从视图控制器 XIB 文件开始,该文件位于 Resources 文件夹中。通过双击该文件夹,可以打开 Interface Builder,这是 UI 工具箱。

图 6. 界面布局

UI 的屏幕截图,带有一个消息文本框、Send 按钮和聊天记录列表

图 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 而开始这个请求。它还创建了用于存储响应数据的数据缓冲区。三个事件处理程序 didReceieveResponsedidReceiveDataconnectionDidFinishLoading 都处理加载数据的各个阶段。

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 的解析器。事件解析器的优点是,内存占用少。但是缺点是比较难以使用,因为在解析期间,所有的状态都需要存储在主机对象中。

过程开始时,所有成员变量(比如 msgAddedmsgUserinUserinText)都被初始化为一个空字符串或 false。然后,随着每个标签在didStartElement 方法中完成初始处理,代码查看标签名,并设置适当的 inUserinText 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;
}

这些就是 UITableViewDataSourceUITableViewDelegate 接口定义的所有方法。最重要的一个是 cellForRowAtIndexPath 方法,它为列表项创建一个定制的 UI,并将它的文本字段设置为这个消息的适当文本。

这个定制的列表项定义在新的 ChatListItem.xib 文件中,您需要将这个文件创建在 Resources 文件夹中。在这个文件中,您创建一个新的 UITableViewCell 条目,其中有两个标签,分别标注为 1 和 2。这个文件以及所有其他代码都可从可下载的项目中得到(参见 下载)。

cellForRowAtIndexPath 方法中的代码分配这些 ChatListItem 单元格中的一个,然后将标签的文本字段设置为我们看到的这个消息的文本和用户值。

我知道要考虑的事项太多,但是已经快结束了。您已经完成了启动视图、获得消息 XML、解析消息和显示消息的代码。惟一剩下要做的事情是编写发送消息的代码。

构建此代码的第一件事是为用户名创建一个设置。iOS 应用程序可以定义进入 Settings 控制面板的定制设置。要创建一个设置,您需要使用 New File 向导在 Resources 文件夹中创建一个设置包。然后您使用图 7 中的 settings 编辑器,将它删除成单个设置。

图 7. 设置 settings

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 页面

Settings 页面的屏幕截图,带有 General、Safari、Photos 和 iOSChatClient 选项

单击 iOSChatClient 应用程序,会显示图 9 所示的 settings 页面。

图 9. 设置用户名

iOSChatClient 设置页面的屏幕截图,User 字段中的值是 'Megan'

然后就像使用手机一样回到应用程序,并像图 10 中一样使用键盘输入一条消息。

图 10. 输入新消息

QWERTY 键盘、消息文本框、Send 按钮和用户 jack 的测试消息的屏幕截图

然后按下 send 按钮,我们看到消息被发送并发布到服务器,并从 messages.php 返回,就像您可以从图 11 中看到的一样。

图 11. 完成的聊天应用程序

消息文本框、Send 按钮和用户 jack 及 megan 的两条测试消息的屏幕截图

您会从代码中看到,send 按钮和消息列表之间没有直接连接。所以消息进入消息列表的惟一方式是,通过服务器成功地将数据插入到数据库中。然后 message.php 代码成功地返回消息列表中的消息用于显示。

结束语

这篇文章无疑让您受益匪浅。您在后台对 XML 数据完成了一些数据库操作。构建了一个带有定制用户界面的 iOS 应用程序,它向服务器发送数据,从服务器检索数据。您使用 XML 解析器解析从服务器返回的响应 XML。您还构建了一个定制列表 UI,以便消息看起来更为美观。

下一步该怎么走完全取决于您自己。Apple 已经为您在 iPhone 或 iPad 上实现您的愿景提供了工具。本文给您构建自己的支持网络的应用程序提供了路线图。我鼓励您亲自动手试一试。如果您确实构建了比较酷的应用程序,请告诉我, 我会帮助您将它提交到 App Store。

下载本文源代码

更多关于PHP的详细信息,或者下载地址请点这里

转载自 IBM Developers