如何创建一个类似 Instagram 的使用 Web Service 作后台的应用 第一部分

Marin Todorov

这篇文章还可以在这里找到 英语

Learn how to make a fun app with a web service backend!

Learn how to make a fun app with a web service backend!

这篇文章是由 iOS 教程组成员,一个拥有12年以上软件开发经历、独立的iOS开发者、并且是 Touch Code Magazine 的创始人,Marin Todorov 所撰写的。

毫无疑问 App Store 上的摄影应用有着上升之势。依靠 iPhone 令人惊叹的摄像头和快速的处理器,拍照并应用各种特效也变得越来越有趣。

你希望有一篇关于如何创建与使用 web service 后台搭档的拍照应用的教程,你的愿望就是我们的命令!:]

在本教程中,你将学习如何做一个简单的照片共享应用,就像 Instagram 的一个非常简单的版本。特别是,你将学习到:

使用一个空白的启动项目,所有的UI已经建立,本教程将介绍如何:

  • 如何使用 Objective-C 连接到一个基于 JSON 网络 API
  • 如何使用 PHP 创建一个简单的 JSON API
  • 如何为此 API 实现用户授权
  • 如何拍照、应用特效,并发送只 JSON 服务。

相当多很酷的东西,不是吗?;]

本教程假定你有事先熟悉 iOS 5 技术,例如 Storyboards 和 ARC。如果你还不熟悉这些,现在是时候看看 这篇教程 或 这篇教程

你还需要与一个运行 MySQL 的 Web 服务器连接。如果这听起来有点吓人,看看 这个项目,它能够很轻易地在 Mac 上运行一个本地测试服务器。

如果你仍然感到关于你的 Mac 上设置 Web 服务器的过程不够安全,看看在这个伟大的教程,它涵盖了所有设置开发环境的基础。

事不宜迟,做好拍照的准备——让我们开始吧!

准备工作

下载启动项目并解压缩 ZIP 文件的内容保存硬盘上的某个便利位置。它包括很少的东西,所以我将简要地去介绍里面有什么。

在本教程中你要开发的项目称为 iReporter。这是一个让你能够看到所有用户的照片流的应用,如果你想,也可以注册并上传自己的照片到照片流中去。

iReporter 共有四个视图控制器。在项目中的文件列表中找到 Storyboard.storyboard 文件,感觉下得到工作流程看起来是什么样的:

The startup project storyboard

初始试图控制器是一个导航控制器,应用启动后显示一个称为 StreamScreen 的页面。这是你要显示所有用户上传的照片流的地方。

如果你按照 segue 继续接下来要显示的页面,你会得到一个称为 StreamPhotoScreen 的控制器。当用户点击照片缩略图时,此页面将出现,并显示一个大尺寸的照片和照片标题。

从 StreamScreen 还有第二个 segue,转到一个称为 PhotoScreen 的页面。 在 PhotoScreen,会显示一个 action sheet,并且允许用户拍照、应用特效、最终发布到 Web API。

为了能够做上述,用户必须授权自己给 API。这意味着 PhotoScreen 出现时,一旦用户尚未登录,将显示一个要求用户登录或注册的 LoginScreen 视图控制器的模态页面。

幸运的是,启动项目已经包含了所有这些页面类,包括已经连接 IBOutlets 和 IBActions(主体空白部分将在教程过程中由你实现)。

在启动项目中还有一些文件。在 Categories 中,你会发现来自 Trevor Harmon 的 UIImage categories。你需要这些来轻松地调整和裁剪图像。有个附带的好处就是,Trevor 的 categories 解决了有时来自 iPhone 摄像头的图片方向问题!所以再次感谢 Trevor!

我还包括一个小的 category,有利于用一个简单的消息在页面上显示 UIAlertView。

正如你可以看到那样,你是一个良好的开端 :] 接下来,你将添加第三方库到项目中,以处理所有对你的网络相关问题。

外包应用的社交生活

你可以使用 iOS 提供的很好但老式的 NSURLConnection 来处理与 Web API 的通信。但是现在已经是2012年了……我们可以做地更性感些 :]

