天天看點

Swift 項目的子產品化

這篇部落格是對最近在新啟動的公司

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

協定來實作,将子產品的一些公有内容放到這個子產品類裡供其他子產品使用,其他子產品便不需要再關注你的子產品到底有哪些頁面/功能。

上面所有的代碼示例在這裡。

下一篇: A. Coins

繼續閱讀