为 iPhone 开发混合应用:使用 HTML、CSS 和 JavaScript 为 iPhone 构建动态应用





4.00/5 (2投票s)
在编写应用程序时,也可以将调试消息写入 Xcode 控制台。访问这些功能不仅限于 Objective-C 应用程序。您的混合应用程序可以通过 JavaScript 完成这些操作。
Lee S. Barney 由 Prentice Hall 出版 ISBN-10: 0-321-60416-4 ISBN-13: 978-0-321-60416-3 |
iPhone 具有许多独特的功能,您可以在应用程序中使用它们。这些功能包括手机振动、播放系统声音、访问加速度计和使用 GPS 位置信息。在编写应用程序时,也可以将调试消息写入 Xcode 控制台。访问这些功能不仅限于 Objective-C 应用程序。您的混合应用程序可以通过 JavaScript 完成这些操作。本章的第一部分解释了如何使用 QuickConnect JavaScript API 将这些功能和其他原生 iPhone 功能结合使用。第二部分展示了 QuickConnect JavaScript 库底层的 Objective-C 代码。
第一节:JavaScript 设备激活
iPhone 是一款改变游戏规则的设备。其中一个原因是,应用程序开发者可以访问诸如加速度计之类的硬件。这些原生 iPhone 功能使您能够创建创新应用程序。您可以决定应用程序如何响应加速或 GPS 位置的变化。您可以决定手机何时振动或播放某种音频。
QuickConnectiPhone 的 com.js 文件有一个函数,它使您能够以简单易用的方式访问此行为。makeCall
函数用于在应用程序中向手机发出请求。要使用 makeCall
,您需要传递两个参数。第一个是命令字符串,第二个是执行命令可能需要的任何参数的字符串版本。表 4.1 列出了每个标准命令、所需的参数以及手机在执行命令时的行为。
表 4.1 MakeCall 命令 API
命令字符串 | 消息字符串 | 行为 |
logMessage | 要记录到 Xcode 终端的任何信息。 | 代码运行时,消息会出现在 Xcode 终端中。 |
rec 一个 JavaScript 数组的 JSON 字符串,其中第一个元素是要创建的音频文件的名称。数组的第二个元素是 start 或 stop,具体取决于您是想开始还是停止录制音频数据。 | 将创建一个名称由消息字符串定义的 caf 音频文件。 | |
play | 一个 JavaScript 数组的 JSON 字符串,其中第一个元素是要播放的音频文件的名称。数组的第二个元素是 start 或 stop,具体取决于您是想开始还是停止播放音频文件。 | 如果 caf 音频文件存在,它将通过设备的扬声器或耳机播放。 |
loc | 无 | 设备的核心位置行为被触发,纬度、经度和海拔信息会传回您的 JavaScript 应用程序。 |
playSound | –1 | 设备振动。 |
playSound | 0 | 激光音频文件被播放。 |
showDate | 日期时间 | 显示原生日期和时间选择器。 |
showDate | 日期 | 显示原生日期选择器。 |
DeviceCatalog 示例应用程序包含一个 Vibrate
按钮,单击该按钮时,手机会振动。按钮的 onclick 事件处理函数名为 vibrateDevice
,如下例所示。此函数调用 makeCall
函数,并传递 playSound
命令,其中 –1 作为附加参数。此调用会使手机振动。它使用 playSound
命令,因为 iPhone 将振动和短系统声音视为声音。
function vibrateDevice(event) { //the -1 indicator causes the phone to vibrate makeCall(“playSound”, -1); }
因为振动和系统声音的处理方式相同,所以播放系统声音与手机振动几乎相同。Sound 按钮的 onclick
事件处理程序名为 playSound
。如下面的代码所示,它与 vibrateDevice
唯一的区别是第二个参数。
如果第二个参数传递 0,则 DeviceCatalog 项目资源中包含的 laser.wav 文件将作为系统声音播放。系统声音音频文件的长度必须小于五秒,否则无法作为声音播放。长于此的音频文件使用 play 命令播放,本节后面会介绍该命令。
function playSound(event) { //the 0 indicator causes the phone to play the laser sound makeCall(“playSound”, 0); }
前面代码中使用的 makeCall
函数完全存在于 JavaScript 中,可以在下面的代码中看到。makeCall
函数由两部分组成。第一部分在消息无法立即发送时将其排队。第二部分将消息发送到底层的 Objective-C 代码进行处理。传递消息的方法是将 window.location
属性更改为不存在的 URL,调用时将函数的两个参数作为 URL 的参数传递。
function makeCall(command, dataString){ var messageString = “cmd=”+command+”&msg=”+dataString; if(storeMessage || !canSend){ messages.push(messageString); } else{ storeMessage = true; window.location = “call?”+messageString; } }
以这种方式设置 URL 会导致一条消息(包括 URL 及其参数)发送到作为底层 QuickConnectiPhone 框架一部分的 Objective-C 组件。此 Objective-C 组件旨在终止新页面的加载,并将命令和发送给它的消息传递给框架的命令处理代码。要了解如何完成此操作,请参阅第 2 节。
playSound
和 logMessage
、rec
以及 play
命令是单向的,这意味着从 JavaScript 到 Objective-C 的通信不期望有数据返回。其余的单向标准命令都会导致数据从 Objective-C 组件发送回 JavaScript。
数据传回 JavaScript 有两种方式。第一种方式的示例是通过调用 handleRequest
JavaScript 函数来传输 x、y 和 z 坐标中的加速度信息,该函数在第 2 章“JavaScript 模块化和 iPhone 应用程序”中描述。该调用使用 accel
命令,并且 x、y 和 z 坐标作为 JavaScript 对象从框架的 Objective-C 组件中传递。
mappings.js 文件指示 accel 命令映射到 displayAccelerationVCF
函数,如下面一行所示。
mapCommandToVCF(‘accel’, displayAccelerationVCF);
这会导致每次加速度计检测到运动时都会调用 displayAccelerationVCF
。此函数负责处理所有加速度事件。在 DeviceCatalog 示例应用程序中,该函数只是将 x、y 和 z 加速度值插入到 HTML div 中。您应该更改此函数,以便在应用程序中使用这些值。
将数据传回 JavaScript 的第二种方式是调用 handleJSONRequest
JavaScript 函数。它的工作方式与第 2 章中描述的 handleRequest
函数非常相似,但期望 JSON 字符串作为其第二个参数。此函数是 handleRequest
函数的门面。如下面的代码所示,它只是将其第二个参数的 JSON 字符串转换为 JavaScript 对象,并将命令和新对象传递给 handleRequest
方法。此数据传输方法用于响应由 makeCall(“loc”)
调用启动的 GPS 位置请求,以及显示日期和时间选择器的请求。
function handleJSONRequest(cmd, parametersString){
var paramsArray = null;
if(parametersString){
var paramsArray = JSON.parse(parametersString);
}
handleRequest(cmd, paramsArray);
}
在这两种情况下,结果数据都转换为 JSON 字符串,然后传递给 handleJSONRequest
。有关 JSON 的更多信息,请参阅附录 A,“JSON 简介”。
由于 JavaScript 和 Objective-C 中都提供了 JSON 库,因此 JSON 成为在应用程序中在两种语言之间传递复杂信息的好方法。一个简单的例子是用于开始和停止录制和播放音频文件的 onclick 处理程序。
playRecording
处理程序是激活设备行为的用户界面按钮的所有处理程序的典型示例。如下例所示,它创建一个 JavaScript 数组,添加两个值,将数组转换为 JSON 字符串,然后使用 play
命令执行 makeCall
函数。
function playRecording(event) { var params = new Array(); params[0] = “recordedFile.caf”; params[1] = “start”; makeCall(“play”, JSON.stringify(params)); }
要停止播放录音,也会发出带有 play
命令的 makeCall
,如上例所示,但第二个参数不是 start
,而是设置为 stop
。main.js 文件中的 terminatePlaying
函数实现了此行为。
开始和停止录制音频文件的方式与 playRecording
和 terminatePlaying
相同,只是使用 rec
代替 play
命令。使这些相关功能的开始和停止实现相似,这使您更容易将这些行为添加到您的应用程序中。
如本节前面所述,某些设备行为(例如振动)只需要从 JavaScript 到 Objective-C 处理程序的通信。其他行为(例如检索当前 GPS 坐标或选择器的结果)需要双向通信。图 4.1 显示了具有 GPS 信息的 DeviceCatalog 应用程序。
与前面已经检查过的一些单向示例一样,通信从应用程序的 JavaScript 部分开始。main.js 文件中的 getGPSLocation
函数使用 makeCall
函数启动通信。请注意,与前面的示例一样,makeCall
不返回任何内容。makeCall
使用异步通信协议与库的 Objective-C 端通信,即使通信是双向的,因此没有返回值。
function getGPSLocation(event)
{
document.getElementById(‘locDisplay’).innerText = ‘’;
makeCall(“loc”);
}
由于通信是异步的,就像 AJAX 一样,需要创建并调用一个回调函数来接收 GPS 信息。在 QuickConnectiPhone 框架中,这是通过在映射文件中创建映射来完成的,该映射将命令 showLoc
映射到一个函数。
mapCommandToVCF(‘showLoc’, displayLocationVCF);
在这种情况下,它映射到 displayLocationVCF
视图控制函数。这个简单的示例函数仅用于在屏幕上的 div 中显示当前 GPS 位置。显然,这些值也可以用于计算要存储在数据库中的距离,或者使用第 8 章“远程数据访问”中描述的 ServerAccessObject
发送到服务器。
function displayLocationVCF(data, paramArray){ document.getElementById(‘locDisplay’).innerText = ‘latitude: ‘+paramArray[0]+’\nlongitude: ‘+paramArray[1]+’\naltitude: ‘+paramArray[2]; }
显示选择器(例如标准日期和时间选择器),然后显示选定的结果与前面的示例相似。此过程也从 JavaScript 调用设备处理代码开始。在这种情况下,按钮的事件处理函数是 main.js 文件中找到的 showDateSelector
函数。
function showDateSelector(event)
{
makeCall(“showDate”, “DateTime”);
}
与 GPS 示例一样,也需要一个映射。此映射将 showPickResults
命令映射到 displayPickerSelectionVCF
视图控制函数,如下所示。
mapCommandToVCF(‘showPickResults’, displayPickerSelectionVCF);
命令映射到的函数将用户的选择结果插入到一个简单的 div 中,如下面的代码所示。显然,此信息可以通过多种方式使用。
function displayPickerSelectionVCF(data, paramArray){
document.getElementById(‘pickerResults’).innerHTML = paramArray[0];
makeCall
的一些用法,例如本节前面示例中的用法,是单向地从 JavaScript 通信到 Objective-C 设备处理程序。刚刚检查的那些使用双向通信往返于处理程序。设备可能进行的另一种通信类型是单向地从设备到您的 JavaScript 代码。加速度计信息的使用就是一个例子。
加速度事件的 Objective-C 处理程序(请参阅第 2 节查看代码)直接进行 JavaScript handleRequest
调用,传递 accel 命令。以下 accel 命令映射到 displayAccelerationVCF
视图控制函数。
mapCommandToVCF(‘accel’, displayAccelerationVCF);与其他 VCF 一样,此 VCF 将加速度值插入到 div 中。
function displayAccelerationVCF(data, param){ document.getElementById(‘accelDisplay’).innerText =’x: ‘+param.x+’\ny: ‘+param.y+’\nz: ‘+param.z; }
此函数与其他函数的一个区别是,此函数传递的是一个对象作为其 param 参数,而不是传递一个数组。第 2 节展示了如何从 Objective-C 加速度事件处理程序传递的信息创建此对象。
本节向您展示了如何将一些最常请求的 iPhone 行为添加到您的基于 JavaScript 的应用程序中。第 2 节展示了支持此功能的框架的 Objective-C 部分。
第二节:Objective-C 设备激活
本节假设您熟悉 Objective-C 以及如何使用它来创建 iPhone 应用程序。如果您不熟悉,可以从 Pearson Publishing 获得 Erica Sadun 的著作《The iPhone Developer’s Cookbook》。如果您只想使用 QuickConnectiPhone 框架为 iPhone 编写 JavaScript 应用程序,则无需阅读本节。
使用 Objective-C 使 iPhone 振动是最容易实现的行为之一。如果将 AudioToolbox 框架包含在项目资源中,可以使用以下单行代码完成此操作。
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
那么问题就变成了,“当 UIWebView 被告知更改其位置时,我如何才能调用 AudioServicesPlaySystemSound 函数?”
QuickConnectViewController 实现了 shouldStartLoadWithRequest
委托方法。由于嵌入式 UIWebView(名为 aWebView
)的委托设置为 QuickConnectViewController
,因此每次嵌入式 UIWebView 被告知更改其位置时都会调用此方法。以下代码和 QuickConnectViewController.m 文件的第 90 行显示了此委托的设置。
[aWebView setDelegate:self];
shouldStartLoadWithRequest
函数的基本行为很简单。它旨在使您能够编写代码来决定是否应该实际加载请求的新页面。QuickConnectiPhone 框架利用决策能力来阻止第 1 节中显示的 JavaScript 调用所做的任何请求加载页面,并执行其他 Objective-C 代码。
shouldStartLoadWithRequest
方法有几个可用的参数。这些参数包括:
curWebView
—包含您的 JavaScript 应用程序的 UIWebView。request
—一个包含新 URL 和其他项目的 NSURLRequest。navigationType
—一个 UIWebViewNavigationType,可用于确定请求是用户选择链接的结果,还是由其他操作生成的结果。
-(BOOL)webView:(UIWebView *)curWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
由 makeCall
JavaScript 函数组装的导致设备振动的 URL,call?cmd=playSound&msg=-1
包含在请求对象中,通过将 URL 消息传递给它,可以轻松地将其作为字符串检索。此消息返回一个 NSURL 类型对象,然后将 absoluteString
消息传递给它。因此,获得了一个表示 URL 的 NSString
指针。此字符串,在以下代码中显示为 url,然后可以使用 ? 作为分隔符将其拆分为一个数组,生成一个 NSString
指针数组。
NSString *url = [[request URL] absoluteString]; NSArray *urlArray = [url componentsSeparatedByString:@”?”];
urlArray
包含两个元素。第一个是 URL 的调用部分,第二个是命令字符串 cmd=playSound&msg=-1
。为了确定要执行哪个命令以及可能需要使用的任何参数(在本例中为 –1),命令字符串需要进一步解析。这是通过在 & 字符处拆分 commandString
来完成的。这会创建另一个名为 urlParamsArray
的数组。
NSString *commandString = [urlArray objectAtIndex:1]; NSArray *urlParamsArray = [commandString componentsSeparatedByString:@”&”]; //the command is the first parameter in the URL cmd = [[[urlParamsArray objectAtIndex:0] componentsSeparatedByString:@”=”] objectAtIndex:1];
在这种情况下,请求设备振动,urlParamsArray
数组的第一个元素变为 cmd=playSound
,第二个元素为 msg=-1
。因此,拆分 urlParamsArray
的元素可以检索要执行的命令和参数。=
字符是拆分 urlParamsArray
的每个元素的分隔符。
以下示例中的第 1-3 行检索作为 URL 中与 msg 键关联的值发送的参数,作为 NSString parameterArrayString
。因为组装 URL 的 JavaScript 将所有此值项转换为 JSON,所以此 NSString 是一个已转换为 JSON 格式的对象。这包括数字(如当前示例)和从 JavaScript 传递的字符串、数组或其他参数。此外,如果数据中出现空格或其他特殊字符,UIWebView 会将它们作为 URL 的一部分进行转义。因此,以下代码中的第 6-8 行需要用于解除 JSON 字符串中的任何特殊字符的转义。
1 NSString *parameterArrayString = [[[urlParamsArray 2 objectAtIndex:1] componentsSeparatedByString:@”=”] 3 objectAtIndex:1]; 4 //remove any encoding added as the UIWebView has 5 //escaped the URL characters. 6 parameterArrayString = [parameterArrayString 7 stringByReplacingPercentEscapesUsingEncoding: 8 NSASCIIStringEncoding]; 9 SBJSON *generator = [SBJSON alloc]; 10 NSError *error; 11 paramsToPass = [[NSMutableArray alloc] 12 initWithArray:[generator 13 objectWithString:parameterArrayString 14 error:&error]]; 15 if([paramsToPass count] == 0){ 16 //if there was no array of data sent then it must have 17 //been a string that was sent as the only parameter. 18 [paramsToPass addObject:parameterArrayString]; 19 } 20 [generator release];
前述代码中的第 9-14 行包含将 JSON 字符串 parameterArrayString
转换为原生 Objective-C NSArray 的代码。第 9 行分配了一个 SBJSON 生成器对象。然后将生成器对象发送 objectWithString
消息,如下所示:
- (id)objectWithString:(NSString*)jsonrep error:(NSError**)error;
此多部分消息传递一个 JSON 字符串(在本例中为 parameterArrayString
)和一个 NSError 指针错误。如果在转换过程中发生错误,则会分配错误指针。如果没有发生错误,则为 nil。
此消息的返回值在本例中是数字 –1。如果 JavaScript 数组被字符串化,它是一个 NSArray 指针;如果它是一个 JavaScript 字符串,它是一个 NSString 指针。如果传递了一个 JavaScript 自定义对象类型,则返回的对象是一个 NSDictionary 指针。
至此,已经检索到命令以及执行命令所需的任何参数,现在可以使用 if 或 case
语句进行实际计算。
然而,这样一套条件语句并非最佳,因为每次添加或删除命令时都必须修改它们。在第 2 章中,QuickConnectiPhone 架构的 JavaScript 部分通过实现一个名为 handleRequest
的前端控制器函数来解决同样的问题,该函数包含对应用程序控制器实现的调用。由于这里的问题相同,因此 handleRequest
的 Objective-C 版本应该可以解决当前问题。第 3 节介绍了前端控制器和应用程序控制器在 Objective-C 中的实现。以下代码行检索 QuickConnect 对象的一个实例,并将其传递给 handleRequest
withParameters
多消息。在 shouldStartLoadWithRequest
委托方法中不再需要进一步计算。
[[QuickConnect getInstance] handleRequest:cmd withParameters:paramsToPass];
由于使用了 QuickConnect 对象的 handleRequest
消息,因此必须有一种方法可以将命令映射到所需的功能,如第 2 章中使用 JavaScript 所示。QCObjC 组的 QCCommandMappings.m 和 .h 文件中找到的 QCCommandMappings
对象包含此示例的所有业务控制对象 (BCO) 和视图控制对象 (VCO) 的映射。
以下代码是 QCCommandMappings
对象的 mapCommands
方法,在应用程序启动时调用。它传递了应用程序控制器的实现,该实现用于创建命令到功能的映射。有关 mapCommandToVCO
消息和 mapCommands
调用的代码解释,请参阅第 3 节。
1 + (void) mapCommands:(QCAppController*)aController{ 2 [aController mapCommandToVCO:@”logMessage” withFunction:@”LoggingVCO”]; 3 [aController mapCommandToVCO:@”playSound” withFunction:@”PlaySoundVCO”]; 4 [aController mapCommandToBCO:@”loc” withFunction:@”LocationBCO”]; 5 [aController mapCommandToVCO:@”sendloc” withFunction:@”LocationVCO”]; 6 [aController mapCommandToVCO:@”showDate” withFunction:@”DatePickerVCO”]; 7 [aController mapCommandToVCO:@”sendPickResults” withFunction:@”PickResultsVCO”]; 8 [aController mapCommandToVCO:@”play” withFunction:@”PlayAudioVCO”]; 9 [aController mapCommandToVCO:@”rec” withFunction:@”RecordAudioVCO”]; 10 }
前述代码的第 3 行与当前设备振动的示例相关。如本节前面所述,从应用程序的 JavaScript 部分收到的命令是 playSound
。通过将此命令作为 mapCommandToVCO
消息的第一个参数,并将 PlaySoundVCO
作为第二个参数 withFunction
传递,创建了一个链接,该链接导致应用程序控制器将带有 –1 参数的 doCommand
消息发送到 PlaySoundVCO
类。如您所见,DeviceCatalog 示例中从 JavaScript 发送的所有其他命令都已在此处映射。
playSound
命令映射到的 PlaySoundVCO
的代码可以在 PlaySoundVCO.m 和 PlaySoundVCO.h 文件中找到。doCommand
方法包含对象的所有行为。
要播放系统声音,必须使用预定义的声音(在编写本书时,振动是唯一的一种),或者必须从声音文件生成系统声音。PlaySoundVCO
类的 doCommand 展示了这两种行为的示例。
1 + (id) doCommand:(NSArray*) parameters{ 2 SystemSoundID aSound = 3 [((NSNumber*)[parameters objectAtIndex:1]) intValue]; 4 if(aSound == -1){ 5 aSound = kSystemSoundID_Vibrate; 6 } 7 else{ 8 NSString *soundFile = 9 [[NSBundle mainBundle] pathForResource:@”laser” 10 ofType:@”wav”]; 11 NSURL *url = [NSURL fileURLWithPath:soundFile]; 12 //if the audio file is takes to long to play 13 //you will get a -1500 error 14 OSStatus error = AudioServicesCreateSystemSoundID( 15 (CFURLRef) url, &aSound ); 16 } 17 AudioServicesPlaySystemSound(aSound); 18 return nil; 19 }
如前例中第 4 行所示,如果索引为 1 的参数的值为 –1,则 SystemSoundID aSound
变量设置为已定义的 kSystemSoundID_Vibrate
值。如果不是,则从应用程序资源组中找到的 laser.wav 文件创建一个系统声音,并将 aSound
变量设置为为新系统声音生成的标识符。
在这两种情况下,都会调用 C 函数 AudioServicesPlaySystemSound
,然后播放声音或设备振动。如果设备是 iPod Touch,则设备会忽略振动请求。在具有多个声音的实际应用程序中,可以通过传递其他数字作为指示符来播放哪种声音,从而轻松扩展此函数。
由于 SystemSoundID
类型变量实际上是数字,因此应在应用程序启动时生成系统声音,并将每个系统声音的 SystemSoundID 传递给应用程序的 JavaScript 部分以供以后使用。这避免了每次需要声音时重新创建系统声音的计算负载,从而提高了用户体验的质量,因为播放声音没有延迟。
现在已经了解了从 JavaScript 向 Objective-C 传递命令以及如何使设备振动或播放短音的过程,现在很容易看到并理解如何向 Objective-C 传递命令并将结果返回到应用程序的 JavaScript 部分。
由于这些类型的通信行为相似,因此以 GPS 位置检测为例,它是 iPhone 应用程序中的一个热门项目。它利用了 QuickConnectiPhone 框架的这种双向 JavaScript-Objective-C 通信能力。
与处理从 JavaScript 框架发送的所有命令一样,必须对 loc 命令进行映射,以便检索数据并发送响应。
[aController mapCommandToBCO:@”loc” withFunction:@”LocationBCO”]; [aController mapCommandToVCO:@”sendloc” withFunction:@”LocationVCO”];
在这种情况下,存在两个映射:第一个映射到 BCO,第二个映射到 VCO。如第 2 章所讨论的,BCO 执行数据检索,VCO 用于数据呈现。
由于 QuickConnectiPhone 框架会在所有 VCO 之前执行给定命令的 BCO,因此首先会向 LocationBCO
类发送 doCommand
消息,该类会检索并返回 GPS 数据。以下 doCommand
方法属于 LocationBCO
类。它执行获取设备开始查找其 GPS 位置所需的调用。
+ (id) doCommand:(NSArray*) parameters{ QuickConnectViewController *controller = (QuickConnectViewController*)[parameters objectAtIndex:0]; [[controller locationManager] startUpdatingLocation]; return nil; }
此方法通过检索传递到方法中的参数数组的第一个项并通知它启动硬件来启动 GPS 定位硬件。该框架始终将第一个参数设置为 QuickConnectViewController
,以便 BCO 或 VCO 可以根据需要与任何命令相关联地使用它。在所有 Objective-C BCO 和 VCO 中,从 JavaScript 发送的任何参数都从索引 1 开始。
QuickConnectViewController
对象具有一个名为 locationManager
的内置 CLLocationManager
属性,您的应用程序会根据需要打开和关闭它。重要的是不要让这个管理器运行超过必要的时间,因为它会消耗大量电池电量。因此,前面的代码通过每次需要位置时向其发送 startUpdatingLocation
消息来打开定位硬件。一旦找到位置,定位硬件就会关闭。
CLLocationManager
对象的行为是异步的。这意味着当请求位置信息时,在确定位置后会调用一个预定义的回调函数。此预定义函数允许您访问位置管理器和两个位置:之前确定的位置和当前位置。
位置管理器通过逐渐细化设备位置来工作。在此过程中,它会多次调用 didUpdateToLocation
。以下代码示例会找出确定新位置所需的时间。第 9 行确定这是否小于 5.0 秒,如果是,则终止位置搜索。
1 (void)locationManager:(CLLocationManager *)manager 2 didUpdateToLocation:(CLLocation *)newLocation 3 fromLocation:(CLLocation *)oldLocation 4 { 5 // If it’s a relatively recent event, turn off updates to save power 6 NSDate* eventDate = newLocation.timestamp; 7 NSTimeInterval howRecent = 8 [eventDate timeIntervalSinceNow]; 9 if (abs(howRecent) < 5.0){ 10 [manager stopUpdatingLocation]; 11 NSMutableArray *paramsToPass = 12 [[NSMutableArray alloc] initWithCapacity:2]; 13 [paramsToPass addObject:self]; 14 [paramsToPass addObject:newLocation]; 15 [[QuickConnect getInstance] 16 handleRequest:@”sendloc” 17 withParameters:paramsToPass]; 18 } 19 // else skip the event and process the next one. 20 }
终止位置搜索后,代码会向 QuickConnect 前端控制器类发送一条消息,说明它应该处理一个 sendloc 请求,其中 QuickConnectViewController
、自身和新位置作为附加参数传递。
sendloc
命令映射到 LocationVCO
处理程序,其 doCommand
方法如下例所示。此方法从发出 GPS 位置信息原始请求的 QuickConnectViewController
中检索名为 webView 的 UIWebView。然后将 GPS 信息放入名为 passingArray
的 NSArray 中。
要将 GPS 信息传回 webView
对象,其中包含它的 NSArray 必须转换为 JSON 字符串。前面用于从 JSON 字符串创建数组的相同 SBJSON 类现在用于从 NSArray 创建 NSString。这在第 21 和 22 行完成。
1 + (id) doCommand:(NSArray*) parameters{ 2 QuickConnectViewController *controller = 3 (QuickConnectViewController*)[parameters 4 objectAtIndex:0]; 5 UIWebView *webView = [controller webView]; 6 CLLocation *location = (CLLocation*)[parameters 7 objectAtIndex:1]; 8 9 NSMutableArray *passingArray = [[NSMutableArray alloc] 10 initWithCapacity:3]; 11 [passingArray addObject: [NSNumber numberWithDouble: 12 location.coordinate.latitude]]; 13 [passingArray addObject: [NSNumber numberWithDouble: 14 location.coordinate.longitude]]; 15 [passingArray addObject: [NSNumber numberWithFloat: 16 location.altitude]]; 17 18 SBJSON *generator = [SBJSON alloc]; 19 20 NSError *error; 21 NSString *paramsToPass = [generator 22 stringWithObject:passingArray error:&error]; 23 [generator release]; 24 NSString *jsString = [[NSString alloc] 25 initWithFormat:@”handleJSONRequest(‘showLoc’, ‘%@’)”, 26 paramsToPass]; 27 [webView 28 stringByEvaluatingJavaScriptFromString:jsString]; 29 return nil; 30 }
在将 GPS 位置信息转换为表示数字数组的 JSON 字符串后,会调用 webView
对象内部的 JavaScript 引擎。这首先通过创建一个作为要执行的 JavaScript 的 NSString 来完成。在此示例中,它是一个 handleJSONRequest
,将 showLoc
作为命令传递,并将 JSON GPS 信息作为字符串传递。如第 1 节所述,此请求会导致 GPS 数据出现在正在显示的 HTML 页面中的 div 中。
看完这个例子,你现在可以查看 DeviceCatalog 示例中的 DatePickerVCO
和 PickResultsVCO
,看看如何使用相同的方法来显示 Objective-C 中可用的标准日期和时间选择器(称为 picker)。尽管在 UIWebView 中使用 JavaScript 可以使用预定义的 picker,但从用户的角度来看,它们不如 Objective-C 中可用的标准 picker 好。通过使用这些标准 picker 和您可能选择定义的任何自定义 picker,您的混合应用程序将拥有更流畅的用户体验。
第三节:QuickConnectiPhone 架构的 Objective-C 实现
第 1 节和第 2 节中显示的代码严重依赖于第 2 章中解释的相同架构的 Objective-C 实现。本节将展示如何在 Objective-C 中实现该架构。要查看每个组件的完整解释,请参阅包含 JavaScript 实现的第 2 章。
与 JavaScript 实现一样,所有应用程序行为请求都通过前端控制器处理。前端控制器实现为 QuickConnect 类,其源代码可在 QuickConnect.m 和 QuickConnect.h 文件中找到。由于发送到 QuickConnect
的消息可能需要在整个应用程序中的许多不同位置进行,因此此是单例类。
单例类的编写方式是,在应用程序中只能分配该类的一个实例化对象。如果操作正确,总有一种方法可以从应用程序的任何地方获取指向此单个对象的指针。对于 QuickConnect
单例对象,这是通过实现一个类方法 getInstance
来实现的,该方法在第一次调用时返回分配的单个 QuickConnect
实例。
因为它是类方法,所以可以在不实例化 QuickConnect
对象的情况下向类发送 getInstance
消息。当调用时,它返回指向底层 QuickConnect
实例的指针。如下面的代码所示,这是通过将类的实例分配给静态定义的 QuickConnect
指针来完成的。
+ (QuickConnect*)getInstance{ //since this line is declared static //it will only be executed once. static QuickConnect *mySelfQC = nil; @synchronized([QuickConnect class]) { if (mySelfQC == nil) { mySelfQC = [QuickConnect singleton]; [mySelfQC init]; } } return mySelfQC; }
在初始化之前发送的单例消息使用了 QuickConnect
对象的超类 FTSWAbstractSingleton
中定义的行为。此超类分配嵌入的单例行为,例如覆盖 new、clone 和其他方法,这些方法可能被错误地尝试用于分配另一个 QuickConnect
实例。因此,只能使用 getInstance
方法来创建和使用 QuickConnect
对象。与 Objective-C 中所有格式良好的对象一样,在分配 QuickConnect
对象后,必须对其进行初始化。
对象的分配和初始化只有在没有 QuickConnect 对象被分配给 mySelfQC
属性时才会发生。此外,由于围绕实例化 QuickConnect
对象的检查的同步调用,检查和初始化是线程安全的。
- (void) handleRequest: (NSString*) aCmd withParameters:(NSArray*) parameters
是 QuickConnect
类的另一个方法。就像第 2 章中的 JavaScript handleRequest(aCmd, parameters)
函数一样,此方法是请求在您的应用程序中执行功能的方式。
命令字符串和参数数组被传递给方法。在下面的例子中,第 3-9 行显示了一系列消息被发送到应用程序控制器。第 3 行和第 4 行首先执行与命令相关的任何 VCO。如果命令和参数通过验证,则通过 dispatchToBCO
消息执行与命令相关的任何 BCO。此消息返回一个 NSMutableArray
,其中包含原始参数数组数据,并添加了任何可能已调用的 BCO 对象累积的任何数据。
1 - (void) handleRequest: (NSString*) aCmd 2 withParameters:(NSArray*) parameters{ 3 if([self->theAppController dispatchToValCO:aCmd 4 withParameters:parameters] != nil){ 5 NSMutableArray *newParameters = 6 [self->theAppController dispatchToBCO:aCmd 7 withParameters:parameters]; 8 [self->theAppController dispatchToVCO:aCmd 9 withParameters:newParameters]; 10 } 11 }
在完成对 dispatchToBCO:withParameters
的调用后,会发送 dispatchToVCO:withParameters
消息。这会导致与给定命令关联的任何 VCO 也被执行。
通过对所有功能请求使用 handleRequest:withParameters 方法,每个请求都经过三步过程。
- 验证。
- 业务规则 (BCO) 的执行。
- 视图更改 (VCO) 的执行。
与 JavaScript 实现一样,每个 dispatchTo
方法都是一个门面。在这种情况下,底层的 Objective-C 方法是 dispatchToCO:withParameters
。
此方法首先检索与传递参数 aMap
中的默认命令关联的所有命令对象。aMap
包含 BCO、VCO 或 ValCO,具体取决于调用了哪个门面方法。如果存在这些默认命令对象,则检索并用于所有命令。如果您希望某些命令对象用于所有命令,则无需将它们映射到每个单独的命令。只需将它们一次映射到 default
命令即可。
要使用检索到的命令对象,必须向它们发送一条消息。要发送的消息是 doCommand。下面示例中的第 19-23 行显示了此消息作为选择器被检索,并传递了 performSelector
消息。这会导致您在 QCCommandObject
中实现的 doCommand
消息被执行。
1 - (id) dispatchToCO: (NSString*)command withParameters: 2 (NSArray*)parameters andMap:(NSDictionary*)aMap{ 3 //create a mutable array that contains all of 4 // the existing parameters. 5 NSMutableArray *resultArray; 6 if(parameters == nil){ 7 resultArray = [[NSMutableArray alloc] 8 initWithCapacity:0]; 9 } 10 else{ 11 resultArray = [NSMutableArray 12 arrayWithArray:parameters]; 13 } 14 //set the result to be something so 15 //that if no mappings are made the 16 //execution will continue. 17 id result = @”Continue”; 18 if([aMap objectForKey:@”default”] != nil){ 19 SEL aSelector = @selector(doCommand); 20 while((result = [((QCCommandObject*) 21 [aMap objectForKey:@”default”]) 22 performSelector:aSelector 23 withObject:parameters]) != nil){ 24 if(aMap == self->businessMap){ 25 [resultArray addObject:result]; 26 } 27 } 28 } 29 //if all of the default command objects’ method calls 30 //return something, execute all of the custom ones. 31 if(result != nil && [aMap objectForKey:command] != 32 nil){ 33 NSArray *theCommandObjects = 34 [aMap objectForKey:command]; 35 int numCommandObjects = [theCommandObjects count]; 36 for(int i = 0; i < numCommandObjects; i++){ 37 QCCommandObject *theCommand = 38 [theCommandObjects objectAtIndex:i]; 39 result = [theCommand doCommand:parameters]; 40 if(result == nil){ 41 resultArray = nil; 42 break; 43 } 44 if(aMap == self->businessMap){ 45 [resultArray addObject:result]; 46 } 47 } 48 } 49 if(aMap == self->businessMap){ 50 return resultArray; 51 } 52 return result; 53 }
在所有 doCommand
消息发送到您映射到默认命令的任何 QCCommandObjects
之后,对您映射到作为参数传递给方法的命令的 QCCommandObjects
执行相同的操作。这些 QCCommandObjects
存在的理由与 JavaScript 实现中的控制函数相同。由于 QCCommandObjects
包含应用程序的所有行为代码,因此了解如何创建它们的一个示例很有帮助。
QCCommandObject
是 LoggingVCO
的父类。因此,LoggingVCO
必须实现 doCommand
方法。DeviceCatalog 示例中 LoggingVCO.m 文件的全部内容如下。其 doCommand
方法写入到正在运行的应用程序的日志文件中。
正在运行的应用程序。此 VCO 记录从应用程序的 JavaScript 代码中生成的调试消息。图 4.2 显示了完成此操作所需的调用。
LoggingVCO
类的 doCommand
方法很小。所有不同类型的命令对象的 doCommand
方法都应该始终很小。它们应该只做一件事,并把它做好。如果您发现您正在处理的 doCommand
方法变得很大,您可能需要考虑将其拆分为逻辑组件并创建多个命令对象类。原因在于,如果这些方法变得很长,它们可能正在做不止一件事。
在下面的示例中,LoggingVCO
所做的“一件事”是将消息记录到 Xcode 中的调试控制台。显然,这个小组件可以与许多命令结合其他命令对象重复使用。
此 VCO 的行为由一行代码组成,该行代码执行 NSLog
函数。在此过程中,参数数组中的第一个对象被附加到一个静态字符串并写入。
#import "LoggingVCO.h" @implementation LoggingVCO + (id) doCommand:(NSArray*) parameters{ NSLog(@"JavaScriptMessage: %@", [parameters objectAtIndex:1]); return nil; } @end
为了进行此日志记录,必须在 logMessage
命令和 LoggingVCO
类之间生成映射。与 JavaScript 实现中一样,这通过将 logMessage
作为键,将 LoggingVCO
类的名称作为值添加到映射中来完成。
映射在 QCCommandMappings.m 文件中完成。以下代码来自 DeviceCatalog 示例中的此文件,并将 logMessage
映射到 LoggingVCO
类。
[aController mapCommandToVCO:@”logMessage” withFunction:@”LoggingVCO”];
应用程序控制器会收到 mapCommandToVCO:withFunction
消息,其中命令是第一个参数,VCO 名称是第二个参数。此方法以及用于映射其他命令对象类型的类似方法都是门面。每个门面方法都调用底层的 mapCommandToCO
方法。
此 mapCommandToCO
方法通过将命令映射到 NSMutableArray
,从而允许将多个命令对象映射到单个命令。然后,此数组用于包含与作为第二个参数传入的类名匹配的 Class 对象。以下代码显示了 mapCommandToCO
方法的实现。
- (void) mapCommandToCO:(NSString*)aCommand withFunction:(NSString*)aClassName toMap:(NSMutableDictionary*)aMap{ NSMutableArray *controlObjects = [[aMap objectForKey:aCommand] retain]; if(controlObjects == nil){ NSMutableArray *tmpCntrlObjs = [[NSMutableArray alloc] initWithCapacity:1]; [aMap setObject: tmpCntrlObjs forKey:aCommand]; controlObjects = tmpCntrlObjs; [tmpCntrlObjs release]; } //Get the control object’s class //for the given name and add an object //of that type to the array for the command. Class aClass = NSClassFromString(aClassName); if(aClass != nil){ [controlObjects addObject:aClass]; } else{ MESSAGE( unable to find the %@ class. Make sure that it exists under this name and try again.”); } }
将 Class 对象添加到 NSMutableArray
中,可以使任意数量的相同类型的命令对象(VCO、BCO 或其他)映射到同一个命令,然后按照发送 mapCommandTo
消息的顺序单独执行。因此,您可以顺序执行多个 VCO。
例如,您可以使用一个显示 UIView 的 VCO,然后是另一个更改另一个 UIView 不透明度的 VCO,然后记录一条消息。发送三个 mapCommandToVCO
消息,使用相同的命令但三个不同的命令对象名称即可完成此操作。
DeviceCatalog 示例中还存在其他几个 BCO 和 VCO 示例。每次从应用程序的 JavaScript 部分发出请求时,都会激活它们。
摘要
本章向您展示了如何从 JavaScript 应用程序中激活 iPhone 或 iPod Touch 设备的几个所需功能。使用 GPS 定位、加速度计值、手机振动以及播放声音和音频等功能可以增加应用程序的丰富性。
通过查看 DeviceCatalog 中包含的示例,如果您使用 Objective-C,您应该能够添加其他功能,例如扫描 Bonjour 网络以查找附近的设备,添加、删除和检索联系人应用程序中的联系人,或者添加、删除和检索 Objective-C 应用程序中可用的其他内置行为。
使用本章描述的方法,您的 JavaScript 应用程序几乎可以做任何纯 Objective-C 应用程序能做的事情。一个例子是在第 8 章中,您将学习如何在任何应用程序中嵌入 Google 地图而不会失去 Apple 地图应用程序的外观和感觉。