在少数有前途的使用 Objective-C 语言处理网络库中,AFNetworking 看起来更加有发展势头,所以你也将使用这个。前往 AFNetworking Git 下载最新版本。(简短的文档同样也包含在网页当中)

下载后,在 AFNetworking 文件夹中,你会发现一个名为 “AFNetworking” 的子文件夹。只要将它拖放到你的项目文件列表。你会得到类似下面的提示:

Instagram1

确保 “Copy items into destination group’s folder (if needed)” 被选中,从而 AFNetworking 文件被复制到项目文件夹中。点击 Finish,你会看到 AFNetworking 的文件包含在文件列表中。

AFNetworking files

AFNetworking 不兼容 ARC,但你的项目骨架设置为使用自动引用计数(ARC)的。所以,你必须设置 AFNetworking 所有类并非 ARC (non-ARC)使得编译器知道如何处理它们。

选择 Xcode 项目的根目录(如上图所示),然后切换到右侧窗格中的 “Build Phases” 标签。找到并打开 “Compile Source” 带,你会看到项目中需要编译的类。

向下滚动。在底部,你会看到 AFNetworking 的所有文件(从 AFHTTPClient.mUIImageView+AFNetworking.m)。选择所有(如下图所示),按键盘上的 Enter 键,在弹出框中输入 “-fno-objc-arc”,并单击 Done。现在所有 AFNetworking 文件标记为不支持 ARC。

Instagram2

按 Cmd+B 来构建项目。如果一切设置正确,项目应编译成功。(除了来自 Trevor 的 UIImage category 类的警告。你可以忽略这些。)

注:当你在项目中使用 AFNetworking 时,你必须在项目的 .pch 文件中添加 “#import <SystemConfiguration/SystemConfiguration.h>”。否则,AFNetworking 将无法编译。在启动项目中已经为你做好了,但是知道它对你以后有好处。

建立 Web API

现在该项目的另一部分—— Web API。对于这一点,你需要一个 HTTP 服务器,支持 FTP,并与 MySQL 服务器连接。如果你的 Mac 上还没有这些,在本文章顶部查看链接以获取关于建立一个本地测试服务器的详细内容。

我还为 Web API 准备了一个启动项目,所以一旦你有 Web 服务器在运行,前往并下载 API 启动项目。 解压缩 ZIP 文件的内容保存到硬盘某个位置。
ZIP 文件中应该包含几个 PHP 文件和一个名为 “upload” 的文件夹。最主要的 API 文件是 index.php,这正是一个你要从 iPhone 应用调用的东西。在 api.php,你要添加几个简单的方法来处理用户请求,lib.php 包含了一些对你有用的方法。

其中一个叫 query 的方法,将帮助你避免编写太多的代码。它需要一个 SQL 查询和参数列表作为输入,并返回一个包含 SQL 查询结果的数组。还有一个 thumb 方法用来裁剪图像和保存缩略图。

上传包含这些 API 的文件到你的 Web 服务器(可以通过浏览器来访问)上的某个位置。重命名文件夹为 “iReporter”,这会更容易识别。(如果你正在你自己家的机器上做这些,你当然可以只拷贝这些文件到你的 Web 文件夹中去)。确保 “upload” 文件夹可以通过 PHP 脚本修改,因为将会使用它来保存上传的照片。

注:继续本教程,你将修改一些 Web 服务的 PHP 文件。请注意,如果你的 Mac 上没有建立 Web 服务器,而是使用远程服务器,你需要每次修改文件的时候重新上传修改过的文件。

最后,为 web API 创建数据库。你需要两个简单的表——一个用来保存用户名和密码、另一个用来保存照片。数据库表结构如下(它相当简单):

SQL database structure

这是创建数据库表的 SQL 文件(数据库命名为 “iReport”)。

现在做个额外的步骤:在你所选择的文本编辑器中打开 lib.php,看看代码的前面几行。取决于你如何建立 MySQL 服务器(或它是如何建立在托管服务器上),你需要编辑代码,使 PHP 知道如何连接到数据库服务器。

第一个功能,mysqli_connect,有三个参数。用这样的信息填充:第一个参数是数据库服务器的名称(如果你连接到自己的计算机的服务器上,保持为 “localhost”),第二个是 MySQL 的用户名,第三个是密码。我已经输入一些默认值,对空白的安装在本地的 MySQL 有效,但正如前面提到的,您可能需要修改这些以与你的设置有效。

