这篇文章是由 iOS 教程组成员,一个拥有12年以上软件开发经历、独立的iOS开发者、并且是 Touch Code Magazine 的创始人,Marin Todorov 所撰写的。
准备好继续创建你的照片分享 iPhone 应用了吗?
上一次,在本教程的第一部分中,你创建了 Web Service 的基础,而且添加了使用用户名/密码登录并上传文件的功能。
在本教程的第二部分也是最后部分中,你继续做很酷的事情——拍照、应用特效和上传文件到服务器。
带上一些像我的一样美味的早餐,让我们出发吧!
准备工作:Photo 页面
好的,是时候祭出 iPhone 摄像头,捕获动作,通过 API 提交结果到服务器。
在 Xcode 中打开教程的项目,看一看 Storyboard.storyboard。在 Photo 页面上你已经有的是一个你将显示照片预览的 UIImageView,一个用户可以输入的照片标题的 UITextField,和一个显示在屏幕菜单上的动作按钮。那就是照片应用全部所需的东西。
转到 PhotoScreen.m 并找到 btnActionTapped:。它是空的,所以添加代码来当动作按钮被点击时显示一个选项菜单:
|
首先你确保屏幕键盘不显示。既然只有一个文本域,所以只调用 resignFirstResponder 就足够了。
然后你显示一个包含用户可能执行的所有动作的动作表单:
- Take Photo —— 调用 iOS 标准拍照对话框。
- Effects! —— 你将从我同事 Jacob Gundersen 的 关于应用图片特效的教程 中借一些代码来对用户照片应用一个老照片风格的效果。
- Post Photo ——通过使用 API 调用,发送照片到服务器。
- Logout ——结束用户与服务器的会话。
你需要实现一个 UIActionSheet 委托方法来处理表单上不同按钮的点击事件。但是做这个之前,你需要建立一些方法来处理上述的每一个动作。在 @implementation 指令上方,添加那些方法的私有定义,像这样:
|
现在添加这段代码到文件末尾,来实现 UIActionSheet:
|
构建并运行项目,登录,点击在标签栏右端的动作按钮。菜单应该看起来像这样:
从实现第一个条目 “Take Photo” 开始。
成为一个抓拍的野兽
对于从没有与 iPhone 摄像头编程交互的你们来说,这事实上非常容易。有一个苹果标准视图控制器,你只需要实例化,创建,然后模态显示。无论用户何时拍照或取消流程,类的回调方法就被调用来处理动作。
在 PhotoScreen.m 的末尾添加 takePhoto 方法:
|
UIImagePickerController 是一个允许用户使用摄像头的视图控制器。正如一般的类一样,你创建一个新的实例。
接下来你创建 sourceType 属性——你可以指示对话框,用户是否应该实际上使用摄像头,还是从设备的相册库中挑选。在上述的代码中,我偷偷加入一些代码来检测这个应用是否在 iPhone 模拟器上运行,如果是,对话框仅仅访问相册库。(因为,你知道……模拟器上没有摄像头。)
将 editing 设置为 YES,允许用户在接受之前对照片做些简单的编辑操作。
最后,你显示拍照对话框作为一个模态视图,然后把控制权交到 Apple 手上。下次你收到用户的信息,它将在方法中处理来自拍照对话框的响应。
你将对用户拍摄的照片进行一些按比例缩小和裁剪的工作,因此你需要手动 import 几个 UIImage categories。滚动到文件顶部,在其他 import 语句的下面,添加这段代码:
|
现在实现两个 UIImagePickerControllerDelegate 方法。在文件末尾添加下面代码:
|
首先来看看 imagePickerController:didFinishPickingMediaWithInfo: 方法:
- 所有关于照片的信息通过 info dictionay 传递。因此你必须首先从这个 dictionary 获取照片。
- 然后,你调用 resizedImageWithContentMode:bounds:interpolationQuality: 来获取按比例缩小了的用户照片。(这个方法也解决了经常出现的图片方向错误的问题)
- 接下来你通过调用 croppedImage: 方法,来对已缩放了的图片(是一个矩形)进行裁剪,使之变成一个正方形格式。正方形图片在这些天非常流行,也是你的 app 所需要的最佳形式。:]
- 你在图片视图中显示已缩放和已裁剪过的图片。
- 最后,你隐藏拍照对话框。
如果用户取消了拍照进程,imagePickerControllerDidCancel: 将被调用。所有你实际上需要做的就是关闭拍照对话框。
就是这样!启动 app(无论是在设备上还是模拟器上)并试着拍摄一些照片。
注意:你只能在设备上使用摄像头。如果你在模拟器上测试,在你相册里需要至少一张图片来做任何事。另外,如果在你本地机器上有你的 Web 服务器,你不能通过在 API.m 中指定的 http://localhost URL 访问 Web 服务器。而是你必须更改 URL 以显示你机器的 IP 地址。
你可以简单地通过拖放图片到模拟器的方法,来添加新的照片到模拟器中。它将在移动 Safari 中显示图片。然后,简单地点击图片并保持住,直到你获得一个允许你保存图片到模拟器的相册中的 Action Sheet :]
及时送回我的早餐
本教程将教你实现仅仅一个特效,但是你肯定受到鼓舞,想阅读更多关于本话题的内容,和通过你自己来实现更多特效。
下面的代码得到在图片视图中的图片并对它应用一种老照片风格的效果。代码来自 Beginning Core Image 教程,所以如果你感兴趣,你可以在这里阅读更多有关的内容。
在 PhotoScreen.m 文件中的 takePhoto 下方添加下面的方法:
|
再次构建并运行项目来看看即将发生的一些很酷的事儿。拍照,点击动作按钮,选择 Effects! 相当酷,不是吗?谢谢你,Jacob!
身处云端
为了使这部分的应用程序运行,你不得不后退一会儿,完成 API 类。关于通过 API 处理文件上传,还记得我当时是怎么说的吗?但是你还没有实现呢。所以打开 API.m 并找到 commandWithParams:onCompletion:
很容易就认出将要添加代码的地方——源代码中有遗留的手动添加的注释。但是,你首先将对方法体做一些改善。我们开始吧!
在方法体的最开始部分,添加这几行代码:
|
这段代码检查 API 命令是否包含了一个 “file” 参数。如果是,就从参数字典中带出 file 参数并单独存储。这是基于这个事实:当其他所有参数被作为一个普通的 POST 请求变量发送时,照片内容将被作为一个请求的多部分的附件单独发送。
现在看看现在只有 “//TODO: attach file if needed” 注释的这块代码。用下面的代码替换注释:
|
代码相当简单:你只添加文件的二进制内容,请求变量的名称,附件的文件名(你将总是用 photo.jpg 这个名字传递)和一个 mime 类型到你将发送给服务器的请求当中。
这大概就是所有你需要添加来处理文件上传的部分!小菜一碟。
回到 PhotoScreen.m 并向类中添加一个新方法(当然是点击 Post Photo 时调用的)
|
既然 API 支持文件上传,你只需要传递参数给它:API 命令是 “upload”;“file” 参数是你传递的照片的 JPEG 表示;而照片名则是从用户接口的 text field 处获得。
现在你到达另一个复杂度:用户打开 Photo 页面之前你授权他们,但是喂!并不保证用户上传照片时总是被授权的。
也许 app 半天都待在后台,然后用户再次打开它,决定上传一张照片。或者也许还有其他情况。但是你不知道。
所以 “upload” 调用可能会因为比仅仅是网络通信错误更多的其他原因而导致失败——可能是因为用户会话过期所导致的。你将不得不用一个合理的方式处理它,提供一个良好的用户体验。
首先,添加下面的 import 语句到文件顶部:
|
接下来,在 uploadPhoto 完成块中,添加处理服务器响应的代码:
|
让我们看看:如果 result 没有一个 “error” 的键,你就假定调用是成功的。你显示给用户一个很好的警告,让他们知道操作已成功。
在 else 分支,你保存错误信息到 errorMsg 并通过警告来再次显示。然后你比较错误信息和 “Authorization required” 字符串(它就是当用户会话不存在时服务器将返回的内容),假如是那种情况,你就调用 segue 来显示 Login 页面。
那种情况将发生什么?用户拍摄的照片将在 UIImageView 中保持加载状态,名称将保持在 text field……并且假如用户通过 API 授权成功,Login 页面将会隐藏,用户也将有另一次机会来试着上传照片到服务器。相当酷吧!!!
你差不多已完成这个画面的所有功能。
使我避免纠结
最后(但不是最没用)用户应该总是能够登出。有两个步骤来使用户登出:
- 在 API 类中销毁 “user” 属性。
- 在服务器端销毁用户会话。
从 Obejective-C 代码开始。添加下面代码到 PhotoScreen.m:
|
你发送 “logout” 命令到服务器,一旦成功就销毁 iPhone 端的用户数据。既然用户不能在这个页面上做任何事情(未登录),你通过调用 login 页面的 segue 给他立即用别的账号登录的机会。
在服务器端还有一些工作要做。打开来自 web 工程的 index.php 文件,再添加一个 case 到 “switch” 语句。
|
现在转到 api.php 在末尾添加你从 index.php 调用的 logout() 方法:
|
很简单!所有保存在服务器上的每个用户数据被存储在 $_SESSION 数组当中。你通过保存一个空数组到 $_SESSION 的方式擦除数据,数据消失了!噗!你也调用 session_destroy() 来 101% 确保用户会话再也不存在。那就是所有你需要做的事情。
恭喜你!你已经就这类重型 Objective-C/PHP 来回交互方面走的很远了。但是嘿,荣耀在等着你!还有一点点要做的,app 就可以全部完工了!
启动 app 然后玩一玩——你值得玩一玩!拍照,应用效果,上传图片到服务器,甚至登出然后重新登录。太酷了!
但是,没有方法看到你已经保存到服务器上的图片。这一点儿也不好玩!
不要灰心!下一步,你将使用 app 的第一个页面来显示照片流。(有或许将必须首先实现上传功能,否则在流中会没有照片显示的!)
整天整夜地流
流功能的计划是显示所有用户上传的最后的 50 张照片。
还记得你当初是如何聪明地为上传的图片同时生成缩略图吗?现在刚好派上用场——在 stream 页面。你将只加载照片缩略图并在 Stream 页面显示列表。
但是对于计划已经足够了;动手吧!打开 index.php(最后一次)并添加最终的 case 到 switch 语句:
|
这儿有点奇怪,不是吗?为什么 API 的 stream 命令要带一个 “IdPhoto” 参数?
你将为两个不同的目的而使用同样的调用。如果没有参数,API 将返回最后的 50 张照片,如计划的那样。如果 IdPhoto 提供了,你将返回单张照片的数据(例如,当用户想看一张缩略图的全尺寸照片)。
转到 api.php 并添加 stream() 方法:
|
正如你看到的那样,你将去找最简单的解决方案。你检查 IdPhoto 参数是否等于 0,如果是,你只查询最后 50 张照片。否则你试图获取请求的照片。
你检查是否无错误(例如,大大的成功)然后发送来自数据库的结果到 iPhone app。很幸运,在 app 中显示照片并不那么难!
你的目标是建立一个照片缩略图列表然后在一个类似表格的布局中显示它们。当用户点击它们中的某一个,你将调用一个 segue 来打开缩略图的全尺寸版本。
你将开发一个新的自定义的缩略图视图,它将显示照片的缩略图,用户名,也将自动计算它的位置和对点击事件做出反应!页面的最终布局将看起来像这样:
在 Xcode 菜单, 选择 File/New/File…,然后选择 Objective-C class 模板。使新的类继承 UIButton(因为你想新的视图来处理点击)并命名为 PhotoView。打开 PhotoView.h 并用这段代码来替换里面所有的东西:
|
这是上面所做的工作:
- 首先,你需要一些联系人——既然照片是正方形的,你只定义缩略图的宽度。你有 90px 作为缩略图的宽度,因此你在页面上将有一个 3 列的布局(你有 3 列总共为 270px 的宽度,乘以列与列之间的 2 个空白总共为 20px)。
- 接下来,你为缩略图视图与你的控制器交互定义一个协议。你有一个控制器必须实现的方法:didSelectPhoto: 。无论用户何时点击一个缩略图,缩略图视图将让它的 delegate 知道它被点击了。然后控制器就可以打开全尺寸照片页面。待会儿你将使得 Stream 页面视图控制器符合这个协议。
- 最后,你定义类的接口。你需要一个属性来保持 delegate 的引用。既然 PhotoView 实例将被直接添加到视图控制器(将成为 delegate)的视图层次中,你使用 assign 属性。
你的类的自定义的初始化方法将带一个 index。这个 index 将被用来计算当缩略图出现时的行与列。它也将获取照片数据并发送请求到服务器来获取全尺寸的照片。
让我们来实现所有这些!
在 PhotoView.m 中有两三件事情要做:
|
现在你将绕会道,添加一个简短的方法到 API 类中。你要它通过获取你想要上传图片的 ID 来给你返回服务器上的图片的 URL。添加方法到 API 类的各自的接口和实现文件中去:
|
既然你上传所有图片到 “upload” 文件夹,方法将生成完整的 URL,包括服务器的主机和路径。同时取决于你是否想获取缩略图还是全尺寸格式,它会小心地在返回的 URL 中提供正确的文件名。
注意:本教程涉及了一个 Web 后台/ iPhone 客户端的基础内容。这些基础内容将加深你关于如何建模和设计你的产品的理解。但是我需要提及的是,创建一个现实生活的照片分享的应用程序需要更多的专业知识,特别是当它与文件存储相关的时候。
我不能在这里涉及更多详细内容,但是我确实想就关于与文件存储服务相关的可能出现的问题给你一些指点。
- 首先,使用数据库 ID 作为文件名对于运行良好的目的来说很好,但是对于生产环境你应该有一个不同的途径。如果你使用增长的数字并保存所有文件到同一个文件夹,从你服务器获取所有照片就相对容易多了(你也可能不需要那个功能)。
- 此外,存储大量照片到你服务器上有可能产生很多拥塞。如果你有全套的主机包(all-inclusive hosting packs)的一个那么你肯能不会关心那个,但是经验告诉我,我知道随着时间的推移,那些类型的提供商质量往往会参差不齐。你想要的一个大规模的照片分享 Web 服务是分布式 CDN,以保证为世界各地的用户提供良好的速度和质量。
- 存储文件的文件类型不可忽视。在一个单独的文件夹里包含 500,000 个文件并不是一个好主意——这使得管理内容变得异常困难(这种现象同样存在于文件存储在 CDN 上的类型)。你要做的是分配上传的文件到一个均衡的树形文件夹结构中,所以你就可以很容易地对一个单独的照片定位,并且文件夹的大小也易于管理。
- 你也需要检查正在上传的文件内容。保存用户文件到你的服务器上总是危险的。至少,你需要检查文件是否有一个有效的 JPG 格式头,因此你知道用户通过 API 上传不会造成损害。最佳方案就是在服务器端进行图片处理,因而保证了你不保持内容,因为它是发给你的。
根据上述说法,让我们回到你的 app!你仍然需要实现缩略图视图——只剩下的唯一方法就是自定义初始化方法。通过两个简单的步骤来添加。打开 PhotoView.m 并添加:
|
这段代码应该很容易理解——大部分都是跟 UI 相关。让我们简单的过一遍:
- 保存照片 ID 到 tag 属性以备以后使用。
- 保存基于照片的 index 计算行和列(例如,在列表中的第 7 张照片将在第 3 行第 1 列)。
- 然后计算行列位置计算视图的 frame,使用缩略图端的约束和列与列之间的空白。
- 然后添加一个显示上传照片的用户名的 UILabel。
你已经照顾了布局!现在继续添加功能到缩略图。添加下面代码到 “//step 2” 注释的位置:
|
首先,通过直接调用 delegate 上的 didSelectPhoto: 来处理点击(你在 PhotoView 中确实不需要其他额外的方法)。
然后抓取一个到共享的 API 实例的引用,获取传递到初始化方法的照片数据的 IdPhoto 值,最后调用urlForImageWithId:isThumb:(你不久前添加到 API 中的)来获取服务器上图片的 URL。
你终于准备好获取来自 Web 的图片并显示到视图上。AFNetworking 定义了一个自定义的 operation 来加载远程图片,那就是你将使用到的。你所做的就是提供一个包含图片 URL 的 NSURLRequest 和一个当图片获取成功后执行的 block。在 block 中,你创建了一个 UIImageView,加载获取到的图片,添加图片视图到你的 PhotoView 实例中并且……瞧!这就是所有的!
好吧……还不完全是。operation 准备好执行,但是还没有执行。在最后几行代码中,初始化一个新的 operation 队列并添加前面那个 operation 到队列中。现在才是所有的了!
自定义的缩略图相当赞,它自己为你做了所有的事情。我个人很喜欢拥有像这样一个手工的组件。剩下的就是显示这些漂亮的缩略图的某些到 Stream 页面!
打开 StreamScreen.m(在 Screens 文件夹中),在顶部单独的 #import 语句下方位置添加一些代码:
|
你在包含 API 类(当然!)、新的自定义的缩略图组件和显示全尺寸照片的类。
在类中你需要几个私有方法,因此在 import 语句下面也加上私有的 interface:
|
你不得不呼叫服务器,用户一打开 app 时立即显示缩略图,因此需要在 viewDidLoad 调用方法来做这个功能。在viewDidLoad 末尾添加:
|
接下来添加 refreshStream: 方法到文件末尾(但是在 @end 前面):
|
到现在,你应该对这段代码将做什么要绝对熟悉。发送 “stream” 命令到服务器 API,获取照片列表的 JSON 数据,并传递 JSON 到 showStream:。
接下来添加 showStream: 的实现部分:
|
- 首先删除在名为 “listView” 的 UIScrollView 中所有子视图。你需要做这个是因为当用户想刷新照片流时会调用同样的方法,因此在 scroll view 中可能已经有照片了。
- 对返回的照片记录使用一个 for 循环,来获取照片数据,然后用它来创建一个 PhotoView 实例。既然 PhotoView 为你照看好一切,你只需要设置视图控制器为 delegate 并添加缩略图作为一个子视图。
- 最后,你更新 scroll view 的高度。这相当容易因为你知道你总共有多少个缩略图,以及他们将占用多少行。你也需要将列表滚动到顶部(当新的照片出现时)。
好,最后点击一次!在你设置缩略图的 delegate 行应该会有一个小小的警告。是的,你得到它是因为视图控制器没有 conform to 必需的协议。快速转到 StreamScreen.h 并修补它:
|
很好,修改好了!嘿,另外有个好消息——Stream Screen 现在可以工作了!如果你已经上传了一些照片到服务器,运行项目,你应该看到缩略图出现在 app 的主页面上。祝贺你!你已经终于在漫长的旅程中跑通了!
点击缩略图看起来并不工作,不是吗?Nope… that was a premature celebration!
你需要添加 didSelectPhoto: 方法到视图控制器,在 StreamScreen.m。仅仅调用 segue 来显示全尺寸的照片。但是还有一点额外的工作来准备 segue,因此你还将需要一个 prepareForSegue:sender: 方法:
|
这里做了一个小把戏。当一个缩略图被选中,你获取它的保持了照片 ID 的 tag 属性,然后你将它包装成一个 number,你稍后依次给 segue 当做 sender 传递(很神气)。它仅仅是一个来发送参数到 prepareSegue 方法的快捷方式。
在 prepareForSegue,检查是否 segue 正是显示全尺寸照片的那个 segue(通过检查它的 identifier),如果是,传递 IdPhoto 到 StreamPhotoScreen,这个 segue 的目标页面。这就足够做到连接缩略图与全尺寸照片页面了。
只对接口做一个小小的调整——在左上角的刷新按钮点击时调用 btnRefreshTapped,但是方法主体是空的。刷新(啊哈!)所需要做的仅仅是调用 refreshStream。所以转到 btnRefreshTapped 并添加下列代码:
|
现在你可以处理显示流,和缩略图上的点击了。也注意到左上角的刷新按钮连接到 refreshStream,所以无论何时,只要用户想要,他们都可以刷新流。
所剩工作已不多了,只需一点耐心跟努力。但是一切都已包装好,所以不要担心!:]
假如你打开 StreamPhotoScreen.h,你会看到 IdPhoto 属性已经在恰当的位置,所以传递 Stream 页面的 ID 到全尺寸照片页面也已经在恰当的位置了。你需要做的只是呼叫服务器来获取全尺寸照片,然后只在图片视图加载全尺寸照片。
转到 StreamPhotoScreen.m 并做如下改变:
|
当视图被加载时,你以与在 Stream 页面相同的方式调用 API,但是你也传递想要的照片 ID 作为一个参数。如果你还记得的话,当这个参数准备好时,API 只返回这个特定文件数据。
当你获得一个来自 API 调用的响应时,你带出照片数据并显示照片标题到名为 lblTitle 的 UILabel (在你的 Storyboard 文件中创建的)中。
啊!到了最后一个步骤了:真正地加载照片到图片视图中。感谢 AFNetworking,使得它变得非常简单。AFNetworking 定义了一个 UIImageView 的分类,添加了一个之前已经使用过的方法,叫做 setImageWithURL:,它获取来自 Web 的远程图片并加载到 UIImageView 中。
现在所有都在恰当的位置。启动项目——看到流,点击照片查看细节。也拍拍照片,上传它们!这是在你创建杀手级的照片分享应用的路上的一个令人惊叹的开端。
从这里到哪里?
你开发项目的全部源代码——包括 Xcode 和 PHP 部分——可以在 这个归档文件找到。
构建服务器/客户端软件是一项全新的复杂度级别,尤其是在周末突然拿出这样的 iPhone 应用。但是你也看到了,一步一步从底层向上层构建并没有那么难。
你也可以做一些立即的步骤来扩展你的演示项目:
- 添加更多效果怎么样?通读 开始 Core Image 教程并观察添加更多效果。
- 拥有一个简单的基于功能的服务很有趣,但是假如你想在 Web 诱人的技术方面实时更新,你需要浏览拥有一个 JSON-RPC 服务。
- 如果照片流自动刷新的话会很棒,那样的话新照片来到时,用户可以看到它们。你也可以在服务器端玩一玩,只提供增量更新(也就是说,自从上次用户获取列表时新照片的数据)。
- 你也可以想更多关于如何扩展服务(很容添加调用,就像你已经做的那样)。你可以添加像对照片的评论、对照片表示喜欢或不喜欢,等等。
我希望本教程可以帮助你构建你自己的基于 Web Service 后台的 iPhone 应用程序,将提供给你许多关于如何扩展演示项目的点子,以及发布你的令人惊叹的照片分享应用到 App Store!