阅读 3351

Swift 项目的模块化

这篇博客是对最近在新启动的公司Swift为基础语言的项目中,对于整个项目架构的一些尝试的整理。

Swift是一门静态的强类型语言,虽然可以在Cocoa框架下开发可以使用Objective-CRuntime,但在我看来,既然选用了全新理念的语言,就应该遵循这种语言的规则来思考问题,因此一开始我在设计项目架构时,是尽量本着回避动态语言特性的原则来思考的。

但是,当我看到通过系统模板创建的空白工程的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通知发生后,才能开始加载首页,还记得吧,因为Moduleinit方法调用的时机是UIApplication刚刚初始化的时候,此时还未到UI操作的时机。这里我写了一个observeNotificationOnce方法,这个方法会一次性地观察某个通知,监听到UIApplicationDidFinishLaunching通知后,再执行UI相关的代码。

我们再回到AppDelegate

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

}
复制代码

干干净净!有没有非常爽?反正我是爽了。

总结

通过这个架构,项目中需要在启动时便加载的模块,便可以通过实现Module协议,并通过plist文件来控制Module的加载顺序,同时结合AppDelegateExtensions可以监听到所有AppDelegate中的事件。

Module协议本身可以添加一些其他的方法,比如现在有load,相应地还可以加一些其他的生命周期方法。其他更多的,这就需要根据不同业务的特点来设计了。

此外,业务模块也可以通过Module协议来实现,将模块的一些公有内容放到这个模块类里供其他模块使用,其他模块便不需要再关注你的模块到底有哪些页面/功能。

上面所有的代码示例在这里

文章分类
iOS