lib.php 第二行,有个 mysqli_select_db 功能,它有两个参数:与数据库服务器连接的链接地址、数据库的名称。如果你的数据库不是叫 iReport,在这里更改数据库的名称。

真棒!你现在有 iPhone 和 Web 服务的项目骨架了。所剩的就是编写一些代码,让他们一起工作。

向 Web 服务注册用户

本应用的计划是只有用户注册了之后才让他们上传照片。因此,应用的工作流程是这样的:

App workflow

让我们开始完成注册和登录到服务器。在你最喜爱的文本编辑器中打开 index.php 文件。在包含 “//API” 的行中,添加一个 API 将处理所有可能命令的检查。

registerlogin 开始。将 “//API” 注释改为:

switch ($_POST['command']) {
	case "login": 
		login($_POST['username'], $_POST['password']); break;
	case "register":
		register($_POST['username'], $_POST['password']); break;
 
}

如果名为 “command” 的 POST 参数保持登录值,那么你向名为 login 的方法传递用户名和密码的 POST 参数。注册参数也是一样,除了方法名为 register。如果客户端请求了一个 API 并不期望的命令,代码执行将会仅仅到达源文件末尾的 exit() 命令,并且没有响应会发送到客户端。

注:不要惊慌,我们不会明文传递或储存密码的。(你也不应该!)稍后会告诉你更多关于这方面的内容 :]

现在开始注册命令。打开 api.php,添加一些辅助方法用来返回错误到客户端(iPhone 应用)。将这个方法添加到 api.php 内的 PHP 脚本标签(“<?” 和 “?>”):

function errorJson($msg){
	print json_encode(array('error'=&gt;$msg));
	exit();
}

由于 iPhone 应用只期望 JSON 响应,因此你使用这个将文本转成 JSON 响应的小方法。json_encode 就是将 PHP 对象转成 JSON 的那个方法——你仅仅只需要将一个包含错误信息的数组传递过去,就这样。

现在进行注册功能。首先,检查用户名是否已经存在。添加以下代码到 api.php:

function register($user, $pass) {
	//check if username exists
	$login = query("SELECT username FROM login WHERE username='%s' limit 1", $user);
	if (count($login['result'])&gt;0) {
		errorJson('Username already exists');
	}
 
}

在第一行中,创建一个 SQL 查询来检查 login 表中对于给定的用户名是否有记录。如果有结果,则调用 errorJson,因为如果用户名已经存在,你就不能注册。errorJson 转储错误消息并退出程序执行,几乎结束脚本。

注意在 PHP 中的 count 方法是如何来检查是否存在任何记录的—— count 返回一个数组元素的个数。

OK,所以事情并不那么困难。将以下内容添加到 register 方法的末尾(在大括号闭合之前):

//try to register the user
$result = query("INSERT INTO login(username, pass) VALUES('%s','%s')", $user, $pass);
if (!$result['error']) {
	//success
	login($user, $pass);
} else {
	//error
	errorJson('Registration failed');
}

你可以使用 query 辅助方法,再次用一条 SQL 命令试图插入一个新记录到数据库。如果该查询不返回一个错误(也就是你成功了),然后你只需要调用 login。
你为什么调用 login?好吧,你想用户注册成功后,然后记录下来,那么为什么不将二者组合成只调用一次 API,对不对?好了,该去看看 login 方法了。哇哦,你移动真迅速 :]

这是整个 login 功能。添加到 api.php,紧跟在 register 方法的后面:

function login($user, $pass) {
	$result = query("SELECT IdUser, username FROM login WHERE username='%s' AND pass='%s' limit 1", $user, $pass);
	if (count($result['result'])&gt;0) {
		//authorized
		$_SESSION['IdUser'] = $result['result'][0]['IdUser'];
		print json_encode($result);
	} else {
		//not authorized
		errorJson('Authorization failed');
	}
}

它看起来跟你已经在 register 所做的相当类似,对不对?

