iOS 系統方案
首先,iOS 系統本身給我們提供了豐富而完備的 API 用於控制頁面轉換的各種細節。讓我們先看一個直接使用系統 API 調起頁面的簡單例子。
假設我們在一個名為 FirtViewController 的 view controller 中,需要進入到另一個名為 SecondViewController 的 view controller 中。 我們至少需要做兩件事情:
首先,需要初始化一個 SecondViewController 的實例 secondViewController。
然後,通過 pushViewController:animated 方法將這個實例推入到 FirstViewController 所屬的 UINavigationController 的頁面棧中。
相關代碼如下:
// in class FirstViewController SecondViewController *secondViewController = [[SecondViewController alloc] init]; [self.navigationController pushViewController:secondViewController animated:YES];
問題
初始化一個目標頁面,將其放入浏覽頁面棧,隨即顯示在前台。這個過程簡單而直接,在小型項目中,沒什麼問題,也是不錯的選擇。
但是在大型項目中,為了項目結構的健康,我們可能不得不做一些模塊化工作。模塊化的基本要求是解除依賴。這種直接使用 iOS 系統的方案中,需要調用者自己直接初始化目標頁面的實例。這就需要知道目標頁面的各種信息,起碼需要知道類名和合適的初始化方法。這造成了 view controller 之間的依賴。
如果,在一個大型項目中,需要解除各個模塊之間的頁面之間的耦合,我們就需要一個間接調用目標頁面的方案。
openURL 方案
在這些間接方案中,有很多是基於 iOS 系統提供的 UIApplicationDelegate 中的一個用於響應 URL 調用的鉤子方法。在 iOS 8 中,該方法為:application:openURL:sourceApplication:annotation:。在 iOS 9 中被它標記為 Deprecated,並提供了一個替代方法:application:openURL:options:。我們將這兩個方法簡稱為 openURL 鉤子方法。
如果,應用注冊了一個 URL scheme。任何第三方應用,以及應用自身對該 URL 的調用,都會調起以上的 openURL 鉤子方法。這就提供了一個在應用間通信的方法。同時這也是目前為止,iOS 系統中,應用間通信的唯一方式。
URL Routes
為了更便捷地使用 UIApplicationDelegate 的這個鉤子方法。iOS 社區中還湧現了很多 URL Routes 項目,例如, JLRoutes。這些 URL routes 項目大致都提供了兩類功能:
對 URL 的模式匹配,使得使用者可以更便捷地歸類 URL 和提取 URL 中的參數。
提供了閉包注冊 URL 的功能。注冊之後,任何應用調用 URL,則會調用對應閉包。
基於 UIApplicationDelegate 的 openURL 鉤子方法,或者更進一步在 JLRoutes 這種 URL Routes 庫的基礎上,我們可以很容易的完成一個 iOS 的頁面調用方案。基本過程如下:
首先,為不同 view controller 注冊不同的 URL,以形成 URL 到 view controller 之間的映射關系。如果使用 JLRoutes,可以在 block 中完成對目標頁面的調用。這實際就形成了 URL 到調用代碼的映射。
調用時,並不用直接初始化 view controller,而是調用一個 URL。
UIApplicationDelegate openURL 鉤子方法將被調用。在鉤子方法中對注冊形成的 URL 到 block 之間的映射關系表進行查詢。找到對應的 block,並執行它。block 中的代碼應該完成這樣的功能:對目標頁面的初始化,並將其推入前台。
也有直接為你提供將 view controller 注冊到 URL 功能的庫。比如,HHRouter。這種庫的目的就更為直接,僅用於頁面調用。
問題
這些使用了 openURL 鉤子方法的 URL Router 的頁面調用方案都有兩個問題:
傳遞復雜對象困難。這是因為在 opeURL 鉤子方法中,調用者和被調用者的通信協議是 URL。URL 本質上是一個一維字符協議。它並不是被設計用來表達具有多個層次的復雜信息。使用 json 或者 xml 這類有結構的格式表達復雜信息會更適用。當然,你可以做一些編碼工作,使其可以間接表達更復雜的信息。但是這就存在一些困難,也並不直接。而且 URL 的長度也是有限制的。將 URL 用於標識一個暴露給外部的資源是合適的。iOS 將其用於應用間的通信也是合適的。但在應用內部,信息的溝通更為直接,也更為復雜。URL 這個協議並不適用。
沒有區分內部調用和外部調用。這是因為 openURL 鉤子方法會響應所有應用以注冊的 URL 的調用。包括外部應用的調用和來自應用本身的調用。當然,鉤子方法的參數 sourceApplication,或者參數 options 的 key sourceApplication 所對應的值,可以幫助你標識出調用方的 bundleID。所以,可以說 iOS 系統提供了區分內部和外部調用的可能性。但是基於此做的 URL routes 庫都沒有實現這個功能。為了區分內部調用和外部調用,就需要提供兩類 URL 的注冊:一類用於外部調用;一類用於內部調用。這在 JLRoutes 和 HHRouter 都未見到。這就會導致我們無法區分內部外部調用,只要注冊了某個頁面調用,即使這個調用只應在內部使用,也會同時開放給外部。
這兩個缺陷都是由於對 openURL 鉤子方法的理解不當。openURL 鉤子方法被設計用於在應用之間進行通信。如果,將其用於應用內部的通信,我們就需要問一下這個 API 是否還適用呢?起碼,在我們的項目實踐中發現,僅僅用 URL 協議在應用內部通信的話,會感覺受到限制。
FRDIntent
由於 URL Routes 存在一些使用上的不便和問題,我們試圖尋找另外的解決方案。Android 系統給出了一個不錯的方案。Android 的 Intent 是一個消息對象,可用於調起代表頁面的 Activity 和代表服務的 Service。並且 Android 的 Intent 還是一個系統的級的消息對象。可以用於調起第三方應用,或者系統級的頁面或者服務。
應用內頁面調用:FRDIntent/Intent
FRDIntent 是一個用於解決應用內應用外頁面調用的庫。FRDIntent 也是該庫中一個重要對象的名字,它是一個消息對象,用於啟動 UIViewController。這是對 Android 系統中的 Intent 的模仿。當然,相對於 Android Intent,FRDIntent/Intent 做了極度簡化。這是因為 FRDIntent/Intent 的使用場景更為簡單:只處理應用內的 view controller 間跳轉。
在 FRDIntent 庫中,使用一個名為 FRDIntent 的對象傳遞消息。這是整個庫的核心類,所以庫的名字也是用這個類命名的。和 URL 相比,應用內部使用對象表達信息更為直接和便利。由此,帶了 FRDIntent 的諸多優勢:
充分解耦。調用者和被調用者完全隔離,調用者只需要依賴協議:FRDIntentReceivable。一個 UIViewControlller 符合該協議即可被啟動。
對於“啟動一個頁面,並從該頁面獲取結果”這種較普遍的需求提供了一個通用的解決方案。具體查看方法:startControllerForResult。這是對 Android 中 startActivityForResult 的模仿和簡化。
支持自定義轉場動畫。
支持傳遞復雜數據對象。
應用外通信:FRDIntent/URLRoutes
FRDIntent/URLRoutes 是一個 URL Router。它提供了將 block 注冊到 URL,建立關聯的能力。然後,通過在 UIApplicationDelegate 的 openURL 鉤子方法中放置代碼,獲知外部的 URL 調用,就可以調用對應的 block。可以看出,FRDIntent/URLRoutes 和社區已經存在的諸如 JLRoutes 和 HHRouter 這樣的庫的功能和目的差別不大。
FRDIntent/Intent 和 FRDIntent/URLRoutes
FRDIntent/Intent 提供了一個消息對象作為通信中介。URL 只是該消息對象一個標識。除此之外,還可以為消息對象配置目標頁面的初始化參數,轉場動畫。這樣,就充分滿足了頁面切換過程中所需要的攜帶的信息。FRDIntent/Intent 只用於管理應用內部的頁面調用。理論上,如果 iOS 系統開放更多 API,應該也可以像 Android Intent 那樣,實現一個應用間傳遞的消息對象。但由於 iOS 系統只提供了 openURL 這一種方式用於應用之間通信,所以現在 FRDIntent/Intent 只能用於應用內部。
FRDIntent/URLRoutes 還是保持了和社區中其他 URL Routes 庫基本一致的思想,仍然使用 URL 作為通信協議。但 FRDIntent/URLRoutes 只用於管理應用向外部暴露的頁面調用。通過 FRDIntent/URLRoutes 向外暴露一個頁面調用服務,其實就是為一個 block 注冊一個 URL。該 block 內執行的代碼僅僅是發出一個 FRDIntent 消息對象,以調起一個頁面。即外部調用在應用內部通過 FRDIntent 落地,最終啟動 view controller。也就是說外部調用實際上是通過內部調用實現的。
可以看出,在 FRDIntent 中,FRDIntent/URLRoutes 和 FRDIntent/Intent 一起相互配合完成了對應用外和應用內的頁面調用。
總結
FRDIntent/Intent 借鑒了 Android 系統,為 iOS 提供了一個類似 Android Intent 的消息對象。用於處理應用內的頁面調用。同時,封裝了一個 FRDIntent/URLRoutes 用於處理外部調用。
FRDIntent 較基於 openURL 鉤子方法的一些 URL Routers 不同之處在於,區分了內部和外部調用的處理方式。外部調用仍然基於 openURL 鉤子方法。內部調用使用消息對象。這個區分改善了 openURL 方案的一些問題。為在大型項目中,解除 view controller 之間的耦合提供了一個選擇。
相關項目倉庫
FRDIntent
JLRoutes
HHRouter