這篇部落格是對最近在新啟動的公司
Swift
為基礎語言的項目中,對于整個項目架構的一些嘗試的整理。
Swift
是一門靜态的強類型語言,雖然可以在
Cocoa
架構下開發可以使用
Objective-C
的
Runtime
,但在我看來,既然選用了全新理念的語言,就應該遵循這種語言的規則來思考問題,是以一開始我在設計項目架構時,是盡量本着回避動态語言特性的原則來思考的。
但是,當我看到通過系統模闆建立的空白工程的
AppDelegate.swift
中的這段代碼時,我又轉變了我的想法:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
}
複制代碼
UIResponder
?這不還是
Objective-C
的類麼,整個App的"門臉"類的父類還是個
Objective-C
的子類。
既然如此,我又可以利用
Runtime
來搞事情了。
首先想到的就是之前我在關于AppDelegate瘦身的多種解決方案中寫的AppDelegateExtensions,既然
AppDelegate
類型還是
NSObject
,那就還是可以繼續用到工程裡來嘛。
NOTE:如果哪天蘋果工程師把UIKIT架構用swift重新給實作了一遍,那就得重新考慮實作方案了。
在
Objective-C
的項目裡,建議的加載
AppDelegateExtensions
代碼的地方,是
main()
函數裡:
int main(int argc, char * argv[]) {
@autoreleasepool {
installAppDelegateExtensionsWithClass([AppDelegate class]);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複制代碼
Swift
工程裡好像沒有
main()
函數了呢,那麼怎麼加載呢? 在官方文檔裡搜到了這麼一篇https://developer.apple.com/swift/blog/?id=7,裡面提到:
Application Entry Points and “main.swift”
You’ll notice that earlier we said top-level code isn’t allowed in most of your app’s source files. The exception is a special file named “main.swift”, which behaves much like a playground file, but is built with your app’s source code. The “main.swift” file can contain top-level code, and the order-dependent rules apply as well. In effect, the first line of code to run in “main.swift” is implicitly defined as the main entrypoint for the program. This allows the minimal Swift program to be a single line — as long as that line is in “main.swift”.
In Xcode, Mac templates default to including a “main.swift” file, but for iOS apps the default for new iOS project templates is to add @UIApplicationMain to a regular Swift file. This causes the compiler to synthesize a main entry point for your iOS app, and eliminates the need for a “main.swift” file.
很好,删除了
Appdelegate.swift
中的
@UIApplicationMain
,并建立
main.swift
檔案,然後執行我們加載
AppDelegateExtensions
的 top-level code:
import AppdelegateExtension
installAppDelegateExtensionsWithClass(AppDelegate.self)
UIApplicationMain(
CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)),
NSStringFromClass(MYApplication.self),
NSStringFromClass(AppDelegate.self)
)
複制代碼
UIApplicationMain
這個方法不用多說了,我們往第三個參數傳入一個
UIApplication
的子類類型,讓系統建立我自定義的
MYApplication
執行個體,這個類稍後會用到。
通過
AppDelegateExtensions
,我們完美解決了
AppDelegate
的備援問題,但是在
Swift
中,你要在哪去注冊通知呢?要知道
Swift
中已經沒有
load
方法了。
沒有
load
方法,那我們就自己造一個吧。結合上篇部落格裡提到的
ModuleManager
的方案,我們聲明一個名為
Module
的協定:
public protocol Module {
static func load() -> Module
}
複制代碼
有了
Module
,需要一個他的管理類:
class ModuleManager {
static let shared = ModuleManager()
private init() {
}
@discardableResult
func loadModule( moduleName: String) -> Module {
let type = moduleName.classFromString() as! Module.Type
let module = type.load()
self.allModules.append(module)
return module
}
class func loadModules(fromPlist fileName: String) {
let plistPath = Bundle.main.path(forResource: fileName, ofType: nil)!
let moduleNames = NSArray(contentsOfFile: plistPath) as! [String]
for(, moduleName) in (moduleNames.enumerated()){
self.shared.loadModule(moduleName)
}
}
var allModules: [Module] = []
}
複制代碼
ModuleManager
提供了一個
loadModules(fromPlist fileName: String)
的方法,可以加載plist檔案中提供的所有子產品。那這個方法在哪裡執行比較合适呢?
剛剛我們自定義的
MYApplication
就可以派上用場了:
class MYApplication: UIApplication {
override init() {
super.init()
ModuleManager.loadModules(fromPlist: "Modules.plist")
}
}
複制代碼
UIApplication
剛剛建立完成,所有的系統事件都還沒有開始,此時加載子產品,是一個非常合适的時機。
子產品加載的機制完成了,接下來添加一個子產品。在一般的工程裡,如果不用IB的話,我們會先删掉main.storyboard,在
AppDelegate
用代碼建立一個vc,像這樣:
func application( application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.backgroundColor = UIColor.white
let homeViewController = ViewController()
let navigationController = UINavigationController(rootViewController: homeViewController)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
return true
}
複制代碼
然後現在利用上面的架構,把首頁的加載也封裝成一個子產品! 聲明一個
HomeModule
來遵循
Module
協定:
class HomeModule: Module {
static func load() -> Module {
return HomeModule()
}
}
複制代碼
然後将首頁初始化的代碼在
HomeModule
中實作:
private init() {
NotificationCenter.observeNotificationOnce(NSNotification.Name.UIApplicationDidFinishLaunching) { (notification) in
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.backgroundColor = UIColor.white
let homeViewController = ViewController()
let navigationController = UINavigationController(rootViewController: homeViewController)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
}
}
複制代碼
需要注意的是,我們得監聽
UIApplicationDidFinishLaunching
通知發生後,才能開始加載首頁,還記得吧,因為
Module
的
init
方法調用的時機是
UIApplication
剛剛初始化的時候,此時還未到UI操作的時機。這裡我寫了一個
observeNotificationOnce
方法,這個方法會一次性地觀察某個通知,監聽到
UIApplicationDidFinishLaunching
通知後,再執行UI相關的代碼。
我們再回到
AppDelegate
:
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
}
複制代碼
幹幹淨淨!有沒有非常爽?反正我是爽了。
總結
通過這個架構,項目中需要在啟動時便加載的子產品,便可以通過實作
Module
協定,并通過plist檔案來控制
Module
的加載順序,同時結合
AppDelegateExtensions
可以監聽到所有
AppDelegate
中的事件。
Module
協定本身可以添加一些其他的方法,比如現在有
load
,相應地還可以加一些其他的生命周期方法。其他更多的,這就需要根據不同業務的特點來設計了。
此外,業務子產品也可以通過
Module
協定來實作,将子產品的一些公有内容放到這個子產品類裡供其他子產品使用,其他子產品便不需要再關注你的子產品到底有哪些頁面/功能。
上面所有的代碼示例在這裡。