你在 login 表上运行一个 SELECT SQL 命令,然后检查返回的结果数。如何有任何结果,说明你已经成功登录了。然后为已登录的用户创建一个服务器会话,把 IdUser 存储到 PHP 会话中。

$_SESSION 是一个特殊的数组——你里面存储的一切将在不同的脚本执行之间被持有——但只有当它们被相同的客户端所生成的情况下。因此你在 SESSION 数组中存储的是 “$result['result'][0]['IdUser']” 的值,也就是在 PHP 中读取“存储的对应 result 键所对应的数组的第一条记录的 IdUser 值”。或者简单来说,你存储这些用户的 ID,这样你就知道在接下来的 API 调用中他们是谁。

信不信由你,这是所有你需要用户注册和登录到 Web 服务的 PHP 代码。

让我们继续前进到 iPhone ——因为我理解你现在是什么感觉!;]

Finally Objective-C!!!

与服务器友好交互

太棒了!你终于可以切换到 Xcode 做一些 Objective-C 相关的事情了!(你最喜欢的编程语言,对不对?)

首先,你需要创建一个类来为你与 API 交互。经过很多思考,我决得你应该把它称之为 “API” :]

所以从 Xcode 的菜单中选择 “File/New/File…”,然后选择 iOSCocoa TouchObjective-C 类作为文件类型。单击 “Next”,进入 “API” 作为类名,使其作为 AFHTTPClient 的子类,并把它保存到你的项目文件夹。

由于它将通过网络与服务器交互,所以你直接继承 AFHTTPClient,使用它来发送 POST 请求,还有做其他事情。

打开 API.h,添加下面的 import 语句到所有用到 AFNetworking 功能的顶部:

#import "AFNetworking.h"

然后在 @interface 行的上面,添加一个定义:

//API call completion block with result as json
typedef void (^JSONResponseBlock)(NSDictionary* json);

上面添加了一个块的定义,它接收一个包含 JSON 信息的 NSDictionary 作为参数。你可以使用这种块来处理所有与 API 交互,因为服务器总是返回一个 JSON 响应。

在 interface 定义内部添加下面的 property 用以保持一旦用户已被授权时的数据:

//the authorized user
@property (strong, nonatomic) NSDictionary* user;

然后……你想能够使用一个来自应用中的所有页面的类。最简单的管理机制就是使它成为一个单例类,所以你需要一个方法来访问类的静态实例。在 @end 行的上面添加此方法定义:

+(API*)sharedInstance;

最后,您需要再添加两个方法来得到过去用户授权状态(仍然在API.h)。一个方法用来检查用户是否被授权,另一个是一个通用方法,你将通过 API 类在 Web 服务器上执行命令。

//check whether there's an authorized user
-(BOOL)isAuthorized;
//send an API command to the server
-(void)commandWithParams:(NSMutableDictionary*)params onCompletion:(JSONResponseBlock)completionBlock;

接下来是 API 类的实现!打开 API.m,刚好在 @implementation 指令的上面添加这些定义:

//the web location of the service
#define kAPIHost @"http://localhost"
#define kAPIPath @"iReporter/"

对于上述的定义,如果您有除了本地机器调用 API 之外的任何设置,你将不得不更改域和路径。如果正如我在上面文字所述的那样,已经准确设置好了,那么上面的代码将与 API 交互——本地机器的 Web 服务器和默认网站的根目录的子文件夹 iReporter。

否则,你需要根据您的设置更改主机和路径。如果根据本教程设置了自定义域,且API 文件在域的根目录,而不是一个子文件夹内,那么把路径设置为空字符串。

刚好在 @implementation 指令的下面,添加用户属性的 synthesizer 和 静态类实例的方法:

@synthesize user;
#pragma mark - Singleton methods
/**
 * Singleton methods
 */
+(API*)sharedInstance
{
    static API *sharedInstance = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&amp;oncePredicate, ^{
        sharedInstance = [[self alloc] initWithBaseURL:[NSURL URLWithString:kAPIHost]];
    });
 
    return sharedInstance;
}

sharedInstance 在被首次调用时创建一个 API 类的实例,而且任何后续的对此方法的调用都将仅仅返回该实例。在 Objective-C 中有许多不同的方式创建一个单例类,但自从 iOS 4.0(当 Grand Central Dispatch 或 GCD 被引入时)开始,这是最简单的做法之一。调用 dispatch_once 确保共享的实例的创建只执行一次。

