这篇博客是对最近在新启动的公司 Swift 为基础语言的项目中, 对于整个项目架构的一些尝试的整理.
Swift 是一门静态的强类型语言, 虽然可以在 Cocoa 框架下开发可以使用 Objective-C 的 Runtime, 但在我看来, 既然选用了全新理念的语言, 就应该遵循这种语言的规则来思考问题, 因此一开始我在设计项目架构时, 是尽量本着回避动态语言特性的原则来思考的.
但是, 当我看到通过系统模板创建的空白工程的 AppDelegate.swift 中的这段代码时, 我又转变了我的想法:
class AppDelegate: UIResponder, UIApplicationDelegate {...}
UIResponder? 这不还是 Objective-C 的类么, 整个 App 的 "门脸" 类的父类还是个 Objective-C 的子类.
既然如此, 我又可以利用 Runtime 来搞事情了.
首先想到的就是之前我在关于 AppDelegate 瘦身的多种解决方案 https://www.jianshu.com/p/a926fd605b7a 中写的 https://github.com/jiaopen/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 协议来实现, 将模块的一些公有内容放到这个模块类里供其他模块使用, 其他模块便不需要再关注你的模块到底有哪些页面 / 功能.
上面所有的代码示例在这里 https://github.com/jiaopen/SwiftModuleManager .
来源: https://juejin.im/post/5b28c74651882574af280d44