這篇部落格是對最近在新啟動的公司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
協議來實現,將模組的一些公有內容放到這個模組類裡供其他模組使用,其他模組便不需要再關注你的模組到底有哪些頁面/功能。
上面所有的程式碼示例在這裡。