你需要一个自定义的 init,但它会很容易。你需要注册默认的 HTTP 操作类(你将使用默认值),并指示 API 类只接收 JSON 作为响应。将添加这段代码到该文件的末尾(但在 @end 之前):

#pragma mark - init
//intialize the API class with the destination host name
-(API*)init
{
    //call super init
    self = [super init];
 
    if (self != nil) {
        //initialize the object
        user = nil;
 
        [self registerHTTPOperationClass:[AFJSONRequestOperation class]];
 
        // Accept HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
        [self setDefaultHeader:@"Accept" value:@"application/json"];
    }
 
    return self;
}

用户属性只持有 login/register API 调用的响应,所以这是一个与当前用户(你从 API 获得的)所有的数据库列的字典。下面是一个简单的检查,看看是否用户已授权:

-(BOOL)isAuthorized
{
    return [[user objectForKey:@"IdUser"] intValue]&gt;0;
}

只是检查 login/register 调用是否有返回,若 IdUser 列是一个大于零的数字——说明已成功授权。

所有剩下的就是完成服务器调用。

添加以下代码到 API.m 末尾:

-(void)commandWithParams:(NSMutableDictionary*)params onCompletion:(JSONResponseBlock)completionBlock
{
    NSMutableURLRequest *apiRequest = 
        [self multipartFormRequestWithMethod:@"POST" 
                                        path:kAPIPath 
                                  parameters:params 
                   constructingBodyWithBlock: ^(id formData) {
                       //TODO: attach file if needed
    }];
 
    AFJSONRequestOperation* operation = [[AFJSONRequestOperation alloc] initWithRequest: apiRequest];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        //success!
        completionBlock(responseObject);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        //failure :(
        completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:@"error"]);
    }];
 
    [operation start];
}
  1. 首先,你创建一个使用你要通过 POST 发送作为参数的 NSMutableURLRequest 实例。请注意,它是一个可变的实例——你刚才不需要,但后来当你给 API 添加文件上传功能时,你需要在创建后调整请求;所以你只需给未来使用准备好。
  2. 接下来,您创建一个运行在后台的处理网络通信的 operation,用你刚准备好的 POST 请求初始化。
  3. operation 初始化后,你设置两个块,在成功或失败时执行。当调用成功时,你只传递 After the operation is initialized, you set the two blocks to execute on success and failure. When the call is successful, you just pass in the JSON response.
  4. 如果有错误和失败的块被调用,你建立一个新的字典,保存网络错误的信息,并将它传递回来——真容易。
  5. 最后,你调用 operation 的 start 方法,此时 AFNetworking 开始在后台施展它的魔法。

这几乎是你在项目中需要的全部 API 类,如你所见,很简单。你将在以后做很少的一些适配,但现在——即将出发!

加点盐

选择 Storyboard.storyboard,并注意照片流如何呈现模态的登录对话框。下一步你将在此画面上通过询问登录或注册的方式为用户授权。

Photo!

跳转到 PhotoScreen.m (它在 Screen 文件夹中)并且通过在现有的 #import 语句下面添加下面的代码的方式导入 API 类:

#import "API.h"

上述允许你在 PhotoScreen 中引用 API 类。
下一步,在 viewDidLoad 末尾,添加此代码来检查用户是否已经被授权了:

if (![[API sharedInstance] isAuthorized]) {
    [self performSegueWithIdentifier:@"ShowLogin" sender:nil];
}

在这里,你只需要调用你不久之前添加到 API 类中的 isAuthorized 方法。如果用户没有被授权,你调用名为 ShowLogin 的 segue 来模态显示登录画面。

如果你想有一番乐趣,运行该项目并点击右上角的按钮——这应该显示登录屏幕!有趣!

Login or register screen

当然,如果你按照上面的做了,然后你发现,登录和注册按钮都不起作用。所以将焦点切换到登录画面,并给它添加一些功能!

首先,添加一个快速可用型的增强功能。给 LoginScreen.m 刚好在 @implementation 行的下方添加一个像这样的 viewDidLoad 方法:

