Swift 專案的模組化

蘇大盒子發表於2018-06-19

這篇部落格是對最近在新啟動的公司Swift為基礎語言的專案中,對於整個專案架構的一些嘗試的整理。

Swift是一門靜態的強型別語言,雖然可以在Cocoa框架下開發可以使用Objective-CRuntime,但在我看來,既然選用了全新理念的語言,就應該遵循這種語言的規則來思考問題,因此一開始我在設計專案架構時,是儘量本著迴避動態語言特性的原則來思考的。

但是,當我看到通過系統模板建立的空白工程的AppDelegate.swift中的這段程式碼時,我又轉變了我的想法:

class AppDelegate: UIResponder, UIApplicationDelegate {
 ...
}
複製程式碼

UIResponder?這不還是Objective-C的類麼,整個App的"門臉"類的父類還是個Objective-C的子類。

Swift 專案的模組化
既然如此,我又可以利用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通知發生後,才能開始載入首頁,還記得吧,因為Moduleinit方法呼叫的時機是UIApplication剛剛初始化的時候,此時還未到UI操作的時機。這裡我寫了一個observeNotificationOnce方法,這個方法會一次性地觀察某個通知,監聽到UIApplicationDidFinishLaunching通知後,再執行UI相關的程式碼。

我們再回到AppDelegate

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

}
複製程式碼

乾乾淨淨!有沒有非常爽?反正我是爽了。

總結

通過這個架構,專案中需要在啟動時便載入的模組,便可以通過實現Module協議,並通過plist檔案來控制Module的載入順序,同時結合AppDelegateExtensions可以監聽到所有AppDelegate中的事件。

Module協議本身可以新增一些其他的方法,比如現在有load,相應地還可以加一些其他的生命週期方法。其他更多的,這就需要根據不同業務的特點來設計了。

此外,業務模組也可以通過Module協議來實現,將模組的一些公有內容放到這個模組類裡供其他模組使用,其他模組便不需要再關注你的模組到底有哪些頁面/功能。

上面所有的程式碼示例在這裡

相關文章