iPhone OS支持很多使移动计算的用户体验更具吸引力的特性。通过iPhone OS,应用程序可以访问诸如加速计和照相机这样的硬件特性,也可以访问像用户照片库这样的软件特性。本文的下面部分将描述这些特性,并向您展示如何将它们集成到您的应用程序中。
确定硬件支持是否存在
为iPhone OS设计的应用程序必须能够运行在具有不同硬件特性的多种设备上。虽然像加速计和Wi-Fi连网这样的特性在所有设备上都是支持的,但是一些设备不包含照相机或GPS硬件。如果您的应用程序要求设备具有这样的特性,应该在用户购买之前通知他们。对于那些不是必需、但如果存在就希望支持的特性,则必须在试图使用之前检测它们是否存在。
重要提示:如果应用程序运行的前提是某个特性一定要存在,则应该在应用程序的
Info.plist
文件中对
UIRequiredDeviceCapabilities
键进行相应的设置,以避免将需要某种特性的应用程序安装在不具有该特性的设备上。但是,如果您的应用程序在给定特性存在或不存在时都可以运行,则不应该包含这个键。更多有关如果配置该键的信息,请参见“信息属性列表”部分。
表8-1列出了确定某种硬件是否存在的方法。如果您的应用程序在缺少某个特性时可以工作,而在该特性存在时又可以加以利用,则应该使用这些技术。
特性 |
选项 |
---|---|
确定网络是否存在... |
使用Software Configuration框架的可达性(reachability)接口检测当前的网络连接。有关如何使用Software Configuration框架的例子请参见可达性部分。 |
确定静态照相机是否存在... |
使用 |
确定音频输入(麦克风)是否存在… |
在iPhone OS 3.0及之后的系统上,可以用 |
确定GPS硬件是否存在… |
在配置 |
确定特定的配件是否存在… |
使用External Accessory框架的类来寻找合适的附近对象,并进行连接。更多信息请参见“和配件进行通讯”部分。 |
和配件进行通讯
在iPhone OS 3.0及之后的系统上,External Accessory框架
(ExternalAccessory.framework
)提供了一种管道机制,使应用程序可以和iPhone或iPod touch设备的配件进行通讯。通过这种管道,应用程序开发者可以将配件级别的功能集成到自己的程序中。
请注意:下面部分将向您展示iPhone应用程序如何连接配件。如果您有兴趣成为iPhone或iPod touch配件的开发者,可以在http://developer.apple.com网站上找到相应的信息。
为了使用External Accessory框架的接口,您必须将
ExternalAccessory.framework
加入到Xcode工程,并连接到相应的目标中。此外,还需要在相应的源代码文件的顶部包含一个
#import <ExternalAccessory/ExternalAccessory.h>
语句,才能访问该框架的类和头文件。有关如何为工程添加框架的更多信息,请参见Xcode工程管理指南中的工程中的文件部分;有关External Accessory框架中类的一般信息,请参见External Accessory框架参考。
配件的基础
在和配件进行通讯之前,需要与配件的制造商紧密合作,理解配件提供的服务。制造商必须在配件的硬件中加入显式的支持,才能和iPhone OS进行通讯。作为这种支持的一部分,配件必须支持至少一种命令协议,也就是支持一种定制的通讯模式,使配件和应用程序之间可以进行数据传输。苹果并不维护一个协议的注册表,支持何种协议及是否使用其他制造商支持的定制或标准协议是由制造商自行决定的。
作为和配件制造商通讯的一部分,您必须找出给定的配件支持什么协议。为了避免名字空间发生冲突,协议的名称由反向的DNS字符串来指定,形式是
com.apple.myProtocol
。这使得每个配件制造商都可以根据自己的需要定义协议,以支持不同的配件产品线。
应用程序通过打开一个使用指定协议的会话来和配件进行通讯。打开会话的方法是创建一个
EASession
类的实例,该类中包含
NSInputStream
和
NSOutputStream
对象,可以和配件进行通讯。通过这些流对象,应用程序可以向配件发送未经加工的数据包,以及接收来自配件的类似数据包。因此,您必须按照期望的协议来理解每个数据包的格式。
声明应用程序支持的协议
能够和配件通讯的应用程序应该在其
Info.plist
文件中声明支持的协议,使系统知道在相应的配件接入时,该应用程序可以被启动。如果当前没有应用程序可以支持接入的配件,系统可以选择启动App Store并指向支持该设备的应用程序。
为了声明支持的协议,您必须在应用程序的
Info.plist
文件中包含
UISupportedExternalAccessoryProtocols
键。该键包含一个字符串数组,用于标识应用程序支持的通讯协议。您的应用程序可以在这个列表中以任意顺序包含任意数量的协议。系统并不使用这个列表来确定应用程序应该选择哪个协议,而只是用它来确定应用程序是否能够和相应的配件进行通讯。您的代码需要在开始和配件进行对话时选择适当的通讯协议。
在运行时连接配件
在配件接入系统并做好通讯准备之前,通过External Accessory框架无法看到配件。当配件变为可见时,您的应用程序就可以获取相应的配件对象,然后用其支持的一或多个协议打开会话。
共享的
EAAccessoryManager
对象为应用程序寻找与之通讯的配件提供主入口点。该类包含一个已经接入的配件对象的数组,您可以对其进行枚举,看看是否存在应用程序支持的配件。
EAAccessory
对象中的绝大多数信息(比如名称、制造商、和型号信息)都只是用于显示。如果您要确定应用程序是否可以连接一个配件,必须看配件的协议,确认应用程序是否支持其中的某个协议。
请注意:多个配件对象支持同一协议是可能的。如果发生这种情况,您的代码必须负责选择使用哪个配件对象。
对于给定的配件对象,每次只能有一个指定协议的会话。
EAAccessory
对象的
protocolStrings
属性包含一个字典,字典的键是配件支持的协议。如果您试图用一个已经在使用的协议创建会话,External Accessory框架就会产生错误。
程序清单8-1展示了如何检查接入配件的列表并从中取得应用程序支持的第一个配件。它为指定的协议创建一个会话,并对会话的输入和输出流进行配置。在这个方法返回会话对象时,已经完成和配件的连接,并可以开始发送和接收数据了。
程序清单8-1 创建和配件的通讯会话
- (EASession *)openSessionForProtocol:(NSString *)protocolString
|
{
|
NSArray *accessories = [[EAAccessoryManager sharedAccessoryManager]
|
connectedAccessories];
|
EAAccessory *accessory = nil;
|
EASession *session = nil;
|
|
for (EAAccessory *obj in accessories)
|
{
|
if ([[obj protocolStrings] containsObject:protocolString])
|
{
|
accessory = obj;
|
break;
|
}
|
}
|
|
if (accessory)
|
{
|
session = [[EASession alloc] initWithAccessory:accessory
|
forProtocol:protocolString];
|
if (session)
|
{
|
[[session inputStream] setDelegate:self];
|
[[session inputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop]
|
forMode:NSDefaultRunLoopMode];
|
[[session inputStream] open];
|
[[session outputStream] setDelegate:self];
|
[[session outputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop]
|
forMode:NSDefaultRunLoopMode];
|
[[session outputStream] open];
|
[session autorelease];
|
}
|
}
|
|
return session;
|
}
|
在配置好输入输出流之后,最好一步就是处理和流相关的数据了。程序清单8-2展示了在委托方法中处理流事件的基本代码结构。清单中的方法可以响应来自配件输入输出流的事件。当配件向应用程序发送数据时,事件发生表示有数据可供读取;类似地,当配件准备好接收应用程序数据时,也通过事件来表示(当然,您并不一定要等到这个事件发生才向流写出数据,应用程序也可以调用流的
hasBytesAvailable
方法来确认配件是否还能够接收数据)。有关流及如何处理流事件的更多信息,请参见Cocoa流编程指南。
程序清单8-2 处理流事件
// Handle communications from the streams.
|
- (void)stream:(NSStream*)theStream handleEvent:(NSStreamEvent)streamEvent
|
{
|
switch (streamEvent)
|
{
|
case NSStreamHasBytesAvailable:
|
// Process the incoming stream data.
|
break;
|
|
case NSStreamEventHasSpaceAvailable:
|
// Send the next queued command.
|
break;
|
|
default:
|
break;
|
}
|
|
}
|
监控与配件有关的事件
当配件接入或断开时,External Accessory框架都可以发送通告。但是这些通告并不自动发送,如果您的应用程序感兴趣,必须调用EAAccessoryManager类的registerForLocalNotifications方法来显式请求。当配件接入、认证、并准备好和应用程序进行交互时,框架可以发出一个
EAAccessoryDidConnectNotification
通告;而当配件断开时,框架则可以发送一个
EAAccessoryDidDisconnectNotification
通告。您可以通过缺省的
NSNotificationCenter
来注册接收这些通告。两种通告都包含受影响的配件的信息。
除了通过缺省的通告中心接收通告之外,当前正在和配件进行交互的应用程序可以为相应的
EAAccessory
对象分配一个委托,使它在发生变化的时候得到通知。委托对象必须遵循
EAAccessoryDelegate
协议,该协议目前包含名为
accessoryDidDisconnect:
的可选方法,您可以通过这个方法来接收配件断开通告,而不需要事先配置通告观察者。
有关如何注册接收通告的更多信息,请参见Cocoa通告编程主题。
访问加速计事件
加速计以时间为轴,测量速度沿着给定线性路径发生的变化。每个iPhone和iPod touch都包含三个加速计,分别负责设备的三个轴向。这种加速计的组合使得我们可以检测设备在任意方向上的运动。您可以用这些数据来跟踪设备突然发生的运动,以及当前相对于重力的方向。
请注意:在iPhone OS 3.0及之后的系统,如果您希望检测特定类型的运动,比如摇摆设备,应该考虑通过运动事件来进行,而不是使用加速计的接口。运动事件为检测特定类型的加速计运动提供一致的接口,更多的细节请参见“运动事件”部分。
每个应用程序都可以通过
UIAccelerometer
的单件
对象来接收加速计数据。您可以通过
UIAccelerometer
的
sharedAccelerometer
类方法来取得该类的实例。之后,您就可以设置加速计数据更新的间隔时间及负责取得数据的自定义委托。数据更新的间隔时间的最小值是10毫秒,对应于100Hz的刷新频率。对于大多数应用程序来说,可以使用更大的时间间隔。您一旦设置了委托对象,加速计就会开始发送数据。而委托对象也会在您请求的时间间隔之后收到数据。
程序清单8-3展示了配置加速计的基本步骤。在这个例子中,更新频率设置为50Hz,对应于20毫秒的时间间隔。
myDelegateObject
是您定义的定制对象,必须支持
UIAccelerometerDelegate
协议,该协议定义了接收加速计数据的方法。
程序清单8-3 配置加速计
#define kAccelerometerFrequency 50 //Hz
|
-(void)configureAccelerometer
|
{
|
UIAccelerometer* theAccelerometer = [UIAccelerometer sharedAccelerometer];
|
theAccelerometer.updateInterval = 1 / kAccelerometerFrequency;
|
|
theAccelerometer.delegate = self;
|
// Delegate events begin immediately.
|
}
|
全局共享的加速计会以固定频率调用委托对象的
accelerometer:didAccelerate:
方法,通过它传送事件数据,如清单8-4所示。在这个方法中,您可以根据自己的需要处理加速计数据。一般地说,我们推荐您使用一些过滤器来分离您感兴趣的数据成分。
程序清单8-4 接收加速计事件
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
|
{
|
UIAccelerationValue x, y, z;
|
x = acceleration.x;
|
y = acceleration.y;
|
z = acceleration.z;
|
|
// Do something with the values.
|
}
|
将全局共享的
UIAccelerometer
对象的委托设置为
nil
,就可以停止加速计事件的递送。将委托对象设置为
nil
的操作会向系统发出通知,使其在需要的时候关闭加速计硬件,从而节省电池的寿命。
在委托方法中收到的加速计数据代表的是来自加速计硬件的实时数据。即使设备完全处于休息状态,加速计硬件报告的数据也可能产生轻微的波动。使用这些数据时,务必通过取平均值或对收到的数据进行调整的方法,来平抑这种波动。作为例子,Bubble Level示例程序提供了一些控制,可以根据已知的表面调整当前的角度,后续读取的数据则是相对于调整后的角度进行调整。如果您的代码需要类似级别的精度,也应该在程序界面中包含一些调整的选项。
选择恰当的更新频率
在配置加速计事件的更新频率时,最好既能满足应用程序的需求,又能使事件发送次数最少。需要系统以每秒100次的频率发送加速计事件的应用程序是很少的。使用较低的频率可以避免应用程序过于繁忙,从而提高电池的寿命。表8-2列出了一些典型的更新频率,以及在该频率下产生的加速计数据适合哪些应用场合。
事件频率(Hz) |
用途 |
---|---|
10–20 |
适合用于确定代表设备当前方向的向量。 |
30–60 |
适合用于游戏和使用加速计进行实时输入的应用程序。 |
70–100 |
适合用于需要检测设备高频运动的应用程序,比如检测用户快速触击或摆动设备。 |
从加速计数据中分离重力成分
如果您希望通过加速计数据来检测设备的当前方向,就需要将数据中源于重力的部分从源于设备运动的部分中分离开来。为此,您可以使用低通滤波器来减少加速计数据中剧烈变化部分的权重,这样过滤之后的数据更能反映由重力产生的较为稳定的因素。
程序清单8-5展示了一个低通滤波器的简化版本。清单中的代码使用一个低通滤波因子生成一个由当前的滤波前数据的10%和前一个滤波后数据的90%组成的值。前一个加速计数值存储在类的
accelX
、
accelY
、和
accelZ
成员变量中。由于加速计数据以固定的频率进入您的应用程序,所以这些数值会很快稳定下来,但过滤后的数据对突然而短暂的运动响应缓慢。
程序清单8-5 从加速计数据中分离出重力的效果
#define kFilteringFactor 0.1
|
|
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
|
// Use a basic low-pass filter to keep only the gravity component of each axis.
|
accelX = (acceleration.x * kFilteringFactor) + (accelX * (1.0 - kFilteringFactor));
|
accelY = (acceleration.y * kFilteringFactor) + (accelY * (1.0 - kFilteringFactor));
|
accelZ = (acceleration.z * kFilteringFactor) + (accelZ * (1.0 - kFilteringFactor));
|
|
// Use the acceleration data.
|
}
|
从加速计数据中分离实时运动成分
如果您希望通过加速计数据检测设备的实时运动,则需要将突然发生的运动变化从稳定的重力效果中分离出来。您可以通过高通滤波器来实现这个目的。
程序清单8-6展示了一个简化版的高通滤波器算法。从前一个事件得到的加速计数值存储在类的
accelX
、
accelY
、和
accelZ
成员变量中。清单中的代码首先计算低通滤波器的值,然后从当前加速计数据中减去该值,得到仅包含实时运动成分的数据。
程序清单8-6 从加速计数据中分离出实时运动成分
#define kFilteringFactor 0.1
|
|
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
|
// Subtract the low-pass value from the current value to get a simplified high-pass filter
|
accelX = acceleration.x - ( (acceleration.x * kFilteringFactor) + (accelX * (1.0 - kFilteringFactor)) );
|
accelY = acceleration.y - ( (acceleration.y * kFilteringFactor) + (accelY * (1.0 - kFilteringFactor)) );
|
accelZ = acceleration.z - ( (acceleration.z * kFilteringFactor) + (accelZ * (1.0 - kFilteringFactor)) );
|
|
// Use the acceleration data.
|
}
|
取得当前设备的方向
如果您需要知道的是设备的大体方向,而不是精确的方向向量,则应该通过
UIDevice
类的相关方法来取得。使用
UIDevice
接口比较简单,不需要自行计算方向向量。
在取得当前方向之前,您必须调用
beginGeneratingDeviceOrientationNotifications
方法,使
UIDevice
类开始产生设备方向通告。对该方法的调用会打开加速计硬件(否则为了省电,加速计硬件处于关闭状态)。
在打开方向通告的很短时间后,您就可以从
UIDevice
对象
orientation
属性声明得到当前的方向。您也可以通过注册接收
UIDeviceOrientationDidChangeNotification
通告来得到方向信息,当设备的大体方向发生改变时,系统就会发出该通告。设备的方向由
UIDeviceOrientation
常量来描述,它可以指示设备处于景观模式还是肖像模式,以及设备的正面是朝上还是朝下。这些常量指示的是设备的物理方向,不一定和应用程序的用户界面相对应。
当您不再需要设备的方向信息时,应该调用
UIDevice
的
endGeneratingDeviceOrientationNotifications
方法来关闭方向通告,使系统有机会关闭加速计硬件,如果其它地方也不使用的话。
使用位置和方向服务
Core Location框架为定位用户当前位置和方向(Heading)提供支持,它负责从相应的设备硬件收集信息,并以异步的方式报告给您的应用程序。数据是否可用取决于设备的类型以及所需的硬件当前是否打开,如果设备处于飞行模式,则某些硬件可能不可用。
在使用Core Location框架的接口之前,必须将
CoreLocation.framework
加入到您的Xcode工程中,并在相关的目标中进行连接。要访问该框架的类和头文件,还需要在相应的源代码文件的顶部包含
#import <CoreLocation/CoreLocation.h>
语句。更多有关如何在工程中加入框架的信息,请参见Xcode工程管理指南文档中的工程中的文件部分。
有关Core Location框架的类的一般性信息请参见Core Location框架参考。
取得用户的当前位置
Core Location框架使您可以定位设备的当前位置,并将这个信息应用到程序中。该框架利用设备内置的硬件,在已有信号的基础上通过三角测量得到固定位置,然后将它报告给您的代码。在接收到新的或更为精确的信号时,该框架还对位置信息进行更新。
如果您确实需要使用Core Location框架,则务必控制在最小程度,且正确地配置位置服务。收集位置数据需要给主板上的接收装置上电,并向基站、Wi-Fi热点、或者GPS卫星查询,这个过程可能要花几秒钟的时间。此外,请求更高精度的位置数据可能需要让接收装置更长时间地处于打开状态,而长时间地打开这个硬件会耗尽设备的电池。如果位置信息不是频繁变化,通常可以先取得初始位置,然后每隔一段时间请求一次更新就可以了。如果您确实需要定期更新位置信息,也可以为位置服务设置一个最小的距离阈值,从而最小化代码必须处理的位置更新。
取得用户当前位置首先要创建
CLLocationManager
类的实例,并用期望的精度和阈值参数进行配置。开始接收通告则需要为该对象分配一个委托,然后调用
startUpdatingLocation
方法来确定用户当前位置。当新的位置数据到来时,位置管理器会通知它的委托对象。如果位置更新通告已经发送完成,您也可以直接从
CLLocationManager
对象获取最新的位置数据,而不需要等待新的事件。
程序清单8-7展示了定制的
startUpdates
方法和
locationManager:didUpdateToLocation:fromLocation:
委托方法的的一个实现。
startUpdates
方法创建一个新的位置管理器对象(如果尚未存在的话),并用它启动位置更新事件的递送(在这个实例中,
locationManager
变量是
MyLocationGetter
类中声明的成员变量,该类遵循
CLLocationManagerDelegate
协议。事件处理方法通过事件的时间戳来确定其延迟的程度,对于太过时的事件,该方法会直接忽略,并等待更为实时的事件。在得到足够实时的数据后,即关闭位置服务。
程序清单8-7 发起和处理位置更新事件
#import <CoreLocation/CoreLocation.h>
|
|
@implementation MyLocationGetter
|
- (void)startUpdates
|
{
|
// Create the location manager if this object does not
|
// already have one.
|
if (nil == locationManager)
|
locationManager = [[CLLocationManager alloc] init];
|
|
locationManager.delegate = self;
|
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
|
|
// Set a movement threshold for new events
|
locationManager.distanceFilter = 500;
|
|
[locationManager startUpdatingLocation];
|
}
|
|
|
// Delegate method from the CLLocationManagerDelegate protocol.
|
- (void)locationManager:(CLLocationManager *)manager
|
didUpdateToLocation:(CLLocation *)newLocation
|
fromLocation:(CLLocation *)oldLocation
|
{
|
// If it's a relatively recent event, turn off updates to save power
|
NSDate* eventDate = newLocation.timestamp;
|
NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
|
if (abs(howRecent) < 5.0)
|
{
|
[manager stopUpdatingLocation];
|
|
printf("latitude %+.6f, longitude %+.6f\n",
|
newLocation.coordinate.latitude,
|
newLocation.coordinate.longitude);
|
}
|
// else skip the event and process the next one.
|
}
|
@end
|
对时间戳进行检查是推荐的做法,因为位置服务通常会立即返回最后缓存的位置事件。得到一个大致的固定位置可能要花几秒钟的时间,更新之前的数据只是反映最后一次得到的数据。您也可以通过精度来确定是否希望接收位置事件。位置服务在收到精度更高的数据时,可能返回额外的事件,事件中的精度值也会反映相应的精度变化。
请注意:Core Location框架在位置请求的一开始(而不是请求返回的时候)记录时间戳。由于Core Location使用几个不同的技术来取得固定位置,位置请求返回的顺序有时可能和时间戳指示的顺序不同。这样,新事件的时间戳有时会比之前的事件还要老一点,这是正常的。Core Location框架致力于提高每个新事件的位置精度,而不考虑时间戳的值。
获取与方向有关的事件
Core Location框架支持两种获取方向信息的方法。包含GPS硬件的设备可以提供当前移动方向的大致信息,该信息和经纬度数据通过同一个位置事件进行传递。包含磁力计的设备可以通过方向对象提供更为精确的方向信息,方向对象是
CLHeading
类的实例。
通过GPS硬件取得大致方向的过程和“取得用户的当前位置”部分的描述是一样的,框架会向您的应用程序委托传递一个
CLLocation
对象,对象中的
course
和
speed
属性声明包含相关的信息。这个接口适用于需要跟踪用户移动的大多数应用程序,比如实现汽车导航系统的导航程序。对于基于指南针或者可能需要了解用户静止时朝向的应用程序,可以请求位置管理器提供方向对象。
您的程序必须运行在包含磁力计的设备上才能接收方向对象。磁力计可以测量地球散发的磁场,进而确定设备的准确方向。虽然磁力计可能受到局部磁场(比如扬声器的永磁铁、马达、以及其它类型电子设备发出的磁场)的影响,但是Core Location框架具有足够的智能,可以过滤很多局部磁场的影响,确保方向对象包含有用的数据。
请注意:如果路线或方向信息对于您的应用程序的必须的,则应该在程序的
Info.plist
文件中正确地包含
UIRequiredDeviceCapabilities
键。这个键用于指定应用程序正常工作需要具备的设备特性,您可以用它来指定设备必须具有GPS和磁力计硬件。更多有关这个键值设置的信息请参见“信息属性列表”部分。
为了接收方向事件,您需要创建一个
CLLocationManager
对象,为其分配一个委托对象,并调用其
startUpdatingHeading
方法,如程序清单8-8所示。然而,在请求方向事件之前,应该检查一下位置管理器的
headingAvailable
属性,确保相应的硬件是存在的。如果该硬件不存在,应用程序应该回退到通过位置事件获取路线信息的代码路径。
程序清单8-8 发起方向事件的传送
CLLocationManager* locManager = [[CLLocationManager alloc] init];
|
if (locManager.headingAvailable)
|
{
|
locManager.delegate = myDelegateObject; // Assign your custom delegate object
|
locManager.headingFilter = 5;
|
[locManager startUpdatingHeading];
|
}
|
else
|
// Use location events instead
|
您赋值给delegate属性的对象必须遵循
CLLocationManagerDelegate
协议。当一个新的方向事件到来时,位置管理器会调用
locationManager:didUpdateHeading:
方法,将事件传递给您的应用程序。一旦收到新的事件,应用程序应该检查
headingAccuracy
属性,确保刚收到的数据是有效的,具体做法如清单8-9。
程序清单8-9 处理方向事件
- (void)locationManager:(CLLocationManager*)manager didUpdateHeading:(CLHeading*)newHeading
|
{
|
// If the accuracy is valid, go ahead and process the event.
|
if (newHeading.headingAccuracy > 0)
|
{
|
CLLocationDirection theHeading = newHeading.magneticHeading;
|
|
// Do something with the event data.
|
}
|
}
|
CLHeading
对象的
magneticHeading
属性包含主方向数据,且该数据一直存在。这个属性给出了相对于磁北极的方向数据,磁北极和北极不在同一个位置上。如果您希望得到相对于北极(也称为地理北极)的方向数据,则必须在
startUpdatingHeading
之前调用
startUpdatingLocation
方法来启动位置更新,然通过
CLHeading
对象的
trueHeading
属性取得相对于地理北极的方向。
显示地图和注解
iPhone OS 3.0引入了Map Kit框架。通过这个框架可以在应用程序的窗口中嵌入一个全功能的地图界面。Maps程序中的很多常见功能都包含在这个框架提供的地图支持中,您可以通过它来显示标准的街道地图、卫星图像,或两者的组合;还可以通过代码来缩放和移动地图。该框架还自动支持触摸事件,用户可以用手指缩放或移动地图。您还可以在地图中加入自己定制的注释信息,以及用框架提供的反向地理编码功能寻找和地图坐标关联的地址。
在使用Map Kit框架的功能之前,必须将
MapKit.framework
加入到Xcode工程中,并且在相关的目标中加以连接;在访问框架的类和头文件之前,需要在相应的源代码文件的顶部加入
#import <MapKit/MapKit.h>
语句。有关如何将框架加入工程的更多信息,请参见Xcode工程管理指南中的工程中的文件部分;有关Map Kit框架类的一般性信息,则请参见MapKit框架参考。
重要提示:Map Kit框架使用Google的服务来提供地图数据。框架及其相关接口的使用必须遵守Google Maps/Google Earth API的服务条款,具体条款信息位于http://code.google.com/apis/maps/iphone/terms.html。
在用户界面中加入地图视图
为应用程序加入地图之前,需要在应用程序的视图层次中嵌入一个
MKMapView
类的实例,该类为地图信息的显示和用户交互提供支持。您可以通过代码来为该类创建实例,并通过
initWithFrame:
方法来对其进行初始化,或者用Interface Builder将它加入到nib文件中。
地图视图也是个视图,因此您可以通过它的
frame
属性声明随意调整它的位置和尺寸。虽然地图视图本身没有提供任何控件,但是您可以在它的上面放置工具条或其它视图,使用户可以和地图内容进行交互。您在地图视图中加入的所有子视图的位置是不变的,不会随着地图内容的滚动而滚动。如果您希望在地图上加入定制的内容,并使它们跟着地图滚动,则必须创建注解,具体描述请参见“显示注解”部分。
MKMapView
类有很多属性,可以在显示之前进行配置,其中最重要的是
region
属性,负责定义最初显示的地图部分及如何缩放和移动地图内容。
缩放和移动地图内容
MKMapView
类的
region
属性控制着当前显示的地图部分。当您希望缩放和移动地图时,需要做的只是正确改变这个属性的值。这个属性包含一个
MKCoordinateRegion
类型的结构,其定义如下:
typedef struct {
|
CLLocationCoordinate2D center;
|
MKCoordinateSpan span;
|
} MKCoordinateRegion;
|
改变center域可以将地图移动到新的位置;而改变span域的值则可以实现缩放。这些域的值需要用地图坐标来指定,地图坐标用度、分、和秒来度量。对于span域,您需要通过经纬度距离来指定它的值。虽然纬度距离相对固定,每度大约111公里,但是经度距离却是随着纬度的变化而变化的。在赤道上,经度距离大约每度111公里;而在地球的极点上,这个值则接近于零。当然,您总是可以通过
MKCoordinateRegionMakeWithDistance
函数来创建基于公里值(而不是度数)的区域。
如果您希望在更新地图时不显示过程动画,可以直接修改
region
或
centerCoordinate
属性的值;如果需要动画过程,则必须使用
setRegion:animated:
或
setCenterCoordinate:animated:
方法。
setCenterCoordinate:animated:
方法可以移动地图,且避免在无意中触发缩放,而
setRegion:animated:
方法则可以同时缩放和移动地图。举例来说,如果您要使地图向左移动,移动距离为当前宽度的一半,则可以通过下面的代码找到地图左边界的坐标,然后将它用于中心点的设置,如下所示:
CLLocationCoordinate2D mapCenter = myMapView.centerCoordinate;
|
mapCenter = [myMapView convertPoint:
|
CGPointMake(1, (myMapView.frame.size.height/2.0))
|
toCoordinateFromView:myMapView];
|
[myMapView setCenterCoordinate:mapCenter animated:YES];
|
缩放地图则应该修改span属性的值,而不是中心坐标。减少span属性值可以使视图缩小;相反,增加该属性值可以使视图放大。换句话说,如果当前的span值是一度,将它指定为两度会使地图跨度放大两倍:
MKCoordinateRegion theRegion = myMapView.region;
|
|
// Zoom out
|
theRegion.span.longitudeDelta *= 2.0;
|
theRegion.span.latitudeDelta *= 2.0;
|
[myMapView setRegion:theRegion animated:YES];
|
显示用户的当前位置
Map Kit框架内置支持将用户的当前位置显示在地图上,具体做法是将地图视图对象的
showsUserLocation
属性值设置为
YES
就可以了。进行这个设置会使地图视图通过Core Location框架找到用户位置,并在地图上加入类型为
MKUserLocation
的注解。
在地图上加入
MKUserLocation
注解对象的事件会通过委托对象进行报告,这和定制注解的报告方式是一样的。如果您希望在用户位置上关联一个定制的注解视图,应该在委托对象的
mapView:viewForAnnotation:
方法中返回该视图。如果您希望使用缺省的注解视图,则应该在该方法中返回
nil
。
坐标和像素之间的转换
您通常通过经纬度值来指定地图上的点,但有些时候也需要在经纬度值和地图视图对象中的像素之间进行转换。举例来说,如果您允许用户在地图表面拖动注解,定制注解视图的事件处理器代码就需要将边框坐标转换为地图坐标,以便更新关联的注解对象。
MKMapView
类中几个例程,用于在地图坐标和地图视图对象的本地坐标系统之间进行转换,这些例包括:
convertCoordinate:toPointToView:
convertPoint:toCoordinateFromView:
有关如何处理定制注解事件的更多信息,请参见“处理注解视图中的事件”部分。
显示注解
注解是您定义并放置在地图上面的信息片段。Map Kit框架将注解实现为两个部分,即注解对象和用于显示注解的视图。大多数情况下,您需要负责提供这些定制对象,但框架也提供一些标准的注解和视图供您使用。
在地图视图上显示注解需要两个步骤:
创建注解对象并将它加入到地图视图中。
在自己的委托对象中实现
mapView:viewForAnnotation:
方法,并在该方法中创建相应的注解视图。
注解对象是指遵循
MKAnnotation
协议的任何对象。通常情况下,注解对象是相对小的数据对象,存储注解的坐标及相关信息,比如注解的名称。注解是通过协议来定义的,因此应用程序中的任何对象都可以成为注解对象。然而,在实践上,注解对象应该是轻量级的,因为在显式删除注解对象之前,地图视图会一直保存它们的引用。注意,同样的结论并不一定适用于注解视图。
在将注解显示在屏幕上时,地图视图负责确保注解对象具有相关联的注解视图,具体的方法是在注解坐标即将变为可见时调用其委托对象的
mapView:viewForAnnotation:
方法。但是,由于注解视图的量级通常总是比其对应的注解对象更重,所以地图对象尽可能不在内存中同时保存很多注解视图。为此,它实现了注解视图的回收机制。这个机制和表视图在滚动时回收表单元使用的机制相类似,即当一个注解视图移出屏幕时,地图视图就解除其与注解对象之间关联,将它放入重用队列。而在创建新的注解视图之前,委托的
mapView:viewForAnnotation:
方法应该总是调用地图对象的
dequeueReusableAnnotationViewWithIdentifier:
方法来检查重用队列中是否还有可用的视图对象。如果该方法返回一个正当的视图对象,您就可以对其进行再次初始化,并将它返回;否则,您再创建和返回一个新的视图对象。
添加和移除注解对象
您不应直接在地图上添加注解视图,而是应该添加注解对象,注解对象通常不是视图。注解对象可以是应用程序中遵循
MKAnnotation
协议的任何对象。注解对象中最重要的部分是它的
coordinate
属性声明,它是
MKAnnotation
协议必需实现的属性,用于为地图上的注解提供锚点。
往地图视图加入注解所需要的全部工作就是调用地图视图对象的
addAnnotation:
或
addAnnotations:
方法。何时往地图视图加入注解以及何时为加入的注解提供用户界面由您自己来决定。您可以提供一个工具条,由用户通过工具条上的命令来创建注解,或者也可以自行编码创建注解,注解信息可能来自本地或远程的数据库信息。
如果您的应用程序需要删除某个老的注解,则在删除之前,应该调用
removeAnnotation:
或
removeAnnotations:
方法将它从地图中移除。地图视图会显示它知道的所有注解,如果您不希望某些注解被显示在地图上,就需要显式地将它们删除。例如,如果您的应用程序允许用户对餐厅或本地风景点进行过滤,就需要删除与过滤条件不相匹配的所有注解。
定义注解视图
Map Kit框架提供了两个注解视图类:
MKAnnotationView
和
MKPinAnnotationView
。
MKAnnotationView
类是一个具体的视图,定义了所有注解视图的基本行为。
MKPinAnnotationView
类则是
MKAnnotationView
的子类,用于在关联的注解坐标点上显示一个标准的系统大头针图像。
您可以将
MKAnnotationView
类用于显示简单的注解,也可以从该类派生出子类,提供更多的交互行为。在直接使用该类时,您需要提供一个定制的图像,用于在地图上表示您希望显示的内容,并将它赋值给注解视图的
image
属性。如果您显示的内容不需要动态改变,而且不需要支持用户交互,则这种用法是非常合适的。但是,如果您需要支持动态内容和用户交互,则必须定义定制子类。
在一个定制的子类中,有两种方式可以描画动态内容:可以继续使用image属性来显示注解图像,这样或许需要设置一个定时器,负责定时改变当前的图像;也可以重载视图的
drawRect:
方法来显示描画您的内容,这种方法也需要设置一个定时器,以定时调用视图的
setNeedsDisplay
方法。
如果您通过
drawRect:
方法来描画内容,则必须记住:要在注解视图初始化后不久为其指定尺寸。注解视图的缺省初始化方法并不包含边框矩形参数,而是在初始化后通过您分配给
image
属性的图像来设置边框尺寸。如果您没有设置图像,就必须显式设置边框尺寸,您渲染的内容才会被显示。
有关如何在注解视图中支持用户交互的信息,请参见“处理注解视图的事件”部分;有关如何设置定时器的信息,则请参见Cocoa定时器编程主题。
创建注解视图
您应该总是在委托对象的
mapView:viewForAnnotation:
创建注解视图。在创建新视图之前,您应该总是调用
dequeueReusableAnnotationViewWithIdentifier:
方法来检查是否有可重用的视图,如果该方法返回非
nil
值,就应该将地图视图提供的注解分配给重用视图的
annotation
属性,并执行其它必要的配置,使视图处于期望的状态,然后将它返回;如果该方法返回
nil
,则应该创建并返回一个新的注解视图对象。
程序清单8-10是
mapView:viewForAnnotation:
方法的一个例子实现,展示了如何为定制注解对象提供大头针注解视图。如果队列中已经存在一个大头针注解视图,该方法就将它和相应的注解对象相关联;如果重用队列中没有视图,该方法则创建一个新的视图,对其基本属性进行配置,并为插图符号配置一个附加视图。
程序清单8-10 创建注解视图
- (MKAnnotationView *)mapView:(MKMapView *)mapView
|
viewForAnnotation:(id <MKAnnotation>)annotation
|
{
|
// If it's the user location, just return nil.
|
if ([annotation isKindOfClass:[MKUserLocation class]])
|
return nil;
|
|
// Handle any custom annotations.
|
if ([annotation isKindOfClass:[CustomPinAnnotation class]])
|
{
|
// Try to dequeue an existing pin view first.
|
MKPinAnnotationView* pinView = (MKPinAnnotationView*)[mapView
|
dequeueReusableAnnotationViewWithIdentifier:@"CustomPinAnnotation"];
|
|
if (!pinView)
|
{
|
// If an existing pin view was not available, create one
|
pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation
|
reuseIdentifier:@"CustomPinAnnotation"]
|
autorelease];
|
pinView.pinColor = MKPinAnnotationColorRed;
|
pinView.animatesDrop = YES;
|
pinView.canShowCallout = YES;
|
|
// Add a detail disclosure button to the callout.
|
UIButton* rightButton = [UIButton buttonWithType:
|
UIButtonTypeDetailDisclosure];
|
[rightButton addTarget:self action:@selector(myShowDetailsMethod:)
|
forControlEvents:UIControlEventTouchUpInside];
|
pinView.rightCalloutAccessoryView = rightButton;
|
}
|
else
|
pinView.annotation = annotation;
|
|
return pinView;
|
}
|
|
return nil;
|
}
|
处理注解视图中的事件
虽然注解视图位于地图内容上面的特殊层中,但它们也是功能完全的视图,能够接收触摸事件。您可以通过这些事件来实现用户和注解之间的交互。比如,您可以通过视图中的触摸事件来实现注解在地图表面的拖拽行为。
请注意:由于地图被显示在一个滚动界面上,所以,在用户触击定制视图和事件最终被派发之间往往有一个小的延迟。滚动视图可以利用这个延迟来确定触摸事件是否为某种滚动手势的一部分。
随后的一系列示例代码将向您展示如何实现一个支持用户拖动的注解视图。例子中的注解视图直接在注解坐标点上显示一个公牛眼图像,并包含一个定制的附加视图,用以显示目的地的详细信息。图8-1显示注解视图的一个实例以及其包含的气泡符号。
图8-1 公牛眼注解视图
程序清单8-11显示了
BullseyeAnnotationView
类的定义。类中包含一些正确跟踪视图移动需要的其它成员变量,以及一个指向地图视图本身的指针,指针的值是在
mapView:viewForAnnotation:
方法中设置的,该方法是创建或再次初始化注解视图的地方。在事件跟踪完成后,代码需要调整注解对象的地图坐标,这时需要用到地图视图对象。
程序清单8-11 BullseyeAnnotationView类
@interface BullseyeAnnotationView : MKAnnotationView
|
{
|
BOOL isMoving;
|
CGPoint startLocation;
|
CGPoint originalCenter;
|
|
MKMapView* map;
|
}
|
|
@property (assign,nonatomic) MKMapView* map;
|
|
- (id)initWithAnnotation:(id <MKAnnotation>)annotation;
|
|
@end
|
|
@implementation BullseyeAnnotationView
|
@synthesize map;
|
- (id)initWithAnnotation:(id <MKAnnotation>)annotation
|
{
|
self = [super initWithAnnotation:annotation
|
reuseIdentifier:@"BullseyeAnnotation"];
|
if (self)
|
{
|
UIImage* theImage = [UIImage imageNamed:@"bullseye32.png"];
|
if (!theImage)
|
return nil;
|
|
self.image = theImage;
|
self.canShowCallout = YES;
|
self.multipleTouchEnabled = NO;
|
map = nil;
|
|
UIButton* rightButton = [UIButton buttonWithType:
|
UIButtonTypeDetailDisclosure];
|
[rightButton addTarget:self action:@selector(myShowAnnotationAddress:)
|
forControlEvents:UIControlEventTouchUpInside];
|
self.rightCalloutAccessoryView = rightButton;
|
}
|
return self;
|
}
|
@end
|
当触击事件首次到达公牛眼视图时,该类的
touchesBegan:withEvent:
方法会记录事件的信息,作为初始信息,如清单8-12所示。
touchesMoved:withEvent:
方法会利用这些信息来调整视图位置。所有的位置信息都存储在父视图的坐标空间中。
程序清单8-12 跟踪视图的位置
@implementation BullseyeAnnotationView (TouchBeginMethods)
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
// The view is configured for single touches only.
|
UITouch* aTouch = [touches anyObject];
|
startLocation = [aTouch locationInView:[self superview]];
|
originalCenter = self.center;
|
|
[super touchesBegan:touches withEvent:event];
|
}
|
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
UITouch* aTouch = [touches anyObject];
|
CGPoint newLocation = [aTouch locationInView:[self superview]];
|
CGPoint newCenter;
|
|
// If the user's finger moved more than 5 pixels, begin the drag.
|
if ( (abs(newLocation.x - startLocation.x) > 5.0) ||
|
(abs(newLocation.y - startLocation.y) > 5.0) )
|
isMoving = YES;
|
|
// If dragging has begun, adjust the position of the view.
|
if (isMoving)
|
{
|
newCenter.x = originalCenter.x + (newLocation.x - startLocation.x);
|
newCenter.y = originalCenter.y + (newLocation.y - startLocation.y);
|
self.center = newCenter;
|
}
|
else // Let the parent class handle it.
|
[super touchesMoved:touches withEvent:event];
|
}
|
@end
|
当用户停止拖动注解视图时,您需要调整原有注解的坐标,确保视图位于新的位置。清单8-13显示了
BullseyeAnnotationView
类的
touchesEnded:withEvent:
方法,该方法通过地图成员变量将基于像素的点转化为地图坐标值。由于注解的
coordinate
属性通常是只读的,所以例子中的注解对象实现了一个名为
changeCoordinate
的定制方法,负责更新它在本地存储的值,而这个值可以通过
coordinate
属性取得。如果触摸事件由于某种原因被取消,
touchesCancelled:withEvent:
方法会使注解视图回到原来的位置。
程序清单8-13 处理最后的触摸事件
@implementation BullseyeAnnotationView (TouchEndMethods)
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
if (isMoving)
|
{
|
// Update the map coordinate to reflect the new position.
|
CGPoint newCenter = self.center;
|
BullseyeAnnotation* theAnnotation = self.annotation;
|
CLLocationCoordinate2D newCoordinate = [map convertPoint:newCenter
|
toCoordinateFromView:self.superview];
|
|
[theAnnotation changeCoordinate:newCoordinate];
|
|
// Clean up the state information.
|
startLocation = CGPointZero;
|
originalCenter = CGPointZero;
|
isMoving = NO;
|
}
|
else
|
[super touchesEnded:touches withEvent:event];
|
}
|
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
if (isMoving)
|
{
|
// Move the view back to its starting point.
|
self.center = originalCenter;
|
|
// Clean up the state information.
|
startLocation = CGPointZero;
|
originalCenter = CGPointZero;
|
isMoving = NO;
|
}
|
else
|
[super touchesCancelled:touches withEvent:event];
|
}
|
@end
|
通过反向地理编码器获取地标信息
Map Kit框架主要处理地图坐标值。地图坐标值由经度和纬度组成的,比较易于在代码中使用,但却不是用户最容易理解的描述方式。为使用户更加易于理解,您可以通过
MKReverseGeocoder
类来取得与地图坐标相关联的地标信息,比如街道地址、城市、州、和国家。
MKReverseGeocoder
类负责向潜在的地图服务查询指定地图坐标的信息。由于需要访问网络,反向地理编码器对象总是以异步的方式执行查询,并将结果返回给相关联的委托对象。委托对象必须遵循
MKReverseGeocoderDelegate
协议。
启动反向地理编码器的具体做法是首先创建一个
MKReverseGeocoder
类的实例,并将恰当的对象赋值给该实例的
delegate
属性,然后调用
start
方法。如果查询成功完成,您的委托就会收到带有一个
MKPlacemark
对象的查询结果。MKPlacemark对象本身也是注解对象—也就是说,它们采纳了
MKAnnotation
协议—因此如果您愿意的话,可以将它们添加到地图视图的注解列表中。
用照相机照相
通过UIKit的
UIImagePickerController
类可以访问设备的照相机。该类可以显示标准的系统界面,使用户可以通过现有的照相机拍照,以及对拍得的图像进行裁剪和尺寸调整;该类还可以用于从用户照片库中选取照片。
照相机界面是一个模式视图,由
UIImagePickerController
类来管理。具体使用时,您不应从代码中直接访问该视图,而是应该调用当前活动的视图控制器的
presentModalViewController:animated:
方法,并向其传入一个
UIImagePickerController
对象作为新的视图控制器。一旦被安装,选取控制器就会自动将照相机界面滑入屏幕,并一直保持活动,直到用户确认或取消图像选取的操作。如果用户做出选择,选取控制器会将这个事件通知其委托对象。
UIImagePickerController
类管理的界面可能并不适用于所有的设备。在显示照相机界面之前,您应该调用
UIImagePickerController
类的
isSourceTypeAvailable:
类方法,确认该界面是否可用。您应该总是尊重该方法的返回值,如果它返回
NO
,意味着当前设备没有照相机,或者照相机由于某种原因不可用;如果返回
YES
,则可以通过下面的步骤显示照相机界面:
创建一个新的
UIImagePickerController
对象。
为该对象分配一个委托对象。
大多数情况下,您可以让当前的视图控制器充当选取控制器的委托,但也可以根据自己的喜好使用完全不同的对象。委托对象必须遵循
UIImagePickerControllerDelegate
和
UINavigationControllerDelegate
协议。
请注意:如果您的委托不遵循
UINavigationControllerDelegate
协议,在编译时就会看到警告信息。然而,由于该协议的方法是可选的,所以不会对代码带来什么影响。如果要消除该警告信息,需要将
UINavigationControllerDelegate
协议加入委托类支持的协议列表中。
将选取控制器的类型设置为
UIImagePickerControllerSourceTypeCamera
。
为
allowsImageEditing
属性声明设置恰当的值,以便激活或者禁用图片编辑控制。这是个可选步骤。
调用当前视图控制器的
presentModalViewController:animated:
方法,显示选取控制器。
程序清单8-14的代码实现了上述步骤。在调用
presentModalViewController:animated
方法之后,选取控制器随即接管控制权,将照相机界面显示出来,并负责响应所有的用户交互,直到退出该界面。而从用户照片库中选取现有照片需要做的只是将选取控制器的
sourceType
属性的值改为
UIImagePickerControllerSourceTypePhotoLibrary
就可以了。
程序清单8-14 显示照相界面
-(BOOL)startCameraPickerFromViewController:(UIViewController*)controller usingDelegate:(id<UIImagePickerControllerDelegate>)delegateObject
|
{
|
if ( (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
|
|| (delegateObject == nil) || (controller == nil))
|
return NO;
|
|
UIImagePickerController* picker = [[UIImagePickerController alloc] init];
|
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
|
picker.delegate = delegateObject;
|
picker.allowsImageEditing = YES;
|
|
// Picker is displayed asynchronously.
|
[controller presentModalViewController:picker animated:YES];
|
return YES;
|
}
|
当用户触击相应的按键关闭照相机界面时,
UIImagePickerController
会将用户的动作通知委托对象,但并不直接实施关闭操作。选取器界面的关闭由委托对象负责(您的应用程序还必须负责在不需要选取器对象时将它释放,这个工作也可以在委托方法中进行)。由于这个原因,委托对象实际上应该是将选取器显示出来的视图控制器对象。一旦收到委托消息,视图控制器会调用其
dismissModalViewControllerAnimated:
方法来关闭照相机界面。
程序清单8-15展示了关闭照相机界面的委托方法,该界面是由程序清单8-14的代码显示出来的。这些方法是由一个名为
MyViewController
的定制类实现的,它是
UIViewController
的一个子类。在这个例子中,执行这些代码和显示选取器的应该是同一个对象。
useImage:
方法是一个空壳,应该被您的定制代码代替,您可以在这个方法中使用用户选取的图像。
程序清单8-15 图像选取器的委托方法
@implementation MyViewController (ImagePickerDelegateMethods)
|
|
- (void)imagePickerController:(UIImagePickerController *)picker
|
didFinishPickingImage:(UIImage *)image
|
editingInfo:(NSDictionary *)editingInfo
|
{
|
[self useImage:image];
|
|
// Remove the picker interface and release the picker object.
|
[[picker parentViewController] dismissModalViewControllerAnimated:YES];
|
[picker release];
|
}
|
|
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
|
{
|
[[picker parentViewController] dismissModalViewControllerAnimated:YES];
|
[picker release];
|
}
|
|
// Implement this method in your code to do something with the image.
|
- (void)useImage:(UIImage*)theImage
|
{
|
}
|
@end
|
如果图像编辑功能被激活,且用户成功选取了一张图片,则
imagePickerController:didFinishPickingImage:editingInfo:
方法的
image
参数会包含编辑后的图像,您应该将这个图像作为用户选取的图像。当然,如果用户希望存储原始图像,可以从
editingInfo
参数的字典中得到(同时还可以得到编辑用的裁剪矩形)。
从照片库中选取照片
UIKit通过
UIImagePickerController
类为访问用户照片库提供支持。这个控制器可以显示照片选取器界面,用户可以通过该界面漫游用户照片库,选取某个图像,并将它返回给应用程序。您也可以打开用户编辑功能,使用户可以移动和裁剪返回的图像。这个类也可以用于显示一个照相机界面。
UIImagePickerController
类既可以显示照相机界面,也可以显示用户照片库,两种显示方式的使用步骤几乎一样。唯一的区别是是否将选取器对象的
sourceType
属性值设置为
UIImagePickerControllerSourceTypePhotoLibrary
。显示照相机选取器的具体步骤请参见“用照相机照相”部分的讨论。
请注意:当您使用照相机选取器时,应该总是调用
UIImagePickerController
类的
isSourceTypeAvailable:
类方法,并尊重其返回值,而不应假定给定的设备总是具有照片库功能。即使设备支持照片库,该方法仍然可能在照片库不可用时返回
NO
。
使用邮件编辑界面
在iPhone OS 3.0及之后的系统中,您可以通过
MFMailComposeViewController
类在应用程序内部显示一个标准的邮件发送界面。在显示该界面之前,您可以用该类的方法来配置邮件的接受者、主题、和希望包含的附件。当邮件在界面显示出来(通过标准的视图控制器技术)之后和提交给Mail程序进行发送之前,用户可以对邮件的内容进行编辑。用户也可以将整个邮件取消。
请注意:在所有版本的iPhone OS中,您可以通过创建和打开一个
mailto
类型的URL来制作邮件,这种类型的URL会自动传递给Mail程序进行处理。有关如何打开这种类型的URL的更多信息,请参见“和其它应用程序间的通讯”部分。
在使用邮件编辑界面之前,您必须首先把
MessageUI.framework
加入到工程中,并在相应的目标中进行连接。为了访问该框架中的类和头文件,还必须在相应的源代码文件的顶部包含
#import <MessageUI/MessageUI.h>
语句。有关如何在工程中加入框架的信息,请参见Xcode工程管理指南文档中的工程中的文件部分。
应用程序在使用
MFMailComposeViewController
类时,必须首先创建一个实例,并使用该实例的方法设置初始的电子邮件数据;还必须为视图控制器的
mailComposeDelegate
属性声明分配一个对象,负责在用户接收或取消邮件发送时退出界面。您指定的委托对象必须遵循
MFMailComposeViewControllerDelegate
协议
在指定电子邮件地址时,应该使用纯字符串对象。如果您希望使用通讯录用户列表中的邮件地址,可以通过Address Book框架来实现。更多有关如何通过该框架获取电子邮件及其它数据的信息,请参见iPhone OS的Address Book编程指南。
程序清单8-16展示了如何在应用程序中创建
MFMailComposeViewController
对象,并用模式视图显示邮件编辑接口的代码。您可以将清单中的
displayComposerSheet
方法包含到定制的视图控制器中,并在需要时通过它来显示邮件编辑界面。在这个例子中,父视图控制器将自身作为委托,并实现了
mailComposeController:didFinishWithResult:error:
方法。该委托方法只是退出邮件编辑界面,没有进行更多的操作。在您自己的应用程序中,可以在委托方法中考察result参数的值,确定用户是否发送或取消了邮件。
程序清单8-16 显示邮件编辑界面
@implementation WriteMyMailViewController (MailMethods)
|
|
-(void)displayComposerSheet
|
{
|
MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
|
picker.mailComposeDelegate = self;
|
|
[picker setSubject:@"Hello from California!"];
|
|
// Set up the recipients.
|
NSArray *toRecipients = [NSArray arrayWithObjects:@"first@example.com",
|
nil];
|
NSArray *ccRecipients = [NSArray arrayWithObjects:@"second@example.com",
|
@"third@example.com", nil];
|
NSArray *bccRecipients = [NSArray arrayWithObjects:@"four@example.com",
|
nil];
|
|
[picker setToRecipients:toRecipients];
|
[picker setCcRecipients:ccRecipients];
|
[picker setBccRecipients:bccRecipients];
|
|
// Attach an image to the email.
|
NSString *path = [[NSBundle mainBundle] pathForResource:@"ipodnano"
|
ofType:@"png"];
|
NSData *myData = [NSData dataWithContentsOfFile:path];
|
[picker addAttachmentData:myData mimeType:@"image/png"
|
fileName:@"ipodnano"];
|
|
// Fill out the email body text.
|
NSString *emailBody = @"It is raining in sunny California!";
|
[picker setMessageBody:emailBody isHTML:NO];
|
|
// Present the mail composition interface.
|
[self presentModalViewController:picker animated:YES];
|
[picker release]; // Can safely release the controller now.
|
}
|
|
// The mail compose view controller delegate method
|
- (void)mailComposeController:(MFMailComposeViewController *)controller
|
didFinishWithResult:(MFMailComposeResult)result
|
error:(NSError *)error
|
{
|
[self dismissModalViewControllerAnimated:YES];
|
}
|
@end
|
有关如何通过标准视图控制器技术显示界面的更多信息,请参见iPhone OS视图控制器编程指南;有关Message UI框架中包含的类信息,则请参见Message UI框架参考。