-(void)viewDidLoad {
    [super viewDidLoad];
 
    //focus on the username field / show keyboard
    [fldUsername becomeFirstResponder];
}

真好!用户并不需要点击 username 字段;键盘本身就弹出。这很简单——让我们继续吧!

在源文件顶部,刚好在两个 import 指令的下方添加:

#import "API.h"
#include &lt;CommonCrypto/CommonDigest.h&gt;
#define kSalt @"adlfu3489tyh2jnkLIUGI&amp;%EV(&amp;0982cbgrykxjnk8855"

哇!那是什么?!

首先,你导入 API 类。然后,你包含了内置的 iOS 加密库。好吧,这是有道理的……

看起来像垃圾的字符串是一个盐字符串(salt string),你将要使用让用户密码难以破解。相反,服务器的明文密码对于那些转储数据库的人来说则很容易看到,我们将稍微转换一下(译者注:原文是 munge it a bit,根据 New Hacker’s Dictionary,munge 有不完全转换信息的意思),而不是使用称为盐哈希(hash with salt)的技术。

这一点也不是你的服务器——客户端模型的防弹措施,但它是一个良好的开端。如果你想了解更多关于为存储的密码加盐的知识,看一看 salting passwordsrainbow tables

注意:本教程的焦点不是关于安全性,所以为密码进行盐哈希等已经偏离了主题。很快哪一天,我们希望发表另一篇专门聚焦于 Web Services 的用户名/密码安全的重点教程 ;]

在登录屏幕上一会儿看看。你总是有相同的文本字段:用户名和密码。登录和注册用例之间唯一的区别是实际调用的API:否则发送过来的数据是相同的。

这意味着你只需要一个方法来处理登录和注册按钮的点击。为确定是否发送一个登录或注册命令到服务器,你只需检查哪一个按钮被点击了。

登录和注册按钮都发送一个 btnLoginRegisterTapped: 消息到视图控制器类。这将会是一个相当长的方法,因此你每一步都要小心检查。在 LoginScreen.mbtnLoginRegisterTapped: 空白中,添加这段代码:

//form fields validation
if (fldUsername.text.length &lt; 4 || fldPassword.text.length &lt; 4) {
    [UIAlertView error:@"Enter username and password over 4 chars each."];
    return;
}
//salt the password
NSString* saltedPassword = [NSString stringWithFormat:@"%@%@", fldPassword.text, kSalt];

首先你对输入的用户名和密码做一个简单的检查,以判决他们是否足够长(如果你想,你可以实现更多检查)。然后你给密码字符串加盐。下一步给这些已加盐的密码进行哈希。添加这段代码到方法中:

//prepare the hashed storage
NSString* hashedPassword = nil;
unsigned char hashedPasswordData[CC_SHA1_DIGEST_LENGTH];
//hash the pass
NSData *data = [saltedPassword dataUsingEncoding: NSUTF8StringEncoding];
if (CC_SHA1([data bytes], [data length], hashedPasswordData)) {
    hashedPassword = [[NSString alloc] initWithBytes:hashedPasswordData length:sizeof(hashedPasswordData) encoding:NSASCIIStringEncoding];
} else {
    [UIAlertView error:@"Password can't be sent"];
    return;
}

你声明两个变量:hashedPassword 将保持已哈希并加了盐的密码,hashedPasswordData 是一个普通的 C 数组,你将使用它作为已哈希的数据的一个中间存储。

通过使用 dataUsingEncoding: 来获取加了盐的密码的数据字节。这是魔法发生的地方:CC_SHA1() 获取已加盐密码的字节,对它们执行 SHA1 哈希并存储结果到 hashedPasswordData。接下来,你很轻松地创建一个数据字节的 NSString。这就是你将发送到服务器的已哈希的密码。

吁!繁重的工作做完了。现在你只需要与 API 交互。

首先你需要检测用户是否点击了登录或注册按钮,然后为调用 API 准备正确的参数。既然注册按钮有一个为 “1” 的 tag(这是通过 Interface Builder 的项目骨架被最初创建时设置),那么就很容易了:

