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协议来实现,将模块的一些公有内容放到这个模块类里供其余模块使用,其余模块便不须要再关注你的模块到底有哪些页面/功能。

上面全部的代码示例在这里

相关文章
相关标签/搜索