iPhone應用程序編程指南

介紹

請注意:本文檔之前命名爲iPhone OS編程指南

iPhone SDK爲創建iPhone的本地應用程序提供必需的工具和資源。在用戶的Home屏幕上,iPhone的本地應用程序表示爲圖標。它們和運行在Safari內部的web應用程序不同,在基於iPhone OS的設備上,它們作爲獨立的執行程序來運行。本地應用程序可以訪問iPhone和iPod Touch的所有特性,比如加速計、位置服務、和多點觸摸接口,正是這些特性使設備變得更加有趣。本地應用程序還可以將數據保存在本地的文件系統中,甚至可以通過定製的URL類型來和安裝在設備上的其它程序進行通訊。

iPhone and iPod touch

爲iPhone OS開發本地應用程序需要使用UIKit框架。利用該框架提供的基礎設施和缺省行爲,您可以在幾分鐘內創建一個具有一定功能的應用程序。UIKit框架(和系統中的其它框架)不但提供大量的缺省行爲,而且提供了一些掛鉤,開發者可以通過這些掛鉤來定製和擴展它的行爲。

誰應該閱讀本文?

本文的目標讀者是希望創建iPhone本地應用程序的新老iPhone OS開發者,目的是向您介紹iPhone應用程序的架構,展示UIKit和其它重要系統框架中的一些關鍵的定製點。在介紹這些內容的同時,本文還將提供一些有助於正確設計的指導意見。文中還指出一些爲特定主題提供建議和進行進一步討論的其它文檔。

雖然本文描述的很多框架也存在於Mac OS X系統中,但閱讀本文並不需要熟悉Mac OS X及其技術。

先決條件

在開始閱讀本文之前,您必須至少對下面這些Cocoa概念有基本的理解:

  • 有關Xcode和Interface Builder的基本信息及其在應用程序開發中的作用。

  • 如何定義新的 Objective-C類。

  • 如何管理內存包括如何創建和釋放Objective-C對象。

  • 委託對象在管理應用程序行爲中的作用。

  • 目標-動作範式在用戶界面管理中的作用。

不熟悉Cocoa和Objective-C的開發者可以在Cocoa基本原理指南中得到相應的信息。

iPhone應用程序的開發需要在運行Mac OS X v10.5或更高版本系統以及基於Intel的Macintosh電腦上進行,還必須下載和安裝iPhone SDK。有關如何得到iPhone SDK的信息,請訪問http://www.apple.com.cn/developer/iphone/網站。

本文的組織

本文有如下章節:

  • “核心應用程序” 描述iPhone應用程序的基本結構,介紹一些所有應用程序都需要做好處理準備的關鍵任務。

  • “窗口和視圖” 描述iPhone的窗口管理模型,展示如何通過視圖來組織用戶界面。

  • “事件處理” 描述iPhone事件處理模型,展示如何處理多點觸摸和運動事件,以及如何在應用程序中使用拷貝和粘貼操作。

  • “圖形和描畫” 描述iPhone OS的圖形架構,展示如何描畫各種形狀和圖像,以及如何在使用動畫。

  • “文本和Web” 描述iPhone OS的文本支持,介紹一些管理系統鍵盤的實例。

  • “文件和網絡” 爲如何操作文件和網絡連接提供一些指導原則。

  • “多媒體支持” 展示如何使用iPhone OS中的音頻和視頻技術。

  • “設備支持” 展示如何使用外接配件接口、位置服務、加速計、和內置的照相機接口。

  • “應用程序的偏好設置” 展示如何配置應用程序的偏好設置及如何將這些設置顯示在Settings應用程序中。

提供反饋

如果您對本文有什麼反饋,可以通過每個頁面下方的內置反饋表進行反映。

如果您發現蘋果軟件或文檔存在問題,我們鼓勵您報告給蘋果公司。如果您希望某個產品或文檔在將來有所改變,則可以提交功能增強報告,具體做法是訪問ADC網站上的缺陷報告(Bug Reporting)頁面並提交報告,其URL如下:

http://developer.apple.com/bugreporter/

您必須有正當的ADC登錄名和密碼才能提交報告。按照缺陷報告頁面上的指令進行操作就可以免費得到一個登錄名。

相關信息

下面的文檔中包含一些重要的信息,所有的開發者在開發iPhone OS的應用程序之前都應該加以閱讀:

  • iPhone開發指南 從工具的角度描述iPhone開發過程中的一些重要信息,介紹如何配置設備及如何使用Xcode(和其它工具)連編、運行、和測試您的軟件。

  • Cocoa基本原理指南 介紹iPhone應用程序開發中使用的設計模式以及其它與實踐相關的信息。

  • iPhone人機界面指南 就如何設計iPhone應用程序的用戶界面提供指導和重要信息。

下面的框架參考和概念性文檔提供一些與iPhone關鍵主題相關的信息:

核心應用程序

所有的iPhone應用程序都是基於UIKit框架構建而成的,因此,它們在本質上具有相同的核心架構。UIKit負責提供運行應用程序和協調用戶輸入及屏幕顯示所需要的關鍵對象。應用程序之間不同的地方在於如何配置缺省對象,以及如何通過定製對象來添加用戶界面和行爲。

雖然應用程序的界面和基本行爲的定製發生在定製代碼的內部,但是,還有很多定製需要在應用程序的最高級別上進行。這些高級的定製會影響應用程序和系統、以及和設備上的其它程序之間的交互方式,因此,理解何時需要定製、何時缺省行爲就已經足夠是很重要的。本章將概要介紹核心應用程序架構和高級別的定製點,幫助您確定什麼時候應該定製,什麼時候應該使用缺省的行爲。

核心應用程序架構

從應用程序啓動到退出的過程中,UIKit框架負責管理大部分關鍵的基礎設施。iPhone應用程序不斷地從系統接收事件,而且必須響應那些事件。接收事件是UIApplication對象的工作,但是,響應事件則需要您的定製代碼來處理。爲了理解事件響應需要在哪裏進行,我們有必要對iPhone應用程序的整個生命週期和事件週期有一些理解。本文的下面部分將描述這些週期,同時還對iPhone應用程序開發過程中使用的一些關鍵設計模式進行總結。

應用程序的生命週期

應用程序的生命週期是由發生在程序啓動到終止期間的一序列事件構成的。在iPhone OS中,用戶可以通過輕點Home屏幕上的圖標來啓動應用程序。在輕點圖標之後的不久,系統就會顯示一個過渡圖形,然後調用相應的main函數來啓動應用程序。從這個點之後,大量的初始化工作就會交給UIKit,由它裝載應用程序的用戶界面和準備事件循環。在事件循環過程中,UIKit會將事件分發給您的定製對象及響應應用程序發出的命令。當用戶進行退出應用程序的操作時,UIKit會通知應用程序,並開始應用程序的終止過程。

圖1-1顯示了一個簡化了的iPhone應用程序生命週期。這個框圖展示了發生在應用程序啓動到退出過程中的事件序列。在應用程序初始化和終止的時候,UIKit會嚮應用程序委託對象發送特定的消息,使其知道正在發生的事件。在事件循環中,UIKit將事件派發給應用程序的定製事件處理器。有關初始化和終止事件的如何處理的信息,將在隨後的“初始化和終止”部分進行討論;事件處理的過程則在“事件處理週期”部分介紹,在後面的章節也還有更爲詳細的討論。

圖1-1  應用程序的生命週期

Application life cycle

主函數

在iPhone的應用程序中,main函數僅在最小程度上被使用,應用程序運行所需的大多數實際工作由UIApplicationMain函數來處理。因此,當您在Xcode中開始一個新的應用程序工程時,每個工程模板都會提供一個main函數的標準實現,該實現和“處理關鍵的應用程序任務”部分提供的實現是一樣的。main例程只做三件事:創建一個自動釋放池,調用UIApplicationMain函數,以及使用自動釋放池。除了少數的例外,您永遠不應該改變這個函數的實現。

程序清單1-1  iPhone應用程序的main函數

#import <UIKit/UIKit.h>
 
int main(int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, nil);
    [pool release];
    return retVal;
}

請注意:自動釋放池用於內存管理,它是Cocoa的一種機制,用於延緩釋放具有一定功能的代碼塊中創建的對象。有關自動釋放池的更多信息,請參見Cocoa內存管理編程指南;如果需要了解與自動釋放池有關的具體內存管理規則,則請參見“恰當地分配內存”部分。

程序清單的核心代碼是UIApplicationMain函數,它接收四個參數,並將它們用於初始化應用程序。傳遞給該函數的缺省值並不需要修改,但是它們對於應用程序啓動的作用還是值得解釋一下。除了傳給main函數的argcargv之外,該函數還需要兩個字符串參數,用於標識應用程序的首要類(即應用程序對象所屬的類)和應用程序委託類。如果首要類字符串的值爲nil, UIKit就缺省使用UIApplication類;如果應用程序委託類爲nil,UIKit就會將應用程序主nib文件(針對通過Xcode模板創建的應用程序)中的某個對象假定爲應用程序的委託對象。如果您將這些參數設置爲非nil值,則在應用程序啓動時,UIApplicationMain函數會創建一個與傳入值相對應的類實例,並將它用於既定的目的。因此,如果您的應用程序使用了UIApplication類的定製子類(這種做法是不推薦的,但確實是可能的),就需要在第三個參數指定該定製類的類名。

應用程序的委託

監控應用程序的高級行爲是應用程序委託對象的責任,而應用程序委託對象是您提供的定製類實例。委託是一種避免對複雜的UIKit對象(比如缺省的UIApplication對象)進行子類化的機制。在這種機制下,您可以不進行子類化和方法重載,而是將自己的定製代碼放到委託對象中,從而避免對複雜對象進行修改。當您感興趣的事件發生時,複雜對象會將消息發送給您定製的委託對象。您可以通過這種“掛鉤”執行自己的定製代碼,實現需要的行爲。

重要提示:委託模式的目的是使您在創建應用程序的時候省時省力,因此是非常重要的設計模式。如果您需要概要了解iPhone應用程序中使用的重要設計模式,請參見“基本設計模式”部分;如果需要對委託和其它UIKit設計模式的詳細描述,則請參見Cocoa基本原理指南部分。

 

應用程序的委託對象負責處理幾個關鍵的系統消息。每個iPhone應用程序都必須有應用程序委託對象,它可以是您希望的任何類的實例,但需要遵循UIApplicationDelegate協議,該協議的方法定義了應用程序生命週期中的某些掛鉤,您可以通過這些方法來實現定製的行爲。雖然您不需要實現所有的方法,但是每個應用程序委託都應該實現“處理關鍵的應用程序任務”部分中描述的方法。

有關UIApplicationDelegate協議方法的更多信息請參見UIApplicationDelegate協議參考

主Nib文件

初始化的另一個任務是裝載應用程序的主nib文件。如果應用程序的信息屬性列表(Info.plist)文件中含有NSMainNibFile鍵,則作爲初始化過程的一個部分,UIApplication對象會裝載該鍵指定的nib文件。主nib文件是唯一一個自動裝載的nib文件,其它的nib文件可以在稍後根據需要進行裝載。

Nib文件是基於磁盤的資源文件,用於存儲一或多個對象的快照。iPhone應用程序的主nib文件通常包含一個窗口對象和一個應用程序委託對象,還可能包含一個或多個管理窗口的其它重要對象。裝載一個nib文件會使該文件中的對象被重新構造,從而將每個對象的磁盤表示轉化爲應用程序可以操作的內存對象。從nib文件中裝載的對象和通過編程方式創建的對象之間沒有區別。然而,對於用戶界面而言,以圖形的方式(使用Interface Builder程序)創建與用戶界面相關聯的對象並將它們存儲在nib文件中通常比以編程的方式進行創建更加方便。

有關nib文件及其在iPhone應用程序中如何使用的更多信息,請參見“Nib文件”部分,有關如何爲應用程序指定主nib文件的信息則請參見“信息屬性列表”部分。

事件處理週期

在應用程序初始化之後,UIApplicationMain函數就會啓動管理應用程序事件和描畫週期的基礎組件,如圖1-2所示。在用戶和設備進行交互的時候,iPhone OS會檢測觸摸事件,並將事件放入應用程序的事件隊列。然後,UIApplication對象的事件處理設施會從隊列的上部逐個取出事件,將它分發到最適合對其進行處理的對象。舉例來說,在一個按鍵上發生的觸摸事件會被分發到對應的按鍵對象。事件也可以被分發給控制器對象和應用程序中不直接負責處理觸摸事件的其它對象。

圖1-2  事件和描畫週期

The event and drawing cycle

在iPhone OS的多點觸摸事件模型中,觸摸數據被封裝在事件對象(UIEvent)中。爲了跟蹤觸摸動作,事件對象中包含一些觸摸對象(UITouch),每個觸摸對象都對應於一個正在觸摸屏幕的手指。當用戶把手指放在屏幕上,然後四處移動,並最終離開屏幕的時候,系統通過對應的觸摸對象報告每個手指的變化。

在啓動一個應用程序時,系統會爲該程序創建一個進程和一個單一的線程。這個初始線程成爲應用程序的主線程,UIApplication對象正是在這個線程中建立主運行循環及配置應用程序的事件處理代碼。圖1-3顯示了事件處理代碼和主運行循環的關係。系統發送的觸摸事件會在隊列中等待,直到被應用程序的主運行循環處理

圖1-3  在主運行循環中處理事件

Processing events in the main run loop

請注意:運行循環負責監視指定執行線程的輸入源。當輸入源有數據需要處理的時候,運行循環就喚醒相應的線程,並將控制權交給輸入源的處理器代碼。處理器在完成任務後將控制權交回運行循環,然後,運行循環就處理下一個事件。如果沒有其它事件,運行循環會使線程進入休眠狀態。您可以通過Foundation框架的NSRunLoop類來安裝自己的輸入源,包括端口和定時器。更多有關NSRunLoop和運行循環的一般性討論,請參見線程編程指南

UIApplication對象用一個處理觸摸事件的輸入源來配置主運行循環,使觸摸事件可以被派發到恰當的響應者對象。響應者對象是繼承自UIResponder類的對象,它實現了一或多個事件方法,以處理觸摸事件不同階段發生的事件。應用程序的響應者對象包括UIApplicationUIWindowUIView、及所有UIView子類的實例。應用程序通常將事件派發給代表應用程序主窗口的UIWindow對象,然後由窗口對象將事件傳送給它的第一響應者,通常是發生觸摸事件的視圖對象(UIView)。

除了定義事件處理方法之外,UIResponder類還定義了響應者鏈的編程結構。響應者鏈是爲實現Cocoa協作事件處理而設計的機制,它由應用程序中一組鏈接在一起的響應者對象組成,通常以第一響應者作爲鏈的開始。當發生某個事件時,如果第一響應者對象不能處理,就將它傳遞給響應者鏈中的下一個對象。消息繼續在鏈中傳遞—從底層的響應者對象到諸如窗口、應用程序、和應用程序委託這樣的高級響應者對象—直到事件被處理。如果事件最終沒有被處理,就會被丟棄。

進行事件處理的響應者對象可能發起一系列程序動作,結果導致應用程序重畫全部或部分用戶界面(也可能導致其它結果,比如播放一個聲音)。舉例來說,一個控鍵對象(也就是一個UIControl的子類對象)在處理事件時向另一個對象(通常是控制器對象,負責管理當前活動的視圖集合)發送動作消息。在處理這個動作消息時,控制器可能以某種方式改變用戶界面或者視圖的位置,而這又要求某些視圖對自身進行重畫。如果這種情況發生,則視圖和圖形基礎組件會接管控制權,儘可能以最有效的方式處理必要的重畫事件。

更多有關事件、響應者、和如何在定製對象中處理事件的信息,請參見“事件處理”部分;更多有關窗口及視圖如何與事件處理機制相結合的信息,請參見“視圖交互模型”部分;有關圖形組件及視圖如何被更新的更多信息,則請參見“視圖描畫週期”部分。

基本設計模式

UIKit框架的設計結合了很多在Mac OS X Cocoa應用程序中使用的設計模式。理解這些設計模式對於創建iPhone應用程序是很關鍵的,我們值得爲此花上幾分鐘時間。下面部分將簡要概述這些設計模式。

表1-1  iPhone應用程序使用的設計模式

設計模式

描述

模型-視圖-控制器

模型-視圖-控制器(MVC)模式將您的代碼分割爲幾個獨立的部分。模型部分定義應用程序的數據引擎,負責維護數據的完整性;視圖部分定義應用程序的用戶界面,對顯示在用戶界面上的數據出處則沒有清楚的認識;控制器部分則充當模型和控制器的橋樑,幫助實現數據和顯示的更新。

委託

委託模式可以對複雜對象進行修改而不需要子類化。與子類化不同的是,您可以照常使用複雜對象,而將對其行爲進行修改的定製代碼放在另一個對象中,這個對象就稱爲委託對象。複雜對象需要在預先定義好的時點上調用委託對象的方法,使其有機會運行定製代碼。

目標-動作

控件通過目標-動作模式將用戶的交互通知給您的應用程序。當用戶以預先定義好的方式(比如輕點一個按鍵)進行交互時,控件就會將消息(動作)發送給您指定的對象(目標)。接收到動作消息後,目標對象就會以恰當的方式進行響應(比如在按動按鍵時更新應用程序的狀態)。

委託內存模型

Objective-C使用引用計數模式來確定什麼時候應該釋放內存中的對象。當一個對象剛剛被創建時,它的引用計數是1。然後,其它對象可以通過該對象的retainrelease、或autorelease方法來增加或減少引用計數。當對象的引用計數變爲0時,Objective-C運行環境會調用對象的清理例程,然後解除分配該對象。

有關這些設計模式更爲詳盡的討論請參見Cocoa基本原理指南

應用程序運行環境

iPhone OS的運行環境被設計爲快速而安全的程序執行環境。下面的部分這個運行環境的關鍵部分,並就如何在這個環境中進行操作提供一些指導。

啓動過程快,使用時間短

iPhone OS設備的優勢是它們的便捷性。用戶通常從口袋裏掏出設備,用上幾秒或幾分鐘,就又放回口袋中了。在這個過程中,用戶可能會打電話、查找聯繫人、改變正在播放的歌曲、或者取得一片信息。

在iPhone OS中,每次只能有一個前臺應用程序。這意味着每次用戶在Home屏幕上輕點您的應用程序圖標時,您的程序必須快速啓動和初始化,以儘可能減少延遲。如果您的應用程序花很長時間來啓動,用戶可能就不喜歡了。

除了快速啓動,您的應用程序還必須做好快速退出的準備。每次用戶離開您的應用程序時,無論是按下Home鍵還是通過軟件提供的功能打開了另一個應用程序,iPhone OS會通知您的應用程序退出。在那個時候,您需要儘快將未保存的修改保存到磁盤上。如果您的應用程序退出的時間超過5秒,系統可能會立刻終止它的運行。

當用戶切換到另一個應用程序時,雖然您的程序不是運行在後臺,但是我們鼓勵您使它看起來好像是在後臺運行。當您的程序退出時,除了對未保存的數據進行保存之外,還應該保存當前的狀態信息;而在啓動時,則應該尋找這些狀態信息,並將程序恢復到最後一次使用時的狀態。這樣可以使用戶回到最後一次使用時的狀態,使用戶體驗更加一致。以這種方式保存用戶的當前位置還可以避免每次啓動都需要經過多個屏幕才能找到需要的信息,從而節省使用的時間。

應用程序沙箱

由於安全的原因,iPhone OS將每個應用程序(包括其偏好設置信息和數據)限制在文件系統的特定位置上。這個限制是安全特性的一部分,稱爲應用程序的“沙箱”。沙箱是一組細粒度的控制,用於限制應用程序對文件、偏好設置、網絡資源、和硬件等的訪問。在iPhone OS中,應用程序和它的數據駐留在一個安全的地方,其它應用程序都不能進行訪問。在應用程序安裝之後,系統就通過計算得到一個不透明的標識,然後基於應用程序的根目錄和這個標識構建一個指向應用程序家目錄的路徑。因此,應用程序的家目錄具有如下結構:

  • /ApplicationRoot/ApplicationID/

在安裝過程中,系統會創建應用程序的家目錄和幾個關鍵的子目錄,配置應用程序沙箱,以及將應用程序的程序包拷貝到家目錄上。將應用程序及其數據放在一個特定的地方可以簡化備份-並-恢復操作,還可以簡化應用程序的更新及卸載操作。有關係統爲每個應用程序創建的專用目錄、應用程序更新、及備份-並-恢復操作的更多信息,請參見“文件和數據管理”部分。

重要提示:沙箱可以限制攻擊者對其它程序和系統造成的破壞,但是不能防止攻擊的發生。換句話說,沙箱不能使您的程序避免惡意的直接攻擊。舉例來說,如果在您的輸入處理代碼中有一個可利用的緩衝區溢出,而您又沒有對用戶輸入進行正當性檢查,則攻擊者可能仍然可以使您的應用程序崩潰,或者通過這種漏洞來執行攻擊者的代碼。

 

虛擬內存系統

在本質上,iPhone OS使用與Mac OS X同樣的虛存系統。在iPhone OS中,每個程序都仍然有自己的虛擬地址空間,但其可用的虛擬內存受限於現有的物理內存的數量(這和Mac OS X不同)。這是因爲當內存用滿的時候,iPhone OS並不將非永久內存頁面(volatile pages)寫入到磁盤。相反,虛擬內存系統會根據需要釋放永久內存(nonvolatile memory),確保爲正在運行的應用程序提供所需的空間。內存的釋放是通過刪除當前沒有正在使用或包含只讀內容(比如代碼頁面)的內存頁面來實現的,這樣的頁面可以在稍後需要使用的時候重新裝載到內存中。

如果內存還是不夠,系統也可能向正在運行的應用程序發出通告,要求它們釋放額外的內存。所有的應用程序都應該響應這種通告,並儘自己所能減輕系統的內存壓力。有關如何在應用程序中處理這種通告的更多信息,請參見“觀察低內存警告”部分。

自動休眠定時器

iPhone OS試圖省電的一個方法是使用自動休眠定時器。如果在一定的時間內沒有檢測到觸摸事件,系統最初會使屏幕變暗,並最終完全關閉屏幕。大多數開發者都應該讓這個定時器打開,但是,遊戲和不使用觸摸輸入的應用程序開發者可以禁用這個定時器,使屏幕在應用程序運行時不會變暗。將共享的UIApplication對象的idleTimerDisabled屬性設置爲YES,就可以禁用自動休眠定時器。

由於禁用休眠定時器會導致更大的電能消耗,所以開發者應該盡一切可能避免這樣做。只有地圖程序、遊戲、以及不依賴於觸摸輸入而又需要在設備屏幕上顯示內容的應用程序才應該考慮禁用休眠定時器。音頻應用程序不需要禁用這個定時器,因爲在屏幕變暗之後,音頻內容可以繼續播放。如果您禁用了定時器,請務必儘快重新激活它,使系統可以更省電。有關應用程序如何省電的其它貼士,請參見“減少電力消耗”部分。

應用程序的程序包

當您連編iPhone程序時,Xcode會將它組織爲程序包程序包是文件系統中的一個目錄,用於將執行代碼和相關資源集合在一個地方。iPhone應用程序包中包含應用程序的執行文件和應用程序需要用到的所有資源(比如應用程序圖標、其它圖像、和本地化內容)。表1-2列出了一個典型的iPhone應用程序包中的內容(爲了便於說明,我們稱之爲MyApp)。這個例子只是爲了演示,表中列出的一些文件可能並不出現在您自己的應用程序包中。

表1-2  一個典型的應用程序包

文件

描述

MyApp

包含應用程序代碼的執行文件,文件名是略去.app後綴的應用程序名。這個文件是必需的。

Settings.bundle

設置程序包是一個文件包,用於將應用程序的偏好設置加入到Settings程序中。這種程序包中包含一些屬性列表和其它資源文件,用於配置和顯示您的偏好設置。更多信息請參見“顯示應用程序的偏好設置”部分。

Icon.png

這是個57 x 57像素的圖標,顯示在設備的Home屏幕上,代表您的應用程序。這個圖標不應該包含任何光亮效果。系統會自動爲您加入這些效果。這個文件是必須的。更多有關這個圖像文件的信息,請參見“應用程序圖標和啓動圖像”部分。

Icon-Settings.png

這是一個29 x 29像素的圖標,用於在Settings程序中表示您的應用程序。如果您的應用程序包含設置程序包,則在Settings程序中,這個圖標會顯示在您的應用程序名的邊上。如果您沒有指定這個圖標文件,系統會將Icon.png文件按比例縮小,然後用做代替文件。有關這個圖像文件的更多信息,青參見“顯示應用程序的偏好設置”部分。

MainWindow.nib

這是應用程序的主nib文件,包含應用程序啓動時裝載的缺省用戶界面對象。典型情況下,這個nib文件包含應用程序的主窗口對象和一個應用程序委託對象實例。其它界面對象則或者從其它nib文件裝載,或者在應用程序中以編程的方式創建(主nib文件的名稱可以通過Info.plist文件中的NSMainNibFile鍵來指定,進一步的信息請參見“信息屬性列表”部分)。

Default.png

這是個480 x 320像素的圖像,在應用程序啓動的時候顯示。系統使用這個文件作爲臨時的背景,直到應用程序完成窗口和用戶界面的裝載。有關這個圖像文件的信息請參見“應用程序圖標和啓動圖像”部分。

iTunesArtwork

這是個512 x 512的圖標,用於通過ad-hoc方式發佈的應用程序。這個圖標通常由App Store來提供,但是通過ad-hoc方式分發的應用程序並不經由App Store,所以在程序包必須包含這個文件。iTunes用這個圖標來代表您的程序(如果您的應用程序在App Store上發佈,則在這個屬性上指定的文件應該和提交到App Store的文件保持一致(通常是個JPEG或PNG 文件),文件名必須和左邊顯示的一樣,而且不帶文件擴展名)。

Info.plist

這個文件也叫信息屬性列表,它是一個定義應用程序鍵值的屬性列表,比如程序包ID、版本號、和顯示名稱。進一步的信息請參見“信息屬性列表”部分。這個文件是必需的。

sun.png (或其它資源文件)

非本地化資源放在程序包目錄的最上層(在這個例子中,sun.png表示一個非本地化的圖像)。應用程序在使用非本地化資源時,不需要考慮用戶選擇的語言設置。

en.lproj

fr.lproj

es.lproj

其它具體語言的工程目錄

本地化資源放在一些子目錄下,子目錄的名稱是ISO 639-1定義的語言縮寫加上.lproj後綴組成的(比如en.lprojfr.lproj、和es.lproj目錄分別包含英語、法語、和西班牙語的本地化資源)。更多信息請參見“國際化您的應用程序”部分。

iPhone應用程序應該是國際化的。程序支持的每一種語言都有一個對應的語言.lproj文件夾。除了爲應用程序提供定製資源的本地化版本之外,您還可以本地化您的應用程序圖標(Icon.png)、缺省圖像(Default.png)、和Settings圖標(Icon-Settings.png),只要將同名文件放到具體語言的工程目錄就可以了。然而,即使您提供了本地化的版本,也還是應該在應用程序包的最上層包含這些文件的缺省版本。當某些的本地化版本不存在的時候,系統會使用缺省版本。

您可以通過NSBundle類的方法或者與CFBundleRef類型相關聯的函數來獲取應用程序包中本地化和非本地化圖形及聲音資源的路徑。舉例來說,如果您希望得到圖像文件sun.png(顯示在“響應中斷”部分中)的路徑並通過它創建一個圖像文件,則需要下面兩行Objective-C代碼:

NSString* imagePath = [[NSBundle mainBundle] pathForResource:@"sun" ofType:@"png"];
UIImage* sunImage = [[UIImage alloc] initWithContentsOfFile:imagePath];

代碼中的mainBundle類方法用於返回一個代表應用程序包的對象。有關資源裝載的信息請參見資源編程指南

信息屬性列表

信息屬性列表是一個名爲Info.plist的文件,通過Xcode創建的每個iPhone應用程序都包含一個這樣的文件。屬性列表中的鍵值對用於指定重要的應用程序運行時配置信息。信息屬性列表的元素被組織在一個層次結構中,每個結點都是一個實體,比如數組、字典、字符串、或者其它數值類型。

在Xcode中,您可以通過在Project菜單中選擇Edit Active Target TargetName命令、然後在目標的Info窗口中點擊Properties控件來訪問信息屬性列表。Xcode會顯示如圖1-4所示的信息面板。

圖1-4  目標Info窗口的屬性面板

The Properties pane of a target’s Info window

屬性面板顯示的是程序包的一些屬性,但並不是所有屬性都顯示在上面。當您選擇“Open Info.plist as File” 按鍵或在Xcode工程中選擇Info.plist文件時,Xcode會顯示如圖1-5所示的屬性列表編輯器窗口,您可以通過這個窗口來編輯屬性值和添加鍵-值對。您還可以查看添加到Info.plist文件中的實際鍵名,具體操作是按住Control鍵的同時點擊編輯器中的信息屬性列表項目,然後選擇上下文菜單中的Show Raw Keys/Values命令。

圖1-5  信息屬性列表編輯器

The information property list editor

Xcode會自動設置某些屬性的值,其它屬性則需要顯式設置。表1-3列出了一些重要的鍵,供您在自己的Info.plist文件中使用(在缺省情況下,Xcode不會直接顯示實際的鍵名,因此,下表在括號中列出了這些鍵在Xcode中顯示的字符串。您可以查看所有鍵的實際鍵名,具體做法是按住Control鍵的同時點擊編輯器中的信息屬性列表項目,然後選擇上下文菜單中的Show Raw Keys/Values命令)。有關屬性列表文件可以包含的完整屬性列表及系統如何使用這些屬性的信息,請參見運行環境配置指南

表1-3  Info.plist文件中重要的鍵

CFBundleDisplayName (程序包顯示名)

顯示在應用程序圖標下方的名稱。這個值應該本地化爲所有支持的語言。

CFBundleIdentifier (程序包標識)

這是由您提供的標識字符串,用於在系統中標識您的應用程序。這個字符串必須是一個統一的類型標識符(UTI),僅包含字母數字(A-Za-z0-9),連字符(-),和句號(.);且應該使用反向DNS格式。舉例來說,如果您的公司的域名爲Ajax.com,且您創建的應用程序名爲Hello,則可以將字符串com.Ajax.Hello作爲應用程序包的標識。

程序包的標識用於驗證應用程序的簽名。

CFBundleURLTypes (URL類型)

這是應用程序能夠處理的URL類型數組。每個URL類型都是一個字典,定義一種應用程序能夠處理的模式(如httpmailto)。應用程序可以通過這個屬性來註冊定製的URL模式。

CFBundleVersion (程序包版本號)

這是一個字符串,指定程序包的連編版本號。它的值是單調遞增的,由一或多個句號分隔的整數組成。這個值不能被本地化。

LSRequiresIPhoneOS

這是一個Boolean值,用於指示程序包是否只能運行在iPhone OS 系統上。Xcode自動加入這個鍵,並將它的值設置爲true。您不應該改變這個鍵的值。

NSMainNibFile (主nib文件的名稱)

這是一個字符串,指定應用程序主nib文件的名稱。如果您希望使用其它的nib文件(而不是Xcode爲工程創建的缺省文件)作爲主nib文件,可以將該nib文件名關聯到這個鍵上。nib文件名不應該包含.nib擴展名。

UIStatusBarStyle

這是個字符串,標識程序啓動時狀態條的風格。這個鍵的值基於UIApplication.h頭文件中聲明的UIStatusBarStyle常量。缺省風格是UIStatusBarStyleDefault。在啓動完成後,應用程序可以改變狀態條的初始風格。

UIStatusBarHidden

這個一個Boolean值,指定在應用程序啓動的最初階段是否隱藏狀態條。將這個鍵值設置爲true將隱藏狀態條。缺省值爲false

UIInterfaceOrientation

這是個字符串,標識應用程序用戶界面的初始方向。這個鍵的值基於UIApplication.h頭文件中聲明的UIInterfaceOrientation 常量。缺省風格是UIInterfaceOrientationPortrait

有關將應用程序啓動爲景觀模式的更多信息,請參見“以景觀模式啓動”部分。

UIPrerenderedIcon

這個一個Boolean值,指示應用程序圖標是否已經包含發光和斜面效果。這個屬性缺省值爲false。如果您不希望系統在您的原圖上加入這些效果,則將它設置爲true。

UIRequiredDeviceCapabilities

這是個信息鍵,作用是使iTunes和App Store知道應用程序運行需要依賴於哪些與設備相關的特性。iTunes和移動App Store程序使用這個列表來避免將應用程序安裝到不支持所需特性的設備上。

這個鍵的值可以是一個數組或者字典如果您使用的是數組,則數組中存在某個鍵就表示該鍵對應的特性是必需的;如果您使用的是字典,則必須爲每個鍵指定一個Boolean值,表示該鍵是否需要。無論哪種情況,不包含某個鍵表示該鍵對應的特性不是必需的。

如果您需要可包含在這個字典中的鍵列表,請參見表1-4。這個鍵在iPhone OS 3.0及更高版本上才被支持。

UIRequiresPersistentWiFi

這是個Boolean值,用於通知系統應用程序是否使用Wi-Fi網絡進行通訊。如果您的應用程序需要在一段時間內使用Wi-Fi,則應該將這個鍵值設置爲true;否則,爲了省電,設備會在30分鐘內關閉Wi-Fi連接。設置這個標誌還可以讓系統在Wi-Fi網絡可用但未被使用的時候顯示網絡選擇對話框。這個鍵的缺省值是false

請注意,當設備處於閒置狀態(也就是屏幕被鎖定的狀態)時,這個屬性的值爲true是沒有作用的。這種情況下,應用程序會被認爲是不活動的,雖然它可能在某些級別上還可以工作,但是沒有Wi-Fi連接。

UISupportedExternalAccessoryProtocols

這是個字符串數組,標識應用程序支持的配件協議。配件協議是應用程序和連接在iPhone或iPod touch上的第三方硬件進行通訊的協議。系統使用這個鍵列出的協議來識別當配件連接到設備上時可以打開的應用程序。

有關配件和協議的更多信息,請參見“和配件通訊”部分。這個鍵只在iPhone OS 3.0和更高版本上支持。

UIViewGroupOpacity

這是個Boolean值,用於指示Core Animation子層是否繼承其超層的不透明特性。這個特性使開發者可以在仿真器上進行更爲複雜的渲染,但是對性能會有顯著的影響。如果屬性列表上沒有這個鍵,則其缺省值爲NO

這個鍵只在iPhone OS 3.0和更高版本上支持。

UIViewEdgeAntialiasing

這是個Boolean值,用於指示在描畫不和像素邊界對齊的層時,Core Animation層是否進行抗鋸齒處理。這個特性使開發者可以在仿真器上進行更爲複雜的渲染,但是對性能會有顯著的影響。如果屬性列表上沒有這個鍵,則其缺省值爲NO

這個鍵只在iPhone OS 3.0和更高版本上支持。

如果信息屬性文件中的屬性值是顯示在用戶界面上的字符串,則應該進行本地化,特別是當Info.plist中的字符串值是與本地化語言子目錄下InfoPlist.strings文件中的字符串相關聯的鍵時。更多信息請參見“國際化您的應用程序”部分。

表1-4列出了和UIRequiredDeviceCapabilities鍵相關聯的數組或字典中可以包含的鍵。您應該僅包含應用程序確實需要的鍵。如果應用程序可以通過不執行某些代碼路徑來適應設備特性不存在的情況,則不需要使用對應的鍵。

表1-4  UIRequiredDeviceCapabilities鍵的字典鍵

描述

telephony

如果您的應用程序需要Phone程序,則包含這個鍵。如果您的應用程序需要打開tel模式的URL,則可能需要這個特性。

sms

如果您的應用程序需要Messages程序,則包含這個鍵。如果您的應用程序需要打開sms模式的URL,則可能需要這個特性。

still-camera

如果您的應用程序使用UIImagePickerController接口來捕捉設備照相機的圖像時,需要包含這個鍵。

auto-focus-camera

如果您的應用程序需要設備照相機的自動對焦能力,則需要包含這個鍵。雖然大多數開發者應該不需要,但是如果您的應用程序支持微距攝影,或者需要更高銳度的圖像以進行某種處理,則可能需要包含這個鍵。

video-camera

如果您的應用程序使用UIImagePickerController接口來捕捉設備攝像機的視頻時,需要包含這個鍵。

wifi

當您的應用程序需要設備的網絡特性時,包含這個鍵。

accelerometer

如果您的應用程序使用UIAccelerometer接口來接收加速計事件,則需要包含這個鍵。如果您的程序僅需要檢測設備的方向變化,則不需要。

location-services

如果您的應用程序使用Core Location框架來訪問設備的當前位置,則需要包含這個鍵(這個鍵指的是一般的位置服務特性。如果您需要GPS級別的精度,則還應該包含gps鍵)。

gps

如果您的應用程序需要GPS(或者AGPS)硬件,以獲得更高精度的位置信息,則包含這個鍵。如果您包含了這個鍵,就應該同時包含location-services鍵。如果您的程序需要更高精度的位置數據,而不是由蜂窩網絡或Wi-fi信號提供的數據,則應該要求只接收GPS數據。

magnetometer

如果您的應用程序使用Core Location框架接收與方向有關的事件時,則需要包含這個鍵。

microphone

如果您的應用程序需要使用內置的麥克風或支持提供麥克風的外設,則包含這個鍵。

opengles-1

如果您的應用程序需要使用OpenGL ES 1.1 接口,則包含這個鍵。

opengles-2

如果您的應用程序需要使用OpenGL ES 2.0 接口,則包含這個鍵。

應用程序圖標和啓動圖像

顯示在用戶Home屏幕上的圖標文件的缺省文件名爲Icon.png(雖然通過Info.plist文件中的CFBundleIconFile屬性可以進行重命名)。它應該是一個位於程序包最上層目錄的PNG文件。應用程序圖標應該是一個57 x 57像素的圖像,不帶任何刨光和圓角斜面效果。典型情況下,系統在顯示之前會將這些效果應用到圖標上。然而,在應用程序的Info.plist文件中加入UIPrerenderedIcon鍵可以重載這個行爲,更多信息請參見表1-3

請注意:如果您以ad-hoc的方式(而不是通過App Store)將應用程序發佈給本地用戶,則程序包中還應該包含一個512 x 512像素版本的應用程序圖標,命名爲iTunesArtwork。在分發您的應用程序時,iTunes需要顯示這個文件提供的圖標。

應用程序的啓動圖像文件的文件名爲Default.png。這個圖像應該和應用程序的初始界面比較相似;系統在應用程序準備好顯示用戶界面之前顯示啓動文件,使用戶覺得啓動速度很快。啓動圖像也應該是PNG圖像文件,位於應用程序包的頂層目錄。如果應用程序是通過URL啓動的,則系統會尋找名爲Default-scheme.png的啓動文件,其中scheme是URL的模式。如果該文件不存在,才選擇Default.png文件。

將一個圖像文件加入到Xcode工程的具體做法是從Project菜單中選擇Add to Project命令,在瀏覽器中定位目標文件,然後點擊Add按鍵。

請注意:除了程序包頂層目錄中的圖標和啓動圖像,您還可以在應用程序中具體語言的工程子目錄下包含這些圖像文件的本地化版本。更多有關應用程序本地化資源的信息請參見“國際化您的應用程序”部分。

Nib文件

nib文件是一種數據文件,用於存儲可在應用程序需要時使用的一些“凍結”的對象。大多數情況下,應用程序使用nib文件來存儲構成用戶界面的窗口和視圖。當您將nib文件載入應用程序時,nib裝載代碼會將文件中的內容轉化爲應用程序可以操作的真正對象。通過這個機制,nib文件省去了用代碼創建那些對象的工作。

Interface Builder是一個可視化的設計環境,您可以用它來創建nib文件。您可以將標準對象(比如UIKit框架中提供的窗口和視圖)和Xcode工程中的定製對象放到nib文件中。在Interface Builder中創建視圖層次相當簡單,只需要對視圖對象進行簡單拖拽就可以了。您也可以通過查看器窗口來配置每個對象的屬性,以及通過創建對象間的連接來定義它們在運行時的關係。您所做的改變最終都會作爲nib文件的一部分存儲到磁盤上。

在運行時,當您需要nib文件中包含的對象時,就將nib文件裝載到程序中。典型情況下,裝載nib文件的時機是當用戶界面發生變化和需要在屏幕上顯示某些新視圖的時候。如果您的應用程序使用視圖控制器,則視圖控制器會自動處理nib文件的裝載過程,當然,您也可以通過NSBundle類的方法自行裝載。

有關如何設計應用程序用戶界面的更多信息,請參見iPhone用戶界面指南。有關如何創建nib文件的信息則參見Interface Builder用戶指南

處理關鍵的應用程序任務

本部分將描述幾個所有iPhone應用程序都應該處理的任務。這些任務是整個應用程序生命週期的一部分,因此也是將應用程序集成到iPhone OS系統的重要方面。在最壞的情況下,沒有很好地處理其中的某些任務甚至可能會導致應用程序被操作系統終止。

初始化和終止

在初始化和終止過程中,UIApplication類會嚮應用程序的委託發送恰當的消息,使其執行必要的任務。雖然系統並不要求您的應用程序響應這些消息,但是,幾乎所有的iPhone應用程序都應該處理這些消息。初始化是您爲應用程序準備用戶界面及使其進入初始運行狀態的階段。類似地,在終止階段,您應該把未保存的數據和關鍵的應用程序狀態寫入磁盤。

由於一個iPhone應用程序必須在其它應用程序啓動之前退出,所以花在初始化和終止階段的執行時間要儘可能少。初始化階段並不適合裝載大的、卻又不需要馬上使用的數據結構。在開始階段,您的目標應該是儘可能快地顯示應用程序的用戶界面,最好是使它進入最後一次退出的狀態。如果您的應用程序在啓動過程中需要更多的時間來裝載網絡數據,或者執行一些可能很慢的任務,則應該首先顯示出用戶界面並運行起來,然後在後臺線程中執行速度慢的任務。這樣,您就有機會向用戶顯示進度條和其它反饋信息,指示應用程序正在裝載必要的數據,或者正在執行重要的任務。

表1-5列舉出UIApplicationDelegate協議定義的方法,您在應用程序委託中需要實現這些協議方法,以處理初始化和終止的事務。表中還列出了您在每個方法中應該執行的關鍵事務。

表1-5  應用程序委託的責任

委託方法

描述

applicationDidFinishLaunching:

使用這個方法來將應用程序恢復到上一個會話的狀態。您也可以在這個方法中執行應用程序數據結構和用戶界面的定製初始化。

applicationWillTerminate:

使用這個方法來將未存數據或關鍵的應用程序狀態存入磁盤。您也可以在這個方法中執行額外的清理工作,比如刪除臨時文件。

響應中斷

除了Home按鍵可以終止您的應用程序之外,系統也可以暫時中斷您的應用程序,使用戶得以響應一些重要的事件。舉例來說,應用程序可能被呼入的電話、SMS信息、日曆警告、或者設備上的Sleep按鍵所打斷。按下Home按鍵會終止您的應用程序,而上述這些中斷則只是暫時的。如果用戶忽略這些中斷,您的應用程序可以象之前那樣繼續運行;然而,如果用戶決定接電話或迴應SMS信息,系統就會開始終止您的程序。

圖1-6顯示了在電話、SMS信息、或者日曆警告到來時發生的事件序列。緊接在圖後面的步驟說明更爲詳細地描述了事件序列的關鍵點,包括您在響應每個事件時應該做的事項。這個序列並不反映當用戶按下Sleep/Wake按鍵時發生的情景;該場景的事件序列在步驟說明之後的部分進行描述。

圖1-6  中斷過程的事件流程

The flow of events during an interruption
  1. 系統檢測到有電話、SMS信息、或者日曆警告發生。

  2. 系統調用應用程序委託applicationWillResignActive:方法,同時禁止將觸摸事件發送給您的應用程序。

    中斷會導致應用程序暫時失去控制權。如果控制權的丟失會影響程序的行爲或導致不好的用戶體驗,您就應該在委託方法中採取恰當的步驟進行規避。舉例來說,如果您的程序是個遊戲,就應該暫停。您還應該禁用定時器、降低OpenGL的幀率(如果正在使用OpenGL的話),通常還應該使應用程序進行休眠狀態。在這休眠狀態下,您的應用程序繼續運行,但是不應該做任何重要的工作。

  3. 系統顯示一個帶有事件信息的警告窗口。用戶可以選擇忽略或響應該事件。

  4. 如果用戶忽略該事件,系統就調用應用程序委託的applicationDidBecomeActive:方法,並重新開始嚮應用程序傳遞觸摸事件。  

    您可以在這個方法中重新激活定時器、提高OpenGL的幀率、以及將應用程序從休眠狀態喚醒。對於處於暫停狀態的遊戲,您應該考慮使它停在當時的狀態上,等待用戶做好重新玩的準備。舉例來說,您可以顯示一個警告窗口,而窗口中帶有重新開始的控件。

  5. 如果用戶選擇響應該事件(而不是忽略),則系統會調用應用程序委託的applicationWillTerminate:方法。您的應用程序應該正常終止,保存所有必要的上下文信息,使應用程序在下一次啓動的時候可以回到同樣的位置。

    在您的應用程序終止之後,系統就開始啓動負責中斷的應用程序。

根據用戶對中斷的不同響應,系統可能在中斷結束之後再次啓動您的應用程序。舉例來說,如果用戶接聽一個電話並在完成後掛斷,則系統會重新啓動您的應用程序;如果用戶在接聽電話過程中回到Home屏幕或啓動另一個程序,則系統就不再啓動您的應用程序了。

重要提示:當用戶接聽電話並在通話過程中重新啓動您的應用程序時,狀態條的高度會變大,以反映當前用戶正在通話中。類似地,當用戶結束通話的時候,狀態條的高度會縮回正常尺寸。您的應用程序應該爲狀態條高度的變化做好準備,並據此調整內容區域的尺寸。視圖控制器會自動處理這個行爲,然而,如果您通過代碼進行用戶界面的佈局,就需要在視圖佈局以及通過layoutSubviews方法處理動態佈局變化時考慮狀態條的高度。

 

在運行您的應用程序時,如果用戶按下設備的休眠/喚醒按鍵,系統會調用應用程序委託的applicationWillResignActive:方法,停止觸摸事件的派發,然後使設備進入休眠狀態。之後,當用戶喚醒設備時,系統會調用應用程序委託的applicationDidBecomeActive:方法,並再次開始嚮應用程序派發事件。如同處理其它中斷一樣,您應該使用這些方法來使應用程序進入休眠狀態(或者暫停遊戲)及再次喚醒它們。在休眠時,您的應用程序應該儘可能少用電力。

觀察低內存警告

當系統向您的應用程序發送低內存警告時,您需要加以注意。當可用內存的數量降低到安全閾值以下時,iPhone OS會通知最前面的應用程序。如果您的應用程序收到這種警告,就必須儘可能多地釋放內存,即釋放不再需要的對象或清理易於在稍後進行重建的緩存。

UIKit提供如下幾種接收低內存警告的方法:

一旦收到上述的任何警告,您的處理代碼就應該立即響應,釋放所有不需要的內存。視圖控制器應該清除當前離屏的視圖對象,您的應用程序委託則應該釋放儘可能多的數據結構,或者通知其它應用程序對象釋放其擁有的內存。

如果您的定製對象知道一些可清理的資源,則可以讓該對象註冊UIApplicationDidReceiveMemoryWarningNotification通告,並在通告處理器代碼中直接釋放那些資源。如果您通過少數對象來管理大多數可清理的資源,且適合清理所有的這些資源,則同樣可以讓這些對象進行註冊。但是,如果您有很多可清理的對象,或者僅希望釋放這些對象的一個子集,則在您的應用程序委託中進行釋放可能更好一些。

重要提示:和系統的應用程序一樣,您的應用程序總是需要處理低內存警告,即使在測試過程中沒有收到那些警告,也一樣要進行處理。系統在處理請求時會消耗少量的內存。在檢測到低內存的情況時,系統會將低內存警告發送給所有正在運行的進程(包括您的應用程序),而且可能終止某些後臺程序(如果必要的話),以減輕內存的壓力。如果釋放後內存仍然不夠—可能因爲您的應用程序發生泄露或消耗太多內存—系統仍然可能會終止您的應用程序。

 

 

定製應用程序的行爲

有幾種方法可以對基本的應用程序行爲進行定製,以提供您希望的用戶體驗。本文的下面部分將描述一些必須在應用程序級別進行的定製。

以景觀模式啓動

爲了配合Home屏幕的方向,iPhone OS的應用程序通常以肖像模式啓動。如果您的應用程序既可以以景觀模式運行,也可以以肖像模式運行,那麼,一開始應該總是以縱向模式啓動,然後由視圖控制器根據設備的方向旋轉用戶界面。但是,如果您的應用程序只能以景觀模式啓動,則必須執行下面的步驟,使它一開始就以景觀模式啓動。

重要提示:上面描述的步驟假定您的應用程序使用視圖控制器來管理視圖層次。視圖控制器爲處理方向改變和複雜的視圖相關事件提供了大量的基礎設施。如果您的應用程序不使用視圖控制器—遊戲和其它基於OpenGL ES的應用程序可能是這樣的—就必須根據需要旋轉繪圖表面(或者調整繪圖命令),以便將您的內容以景觀模式展示出來。

 

UIInterfaceOrientation屬性提示iPhone OS在啓動時應該配置應用程序狀態條(如果有的話)的方向,就象配置視圖控制器管理下的視圖方向一樣。在iPhone OS 2.1及更高版本的系統中,視圖控制器會尊重這個屬性,將視圖的初始方向設置爲指定的方向。使用這個屬性相當於在applicationDidFinishLaunching:方法的一開始執行UIApplicationsetStatusBarOrientation:animated:方法。

請注意:在v2.1之前的iPhone OS系統中,如果要以景觀模式啓動基於視圖控制器的應用程序,需要在上文描述的所有步驟的基礎上對應用程序根視圖的轉換矩陣進行一個90度的旋轉。在iPhone OS 2.1之前,視圖控制器並不會根據UIInterfaceOrientation鍵的值自動進行旋轉,當然在iPhone OS 2.1及更高版本的系統中不需要這個步驟。

和其它應用程序進行通訊

如果一個應用程序支持一些已知類型的URL,您就可以通過對應的URL模式和該程序進行通訊。然而,在大多數情況下,URL只是用於簡單地啓動一個應用程序並顯示一些和調用方有關的信息。舉例來說,對於一個用於管理地址信息的應用程序,您就可以在發送給它的URL中包含一個Maps程序可以處理的地址,以便顯示相應的位置。這個級別的通訊爲用戶創造一個集成度高得多的環境,減少應用程序重新實現設備上其它程序已經實現的功能的必要性。

蘋果內置支持httpmailtotel、和sms這些URL模式,還支持基於http的、指向Maps、YouTube、和iPod程序的URL。應用程序也可以自己註冊定製的URL模式。您的應用程序可以和其它應用程序通訊,具體方法是用正確格式的內容創建一個NSURL對象,然後將它傳給共享UIApplication對象openURL:方法。openURL:方法會啓動註冊接收該URL類型的應用程序,並將URL傳給它。當用戶最終退出該應用程序時,系統通常會重新啓動您的應用程序,但並不總是這樣。系統會考慮用戶在URL處理程序中的動作及在用戶看來返回您的應用程序是否合理,然後做出決定。

下面的代碼片斷展示了一個程序如何請求另一個程序提供的服務(假定這個例子中的“todolist”是由應用程序註冊的定製模式):

NSURL *myURL = [NSURL URLWithString:@"todolist://www.acme.com?Quarterly%20Report#200806231300"];
[[UIApplication sharedApplication] openURL:myURL];

重要提示:如果您的URL類型包含的模式和蘋果定義的一樣,則啓動的是蘋果提供的程序,而不是您的程序。如果有多個第三方的應用程序註冊處理同樣的URL模式,則該類型的URL由哪個程序處理是沒有定義的。

 

如果您的應用程序定義了自己的URL模式,則應該實現對該模式進行處理的方法,具體信息在“實現定製的URL模式”部分中進行描述。有關係統支持的URL處理,包括如何處理URL的格式,請參見蘋果的URL模式參考

實現定製的URL模式

您可以爲自己的應用程序註冊包含定製模式的URL類型。定製的URL模式是第三方應用程序和其它程序及系統進行交互的機制。通過定製的URL模式,應用程序可以將自己的服務提供給其它程序。

註冊定製的URL模式

在爲您的應用程序註冊URL類型時,必須指定CFBundleURLTypes屬性的子屬性,我們已經在“信息屬性列表”部分中介紹過這個屬性了。CFBundleURLTypes屬性是應用程序的Info.plist文件中的一個字典數組,每個字典負責定義一個應用程序支持的URL類型。表1-6描述了CFBundleURLTypes字典的鍵和值。

表1-6  CFBundleURLTypes屬性的鍵和值

CFBundleURLName

這是個字符串,表示URL類型的抽象名。爲了確保其唯一性,建議您使用反向DNS風格的標識,比如com.acme.myscheme

這裏提供的URL類型名是一個指向本地化字符串的鍵,該字符串位於本地化語言包子目錄中的InfoPlist.strings文件中。本地化字符串是人類可識別的URL類型名稱,用相應的語言來表示。

CFBundleURLSchemes

這是個URL模式的數組,表示歸屬於這個URL類型的URL。每個模式都是一個字符串。屬於指定URL類型的URL都帶有它們的模式組件。

圖1-7顯示了一個正在用內置的Xcode編輯器編輯的Info.plist文件。在這個圖中,左列中的URL類型入口相當於您直接加入到Info.plist文件的CFBundleURLTypes鍵。類似地,“URL identifier”和“URL Schemes”入口相當於CFBundleURLNameCFBundleURLSchemes鍵。

圖1-7  Info.plist文件中定義一個定製的URL模式

Defining a custom URL scheme in the Info.plist file

您在對CFBundleURLTypes屬性進行定義,從而註冊帶有定製模式的URL類型之後,可以通過下面的方式來進行測試:

  1. 連編、安裝、和運行您的應用程序。

  2. 回到Home屏幕,啓動Safari(在iPhone仿真器上,在菜單上選擇Hardware > Home命令就可以回到Home屏幕)。

  3. 在Safari的地址欄中,鍵入使用定製模式的URL。

  4. 確認您的應用程序是否啓動,以及應用程序委託是否收到application:handleOpenURL:消息。

處理URL請求

應用程序委託在application:handleOpenURL:方法中處理傳遞給應用程序的URL請求。如果您已經爲自己的應用程序註冊了定製的URL模式,則務必在委託中實現這個方法。

基於定製模式的URL採用的協議是請求服務的應用程序能夠理解的。URL中包含一些註冊模式的應用程序期望得到的信息,這些信息是該程序在處理或響應URL請求時需要的。傳遞給application:handleOpenURL:方法的NSURL對象表示的是Cocoa Touch框架中的URL。NSURL遵循RFC 1808規範,該類中包含一些方法,用於返回RFC 1808定義的各個URL要素,包括用戶名、密碼、請求、片斷、和參數字符串。與您註冊的定製模式相對應的“協議”可以使用這些URL要素來傳遞各種信息。

在程序清單1-2顯示的application:handleOpenURL:方法實現中,傳入的URL對象在其請求和片斷部分帶有具體應用程序的信息。應用程序委託抽出這些信息—在這個例子中,是指一個to-do任務的名稱和到期日—並根據這些信息創建應用程序的模型對象。

程序清單1-2  處理基於定製模式的URL請求

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    if ([[url scheme] isEqualToString:@"todolist"]) {
        ToDoItem *item = [[ToDoItem alloc] init];
        NSString *taskName = [url query];
        if (!taskName || ![self isValidTaskString:taskName]) { // must have a task name
            [item release];
            return NO;
        }
        taskName = [taskName stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
 
        item.toDoTask = taskName;
        NSString *dateString = [url fragment];
        if (!dateString || [dateString isEqualToString:@"today"]) {
            item.dateDue = [NSDate date];
        } else {
            if (![self isValidDateString:dateString]) {
                [item release];
                return NO;
            }
            // format: yyyymmddhhmm (24-hour clock)
            NSString *curStr = [dateString substringWithRange:NSMakeRange(0, 4)];
            NSInteger yeardigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(4, 2)];
            NSInteger monthdigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(6, 2)];
            NSInteger daydigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(8, 2)];
            NSInteger hourdigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(10, 2)];
            NSInteger minutedigit = [curStr integerValue];
 
            NSDateComponents *dateComps = [[NSDateComponents alloc] init];
            [dateComps setYear:yeardigit];
            [dateComps setMonth:monthdigit];
            [dateComps setDay:daydigit];
            [dateComps setHour:hourdigit];
            [dateComps setMinute:minutedigit];
            NSCalendar *calendar = [NSCalendar currentCalendar];
            NSDate *itemDate = [calendar dateFromComponents:dateComps];
            if (!itemDate) {
                [dateComps release];
                [item release];
                return NO;
            }
            item.dateDue = itemDate;
            [dateComps release];
        }
 
        [(NSMutableArray *)self.list addObject:item];
        [item release];
        return YES;
    }
    return NO;
}

請務必對傳入的URL輸入進行驗證。如果您希望瞭解如何避免URL處理的相關問題,請參見安全編碼指南文檔中的驗證輸入部分。如果要了解蘋果定義的URL模式,請參見蘋果的URL模式參考

顯示應用程序的偏好設置

如果您的應用程序通過偏好設置來控制其行爲的不同方面,那麼,以何種方式向用戶提供偏好設置就取決於它們是否爲程序的必需部分。

  • 如果偏好設置是程序使用的必需部分(且直接實現起來足夠簡單),那麼應該直接通過應用程序的定製界面來呈現。

  • 如果偏好設置不是必需的,且要求相對複雜的界面,則應該通過系統的Settings程序來呈現。

在確定一組偏好設置是否爲程序的必需部分時,請考慮您爲程序設計的使用模式。如果您希望用戶相對頻繁地修改偏好設置,或者這些偏好設置對程序的行爲具有相對重要的影響,則可能就是必需部分。舉例來說,遊戲中的設置通常都是玩遊戲的必需部分,或者是用戶希望快速改變的項目。然而,由於Settings程序是一個獨立的程序,所以只能用於處理用戶不頻繁訪問的偏好設置。

如果您選擇在應用程序內進行偏好設置管理,則可以自行定義用戶界面及編寫代碼來實現。但是,如果您選擇使用Settings程序,則必須提供一個設置包(Settings Bundle)來進行管理。

設置包是位於應用程序的程序包目錄最頂層的定製資源,它是一個封裝了的目錄,名字爲Settings.bundle。設置包中包含一些具有特別格式的數據文件(及其支持資源),其作用是告訴Settings程序如何顯示您的偏好設置。這些文件還告訴Settings程序應該把結果值存儲在偏好設置數據庫的什麼位置上,以便應用程序隨後可以通過NSUserDefaultsCFPreferences API來進行訪問。

如果您通過設置包來實現偏好設置管理,則還應該提供一個定製的圖標。Settings程序會在您的應用程序包的最頂層尋找名爲Icon-Settings.png的圖像文件,並將該圖像顯示在應用程序名稱的邊上。該文件應該是一個29 x 29像素的PNG圖像文件。如果您沒有在應用程序包的最頂層提供這個文件,則Settings程序會缺省使用縮放後的應用程序圖標(Icon.png)。

有關如何爲應用程序創建設置包的更多信息,請參見“應用程序的偏好設置”部分。

關閉屏幕鎖定

如果一個基於iPhone OS的設備在某個特定時間段中沒有接收到觸摸事件,就會關閉屏幕,並禁用觸摸傳感器。以這種方式鎖定屏幕是省電的重要方法。因此,除非您確實需要在應用程序中避免無意的行爲,否則應該總是打開屏幕鎖定功能。舉例來說,如果您的應用程序不接收屏幕事件,而是使用其它特性(比如加速計)來進行輸入,則可能需要禁用屏幕鎖定功能。

將共享的UIApplication對象的idleTimerDisabled屬性設置爲YES,就可以禁止屏幕鎖定。請務必在程序不需要禁止屏幕鎖定功能時將該屬性重置爲NO。舉例來說,您可能在用戶玩遊戲的時候禁止了屏幕鎖定,但是,當用戶處於配置界面或沒有處於遊戲活躍狀態時,應該重新打開這個功能。

國際化您的應用程序

理想情況下,iPhone應用程序顯示給用戶的文本、圖像、和其它內容都應該本地化爲多種語言。比如,警告對話框中顯示的文本就應該以用戶偏好的語言顯示。爲工程準備特定語言的本地化內容的過程就稱爲國際化。工程中需要本地化的候選組件包括:

  • 代碼生成的文本,包括與具體區域設置有關的日期、時間、和數字格式。

  • 靜態文本—比如裝載到web視圖、用於顯示應用程序幫助的HTML文件。

  • 圖標(包括您的應用程序圖標)及其它包含文本或具體文化意義的圖像。

  • 包含發聲語言的聲音文件。

  • Nib文件

通過Settings程序,用戶可以從Language偏好設置視圖(參見圖1-8)中選擇希望在用戶界面上看到的語言。您可以訪問General設置,然後在International組中找到該視圖。

圖1-8  語言偏好設置視圖

The Language preference view

用戶選擇的語言和程序包中的一個子目錄相關聯,該子目錄名由兩個部分組成,分別是ISO 639-1定義的語言碼和.lproj後綴。您還可以對語言碼進行修改,使之包含具體的地區,方法是在後面(在下劃線之後)加入ISO 3166-1定義的區域指示符。舉例來說,如果要指定美國英語的本地化資源,程序包中的子目錄應該命名爲en_US.lproj。我們約定,本地化語言子目錄稱爲lproj文件夾。

請注意:您也可以使用ISO 639-2語言碼,而不一定使用ISO 639-1的定義。有關語言和區域代碼的信息,請參見國際化編程主題文檔中的“語言和地域的指定”部分。

一個lproj文件夾中包含所有指定語言(還可能包含指定地區)的本地化內容。您可以用NSBundle類或CFBundleRef封裝類型提供的工具來(在應用程序的lproj文件夾)定位當前選定語言的本地化資源。列表1-3給出一個包含英語(en)本地化內容的目錄。

列表1-3  本地化語言子目錄的內容

en.lproj/
    InfoPlist.strings
    Localizable.strings
    sign.png

這個例子目錄有下面幾個項目:

  • InfoPlist.strings文件,包含與Info.plist文件中特定鍵(比如CFBundleDisplayName)相關聯的本地化字符串值。比如,一個英文名稱爲Battleship的應用程序,其CFBundleDisplayName鍵在fr.lproj子目錄的InfoPlist.strings文件中有如下的入口:

    CFBundleDisplayName = "Cuirassé";
  • Localizable.strings文件,包含應用程序代碼生成的字符串的本地化版本。

  • 本例子中的sign.png,是一個包含本地化圖像的文件。

爲了本地化,我們需要國際化代碼中的字符串,具體做法是用NSLocalizedString宏來代替字符串。這個宏的定義如下:

NSString *NSLocalizedString(NSString *key, NSString *comment);

第一個參數是一個唯一的鍵,指向給定lproj文件夾中Localizable.strings文件裏的一個本地化字符串;第二個參數是一個註釋,說明字符串如何使用,因此可以爲翻譯人員提供額外的上下文。舉例來說,假定您正在設置用戶界面中一個標籤(UILabel對象)的內容,則下面的代碼可以國際化該標籤的文本:

label.text = NSLocalizedString(@"City", @"Label for City text field");

然後,您就可以爲給定語言創建一個Localizable.strings文件,並將它加入到相應的lproj文件夾中。對於上文例子中的鍵,該文件中應該有如下入口:

"City" = "Ville";

請注意:另一種方法是在代碼中恰當的地方插入NSLocalizedString調用,然後運行genstrings命令行工具。該工具會生成一個Localizable.strings文件的模板,包含每個需要翻譯的鍵和註釋。更多有關genstrings的信息,請參見genstrings(1)的man頁面。

更多有關國際化的信息,請參見國際化編程主題

 

性能和響應速度的調優

在應用程序開發過程的每一步,您都應該考慮自己所做的設計對應用程序總體性能的影響。由於iPhone和iPod touch設備的移動本質,iPhone應用程序的操作環境受到更多的限制。本文的下面部分將描述在開發過程中應該考慮哪些因素。

不要阻塞主線程

您應該認真考慮在應用程序主線程上執行的任務。主線程是應用程序處理觸摸事件和其它用戶輸入的地方。爲了確保應用程序總是可以響應用戶,我們不應該在主線程中執行運行時間很長或可能無限等待的任務,比如訪問網絡的任務。相反,您應該將這些任務放在後臺線程。一個推薦的方法是將每個任務都封裝在一個操作對象中,然後加入操作隊列。當然,您也可以自己創建顯式的線程。

將任務轉移到後臺可以使您的主線程繼續處理用戶輸入,這對於應用程序的啓動和退出尤其重要。在這些時候,系統期望您的應用程序及時響應事件。如果應用程序的主線程在啓動過程中被阻塞住了,系統甚至可能在啓動完成之前將它殺死;如果主線程在退出時被阻塞了,則應用程序可能來不及保存關鍵用戶數據就被殺死了。

更多有關如何使用操作對象和線程的信息,請參見線程編程指南

有效地使用內存

由於iPhone OS的虛存模型並不包含磁盤交換區空間,所以應用程序在更大程度上受限於可供使用的內存。對內存的大量使用會嚴重降低系統的性能,可能導致應用程序被終止。因此,在設計階段,您應該把減少應用程序的內存開銷放在較高優先級上。

應用程序的可用內存和相對性能之間有直接的聯繫。可用內存越少,系統在處理未來的內存請求時就越可能出問題。如果發生這種情況,系統總是先把代碼頁和其它非易失性資源從內存中移除。但是,這可能只是暫時的修復,特別是當系統在短時間後又再次需要那些資源的時候。相反,您需要儘可能使內存開銷最小化,並及時清除自己使用的內存。

本文的下面部分將就如何有效使用內存和在只有少量內存時如何反應方面提供更多的指導。

減少應用程序的內存印跡

表1-7列出一些如何減少應用程序總體內存印跡的技巧。在開始時將內存印跡降低了,隨後就可以有更多的空間用於需要操作的數據。

表1-7  減少應用程序內存印跡的技巧

技巧

採取的措施

消除內存泄露

由於內存是iPhone OS的關鍵資源,所以您的應用程序不應該有任何的內存泄露。存在內存泄露意味着應用程序在之後可能沒有足夠的內存。您可以用Instruments程序來跟蹤代碼中的泄露,該程序既可以用於仿真器,也可以用於實際的設備。有關如何使用Instruments的更多信息,請參見Instruments用戶指南

使資源文件儘可能小

文件駐留在磁盤中,但在使用時需要載入內存。屬性列表文件和圖像文件是通過簡單的處理就可以節省空間的兩種資源類型。您可以通過NSPropertyListSerialization類將屬性列表文件存儲爲二進制格式,從而減少它們的使用空間;對於圖像,可以將所有圖像文件壓縮得儘可能小(PNG圖像是iPhone應用程序的推薦圖像格式,可以用pngcrush工具來進行壓縮)。

使用Core Data 或SQLite來處理大的數據集合

如果您的應用程序需要操作大量的結構化數據,請將它存儲在Core Data的持久存儲或SQLite數據庫,而不是使用扁平文件。Core Data和SQLite都提供了管理大量數據的有效方法,不需要將整個數據一次性地載入內存。

Core Data的支持是在iPhone OS 3.0系統上引入的。

延緩裝載資源

在真正需要資源文件之前,永遠不應該進行裝載。預先載入資源文件表面看好象可以節省時間,但實際上會使應用程序很快變慢。此外,如果您最終沒有用到那些資源,預先載入將只是浪費內存。

將程序連編爲Thumb格式

加入-mthumb開關可以將代碼的尺寸減少最多達35%。但是,對於具有大量浮點數運算的代碼模塊,請務必將這個選項關閉,因爲對那樣的模塊使用Thumb反而會導致性能的下降。

恰當地分配內存

iPhone應用程序使用委託內存模式,因此,您必須顯式保持和釋放內存。表1-8列出了一些在程序中分配內存的技巧。

表1-8  分配內存的技巧

技巧

採取的措施

減少自動釋放對象的使用

通過autorelease方法釋放的對象會留在內存中,直到顯式清理自動釋放池或者程序再次回到事件循環。在任何可能的時候,請避免使用autorelease方法,而是通過release方法立即收回對象佔用的空間。如果您必須創建一定數量的自動釋放對象,則請創建局部的自動釋放池,以便在返回事件循環之前定期對其進行清理,回收那些對象的內存。

爲資源設置尺寸限制

避免裝載大的資源文件,如果有更小的文件可用的話。請用適合於iPhone OS設備的恰當尺寸圖像來代替高清晰度的圖像。如果您必須使用大的資源文件,需要考慮僅裝載當前需要的部分。舉例來說,您可以通過mmapmunmap函數來將文件的一部分載入內存或從內存卸載,而不是操作整個文件。有關如何將文件映射到內存的更多信息,請參見文件系統性能指南

避免無邊界的問題集

無邊界的問題集可能需要計算任意大量的數據。如果該集合需要的內存比當前系統能提供的還要多,則您的應用程序可能無法進行計算。您的應用程序應該儘可能避免處理這樣的集合,而將它們轉化爲內存使用極限已知的問題。

有關如何在iPhone應用程序中分配內存及使用自動釋放池的詳細信息,請參見Cocoa基本原理指南文檔的Cocoa對象部分。

浮點數學運算的考慮

iPhone–OS設備上的處理器有能力在硬件上處理浮點數計算。如果您目前的程序使用基於軟件的定點數數學庫進行計算,則應該考慮對代碼進行修改,轉向使用浮點數數學庫。典型情況下,基於硬件的浮點數計算比對應的基於軟件的定點數計算快得多。

重要提示:當然,如果您的代碼確實廣泛地使用浮點數計算,請記住不要使用-mthumb選項來編譯代碼。Thumb選項可以減少代碼模塊的尺寸,但是也會降低浮點計算代碼的性能。

 

減少電力消耗

移動設備的電力消耗一直是個問題。iPhone OS的電能管理系統保持電能的方法是關閉當前未被使用的硬件功能。此外,要避免CPU密集型和高圖形幀率的操作。您可以通過優化如下組件的使用來提高電池的壽命:

  • CPU

  • Wi-Fi和基帶(EDGE, 3G)無線信號

  • Core Location框架

  • 加速計

  • 磁盤

您的優化目標應該是以儘可能有效的方式完成大多數的工作。您應該總是採用Instruments和Shark工具對應用程序的算法進行優化。但是,很重要的一點是,即使最優化的算法也可能對設備的電池壽命造成負面的影響。因此,在寫代碼的時候應該考慮如下的原則:

  • 避免需要輪詢的工作,因爲輪詢會阻止CPU進入休眠狀態。您可以通過NSRunLoop或者NSTimer類來規劃需要做的工作,而不是使用輪詢。

  • 盡一切可能使共享的UIApplication對象的idleTimerDisabled屬性值保持爲NO。當設備處於不活動狀態一段時間後,空閒定時器會關閉設備的屏幕。如果您的應用程序不需要設備屏幕保持打開狀態,就讓系統將它關閉。如果關閉屏幕給您的應用程序的體驗帶來負面影響,則需要通過修改代碼來消除那些影響,而不是不必要地關閉空閒定時器。

  • 儘可能將任務合併在一起,以便使空閒時間最大化。每隔一段時間就間歇性地執行部分任務比一次性完成相同數量的所有任務開銷更多的電能。間歇性地執行任務會阻止系統在更長時間內無法關閉硬件。

  • 避免過度訪問磁盤。舉例來說,如果您需要將狀態信息保存在磁盤上,則僅當該狀態信息發生變化時才進行保存,或者儘可能將狀態變化合並保存,以避免短時間頻繁進行磁盤寫入操作。

  • 不要使屏幕描畫速度比實際需求更快。從電能消耗的角度看,描畫的開銷很大。不要依賴硬件來壓制應用程序的幀率,而是應該根據程序實際需要的幀率來進行幀的描畫。

  • 如果你通過UIAccelerometer類來接收常規的加速計事件,則當您不再需要那些事件時,要禁止這些事件。類似地,請將事件傳送的頻率設置爲滿足應用程序需要的最小值。更多信息請參見“訪問加速計事件”部分。

您向網絡傳遞的數據越多,就需要越多的電能來進行無線發射。事實上,訪問網絡是您所能進行的最耗電的操作,您應該遵循下面的原則,使網絡訪問最小化:

  • 僅在需要的時候連接外部網絡,不要對服務器進行輪詢。

  • 當您需要連接網絡時,請僅傳遞完成工作所需要的最少數據。請使用緊湊的數據格式,不要包含可被簡單忽略的額外數據。

  • 儘可能快地以羣發(in burst)方式傳遞數據包,而不是拉長數據傳輸的時間。當系統檢測到設備沒有活動時,就會關閉Wi-Fi和蜂窩無線信號。您的應用程序以較長時間傳輸數據比以較短時間傳輸同樣數量的數據要消耗更多的電能。

  • 儘可能通過Wi-Fi無線信號連接網絡。Wi-Fi耗電比基帶無線少,是推薦的方式。

  • 如果您通過Core Location框架收集位置數據,則請儘可能快地禁止位置更新,以及將位置過濾器和精度水平設置爲恰當的值。Core Location通過可用的GPS、蜂窩、和Wi-Fi網絡來確定用戶的位置。雖然Core Location已經努力使無線信號的使用最小化了,但是,設置恰當的精度和過濾器的值可以使Core Location在不需要位置服務的時候完全關閉硬件。更多信息請參見“獲取用戶的當前位置”部分。

代碼的優化

和iPhone OS一起推出的還有幾個應用程序的優化工具。它們中的大部分都運行在Mac OS X上,適合於調整運行在仿真器上的代碼的某些方面。舉例來說,您可以通過仿真器來消除內存泄露,確保總的內存開銷儘可能小。藉助這些工具,您還可以排除代碼中可能由低效算法或已知瓶頸引起的計算熱點。

在仿真器上進行代碼優化之後,還應該在設備上用Instruments程序進行進一步優化。在實際設備上運行代碼是對其進行完全優化的唯一方式。因爲仿真器運行在Mac OS X上,而運行Mac OS X的系統具有更快的CPU和更多的可用內存,所以其性能通常比實際設備的性能好很多。在實際設備上用Instruments跟蹤代碼可能會發現額外的性能瓶頸,您需要進行優化。

更多有關Instruments的使用信息,請參見Instruments用戶指南

窗口和視圖

窗口和視圖是爲iPhone應用程序構造用戶界面的可視組件。窗口爲內容顯示提供背景平臺,而視圖負責絕大部分的內容描畫,並負責響應用戶的交互。雖然本章討論的概念和窗口及視圖都相關聯,但是討論過程更加關注視圖,因爲視圖對系統更爲重要。

視圖對iPhone應用程序是如此的重要,以至於在一個章節中討論視圖的所有方面是不可能的。本章將關注窗口和視圖的基本屬性、各個屬性之間的關係、以及在應用程序中如何創建和操作這些屬性。本章不討論視圖如何響應觸摸事件或如何描畫定製內容,有關那些主題的更多信息,請分別參見“事件處理”“圖形和描畫”部分。

什麼是窗口和視圖?

和Mac OS X一樣,iPhone OS通過窗口和視圖在屏幕上展現圖形內容。雖然窗口和視圖對象之間在兩個平臺上有很多相似性,但是具體到每個平臺上,它們的作用都有輕微的差別。

UIWindow的作用

和Mac OS X的應用程序有所不同,iPhone應用程序通常只有一個窗口,表示爲一個UIWindow類的實例。您的應用程序在啓動時創建這個窗口(或者從nib文件進行裝載),並往窗口中加入一或多個視圖,然後將它顯示出來。窗口顯示出來之後,您很少需要再次引用它。

在iPhone OS中,窗口對象並沒有像關閉框或標題欄這樣的視覺裝飾,用戶不能直接對其進行關閉或其它操作。所有對窗口的操作都需要通過其編程接口來實現。應用程序可以藉助窗口對象來進行事件傳遞。窗口對象會持續跟蹤當前的第一響應者對象,並在UIApplication對象提出請求時將事件傳遞它。

還有一件可能讓有經驗的Mac OS X開發者覺得奇怪的事是UIWindow類的繼承關係。在Mac OS X中,NSWindow的父類是NSResponder;而在iPhone OS中,UIWindow的父類是UIView。因此,窗口在iPhone OS中也是一個視圖對象。不管其起源如何,您通常可以將iPhone OS上的窗口和Mac OS X的窗口同樣對待。也就是說,您通常不必直接操作UIWindow對象中與視圖有關的屬性變量

在創建應用程序窗口時,您應該總是將其初始的邊框尺寸設置爲整個屏幕的大小。如果您的窗口是從nib文件裝載得到,Interface Builder並不允許創建比屏幕尺寸小的窗口;然而,如果您的窗口是通過編程方式創建的,則必須在創建時傳入期望的邊框矩形。除了屏幕矩形之外,沒有理由傳入其它邊框矩形。屏幕矩形可以通過UIScreen對象來取得,具體代碼如下所示:

UIWindow* aWindow = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

雖然iPhone OS支持將一個窗口疊放在其它窗口的上方,但是您的應用程序永遠不應創建多個窗口。系統自身使用額外的窗口來顯示系統狀態條、重要的警告、以及位於應用程序窗口上方的其它消息。如果您希望在自己的內容上方顯示警告,可以使用UIKit提供的警告視圖,而不應創建額外的窗口。

UIView是作用

視圖UIView類的實例,負責在屏幕上定義一個矩形區域。在iPhone的應用程序中,視圖在展示用戶界面及響應用戶界面交互方面發揮關鍵作用。每個視圖對象都要負責渲染視圖矩形區域中的內容,並響應該區域中發生的觸碰事件。這一雙重行爲意味着視圖是應用程序與用戶交互的重要機制。在一個基於模型-視圖-控制器的應用程序中,視圖對象明顯屬於視圖部分。

除了顯示內容和處理事件之外,視圖還可以用於管理一或多個子視圖。子視圖是指嵌入到另一視圖對象邊框內部的視圖對象,而被嵌入的視圖則被稱爲父視圖或超視圖。視圖的這種佈局方式被稱爲視圖層次,一個視圖可以包含任意數量的子視圖,通過爲子視圖添加子視圖的方式,視圖可以實現任意深度的嵌套。視圖在視圖層次中的組織方式決定了在屏幕上顯示的內容,原因是子視圖總是被顯示在其父視圖的上方;這個組織方法還決定了視圖如何響應事件和變化。每個父視圖都負責管理其直接的子視圖,即根據需要調整它們的位置和尺寸,以及響應它們沒有處理的事件。

由於視圖對象是應用程序和用戶交互的主要途徑,所以需要在很多方面發揮作用,下面是其中的一小部分:

  • 描畫和動畫

    • 視圖負責對其所屬的矩形區域進行描畫。

    • 某些視圖屬性變量可以以動畫的形式過渡到新的值。

  • 佈局和子視圖管理

    • 視圖管理着一個子視圖列表。

    • 視圖定義了自身相對於其父視圖的尺寸調整行爲。

    • 必要時,視圖可以通過代碼調整其子視圖的尺寸和位置。

    • 視圖可以將其座標系統下的點轉換爲其它視圖或窗口座標系統下的點。

  • 事件處理

    • 視圖可以接收觸摸事件。

    • 視圖是響應者鏈的參與者。

在iPhone應用程序中,視圖和視圖控制器緊密協作,管理若干方面的視圖行爲。視圖控制器的作用是處理視圖的裝載與卸載、處理由於設備旋轉導致的界面旋轉,以及和用於構建複雜用戶界面的高級導航對象進行交互。更多這方面的信息請參見“視圖控制器的作用”部分。

本章的大部分內容都着眼於解釋視圖的這些作用,以及說明如何將您自己的定製代碼關聯到現有的UIView行爲中。

UIKit的視圖類

UIView類定義了視圖的基本行爲,但並不定義其視覺表示。相反,UIKit通過其子類來爲像文本框、按鍵、及工具條這樣的標準界面元素定義具體的外觀和行爲。圖2-1顯示了所有UIKit視圖類的層次框圖。除了UIViewUIControl類是例外,這個框圖中的大多數視圖都設計爲可直接使用,或者和委託對象結合使用。

圖2-1  視圖的類層次

View class hierarchy

這個視圖層次可以分爲如下幾個大類:

  • 容器

    容器視圖用於增強其它視圖的功能,或者爲視圖內容提供額外的視覺分隔。比如,UIScrollView類可以用於顯示因內容太大而無法顯示在一個屏幕上的視圖。UITableView類是UIScrollView類的子類,用於管理數據列表。表格的行可以支持選擇,所以通常也用於層次數據的導航—比如用於挖掘一組有層次結構的對象。

    UIToolbar對象則是一個特殊類型的容器,用於爲一或多個類似於按鍵的項提供視覺分組。工具條通常出現在屏幕的底部。Safari、Mail、和Photos程序都使用工具條來顯示一些按鍵,這些按鍵代表經常使用的命令。工具條可以一直顯示,也可以根據應用程序的需要進行顯示。

  • 控件

    控件用於創建大多數應用程序的用戶界面。控件是一種特殊類型的視圖,繼承自UIControl超類,通常用於顯示一個具體的值,並處理修改這個值所需要的所有用戶交互。控件通常使用標準的系統範式(比如目標-動作模式和委託模式)來通知應用程序發生了用戶交互。控件包括按鍵、文本框、滑塊、和切換開關。

  • 顯示視圖

    控件和很多其它類型的視圖都提供了交互行爲,而另外一些視圖則只是用於簡單地顯示信息。具有這種行爲的UIKit類包括UIImageView、 UILabelUIProgressViewUIActivityIndicatorView

  • 文本和web視圖

    文本和web視圖爲應用程序提供更爲高級的顯示多行文本的方法。UITextView類支持在滾動區域內顯示和編輯多行文本;而UIWebView類則提供了顯示HTML內容的方法,通過這個類,您可以將圖形和高級的文本格式選項集成到應用程序中,並以定製的方式對內容進行佈局。

  • 警告視圖和動作表單

    警告視圖和動作表單用於即刻取得用戶的注意。它們向用戶顯示一條消息,同時還有一或多個可選的按鍵,用戶通過這些按鍵來響應消息。警告視圖和動作表單的功能類似,但是外觀和行爲不同。舉例來說,UIAlertView類在屏幕上彈出一個藍色的警告框,而UIActionSheet類則從屏幕的底部滑出動作框。

  • 導航視圖

    頁籤條和導航條和視圖控制器結合使用,爲用戶提供從一個屏幕到另一個屏幕的導航工具。在使用時,您通常不必直接創建UITabBarUINavigationBar的項,而是通過恰當的控制器接口或Interface Builder來對其進行配置。

  • 窗口

    窗口提供一個描畫內容的表面,是所有其它視圖的根容器。每個應用程序通常都只有一個窗口。更多信息請參見“UIWindow的作用”部分。

除了視圖之外,UIKit還提供了視圖控制器,用於管理這些對象。更多信息請參見“視圖控制器的作用”部分。

視圖控制器的作用

運行在iPhone OS上的應用程序在如何組織內容和如何將內容呈現給用戶方面有很多選擇。含有很多內容的應用程序可以將內容分爲多個屏幕。在運行時,每個屏幕的背後都是一組視圖對象,負責顯示該屏幕的數據。一個屏幕的視圖後面是一個視圖控制器其作用是管理那些視圖上顯示的數據,並協調它們和應用程序其它部分的關係。

UIViewController類負責創建其管理的視圖及在低內存時將它們從內容中移出。視圖控制器還爲某些標準的系統行爲提供自動響應。比如,在響應設備方向變化時,如果應用程序支持該方向,視圖控制器可以對其管理的視圖進行尺寸調整,使其適應新的方向。您也可以通過視圖控制器來將新的視圖以模式框的方式顯示在當前視圖的上方。

除了基礎的UIViewController類之外,UIKit還包含很多高級子類,用於處理平臺共有的某些高級接口。特別需要提到的是,導航控制器用於顯示多屏具有一定層次結構的內容;而頁籤條控制器則支持用戶在一組不同的屏幕之間切換,每個屏幕都代表應用程序的一種不同的操作模式。

有關如何通過視圖控制器管理用戶界面上視圖的更多信息,請參見iPhone OS的視圖控制器編程指南

視圖架構和幾何屬性

由於視圖是iPhone應用程序的焦點對象,所以對視圖與系統其它部分的交互機制有所瞭解是很重要的。UIKit中的標準視圖類爲應用程序免費提供相當數量的行爲,還提供了一些定義良好的集成點,您可以通過這些集成點來對標準行爲進行定製,完成應用程序需要做的工作。

本文的下面部分將解釋視圖的標準行爲,並說明哪些地方可以集成您的定製代碼。如果需要特定類的集成點信息,請參見該類的參考文檔。您可以從UIKit框架參考中取得所有類參考文檔的列表。

視圖交互模型

任何時候,當用戶和您的程序界面進行交互、或者您的代碼以編程的方式進行某些修改時,UIKit內部都會發生一個複雜的事件序列。在事件序列的一些特定的點上,UIKit會調用您的視圖類,使它們有機會代表應用程序進行事件響應。理解這些調用點是很重要的,有助於理解您的視圖對象和系統在哪裏進行結合。圖2-2顯示了從用戶觸擊屏幕到圖形系統更新屏幕內容這一過程的基本事件序列。以編程方式觸發事件的基本步驟與此相同,只是沒有最初的用戶交互。

圖2-2  UIKit和您的視圖對象之間的交互

UIKit interactions with your view objects

下面的步驟說明進一步刨析了圖2-2中的事件序列,解釋了序列的每個階段都發生了什麼,以及應用程序可能如何進行響應。

  1. 用戶觸擊屏幕。

  2. 硬件將觸擊事件報告給UIKit框架。

  3. UIKit框架將觸擊信息封裝爲一個UIEvent對象,並派發給恰當的視圖(有關UIKit如何將事件遞送給您的視圖的詳細解釋,請參見“事件的傳遞”部分)。

  4. 視圖的事件處理方法可以通過下面的方式來響應事件:

    • 調整視圖或其子視圖的屬性變量(邊框、邊界、透明度等)。

    • 將視圖(或其子視圖)標識爲需要修改佈局。

    • 將視圖(或其子視圖)標識爲佈局需要重畫。

    • 將數據發生的變化通報給控制器

    當然,上述的哪些事情需要做及調用什麼方法來完成是由視圖來決定的。

  5. 如果視圖被標識爲需要重新佈局,UIKit就調用視圖的layoutSubviews方法。

    您可以在自己的定製視圖中重載這個方法,以便調整子視圖的尺寸和位置。舉例來說,如果一個視圖具有很大的滾動區域,就需要使用幾個子視圖來“平鋪”,而不是創建一個內存很可能裝不下的大視圖。在這個方法的實現中,視圖可以隱藏所有不需顯示在屏幕上的子視圖,或者在重新定位之後將它們用於顯示新的內容。作爲這個過程的一部分,視圖也可以將用於“平鋪”的子視圖標識爲需要重畫。

  6. 如果視圖的任何部分被標識爲需要重畫,UIKit就調用該視圖的drawRect:方法。

    UIKit只對那些需要重畫的視圖調用這個方法。在這個方法的實現中,所有視圖都應該儘可能快地重畫指定的區域,且都應該只重畫自己的內容,不應該描畫子視圖的內容。在這個調用點上,視圖不應該嘗試進一步改變其屬性或佈局。

  7. 所有更新過的視圖都和其它可視內容進行合成,然後發送給圖形硬件進行顯示。

  8. 圖形硬件將渲染完成的內容轉移到屏幕。

請注意:上述的更新模型主要適用於採納內置視圖和描畫技術的應用程序。如果您的應用程序使用OpenGL ES來描畫內容,則通常要配置一個全屏的視圖,然後直接在OpenGL的圖形上下文中進行描畫。您的視圖仍然需要處理觸碰事件,但不需要對子視圖進行佈局或者實現drawRect:方法。有關OpenGL ES的更多信息,請參見“用OpenGL ES進行描畫”部分。

基於上述的步驟說明可以看出,UIKit爲您自己定製的視圖提供如下主要的結合點:

  1. 下面這些事件處理方法:

  2. layoutSubviews方法

  3. drawRect:方法

大多數定製視圖通過實現這些方法來得到自己期望的行爲。您可能不需要重載所有方法,舉例來說,如果您實現的視圖是固定尺寸的,則可能不需要重載layoutSubviews方法。類似地,如果您實現的視圖只是顯示簡單的內容,比如文本或圖像,則通常可以通過簡單地嵌入UIImageViewUILabel對象作爲子視圖來避免描畫。

重要的是要記住,這些是主要的結合點,但不是全部。UIView類中有幾個方法的設計目的就是讓子類重載的。您可以通過查閱UIView類參考中的描述來了解哪些方法可以被重載。

視圖渲染架構

雖然您通過視圖來表示屏幕上的內容,但是UIView類自身的很多基礎行爲卻嚴重依賴於另一個對象。UIKit中每個視圖對象的背後都有一個Core Animation層對象,它是一個CALayer類的實例,該類爲視圖內容的佈局和渲染、以及合成和動畫提供基礎性的支持。

和Mac OS X(在這個平臺上Core Animation支持是可選的)不同的是,iPhone OS將Core Animation集成到視圖渲染實現的核心。雖然Core Animation發揮核心作用,但是UIKit在Core Animation上面提供一個透明的接口層,使編程體驗更爲流暢。這個透明的接口使開發者在大多數情況下不必直接訪問Core Animation的層,而是通過UIView的方法和屬性聲明取得類似的行爲。然而,當UIView類沒有提供您需要的接口時,Core Animation就變得重要了,在那種情況下,您可以深入到Core Animation層,在應用程序中實現一些複雜的渲染。

本文的下面部分將介紹Core Animation技術,描述它通過UIView類爲您提供的一些功能。有關如何使用Core Animation進行高級渲染的更多信息,請參見Core Animation編程指南

Core Animation基礎

Core Animation利用了硬件加速和架構上的優化來實現快速渲染和實時動畫。當視圖的drawRect:方法首次被調用時,層會將描畫的結果捕捉到一個位圖中,並在隨後的重畫中儘可能使用這個緩存的位圖,以避免調用開銷很大的drawRect:方法。這個過程使Core Animation得以優化合成操作,取得期望的性能。

Core Animation把和視圖對象相關聯的層存儲在一個被稱爲層樹的層次結構中。和視圖一樣,層樹中的每個層都只有一個父親,但可以嵌入任意數量的子層。缺省情況下,層樹中對象的組織方式和視圖在視圖層次中的組織方式完全一樣。但是,您可以在層樹中添加層,而不同時添加相應的視圖。當您希望實現某種特殊的視覺效果、而又不需要在視圖上保持這種效果時,就可能需要這種技術。

實際上,層對象是iPhone OS渲染和佈局系統的推動力,大多數視圖屬性實際上是其層對象屬性的一個很薄的封裝。當您(直接使用CALayer對象)修改層樹上層對象的屬性時,您所做的改變會立即反映在層對象上。但是,如果該變化觸發了相應的動畫,則可能不會立即反映在屏幕上,而是必須隨着時間的變化以動畫的形式表現在屏幕上。爲了管理這種類型的動畫,Core Animation額外維護兩組層對象,我們稱之爲表示樹渲染樹

表示樹反映的是層在展示給用戶時的當前狀態。假定您對層值的變化實行動畫,則在動畫開始時,表示層反映的是老的值;隨着動畫的進行,Core Animation會根據動畫的當前幀來更新表示樹層的值;然後,渲染樹就和表示樹一起,將變化渲染在屏幕上。由於渲染樹運行在單獨的進程或線程上,所以它所做的工作並不影響應用程序的主運行循環。雖然層樹和表示樹都是公開的,但是渲染樹的接口是私有。

在視圖後面設置層對象對描畫代碼的性能有很多重要的影響。使用層的好處在於視圖的大多數幾何變化都不需要重畫。舉例來說,改變視圖的位置和尺寸並需要重畫視圖的內容,只需簡單地重用層緩存的位圖就可以了。對緩存的內容實行動畫比每次都重畫內容要有效得多。

使用層的缺點在於層是額外的緩存數據,會增加應用程序的內存壓力。如果您的應用程序創建太多的視圖,或者創建多個很大的視圖,則可能很快就會出現內存不夠用的情形。您不用擔心在應用程序中使用視圖,但是,如果有現成的視圖可以重用,就不要創建新的視圖對象。換句話說,您應該設法使內存中同時存在的視圖對象數量最小。

有關Core Animation的進一步概述、對象樹、以及如何創建動畫,請參見Core Animation編程指南

改變視圖的層

在iPhone OS系統中,由於視圖必須有一個與之關聯的層對象,所以UIView類在初始化時會自動創建相應的層。您可以通過視圖的layer屬性訪問這個層,但是不能在視圖創建完成後改變層對象。

如果您希望視圖使用不同類型的層,必須重載其layerClass類方法,並在該方法中返回您希望使用的層對象。使用不同層類的最常見理由是爲了實現一個基於OpenGL的應用程序。爲了使用OpenGL描畫命令,視圖下面的層必須是CAEAGLLayer類的實例,這種類型的層可以和OpenGL渲染調用進行交互,最終在屏幕上顯示期望的內容。

重要提示:您永遠不應修改視圖層的delegate屬性,該屬性用於存儲一個指向視圖的指針,應該被認爲是私有的。類似地,由於一個視圖只能作爲一個層的委託,所以您必須避免將它作爲其它層對象的委託,否則會導致應用程序崩潰。

 

動畫支持

iPhone OS的每個視圖後面都有一個層對象,這樣做的好處之一是使視圖內容更加易於實現動畫。請記住,動畫並不一定是爲了在視覺上吸引眼球,它可以將應用程序界面變化的上下文呈現給用戶。舉例來說,當您在屏幕轉移過程中使用過渡時,過渡本身就向用戶指示屏幕之間的聯繫。系統自動支持了很多經常使用的動畫,但您也可以爲界面上的其它部分創建動畫。

UIView類的很多屬性都被設計爲可動畫的(animatable)。可動畫的屬性是指當屬性從一個值變爲另一個值的時候,可以半自動地支持動畫。您仍然必須告訴UIKit希望執行什麼類型的動畫,但是動畫一旦開始,Core Animation就會全權負責。UIView對象中支持動畫的屬性有如下幾個:

雖然其它的視圖屬性不直接支持動畫,但是您可以爲其中的一部分顯式創建動畫。顯式動畫要求您做很多管理動畫和渲染內容的工作,通過使用Core Animation提供的基礎設施,這些工作仍然可以得到良好的性能。

有關如何通過UIView類創建動畫的更多信息,請參見“實現視圖動畫”部分;有關如何創建顯式動畫的更多信息,則請參見Core Animation編程指南

視圖座標系統

UIKit中的座標是基於這樣的座標系統:以左上角爲座標的原點,原點向下和向右爲座標軸正向。座標值由浮點數來表示,內容的佈局和定位因此具有更高的精度,還可以支持與分辨率無關的特性。圖2-3顯示了這個相對於屏幕的座標系統,這個座標系統同時也用於UIWindowUIView類。視圖座標系統的方向和Quartz及Mac OS X使用的缺省方向不同,選擇這個特殊的方向是爲了使佈局用戶界面上的控件及內容更加容易。

圖2-3  視圖座標系統

View coordinate system

您在編寫界面代碼時,需要知道當前起作用的座標系統。每個窗口和視圖對象都維護一個自己本地的座標系統。視圖中發生的所有描畫都是相對於視圖本地的座標系統。但是,每個視圖的邊框矩形都是通過其父視圖的座標系統來指定,而事件對象攜帶的座標信息則是相對於應用程序窗口的座標系統。爲了方便,UIWindowUIView類都提供了一些方法,用於在不同對象之間進行座標系統的轉換。

雖然Quartz使用的座標系統不以左上角爲原點,但是對於很多Quartz調用來說,這並不是問題。在調用視圖的drawRect:方法之前,UIKit會自動對描畫環境進行配置,使左上角成爲座標系統的原點,在這個環境中發生的Quartz調用都可以正確地在視圖中描畫。您唯一需要考慮不同座標系統之間差別的場合是當您自行通過Quartz建立描畫環境的時候。

更多有關座標系統、Quartz、和描畫的一般信息,請參見“圖形和描畫”部分。

邊框、邊界、和中心的關係

視圖對象通過framebounds、和center屬性聲明來跟蹤自己的大小和位置。frame屬性包含一個矩形,即邊框矩形,用於指定視圖相對於其父視圖座標系統的位置和大小。bounds屬性也包含一個矩形,即邊界矩形,負責定義視圖相對於本地座標系統的位置和大小。雖然邊界矩形的原點通常被設置爲 (0, 0),但這並不是必須的。center屬性包含邊框矩形的中心點

在代碼中,您可以將framebounds、和center屬性用於不同的目的。邊界矩形代表視圖本地的座標系統,因此,在描畫和事件處理代碼中,經常藉助它來取得視圖中發生事件或需要更新的位置。中心點代表視圖的中心,改變中心點一直是移動視圖位置的最好方法。邊框矩形是一個通過boundscenter屬性計算得到的便利值,只有當視圖的變換屬性被設置恆等變換時,邊框矩形纔是有效的。

圖2-4顯示了邊框矩形和邊界矩形之間的關係。右邊的整個圖像是從視圖的(0, 0)開始描畫的,但是由於邊界的大小和整個圖像的尺寸不相匹配,所以位於邊界矩形之外的圖像部分被自動裁剪。在視圖和它的父視圖進行合成的時候,視圖在其父視圖中的位置是由視圖邊框矩形的原點決定的。在這個例子中,該原點是(5, 5)。結果,視圖的內容就相對於父視圖的原點向下向右移動相應的尺寸。

圖2-4  視圖的邊框和邊界之間的關係

Relationship between a view's frame and bounds

如果沒有經過變換,視圖的位置和大小就由上述三個互相關聯的屬性決定的。當您在代碼中通過initWithFrame:方法創建一個視圖對象時,其frame屬性就會被設置。該方法同時也將bounds矩形的原點初始化爲(0.0, 0.0),大小則和視圖的邊框相同。然後center屬性會被設置爲邊框的中心點。

雖然您可以分別設置這些屬性的值,但是設置其中的一個屬性會引起其它屬性的改變,具體關係如下:

  • 當您設置frame屬性時,bounds屬性的大小會被設置爲與frame屬性的大小相匹配的值,center屬性也會被調整爲與新的邊框中心點相匹配的值。

  • 當您設置center屬性時,frame的原點也會隨之改變。

  • 當您設置bounds矩形的大小時,frame矩形的大小也會隨之改變。

您可以改變bounds的原點而不影響其它兩個屬性。當您這樣做時,視圖會顯示您標識的圖形部分。在圖2-4中,邊界的原點被設置爲(0.0, 0.0)。在圖2-5中,該原點被移動到(8.0, 24.0)。結果,顯示出來的是視圖圖像的不同部分。但是,由於邊框矩形並沒有改變,新的內容在父視圖中的位置和之前是一樣的。

圖2-5  改變視圖的邊界

Altering a view's bounds

請注意:缺省情況下,視圖的邊框並不會被父視圖的邊框裁剪。如果您希望讓一個視圖裁剪其子視圖,需要將其clipsToBounds屬性設置爲YES

座標系統變換

在視圖的drawRect:方法中常常藉助座標系統變換來進行描畫。而在iPhone OS系統中,您還可以用它來實現視圖的某些視覺效果。舉例來說,UIView類中包含一個transform屬性聲明,您可以通過它來對整個視圖實行各種類型的平移、比例縮放、和變焦縮放效果。缺省情況下,這個屬性的值是一個恆等變換,不會改變視圖的外觀。在加入變換之前,首先要得到該屬性中存儲的CGAffineTransform結構,用相應的Core Graphics函數實行變換,然後再將修改後的變換結構重新賦值給視圖的transform屬性。

請注意:當您將變換應用到視圖時,所有執行的變換都是相對於視圖的中心點。

平移一個視圖會使其所有的子視圖和視圖本身的內容一起移動。由於子視圖的座標系統是繼承並建立在這些變化的基礎上的,所以比例縮放也會影響子視圖的描畫。有關如何控制視圖內容縮放的更多信息,請參見“內容模式和比例縮放”部分。

重要提示:如果transform屬性的值不是恆等變換,則frame屬性的值就是未定義的,必須被忽略。在設置變換屬性之後,請使用boundscenter屬性來獲取視圖的位置和大小。

 

有關如何在drawRect:方法中使用變換的信息,請參見“座標和座標變換”部分;有關用於修改CGAffineTransform結構的函數,則請參見CGAffineTransform參考

內容模式與比例縮放

當您改變視圖的邊界,或者將一個比例因子應用到視圖的transform屬性聲明時,邊框矩形會發生等量的變化。根據內容模式的不同,視圖的內容也可能被縮放或重新定位,以反映上述的變化。視圖的contentMode屬性決定了邊界變化和縮放操作作用到視圖上產生的效果。缺省情況下,這個屬性的值被設置爲UIViewContentModeScaleToFill,意味着視圖內容總是被縮放,以適應新的邊框尺寸。作爲例子,圖2-6顯示了當視圖的水平縮放因子放大一倍時產生的效果。

圖2-6 使用scale-to-fill內容模式縮放視圖

View scaled using the scale-to-fill content mode

視圖內容的縮放僅在首次顯示視圖的時候發生,渲染後的內容會被緩存在視圖下面的層上。當邊界或縮放因子發生變化時,UIKit並不強制視圖進行重畫,而是根據其內容模式決定如何顯示緩存的內容。圖2-7比較了在不同的內容模式下,改變視圖邊界或應用不同的比例縮放因子時產生的結果。

圖2-7  內容模式比較

Content mode comparisons

對視圖應用一個比例縮放因子總是會使其內容發生縮放,而邊界的改變在某些內容模式下則不會發生同樣的結果。不同的UIViewContentMode常量(比如UIViewContentModeTopUIViewContentModeBottomRight)可以使當前的內容在視圖的不同角落或沿着視圖的不同邊界顯示,還有一種模式可以將內容顯示在視圖的中心。在這些模式的作用下,改變邊界矩形只會簡單地將現有的視圖內容移動到新的邊界矩形中對應的位置上。

當您希望在應用程序中實現尺寸可調整的控件時,請務必考慮使用內容模式。這樣做可以避免控件的外觀發生變形,以及避免編寫定製的描畫代碼。按鍵和分段控件(segmented control)特別適合基於內容模式的描畫。它們通常使用幾個圖像來創建控件外觀。除了有兩個固定尺寸的蓋帽圖像之外,按鍵可以通過一個可伸展的、寬度只有一個像素的中心圖像來實現水平方向的尺寸調整。它將每個圖像顯示在自己的圖像視圖中,而將可伸展的中間圖像的內容模式設置爲UIViewContentModeScaleToFill,使得在尺寸調整時兩端的外觀不會變形。更爲重要的是,每個圖像視圖的關聯圖像都可以由Core Animation來緩存,因此不需要編寫描畫代碼就可以支持動畫,從而使大大提高了性能。

內容模式通常有助於避免視圖內容的描畫,但是當您希望對縮放和尺寸調整過程中的視圖外觀進行特別的控制時,也可以使用UIViewContentModeRedraw模式。將視圖的內容模式設置爲這個值可以強制Core Animation使視圖的內容失效,並調用視圖的drawRect:方法,而不是自動進行縮放或尺寸調整。

自動尺寸調整行爲

當您改變視圖的邊框矩形時,其內嵌子視圖的位置和尺寸往往也需要改變,以適應原始視圖的新尺寸。如果視圖的autoresizesSubviews屬性聲明被設置爲YES,則其子視圖會根據autoresizingMask屬性的值自動進行尺寸調整。簡單配置一下視圖的自動尺寸調整掩碼常常就能使應用程序得到合適的行爲;否則,應用程序就必須通過重載layoutSubviews方法來提供自己的實現。

設置視圖的自動尺寸調整行爲的方法是通過位OR操作符將期望的自動尺寸調整常量連結起來,並將結果賦值給視圖的autoresizingMask屬性。表2-1列舉了自動尺寸調整常量,並描述這些常量如何影響給定視圖的尺寸和位置。舉例來說,如果要使一個視圖和其父視圖左下角的相對位置保持不變,可以加入UIViewAutoresizingFlexibleRightMarginUIViewAutoresizingFlexibleTopMargin常量,並將結果賦值給autoresizingMask屬性。當同一個軸向有多個部分被設置爲可變時,尺寸調整的裕量會被平均分配到各個部分上。

表2-1  自動尺寸調整掩碼常量

自動尺寸調整掩碼

描述

UIViewAutoresizingNone

這個常量如果被設置,視圖將不進行自動尺寸調整。

UIViewAutoresizingFlexibleHeight

這個常量如果被設置,視圖的高度將和父視圖的高度一起成比例變化。否則,視圖的高度將保持不變。

UIViewAutoresizingFlexibleWidth

這個常量如果被設置,視圖的寬度將和父視圖的寬度一起成比例變化。否則,視圖的寬度將保持不變。

UIViewAutoresizingFlexibleLeftMargin

這個常量如果被設置,視圖的左邊界將隨着父視圖寬度的變化而按比例進行調整。否則,視圖和其父視圖的左邊界的相對位置將保持不變。

UIViewAutoresizingFlexibleRightMargin

這個常量如果被設置,視圖的右邊界將隨着父視圖寬度的變化而按比例進行調整。否則,視圖和其父視圖的右邊界的相對位置將保持不變。

UIViewAutoresizingFlexibleBottomMargin

這個常量如果被設置,視圖的底邊界將隨着父視圖高度的變化而按比例進行調整。否則,視圖和其父視圖的底邊界的相對位置將保持不變。

UIViewAutoresizingFlexibleTopMargin

這個常量如果被設置,視圖的上邊界將隨着父視圖高度的變化而按比例進行調整。否則,視圖和其父視圖的上邊界的相對位置將保持不變。

圖2-8爲這些常量值的位置提供了一個圖形表示。如果這些常量之一被省略,則視圖在相應方向上的佈局就被固定;如果某個常量被包含在掩碼中,在該方向的視圖佈局就就靈活的。

圖2-8  視圖的自動尺寸調整掩碼常量

View autoresizing mask constants

如果您通過Interface Builder配置視圖,則可以用Size查看器的Autosizing控制來設置每個視圖的自動尺寸調整行爲。上圖中的靈活寬度及高度常量和Interface Builder中位於同樣位置的彈簧具有同樣的行爲,但是空白常量的行爲則是正好相反。換句話說,如果要將靈活右空白的自動尺寸調整行爲應用到Interface Builder的某個視圖,必須使相應方向空間的Autosizing控制爲空,而不是放置一個支柱。幸運的是,Interface Builder通過動畫顯示了您的修改對視圖自動尺寸調整行爲的影響。

如果視圖的autoresizesSubviews屬性被設置爲NO,則該視圖的直接子視圖的所有自動尺寸調整行爲將被忽略。類似地,如果一個子視圖的自動尺寸調整掩碼被設置爲UIViewAutoresizingNone,則該子視圖的尺寸將不會被調整,因而其直接子視圖的尺寸也不會被調整。

請注意:爲了使自動尺寸調整的行爲正確,視圖的transform屬性必須設置爲恆等變換;其它變換下的尺寸自動調整行爲是未定義的。

自動尺寸調整行爲可以適合一些佈局的要求,但是如果您希望更多地控制視圖的佈局,可以在適當的視圖類中重載layoutSubviews方法。有關視圖佈局管理的更多信息,請參見“響應佈局的變化”部分。

創建和管理視圖層次

管理用戶界面的視圖層次是開發應用程序用戶界面的關鍵部分。視圖的組織方式不僅定義了應用程序的視覺外觀,而且還定義了應用程序如何響應變化。視圖層次中的父-子關係可以幫助我們定義應用程序中負責處理觸摸事件的對象鏈。當用戶旋轉設備時,父-子關係也有助於定義每個視圖的尺寸和位置是如何隨着界面方向的變化而變化的。

圖2-9顯示了一個簡單的例子,說明如何通過視圖的分層來創建期望的視覺效果。在Clock程序中,頁籤條和導航條視圖,以及定製視圖混合在一起,實現了整個界面。

圖2-9  Clock程序的視圖層

Layered views in the Clock application

如果您探究Clock程序中視圖之間的關係,就會發現它們很像“改變視圖的層”部分中顯示的關係,窗口對象是應用程序的頁籤條、導航條、和定製視圖的根視圖。

圖2-10  Clock程序的視圖層次

View hierarchy for the Clock application

在iPhone應用程序的開發過程中,有幾種建立視圖層次的方法,包括基於Interface Builder的可視化方法和通過代碼編程的方法。本文的下面部分將向您介紹如何裝配視圖層次,以及如何在建立視圖層次之後尋找其中的視圖,還有如何在不同的視圖座標系統之間進行轉換。

創建一個視圖對象

創建視圖對象的最簡單方法是使用Interface Builder進行製作,然後將視圖對象從作成的nib文件載入內存。在Interface Builder的圖形環境中,您可以將新的視圖從庫中拖出,然後放到窗口或另一個視圖中,以快速建立需要的視圖層次。Interface Builder使用的是活的視圖對象,因此,當您用這個圖形環境構建用戶界面時,所看到的就是運行時裝載的外觀,而且不需要爲視圖層次中的每個視圖編寫單調乏味的內存分配和初始化代碼。

如果您不喜歡Interface Builder和nib文件,也可以通過代碼來創建視圖。創建一個新的視圖對象時,需要爲其分配內存,並向該對象發送一個initWithFrame:消息,以對其進行初始化。舉例來說,如果您要創建一個新的UIView類的實例作爲其它視圖的容器,則可以使用下面的代碼:

CGRect  viewRect = CGRectMake(0, 0, 100, 100);
UIView* myView = [[UIView alloc] initWithFrame:viewRect];

請注意:雖然所有系統提供的視圖對象都支持initWithFrame:消息,但是其中的一部分可能有自己偏好的初始化方法,您應該使用那些方法。有關定製初始化方法的更多信息,請參見相應的類參考文檔。

您在視圖初始化時指定的邊框矩形代表該視圖相對於未來父視圖的位置和大小。在將視圖顯示於屏幕上之前,您需要將它加入到窗口或其它視圖中。在這個時候,UIKit會根據您指定的邊框矩形將視圖放置到其父視圖的相應位置中。有關如何將視圖添加到視圖層次的信息,請參見“添加和移除子視圖”部分。

添加和移除子視圖

Interface Builder是建立視圖層次的最便利工具,因爲它可以讓您看到視圖在運行時的外觀。在界面製作完成後,它將視圖對象及其層次關係保存在nib文件中。在運行時,系統會按照nib文件的內容爲應用程序重新創建那些對象和關係。當一個nib文件被裝載時,系統會自動調用重建視圖層次所需要的UIView方法。

如果您不喜歡通過Interface Builder和nib文件來創建視圖層次,則可以通過代碼來創建。如果一個視圖必須具有某些子視圖才能工作,則應該在其initWithFrame:方法中進行對其創建,以確保子視圖可以和視圖一起被顯示和初始化。如果子視圖是應用程序設計的一部分(而不是視圖工作必需的),則應該在視圖的初始化代碼之外進行創建。在iPhone程序中,有兩個地方最常用於創建視圖和子視圖,它們是應用程序委託對象的applicationDidFinishLaunching:方法和視圖控制器loadView方法。

您可以通過下面的方法來操作視圖層次中的視圖對象:

  • 調用父視圖的addSubview:方法來添加視圖,該方法將一個視圖添加到子視圖列表的最後。

  • 調用父視圖的insertSubview:...方法可以在父視圖的子視圖列表中間插入視圖。

  • 調用父視圖的bringSubviewToFront:sendSubviewToBack:、或exchangeSubviewAtIndex:withSubviewAtIndex:方法可以對父視圖的子視圖進行重新排序。使用這些方法比從父視圖中移除子視圖並再次插入要快一些。

  • 調用子視圖(而不是父視圖)的removeFromSuperview方法可以將子視圖從父視圖中移除。

在添加子視圖時,UIKit會根據子視圖的當前邊框矩形確定其在父視圖中的初始位置。您可以隨時通過修改子視圖的frame屬性聲明來改變其位置。缺省情況下,邊框位於父視圖可視邊界外部的子視圖不會被裁剪。如果您希望激活裁剪功能,必須將父視圖的clipsToBounds屬性設置爲YES

程序清單2-1顯示了一個應用程序委託對象的applicationDidFinishLaunching:方法示例。在這個例子中,應用程序委託在啓動時通過代碼創建全部的用戶界面。界面中包含兩個普通的UIView對象,用於顯示基本顏色。每個視圖都被嵌入到窗口中,窗口也是UIView 的一個子類,因此可以作爲父視圖。父視圖會保持它們的子視圖,因此這個方法釋放了新創建的視圖對象,以避免重複保持。

程序清單2-1  創建一個帶有視圖的窗口

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    // Create the window object and assign it to the
    // window instance variable of the application delegate.
    window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    window.backgroundColor = [UIColor whiteColor];
 
    // Create a simple red square
    CGRect redFrame = CGRectMake(10, 10, 100, 100);
    UIView *redView = [[UIView alloc] initWithFrame:redFrame];
    redView.backgroundColor = [UIColor redColor];
 
    // Create a simple blue square
    CGRect blueFrame = CGRectMake(10, 150, 100, 100);
    UIView *blueView = [[UIView alloc] initWithFrame:blueFrame];
    blueView.backgroundColor = [UIColor blueColor];
 
    // Add the square views to the window
    [window addSubview:redView];
    [window addSubview:blueView];
 
    // Once added to the window, release the views to avoid the
    // extra retain count on each of them.
    [redView release];
    [blueView release];
 
    // Show the window.
    [window makeKeyAndVisible];
}

重要提示:在內存管理方面,可以將子視圖考慮爲其它的集合對象。特別是當您通過addSubview:方法將一個視圖作爲子視圖插入時,父視圖會對其進行保持操作。反過來,當您通過removeFromSuperview方法將子視圖從父視圖移走時,子視圖會被自動釋放。在將視圖加入視圖層次之後釋放該對象可以避免多餘的保持操作,從而避免內存泄露。

有關Cocoa內存管理約定的更多信息,請參見Cocoa內存管理編程指南

 

當您爲某個視圖添加子視圖時,UIKit會向相應的父子視圖發送幾個消息,通知它們當前發生的狀態變化。您可以在自己的定製視圖中對諸如willMoveToSuperview:willMoveToWindow:willRemoveSubview:didAddSubview:didMoveToSuperview、和didMoveToWindow這樣的方法進行重載,以便在事件發生的前後進行必要的處理,並根據發生的變化更新視圖的狀態信息。

在視圖層次建立之後,您可以通過視圖的superview屬性來取得其父視圖,或者通過subviews屬性取得視圖的子視圖。您也可以通過isDescendantOfView:方法來判定一個視圖是否在其父視圖的視圖層中。一個視圖層次的根視圖沒有父視圖,因此其superview屬性被設置爲nil。對於當前被顯示在屏幕上的視圖,窗口對象通常是整個視圖層次的根視圖。

您可以通過視圖的window屬性來取得指向其父窗口(如果有的話)的指針,如果視圖還沒有被鏈接到窗口上,則該屬性會被設置爲nil

視圖層次中的座標轉換

很多時候,特別是處理事件的時候,應用程序可能需要將一個相對於某邊框的座標值轉換爲相對於另一個邊框的值。例如,觸摸事件通常使用基於窗口指標系統的座標值來報告事件發生的位置,但是視圖對象需要的是相對於視圖本地座標的位置信息,兩者可能是不一樣的。UIView類定義了下面這些方法,用於在不同的視圖本地座標系統之間進行座標轉換:

convert...:fromView:方法將指定視圖的座標值轉換爲視圖本地座標系統的座標值;convert...:toView:方法則將視圖本地座標系統的座標值轉換爲指定視圖座標系統的座標值。如果傳入nil作爲視圖引用參數的值,則上面這些方法會將視圖所在窗口的座標系統作爲轉換的源或目標座標系統。

除了UIView的轉換方法之外,UIWindow類也定義了幾個轉換方法。這些方法和UIView的版本類似,只是UIView定義的方法將視圖本地座標系統作爲轉換的源或目標座標系統,而UIWindow的版本則使用窗口座標系統。

當參與轉換的視圖沒有被旋轉,或者被轉換的對象僅僅是點的時候,座標轉換相當直接。如果是在旋轉之後的視圖之間轉換矩形或尺寸數據,則其幾何結構必須經過合理的改變,才能得到正確的結果座標。在對矩形結構進行轉換時,UIView類假定您希望保證原來的屏幕區域被覆蓋,因此轉換後的矩形會被放大,其結果是使放大後的矩形(如果放在對應的視圖中)可以完全覆蓋原來的矩形區域。圖2-11顯示了將rotatedView對象的座標系統中的矩形轉換到其超類(outerView)座標系統的結果。

圖2-11  對旋轉後視圖中的值進行轉換

Converting values in a rotated view

對於尺寸信息,UIView簡單地將它處理爲分別相對於源視圖和目標視圖(0.0, 0.0)點的偏移量。雖然偏移量保持不變,但是相對於座標軸的差額會隨着視圖的旋轉而移動。在轉換尺寸數據時,UIKit總是返回正的數值。

標識視圖

UIView類中包含一個tag屬性。藉助這個屬性,您可以通過一個整數值來標識一個視圖對象。您可以通過這個屬性來唯一標識視圖層次中的視圖,以及在運行時進行視圖的檢索(基於tag標識的檢索比您自行遍歷視圖層次要快)。tag屬性的缺省值爲0

您可以通過UIViewviewWithTag:方法來檢索標識過的視圖。該方法從消息的接收者自身開始,通過深度優先的方法來檢索接收者的子視圖。

在運行時修改視圖

應用程序在接收用戶輸入時,需要通過調整自己的用戶界面來進行響應。應用程序可能重新排列界面上的視圖、刷新屏幕上模型數據已被改變的視圖、或者裝載一組全新的視圖。在決定使用哪種技術時,要考慮您的用戶界面,以及您希望實現什麼。但是,如何初始化這些技術對於所有應用程序都是一樣的。本章的下面部分將描述這些技術,以及如何通過這些技術在運行時更新您的用戶界面。

請注意:如果您需要了解UIKit如何在框架內部和您的定製代碼之間轉移事件和消息的背景信息,請在繼續閱讀本文之前查閱“視圖交互模型”部分。

實現視圖動畫

動畫爲用戶界面在不同狀態之間的遷移過程提供流暢的視覺效果。在iPhone OS中,動畫被廣泛用於視圖的位置調整、尺寸變化、甚至是alpha值的變化(以實現淡入淡出的效果)。動畫支持對於製作易於使用的應用程序是至關重要的,因此,UIKit直接將它集成到UIView類中,以簡化動畫的創建過程。

UIView類定義了幾個內在支持動畫屬性聲明—也就是說,當這些屬性值發生變化時,視圖爲其變化過程提供內建的動畫支持。雖然執行動畫所需要的工作由UIView類自動完成,但您仍然必須在希望執行動畫時通知視圖。爲此,您需要將改變給定屬性的代碼包裝在一個動畫塊中。

動畫塊從調用UIViewbeginAnimations:context:類方法開始,而以調用commitAnimations類方法作爲結束。在這兩個調用之間,您可以配置動畫的參數和改變希望實行動畫的屬性值。一旦調用commitAnimations方法,UIKit就會開始執行動畫,即把給定屬性從當前值到新值的變化過程用動畫表現出來。動畫塊可以被嵌套,但是在最外層的動畫塊提交之前,被嵌套的動畫不會被執行。

表2-2列舉了UIView類中支持動畫的屬性。

表2-2  支持動畫的屬性

屬性

描述

frame

視圖的邊框矩形,位於父視圖的座標系中。

bounds

視圖的邊界矩形,位於視圖的座標系中。

center

邊框的中心,位於父視圖的座標系中。

transform

視圖上的轉換矩陣,相對於視圖邊界的中心。

alpha

視圖的alpha值,用於確定視圖的透明度。

配置動畫的參數

除了在動畫塊中改變屬性值之外,您還可以對其它參數進行配置,以確定您希望得到的動畫行爲。爲此,您可以調用下面這些UIView的類方法:

  • setAnimationStartDate:方法來設置動畫在commitAnimations方法返回之後的發生日期。缺省行爲是使動畫立即在動畫線程中執行。

  • setAnimationDelay:方法來設置實際發生動畫和commitAnimations方法返回的時間點之間的間隔。

  • setAnimationDuration:方法來設置動畫持續的秒數。

  • setAnimationCurve:方法來設置動畫過程的相對速度,比如動畫可能在啓示階段逐漸加速,而在結束階段逐漸減速,或者整個過程都保持相同的速度。

  • setAnimationRepeatCount:方法來設置動畫的重複次數。

  • setAnimationRepeatAutoreverses:方法來指定動畫在到達目標值時是否自動反向播放。您可以結合使用這個方法和setAnimationRepeatCount:方法,使各個屬性在初始值和目標值之間平滑切換一段時間。

commitAnimations類方法在調用之後和動畫開始之前立刻返回。UIKit在一個獨立的、和應用程序的主事件循環分離的線程中執行動畫。commitAnimations方法將動畫發送到該線程,然後動畫就進入線程中的隊列,直到被執行。缺省情況下,只有在當前正在運行的動畫塊執行完成後,Core Animation纔會啓動隊列中的動畫。但是,您可以通過向動畫塊中的setAnimationBeginsFromCurrentState:類方法傳入YES來重載這個行爲,使動畫立即啓動。這樣做會停止當前正在執行的動畫,而使新動畫在當前狀態下開始執行。

缺省情況下,所有支持動畫的屬性在動畫塊中發生的變化都會形成動畫。如果您希望讓動畫塊中發生的某些變化不產生動畫效果,可以通過setAnimationsEnabled:方法來暫時禁止動畫,在完成修改後才重新激活動畫。在調用setAnimationsEnabled:方法並傳入NO值之後,所有的改變都不會產生動畫效果,直到用YES值再次調用這個方法或者提交整個動畫塊時,動畫纔會恢復。您可以用areAnimationsEnabled方法來確定當前是否激活動畫。

配置動畫的委託

您可以爲動畫塊分配一個委託,並通過該委託接收動畫開始和結束的消息。當您需要在動畫開始前和結束後立即執行其它任務時,可能就需要這樣做。您可以通過UIViewsetAnimationDelegate:類方法來設置委託,並通過setAnimationWillStartSelector:setAnimationDidStopSelector:方法來指定接收消息的選擇器方法。消息處理方法的形式如下:

- (void)animationWillStart:(NSString *)animationID context:(void *)context;
- (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context;

上面兩個方法的animationIDcontext參數和動畫塊開始時傳給beginAnimations:context:方法的參數相同:

  • animationID - 應用程序提供的字符串,用於標識一個動畫塊中的動畫。

  • context - 也是應用程序提供的對象,用於向委託對象傳遞額外的信息。

setAnimationDidStopSelector:選擇器方法還有一個參數—即一個布爾值。如果動畫順利完成,沒有被其它動畫取消或停止,則該值爲YES

響應佈局的變化

任何時候,當視圖的佈局發生改變時,UIKit會激活每個視圖的自動尺寸調整行爲,然後調用各自的layoutSubviews方法,使您有機會進一步調整子視圖的幾何尺寸。下面列舉的情形都會引起視圖佈局的變化:

  • 視圖邊界矩形的尺寸發生變化。

  • 滾動視圖的內容偏移量—也就是可視內容區域的原點—發生變化。

  • 和視圖關聯的轉換矩陣發生變化。

  • 和視圖層相關聯的Core Animation子層組發生變化。

  • 您的應用程序調用視圖的setNeedsLayoutlayoutIfNeeded方法來強制進行佈局。

  • 您的應用程序調用視圖背後的層對象的setNeedsLayout方法來強制進行佈局。

子視圖的初始佈局由視圖的自動尺寸調整行爲來負責。應用這些行爲可以保證您的視圖接近其設計的尺寸。有關自動尺寸調整行爲如何影響視圖的尺寸和位置的更多信息,請參見“自動尺寸調整行爲”部分。

有些時候,您可能希望通過layoutSubviews方法來手工調整子視圖的佈局,而不是完全依賴自動尺寸調整行爲。舉例來說,如果您要實現一個由幾個子視圖元素組成的定製控件,則可以通過手工調整子視圖來精確控制控件在一定尺寸範圍內的外觀。還有,如果一個視圖表示的滾動內容區域很大,可以選擇將內容顯示爲一組平鋪的子視圖,在滾動過程中,可以回收離開屏幕邊界的視圖,並在填充新內容後將它重新定位,使它成爲下一個滾入屏幕的視圖。

請注意:您也可以用layoutSubviews方法來調整作爲子層鏈接到視圖層的定製CALayer對象。您可以通過對隱藏在視圖後面的層層次進行管理,實現直接基於Core Animation的高級動畫。有關如何通過Core Animation管理層層次的更多信息,請參見Core Animation編程指南

在編寫佈局代碼時,請務必在應用程序支持的每個方向上都進行測試。對於同時支持景觀方向和肖像方向的應用程序,必須確認其是否能正確處理兩個方向上的佈局。類似地,您的應用程序應該做好處理其它系統變化的準備,比如狀態條高度的變化,如果用戶在使用您的應用程序的同時接聽電話,然後再掛斷,就會發生這種變化。在掛斷時,負責管理視圖的視圖控制器可能會調整視圖的尺寸,以適應縮小的狀態條。之後,這樣的變化會向下滲透到應用程序的其它視圖。

重畫視圖的內容

有些時候,應用程序數據模型的變化會影響到相應的用戶界面。爲了反映這些變化,您可以將相應的視圖標識爲需要刷新(通過調用setNeedsDisplaysetNeedsDisplayInRect:方法)。和簡單創建一個圖形上下文並進行描畫相比,將視圖標識爲需要刷新的方法使系統有機會更有效地執行描畫操作。舉例來說,如果您在某個運行週期中將一個視圖的幾個區域標識爲需要刷新,系統就會將這些需要刷新的區域進行合併,並最終形成一個drawRect:方法的調用。結果,只需要創建一個圖形上下文就可以描畫所有這些受影響的區域。這個做法比連續快速創建幾個圖形上下文要有效得多。

實現drawRect:方法的視圖總是需要檢查傳入的矩形參數,並用它來限制描畫操作的範圍。因爲描畫是開銷相對昂貴的操作,以這種方式來限制描畫是提高性能的好方法。

缺省情況下,視圖在幾何上的變化並不自動導致重畫。相反,大多數幾何變化都由Core Animation來自動處理。具體來說,當您改變視圖的frameboundscenter、或transform屬性時,Core Animation會將相應的幾何變化應用到與視圖層相關聯的緩存位圖上。在很多情況下,這種方法是完全可以接受的,但是如果您發現結果不是您期望得到的,則可以強制UIKit對視圖進行重畫。爲了避免Core Animation自動處理幾何變化,您可以將視圖的contentMode屬性聲明設置爲UIViewContentModeRedraw。更多有關內容模式的信息,請參見“內容模式和比例縮放”部分。

隱藏視圖

您可以通過改變視圖的hidden屬性聲明來隱藏或顯示視圖。將這個屬性設置爲YES會隱藏視圖,設置爲NO則可以顯示視圖。對一個視圖進行隱藏會同時隱藏其內嵌的所有子視圖,就好象它們自己的hidden屬性也被設置一樣。

當您隱藏一個視圖時,該視圖仍然會保留在視圖層次中,但其內容不會被描畫,也不會接收任何觸摸事件。由於隱藏視圖仍然存在於視圖層次中,所以會繼續參與自動尺寸調整和其它佈局操作。如果被隱藏的視圖是當前的第一響應者,則該視圖會自動放棄其自動響應者的狀態,但目標爲第一響應者的事件仍然會傳遞給隱藏視圖。有關響應者鏈的更多信息,請參見“響應者對象和響應者鏈”部分。

創建一個定製視圖

UIView類爲在屏幕上顯示內容及處理觸摸事件提供了潛在的支持,但是除了在視圖區域內描畫帶有alpha值的背景色之外,UIView類的實例不做其它描畫操作,包括其子視圖的描畫。如果您的應用程序需要顯示定製的內容,或以特定的方式處理觸摸事件,必須創建UIView的定製子類。

本章的下面部分將描述一些定製視圖對象可能需要實現的關鍵方法和行爲。有關子類化的更多信息,請參見UIView類參考

初始化您的定製視圖

您定義的每個新的視圖對象都應該包含initWithFrame:初始化方法。該方法負責在創建對象時對類進行初始化,使之處於已知的狀態。在通過代碼創建您的視圖實例時,需要使用這個方法。

程序清單2-2顯示了標準的initWithFrame:方法的一個框架實現。該實現首先調用繼承自超類的實現,然後初始化類的實例變量和狀態信息,最後返回初始化完成的對象。您通常需要首先執行超類的實現,以便在出現問題時可以簡單地終止自己的初始化代碼,返回nil

程序清單2-2  初始化一個視圖的子類

- (id)initWithFrame:(CGRect)aRect {
    self = [super initWithFrame:aRect];
    if (self) {
          // setup the initial properties of the view
          ...
       }
    return self;
}

如果您從nib文件中裝載定製視圖類的實例,則需要知道:在iPhone OS中,裝載nib的代碼並不通過initWithFrame:方法來實例化新的視圖對象,而是通過NSCoding協議定義的initWithCoder:方法來進行。

即使您的視圖採納了NSCoding協議,Interface Builder也不知道它的定製屬性,因此不知道如何將那些屬性編碼到nib文件中。所以,當您從nib文件裝載定製視圖時,initWithCoder:方法不具有進行正確初始化所需要的信息。爲了解決這個問題,您可以在自己的類中實現awakeFromNib方法,特別用於從nib文件裝載的定製類。

描畫您的視圖內容

當您改變視圖內容時,可以通過setNeedsDisplaysetNeedsDisplayInRect:方法來將需要重畫的部分通知給系統。在應用程序返回運行循環之後,會對所有的描畫請求進行合併,計算界面中需要被更新的部分;之後就開始遍歷視圖層次,向需要更新的視圖發送drawRect:消息。遍歷的起點是視圖層次的根視圖,然後從後往前遍歷其子視圖。在可視邊界內顯示定製內容的視圖必須實現其drawRect:方法,以便對該內容進行渲染。

在調用視圖的drawRect:方法之前,UIKit會爲其配置描畫的環境,即創建一個圖形上下文,並調整其座標系統和裁剪區,使之和視圖的座標系統及邊界相匹配。因此,在您的drawRect:方法被調用時,您可以使用UIKit的類和函數、Quartz的函數、或者使用兩者相結合的方法來直接進行描畫。需要的話,您可以通過UIGraphicsGetCurrentContext函數來取得當前圖形上下文的指針,實現對它的訪問。

重要提示:只有當定製視圖的drawRect:方法被調用的期間,當前圖形上下文才是有效的。UIKit可能爲該方法的每個調用創建不同的圖形上下文,因此,您不應該對該對象進行緩存並在之後使用。

 

程序清單2-3顯示了drawRect:方法的一個簡單實現,即在視圖邊界描畫一個10像素寬的紅色邊界。由於UIKit描畫操作的實現也是基於Quartz,所以您可以像下面這樣混合使用不同的描畫調用來得到期望的結果。

程序清單2-3  一個描畫方法

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect    myFrame = self.bounds;
 
    CGContextSetLineWidth(context, 10);
 
    [[UIColor redColor] set];
    UIRectFrame(myFrame);
}

如果您能確定自己的描畫代碼總是以不透明的內容覆蓋整個視圖的表面,則可以將視圖的opaque屬性聲明設置爲YES,以提高描畫代碼的總體效率。當您將視圖標識爲不透明時,UIKit會避免對該視圖正下方的內容進行描畫。這不僅減少了描畫開銷的時間,而且減少內容合成需要的工作。然而,只有當您能確定視圖提供的內容爲不透明時,才能將這個屬性設置爲YES;如果您不能保證視圖內容總是不透明,則應該將它設置爲NO

提高描畫性能(特別是在滾動過程)的另一個方法是將視圖的clearsContextBeforeDrawing屬性設置爲NO。當這個屬性被設置爲YES時,UIKIt會在調用drawRect:方法之前,把即將被該方法更新的區域填充爲透明的黑色。將這個屬性設置爲NO可以取消相應的填充操作,而由應用程序負責完全重畫傳給drawRect:方法的更新矩形中的部分。這樣的優化在滾動過程中通常是一個好的折衷。

響應事件

UIView類是UIResponder的一個子類,因此能夠接收用戶和視圖內容交互時產生的觸摸事件。觸摸事件從發生觸摸的視圖開始,沿着響應者鏈進行傳遞,直到最後被處理。視圖本身就是響應者,是響應者鏈的參與者,因此可以收到所有關聯子視圖派發給它們的觸摸事件。

處理觸摸事件的視圖通常需要實現下面的所有方法,更多細節請參見“事件處理”部分:

請記住,在缺省情況下,視圖每次只響應一個觸摸動作。如果用戶將第二個手指放在屏幕上,系統會忽略該觸摸事件,而不會將它報告給視圖對象。如果您希望在視圖的事件處理器方法中跟蹤多點觸摸手勢,則需要重新激活多點觸摸事件,具體方法是將視圖的multipleTouchEnabled屬性聲明設置爲YES

某些視圖,比如標籤和圖像視圖,在初始狀態下完全禁止事件處理。您可以通過改變視圖的userInteractionEnabled屬性值來控制視圖是否可以對事件進行處理。當某個耗時很長的操作被掛起時,您可以暫時將這個屬性設置爲NO,使用戶無法對視圖的內容進行操作。爲了阻止事件到達您的視圖,還可以使用UIApplication對象的beginIgnoringInteractionEventsendIgnoringInteractionEvents方法。這些方法影響的是整個應用程序的事件分發,而不僅僅是某個視圖。

在處理觸摸事件時,UIKit會通過UIViewhitTest:withEvent:pointInside:withEvent:方法來確定觸摸事件是否發生在指定的視圖上。雖然很少需要重載這些方法,但是您可以通過重載來使子視圖無法處理觸摸事件。

視圖對象的清理

如果您的視圖類分配了任何內存、存儲了任何對象的引用、或者持有在釋放視圖時也需要被釋放的資源,則必須實現其dealloc方法。當您的視圖對象的保持數爲零、且視圖本身即將被解除分配時,系統會調用其dealloc方法。您在這個方法的實現中應該釋放視圖持有的對象和資源,然後調用超類的實現,如程序程序清單2-4所示。

程序清單2-4  實現dealloc方法

- (void)dealloc {
    // Release a retained UIColor object
    [color release];
 
    // Call the inherited implementation
    [super dealloc];
}

事件處理

本章將描述iPhone OS系統中的事件類型,並解釋如何處理這些事件。文中還將討論如何在應用程序內部或不同應用程序間通過UIPasteboard類提供的設施進行數據的拷貝和粘貼,該類是iPhone OS 3.0引入的。

iPhone OS支持兩種類型的事件:即觸摸事件或運動事件。在iPhone OS 3.0中,UIEvent類已經被擴展爲不僅可以包含觸摸事件和運動事件,還可以容納將來可能引入的其它事件類型。每個事件都有一個與之關聯的事件類型和子類型,可以通過UIEventtypesubtype屬性聲明進行訪問,類型既包括觸摸事件,也包括運動事件。在iPhone OS 3.0上,子類型只有一種,即搖擺-運動子類型(UIEventSubtypeMotionShake)。

觸摸事件

iPhone OS中的觸摸事件基於多點觸摸模型。用戶不是通過鼠標和鍵盤,而是通過觸摸設備的屏幕來操作對象、輸入數據、以及指示自己的意圖。iPhone OS將一個或多個和屏幕接觸的手指識別爲多點觸摸序列的一部分,該序列從第一個手指碰到屏幕開始,直到最後一個手指離開屏幕結束。iPhone OS通過一個多點觸摸序列來跟蹤與屏幕接觸的手指,記錄每個手指的觸摸特徵,包括手指在屏幕上的位置和發生觸摸的時間。應用程序通常將特定組合的觸摸識別爲手勢,並以用戶直覺的方式來進行響應,比如對收縮雙指距離的手勢,程序的響應是縮小顯示的內容;對輕拂屏幕的手勢,則響應爲滾動顯示內容。

請注意:手指在屏幕上能達到的精度和鼠標指針有很大的不同。當用戶觸擊屏幕時,接觸區域實際上是橢圓形的,而且比用戶想像的位置更靠下一點。根據觸摸屏幕的手指、手指的尺寸、手指接觸屏幕的力量、手指的方向、以及其它因素的不同,其“接觸部位”的尺寸和形狀也有所不同。底層的多點觸摸系統會分析所有的這些信息,爲您計算出單一的觸點。

很多UIKit類對多點觸摸事件的處理方式不同於它的對象實例,特別是像UIButtonUISlider這樣的UIControl的子類。這些子類的對象—被稱爲控件對象—只接收特定類型的手勢,比如觸擊或向特定方向拖拽。控件對象在正確配置之後,會在某種手勢發生後將動作消息發送給目標對象。其它的UIKit類則在其它的上下文中處理手勢,比如UIScrollView可以爲表格視圖和具有很大內容區域的文本視圖提供滾動行爲。

某些應用程序可能不需要直接處理事件,它們可以依賴UIKit類實現的行爲。但是,如果您創建了UIView定製子類—這是iPhone OS系統開發的常見模式—且希望該視圖響應特定的觸摸事件,就需要實現處理該事件所需要的代碼。而且,如果您希望一個UIKit對象以不同的方式響應事件,就必須創建框架類的子類,並重載相應的事件處理方法。

事件和觸摸

在iPhone OS中,觸摸動作是指手指碰到屏幕或在屏幕上移動,它是一個多點觸摸序列的一部分。比如,一個pinch-close手勢就包含兩個觸摸動作:即屏幕上的兩個手指從相反方向靠近對方。一些單指手勢則比較簡單,比如觸擊、雙擊、或輕拂(即用戶快速碰擦屏幕)。應用程序也可以識別更爲複雜的手勢,舉例來說,如果一個應用程序使用具有轉盤形狀的定製控件,用戶就需要用多個手指來“轉動”轉盤,以便進行某種精調。

事件是當用戶手指觸擊屏幕及在屏幕上移動時,系統不斷髮送給應用程序的對象。事件對象爲一個多點觸摸序列中所有觸摸動作提供一個快照,其中最重要的是特定視圖中新發生或有變化的觸摸動作。一個多點觸摸序列從第一個手指碰到屏幕開始,其它手指隨後也可能觸碰屏幕,所有手指都可能在屏幕上移動。當最後一個手指離開屏幕時,序列就結束了。在觸摸的每個階段,應用程序都會收到事件對象。

觸摸信息有時間和空間兩方面,時間方面的信息稱爲階段(phrase),表示觸摸是否剛剛開始、是否正在移動或處於靜止狀態,以及何時結束—也就是手指何時從屏幕舉起(參見圖3-1)。觸摸信息還包括當前在視圖或窗口中的位置信息,以及之前的位置信息(如果有的話)。當一個手指接觸屏幕時,觸摸就和某個窗口或視圖關聯在一起,這個關聯在事件的整個生命週期都會得到維護。如果有多個觸摸同時發生,則只有和同一個視圖相關聯的觸摸會被一起處理。類似地,如果兩個觸摸事件發生的間隔時間很短,也只有當它們和同一個視圖相關聯時,纔會被處理爲多觸擊事件。

圖3-1 多點觸摸序列和觸摸階段

A multi-touch sequence and touch phases

在iPhone OS中,一個UITouch對象表示一個觸摸,一個UIEvent對象表示一個事件。事件對象中包含與當前多點觸摸序列相對應的所有觸摸對象,還可以提供與特定視圖或窗口相關聯的觸摸對象(參見圖3-2)。在一個觸摸序列發生的過程中,對應於特定手指的觸摸對象是持久的,在跟蹤手指運動的過程中,UIKit會對其進行修改。發生改變的觸摸屬性變量有觸摸階段、觸摸在視圖中的位置、發生變化之前的位置、以及時間戳。事件處理代碼通過檢查這些屬性的值來確定如何響應事件。

圖3-2 UIEvent對象及其UITouch對象間的關係

Relationship of a UIEvent object and its UITouch objects

系統可能隨時取消多點觸摸序列,進行事件處理的應用程序必須做好正確響應的準備。事件的取消可能是由於重載系統事件引起的,電話呼入就是這樣的例子。

事件的傳遞

系統將事件按照特定的路徑傳遞給可以對其進行處理的對象。如“核心應用程序架構”部分描述的那樣,當用戶觸摸設備屏幕時,iPhone OS會將它識別爲一組觸摸對象,並將它們封裝在一個UIEvent對象中,放入當前應用程序的事件隊列中。事件對象將特定時刻的多點觸摸序列封裝爲一些觸摸對象。負責管理應用程序的UIApplication單件對象將事件從隊列的頂部取出,然後派發給其它對象進行處理。典型情況下,它會將事件發送給應用程序的鍵盤焦點窗口—即擁有當前用戶事件焦點的窗口,然後代表該窗口的UIWindow對象再將它發送給第一響應者進行處理(第一響應者在 “響應者對象和響應者鏈”部分中描述)

應用程序通過觸碰測試(hit-testing)來尋找事件的第一響應者,即通過遞歸調用視圖層次中視圖對象的hitTest:withEvent:方法來確認發生觸摸的子視圖。觸摸對象的整個生命週期都和該視圖互相關聯,即使觸摸動作最終移動到該視圖區域之外也是如此。“事件處理技巧”部分對觸碰測試在編程方面的一些隱含意義進行討論。

UIApplication對象和每個UIWindow對象都在sendEvent:方法(兩個類都聲明瞭這個方法)中派發事件。由於這些方法是事件進入應用程序的通道,所以,您可以從UIApplicationUIWindow派生出子類,重載其sendEvent:方法,實現對事件的監控或執行特殊的事件處理。但是,大多數應用程序都不需要這樣做。

響應者對象和響應者鏈

響應者對象是可以響應事件並對其進行處理的對象。UIResponder是所有響應者對象的基類,它不僅爲事件處理,而且也爲常見的響應者行爲定義編程接口。UIApplicationUIView、和所有從UIView派生出來的UIKit類(包括UIWindow)都直接或間接地繼承自UIResponder類。

第一響應者是應用程序中當前負責接收觸摸事件的響應者對象(通常是一個UIView對象)。UIWindow對象以消息的形式將事件發送給第一響應者,使其有機會首先處理事件。如果第一響應者沒有進行處理,系統就將事件(通過消息)傳遞給響應者鏈中的下一個響應者,看看它是否可以進行處理。

響應者鏈是一系列鏈接在一起的響應者對象,它允許響應者對象將處理事件的責任傳遞給其它更高級別的對象。隨着應用程序尋找能夠處理事件的對象,事件就在響應者鏈中向上傳遞。響應者鏈由一系列“下一個響應者”組成,其順序如下:

  1. 第一響應者將事件傳遞給它的視圖控制器(如果有的話),然後是它的父視圖。

  2. 類似地,視圖層次中的每個後續視圖都首先傳遞給它的視圖控制器(如果有的話),然後是它的父視圖。

  3. 最上層的容器視圖將事件傳遞給UIWindow對象。
  4. UIWindow對象將事件傳遞給UIApplication單件對象。

如果應用程序找不到能夠處理事件的響應者對象,則丟棄該事件。

響應者鏈中的所有響應者對象都可以實現UIResponder的某個事件處理方法,因此也都可以接收事件消息。但是,它們可能不願處理或只是部分處理某些事件。如果是那樣的話,它們可以將事件消息轉送給下一個響應者,方法大致如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch* touch = [touches anyObject];
    NSUInteger numTaps = [touch tapCount];
    if (numTaps < 2) {
        [self.nextResponder touchesBegan:touches withEvent:event];
   } else {
        [self handleDoubleTap:touch];
   }
}

請注意:如果一個響應者對象將一個多點觸摸序列的初始階段的事件處理消息轉發給下一個響應者(在touchesBegan:withEvent:方法中), 就應該同樣轉發該序列的其它事件處理消息。

動作消息的處理也使用響應者鏈。當用戶對諸如按鍵或分頁控件這樣的UIControl對象進行操作時,控件對象(如果正確配置的話)會向目標對象發送動作消息。但是,如果目標對象被指定爲nil,應用程序就會像處理事件消息那樣,把該動作消息路由給第一響應者。如果第一響應者沒有進行處理,再發送給其下一個響應者,以此類推,將消息沿着響應者鏈向上傳遞。

調整事件的傳遞

UIKit爲應用程序提供了一些簡化事件處理、甚至完全關閉事件流的編程接口。下面對這些方法進行總結:

  • 關閉事件的傳遞。缺省情況下,視圖會接收觸摸事件。但是,您可以將其userInteractionEnabled屬性聲明設置爲NO,關閉事件傳遞的功能。隱藏或透明的視圖也不能接收事件。

  • 在一定的時間內關閉事件的傳遞。應用程序可以調用UIApplicationbeginIgnoringInteractionEvents方法,並在隨後調用endIgnoringInteractionEvents方法來實現這個目的。前一個方法使應用程序完全停止接收觸摸事件消息,第二個方法則重啓消息的接收。某些時候,當您的代碼正在執行動畫時,可能希望關閉事件的傳遞。

  • 打開多點觸摸的傳遞。 缺省情況下,視圖只接收多點觸摸序列的第一個觸摸事件,而忽略所有其它事件。如果您希望視圖處理多點觸摸,就必須使它啓用這個功能。在代碼或Interface Builder的查看器窗口中將視圖的multipleTouchEnabled屬性設置爲YES,就可以實現這個目標。

  • 將事件傳遞限制在某個單獨的視圖上。 缺省情況下,視圖的exclusiveTouch屬性被設置爲NO。將這個屬性設置爲YES會使相應的視圖具有這樣的特性:即當該視圖正在跟蹤觸摸動作時,窗口中的其它視圖無法同時進行跟蹤,它們不能接收到那些觸摸事件。然而,一個標識爲“獨佔觸摸”的視圖不能接收與同一窗口中其它視圖相關聯的觸摸事件。如果一個手指接觸到一個獨佔觸摸的視圖,則僅當該視圖是窗口中唯一一個跟蹤手指的視圖時,觸摸事件纔會被傳遞。如果一個手指接觸到一個非獨佔觸摸的視圖,則僅當窗口中沒有其它獨佔觸摸視圖跟蹤手指時,該觸摸事件纔會被傳遞。

  • 將事件傳遞限制在子視圖上。一個定製的UIView類可以通過重載hitTest:withEvent:方法來將多點觸摸事件的傳遞限制在它的子視圖上。這個技巧的討論請參見“事件處理技巧”部分。

處理多點觸摸事件

爲了處理多點觸摸事件,UIView的定製子類(比較不常見的還有UIApplicationUIWindow的定製子類)必須至少實現一個UIResponder的事件處理方法。本文的下面部分將對這些方法進行描述,討論處理常見手勢的方法,並展示一個處理複雜多點觸摸事件的響應者對象實例,以及就事件處理的某些技術提出建議。

事件處理方法

在一個多點觸摸序列發生的過程中,應用程序會發出一系列事件消息。爲了接收和處理這些消息,響應者對象的類必須至少實現下面這些由UIResponder類聲明的方法之一:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

在給定的觸摸階段中,如果發生新的觸摸動作或已有的觸摸動作發生變化,應用程序就會發送這些消息:

上面這些方法都和特定的觸摸階段(比如UITouchPhaseBegan)相關聯,該信息存在於UITouch對象的phase屬性聲明中。

每個與事件處理方法相關聯的消息都有兩個參數。第一個參數是一個UITouch對象的集合,表示給定階段中新的或者發生變化的觸摸動作;第二個參數是一個UIEvent對象,表示這個特定的事件。您可以通過這個事件對象得到與之相關聯的所有觸摸對象(allTouches),或者發生在特定的視圖或窗口上的觸摸對象子集。其中的某些觸摸對象表示自上次事件消息以來沒有發生變化,或雖然發生變化但處於不同階段的觸摸動作。

爲了處理給定階段的事件,響應者對象常常從傳入的集合參數中取得一或多個UITouch對象,然後考察這些對象的屬性或取得它們的位置(如果需要處理所有觸摸對象,可以向該NSSet對象發送anyObject消息)。UITouch類中有一個名爲locationInView:的重要方法,如果傳入self參數值,它會給出觸摸動作在響應者座標系統中的位置(假定該響應者是一個UIView對象,且傳入的視圖參數不爲nil)。另外,還有一個與之平行的方法,可以給出觸摸動作之前位置(previousLocationInView:)。UITouch實例的屬性還可以給出發生多少次觸碰(tapCount)、觸摸對象的創建或最後一次變化發生在什麼時間(timestamp)、以及觸摸處於什麼階段(phase)。

響應者類並不是必須實現上面列出的所有三個事件方法。舉例來說,如果它只對手指離開屏幕感興趣,則只需要實現touchesEnded:withEvent:方法就可以了。

在一個多點觸摸序列中,如果響應者在處理事件時創建了某些持久對象,則應該實現touchesCancelled:withEvent:方法,以便當系統取消該序列的時候對其進行清理。多點觸摸序列的取消常常發生在應用程序的事件處理遭到外部事件—比如電話呼入—破壞的時候。請注意,響應者對象同樣應該在收到多點觸摸序列的touchesEnded:withEvent:消息時清理之前創建的對象(“事件處理技巧”部分討論瞭如何確定一個序列中的最後一個touch-up事件)。

處理單個和多個觸碰手勢

iPhone應用程序中一個很常見的手勢是觸擊:即用戶用手指觸碰一個對象。響應者對象可以以一種方式響應單擊,而以另外一種方式響應雙擊,甚至可能以第三種方式響應三次觸擊。您可以通過考察UITouch對象的tapCount屬性聲明值來確定用戶在一個響應者對象上的觸擊次數,

取得這個值的最好地方是touchesBegan:withEvent:touchesEnded:withEvent:方法。在很多情況下,我們更傾向於後者,因爲它與用戶手指離開屏幕的階段相對應。在觸摸結束階段(UITouchPhaseEnded)考察觸擊的次數可以確定手指是真的觸擊,而不是其它動作,比如手指接觸屏幕後拖動的動作。

程序清單3-1展示瞭如何檢測某個視圖上是否發生雙擊。

程序清單3-1  檢測雙擊手勢

- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch       *touch = [touches anyObject];
 
    if ([touch tapCount] == 2) {
        CGPoint tapPoint = [theTouch locationInView:self];
        // Process a double-tap gesture
    }
}

當一個響應者對象希望以不同的方式響應單擊雙擊事件時,就會出現複雜的情況。舉例來說,單擊的結果可能是選定一個對象,而雙擊則可能是顯示一個編輯視圖,用於編輯被雙擊的對象。那麼,響應者對象如何知道一個單擊不是另一個雙擊的起始部分呢?我們接下來解釋響應者對象如何藉助上文剛剛描述的事件處理方法來處理這種情況:

  1. touchesEnded:withEvent:方法中,當觸擊次數爲一時,響應者對象就向自身發送一個performSelector:withObject:afterDelay:消息,其中的選擇器標識由響應者對象實現的、用於處理單擊手勢的方法;第二個參數是一個NSValueNSDictionary對象,用於保存相關的UITouch對象;時延參數則表示單擊和雙擊手勢之間的合理時間間隔。

    請注意:使用一個NSValue對象或字典來保存觸摸對象是因爲它們會保持傳入的對象。然而,您自己在進行事件處理時,不應該對UITouch對象進行保持。

  2. touchesBegan:withEvent:方法中,如果觸擊次數爲二,響應者對象會向自身發送一個cancelPreviousPerformRequestsWithTarget:消息,取消當前被掛起和延期執行的調用。如果觸碰次數不爲二,則在指定的延時之後,先前步驟中由選擇器標識的方法就會被調用,以處理單擊手勢。

  3. touchesEnded:withEvent:方法中,如果觸碰次數爲二,響應者會執行處理雙擊手勢的代碼。

檢測碰擦手勢

水平和垂直的碰擦(Swipe)是簡單的手勢類型,您可以簡單地在自己的代碼中進行跟蹤,並通過它們執行某些動作。爲了檢測碰擦手勢,您需要跟蹤用戶手指在期望的座標軸方向上的運動。碰擦手勢如何形成是由您自己來決定的,也就是說,您需要確定用戶手指移動的距離是否足夠長,移動的軌跡是否足夠直,還有移動的速度是否足夠快。您可以保存初始的觸碰位置,並將它和後續的touch-moved事件報告的位置進行比較,進而做出這些判斷。

程序清單3-2展示了一些基本的跟蹤方法,可以用於檢測某個視圖上發生的水平碰擦。在這個例子中,視圖將觸摸的初始位置存儲在名爲startTouchPosition的成員變量中。隨着用戶手指的移動,清單中的代碼將當前的觸摸位置和起始位置進行比較,確定是否爲碰擦手勢。如果觸摸在垂直方向上移動得太遠,就會被認爲不是碰擦手勢,並以不同的方式進行處理。但是,如果手指繼續在水平方向上移動,代碼就繼續將它作爲碰擦手勢來處理。一旦碰擦手勢在水平方向移動得足夠遠,以至於可以認爲是完整的手勢時,處理例程就會觸發相應的動作。檢測垂直方向上的碰擦手勢可以用類似的代碼,只是把x和y方向的計算互換一下就可以了。

程序清單3-2  在視圖中跟蹤碰擦手勢

#define HORIZ_SWIPE_DRAG_MIN  12
#define VERT_SWIPE_DRAG_MAX    4
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    startTouchPosition = [touch locationInView:self];
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint currentTouchPosition = [touch locationInView:self];
 
    // If the swipe tracks correctly.
    if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
        fabsf(startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
    {
        // It appears to be a swipe.
        if (startTouchPosition.x < currentTouchPosition.x)
            [self myProcessRightSwipe:touches withEvent:event];
        else
            [self myProcessLeftSwipe:touches withEvent:event];
    }
    else
    {
        // Process a non-swipe event.
    }
}

處理複雜的多點觸摸序列

觸擊和碰擦是簡單的手勢。如何處理更爲複雜的多點觸摸序列—實際上是解析應用程序特有的手勢—取決於應用程序希望完成的具體目標。您可以跟蹤所有階段的所有觸摸動作,記錄觸摸對象中發生變化的屬性變量,並正確地改變內部的狀態。

說明如何處理複雜的多點觸摸序列的最好方法是通過實例。程序清單3-3展示一個定製的UIView對象如何通過在屏幕上動畫移動“Welcome”標語牌來響應用戶手指的移動,以及如何通過改變歡迎標語的語言來響應用戶的雙擊手勢(例子中的代碼來自一個名爲MoveMe的示例工程,進一步考察該工程可以更好地理解事件處理的上下文)。

程序清單3-3  處理複雜的多點觸摸序列

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
    // Only move the placard view if the touch was in the placard view
    if ([touch view] != placardView) {
        // On double tap outside placard view, update placard's display string
        if ([touch tapCount] == 2) {
            [placardView setupNextDisplayString];
        }
        return;
    }
    // "Pulse" the placard view by scaling up then down
    // Use UIView's built-in animation
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5];
    CGAffineTransform transform = CGAffineTransformMakeScale(1.2, 1.2);
    placardView.transform = transform;
    [UIView commitAnimations];
 
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5];
    transform = CGAffineTransformMakeScale(1.1, 1.1);
    placardView.transform = transform;
    [UIView commitAnimations];
 
    // Move the placardView to under the touch
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.25];
    placardView.center = [self convertPoint:[touch locationInView:self] fromView:placardView];
    [UIView commitAnimations];
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
 
    // If the touch was in the placardView, move the placardView to its location
    if ([touch view] == placardView) {
        CGPoint location = [touch locationInView:self];
        location = [self convertPoint:location fromView:placardView];
        placardView.center = location;
        return;
    }
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
 
    // If the touch was in the placardView, bounce it back to the center
    if ([touch view] == placardView) {
        // Disable user interaction so subsequent touches don't interfere with animation
        self.userInteractionEnabled = NO;
        [self animatePlacardViewToCenter];
        return;
    }
}

請注意:對於通過描畫自身的外觀來響應事件的定製視圖,在事件處理方法中通常應該只是設置描畫狀態,而在drawRect:方法中執行所有的描畫操作。如果需要了解更多關於描畫視圖內容的方法,請參見“圖形和描畫”部分。

 

事件處理技巧

下面是一些事件處理技巧,您可以在自己的代碼中使用。

  • 跟蹤UITouch對象的變化

    在事件處理代碼中,您可以將觸摸狀態的相關位置保存下來,以便在必要時和變化之後的UITouch實例進行比較。作爲例子,假定您希望將每個觸摸對象的最後位置和其初始位置進行比較,則在touchesBegan:withEvent:方法中,您可以通過locationInView:方法得到每個觸摸對象的初始位置,並以UITouch對象的地址作爲鍵,將它們存儲在CFDictionaryRef封裝類型中;然後,在touchesEnded:withEvent:方法中,可以通過傳入UITouch對象的地址取得該對象的初始位置,並將它和當前位置進行比較(您應該使用CFDictionaryRef類型,而不是NSDictionary對象,因爲後者需要對其存儲的項目進行拷貝,而UITouch類並不採納NSCopying協議,該協議在對象拷貝過程中是必須的)。

  • 對子視圖或層上的觸摸動作進行觸碰測試

    定製視圖可以用UIViewhitTest:withEvent:方法或CALayerhitTest:方法來尋找接收觸摸事件的子視圖或層,進而正確地處理事件。下面的例子用於檢測定製視圖的層中的“Info” 圖像是否被觸碰。

    - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
        CGPoint location = [[touches anyObject] locationInView:self];
        CALayer *hitLayer = [[self layer] hitTest:[self convertPoint:location fromView:nil]];
     
        if (hitLayer == infoImage) {
            [self displayInfo];
        }
    }

    如果您有一個攜帶子視圖的定製視圖,就需要明確自己是希望在子視圖的級別上處理觸摸事件,還是在父視圖的級別上進行處理。如果子視圖沒有實現touchesBegan:withEvent:touchesEnded:withEvent:、或者touchesMoved:withEvent:方法,則這些消息就會沿着響應者鏈被傳播到父視圖。然而,由於多次觸碰和多點觸摸事件與發生這些動作所在的子視圖是互相關聯的,所以父視圖不會接收到這些事件。爲了保證能接收到所有的觸摸事件,父視圖必須重載hitTest:withEvent:方法,並在其中返回其本身,而不是它的子視圖。

  • 確定多點觸摸序列中最後一個手指何時離開

    當您希望知道一個多點觸摸序列中的最後一個手指何時從視圖離開時,可以將傳入的集合參數中包含的UITouch對象數量和UIEvent參數對象中與該視圖關聯的觸摸對象數量相比較。請看下面的例子:

    - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
        if ([touches count] == [[event touchesForView:self] count]) {
            // last finger has lifted....
        }
    }

 

運動事件

當用戶以特定方式移動設備,比如搖擺設備時,iPhone或者iPod touch會產生運動事件。運動事件源自設備加速計。系統會對加速計的數據進行計算,如果符合某種模式,就將它解釋爲手勢,然後創建一個代表該手勢的UIEvent對象,併發送給當前活動的應用程序進行處理。

請注意:在iPhone 3.0上,只有搖擺設備的動作會被解釋爲手勢,並形成運動事件。

運動事件比觸摸事件簡單得多。系統只是告訴應用程序動作何時開始及何時結束,而不包括在這個過程中發生的每個動作的時間。而且,觸摸事件中包含一個觸摸對象的集合及其相關的狀態,而運動事件中除了事件類型、子類型、和時間戳之外,沒有其它狀態。系統以這種方式來解析運動手勢,避免和方向變化事件造成衝突。

爲了處理運動事件,UIResponder的子類必須實現motionBegan:withEvent:motionEnded:withEvent:方法之一,或者同時實現這兩個方法。舉例來說,如果用戶希望賦以水平擺動和垂直襬動不同的意義,就可以在motionBegan:withEvent:方法中將當前加速計軸的值緩存起來,並將它們和motionEnded:withEvent:消息傳入的值相比較,然後根據不同的結果進行動作。響應者還應該實現motionCancelled:withEvent:方法,以便響應系統發出的運動取消的事件。有些時候,這些事件會告訴您整個動作根本不是一個正當的手勢。

應用程序及其鍵盤焦點窗口會將運動事件傳遞給窗口的第一響應者。如果第一響應者不能處理,事件就沿着響應者鏈進行傳遞,直到最終被處理或忽略,這和觸摸事件的處理相類似(詳細信息請參見“事件的傳遞”部分)。但是,擺動事件和觸摸事件有一個很大的不同,當用戶開始擺動設備時,系統就會通過motionBegan:withEvent:消息的方式向第一響應者發送一個運動事件,如果第一響應者不能處理,該事件就在響應者鏈中傳遞;如果擺動持續的時間小於1秒左右,系統就會向第一響應者發送motionEnded:withEvent:消息;但是,如果擺動時間持續更長,如果系統確定當前的動作不是擺動,則第一響應者會收到一個motionCancelled:withEvent:消息。

如果擺動事件沿着響應者鏈傳遞到窗口而沒有被處理,且UIApplicationapplicationSupportsShakeToEdit屬性被設置爲YES,則iPhone OS會顯示一個帶有撤消(Undo)和重做(Redo)的命令。缺省情況下,這個屬性的值爲NO

拷貝、剪切、和粘貼操作

在iPhone OS 3.0之後,用戶可以在一個應用程序上拷貝文本、圖像、或其它數據,然後粘貼到當前或其它應用程序的不同位置上。比如,您可以從某個電子郵件中拷貝一個地址,然後粘貼到Contacts程序的地址域中。目前,UIKit框架UITextViewUITextField、和UIWebView類中實現了拷貝-剪切-粘貼支持。如果您希望在自己的應用程序中得到這個行爲,可以使用這些類的對象,或者自行實現。

本文的下面部分將描述UIKit中用於拷貝、剪切、和粘貼操作的編程接口,並解釋其用法。

請注意:與拷貝和粘貼操作相關的使用指南,請參見iPhone人機界面指南文檔中的“支持拷貝和粘貼”部分。

UIKit中支持拷貝-粘貼操作的設施

UIKit框架提供幾個類和一個非正式協議,用於爲應用程序中的拷貝、剪切、和粘貼操作提供方法和機制。具體如下:

  • UIPasteboard類提供了粘貼板的接口。粘貼板是用於在一個應用程序內或不同應用程序間進行數據共享的受保護區域。該類提供了讀寫剪貼板上數據項目的方法。

  • UIMenuController類可以在選定的拷貝、剪切、和粘貼對象的上下方顯示一個編輯菜單。編輯菜單上的命令可以有拷貝、剪切、粘貼、選定、和全部選定。

  • UIResponder類聲明瞭canPerformAction:withSender:方法。響應者類可以實現這個方法,以根據當前的上下文顯示或移除編輯菜單上的命令。

  • UIResponderStandardEditActions非正式協議聲明瞭處理拷貝、剪切、粘貼、選定、和全部選定命令的接口。當用戶觸碰編輯菜單上的某個命令時,相應的UIResponderStandardEditActions方法就會被調用。

粘貼板的概念

粘貼板是同一應用程序內或不同應用程序間交換數據的標準化機制。粘貼板最常見的的用途是處理拷貝、剪貼、和粘貼操作:

  • 當用戶在一個應用程序中選定數據並選擇拷貝(或剪切)菜單命令時,被選擇的數據就會被放置在粘貼板上。

  • 當用戶選擇粘貼命令時(可以在同一或不同應用程序中),粘貼板上的數據就會被拷貝到當前應用程序上。

在iPhone OS中,粘貼板也用於支持查找(Find)操作。此外,還可以用於在不同應用程序間通過定製的URL類型傳輸數據(而不是通過拷貝、剪切、和粘貼命令,關於這個技巧的信息請參見“和其它應用程序間的通訊”部分。

無論是哪種操作,您通過粘貼板執行的基本任務是讀寫粘貼板數據。雖然這些任務在概念上很簡單,但是它們屏蔽了很多重要的細節。複雜的原因主要在於數據的表現方式可能有很多種,而這個複雜性又引入了效率的考慮。本文的下面部分將對這些以及其它的問題進行討論。

命名粘貼板

粘貼板可能是公共的,也可能是私有的。公共粘貼板被稱爲系統粘貼板;私有粘貼板則由應用程序自行創建,因此被稱爲應用程序粘貼板。粘貼板必須有唯一的名字。UIPasteboard定義了兩個系統粘貼板,每個都有自己的名字和用途:

典型情況下,您只需使用系統定義的粘貼板就夠了。但在必要時,您也可以通過pasteboardWithName:create:方法來創建自己的應用程序粘貼板。如果您調用pasteboardWithUniqueName方法,UIPasteboard會爲您提供一個具有唯一名稱的應用程序粘貼板。您可以通過其name屬性聲明來取得這個名稱。

粘貼板的持久保留

您可以將粘貼板標識爲持久保留,使其內容在當前使用的應用程序終止後繼續存在。不持久保留的粘貼板在其創建應用程序退出後就會被移除。系統粘貼板是持久保留的,而應用程序粘貼板在缺省情況下是不持久保留的。將其應用程序粘貼板的persistent屬性設置爲YES可以使其持久保留。當持久粘貼板的擁有者程序被用戶卸載時,其自身也會被移除。

粘貼板的擁有者和數據項

最後將數據放到粘貼板的對象被稱爲該粘貼板的擁有者。放到粘貼板上的每一片數據都稱爲一個粘貼板數據項。粘貼板可以保有一個或多個數據項。應用程序可以放入或取得期望數量的數據項。舉例來說,假定用戶在視圖中選擇的內容包含一些文本和一個圖像,粘貼板允許您將文本和圖像作爲不同的數據項進行拷貝。從粘貼板讀取多個數據項的應用程序可以選擇只讀取被支持的數據項(比如只是文本,而不支持圖像)。

重要提示:當一個應用程序將數據寫入粘貼板時,即使只是單一的數據項,該數據也會取代粘貼板的當前內容。雖然您可能使用UIPasteboardaddItems:方法來添加項目,但是該寫入方法並不會將那些項目加入到粘貼板當前內容之後。

 

數據的表示和UTI

粘貼板操作經常在不同的應用程序間執行。系統並不要求應用程序瞭解對方的信息,包括對方可以處理的數據種類。爲了最大化潛在的數據分享能力,粘貼板可以保留同一個數據項的多種表示。例如,一個富文本編輯器可以提供被拷貝數據的HTML、PDF、和純文本表示。粘貼板上的一個數據項包括應用程序可爲該數據提供的所有表示。

粘貼板數據項的每種表示通常都有一個唯一類型標識符(Unique Type Identifier,縮寫爲UTI)。UTI簡單定義爲一個唯一標識特定數據類型的字符串。UTI提供了一個標識數據類型的常用手段。如果您希望支持一個定製的數據類型,就必須爲其創建一個唯一的標識符。爲此,您可以用反向DNS表示法來定義類型標識字符串,以確保其唯一性。例如,您可以用com.myCompany.myApp.myType來表示一個定製的類型標識。更多有關UTI的信息請參見統一類型標識符概述

作爲例子,假定一個應用程序支持富文本和圖像的選擇,它可能希望將富文本和Unicode版本的選定文本,以及選定圖像的不同表示放到粘貼板上。在這樣的場景下,每個數據項的每種表示都和它自己的數據一起保存,如圖3-3所示。

圖3-3  粘貼板及其表示

Pasteboard items and representations

一般情況下,爲了最大化潛在的共享可能性,粘貼板數據項應該包括儘可能多的表示。

粘貼板的讀取程序必須找到最適合自身能力(如果有的話)的數據類型。通常情況下,這意味着選擇內涵最豐富的可用類型。舉例來說,一個文本編輯器可能爲被拷貝的數據提供HTML(富文本)和純文本表示,支持富文本的應用程序應該選擇HTML表示,而只支持純文本的應用程序則應該選擇純文本的表示。

變化記數

變化記數是每個粘貼板都有的變量,它隨着每次粘貼板內容的變化而遞增—特別是發生增加、修改、或移除數據項的時候。應用程序可以通過考察變化記數(通過changeCount屬性)來確定粘貼板的當前數據是否和最後一次取得的數據相同。每次變化記數遞增時,粘貼板都會向對此感興趣的觀察者發送通告。

選擇和菜單管理

在拷貝或剪切視圖中的某些內容之前,必須首先選擇“某些內容”。它可能是一些文本、一個圖像、一個URL、一種顏色、或者其它類型的數據,包括定製對象。爲了在定製視圖中實現拷貝-和-粘貼行爲,您必須自行管理該視圖中對象的選擇。如果用戶通過特定的觸摸手勢(比如雙擊)來選擇視圖中的對象,您就必須處理該事件,即在程序內部記錄該選擇(同時取消之前的選擇),可能還要在視圖中指示新的選擇。如果用戶可以在視圖中選擇多個對象,然後進行拷貝-剪切-粘貼操作,您就必須實現多選的行爲。

請注意:觸摸事件及其處理技巧在“觸摸事件”部分進行討論。

當應用程序確定用戶請求了編輯菜單時—可能就是一個選擇的動作—您應該執行下面的步驟來顯示菜單:

  1. 調用UIMenuControllersharedMenuController類方法來取得全局的,即菜單控制器實例。

  2. 計算選定內容的邊界,並用得到的邊界矩形調用setTargetRect:inView:方法。系統會根據選定內容與屏幕頂部和底部的距離,將編輯菜單顯示在該矩形的上方或下方。

  3. 調用setMenuVisible:animated:方法(兩個參數都傳入YES),在選定內容的上方或下方動畫顯示編輯菜單。

程序清單3-4演示瞭如何在touchesEnded:withEvent:方法的實現中顯示編輯菜單(注意,例子中省略了處理選擇的代碼)。在這個代碼片段中,定製視圖還向自己發送一個becomeFirstResponder消息,確保自己在隨後的拷貝、剪切、和粘貼操作中是第一響應者。

程序清單3-4  顯示編輯菜單

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *theTouch = [touches anyObject];
 
    if ([theTouch tapCount] == 2  && [self becomeFirstResponder]) {
 
        // selection management code goes here...
 
        // bring up editing menu.
        UIMenuController *theMenu = [UIMenuController sharedMenuController];
        CGRect selectionRect = CGRectMake(currentSelection.x, currentSelection.y, SIDE, SIDE);
        [theMenu setTargetRect:selectionRect inView:self];
        [theMenu setMenuVisible:YES animated:YES];
 
    }
}

初始的菜單包含所有的命令,因此第一響應者提供了相應的UIResponderStandardEditActions方法的實現(copy:paste:等)。但是在菜單被顯示之前,系統會向第一響應者發送一個canPerformAction:withSender:消息。在很多情況下,第一響應者就是定製視圖的本身。在該方法的實現中,響應者考察給定的命令(由第一個參數傳入的選擇器表示)是否適合當前的上下文。舉例來說,如果該選擇器是paste:,而粘貼板上沒有該視圖可以處理的數據,則響應者應該返回NO,以便禁止粘貼命令。如果第一響應者沒有實現canPerformAction:withSender:方法,或者沒有處理給定的命令,該消息就會進入響應者鏈。

程序清單3-5展示了canPerformAction:withSender:方法的一個實現。該實現首先尋找和copy:copy:、及paste:選擇器相匹配的消息,並根據當前選擇的上下文激活或禁用拷貝、剪切、和粘貼菜單命令。對於粘貼命令,還考慮了粘貼板的內容。

程序清單3-5  有條件地激活菜單命令

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    BOOL retValue = NO;
    ColorTile *theTile = [self colorTileForOrigin:currentSelection];
 
    if (action == @selector(paste:) )
        retValue = (theTile == nil) &&
             [[UIPasteboard generalPasteboard] containsPasteboardTypes:
             [NSArray arrayWithObject:ColorTileUTI]];
    else if ( action == @selector(cut:) || action == @selector(copy:) )
        retValue = (theTile != nil);
    else
        retValue = [super canPerformAction:action withSender:sender];
    return retValue;
}

請注意,這個方法的最後一個else子句調用了超類的實現,使超類有機會處理子類忽略的命令。

還要注意,操作一個菜單命令可能會改變其它菜單命令的上下文。比如,當用戶選擇視圖中的所有對象時,拷貝和剪切命令就應該被包含在菜單中。在這種情況下,雖然菜單仍然可見,但是響應者可以調用菜單控制器的update方法,使第一響應者的canPerformAction:withSender:再次被調用。

拷貝和剪切選定的內容

當用戶觸碰編輯菜單上的拷貝或剪切命令時,系統會分別調用響應者對象的copy:cut:方法。通常情況下,第一響應者—也就是您的定製視圖—會實現這些方法,但如果沒有實現的話,該消息會按正常的方式進入響應者鏈。請注意,UIResponderStandardEditActions非正式協議聲明瞭這些方法。

請注意:由於UIResponderStandardEditActions是非正式協議,應用程序中的任何類都可以實現它的方法。但是,爲了使命令可以按缺省的方式在響應者鏈上傳遞,實現這些方法的類應該繼承自UIResponder類,且應該被安裝到響應者鏈中。

copy:cut:消息的響應代碼中,您需要把和選定內容相對應的對象或數據以儘可能多的表示形式寫入到粘貼板上。這個操作涉及到如下這些步驟(假定只有一個的粘貼板數據項):

  1. 標識或取得和選定內容相對應的對象或二進制數據。

    二進制數據必須封裝在NSData對象中。其它可以寫入到粘貼板的對象必須是屬性列表對象—也就是說,必須是下面這些類的對象:NSStringNSArrayNSDictionaryNSDateNSNumber、或者NSURL(有關屬性列表對象的更多信息,請參見屬性列表編程指南)。

  2. 可能的話,請爲對象或數據生成一或多個其它的表示。

    舉例來說,在之前提到的爲選定圖像創建UIImage對象的步驟中,您可以通過UIImageJPEGRepresentationUIImagePNGRepresentation函數將圖像轉換爲不同的表示。

  3. 取得粘貼板對象。

    在很多情況下,使用通用粘貼板就可以了。您可以通過generalPasteboard類方法來取得該對象。

  4. 爲寫入到粘貼板數據項的每個數據表示分配一個合適的UTI。

    這個主題的討論請參見“粘貼板的概念”部分。

  5. 將每種表示類型的數據寫入到第一個粘貼板數據項中:

  6. 對於剪切(cut:方法)命令,需要從應用程序的數據模型中移除選定內容所代表的對象,並更新視圖。

程序清單3-6展示了copy:cut:方法的一個實現。cut:方法調用了copy:方法,然後從視圖和數據模型中移除選定的對象。注意,copy:方法對定製對象進行歸檔,目的是得到一個NSData對象,以便作爲參數傳遞給粘貼板的setData:forPasteboardType:方法。

程序清單3-6  拷貝和剪切操作

- (void)copy:(id)sender {
    UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
    ColorTile *theTile = [self colorTileForOrigin:currentSelection];
    if (theTile) {
        NSData *tileData = [NSKeyedArchiver archivedDataWithRootObject:theTile];
        if (tileData)
            [gpBoard setData:tileData forPasteboardType:ColorTileUTI];
    }
}
 
- (void)cut:(id)sender {
     [self copy:sender];
     ColorTile *theTile = [self colorTileForOrigin:currentSelection];
 
     if (theTile) {
         CGPoint tilePoint = theTile.tileOrigin;
         [tiles removeObject:theTile];
          CGRect tileRect = [self rectFromOrigin:tilePoint inset:TILE_INSET];
         [self setNeedsDisplayInRect:tileRect];
     }
}

粘貼選定內容

當用戶觸碰編輯菜單上的粘貼命令時,系統會調用響應者對象的paste:方法。通常情況下,第一響應者—也就是您的定製視圖—會實現這些方法,但如果沒有實現的話,該消息會按正常的方式進入響應者鏈。paste:方法在UIResponderStandardEditActions非正式協議中聲明。

paste: 消息的響應代碼中,您可以從粘貼板中讀取應用程序支持的表示,然後將被粘貼對象加入到應用程序的數據模型中,並將新對象顯示在用戶指定的視圖位置上。這個操作涉及到如下這些步驟(假定只有單一的粘貼板數據項):

  1. 取得粘貼板對象。

    在很多情況下,使用通用粘貼板就可以了,您可以通過generalPasteboard類方法來取得該對象。

  2. 確認第一個粘貼板數據項是否包含應用程序可以處理的表示,這可以通過調用containsPasteboardTypes:方法,或者調用pasteboardTypes方法並考察其返回的類型數組來實現。

    請注意,您在canPerformAction:withSender:方法的實現中應該已經執行過這個步驟。

  3. 如果粘貼板的第一個數據項包含應用程序可以處理的數據,則可以調用下面的方法來讀取:

  4. 將對象加入到應用程序的數據模型中。

  5. 將對象的表示顯示在用戶界面中用戶指定的位置上。

程序清單3-7paste:方法的一個實現實例,該方法執行與cut:copy:方法相反的操作。示例中的視圖首先確認粘貼板是否包含自身支持的定製表示數據,如果是的話,就讀取該數據並將它加入到應用程序的數據模型中,然後將視圖的一部分—當前選定區域—標識爲需要重畫。

程序清單3-7  將粘貼板的數據粘貼到選定位置上

- (void)paste:(id)sender {
     UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
     NSArray *pbType = [NSArray arrayWithObject:ColorTileUTI];
     ColorTile *theTile = [self colorTileForOrigin:currentSelection];
     if (theTile == nil && [gpBoard containsPasteboardTypes:pbType]) {
 
        NSData *tileData = [gpBoard dataForPasteboardType:ColorTileUTI];
        ColorTile *theTile = (ColorTile *)[NSKeyedUnarchiver unarchiveObjectWithData:tileData];
         if (theTile) {
             theTile.tileOrigin = self.currentSelection;
             [tiles addObject:theTile];
             CGRect tileRect = [self rectFromOrigin:currentSelection inset:TILE_INSET];
             [self setNeedsDisplayInRect:tileRect];
         }
     }
}

消除編輯菜單

在您實現的cut:copy:、或paste:命令返回後,編輯菜單會被自動隱藏。通過下面的代碼使它保持可見:

[UIMenuController setMenuController].menuVisible = YES;

系統可能在任何時候隱藏編輯菜單,比如當顯示警告信息或用戶觸碰屏幕其它區域時,編輯菜單就會被隱藏。如果您有某些狀態或屏幕顯示需要依賴於編輯菜單是否顯示的話,就應該偵聽UIMenuControllerWillHideMenuNotification通告,並執行恰當的動作。

圖形和描畫

高質量的圖形是應用程序用戶界面的重要組成部分。提供高質量的圖形不僅會使應用程序具有好的的外觀,還會使它看起來象是系統的自然擴展。iPhone OS爲創建高質量的圖形提供兩種路徑:即通過OpenGL進行渲染,或者通過Quartz、Core Animation、和UIKit進行渲染。

OpenGL框架主要適用於遊戲或要求高幀率的應用程序開發。它是一組基於C語言的接口,用於在桌面電腦上創建2D和3D內容。iPhone OS通過OpenGL ES框架來支持OpenGL描畫,該框架同時支持OpenGL ES 2.0和OpenGL ES v1.1。OpenGL ES是特別爲嵌入式硬件系統設計的,和桌面版本的OpenGL有很多不同。

對於希望採用更爲面向對象的方法進行描畫的開發者,iPhone OS提供了Quartz、Core Animation、還有UIKit中的圖形支持。Quartz是主要的描畫接口,支持基於路徑的描畫、抗鋸齒渲染、漸變填充模式、圖像、顏色、座標空間變換、以及PDF文檔的創建、顯示、和分析。UIKit爲Quartz的圖像和顏色操作提供了Objective-C的封裝。Core Animation爲很多UIKit的視圖屬性聲明的動畫效果提供底層支持,也可以用於實現定製的動畫。

本章將爲iPhone應用程序的描畫過程提供一個概覽,同時介紹描畫技術的一些具體描畫技巧。本章還爲如何優化iPhone OS平臺的描畫代碼提供一些指導原則和小貼士。

UIKit的圖形系統

在iPhone OS上,所有的描畫—無論是否採用OpenGL、Quartz、UIKit、或者Core Animation—都發生在UIView對象的區域內。視圖定義描畫發生的屏幕區域。如果您使用系統提供的視圖,描畫工作會自動得到處理;然而,如果您定義自己的定製視圖,則必須自行提供描畫代碼。對於使用OpenGL進行描畫的應用程序,一旦建立了渲染表面,就必須使用OpenGL指定的描畫模型。

對於Quartz、Core Animation、和UIKit,您需要使用本文下面部分描述的概念。

視圖描畫週期

UIView對象的基本描畫模型涉及到如何按需更新視圖的內容。通過收集您發出的更新請求、並在最適合的時機將它們發送給您的描畫代碼,UIView類使內容更新過程變得更爲簡單和高效。

任何時候,當視圖的一部分需要重畫時,UIView對象內置的描畫代碼就會調用其drawRect:方法,並向它傳入一個包含需要重畫的視圖區域的矩形。您需要在定製視圖子類中重載這個方法,並在這個方法中描畫視圖的內容。在首次描畫視圖時,UIView傳遞給drawRect:方法的矩形包含視圖的全部可見區域。但在隨後的調用中,該矩形只代表實際需要被描畫的部分。觸發視圖更新的動作有如下幾種:

  • 對遮擋您的視圖的其它視圖進行移動或刪除操作。

  • 將視圖的hidden屬性聲明設置爲NO,使其從隱藏狀態變爲可見。

  • 將視圖滾出屏幕,然後再重新回到屏幕上。

  • 顯式調用視圖的setNeedsDisplay或者setNeedsDisplayInRect:方法。

在調用drawRect:方法之後,視圖會將自己標誌爲已更新,然後等待新的更新動作觸發下一個更新週期。如果您的視圖顯示的是靜態內容,則只需要在視圖的可見性發生變化時進行響應就可以了,這種變化可能由滾動或其它視圖是否被顯示引起的。然而,如果您需要週期性地更新視圖內容,就必須確定什麼時候調用setNeedsDisplaysetNeedsDisplayInRect:方法來觸發更新。舉例來說,如果您需要每秒數次地更新內容,則可能要使用一個定時器。在響應用戶交互或生成新的視圖內容時,也可能需要更新視圖。

座標和座標變換

“視圖座標系統”部分描述的那樣,窗口或視圖的座標原點位於左上角,座標的值向下向右遞增。當您編寫描畫代碼時,需要通過這個座標系統來指定描畫內容中點的位置。

如果您需要改變缺省的座標系統,可以通過修改當前的轉換矩陣來實現。當前轉換矩陣(CTM)是一個數學矩陣,用於將視圖座標系統上的點映射到設備的屏幕上。在視圖的drawRect:方法首次被調用時,就需要建立CTM,使座標系統的原點和視圖的原點互相匹配,且將座標軸的正向分別處理爲向下和向右。然而,您可以通過加入縮放、旋轉、和轉換因子來改變CTM,從而改變缺省座標系統相對於潛在視圖或窗口的尺寸、方向、和位置。

修改CTM是在視圖內容描畫的標準技術,因爲它需要的工作比其它方法少得多。如果您希望在當前描畫系統中座標爲(20, 20)的位置上畫出一個10 x 10的方形,可以首先創建一個路徑,將它的起始點移動到座標爲(20, 20)的位置上,然後再畫出組成方形的幾條線。然而,如果您在之後希望將方形移動到座標爲(10, 10)的位置上,就必須用新的起始點重新創建路徑。事實上,每次改變原點,您都必須重新創建路徑。創建路徑是開銷相對較大的操作,相比之下,創建一個起始點爲(0, 0)的方形,然後通過修改CTM來匹配目標描畫原點的開銷就少一些。

在Core Graphics框架中,有兩種修改CTM的方法。您可以通過CGContext參考定義的CTM操控函數來直接修改CTM,也可以創建一個CGAffineTransform結構,將您希望的轉換應用到該結構上,然後將它連結到CTM上。使用仿射變換可以將各種變換組合在一起,然後一次性地應用到CTM上。您也可以通過修改和恢復仿射變換來調整點、尺寸、和矩形的值。有關仿射變換的更多信息,請參見Quartz 2D編程指南CGAffineTransform參考

圖形上下文

在調用您提供的drawRect:方法之前,視圖對象會自動配置其描畫環境,使您的代碼可以立即進行描畫。作爲這些配置的一部分,UIView對象會爲當前描畫環境創建一個圖形上下文(對應於CGContextRef封裝類型)。該圖形上下文包含描畫系統執行後續描畫命令所需要的信息,定義了各種基本的描畫屬性,比如描畫使用的顏色、裁剪區域、線的寬度及風格信息、字體信息、合成選項、以及幾個其它信息。

當您希望在視圖之外的其它地方進行描畫時,可以創建定製的圖形上下文對象。在Quartz中,當您希望捕捉一系列描畫命令並將它們用於創建圖像或PDF文件時,就需要這樣做。您可以用CGBitmapContextCreateCGPDFContextCreate函數來創建上下文。有了上下文對象之後,您可以將它傳遞給創建內容時需要調用的描畫函數。

您創建的定製圖形上下文的座標系統和iPhone OS使用的本地座標系統是不同的。與後者的座標原點位於左上角不同的是,前者的座標原點位於左下角,其座標值向上向右遞增。您在描畫命令中指定的座標必須對此加以考慮,否則,結果圖像或PDF文件在渲染時就可能會發生錯誤。

重要提示:由於在位圖或PDF上下文中進行描畫時使用的是左下原點,所以在將描畫結果渲染到視圖上的時候,必須對座標系統進行補償。換句話說,如果您創建一個圖像,並調用CGContextDrawImage函數來進行描畫,則該圖像在缺省情況下是上下顛倒的。爲了糾正這個問題,您必須將CTM的y軸進行翻轉(即將該值乘以-1),使其原點從左下角移動到視圖的左上角。

如果使用UIImage對象來包裝您所創建的CGImageRef類型,則不需要修改CTM。UIImage對象會自動對CGImageRef 類型的座標系統進行翻轉補償。

 

有關圖形上下文、如何修改圖形狀態信息、以及如何用圖形上下文來創建定製內容的更多信息,請參見Quartz 2D編程指南。如果需要與圖形上下文結合使用的函數列表,則請參見CGContext參考CGBitmapContext參考、以及CGPDFContext參考

點和像素的不同

Quartz描畫系統使用基於向量的描畫模型,這不同於基於柵格的描畫模型。在柵格描畫模型中,描畫命令操作的是每個獨立的像素,而Quartz的描畫命令則是通過固定比例的描畫空間來指定,這個描畫空間就是所謂的用戶座標空間。然後,由iPhone OS將該描畫空間的座標映射爲設備的實際像素。這個模型的優勢在於,使用向量命令描畫的圖形在通過仿射變換放大或縮小之後仍然顯示良好。

爲了維持基於向量的描畫系統固有的精度,Quratz描畫系統使用浮點數(而不是定點數)作爲座標值。使用浮點類型的座標值可以非常精確地指定描畫內容的位置。在大多數情況下,您不必擔心這些值最終如何映射到設備的屏幕。

用戶座標空間是您發出的所有描畫命令的工作環境。該空間的單位由點來表示。設備座標空間指的是設備內在的座標空間,由像素來表示。缺省情況下,用戶座標空間上的一個點等於設備座標空間的一個像素,這意味着一個點等於1/160英寸。然而,您不應該假定這個比例總是1:1。

顏色和顏色空間

iPhone OS支持Quartz中具有的所有顏色空間,但是,大多數應用程序應該只需要RGB顏色空間,因爲iPhone OS是爲嵌入式硬件設計的,而且只在一個屏幕上顯示,在這種場合下,RGB顏色空間是最合適的。

UIColor對象提供了一些便利方法,用於通過RGB、HSB、和灰度值指定顏色值。以這種方式創建顏色不需要指定顏色空間,UIColor對象會自動爲您指定。

您也可以使用Core Graphics框架中的CGContextSetRGBStrokeColorCGContextSetRGBFillColor函數來創建和設置顏色。雖然Core Graphics框架支持用其它的顏色空間來創建顏色,還支持創建定製的顏色空間,但是我們不推薦在描畫代碼中使用那些顏色。您的描畫代碼應該總是使用RGB顏色。

支持的圖像格式

表4-1列出了iPhone OS直接支持的圖像格式。在這些格式中,我們優先推薦PNG格式。

表4-1  支持的圖像格式

格式

文件擴展名

可移植網絡圖像格式(PNG)

.png

標記圖像文件格式(TIFF)

.tiff.tif

聯合影像專家組格式(JPEG)

.jpeg.jpg

圖形交換格式(GIF)

.gif

視窗位圖格式(DIB)

.bmp.BMPf

視窗圖標格式

.ico

視窗光標

.cur

XWindow位圖

.xbm

描畫貼士

本文的下面部分將爲您提供一些貼士,討論如何在編寫高質量描畫代碼的同時確保應用程序外觀對最終用戶具有吸引力。

確定何時使用定製的描畫代碼

根據您創建的應用程序類型,不使用或使用很少的定製代碼進行描畫是可能的。雖然沉浸式的應用程序通常廣泛使用定製的描畫代碼,但是工具型和效率型的應用程序則可以使用標準的視圖和控件來顯示內容。

定製描畫代碼的使用應該限制在當顯示在屏幕上的內容需要動態改變的場合。比如,用於跟蹤用戶描畫命令的應用程序需要使用定製描畫代碼;還比如,遊戲程序也需要經常更新屏幕,以反映遊戲環境的改變。在那些情況下,您需要選擇合適的描畫技術,以及創建定製的視圖類來正確處理事件和更新屏幕。

另一方面,如果應用程序中大量的用戶界面是固定的,則可以事先將那些界面渲染到一或多個圖像文件中,然後在運行時通過UIImageView對象顯示出來。您可以根據自己的需要,將圖像視圖和其它內容組合在一起。比如,您可以用UILabel對象來顯示需要配置的文本,用按鍵或其它控件來進行交互。

提高描畫的性能

在任何平臺上,描畫的開銷都比較昂貴,對描畫代碼進行優化一直都是開發過程的重要步驟。表4-2列舉了幾個貼士,用於確保您的描畫代碼得到儘可能的優化。除了這些貼士,您還應該用現有的性能工具對代碼進行測試,消除描畫熱點和多餘的描畫操作。

表4-2  提高描畫性能的貼士

Tip

Action

使描畫工作最小化

在每個更新週期中,您應該只更新視圖中真正發生變化的部分。如果您使用UIView的drawRect:方法來進行描畫,則要通過傳給該方法的更新矩形來限制描畫的範圍。對於基於OpenGL的描畫,您必須自行跟蹤更新區域。

儘可能將視圖標識爲不透明

合成不透明的視圖所需要的開銷比合成部分透明的視圖要少得多。一個不透明的視圖必須不包含任何透明的內容,且視圖的opaque屬性必須設置爲YES

刪除不透明的PNG文件中的alpha通道

如果一個PNG圖像的每個像素都是不透明的,則將其alpha通道刪除可以避免對包含該圖像的圖層進行融合操作,從而很大程度上簡化了該圖像的合成,提高描畫的性能。

在滾動過程中重用表格單元和視圖

應該避免在滾動過程種創建新的視圖。創建新視圖的開銷會減少用於更新屏幕的時間,因而導致滾動不平滑。

避免在滾動過程中清除原先的內容

缺省情況下,在調用drawRect:方法對視圖的某個區域進行更新之前,UIKit會清除該區域對應的上下文緩衝區。如果您對視圖的滾動事件進行響應,則在滾動過程中反覆清除緩衝區的開銷是很大的。爲了禁止這種行爲,可以將clearsContextBeforeDrawing屬性設置爲NO

在描畫過程中儘可能不改變圖形狀態

改變圖形狀態需要窗口服務器的參與。如果您要描畫的內容使用類似的圖形狀態,則儘可能將這些內容一起描畫,以減少需要改變的狀態。

保持圖像的質量

爲用戶界面提供高品質的圖像應該是設計工作中的重點之一。圖像是一種合理而有效的顯示覆雜圖形的方法,任何合適的地方都可以使用。在爲應用程序創建圖像的時候,請記住下面的原則:

  • 使用PNG格式的圖像。PNG格式可以提供高品質的圖像內容,是iPhone OS系統上推薦的圖像格式。另外,iPhone OS對PNG圖像的描畫路徑是經過優化的,通常比其它格式具有更高的效率。

  • 創建大小合適的圖像,避免在顯示時調整尺寸。如果您計劃使用特定尺寸的圖像,則在創建圖像資源時,務必使用相同的尺寸。不要創建一個大的圖像,然後再縮小,因爲縮放需要額外的CPU開銷,而且需要進行插值。如果您需要以不同的尺寸顯示圖像,則請包含多個版本的圖像,並選擇與目標尺寸相對接近的圖像來進行縮放。

用Quartz和UIKit進行描畫

Quartz是iPhone OS的窗口服務器和描畫技術的一般叫法。Core Graphics框架是Quartz的核心,也是內容描畫的基本接口。該框架提供的數據類型和函數用於操作如下對象:

  • 圖形上下文

  • 路徑

  • 圖像和位圖

  • 透明層

  • 顏色、圖案顏色、和顏色空間

  • 漸變和陰影

  • 字體

  • PDF內容

UIKit在Quartz基本特性的基礎上提供了一組專門的類,用於與圖形相關的操作。UIKit的圖形類並不是爲了向您提供一個全面的描畫工具箱—因爲這樣的工具在Core Graphics框架中已經有了,而是爲了向其它UIKit類提供描畫支持。UIKit包括下面的類和函數:

  • UIImage, 一個不可變類,用於圖像顯示。

  • UIColor, 爲設備顏色提供基本的支持。

  • UIFont, 爲需要字體的類提供字體信息。

  • UIScreen, 提供屏幕的基本信息。

  • 生成UIImage對象的JPEG或PNG表示的函數。

  • 描畫矩形和對描畫區域進行裁剪的函數。

  • 改變和獲取當前圖形上下文的函數

有關UIKit包含的類和方法的信息,請參見UIKit框架參考,有關組成Core Graphics框架的封裝類型和函數,請參見Core Graphics框架參考

配置圖形上下文

在您的drawRect:方法被調用時,視圖對象的內置描畫代碼已經爲您創建並配置好了一個缺省圖形上下文。您可以通過調用UIGraphicsGetCurrentContext函數來取得當前上下文的指針,該函數返回一個類型爲CGContextRef的引用,您可以將它傳給Core Graphics函數,以修改當前的圖形狀態。表4-3列出了負責設置各種圖形狀態的一些主要函數,如果需要完整的函數列表,請參見CGContext參考。該表還列出了UIKit中和這些函數對應的組件,如果有的話。

表 4-3  修改圖形狀態的Core Graphics函數

圖形狀態

Core Graphics函數

UIKit對應組件

當前轉換矩陣(CTM)

CGContextRotateCTM

CGContextScaleCTM

CGContextTranslateCTM

CGContextConcatCTM

裁剪區域

CGContextClipToRect

線: 寬度,線間鏈接,線端點,破折號,斜角限制

CGContextSetLineWidth

CGContextSetLineJoin

CGContextSetLineCap

CGContextSetLineDash

CGContextSetMiterLimit

曲線擬合的精度(平滑度)

CGContextSetFlatness

抗鋸齒設置

CGContextSetAllowsAntialiasing

顏色:填充和筆劃設置

CGContextSetRGBFillColor

CGContextSetRGBStrokeColor

UIColor

Alpha值(透明度)

CGContextSetAlpha

渲染意圖

CGContextSetRenderingIntent

顏色空間:填充和筆劃設置

CGContextSetFillColorSpace

CGContextSetStrokeColorSpace

文本:字體,字體尺寸,字符間隔,文本描畫模式

CGContextSetFont

CGContextSetFontSize

CGContextSetCharacterSpacing

UIFont

混合模式

CGContextSetBlendMode

您可以爲UIImage類和各種描畫函數指定混合模式

圖形上下文中包含一個保存過的圖形狀態堆棧。在Quartz創建圖形上下文時,該堆棧是空的。CGContextSaveGState函數的作用是將當前圖形狀態推入堆棧。之後,您對圖形狀態所做的修改會影響隨後的描畫操作,但不影響存儲在堆棧中的拷貝。在修改完成後,您可以通過CGContextRestoreGState函數把堆棧頂部的狀態彈出,返回到之前的圖形狀態。這種推入和彈出的方式是回到之前圖形狀態的快速方法,避免逐個撤消所有的狀態修改;這也是將某些狀態(比如裁剪路徑)恢復到原有設置的唯一方式。

有關圖形上下文及如何用它來配置描畫環境的一般信息,請參見Quartz 2D編程指南圖形上下文部分。

創建和描畫圖像

iPhone OS同時支持通過UIKit和Core Graphics框架裝載和顯示圖像。到底選擇哪些類和函數描畫圖像取決於具體的應用場合。但是,我們推薦您儘可能使用UIKit來表示圖像。表4-4列舉了一些使用場景及處理這些場景的推薦方法。

表 4-4  圖像使用場景

場景

推薦用法

將圖像作爲視圖的內容

使用UIImageView類裝載和顯示圖像。這種方法假定視圖的內容就是一個圖像,但您仍然可以在圖像視圖上面放置其它視圖,用於描畫其它控件或內容。

將圖像作爲部分視圖的裝飾

UIImage類裝載和描畫圖像。

將某些位圖數據保存到圖像對象中

使用UIGraphicsBeginImageContext函數創建一個新的、基於圖像的圖形上下文。在這之後,您就可以將圖像內容描畫在上面,然後用UIGraphicsGetImageFromCurrentImageContext函數生成一個圖像(如果需要的話,您甚至可以繼續描畫並生成其它的圖像)。在圖像創建完成後,可以用UIGraphicsEndImageContext函數來關閉圖形上下文。

如果您更喜歡使用Core Graphics,則可以用CGBitmapContextCreate函數創建一個位圖的圖形上下文,並在上面描畫您的圖像內容。畫完之後,用CGBitmapContextCreateImage函數把位圖上下文中的內容創建爲一個CGImageRef類型的圖像。您可以直接描畫Core Graphics圖像,或者用它來初始化一個UIImage

將圖像保存爲JPEG或PNG文件

基於原始的圖像數據創建一個UIImage對象。通過UIImageJPEGRepresentationUIImagePNGRepresentation函數取得一個NSData對象,並使用該對象的方法將數據保存爲文件。

下面的例子將展示如何從應用程序的程序包中裝載一個圖像。在該圖像裝載完成後,您可以將它用於初始化UIImageView對象、將它保存到磁盤、或者在視圖的drawRect:方法中進行顯式描畫。

NSString* imagePath = [[NSBundle mainBundle] pathForResource:@"myImage" ofType:@"png"];
UIImage* myImageObj = [[UIImage alloc] initWithContentsOfFile:imagePath];

在視圖的drawRect:方法中,您可以使用UIImage類提供的任何描畫方法。您可以指定希望在視圖的什麼位置描畫圖像,從而避免在描畫之前進行位置的轉換。假定您將之前裝載的圖像存儲在一個名爲anImage的成員變量中,下面的代碼會將該圖像畫在視圖的(10, 10) 座標位置上:

- (void)drawRect:(CGRect)rect
{
    // Draw the image
    [anImage drawAtPoint:CGPointMake(10, 10)];
}

重要提示:如果您使用CGContextDrawImage函數來直接描畫位圖,則在缺省情況下,圖像數據會上下倒置,因爲Quartz圖像假定座標系統的原點在左下角,且座標軸的正向是向上和向右。雖然您可以在描畫之前對其進行轉換,但是將Quartz圖像包裝爲一個UIImage對象是更簡單的方法,這樣可以自動補償座標空間的差別。有關如何用Core Graphics創建和描畫圖像的更多信息,請參見Quartz 2D編程指南

 

創建和描畫路徑

路徑用於描述由一序列線和Bézier曲線構成的2D幾何形狀。UIKit中的UIRectFrameUIRectFill函數(以及其它函數)的功能是在視圖中描畫象矩形這樣的簡單路徑。Core Graphics中也有一些用於創建簡單路徑(比如矩形和橢圓形)的便利函數。對於更爲複雜的路徑,必須用Core Graphics框架提供的函數自行創建。

在創建路徑時,需要首先通過CGContextBeginPath函數配置一個接收路徑命令的圖形上下文。調用該函數之後,就可以使用與路徑相關的函數來設置路徑的起始點,描畫直線和曲線,加入矩形和橢圓形等等。路徑的幾何形狀指定完成後,就可以直接進行描畫,或者將其引用存儲在CGPathRefCGMutablePathRef數據類型中,以備後用。

在視圖上描畫路徑時,可以描畫輪廓,也可以進行填充,或者同時進行這兩種操作。路徑輪廓可以用像CGContextStrokePath這樣的函數來畫,即用當前的筆劃顏色畫出以路徑爲中心位置的線。路徑的填充則可以用CGContextFillPath函數來實現,它的功能是用當前的填充顏色或樣式填充路徑線段包圍的區域。

有關如何描畫路徑的更多信息,包括如何爲複雜路徑元素指定點的信息,請參見Quartz 2D編程指南路徑部分。有關路徑創建函數的信息,則請參見CGContext參考CGPath參考

創建樣式、漸變、和陰影

Core Graphics框架還包含一些用於創建樣式、漸變、和陰影類型的函數。基於這些類型,您可以創建複雜的顏色,並用它們來填充自己創建的路徑。樣式是從重複出現的圖像或內容創建而來的,漸變和陰影則是不同顏色之間平滑過渡的方式。

有關創建樣式、漸變、和陰影的詳細信息,在Quartz 2D編程指南中進行討論。

用OpenGL ES進行描畫

開放圖形庫(Open Graphics Library,即OpenGL)是一個跨平臺的、基於C語言的接口,用於在桌面系統中創建2D和3D內容。遊戲或需要以高幀率進行描畫的開發者通常需要使用這個接口。您可以用OpenGL函數來指定圖元結構,比如點、線、多邊形和紋理,以及增強這些結構外觀的特殊效果。您調用的函數會將圖形命令發送給底層的硬件,然後由硬件進行渲染。由於大多數渲染工作是由硬件來完成,所以OpenGL的描畫速度通常很快。

OpenGL的嵌入式系統版本是OpenGL的精簡版本,是專門爲移動設備設計的,可以充分利用現代圖形硬件的優勢。如果您希望爲基於iPhone OS的設備—也就是iPhone或iPod Touch—創建OpenGL內容,就要使用OpenGL ES。iPhone OS系統提供的OpenGL ES框架(OpenGLES.framework)同時支持OpenGL ES v1.1和OpenGL ES v2.0規範。

有關iPhone OS系統上的OpenGL ES的更多信息,請參見iPhone OpenGL ES編程指南.

應用Core Animation的效果

Core Animation是一個Objective-C語言的框架,其目的是爲快速創建實時動畫提供基礎設施。Core Animation本身並不是一個描畫技術,因爲它並不提供創建形狀、圖像、或其它內容的基本例程;相反,它是一種操作和顯示由其它技術創建的內容的技術。

在iPhone OS上,大多數程序都會以某種形式受益於Core Animation技術。動畫可以將當前正在發生的事情呈現給用戶。比如,在用戶使用Settings程序時,屏幕會根據用戶是向預置的更深層次移動還是返回根結點而滑入或滑出視圖。這種反饋是很重要的,可以爲用戶提供上下文的信息。動畫還可以增強應用程序的視覺效果。

大多數情況下,您通過很少的工作就可以得到Core Animation的好處。舉例來說,您可以對UIView類的幾個屬性聲明(其中包括視圖的邊框、中心、顏色、和透明度等)進行配置,使得當它們的值發生變化時,可以觸發動畫效果。您需要通過少量的工作讓UIKit知道您希望執行哪些動畫,但動畫的創建和運行都是自動的。有關如何觸發內置視圖動畫的更多信息,請參見“視圖動畫”部分。

如果您要超越基本的動畫效果,就必須直接和Core Animation的類及方法進行更多的交互。本文的下面部分將進一步提供有關Core Animation的信息,向您展示如何用它提供的類和方法創建iPhone OS上的典型動畫。更多有關Core Animation及其用法的信息,請參見Core Animation編程指南

關於層

Core Animation的關鍵技術是層對象。層是一種輕量級的對象,在本質上類似於視圖,但實際上是模型對象,負責封裝顯示內容的幾何屬性、顯示時機、和視覺屬性變量。內容本身可以通過如下三種方式來提供:

  • 您可以將一個CGImageRef類型的數據賦值給層對象的contents屬性變量

  • 您可以爲層分配一個委託,讓它負責描畫工作。

  • 您可以從CALayer派生出子類,並對其顯示方法進行重載。

當您操作層對象的屬性時,您真正操作的是模型級別的數據,該數據決定了與之關聯的內容應該如何被顯示,而實際的渲染則由您的代碼之外的模塊來處理,系統對這個過程進行了大量的優化,確保渲染工作能快速完成。您需要做的只是設置層的內容和配置動畫屬性,然後讓Core Animation接管剩下的工作。

更多有關層及如何使用層的信息,請參見Core Animation編程指南

關於動畫

對於具有動畫效果的層,Core Animation使用獨立的動畫對象來控制動畫的時機和行爲。CAAnimation類及其子類實現了不同類型的動畫行爲,供您在代碼中使用。您可以創建簡單的動畫,將某個屬性變量從一個值變爲另一個值;也可以創建複雜的關鍵幀動畫,通過您自己提供的值和時間函數來跟蹤動畫。

Core Animation還可以將多個動畫組合爲一個單獨的單元,稱爲事務。CATransaction對象負責將一組動畫組合成一個單元來管理,您也可以用它提供的方法來設置動畫的持續時間。

如果您需要如何創建定製動畫的實例,請參見動畫類型和時機的編程指南

文本和Web

iPhone OS文本系統的設計者考慮了移動設備用戶的基本需求,將文本系統設計爲電子郵件和SMS程序中常用的單行和多行文本輸入控件。文本系統支持Unicode,且包含幾個不同的輸入法,方便顯示和讀取不同語言的文本。

關於文本和Web的支持

iPhone OS的文本系統提供了大量的功能,同時又非常簡單易用。UIKit框架中包含幾個高級類,負責管理文本的顯示和輸入。該框架還含有一個更爲高級的類,用於顯示HTML和基於JavaScript的內容。

本文的下面部分將描述iPhone OS對文本和web內容的基本支持。如果您需要這裏列舉的各個類的更多信息,請參見UIKit框架參考

文本視圖

UIKit框架提供三個顯示文本內容的基本類:

雖然標籤和文本編輯框通常用於顯示相對少量的文本,但實際上這些類可以顯示任意數量的文本。然而,基於iPhone OS的設備的屏幕比較小,爲了使顯示在屏幕上的文本便於閱讀,這些類不支持像Mac OS X這樣的桌面操作系統上常見的高級格式功能。另一方面,考慮到可能的需要,這三個類仍然支持指定字體信息,包括字體的尺寸和風格選項,只是指定的字體會應用到對象中顯示的所有文本。

圖5-1顯示了這些文本類在屏幕上的顯示實例。這些例子來自UICatalog示例程序,該程序演示了UIKit框架中的很多視圖和控件。左圖顯示的是幾個不同風格的文本輸入框,右圖則顯示一個文本視圖。灰色背景中顯示的說明文字所在的視圖是一些UILabel對象,它們被嵌入到負責顯示各種視圖的表格單元中。左圖的屏幕底部還有一個UILabel對象,顯示內容爲 “Left View”。

圖5-1  UICatalog應用程序的文本類

Text classes in the UICatalog application

在使用可編輯的文本視圖時,您必須提供一個委託對象,負責管理編輯會話。文本視圖會向委託對象發送幾個不同的通告,讓它知道編輯何時開始,何時結束,並使它有機會重載某些編輯動作。舉例來說,委託可以決定當前文本是否包含有效的值,還可以在需要的時候防止編輯會話被終止。在編輯過程最終結束的時候,您可以通過委託取得編輯結果,更新應用程序的數據模型。

由於各種文本視圖的用法有輕微的不同,所以它們的委託方法也有所不同。爲UITextField類提供支持的委託需要實現UITextFieldDelegate協議定義的方法。類似地,爲UITextView類提供支持的委託需要實現UITextViewDelegate協議定義的方法。對於上述兩種情形,系統並沒有要求您一定要實現協議中的任何方法,但是如果沒有實現必要的方法,文本輸入框就沒有什麼用處了。有關這兩個協議的更多信息,請參見UITextFieldDelegate協議參考UITextViewDelegate協議參考

Web視圖

UIWebView類使您可以將一個微型web瀏覽器集成到應用程序的用戶界面上。UIWebView類充分使用了iPhone OS上的web技術,同樣的這些技術也用於實現iPhone OS上的Safari、實現對HTML、CSS、和JavaScript內容的全面支持。UIWebView還支持很多用戶在Safari中已經熟悉了的手勢,比如通過雙擊和雙指捏夾(pinch)的手勢來放大和縮小頁面,還有通過手指拖動來滾動頁面。

除了顯示內容,您還可以用web視圖對象來顯示web表單,收集用戶輸入。和UIKit的其它文本類相似,如果您在web頁面的表單中有可編輯的文本框,則輕觸該文本框就會彈出鍵盤,用戶可以通過鍵盤輸入文本。這是web瀏覽整體體驗的一部分,web視圖會自行管理鍵盤的顯示和消除。

圖5-2顯示了一個UIWebView對象的例子,它來自UICatalog示例程序,該程序演示了UIKit框架中的很多視圖和控件。這個例子只是顯示HTML內容,如果您希望用戶可以象使用web瀏覽器那樣在網頁之間進行漫遊,需要加入一些控件。比如,圖中的web視圖只是佔用URL文本框下面的空間,而不包含文本框的本身。

圖5-2  web視圖

A web view

web視圖通過其關聯的委託對象提供有關網頁何時被裝載、及裝載過程是否發生錯誤的信息。web委託是指實現一個或多個UIWebViewDelegate協議方法的對象。您可以通過實現委託方法來響應裝載錯誤或處理一些與裝載有關的其它任務。更多有關UIWebViewDelegate協議方法的信息請參見UIWebViewDelegate協議參考

鍵盤和輸入法

每當用戶觸擊一個可以接受文本輸入的對象時,該對象就會請求系統顯示一個合適的鍵盤。根據用戶程序的需要和偏好的語言,系統可以顯示幾種不同的鍵盤。您的應用程序雖然不能控制用戶的偏好語言(因此也不能控制鍵盤的輸入法),但可以控制鍵盤的使用屬性,比如特殊鍵的配置及其行爲。

您可以直接通過應用程序中的文本對象來配置鍵盤的屬性。UITextFieldUITextView類都遵循UITextInputTraits協議,該協議定義了一些配置鍵盤的屬性。在程序或Interface Builder的查看器窗口中設置這些屬性就可以使系統顯示指定類型的鍵盤。

請注意:雖然UIWebView類並不直接支持UITextInputTraits協議,但您還是可以配置文本輸入元素的一些鍵盤屬性。特別值得一提的是,您可以在輸入元素的定義中包含autocorrectautocapitalization屬性,通過這些屬性來指定鍵盤的行爲,如下面的例子所示:

<input type="text" size="30" autocorrect="off" autocapitalization="on">
您不能在輸入元素中指定鍵盤的類型。web視圖顯示的是缺省的鍵盤,但包含一些額外的控制,可以進行表單元素之間漫遊。

 

缺省的鍵盤配置是爲一般的文本輸入設計的。圖5-3顯示了缺省的和其它的幾個鍵盤配置。缺省鍵盤顯示的是一個字母鍵盤,用戶可以將它切換爲數字和標點符號鍵盤。大多數其它鍵盤在都提供與缺省鍵盤類似的功能,同時又提供一些適合於特定任務的其它按鍵。但是,電話和數字鍵盤的佈局顯著不同,它們是特別爲數字輸入設計的。

圖5-3  幾個不同的鍵盤類型

Several different keyboard types

爲了實現不同的語言偏好,iPhone OS還支持與不同語言相對應的輸入法和鍵盤佈局, 圖5-4顯示了部分輸入法和佈局。輸入法和鍵盤佈局是由用戶語言偏好設置決定的。

圖5-4  幾個不同的鍵盤和輸入法

Several different keyboards and input methods

管理鍵盤

雖然很多UIKit對象在響應用戶交互時會自動顯示鍵盤,但您的程序仍然需要配置和管理鍵盤。本文的下面部分將描述應用程序在鍵盤管理方面應該承擔的責任。

接收鍵盤通告

當鍵盤被顯示或隱藏的時候,iPhone OS會向所有經過註冊的觀察者對象發出如下通告

當鍵盤首次出現或者消失,以及鍵盤的所有者或應用程序的方向發生變化的任何時候,系統都會發出鍵盤通告。在上述的各種情況下,系統只發送與具體場景相關的的消息集合。舉例來說,如果鍵盤的所有者發生變化,系統只向當前的擁有者發送UIKeyboardWillHideNotification消息,但不發送UIKeyboardDidHideNotification消息,因爲這個變化不會導致鍵盤最終被隱藏。UIKeyboardWillHideNotification消息只是簡單地通知鍵盤當前的所有者即將失去鍵盤焦點。而改變鍵盤的方向則會使系統發出上述的兩種消息,因爲每個方向的鍵盤是不同的,在顯示新的鍵盤之前,必須先隱藏原來的鍵盤。

每個鍵盤通告都包含鍵盤在屏幕上的位置和尺寸。您應該使用通告中的信息來確定鍵盤的尺寸和位置,而不是假定鍵盤具有某個特定的尺寸或處於某個特定的位置。鍵盤在使用不同輸入法時並一定總是一樣的,在不同版本的iPhone OS上也可能會發生變化。另外,即使對於特定的某種語言和某個系統版本,鍵盤的尺寸也會因爲應用程序方向的不同而不同。作爲例子,請看圖5-5顯示了URL鍵盤在肖像模式和景觀模式下的相對尺寸。使用鍵盤通告中的信息可以確保得到正確的尺寸和位置信息。

圖5-5  在肖像模式和景觀模式下的相對鍵盤尺寸

Relative keyboard sizes in portrait and landscape modes

請注意:info字典中的UIKeyboardBoundsUserInfoKey鍵包含的矩形只能用於取得尺寸信息,不要將該矩形的原點(它的值總是爲{0.0, 0.0})用於矩形計算。由於鍵盤是以動畫的形式出現在它的位置上的,其實際的邊界尺寸會隨着時間的不同而不同,因此,info字典中有UIKeyboardCenterBeginUserInfoKeyUIKeyboardCenterEndUserInfoKey兩個鍵,用於保存鍵盤的起始和終止的位置,您可以根據這些位置計算出鍵盤的原點。

使用鍵盤通告的一個原因是爲了重新定位被鍵盤遮掩的內容。有關如何進行重新定位的信息,請參見“移動鍵盤下面的內容”部分。

顯示鍵盤

當用戶觸擊一個視圖時,系統就會自動將該視圖作爲第一響應者。而當這種場景發生在包含可編輯文本的視圖時,該視圖就會啓動一個文本編輯會話。如果當前鍵盤不可見,該視圖會在編輯會話剛開始時請求系統顯示鍵盤。如果鍵盤已經顯示在屏幕上了,第一響應者的改變會導致來自鍵盤的文本輸入被重定向到用戶剛剛觸擊的視圖上。

鍵盤是在視圖變爲第一響應者時自動被顯示的,因此,您通常不需要爲了顯示它而做什麼工作。但是,您可以通過調用視圖對象的becomeFirstResponder方法來爲可編輯的文本視圖顯示鍵盤。調用這個方法可以使目標視圖成爲第一響應者,並開始編輯過程,其效果和用戶觸擊該視圖是一樣的。

如果您的應用程序在一個屏幕上管理幾個基於文本的視圖,則需要跟蹤當前哪個視圖是第一響應者,以便在需要的時候取消鍵盤的顯示。

取消鍵盤

雖然鍵盤通常是自動顯示的,但它並不自動取消。相反,您的應用程序需要在恰當的時機取消鍵盤。通常情況下,您在響應用戶動作的時候進行這樣的操作,比如當用戶觸擊鍵盤上的Return或Done按鍵、或者觸擊應用程序界面上的其它按鍵時。根據鍵盤配置的不同,您可能需要在用戶界面上加入額外的控件來取消鍵盤。

您可以調用作爲當前第一響應者的文本視圖的resignFirstResponder方法來取消鍵盤。當文本視圖失去第一響應者的狀態時,就會結束其當前的編輯會話,將這個變化通知它的委託對象,並取消鍵盤。換句話說,如果您有一個名爲myTextField的變量,指向一個UITextField對象,假定該對象是當前的第一響應者,則可以簡單地通過下面的代碼來取消鍵盤:

[myTextField resignFirstResponder];

從這個點之後的所有操作都由文本對象自動處理。

移動鍵盤下面的內容

當系統收到顯示鍵盤的請求時,就從屏幕的底部滑出鍵盤,並將它放在應用程序內容的上方。由於鍵盤位於您的內容的上面,所以有可能遮掩住用戶希望編輯的文本對象。如果這種情況發生,就必須對內容進行調整,使目標對象保持可見。

需要做的調整通常包括暫時調整一或多個視圖的尺寸和位置,從而使文本對象可見。管理帶有鍵盤的文本對象的最簡單方法是將它們嵌入到一個UIScrollView(或其子類,如UITableView)對象。當鍵盤被顯示出來時,您需要做的只是調整滾動視圖的尺寸,並將目標文本對象滾動到合適的位置。爲此,在UIKeyboardDidShowNotification通告的處理代碼中需要進行如下操作:

  1. 取得鍵盤的尺寸。

  2. 將滾動視圖的高度減去鍵盤的高度。

  3. 將目標文本框滾動到視圖中。

圖5-6演示了一個簡單的應用程序如何處理上述的幾個步驟。該程序將幾個文本輸入框嵌入到UIScrollView對象中,當鍵盤出現時,通告處理代碼首先調整滾動視圖的尺寸,然後用UIScrollView類的scrollRectToVisible:animated:方法將被觸擊的文本框滾動到視圖中。

圖5-6  調整內容的位置,使其適應鍵盤

Adjusting content to accommodate the keyboard

請注意:在配置滾動視圖時,請務必爲所有的內容視圖配置恰當的自動尺寸調整規則。在之前的圖中,文本框實際上是一個UIView對象的子視圖,該UIView對象又是UIScrollView對象的子視圖。如果該UIView對象的UIViewAutoresizingFlexibleWidthUIViewAutoresizingFlexibleHeight選項被設置了,則改變滾動視圖的邊框尺寸會同時改變它的邊框,因而可能導致不可預料的結果。禁用這些選項可以確保該視圖保持尺寸不變,並正確滾動。

程序清單5-1顯示瞭如何註冊接收鍵盤通告和如何實現相應的處理器方法。這段代碼是由負責滾動視圖管理的視圖控制器實現的,其中scrollView變量是一個指向滾動視圖對象的插座變量。每個處理器方法都從通告的info對象取得鍵盤的尺寸,並根據這個尺寸調整滾動視圖的高度。此外,keyboardWasShown:方法的任務是將當前活動的文本框矩形滾入視圖,該文本框對象存儲在一個定製變量中(在本例子中名爲activeField),該變量是視圖控制器的一個成員變量,在textFieldDidBeginEditing:委託方法中進行賦值,委託方法本身的代碼顯示在程序清單5-2中(在這個例子中,視圖控制器同時也充當所有文本輸入框的委託)。

程序清單5-1  處理鍵盤通告

// Call this method somewhere in your view controller setup code.
- (void)registerForKeyboardNotifications
{
    [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(keyboardWasShown:)
            name:UIKeyboardDidShowNotification object:nil];
 
    [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(keyboardWasHidden:)
            name:UIKeyboardDidHideNotification object:nil];
}
 
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
    if (keyboardShown)
        return;
 
    NSDictionary* info = [aNotification userInfo];
 
    // Get the size of the keyboard.
    NSValue* aValue = [info objectForKey:UIKeyboardBoundsUserInfoKey];
    CGSize keyboardSize = [aValue CGRectValue].size;
 
    // Resize the scroll view (which is the root view of the window)
    CGRect viewFrame = [scrollView frame];
    viewFrame.size.height -= keyboardSize.height;
    scrollView.frame = viewFrame;
 
    // Scroll the active text field into view.
    CGRect textFieldRect = [activeField frame];
    [scrollView scrollRectToVisible:textFieldRect animated:YES];
 
    keyboardShown = YES;
}
 
 
// Called when the UIKeyboardDidHideNotification is sent
- (void)keyboardWasHidden:(NSNotification*)aNotification
{
    NSDictionary* info = [aNotification userInfo];
 
    // Get the size of the keyboard.
    NSValue* aValue = [info objectForKey:UIKeyboardBoundsUserInfoKey];
    CGSize keyboardSize = [aValue CGRectValue].size;
 
    // Reset the height of the scroll view to its original value
    CGRect viewFrame = [scrollView frame];
    viewFrame.size.height += keyboardSize.height;
    scrollView.frame = viewFrame;
 
    keyboardShown = NO;
}

上面程序清單中的keyboardShown變量是一個布爾值,用於跟蹤鍵盤是否可見。如果您的用戶界面有多個文本輸入框,則用戶可能觸擊其中的任意一個進行編輯。發生這種情況時,雖然鍵盤並不消失,但是每次開始編輯新的文本框時,系統都會產生UIKeyboardDidShowNotification通告。您可以通過跟蹤鍵盤是否確實被隱藏來避免多次減少滾動視圖的尺寸。

程序清單5-2顯示了一些額外的代碼,視圖控制器用這些代碼來設置和清理之前例子中的activeField變量。在初始化時,界面中的每個文本框都將視圖控制器設置爲自己的委託。因此,當文本編輯框被激活的時候,這些方法就會被調用。更多關於文本框及其委託通告的信息,請參見UITextField類參考

程序清單5-2  跟蹤活動文本框的方法

- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    activeField = textField;
}
 
- (void)textFieldDidEndEditing:(UITextField *)textField
{
    activeField = nil;
}

描畫文本

除了顯示和編輯文本的UIKit類之外,iPhone OS還包含幾個直接在屏幕上描畫文本的方法。描畫簡單字符串的最簡單有效的方法是使用NSString類的UIKit擴展,該擴展包含一些在屏幕上描畫字符串的方法,並且可以描畫時使用多種屬性。還有一些方法,可以在真正描畫之前計算渲染字符串所需要的尺寸,這些方法有助於更加精確佈局應用程序的內容。

重要提示:由於性能上的考慮,您應該儘可能避免直接描畫文本。對於靜態文本,通過一或多個UILabel對象進行描畫比使用定製描畫例程要高效得多。類似地,UITextField類也支持不同的風格,這些風格使您更加易於將可編輯的文本區域集成到您的內容中。

 

當您需要在界面上描畫定製文本字符串時,請使用NSString方法。UIKit包含一些對基本NSString類的擴展,用於在視圖中描畫字符串。這些方法使您可以精確調整文本的位置,以及將文本和視圖內容進行融合;這個類的方法還可以根據指定的字體和風格屬性計算文本的包圍矩形。更多信息請參見NSString UIKit擴展參考

如果您需要對描畫過程中用到的字體有更多的控制,還可以使用Core Graphics框架中的函數來進行描畫。Core Graphics框架提供的方法可以對字形和文本進行精確描畫和定位。有關這些函數及其用法的更多信息,請參見Quartz 2D編程指南Core Graphics框架參考

在Web視圖中顯示內容

如果您的用戶界面包含UIWebView對象,就可以顯示本地或網絡上的內容。對於本地的內容,您可以動態創建,也可以使用文件,然後調用loadData:MIMEType:textEncodingName:baseURL:loadHTMLString:baseURL:方法;如果要從網絡加載,則需要創建一個NSURLRequest對象,然後傳遞給web視圖對象的loadRequest:方法。

在發起一個基於網絡的請求後,如果由於某種原因必須釋放web視圖,則必須在釋放之前取消待處理的請求。爲此,您可以調用web視圖的stopLoading方法。通常情況下,您可以在web視圖的視圖控制器的viewWillDisappear:方法中執行這些代碼。如果需要確定一個請求是否處於等待狀態,可以通過web視圖的loading屬性來判斷。

文件和網絡

運行在iPhone OS系統上的應用程序可以通過各種Core OS和Core Services框架來訪問本地的文件系統和網絡。讀寫本地文件系統的能力使您可以保存用戶數據和應用程序狀態,以備後用;而訪問網絡的能力則使您可以和網絡服務器進行交流,進而實現遠程操作的執行和數據的收發。

文件和數據管理

iPhone OS系統上的文件和用戶的媒體數據及個人文件共享閃存上的空間。出於安全的目的,您的應用程序被放在其自己的目錄下,並且只能對該目錄進行讀寫。本章的下面部分將描述應用程序本地文件系統的結構及幾個讀寫文件的技術。

常用目錄

出於安全的目的,應用程序只能將自己的數據和偏好設置寫入到幾個特定的位置上。當應用程序被安裝到設備上時,系統會爲其創建一個家目錄。表6-1列出了應用程序家目錄下的一些重要子目錄,您的程序可能需要對其進行訪問。表中還描述了每個目錄的設計目的和訪問限制,以及iTunes是否對該目錄下的內容進行備份。有關備份和恢復過程的更多信息,請參見“備份和恢復” 部分;有關應用程序家目錄本身的信息,則請參見 “應用程序沙箱”部分。

表 6-1  iPhone應用程序的目錄

目錄

描述

<Application_Home>/AppName.app

這是程序包目錄,包含應用程序的本身。由於應用程序必須經過簽名,所以您在運行時不能對這個目錄中的內容進行修改,否則可能會使應用程序無法啓動。

在iPhone OS 2.1及更高版本的系統,iTunes不對這個目錄的內容進行備份。但是,iTunes會對在App Store上購買的應用程序進行一次初始的同步。

<Application_Home>/Documents/

您應該將所有的應用程序數據文件寫入到這個目錄下。這個目錄用於存儲用戶數據或其它應該定期備份的信息。有關如何取得這個目錄路徑的信息,請參見“獲取應用程序目錄的路徑”部分。

iTunes會備份這個目錄的內容。

<Application_Home>/Library/Preferences

這個目錄包含應用程序的偏好設置文件。您不應該直接創建偏好設置文件,而是應該使用NSUserDefaults類或CFPreferences API來取得和設置應用程序的偏好,詳情請參見“添加Settings程序包”部分。

iTunes會備份這個目錄的內容。

<Application_Home>/Library/Caches

這個目錄用於存放應用程序專用的支持文件,保存應用程序再次啓動過程中需要的信息。您的應用程序通常需要負責添加和刪除這些文件,但在對設備進行完全恢復的過程中,iTunes會刪除這些文件,因此,您應該能夠在必要時重新創建。您可以使用“獲取應用程序目錄的路徑” 部分描述的接口來獲取該目錄的路徑,並對其進行訪問。

在iPhone OS 2.2及更高版本,iTunes不對這個目錄的內容進行備份。

<Application_Home>/tmp/

這個目錄用於存放臨時文件,保存應用程序再次啓動過程中不需要的信息。當您的應用程序不再需要這些臨時文件時,應該將其從這個目錄中刪除(系統也可能在應用程序不運行的時候清理留在這個目錄下的文件)。有關如何獲得這個目錄路徑的信息,請參見“獲取應用程序目錄的路徑”部分。

在iPhone OS 2.1及更高版本,iTunes不對這個目錄的內容進行備份。

備份和恢復

您不需要在應用程序中爲備份和恢復操作做任何準備。在iPhone OS 2.2及更高版本的系統中,當設備被連接到計算機並完成同步時,iTunes會對除了下面這些目錄之外的所有文件進行增量式的備份:

  • <Application_Home>/AppName.app

  • <Application_Home>/Library/Caches

  • <Application_Home>/tmp

雖然iTunes確實對應用程序的程序包本身進行備份,但並不是在每次同步時都進行這樣的操作。通過設備上的App Store購買的應用程序在下一次設備和iTunes同步時進行備份。而在之後的同步操作中,應用程序並不進行備份,除非應用程序包本身發生了變化(比如由於應用程序被更新了)。

爲了避免同步過程花費太長時間,您應該有選擇地往應用程序家目錄中存放文件。<Application_Home>/Documents目錄應該用於存放用戶數據文件或不容易在應用程序中重新創建的文件。存儲臨時數據的文件應該放在Application Home/tmp目錄,而且應該在不需要的時候將其刪除。如果您的應用程序需要創建用於下次啓動的數據文件,則應該將那些文件放到Application Home/Library/Caches目錄下。

請注意:如果您的應用程序需要創建數據量大或頻繁變化的文件,則應該考慮將它們存儲在Application Home/Library/Caches目錄下,而不是<Application_Home>/Documents目錄。備份大數據文件會使備份過程顯著變慢,備份頻繁變化(因此必須頻繁備份)的文件也同樣如此。將這些文件放到Caches目錄下可以避免每次同步都對其進行備份(在iPhone OS 2.2及更高版本)。

有關如何在應用程序中使用目錄的更多信息,請參見表6-1

在應用程序更新過程中被保存的文件

更新應用程序就是將用戶下載的新版應用程序代替之前的版本。在這個過程中,iTunes會將更新過的應用程序安裝到新的應用程序目錄下,並在刪除老版本之前,將用戶數據文件轉移到新的應用程序目錄下。在更新的過程中,iTunes保證如下目錄中的文件會得以保留:

  • <Application_Home>/Documents

  • <Application_Home>/Library/Preferences

雖然其它用戶目錄下的文件也可能被轉移,但是您不應該假定更新之後該文件還仍然存在。

Keychain數據

keychain是一個安全、經過加密保護的容器,用於保存密碼和其它祕密信息。應用程序的keychain數據存儲在應用程序沙箱之外。如果應用程序被卸載,則該數據會自動被刪除。當用戶通過iTunes備份應用程序數據時,keychain數據也會被備份。然而,keychain數據只能被恢復到之前做備份的設備上。應用程序的更新並不影響其keychain數據。

有關iPhone OS keychain的更多信息,請參見Keychain服務編程指南文檔中的“Keychain服務的概念”部分。

獲取應用程序目錄的路徑

系統在各個級別上都提供了用於獲取應用程序沙箱目錄路徑的編程方法。然而,取得這些路徑的推薦方式還是使用Cocoa編程接口。NSHomeDirectory函數(在Foundation框架中)負責返回頂級家目錄的路徑—也就是包含應用程序、DocumentsLibrary、和tmp目錄的路徑。除了這個函數,您還可以用NSSearchPathForDirectoriesInDomainsNSTemporaryDirectory函數來取得DocumentsCaches、和tmp目錄的準確路徑。

NSHomeDirectoryNSTemporaryDirectory函數都通過NSString對象返回正確格式的路徑。您可以通過NSString類提供的與路徑相關的方法來修改路徑信息或創建新的路徑字符串。舉例來說,在取得臨時的目錄路徑之後,您可以附加一個文件名,並用結果字符串在臨時目錄下創建給定名稱的文件。

請注意:如果您使用帶有ANSI C編程接口的框架—包括那些接受路徑參數的接口—請記住NSString對象和其在Core Foundation框架中的等價類型之間是“免費橋接”的。這意味着您可以將一個NSString對象(比如上述某個函數的返回結果)強制類型轉換爲一個CFStringRef類型,如下面的例子所示:

CFStringRef homeDir = (CFStringRef)NSHomeDirectory();
有關免費橋接的更多信息,請參見 Carbon-Cocoa集成指南文檔。

Foundation框架中的NSSearchPathForDirectoriesInDomains函數用於取得幾個應用程序相關目錄的全路徑。在iPhone OS上使用這個函數時,第一個參數指定正確的搜索路徑常量,第二個參數則使用NSUserDomainMask常量。表6-2列出了大多數常用的常量及其返回的目錄。

表6-2  常用的搜索路徑常量

常量

目錄

NSDocumentDirectory

<Application_Home>/Documents

NSCachesDirectory

<Application_Home>/Library/Caches

NSApplicationSupportDirectory

<Application_Home>/Library/Application Support

由於NSSearchPathForDirectoriesInDomains函數最初是爲Mac OS X設計的,而Mac OS X上可能存在多個這樣的目錄,所以它的返回值是一個路徑數組,而不是單一的路徑。在iPhone OS上,結果數組中應該只包含一個給定目錄的路徑。程序清單6-1顯示了這個函數的典型用法。

程序清單6-1 取得指向應用程序Documents目錄的文件系統路徑

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];

在調用NSSearchPathForDirectoriesInDomains函數時,您可以使用NSUserDomainMask之外的其它域掩碼參數,或者使用表6-2之外的其它目錄常量,但是應用程序不能向其返回的目錄寫入數據。舉例來說,如果您指定NSApplicationDirectory作爲目錄參數,同時指定NSSystemDomainMask作爲域掩碼參數,則可以返回(設備上的)/Applications路徑,但是,您的應用程序不能往該位置寫入任何文件。

另外一個需要記住的考慮是,不同平臺的目錄位置是不一樣的。NSSearchPathForDirectoriesInDomainsNSHomeDirectoryNSTemporaryDirectory、和其它類似函數的返回路徑取決於應用程序運行在設備還是仿真器上。作爲例子,程序清單6-1上顯示的函數調用在設備上返回的路徑(documentsDirectory)大致如下:

/var/mobile/Applications/30B51836-D2DD-43AA-BCB4-9D4DADFED6A2/Documents

但是,它在仿真器上返回的路徑則具有如下的形式:

/Volumes/Stuff/Users/johnDoe/Library/Application Support/iPhone Simulator/User/Applications/118086A0-FAAF-4CD4-9A0F-CD5E8D287270/Documents

在讀寫用戶偏好設置時,請使用NSUserDefaults類或CFPreferences API。這些接口使您免於構造Library/Preferences/目錄路徑和直接讀寫偏好文件。有關使用這些接口的更多信息,請參見“添加Settings程序包”部分。

如果應用程序的程序包中包含聲音、圖像、或其它資源,則應該使用NSBundle類或CFBundleRef封裝類型來裝載那些資源。程序包知道應用程序內部資源應該在什麼位置上,此外,它還知道用戶的語言偏好,能夠自動選擇本地化的資源。有關程序包的更多信息,請參見“應用程序的程序包”部分。

文件數據的讀寫

iPhone OS提供瞭如下幾種讀、寫、和管理文件的方法:

  • Foundation框架:

  • Core OS調用:

    • 諸如fopenfread、和fwrite這些調用可以用於對文件進行順序或隨機讀寫。

    • mmapmunmap調用是將大文件載入內存並訪問其內容的有效方法。

請注意:上面的Core OS調用列表只是列舉一些較爲常用的例子。更完全的可用函數列表請參見iPhone OS手冊的第三部分中的函數列表。

本章的下面部分將描述如何使用一些高級技術來進行文件的讀寫。有關Foundation框架中與文件相關類的更多信息,請參見Foundation框架參考

屬性列表數據的讀寫

屬性列表是一種數據表示形式,用於封裝幾種Foundation(及 Core Foundation)的數據類型,包括字典、數組字符串、日期、二進制數據、數值及布爾值。屬性列表通常用於存儲結構化的配置數據。舉例來說,每個Cocoa和iPhone應用程序中都有一個Info.plist文件,它就是用於存儲應用程序本身配置信息的屬性列表。您自己也可以用屬性列表來存儲其它信息,比如應用程序退出時的狀態等。

在代碼中,屬性列表的構造通常從構造一個字典或數組、並將它作爲容器對象開始,然後在容器中加入其它的屬性列表對象,(可能)包含其它的字典和數組。字典的鍵必須是字符串對象,鍵的值則是NSDictionaryNSArrayNSStringNSDateNSData、和NSNumber類的實例。

對於可以將數據表示爲屬性列表對象的應用程序(比如NSDictionary對象),您可以用程序清單6-2所示的方法來將屬性列表寫入磁盤。該方法將屬性列表序列化爲NSData對象,然後調用writeApplicationData:toFile:方法(其實現如程序清單6-4所示)將數據寫入磁盤。

程序清單6-2  將屬性列表對象轉換爲NSData對象並寫入存儲

- (BOOL)writeApplicationPlist:(id)plist toFile:(NSString *)fileName {
    NSString *error;
    NSData *pData = [NSPropertyListSerialization dataFromPropertyList:plist format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
    if (!pData) {
        NSLog(@"%@", error);
        return NO;
    }
    return ([self writeApplicationData:pData toFile:(NSString *)fileName]);
}

在iPhone OS系統上保存屬性列表文件時,採用二進制格式進行存儲是很重要的。在編碼時,可以通過爲dataFromPropertyList:format:errorDescription:方法的format 參數指定NSPropertyListBinaryFormat_v1_0值來實現。二進制格式比其它基於文本的格式緊湊得多,這種緊湊不僅使屬性列表在用戶設備上佔用的空間最小,還可以減少讀寫屬性列表的時間。

程序清單6-3的代碼展示瞭如何從磁盤裝載屬性列表,並重新生成屬性列表中的對象。

程序清單 6-3 從應用程序的Documents目錄讀取屬性列表對象

- (id)applicationPlistFromFile:(NSString *)fileName {
    NSData *retData;
    NSString *error;
    id retPlist;
    NSPropertyListFormat format;
 
    retData = [self applicationDataFromFile:fileName];
    if (!retData) {
        NSLog(@"Data file not returned.");
        return nil;
    }
    retPlist = [NSPropertyListSerialization propertyListFromData:retData  mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&error];
    if (!retPlist){
        NSLog(@"Plist not returned, error: %@", error);
    }
    return retPlist;
}

有關屬性列表和NSPropertyListSerialization類的更多信息,請參見屬性列表編程指南

用歸檔器進行數據讀寫

歸檔器的作用是將任意的對象集合轉換爲字節流。這聽起來像是NSPropertyListSerialization類採用的過程,但它們之間有一個重要的區別。屬性列表序列化只能轉換一個有限集合的數據類型(大多數是數量類型),而歸檔器可以轉換任意的Objective-C對象、數量類型、數組、結構、字符串、及更多其它類型。

歸檔過程的關鍵在於目標對象的本身。歸檔器操作的對象必須遵循NSCoding協議,該協議定義了讀寫對象狀態的接口。歸檔器在編碼一組對象時,會向每個對象發送一個encodeWithCoder:消息,目標對象則在這個方法中將自身的關鍵狀態信息寫入到對應的檔案中。解檔過程的信息流與此相反,在解檔過程中,每個對象都會接收到一個initWithCoder:消息,用於從檔案中讀取當前狀態信息,並基於這些信息進行初始化。解檔過程完成後,字節流就被重新組成一組與之前寫入檔案時具有相同狀態的新對象。

Foundation框架支持兩種歸檔器—順序歸檔和基於鍵的歸檔。基於鍵的歸檔器更加靈活,是應用程序開發中推薦使用的歸檔器。下面的例子顯示如何用一個基於鍵的歸檔器對一個對象圖進行歸檔。_myDataSource對象的representation方法返回一個單獨的對象(可能是一個數組或字典),指向將要包含到檔案中的所有對象,之後該數據對象就被寫入由myFilePath變量指定路徑的文件中。

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[_myDataSource representation]];
[data writeToFile:myFilePath atomically:YES];

請注意:您還可以向NSKeyedArchiver對象發送archiveRootObject:toFile:消息,以便在一個步驟中完成檔案的創建和將檔案寫入存儲。

您可以簡單地通過相反的流程來裝載磁盤上的檔案內容。在裝載磁盤數據之後,可以通過NSKeyedUnarchiver類及其unarchiveObjectWithData:類方法來取回模型對象圖。例如,您可以用下面的代碼來解檔之前例子中的數據:

NSData* data = [NSData dataWithContentsOfFile:myFilePath];
id rootObject = [NSKeyedUnarchiver unarchiveObjectWithData:data];

更多如何使用歸檔器和如何使對象支持NSCoding協議的信息,請參見Cocoa的歸檔和序列化編程指南

將數據寫到Documents目錄

有了封裝應用程序數據的NSData對象(或者是檔案,或者是序列化了的屬性列表)之後,您就可以調用程序清單6-4所示的方法來將數據寫到應用程序的Documents目錄中。

程序清單6-4  將數據寫到應用程序的Documents目錄

- (BOOL)writeApplicationData:(NSData *)data toFile:(NSString *)fileName {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    if (!documentsDirectory) {
        NSLog(@"Documents directory not found!");
        return NO;
    }
    NSString *appFile = [documentsDirectory stringByAppendingPathComponent:fileName];
    return ([data writeToFile:appFile atomically:YES]);
}

從Documents目錄讀取數據

爲了從應用程序的Documents目錄讀取文件,您首先需要根據文件名構建相應的路徑,然後以期望的方法將文件內容讀入內存。對於相對較小的文件—也就是尺寸小於幾個內存頁面的文件—您可以用程序清單6-5中的代碼來取得文件內容。該代碼首先爲Documents目錄下的文件構建一個全路徑,併爲這個路徑創建一個數據對象,然後返回。

程序清單6-5  從應用程序的Documents目錄讀取數據

- (NSData *)applicationDataFromFile:(NSString *)fileName {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *appFile = [documentsDirectory stringByAppendingPathComponent:fileName];
    NSData *myData = [[[NSData alloc] initWithContentsOfFile:appFile] autorelease];
    return myData;
}

對於載入時需要多個內存頁面的文件,應該避免一次性地裝載整個文件。如果您只是計劃使用部分文件,這一點就尤其重要。對於大文件,您應該考慮用mmap函數或NSDatainitWithContentsOfMappedFile:方法來將文件映射到內存。

到底是採用映射文件還是直接裝載取決於您的考慮。如果只需要少量(3-4)內存頁面,則將整個文件載入內存相對安全一些。但是,如果您的文件需要數十或上百個頁面,則將文件映射到內存可能更爲有效一些。當然,無論採用什麼方法,您都應該測量應用程序的性能,確定裝載文件和爲其分配必要內存需要多長時間。

文件訪問的指導原則

在您創建文件或寫入文件數據時,請記住下面這些指導原則:

  • 使寫入磁盤的數據量儘可能少。文件操作速度相對較慢,且涉及到Flash盤的寫操作,有一定的壽命限制。下面這些具體的小貼士可以幫助您最少化與文件相關的操作:

    • 只寫入發生變化的文件部分,但要儘可能對變化進行累計,避免在只有少數字節發生改變時對整個文件進行寫操作。

    • 在定義文件格式時,將頻繁變化的內容放在一起,以便使每次需要寫入磁盤的總塊數最少。

    • 如果您的數據是需要隨機訪問的結構化內容,則可以將它們存儲在Core Data持久倉庫或SQLite數據庫中。如果您處理的數據量可能增長到數兆以上,這一點尤其重要。

  • 避免將緩存文件寫入磁盤。這個原則的唯一例外是:在應用程序退出時,您需要寫入某些狀態信息,使程序在下次啓動時可以回到之前的狀態。

保存狀態信息

當用戶按下Home鍵時,iPhone OS會退出您的應用程序,返回到Home屏幕。類似地,如果您的應用程序打開一個由其它應用程序處理的URI模式,iPhone OS也會退出您的應用程序,在相應的應用程序上打開該URI。換句話說,在Mac OS X上引起應用程序掛起或轉向後臺的動作,在iPhone OS上都會使其退出。這些動作在移動設備上經常發生,因此,您的應用程序必須改變管理可變數據和程序狀態的方式。

大多數桌面應用程序由用戶手工選擇將文件存入磁盤的時機,與此不同的是,iPhone應用程序應該在工作流的關鍵點上自動保存已發生的變化。究竟何時保存數據由您自己來決定,但是有兩個潛在的時間點:或者在用戶做出改變之後馬上進行保存;或者將同一頁面上的變化累計成批,然後在退出該頁面、顯示新頁面、或者應用程序退出的時候進行保存。在任何情況下,您不應該讓用戶漫遊到新的頁面而不保存之前頁面的內容。

當您的應用程序被要求退出時,應該將當前狀態保持到臨時的緩存文件或偏好數據庫中。在用戶下次啓動應用程序時,可以根據這些信息將程序恢復到之前的狀態。您保持的狀態信息應該儘可能少,但同時又足夠使應用程序恢復到恰當的點。您不必一定要顯示用戶上次退出時操作的頁面,如果那樣做並不合理的話。比如,如果一個用戶在編輯某個聯繫人的時候離開了Phone程序,那麼在下次運行時,Phone程序顯示的是聯繫人的頂級列表,而不是該聯繫人的編輯屏幕。

大小寫敏感性

iPhone OS設備的文件系統是大小寫敏感的。在處理文件名的任何時候,您都應該確保大小寫準確匹配,否則可能不能打開或訪問文件。

網絡

iPhone OS的網絡棧中包含幾個基於(iPhone和iPod touch設備上的)無線通訊硬件的編程接口。主編程接口是CFNetwork框架,該框架在BSD套接字和Core Foundation框架的封裝類型之上,實現了網絡實體間的通訊。您也可以用Foundation框架的NSStream類和位於系統Core OS層中的BSD套接字來進行通訊。

本文的下面部分將爲需要集成網絡功能的開發者提供一些專門針對iPhone的貼士。有關如何通過CFNetwork框架實現網絡通訊的信息,請參見CFNetwork編程指南CFNetwork框架參考;有關如何使用NSStream類的信息,則請參見Foundation框架參考

有效進行網絡通訊的貼士

在實現收發網絡數據的代碼時,請記住這是設備上最耗電的操作之一。最少化收發數據的時間有助於提高電池的使用壽命。爲此,您在編寫與網絡相關的代碼時需要考慮如下貼士:

  • 對於您自己控制的協議,請將數據格式定義得儘可能緊湊。

  • 避免使用聊天式的協議進行通訊。

  • 在任何可能的時候,將數據包成羣傳輸。

蜂窩網和Wi-Fi無線網都被設計爲在沒有數據傳輸活動時關閉電源。然而,根據無線網絡的不同,這樣做可能需要花幾秒鐘的時間。如果您的應用程序每隔數秒就發送少量的數據,則即使無線裝置實際上並沒做什麼,也會一直保持電源打開,持續耗電。相比於經常性地傳輸少量數據,一次性傳遞所有數據或間隔時間較長但每次傳遞數據量較大是更好的選擇。

在進行網絡通訊時,意識到數據包在任何時候都可能丟失是很重要的。在編寫網絡通訊代碼時,請務必在出現錯誤時進行處理,使程序儘可能強壯。實現響應網絡條件變化的處理程序是完全合理的,但如果這些處理程序始終沒有被調用,也不要覺得奇怪。舉例來說,在網絡服務消失時,Bonjour的網絡回調函數並不總是立即被調用。當接收到某個服務即將消失的通告時,Bonjour系統服務確實立即調用瀏覽回調函數(browsing callbacks),然而,網絡服務可能沒有通告就消失了,如果設備提供的網絡服務意外地丟掉網絡連接,或者通告在傳遞中丟失,就可能出現這種情況。

使用Wi-Fi

如果您的應用程序通過Wi-Fi無線信號訪問網絡,則必須將這個事實通知系統,即在應用程序的Info.plist文件中包含UIRequiresPersistentWiFi鍵。包含這個鍵使系統知道在檢測到活動的Wi-Fi 熱區時應該彈出網絡選擇框,同時還使系統知道在您的應用程序運行時不應試圖關閉Wi-Fi硬件。

爲了防止Wi-Fi硬件消耗太多的電能,iPhone OS內置一個定時器,如果在30分鐘內沒有應用程序通過UIRequiresPersistentWiFi鍵請求使用Wi-Fi,就會完全關閉該硬件。如果用戶啓動某個包含該鍵的應用程序,則在該程序的生命週期中,iPhone OS會有效地禁用該定時器。但是一旦該程序退出,系統就會重新啓用該定時器。

請注意:即使UIRequiresPersistentWiFi鍵的值爲true,在設備空閒(也就是處於屏幕鎖定狀態)時也是沒有效果的。在那種情況下,應用程序被認爲是不活動的,雖然它可能在某些級別上還在工作,但沒有Wi-Fi連接。

有關UIRequiresPersistentWiFi鍵及Info.plist文件中其它鍵的更多信息,請參見“信息屬性列表”部分。

飛行模式警告

當應用程序啓動時,如果設備處於飛行模式,系統可能會顯示一個對話框通知用戶。系統僅在下面的所有條件都滿足時纔會顯示這個通知對話框:

  • 應用程序的信息屬性列表(Info.plist) 文件包含UIRequiresPersistentWiFi鍵,且該鍵的值被設置爲true。

  • 應用程序啓動的同時設備處於飛行模式。

  • 在切換到飛行模式後設備上的Wi-Fi還沒有被手工激活。

多媒體支持

無論多媒體功能在您的應用程序中是處於中心地位,還是偶爾被使用,iPhone用戶都期望有很高的品質。視頻應該充分利用設備攜帶的高分辨率屏幕和高幀率,而引人注目的音頻也會對應用程序的總體用戶體驗有不可估量的增強作用。

您可以利用iPhone OS的多媒體框架來爲應用程序加入下面這些功能:

  • 高品質的音頻錄製和回放

  • 生動的遊戲聲音

  • 實時的聲音聊天

  • 用戶iPod音樂庫內容的回放

  • 在支持的設備上進行視頻的回放和錄製

本章將介紹iPhone OS上爲應用程序添加音視頻功能的多媒體技術。

在iPhone OS上使用聲音

iPhone OS爲應用程序提供一組豐富的聲音處理工具。根據功能的不同,這些工具被安排到如下的框架中:

  • 如果希望用簡單的Objective-C接口進行音頻的播放和錄製,可以使用AV Foundation框架。

  • 如果要播放和錄製帶有同步能力的音頻、解析音頻流、或者進行音頻格式轉換,可以使用Audio Toolbox框架。

  • 如果要連接和使用音頻處理插件,可以使用Audio Unit框架。

  • 如果希望在遊戲和其它應用程序中回放位置音頻,需要使用OpenAL框架。iPhone OS對OpenAL 1.1的支持是建立在Core Audio基礎上的。

  • 如果希望播放iPod庫中的歌曲、音頻書、或音頻播客,需要使用Media Player框架中的iPod媒體庫訪問接口。

Core Audio框架(和其它音頻框架對等)中提供所有Core Audio服務需要使用的數據類型。

本部分將就如何着手實現各種音頻功能提供一些指導,如下表所示:

請務必閱讀本文接下來的部分,即“基礎:硬件編解碼器、音頻格式、和音頻會話”部分,以瞭解在基於iPhone OS的設備上音頻工作機制的關鍵信息;而且也請您閱讀“iPhone音頻的最佳實踐”部分,該部分提供了一些指導原則,並列舉了一些能得到最好性能和最佳用戶體驗的音頻和文件格式。

當您準備好進一步學習時,請訪問iPhone Dev Center。這個開發者中心包含各種指南文檔、實例代碼、及更多其它信息。有關如何執行常見音頻任務的貼士,請參見音頻&視頻編程的How-To's部分;如果需要iPhone OS音頻開發的深入解釋,則請參見Core Audio概述音頻隊列服務編程指南、和音頻會話編程指南

基礎:硬件編解碼器、音頻格式、和音頻會話

在開始iPhone音頻開發之前,瞭解iPhone OS設備的一些硬軟件架構知識是很有幫助的。

iPhone音頻硬件編解碼

iPhone OS的應用程序可以使用廣泛的音頻數據格式。從iPhone OS 3.0開始,這些格式中的大多數都可以支持基於軟件的編解碼。您可以同時播放多路各種格式的聲音,雖然出於性能的考慮,您應該針對給定的場景選擇最佳的格式。通常情況下,硬件解碼帶來的性能影響比軟件解碼要小。

下面這些iPhone OS音頻格式可以利用硬件解碼進行回放:

  • AAC

  • ALAC (Apple Lossless)

  • MP3

通過硬件,設備每次只能播放這些格式中的一種。舉例來說,如果您正在播放的是MP3立體聲,則第二個同時播放的MP3聲音就只能使用軟件解碼。類似地,您不能通過硬件同時播放一個AAC聲音和一個ALAC聲音。如果iPod應用程序正在後臺播放AAC聲音,則您的應用程序只能使用軟件解碼來播放AAC、ALAC、和MP3音頻。

爲了以最佳性能播放多種聲音,或者爲了在iPod程序播放音樂的同時能更有效地播放聲音,可以使用線性PCM(無壓縮)或者IMA4(有壓縮)格式的音頻。

如果需要了解如何檢測設備硬軟件編解碼器是否可用,請查閱音頻格式服務參考中有關kAudioFormatProperty_HardwareCodecCapabilities常量的討論。

音頻回放和錄製格式

下面是一些iPhone OS支持的音頻回放格式:

  • AAC

  • HE-AAC

  • AMR (Adaptive Multi-Rate,是一種語音格式)

  • ALAC (Apple Lossless)

  • iLBC (互聯網Low Bitrate Codec,另一種語音格式)

  • IMA4 (IMA/ADPCM)

  • 線性PCM (無壓縮)
  • µ-law和a-law

  • MP3 (MPEG-1 音頻第3層)

下面是一些iPhone OS支持的音頻錄製格式:

  • ALAC (Apple Lossless)

  • iLBC (互聯網Low Bitrate Codec,用於語音)

  • IMA/ADPCM (IMA4)

  • 線性PCM

  • µ-law和a-law

下面的列表總結了iPhone OS如何支持單路或多路音頻格式:

  • 線性PCM和IMA4 (IMA/ADPCM) 在iPhone OS上,您可以同時播放多路線性PCM或IMA4聲音,而不會導致CPU資源的問題。這一點同樣適用於AMR和iLBC語音品質格式,以及µ-law和a-law壓縮格式。在使用壓縮格式時,請檢查聲音的品質,確保滿足您的需要。

  • AAC、MP3、和ALAC (Apple Lossless) AAC、MP3、和ALAC聲音的回放可以使用iPhone OS設備上高效的硬件解碼,但是這些編解碼器共用一個硬件路徑,通過硬件,設備每次只能播放上述格式的一種。

AAC、MP3、和ALAC的回放共用同一硬件路徑的事實會對“合作播放”風格的應用程序(比如虛擬鋼琴)產生影響。如果用戶在iPod程序上播放上述三種格式之一的音頻,則您的應用程序—如果要和該音頻一起播放聲音—需要使用軟件解碼。

音頻會話

Core Audio的音頻會話接口(具體描述請見音頻會話服務參考)使應用程序可以爲自己定義一般的音頻行爲,並在更大的音頻上下文中良好工作。您能夠影響的行爲有:

  • 您的音頻在Ring/Silent切換過程中是否變爲無聲

  • 在屏幕鎖定狀態時您的音頻是否停止

  • 當您的音頻開始播放時,iPod音頻是繼續播放,還是變爲無聲

更大的音頻上下文包括用戶所做的改變,比如用戶插入耳機,處理Clock和Calendar這樣的警告事件,或者處理呼入的電話。通過音頻會話,您可以對這樣的事件做出恰當的響應。

音頻會話服務提供了三種編程特性,如表7-1所述。

表7-1 音頻會話接口提供的特性

音頻會話特性

描述

範疇

範疇是標識一組應用程序音頻行爲的鍵。您可以通過範疇的設置來指示自己希望得到的音頻行爲,比如希望在屏幕鎖定狀態時繼續播放音頻。

中斷和路由變化

當您的音頻發生中斷或中斷結束,以及當硬件音頻路由發生變化時,音頻會話會發出通告,使您可以優雅地響應發生在更大音頻環境中的變化—比如由於電話呼入而導致的中斷。

硬件特徵

您可以通過查詢音頻會話來了解應用程序所在的設備的特徵,比如硬件採樣率,硬件通道數量,以及是否有音頻輸入。

AVAudioSession類參考AVAudioSessionDelegate協議參考描述了一個管理音頻會話的精簡接口。如果要使音頻會話支持中斷,則可以直接使用基於C語言的音頻會話服務接口,該接口的描述請見音頻會話服務參考。在應用程序中,這兩個接口的代碼可以混用及互相匹配。

音頻會話帶有一些缺省的行爲,可以作爲開發的起點。但是,除了某些特殊的情況之外,採用缺省行爲的音頻應用程序並不適合發行。您需要通過配置和使用音頻會話來表達自己使用音頻的意圖,響應OS級別的音頻變化。

舉例來說,在使用缺省的音頻會話時,如果出現Auto-Lock超時或屏幕鎖定,應用程序的音頻就會停止。如果您希望在屏幕被鎖定時繼續播放音頻,則必須將下面的代碼包含到應用程序的初始化代碼中:

[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error: nil];
[[AVAudioSession sharedInstance] setActive: YES error: nil];

AVAudioSessionCategoryPlayback範疇確保音頻的回放可以在屏幕鎖定時繼續。激活音頻會話會使指定的範疇也被激活。範疇的詳細信息請參見音頻會話編程指南中的音頻會話範疇部分。

如何處理呼入電話或時鐘警告引起的中斷取決於您使用的音頻技術,如表7-2所示。

表7-2  處理音頻中斷

音頻技術

中斷如何工作

系統聲音服務

當中斷開始時,系統聲音和警告聲音會變爲無聲。如果中斷結束—當用戶取消警告或選擇忽略呼入電話時,會發生這種情況—它們就又自動變爲可用。使用這種技術的應用程序無法影響聲音中斷的行爲。

音頻隊列服務、OpenAL、I/O音頻單元

這些技術爲中斷的處理提供最大的靈活性。您需要編寫一箇中斷監聽回調函數,具體描述請參見音頻會話編程指南中的 “響應音頻中斷”部分。

AVAudioPlayer

AVAudioPlayer類爲中斷的開始和結束提供了委託方法。根據實際的需要,您可以在audioPlayerBeginInterruption:方法中更新用戶界面,音頻播放器對象會負責暫停回放。您也可以利用audioPlayerEndInterruption:方法來重啓音頻的回放,並在必要時更新用戶界面。音頻播放器會負責重新激活您的音頻會話。

每個iPhone OS應用程序—除了很少的例外—都應該採納音頻會話服務。如果需要了解具體的用法,請閱讀音頻會話編程指南

播放音頻

本部分將介紹如何用iPod媒體庫訪問接口、系統聲音服務、音頻隊列服務、AV Foundation框架、和OpenAL來播放iPhone OS上的聲音。

通過iPod媒體庫訪問接口播放媒體項

從iPhone OS 3.0開始,iPod媒體庫訪問接口使應用程序可以播放用戶的歌曲、音頻書,和音頻播客。這個API的設計使基本回放變得非常簡單,同時又支持高級的檢索和回放控制。

如圖7-1所示,您的應用程序有兩種方式可以取得媒體項,一種是通過媒體項選擇器,如圖左所示,它是個易於使用、預先封裝好的視圖控制器,其行爲和內置iPod程序的音樂選擇接口類似。對於很多應用程序,這種方式就夠用了。如果媒體選擇器沒有提供您需要的某種訪問控制,則可以使用媒體查詢接口,該接口支持以基於斷言(predicate)的方式指定iPod媒體庫中的項目。

圖7-1  使用iPod媒體庫訪問接口

Using iPod library access

如上圖所示,位於右邊的應用程序在取得媒體項之後,可以通過這個API提供的音樂播放器進行播放。

有關如何在應用程序中加入媒體項回放功能的完整解釋,請參見iPod媒體庫訪問接口指南

使用系統聲音服務播放短聲音及觸發震動

當您需要播放用戶界面聲音效果(比如觸擊按鍵)或警告聲音,或者使支持震動的設備產生震動時,可以使用系統聲音服務。這個簡潔接口的描述請參見系統聲音服務參考。您可以在iPhone Dev Center中找到SysSound實例代碼。

請注意:通過系統聲音服務播放的聲音不受音頻會話配置的控制。因此,您無法使系統聲音服務的音頻行爲和應用程序的其它音頻行爲保持一致。這也是需要避免使用系統聲音服務播放音頻的最重要原因,除非您有意爲之。

AudioServicesPlaySystemSound函數使您可以非常簡單地播放短聲音文件。使用上的簡單也帶來一些限制。您的聲音文件必須是:

  • 長度小於30秒

  • 採用PCM或者IMA4 (IMA/ADPCM) 格式

  • 包裝爲.caf.aif、或者.wav文件

此外,當您使用AudioServicesPlaySystemSound函數時:

  • 聲音會以當前系統音量播放,且無法控制音量

  • 聲音立即被播放

  • 不支持環繞和立體效果

AudioServicesPlayAlertSound是一個類似的函數,用於播放一個短聲音警告。如果用戶在聲音設置中將設備配置爲震動,則這個函數在播放聲音文件之外還會產生震動。

請注意:系統和用戶界面的聲音效果並不提供給您的應用程序。舉例來說,將kSystemSoundID_UserPreferredAlert常量作爲參數傳遞給AudioServicesPlayAlertSound函數將不會播放任何聲音。

在用AudioServicesPlaySystemSoundAudioServicesPlayAlertSound函數時,您需要首先創建一個聲音ID對象,如程序清單7-1所示。

程序清單7-1  創建一個聲音ID對象

    // Get the main bundle for the app
    CFBundleRef mainBundle = CFBundleGetMainBundle ();
 
    // Get the URL to the sound file to play. The file in this case
    // is "tap.aiff"
    soundFileURLRef  =    CFBundleCopyResourceURL (
                                mainBundle,
                                CFSTR ("tap"),
                                CFSTR ("aif"),
                                NULL
                            );
 
    // Create a system sound object representing the sound file
    AudioServicesCreateSystemSoundID (
        soundFileURLRef,
        &soundFileObject
    );

然後再播放聲音,如清單7-2所示。

程序清單7-2  播放一個系統聲音

- (IBAction) playSystemSound {
    AudioServicesPlaySystemSound (self.soundFileObject);
}

這片代碼經常用於偶爾或者反覆播放聲音。如果您希望反覆播放,就需要保持聲音ID對象,直到應用程序退出。如果您確定聲音只用一次—比如程序啓動的聲音—則可以在播放完成後立即銷燬聲音ID,釋放其佔用的內存。

如果iPhone OS設備支持振動,則運行在該設備上的應用程序可以通過系統聲音服務觸發振動,振動的選項通過kSystemSoundID_Vibrate標識符來指定。AudioServicesPlaySystemSound函數可以用於觸發振動,具體如程序清單7-3所示。

程序清單7-3  觸發振動

#import <AudioToolbox/AudioToolbox.h>
#import <UIKit/UIKit.h>
- (void) vibratePhone {
    AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
}

如果您的應用程序運行在iPod touch上,則上面的代碼不執行任何操作。

通過AVAudioPlayer類輕鬆播放聲音

AVAudioPlayer類提供了一個簡單的Objective-C接口,用於播放聲音。如果您的應用程序不需要立體聲或精確同步,且不播放來自網絡數據流的音頻,則我們推薦您使用這個類來回放聲音。

通過音頻播放器可以實現如下任務:

  • 播放任意長度的聲音

  • 播放文件或內存緩衝區中的聲音

  • 循環播放聲音

  • 同時播放多路聲音(雖然不能精確同步)

  • 控制每個正在播放聲音的相對音量

  • 跳到聲音文件的特定點上,這可以爲需要快進和反繞的應用程序提供支持

  • 取得音頻強度數據,用於測量音量

AVAudioPlayer類可以播放iPhone OS上有的所有音頻格式,具體描述請參見“音頻回放和錄製格式”部分。或者。如果您需要該類接口的完整描述,請參見AVAudioPlayer類參考

爲了使音頻播放器播放音頻,您需要爲其分配一個聲音文件,使其做好播放的準備,併爲其指定一個委託對象。程序清單7-4中的代碼通常放在應用程序控制器類的初始化方法中。

程序清單7-4  配置AVAudioPlayer對象

// in the corresponding .h file:
// @property (nonatomic, retain) AVAudioPlayer *player;
 
@synthesize player; // the player object
 
NSString *soundFilePath =
                [[NSBundle mainBundle] pathForResource: @"sound"
                                                ofType: @"wav"];
 
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath: soundFilePath];
 
AVAudioPlayer *newPlayer =
                [[AVAudioPlayer alloc] initWithContentsOfURL: fileURL
                                                       error: nil];
[fileURL release];
 
self.player = newPlayer;
[newPlayer release];
 
[player prepareToPlay];
[player setDelegate: self];

您可以通過委託對象(可能是您的控制器對象)來處理中斷,以及在聲音播放完成後更新用戶界面。有關AVAudioPlayer類的委託對象的具體描述請參見AVAudioPlayerDelegate協議參考。程序清單7-5顯示了一個委託方法的簡單實現,其中的代碼在聲音播放完成時更新了播放/暫停切換按鍵的標題。

程序清單7-5  實現AVAudioPlayer類的委託方法

- (void) audioPlayerDidFinishPlaying: (AVAudioPlayer *) player
                        successfully: (BOOL) flag {
    if (flag == YES) {
        [self.button setTitle: @"Play" forState: UIControlStateNormal];
    }
}

調用回放控制方法可以使AVAudioPlayer對象執行播放、暫停、或者停止操作。您可以通過playing屬性來檢測當前是否正在播放。程序清單7-6顯示了播放/暫停切換方法的基本實現,其功能是控制回放和更新UIButton對象的標題。

程序清單7-6  控制AVAudioPlayer對象

- (IBAction) playOrPause: (id) sender {
 
    // if already playing, then pause
    if (self.player.playing) {
        [self.button setTitle: @"Play" forState: UIControlStateHighlighted];
        [self.button setTitle: @"Play" forState: UIControlStateNormal];
        [self.player pause];
 
    // if stopped or paused, start playing
    } else {
        [self.button setTitle: @"Pause" forState: UIControlStateHighlighted];
        [self.button setTitle: @"Pause" forState: UIControlStateNormal];
        [self.player play];
    }
}

AVAudioPlayer類使用Objective-C的屬性聲明來管理聲音信息—比如取得聲音時間線上的回放點和訪問回放選項(如音量和是否重複播放的設置)。舉例來說,您可以通過如下的代碼設置一個音頻播放器的回放音量:

[self.player setVolume: 1.0];    // available range is 0.0 through 1.0

有關AVAudioPlayer類的更多信息,請參見AVAudioPlayer類參考

用音頻隊列服務播放和控制聲音

音頻隊列服務(Audio Queue Services)加入了一些AVAudioPlayer類不具有的回放能力。通過音頻隊列服務進行回放可以:

  • 精確計劃聲音的播放,支持聲音的同步。

  • 精確控制音量—基於一個個的緩衝區。

  • 通過音頻文件流服務(Audio File Stream Services)來播放從流中捕捉的音頻。

音頻隊列服務可以播放iPhone OS支持的所有音頻格式,具體描述請見“音頻回放和錄製格式”部分;還支持錄製,詳見“錄製音頻”部分。

有關如何使用這個技術的詳細信息,請參見音頻隊列服務編程指南音頻隊列服務參考。如果需要實例代碼,請見iPhone Dev Center網站的SpeakHere實例(Mac OS X系統上的實現則見Core Audio SDK的AudioQueueTools工程,在Mac OS X上安裝Xcode工具之後,在/Developer/Examples/CoreAudio/SimpleSDK/AudioQueueTools路徑下可以找到AudioQueueTools工程)。

創建一個音頻隊列對象

創建一個音頻隊列對象需要下面三個步驟:

  1. 創建管理音頻隊列所需的數據結構,比如您希望播放的音頻格式。

  2. 定義管理音頻隊列緩衝區的回調函數。在回調函數中,您可以使用音頻文件服務來讀取希望播放的文件(在iPhone OS 2.1及更高版本中,您還可以用擴展音頻文件服務來讀取文件)。

  3. 通過AudioQueueNewOutput函數實例化回放音頻隊列。

程序清單7-7是上述步驟的ANSI C代碼。SpeakHere示例工程中也有同樣的步驟,只是它們位於Objective-C程序的上下文中。

程序清單7-7  創建一個音頻隊列對象

static const int kNumberBuffers = 3;
// Create a data structure to manage information needed by the audio queue
struct myAQStruct {
    AudioFileID                     mAudioFile;
    CAStreamBasicDescription        mDataFormat;
    AudioQueueRef                   mQueue;
    AudioQueueBufferRef             mBuffers[kNumberBuffers];
    SInt64                          mCurrentPacket;
    UInt32                          mNumPacketsToRead;
    AudioStreamPacketDescription    *mPacketDescs;
    bool                            mDone;
};
// Define a playback audio queue callback function
static void AQTestBufferCallback(
    void                   *inUserData,
    AudioQueueRef          inAQ,
    AudioQueueBufferRef    inCompleteAQBuffer
) {
    myAQStruct *myInfo = (myAQStruct *)inUserData;
    if (myInfo->mDone) return;
    UInt32 numBytes;
    UInt32 nPackets = myInfo->mNumPacketsToRead;
 
    AudioFileReadPackets (
        myInfo->mAudioFile,
        false,
        &numBytes,
        myInfo->mPacketDescs,
        myInfo->mCurrentPacket,
        &nPackets,
        inCompleteAQBuffer->mAudioData
    );
    if (nPackets > 0) {
        inCompleteAQBuffer->mAudioDataByteSize = numBytes;
        AudioQueueEnqueueBuffer (
            inAQ,
            inCompleteAQBuffer,
            (myInfo->mPacketDescs ? nPackets : 0),
            myInfo->mPacketDescs
        );
        myInfo->mCurrentPacket += nPackets;
    } else {
        AudioQueueStop (
            myInfo->mQueue,
            false
        );
        myInfo->mDone = true;
    }
}
// Instantiate an audio queue object
AudioQueueNewOutput (
    &myInfo.mDataFormat,
    AQTestBufferCallback,
    &myInfo,
    CFRunLoopGetCurrent(),
    kCFRunLoopCommonModes,
    0,
    &myInfo.mQueue
);
控制回放音量

音頻隊列對象爲您提供兩種控制回放音量的方法。

您可以通過調用AudioQueueSetParameter函數並傳入kAudioQueueParam_Volume參數來直接設置回放的音量,如程序清單7-8所示,音量的變化會立即生效。

程序清單7-8  直接設置回放的音量

Float32 volume = 1;    // linear scale, range from 0.0 through 1.0
AudioQueueSetParameter (
    myAQstruct.audioQueueObject,
    kAudioQueueParam_Volume,
    volume
);

您還可以通過AudioQueueEnqueueBufferWithParameters函數來設置音頻隊列緩衝區的回放音量。這個函數可以指定音頻隊列緩衝區進入隊列時攜帶的音頻隊列設置。通過這個函數做出的改變在音頻隊列緩衝區開始播放的時候生效。

在上述的兩種情況下,對音頻隊列的音量所做的修改都會一直保持下來,直到再次被改變。

指示回放音量

您可以通過下面的方式得到音頻隊列對象的當前回放音量:

  1. 啓用音頻隊列對象的音量計,具體方法是將其kAudioQueueProperty_EnableLevelMetering屬性設置爲true

  2. 查詢音頻隊列對象的kAudioQueueProperty_CurrentLevelMeter屬性。

這個屬性的值是一個AudioQueueLevelMeterState結構的數組,每個聲道都有一個相對應的結構。程序清單7-9顯示了這個結構的內容:

程序清單7-9  AudioQueueLevelMeterState結構

typedef struct AudioQueueLevelMeterState {
    Float32     mAveragePower;
    Float32     mPeakPower;
};  AudioQueueLevelMeterState;
同時播放多路聲音

爲了同時播放多路聲音,需要爲每路聲音創建一個回放音頻隊列對象,並對每個音頻隊列調用 AudioQueueEnqueueBufferWithParameters函數,將第一個音頻緩衝區排入隊列,使之開始播放。

在基於iPhone OS的設備中同時播放聲音時,音頻格式是很關鍵的。如果要同時播放,您需要使用線性PCM (無壓縮) 音頻格式或特定的有壓縮音頻格式,具體描述請參見“音頻回放和錄製格式”部分。

使用OpenAL播放和定位聲音

開源的OpenAL音頻API位於iPhone OS系統的OpenAL框架中,它提供了一個優化接口,用於定位正在回放的立體聲場中的聲音。使用OpenAL進行聲音的播放、定位、和移動是很簡單的—其工作方式和其它平臺一樣。此外,OpenAL還可以進行混音。OpenAL使用Core Audio的I/O單元進行回放,從而使延遲最低。

由於所有的這些原因,OpenAL是iPhone OS設備中游戲程序的最好選擇。當然,OpenAL也是一般的iPhone OS應用程序進行音頻播放的良好選擇。

iPhone OS對OpenAL 1.1的支持是構建在Core Audio之上的。更多的信息請參見iPhone OS系統的OpenAL FAQ。如果需要有關OpenAL的文檔,請參見http://openal.org的OpenAL網站;如果需要演示如何播放OpenAL音頻的示例程序,請參見oalTouch

錄製音頻

在iPhone OS系統上,可以通過AVAudioRecorder類和音頻隊列服務來進行音頻錄製,而Core Audio則爲其提供底層的支持。這些接口所做的工作包括連接音頻硬件、管理內存、以及在需要時使用編解碼器。您可以錄製“音頻的回放和錄製格式”部分列出的所有格式的音頻。

本部分將介紹如何通過AVAudioRecorder類和音頻隊列服務在iPhone OS系統上錄製音頻。

通過AVAudioRecorder類進行錄製

iPhone OS上最簡單的錄音方法是使用AVAudioRecorder類,類的具體描述請參見AVAudioRecorder類參考。該類提供了一個高度精簡的Objective-C接口。通過這個接口,您可以輕鬆實現諸如暫停/重啓錄音這樣的功能,以及處理音頻中斷。同時,您還可以對錄製格式保持完全的控制。

進行錄製時,您需要提供一個聲音文件的URL、建立音頻會話、以及配置錄音對象。進行這些準備工作的一個良好時機就是應用程序啓動的時候,如程序清單7-10所示。諸如soundFileURLrecording這樣的變量都在類接口文件中進行聲明。

程序清單7-10  建立音頻會話和聲音文件的URL

- (void) viewDidLoad {
 
    [super viewDidLoad];
 
    NSString *tempDir = NSTemporaryDirectory ();
    NSString *soundFilePath = [tempDir stringByAppendingString: @"sound.caf"];
 
    NSURL *newURL = [[NSURL alloc] initFileURLWithPath: soundFilePath];
    self.soundFileURL = newURL;
    [newURL release];
 
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    audioSession.delegate = self;
    [audioSession setActive: YES error: nil];
 
    recording = NO;
    playing = NO;
}

您需要在接口聲明中加入AVAudioSessionDelegateAVAudioRecorderDelegateAVAudioPlayerDelegate(如果同時支持聲音回放的話)協議。

然後,就可以實現如程序清單7-11所示的錄製方法。

程序清單7-11  一個基於AVAudioRecorder類的錄製/停止方法

-(IBAction) recordOrStop: (id) sender {
 
    if (recording) {
 
        [soundRecorder stop];
        recording = NO;
        self.soundRecorder = nil;
 
        [recordOrStopButton setTitle: @"Record" forState: UIControlStateNormal];
        [recordOrStopButton setTitle: @"Record" forState: UIControlStateHighlighted];
 
        [[AVAudioSession sharedInstance] setActive: NO error: nil];
 
    } else {
 
        [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryRecord error: nil];
 
        NSDictionary *recordSettings =
            [[NSDictionary alloc] initWithObjectsAndKeys:
                [NSNumber numberWithFloat: 44100.0],                 AVSampleRateKey,
                [NSNumber numberWithInt: kAudioFormatAppleLossless], AVFormatIDKey,
                [NSNumber numberWithInt: 1],                         AVNumberOfChannelsKey,
                [NSNumber numberWithInt: AVAudioQualityMax],         AVEncoderAudioQualityKey,
            nil];
 
        AVAudioRecorder *newRecorder = [[AVAudioRecorder alloc] initWithURL: soundFileURL
                                                                   settings: recordSettings
                                                                      error: nil];
        [recordSettings release];
        self.soundRecorder = newRecorder;
        [newRecorder release];
 
        soundRecorder.delegate = self;
        [soundRecorder prepareToRecord];
        [soundRecorder record];
        [recordOrStopButton setTitle: @"Stop" forState: UIControlStateNormal];
        [recordOrStopButton setTitle: @"Stop" forState: UIControlStateHighlighted];
 
        recording = YES;
    }
}

有關AVAudioRecorder類的更多信息,請參見AVAudioRecorder類參考

用音頻隊列服務進行錄製

用音頻隊列服務進行錄製時,您的應用程序需要配置音頻會話、實例化一個錄音音頻隊列對象,併爲其提供一個回調函數。回調函數負責將音頻數據存入內存以備隨時使用,或者寫入文件進行長期存儲。

聲音的錄製發生在iPhone OS的系統定義級別(system-defined level)。系統會從用戶選擇的音頻源取得輸入—比如內置的麥克風、耳機麥克風(如果連接到iPhone上的話)、或者其它輸入源。

和聲音的回放一樣,您可以通過查詢音頻隊列對象的kAudioQueueProperty_CurrentLevelMeter屬性來取得當前的錄製音量,具體描述請見“指示回放音量”部分。

有關如何通過音頻隊列服務錄製音頻的詳細實例,請參見音頻隊列服務編程指南錄製音頻部分,實例代碼則請見iPhone Dev Center網站上的SpeakHere

解析音頻流

爲了播放音頻流內容,比如來自網絡連接的音頻流,可以結合使用音頻文件流服務和音頻隊列服務。音頻文件流服務負責從常見的、採用網絡位流格式的音頻文件容器中解析出音頻數據和元數據。您也可以用它來解析磁盤文件中的數據包和元數據。

iPhone OS可以解析的音頻文件和位流格式和Mac OS X相同,具體如下:

  • MPEG-1 Audio Layer 3,用於.mp3文件

  • MPEG-2 ADTS,用於.aac音頻數據格式

  • AIFC

  • AIFF

  • CAF

  • MPEG-4,用於.m4a、.mp4、和.3gp文件

  • NeXT

  • WAVE

在取得音頻數據包之後,您就可以以任何iPhone OS系統支持的格式進行播放,這些格式在“音頻回放和錄製格式”部分中列出。

爲了獲得最好的性能,處理網絡音頻流的應用程序應該僅使用來自Wi-Fi連接的數據。您可以通過iPhone OS提供的System Configuration框架及其SCNetworkReachability.h頭文件定義的接口來確定什麼網絡是可到達和可用的。如果需要實例代碼,請參見iPhone Dev Center網站的Reachability工程。

爲了連接網絡音頻流,可以使用iPhone OS系統中的Core Foundation框架中的接口,比如CFHTTPMesaage接口,具體描述請見CFHTTPMessage參考。通過音頻文件流服務解析網絡數據包,將它恢復爲音頻數據包,然後放入緩衝區,發送給負責回放的音頻隊列對象。

音頻文件流服務依賴於音頻文件服務定義的接口,比如AudioFramePacketTranslation結構和AudioFilePacketTableInfo結構,具體描述請見音頻文件服務參考

有關如何使用流的更多信息,請參見音頻文件流服務參考。實例代碼則請參見位於<Xcode>/Examples/CoreAudio/Services/目錄下的AudioFileStream例子工程,其中<Xcode>是開發工具所在的目錄。

iPhone OS系統上的音頻單元支持

iPhone OS提供一組音頻插件,稱爲音頻單元,可以用於所有的應用程序。您可以通過Audio Unit框架提供的接口來打開、連接、和使用音頻單元;還可以定義定製的音頻單元,在自己的應用程序內部使用。由於應用程序必須靜態連接定製的音頻單元,所以iPhone OS系統上的其它應用程序不能使用您開發的音頻單元。

表7-3列出了iPhone OS提供的音頻單元。

表7-3  系統提供的音頻單元

音頻單元

描述

轉換器單元

轉換器單元,類型爲kAudioUnitSubType_AUConverter,用於音頻數據的格式轉換。

iPod均衡器單元

iPod EQ單元,類型爲kAudioUnitSubType_AUiPodEQ,提供一個簡單的、基於預設的均衡器,可以在應用程序中使用。

3D混音器單元

3D混音器單元,類型爲kAudioUnitSubType_AU3DMixerEmbedded,用於混合多個音頻流,指定立體聲輸出移動,操作採樣率,等等。

多通道混音器單元

多通道混音器單元,類型爲kAudioUnitSubType_MultiChannelMixer,用於將多個音頻流混合成爲單一的音頻流。

一般輸出單元

一般輸出單元,類型爲kAudioUnitSubType_GenericOutput,支持和線性PCM格式互相轉換,可以用於開始或結束一個音頻單元圖。

I/O單元

I/O單元,類型爲kAudioUnitSubType_RemoteIO,用於連接音頻輸入和輸入硬件,支持實時I/O。如何使用音頻單元的實例代碼請見aurioTouch工程。

語音處理I/O單元

語音處理I/O單元,類型爲kAudioUnitSubType_VoiceProcessingIO,具有I/O單元的特徵,同時爲了支持雙向交流,加入了迴響抑制功能。

有關係統音頻單元的更多信息,請參見系統音頻單元訪問指南

iPhone音頻的最佳實踐

操作音頻的貼士

在操作iPhone OS系統上的音頻內容時,您需要記住表7-4列出的基本貼士。

表7-4  音頻貼士

貼士

動作

正確地使用壓縮音頻

對於AAC、MP3、和ALAC (Apple Lossless) 音頻,解碼過程是由硬件來完成的,雖然比較有效,但同時只能解碼一個音頻流。如果您需要同時播放多路聲音,請使用IMA4 (壓縮) 或者線性PCM (無壓縮) 格式來存儲那些文件。

將音頻轉換爲您需要的數據格式和文件格式

Mac OS X的afconvert工具可以進行很多數據格式和文件類型的轉換。請參見“iPhone OS偏好的音頻格式” 部分和afconvert工具的手冊頁面。

評價音頻的內存使用問題

當您使用音頻隊列服務播放音頻時,需要編寫一個回調函數,負責將較短的音頻數據片斷髮送到音頻隊列的緩衝區。在某些情況下,將整個音頻文件載入內存是最佳的選擇,這樣可以使播放時的磁盤訪問盡最少;而在另外一些情況下,最好的方法則是每次只載入足夠填滿緩衝區的數據。請測試和評價哪種策略對您的應用程序最好。

限制音頻的採樣率和位深度,減少音頻文件的尺寸

採樣率和每個樣本的位深度對無壓縮音頻的尺寸有直接的影響。如果您需要播放很多這樣的聲音,則應該考慮降低這些指標,以減少音頻數據的內存開銷。舉例來說,相對於使用採樣率爲44.1 kHz的音頻作爲聲音效果, 您可以使用採樣率爲32 kHz(或可能更低)的音頻,仍然可以得到很合理的品質。

選擇恰當的技術

使用Core Audio的系統聲音服務來播放警告和用戶界面聲音效果。當您希望使用便利的高級接口來定位立體聲場中的聲音,或者要求很低的回放延遲時,則應該使用OpenAL。如果需要從文件或網絡數據流中解析出音頻數據,可以使用音頻文件服務接口。如果只是簡單回放一路或多路聲音,則應該使用AVAudioPlayer類。對於具有其它音頻功能的應用程序,包括音頻流的回放和音頻錄製,可以使用音頻隊列服務。

低延遲編碼

如果需要儘可能低的回放延遲,可以使用OpenAL,或者直接使用I/O單元。

iPhone OS偏好的音頻格式

對於無壓縮(最高品質)音頻,請使用封裝在CAF文件中的、16位、低位在前(little endian)的線性PCM音頻數據。您可以用Mac OS X的afconvert命令行工具來將音頻文件轉換爲上述格式:

/usr/bin/afconvert -f caff -d LEI16 {INPUT} {OUTPUT}

afconvert工具可以進行廣泛的音頻數據格式和文件類型轉換。您可以通過afconvert的手冊頁面,以及在shell提示符下鍵入afconvert -h命令獲取更多信息。

對於壓縮音頻,當每次只需播放一個聲音,或者當不需要和iPod同時播放音頻時,適合使用AAC格式的CAF或m4a文件。

當您需要在同時播放多路聲音時減少內存開銷時,請使用IMA4 (IMA/ADPCM) 壓縮格式,這樣可以減少文件尺寸,同時在解壓縮過程中對CPU的影響又最小。和線性PCM數據一樣,請將IMA4數據封裝在CAF文件中。

在iPhone OS使用視頻

錄製視頻

從iPhone OS 3.0開始,您可以在具有錄製支持的設備上錄製視頻,包括當時的音頻。顯示視頻錄製界面的方法是創建和推出一個UIImagePickerController對象,和顯示靜態圖片照相機界面完全一樣。

在錄製視頻時,您必須首先檢查是否存在照相機源類型 (UIImagePickerControllerSourceTypeCamera) ,以及照相機是否支持電影媒體類型 (kUTTypeMovie) 。根據您爲mediaTypes屬性分配的媒體類型的不同,選擇器對象可以直接顯示靜態圖像照相機,或者視頻攝像機,還可以顯示一個選擇界面,讓用戶選擇。

使用UIImagePickerControllerDelegate協議,註冊爲圖像選擇器的委託。在視頻錄製完成時,您的委託對象的 imagePickerController:didFinishPickingMediaWithInfo:方法會備調用。

對於支持錄製的設備,您也可以從用戶照片庫中選擇之前錄製的視頻。

有關如何使用圖像選擇器的更多信息,請參見UIImagePickerController類參考

播放視頻文件

在iPhone OS系統上,應用程序可以通過Media Player框架(MediaPlayer.framework)來播放視頻文件。視頻的回放只支持全屏模式,需要播放場景切換動畫的遊戲開發者或需要播放媒體文件的其它開發者可以使用。當應用程序開始播放視頻時,媒體播放器界面就會接管,將屏幕漸變爲黑色,然後漸漸顯示視頻內容。視頻播放界面上可以顯示或者不顯示調整回放的用戶控件。您可以通過部分或全部激活這些控件(如圖7-2所示),使用戶可以改變音量、改變回放點、開始或停止視頻的播放。如果禁用所有的控件,視頻會一直播放,直到結束。

圖7-2  帶有播放控制的媒體播放器界面

Media player interface with transport controls

在開始播放前,您必須知道希望播放的URL。對於應用程序提供的文件,這個URL通常是指向應用程序包中某個文件的指針;但是,它也可以是指向遠程服務器文件的指針。您可以用這個URL來實例化一個新的MPMoviePlayerController類的實例。這個類負責視頻文件的回放和管理用戶交互,比如響應用戶對播放控制(如果顯示的話)的觸擊動作。簡單調用控制器的play方法,就可以開始播放了。

程序清單7-12顯示一個實例方法,功能是播放位於指定URL的視頻。play方法是異步的調用,在電影播放時會將控制權返回給調用者。電影控制器負責將電影載入一個全屏的視圖,並通過動畫效果將電影放到應用程序現有內容的上方。在視頻回放完成後,電影控制器會向委託對象發出一個通告,該委託對象負責在不再需要時釋放電影控制器。

程序清單7-12  播放全屏電影

-(void)playMovieAtURL:(NSURL*)theURL
{
    MPMoviePlayerController* theMovie = [[MPMoviePlayerController alloc] initWithContentURL:theURL];
 
    theMovie.scalingMode = MPMovieScalingModeAspectFill;
    theMovie.movieControlMode = MPMovieControlModeHidden;
 
    // Register for the playback finished notification.
    [[NSNotificationCenter defaultCenter] addObserver:self
                selector:@selector(myMovieFinishedCallback:)
                name:MPMoviePlayerPlaybackDidFinishNotification
                object:theMovie];
 
    // Movie playback is asynchronous, so this method returns immediately.
    [theMovie play];
}
 
// When the movie is done, release the controller.
-(void)myMovieFinishedCallback:(NSNotification*)aNotification
{
    MPMoviePlayerController* theMovie = [aNotification object];
 
    [[NSNotificationCenter defaultCenter] removeObserver:self
                name:MPMoviePlayerPlaybackDidFinishNotification
                object:theMovie];
 
    // Release the movie instance created in playMovieAtURL:
    [theMovie release];
}

有關Media Player框架的各個類的更多信息,請參見Media Player框架參考。有關它支持的視頻格式列表,請參見iPhone OS技術概覽

 

設備支持

iPhone OS支持很多使移動計算的用戶體驗更具吸引力的特性。通過iPhone OS,應用程序可以訪問諸如加速計和照相機這樣的硬件特性,也可以訪問像用戶照片庫這樣的軟件特性。本文的下面部分將描述這些特性,並向您展示如何將它們集成到您的應用程序中。

確定硬件支持是否存在

爲iPhone OS設計的應用程序必須能夠運行在具有不同硬件特性的多種設備上。雖然像加速計和Wi-Fi連網這樣的特性在所有設備上都是支持的,但是一些設備不包含照相機或GPS硬件。如果您的應用程序要求設備具有這樣的特性,應該在用戶購買之前通知他們。對於那些不是必需、但如果存在就希望支持的特性,則必須在試圖使用之前檢測它們是否存在。

重要提示:如果應用程序運行的前提是某個特性一定要存在,則應該在應用程序的Info.plist文件中對UIRequiredDeviceCapabilities鍵進行相應的設置,以避免將需要某種特性的應用程序安裝在不具有該特性的設備上。但是,如果您的應用程序在給定特性存在或不存在時都可以運行,則不應該包含這個鍵。更多有關如果配置該鍵的信息,請參見“信息屬性列表”部分。

 

表8-1列出了確定某種硬件是否存在的方法。如果您的應用程序在缺少某個特性時可以工作,而在該特性存在時又可以加以利用,則應該使用這些技術。

表8-1  識別可用的硬件特性

特性

選項

確定網絡是否存在...

使用Software Configuration框架的可達性(reachability)接口檢測當前的網絡連接。有關如何使用Software Configuration框架的例子請參見可達性部分。

確定靜態照相機是否存在...

使用UIImagePickerController類的isSourceTypeAvailable:方法來確定照相機是否存在。更多信息請參見“使用照相機進行照相”部分。

確定音頻輸入(麥克風)是否存在…

在iPhone OS 3.0及之後的系統上,可以用AVAudioSession類來確定音頻輸入是否存在。該類考慮了iPhone OS設備上的很多不同的音頻輸入設備,包括內置的麥克風、耳機插座、和連接的配件。更多信息請參見AVAudioSession類參考部分。

確定GPS硬件是否存在…

在配置CLLocationManager對象、使應用程序可以獲取位置變化時,指定高精度級別。Core Location框架並不指定硬件是否存在的直接信息,而是使用精度值來提供您所需要的數據。如果一系列位置事件報告的精度都不夠高,您可以通知用戶。更多信息請參見“獲取用戶的當前位置”部分。

確定特定的配件是否存在…

使用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類的實例,該類中包含NSInputStreamNSOutputStream對象,可以和配件進行通訊。通過這些流對象,應用程序可以向配件發送未經加工的數據包,以及接收來自配件的類似數據包。因此,您必須按照期望的協議來理解每個數據包的格式。

聲明應用程序支持的協議

能夠和配件通訊的應用程序應該在其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單件對象來接收加速計數據。您可以通過UIAccelerometersharedAccelerometer類方法來取得該類的實例。之後,您就可以設置加速計數據更新的間隔時間及負責取得數據的自定義委託。數據更新的間隔時間的最小值是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列出了一些典型的更新頻率,以及在該頻率下產生的加速計數據適合哪些應用場合。

表8-2  常用的加速計事件更新頻率

事件頻率(Hz)

用途

10–20

適合用於確定代表設備當前方向的向量。

30–60

適合用於遊戲和使用加速計進行實時輸入的應用程序。

70–100

適合用於需要檢測設備高頻運動的應用程序,比如檢測用戶快速觸擊或擺動設備。

從加速計數據中分離重力成分

如果您希望通過加速計數據來檢測設備的當前方向,就需要將數據中源於重力的部分從源於設備運動的部分中分離開來。爲此,您可以使用低通濾波器來減少加速計數據中劇烈變化部分的權重,這樣過濾之後的數據更能反映由重力產生的較爲穩定的因素。

程序清單8-5展示了一個低通濾波器的簡化版本。清單中的代碼使用一個低通濾波因子生成一個由當前的濾波前數據的10%和前一個濾波後數據的90%組成的值。前一個加速計數值存儲在類的accelXaccelY、和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展示了一個簡化版的高通濾波器算法。從前一個事件得到的加速計數值存儲在類的accelXaccelY、和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常量來描述,它可以指示設備處於景觀模式還是肖像模式,以及設備的正面是朝上還是朝下。這些常量指示的是設備的物理方向,不一定和應用程序的用戶界面相對應。

當您不再需要設備的方向信息時,應該調用UIDeviceendGeneratingDeviceOrientationNotifications方法來關閉方向通告,使系統有機會關閉加速計硬件,如果其它地方也不使用的話。

使用位置和方向服務

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對象,對象中的coursespeed屬性聲明包含相關的信息。這個接口適用於需要跟蹤用戶移動的大多數應用程序,比如實現汽車導航系統的導航程序。對於基於指南針或者可能需要了解用戶靜止時朝向的應用程序,可以請求位置管理器提供方向對象。

您的程序必須運行在包含磁力計的設備上才能接收方向對象。磁力計可以測量地球散發的磁場,進而確定設備的準確方向。雖然磁力計可能受到局部磁場(比如揚聲器的永磁鐵、馬達、以及其它類型電子設備發出的磁場)的影響,但是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函數來創建基於公里值(而不是度數)的區域。

如果您希望在更新地圖時不顯示過程動畫,可以直接修改regioncenterCoordinate屬性的值;如果需要動畫過程,則必須使用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類中幾個例程,用於在地圖座標和地圖視圖對象的本地座標系統之間進行轉換,這些例包括:

有關如何處理定製註解事件的更多信息,請參見“處理註解視圖中的事件”部分。

顯示註解

註解是您定義並放置在地圖上面的信息片段。Map Kit框架將註解實現爲兩個部分,即註解對象和用於顯示註解的視圖。大多數情況下,您需要負責提供這些定製對象,但框架也提供一些標準的註解和視圖供您使用。

在地圖視圖上顯示註解需要兩個步驟:

  1. 創建註解對象並將它加入到地圖視圖中。

  2. 在自己的委託對象中實現mapView:viewForAnnotation:方法,並在該方法中創建相應的註解視圖。

註解對象是指遵循MKAnnotation協議的任何對象。通常情況下,註解對象是相對小的數據對象,存儲註解的座標及相關信息,比如註解的名稱。註解是通過協議來定義的,因此應用程序中的任何對象都可以成爲註解對象。然而,在實踐上,註解對象應該是輕量級的,因爲在顯式刪除註解對象之前,地圖視圖會一直保存它們的引用。注意,同樣的結論並不一定適用於註解視圖。

在將註解顯示在屏幕上時,地圖視圖負責確保註解對象具有相關聯的註解視圖,具體的方法是在註解座標即將變爲可見時調用其委託對象的mapView:viewForAnnotation:方法。但是,由於註解視圖的量級通常總是比其對應的註解對象更重,所以地圖對象儘可能不在內存中同時保存很多註解視圖。爲此,它實現了註解視圖的回收機制。這個機制和表視圖在滾動時回收表單元使用的機制相類似,即當一個註解視圖移出屏幕時,地圖視圖就解除其與註解對象之間關聯,將它放入重用隊列。而在創建新的註解視圖之前,委託的mapView:viewForAnnotation:方法應該總是調用地圖對象的dequeueReusableAnnotationViewWithIdentifier:方法來檢查重用隊列中是否還有可用的視圖對象。如果該方法返回一個正當的視圖對象,您就可以對其進行再次初始化,並將它返回;否則,您再創建和返回一個新的視圖對象。

添加和移除註解對象

您不應直接在地圖上添加註解視圖,而是應該添加註解對象,註解對象通常不是視圖。註解對象可以是應用程序中遵循MKAnnotation協議的任何對象。註解對象中最重要的部分是它的coordinate屬性聲明,它是MKAnnotation協議必需實現的屬性,用於爲地圖上的註解提供錨點。

往地圖視圖加入註解所需要的全部工作就是調用地圖視圖對象的addAnnotation:addAnnotations:方法。何時往地圖視圖加入註解以及何時爲加入的註解提供用戶界面由您自己來決定。您可以提供一個工具條,由用戶通過工具條上的命令來創建註解,或者也可以自行編碼創建註解,註解信息可能來自本地或遠程的數據庫信息。

如果您的應用程序需要刪除某個老的註解,則在刪除之前,應該調用removeAnnotation:removeAnnotations:方法將它從地圖中移除。地圖視圖會顯示它知道的所有註解,如果您不希望某些註解被顯示在地圖上,就需要顯式地將它們刪除。例如,如果您的應用程序允許用戶對餐廳或本地風景點進行過濾,就需要刪除與過濾條件不相匹配的所有註解。

定義註解視圖

Map Kit框架提供了兩個註解視圖類:MKAnnotationViewMKPinAnnotationViewMKAnnotationView類是一個具體的視圖,定義了所有註解視圖的基本行爲。MKPinAnnotationView類則是MKAnnotationView的子類,用於在關聯的註解座標點上顯示一個標準的系統大頭針圖像。

您可以將MKAnnotationView類用於顯示簡單的註解,也可以從該類派生出子類,提供更多的交互行爲。在直接使用該類時,您需要提供一個定製的圖像,用於在地圖上表示您希望顯示的內容,並將它賦值給註解視圖的image屬性。如果您顯示的內容不需要動態改變,而且不需要支持用戶交互,則這種用法是非常合適的。但是,如果您需要支持動態內容和用戶交互,則必須定義定製子類。

在一個定製的子類中,有兩種方式可以描畫動態內容:可以繼續使用image屬性來顯示註解圖像,這樣或許需要設置一個定時器,負責定時改變當前的圖像;也可以重載視圖的drawRect:方法來顯示描畫您的內容,這種方法也需要設置一個定時器,以定時調用視圖的setNeedsDisplay方法。

如果您通過drawRect:方法來描畫內容,則必須記住:要在註解視圖初始化後不久爲其指定尺寸。註解視圖的缺省初始化方法並不包含邊框矩形參數,而是在初始化後通過您分配給image屬性的圖像來設置邊框尺寸。如果您沒有設置圖像,就必須顯式設置邊框尺寸,您渲染的內容纔會被顯示。

有關如何在註解視圖中支持用戶交互的信息,請參見“處理註解視圖的事件”部分;有關如何設置定時器的信息,則請參見Cocoa定時器編程主題

創建註解視圖

您應該總是在委託對象mapView:viewForAnnotation:創建註解視圖。在創建新視圖之前,您應該總是調用dequeueReusableAnnotationViewWithIdentifier:方法來檢查是否有可重用的視圖,如果該方法返回非nil值,就應該將地圖視圖提供的註解分配給重用視圖的annotation屬性,並執行其它必要的配置,使視圖處於期望的狀態,然後將它返回;如果該方法返回nil,則應該創建並返回一個新的註解視圖對象。

程序清單8-10mapView: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  公牛眼註解視圖

The bullseye annotation view

程序清單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,則可以通過下面的步驟顯示照相機界面:

  1. 創建一個新的UIImagePickerController對象。

  2. 爲該對象分配一個委託對象

    大多數情況下,您可以讓當前的視圖控制器充當選取控制器的委託,但也可以根據自己的喜好使用完全不同的對象。委託對象必須遵循UIImagePickerControllerDelegateUINavigationControllerDelegate協議

    請注意:如果您的委託不遵循UINavigationControllerDelegate協議,在編譯時就會看到警告信息。然而,由於該協議的方法是可選的,所以不會對代碼帶來什麼影響。如果要消除該警告信息,需要將UINavigationControllerDelegate協議加入委託類支持的協議列表中。

  3. 將選取控制器的類型設置爲UIImagePickerControllerSourceTypeCamera

  4. allowsImageEditing屬性聲明設置恰當的值,以便激活或者禁用圖片編輯控制。這是個可選步驟。

  5. 調用當前視圖控制器的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:@"[email protected]",
                                   nil];
    NSArray *ccRecipients = [NSArray arrayWithObjects:@"[email protected]",
                                   @"[email protected]", nil];
    NSArray *bccRecipients = [NSArray arrayWithObjects:@"[email protected]",
                                   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框架參考

應用程序偏好設置

在傳統的桌面應用程序中,偏好設置是一些專門面嚮應用程序的設置,用於配置應用程序的行爲和外觀。iPhone OS也支持應用程序偏好設置,但並不將它作爲應用程序整體的一部分。在iPhone OS上,應用程序級別的偏好設置並不由各個程序本身的定製界面來顯示,而是由系統提供的Settings程序統一顯示。

爲了將定製的應用程序偏好設置集成到Settings程序中,您必須在應用程序包的頂級目錄中包含一個特殊格式的Settings程序包,由它負責將應用程序的偏好設置信息提供給Settings程序,而Settings程序則負責對其進行顯示,並將用戶提供的值寫入偏好設置數據庫。在運行時,您的應用程序可以通過標準的API取得這些偏好設置的值。本章的下面部分將描述Settings程序包的格式,以及用於取得偏好設置值的API。

偏好設置的指導原則

將偏好設置加入到Settings程序的做法最適合於效率工具類型的應用程序,以及偏好設置值配置完成後很少再改變的程序。Mail程序就是一個例子,它通過這種形式的偏好設置來存儲用戶賬戶信息及消息檢查設置。由於Settings程序可以按層次進行顯示,所以當您有大量的偏好設置時,通過Settings程序來進行操作也是比較合適的,在自己的應用程序中提供同樣的偏好設置集合可能需要太多屏幕,而且可能造成用戶的混淆。

當您的應用程序只需要少數的選項,或者用戶需要經常改變這些選項時,應該認真考慮是否用Settings程序來管理。舉例來說,工具程序更適合在主視圖的背面提供定製的配置選項,即在視圖上通過一個特殊的控件翻轉視圖,顯示應用程序的選項,再通過另一個控件將視圖翻轉回來。對於簡單的應用程序,這種方式使用戶可以立即訪問應用程序選項,比使用Settings程序方便得多。

對於遊戲和其它全屏程序的預置,可以使用Settings程序或自行實現定製的屏幕。定製屏幕通常更適合遊戲程序,因爲偏好設置可以處理爲遊戲設置的一部分。當然,您也可以使用Settings程序,如果您認爲那樣對遊戲的使用流程更好的話。

請注意:永遠不要使偏好設置同時存在於Setting程序和自定義的應用程序屏幕上。舉例來說,如果工具類應用程序在主視圖的背面有偏好設置,則在Settings程序中就不應該再有可配置的設置。如果您的應用程序需要進行偏好設置,則請僅選擇和使用一種方案。

偏好設置的接口

Settings程序實現了一組有層次的頁面,用於訪問應用程序的偏好設置。Settings程序的主視圖顯示了可以進行偏好設置的系統程序及第三方應用程序,用戶選擇一個第三方程序後會進入該程序的偏好設置頁面。

每個應用程序都至少有一個偏好設置頁面,我們稱爲主頁面。如果您的應用程序只有少數幾個偏好設置,則一個主頁面可能就夠了。然而,如果偏好設置太多,在主頁面上放不下,也可以加入更多頁面。這些額外的頁面就成爲主頁面的子頁面,用戶通過輕觸特定類型的偏好設置來訪問這些頁面。

您顯示的每一個偏好設置都必須具有特定的類型。偏好設置的類型定義了Settings程序如何對其進行顯示。大多數偏好設置類型都和某種類型的、用於進行設置的控件相關聯,而另外一些類型則提供一種偏好設置的組織方式。表9-1列出了Settings程序支持的各種元素類型,以及如何用這些類型來實現自己的偏好設置頁面。

表 9-1  偏好設置元素的類型

元素類型

描述

文本框

文本框類型顯示一個可選的標題和一個可編輯的文本輸入框,適用於需要用戶輸入自定義字符串的偏好設置。

這個類型的鍵是PSTextFieldSpecifier

標題

標題類型顯示一個只讀的字符串,適用於顯示只讀字符串的偏好設置(如果偏好設置包含隱含或非直接的值,這個類型可以將可能的值映射爲字符串)。

這個類型的鍵是PSTitleValueSpecifier

撥動開關

撥動開關類型顯示一個ON/OFF撥動按鍵,適用於配置值爲二選一的偏好設置。這個類型通常用於表示包含布爾值的偏好設置,但也可以用於表示包含非布爾值的偏好設置。

這個類型的鍵是PSToggleSwitchSpecifier

滑塊

滑塊類型顯示一個滑塊控件,適用於值爲一個範圍的偏好設置。這個類型的值是一個實數,值的最小和最大值由您來指定。

這個類型的鍵是PSSliderSpecifier

值列表

值列表類型使用戶可以從一個值的列表中選擇其一,適用於支持多個互斥值的偏好設置,這些值的類型可以是任意的。

這個類型的鍵是PSMultiValueSpecifier

組類型使您可以將幾組不同的偏好設置組織到一個頁面上。組類型並不表示一個可配置的偏好設置,而只是包含一個標題字符串,顯示在一或多個可配置的偏好設置之前。

這個類型的鍵是PSGroupSpecifier

子頁面

子頁面類型使用戶可以訪問新的偏好設置頁面,適用於實現多層次的偏好設置。有關如何配置和使用這個類型的更多信息,請參見“多層次的偏好設置” 。這個類型的鍵是 PSChildPaneSpecifier

各種偏好設置類型的詳細格式信息請參見Settings程序的結構參考。如果要了解如何創建和編輯Setting程序的頁面文件,則請參見“添加和修改Settings程序包”部分。

Settings程序包

在iPhone OS中,開發者通過一種特殊的Settings程序包來指定應用程序的偏好設置,這種程序包命名爲Settings.bundle,駐留在應用程序程序包的頂級目錄上。該程序包中包含一或多個Settings頁面文件,用於定義應用程序偏好設置的詳細信息;還可以包含顯示偏好設置需要的其它支持文件,比如圖像或本地化文件。表9-2列出了一個典型Settings程序包的內容。

表9-2  Settings.bundle目錄下的內容

項目名稱

描述

Root.plist

這個Settings頁面文件包含根頁面的偏好設置,它的內容在“Settings頁面文件的格式” 部分有更詳細的描述。

其它.plist文件

如果您需要通過多個子面板來構建一組有層次結構的偏好設置,則每個子面板的內容都分別存儲在不同的Settings頁面文件中。您需要負責命名這些文件,並將它們關聯到正確的子面板上。

一或多個.lproj 目錄

這些目錄用於存儲Settings頁面文件的本地化字符串資源。每個目錄都包含一個字符串文件,文件的標題在Settings頁面中指定。這些字符串文件爲偏好設置提供可以直接顯示給用戶的本地化內容。

其它圖像

如果您使用滑塊控件,則可以將滑塊的圖像存儲在程序包的頂級目錄下。

除了Settings程序包之外,應用程序的程序包中還可以包含應用程序設置的定製圖標。如果應用程序包的頂級目錄含有名爲Icon-Settings.png的文件,則該文件包含的圖標會被Settings程序用於標識應用程序的偏好設置。如果不存在這樣的文件,Settings程序會轉而採用應用程序的圖標文件(缺省爲Icon.png),並進行必要的縮放處理。您的Icon-Settings.png文件必須是29 x 29像素的圖像。

在啓動時,Settings程序會檢查每一個定製的應用程序是否包含Settings程序包,並對其進行裝載,然後將相應的應用程序名稱和圖標顯示在Settings程序的主頁面上。當用戶輕觸您的應用程序對應的行時,Settings程序會裝載Settings程序包的Root.plist頁面文件,並根據該文件的定義顯示應用程序的主設置頁面。

除了裝載程序包的Root.plist頁面文件之外,Settings程序還會在必要時裝載與該文件相關聯的語言資源。每個Settings頁面文件都可以有一個關聯的.strings文件,用於包含可見字符串的本地化值。在準備顯示偏好設置信息時,Settings程序會根據用戶偏好的語言來尋找相應的字符串資源,並在顯示之前替換偏好設置頁面中對應的內容。

Settings頁面文件的格式

Settings程序包中的每個Settings頁面文件都以iPhone設置屬性列表的文件格式(它是一種結構化的文件格式)進行存儲。編輯Settings頁面文件的最簡單方法,就是使用Xcode內置的編輯器組件,具體做法請參見“爲Settings頁面的編輯做準備”部分;您也可以用屬性列表編輯器程序來進行編輯,它是Xcode的工具之一。

請注意:在連編時,Xcode會將工程中基於XML的屬性文件自動轉換爲二進制格式,轉換過程是連編時自動完成的,目的是節省磁盤空間。

每個Settings頁面文件的根元素都包含表9-3列出的鍵。事實上,只有一個鍵是必須的,但我們推薦包含所有的兩個鍵。

表9-3 Settings頁面文件中的根鍵

類型

PreferenceSpecifiers (必須包含)

數組

這個鍵的值是一個字典數組,數組中的每個字典都包含一個偏好設置元素的信息。有關元素類型列表請參見表9-1,與元素類型相關聯的鍵的描述,則請參見Settings程序的結構參考

StringsTable

字符串

和這個頁面文件相關聯的字符串文件的名稱。程序包中專用於語言的工程目錄應該包含這個字符串文件的一個拷貝(帶有相應的本地化字符串)。如果您沒有包含這個鍵,則表示頁面文件中的字符串沒有被本地化。有關如何使用這些字符串的信息,請參見“本地化資源”部分。

多層次的偏好設置

如果您希望以一定的層次結構組織偏好設置,則您定義的每個頁面都必須有它自己的.plist文件,每個.plist文件包含一組僅在該頁面顯示的偏好設置。應用程序偏好設置的主頁面總是存儲在Root.plist文件中,其它頁面則可以根據自己的喜好進行命名。

爲了建立父子頁面之間的連接,您需要在父頁面中包含一個子面板元素。子面板元素負責佔據一行,在用戶觸擊時顯示一個新的設置Settings頁面。子面板元素的File鍵標識一個.plist文件的名稱,該文件負責定義子頁面的內容;Title鍵則標識子頁面的標題,該標題也作爲子面板元素行的文本。Settings程序會自動提供子頁面的漫遊控制,使用戶可以回到父頁面。

圖9-1展示了一組多層次的頁面是如何工作的。圖的左邊顯示了.plist文件,右邊則顯示各個頁面之間的關係。

圖9-1  用子面板組織偏好設置

Organizing preferences using child panes

有關子面板元素及其關聯鍵的更多信息,請參見Settings程序的結構參考

本地化資源

由於偏好設置中包含用戶可見的字符串,所以您應該在Settings程序包中爲那些字符串提供本地化版本。對於程序包支持的每種本地化語言,偏好設置頁面都可以有一個.strings文件與之對應。當Settings程序碰到一個支持本地化的鍵時,就會在相應本地化版本的.strings文件中尋找匹配的鍵,如果找到了,就顯示與之關聯的值。

在尋找諸如.strings文件這樣的本地化資源時,Settings程序遵循和Mac OS X程序一樣的規則,即首先尋找與用戶偏好語言相匹配的本地化資源,如果該版本的資源不存在,再選擇缺省語言的版本。

有關字符串文件的格式、語言工程目錄、以及如何從程序包中取得特定語言資源的相關信息,請參見國際化編程主題

添加和修改Settings程序包

Xcode提供了一個爲當前工程添加Settings程序包的模板。缺省的Settings程序包中包含一個Root.plist文件,以及一個用於存放本地化資源的缺省語言目錄。您可以在這個基礎上進行擴展,加入Settings程序包需要的其它屬性列表文件和資源。

添加Settings程序包

通過如下步驟可以爲您的Xcode工程添加一個Settings程序包:

  1. 選擇File > New File.

  2. 選擇iPhone OS > Settings > Settings Bundle template.

  3. 將文件命名爲Settings.bundle.

除了在工程中添加一個新的Settings程序包之外,Xcode還自動將該程序包加入到應用程序目標的Copy Bundle Resources連編階段中。這樣,您需要做的就只是修改Settings程序包中的屬性列表文件和添加其它資源了。

新添加的Settings.bundle程序包具有如下結構:

Settings.bundle/
    Root.plist
    en.lproj/
        Root.strings

爲Settings頁面的編輯做準備

用Settings程序包模板創建Settings程序包之後,您可以將結構文件(schema file)的內容進行格式化,使它們更容易編輯。下面的步驟向您展示如何格式化Settings程序包的Root.plist文件,這些步驟同樣適用於您創建的其它結構文件。

  1. 顯示Settings程序包中Root.plist文件的內容。

    1. 在Groups & Files列表中,展開Settings.bundle,查看程序包的內容。

    2. 選擇Root.plist文件,其內容就會顯示在Detail視圖中.

  2. 在Detail視圖中,選擇Root.plist文件的Root鍵。

  3. 選擇View > Property List Type > iPhone Settings plist.

    這個命令會將Detail視圖中的屬性列表內容進行格式化。Xcode不是直接顯示屬性列表的鍵和值,而是將它們顯示爲可讀的字符串(如圖9-2所示),使我們更加易於理解和編輯文件的內容。

    圖9-2  格式化過的Root.plist文件內容

    Formatted contents of the Root.plist file

配置一個Settings頁面:一個教程

這個部分包含一個教程,目的是向您展示如果配置一個Settings頁面,使它顯示您需要的內容。教程的目標是創建一個像圖9-2這樣的頁面,如果您之前還沒有爲自己的工程創建Settings程序包,則在執行下面這些步驟之前,應該按照“爲Settings頁面的編輯做好準備”部分的描述進行準備。

圖9-3  一個根Settings頁面

A root Settings page
  1. 將Settings Page Title 鍵的值改爲您的應用程序名稱。

    雙擊YOUR_PROJECT_NAME文本並將它改爲MyApp

  2. 展開Preference Items鍵,顯示模板包含的缺省項目。

  3. Item 1的標題改爲Sound

    • 展開Preference ItemsItem 1

    • Title鍵的值由Group改爲Sound

    • 保持Type鍵的值不變,仍然爲Group

  4. 爲新命名的Sound組創建第一個撥動開關。

    • 選中Preference ItemsItem 3項,並選擇Edit > Cut命令。

    • 選中Item 1,並選擇Edit > Paste命令(這會將撥動開關項移到文本框項的前面)。

    • 展開撥動開關項,顯示其配置鍵。

    • Title 鍵的值改爲Play Sounds

    • Identifier鍵的值改爲play_sounds_preference。現在,這個項目的配置應該如下圖所示:

      Changing the value of the identifier property
  5. 爲Sound 組創建第二個撥動開關。

    • 選中Item 2(即Play Sounds撥動開關)。

    • 選擇Edit > Copy命令。

    • 選擇Edit >Paste命令,將撥動開關的拷貝放到第一個的下面。

    • 展開新的撥動開關項,顯示其配置鍵。

    • 將其Title鍵的值改爲3D Sound

    • 將其Identifier鍵的值改爲3D_sound_preference

    現在,您已經完成了第一組設置,可以開始創建User Info組了。

  6. Item 4改爲Group類型的元素,並命名爲User Info

    • Preferences Items中點擊Item 4,顯示一個項目類型列表的下拉菜單。

    • 從下拉菜單中,選擇Group元素類型。

      Configuring a group item
    • 展開Item 4的內容。

    • Title鍵的值設置爲User Info

  7. 創建Name域。

    • 選擇Preferences Item中的Item 5

    • 使用下拉菜單,將其類型改爲Text Field

    • Title鍵的值改爲User Info

    • Identifier鍵的值改爲user_name

    • 合上展開按鍵,隱藏這個項目的內容。

  8. 創建Experience Level設置。

    • 選擇Item 5並點擊加號(+)鍵(或者按下回車鍵),創建一個新的項目。

    • 點擊這個新創建的項目,將其類型設置爲Multi Value

    • 展開項目的內容,將其標題設置爲Experience Level,標識設置爲experience_preference,缺省值設置爲0

    • 選中Default Value鍵,點擊加號鍵加入一個Titles數組。

    • 通過展開鍵打開Titles數組,點擊表格右側的項目按鍵。點擊這個鍵可以爲Titles添加一個新的子項目。

    • 選中新添加的子項目,點擊兩次加號鍵,創建總共三個子項目。
    • 將子項目的值設置爲BeginnerExpert、和Master

    • 再次選擇Titles鍵,點擊其展開鍵,將子項目隱藏起來。

    • 點擊加號鍵,創建Values數組。

    • Values數組中加入三個子項目,將它們的值分別設置爲01、和2

    • 點擊Item 6的展開按鍵,隱藏其內容。

  9. 添加設置頁面的最好一組。

    • 創建一個新項目,將其類型設置爲Group,標題設置爲Gravity

    • 再次創建一個新項目,將其類型設置爲Slider,標識設置爲gravity_preference,缺省值設置爲1,最大值設置爲2

創建額外的Settings頁面文件

Settings程序包模板包含一個Root.plist文件,用於定義應用程序的頂級Settings頁面。您如果要定義額外的Settings頁面,必須在Settings程序包中加入額外的屬性列表文件,您可以在Finder或Xcode中進行添加。

在Xcode中爲Settings程序包添加屬性列表的步驟如下:

  1. 在Groups & Files面板中,打開Settings程序包,選中Root.plist文件。

  2. 選擇File > New命令。

  3. 選擇Other > Property List命令。

  4. 選中新生成的文件,並選擇View > Property List Type > iPhone Settings plist命令,將它配置爲一個設置文件。

往Settings程序包加入新的Settings頁面之後,就可以按照“配置一個Settings頁面:一個教程”部分描述的那樣,在頁面中顯示設置。您必須通過一個子面板元素對其進行引用,詳情請參見“多層次的偏好設置”部分的描述。

訪問您的偏好設置

iPhone應用程序可以通過Foundation或者Core Foundation框架來讀寫偏好設置的值。在Foundation框架中,您可以通過NSUserDefaults類來讀寫偏好設置的值;而在Core Foundation框架中,您則可以使用幾個與偏好設置相關的函數。

程序清單9-1展示一個如何在應用程序中讀取偏好設置的簡單實例,例子中通過NSUserDefaults類取得一個在“配置一個Settings頁面:一個教程”部分中創建的偏好設置值,並將它賦值給應用程序的一個實例變量。

程序清單9-1  訪問應用程序偏好設置的值

- (void)applicationDidFinishLaunching:(UIApplication *)application
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [self setShouldPlaySounds:[defaults boolForKey:play_sounds_preference]];
 
    // Finish app initialization...
}

有關NSUserDefaults類中用於讀寫偏好設置值的方法的更多信息,請參見NSUserDefaults類參考;有關讀寫偏好設置的Core Foundation函數,請參見偏好設置工具參考

在仿真器中調試應用程序的偏好設置

在運行您的應用程序時,iPhone Simulator會將所有偏好設置的值保存在~/Library/Application Support/iPhone Simulator/User/Applications/<APP_ID>/Library/Preferences目錄下,這裏的<APP_ID>是一個由程序生成的目錄名,iPhone OS用它來標識您的應用程序。

每次重新安裝應用程序時,iPhone OS都會執行一次乾淨的安裝,將之前所有的偏好設置刪除。換句話說,在Xcode中連編或運行應用程序會導致老版本的所有內容被新版本所代替。如果您要測試應用程序在兩次運行之間偏好設置發生的變化,則必須直接從仿真器界面上運行,而不應該通過Xcode運行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章