//check whether it's a login or register
NSString* command = (sender.tag==1)?@"register":@"login";
NSMutableDictionary* params =[NSMutableDictionary dictionaryWithObjectsAndKeys: 
 command, @"command",
 fldUsername.text, @"username",
 hashedPassword, @"password",
 nil];

既然你为调用准备好了所有的参数,因此实际调用也就相当容易了:

//make the call to the web API
[[API sharedInstance] commandWithParams:params
                           onCompletion:^(NSDictionary *json) {
                           //handle the response
 
                           }];

来自服务器的结果(或因通信问题而产生的错误)将被作为一个 NSDictionary 向上述的块传递。让我们跳到到处理响应部分。用这段代码替换 “//handle the response” 注释:

//result returned
NSDictionary* res = [[json objectForKey:@"result"] objectAtIndex:0];
if ([json objectForKey:@"error"]==nil &amp;&amp; [[res objectForKey:@"IdUser"] intValue]&gt;0) {
   //success
 
} else {
   //error
   [UIAlertView error:[json objectForKey:@"error"]];
}

json 是一个 NSDictionary,并且同你之前写的 PHP API 代码类似,它应该保持一个结果键。你获取来自这个键的另一个字典——希望它是已登录用户的信息!

你然后检查是否返回一个错误,或者用户信息无效,假如是这样的话,你显示一个错误警告。否则,你让这个用户登录并带他(她)到 Photo 画面。为了达到,用这段代码替换 “//success” 行:

[[API sharedInstance] setUser: res];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
//show message to the user
[[[UIAlertView alloc] initWithTitle:@"Logged in" 
                           message:[NSString stringWithFormat:@"Welcome %@",[res objectForKey:@"username"] ]
                         delegate:nil 
                cancelButtonTitle:@"Close" 
                 otherButtonTitles: nil] show];

你存储用户数据到 API 类并关闭登录对话框。一个很好的警告也弹出了,根据用户名来祝贺他们。

很棒吧!

假如用户想注册,这个方法将注册并同时使他们登录。除此之外,用户也可以使用一个已存在的用户账号和密码简单的登录到系统。假如登录不成功,你显示来自服务器的错误信息。所有这些都如此容易 :]

到目前为止那是个很棒的工作!现在一切都应该正确工作了,所以启动项目,转到登录画面。选择一个用户名和一个密码,点击登录。如果你有正在工作的服务器环境,你可以看到登录画面消失,并且 Photo 画面可以达到!很赞吧!

Logged in the app

我为文件做好准备了,宝贝

转到后台的 Web 服务,你将继续接收来自用户和提交照片和保存到服务器的 Web 服务的部分。首先你需要添加代码来处理上传命令。

打开 index.php 在 switch 命令中添加一个新的 case:

case "upload":
	upload($_SESSION['IdUser'], $_FILES['file'], $_POST['title']);break;

假如 API 的上传命令被调用了,你从用户会话中获取用户 ID,然后把它与随提交的文件和用户为那张张照片提供的标题一同发送到 upload 方法(来自 PHP 的 $_FILES 数组)。

啊!upload 方法。完全没问题。

打开 api.php。这就是你将添加到之前提到的方法的地方。在文件底部添加这段代码:

function upload($id, $photoData, $title) {
	//check if a user ID is passed
	if (!$id) errorJson('Authorization required');
 
	//check if there was no error during the file upload
	if ($photoData['error']==0) {
		$result = query("INSERT INTO photos(IdUser,title) VALUES('%d','%s')", $id, $title);
		if (!$result['error']) {
 
			//inserted in the database, go on with file storage
 
		} else {
			errorJson('Upload database problem.'.$result['error']);
		}
	} else {
		errorJson('Upload malfunction');
	}
}

这是你需要做照片上传的代码的第一部分。让我们看看你在哪:

  1. 既然这是一个 protected 方法(换言之,只对已登录用户可访问)你检查在会话中是否已储存了一个用户 ID(传递到方法的 $id 参数)。
  2. $photoData(一个数组)通过 PHP 用文件上传详细信息来填充。你检查 error 键来看上传是否成功。如果有错误,继续插入数据到数据库中就无意义了。
  3. 如果上传成功了,你继续插入照片标题和用户 ID 到数据库中。
  4. 你将在数据库表 “photo” 中使用自动创建的 ID 作为照片文件本身的一个标识符。你将过一会看到这个。
  5. 如果数据库插入成功,你继续保存文件——你看代码将在的注释那个位置。

