Swift 項目的模塊化

這篇博客是對最近在新啓動的公司Swift爲基礎語言的項目中,對於整個項目架構的一些嘗試的整理。git

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

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

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

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

既然如此,我又能夠利用 Runtime來搞事情了。

首先想到的就是以前我在關於AppDelegate瘦身的多種解決方案中寫的AppDelegateExtensions,既然AppDelegate類型仍是NSObject,那就仍是能夠繼續用到工程裏來嘛。app

NOTE:若是哪天蘋果工程師把UIKIT框架用swift從新給實現了一遍,那就得從新考慮實現方案了。框架

Objective-C的項目裏,建議的加載AppDelegateExtensions代碼的地方,是main()函數裏:ide

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」.post

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.ui

很好,刪除了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協議來實現,將模塊的一些公有內容放到這個模塊類裏供其餘模塊使用,其餘模塊便不須要再關注你的模塊到底有哪些頁面/功能。

上面全部的代碼示例在這裏

相關文章
相關標籤/搜索