正如你看到的那样,目前为止在上传进程中的每一步都可能因不同的方式失败,因此你已经有三个 if-else 语句使用 errorJson 来返回不同的失败响应。在解耦系统(decoupled system)中(像你的客户端——服务器应用),与一般的软件相比,在工作流中会有更多的步骤可能出错,因此假如你想建立一个健壮的复杂的应用程序,拥有良好的错误处理逻辑是必不可少的。

现在添加代码来存储服务器上的已上传的文件。

首先你需要保持一个到数据库的链接,因此你可以获得在 photos 表中自动生成的 ID。幸运地,lib.php 已经为你抓取了一个到数据库的链接,所以刚好用那个。那个链接叫做 “$link”,在全局变量命名空间中可被发现。

添加代码看看它如何工作。用这段代码替换 “//inserted in the database…”:

	//database link
	global $link;
 
	//get the last automatically generated ID
	$IdPhoto = mysqli_insert_id($link);
 
	//move the temporarily stored file to a convenient location
	if (move_uploaded_file($photoData['tmp_name'], "upload/".$IdPhoto.".jpg")) {
		//file moved, all good, generate thumbnail
		thumb("upload/".$IdPhoto.".jpg", 90);
		print json_encode(array('successful'=&gt;1));
	} else {
		errorJson('Upload on server problem');
	};

你调用 mysqli_insert_id,返回数据库中上次自动生成的 ID ——也即最新保存的照片记录 ID。当你使用 PHP 上传一个文件到服务器时,该文件被保存到服务器的一个临时位置,并且到那个位置的路径被传递到通过 $_FILES 数组处理上传的 PHP 脚本中。你调用 move_upload_file,如名称所描述的那样,它将最近上传的照片文件从它的临时位置移动到你想保存它的那个文件夹中。

你重命名文件,遵从 [数据库 ID].jpg 格式,使得文件名总是对应于数据库文件记录的 ID。然后你调用俏皮的 thumb 方法来生成全尺寸照片的一个缩略图。

最后,假如所有进展顺利,你返回一个带有 “successful” 键的 JSON 响应。使 iPhone 应用知道所有进展都很好,照片成功上传,并保存到系统中。

从这里去哪里?

你拥有一个与服务器交互的应用,并且它相当酷,因此你可以随便玩一玩,发送数据,并可能调整服务器端响应。

但是,你离完成还远着呢!当你准备好时,本教程的第二部分在等着,你将开发所有 iPhone 很酷的功能:允许用户拍照,应用特效,发送到服务器。

与此同时,如果你有任何问题或意见,请加入下面的论坛讨论!

这篇文章由 iOS 教程组成员,一个拥有12年以上软件开发经验、一个独立的 iOS 开发者、并且是 Touch Code Magazine 创始人,Marin Todorov 所编写。

Marin Todorov

Marin Todorov is an independent iOS developer and publisher with background in various platforms and languages. He started developing on an Apple ][ more than 20 years ago and keeps rocking till today. Meanwhile he has worked in great companies like Monster and Native Instruments, has lived in 4 different countries, and (more recently) has worked on 10+ titles on the App Store.

Besides crafting code, Marin also enjoys teaching and training others. He sometimes speaks at iOS conferences.

Throughout his career he has authored or contributed to 7 great book titles, including "iOS7 by Tutorials", "iOS6 by Tutorials", "iOS Games by Tutorials", and O'Reilly's "Adobe AIR Cookbook". He maintains Touch Code Magazine, his tech blog about iOS development and also has a successful video course on game programming.

用户评论

0 Comment

Other Items of Interest

Ray 的每月简报

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    加载中 ... 加载中 ...

我们的书

Our Team

教程组

  • Adam Burkepile
  • Marcelo Fabri

... 55 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 1 total!

翻译团队

  • David Hidalgo
  • Takeichi Kanzaki Cabrera

... 38 total!

Subject Matter Experts

  • Richard Casey

... 4 total!