这篇博客是对最近在新启动的公司Swift
为基础语言的项目中,对于整个项目架构的一些尝试的整理。
Swift
是一门静态的强类型语言,虽然可以在Cocoa
框架下开发可以使用Objective-C
的Runtime
,但在我看来,既然选用了全新理念的语言,就应该遵循这种语言的规则来思考问题,因此一开始我在设计项目架构时,是尽量本着回避动态语言特性的原则来思考的。
但是,当我看到通过系统模板创建的空白工程的AppDelegate.swift
中的这段代码时,我又转变了我的想法:
class AppDelegate: UIResponder, UIApplicationDelegate { ...}复制代码
UIResponder
?这不还是Objective-C
的类么,整个App的"门脸"类的父类还是个Objective-C
的子类。
Runtime
来搞事情了。 首先想到的就是之前我在中写的,既然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 AppdelegateExtensioninstallAppDelegateExtensionsWithClass(AppDelegate.self)UIApplicationMain( CommandLine.argc, UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer.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 UIKitclass AppDelegate: UIResponder, UIApplicationDelegate {}复制代码
干干净净!有没有非常爽?反正我是爽了。
总结
通过这个架构,项目中需要在启动时便加载的模块,便可以通过实现Module
协议,并通过plist文件来控制Module
的加载顺序,同时结合AppDelegateExtensions
可以监听到所有AppDelegate
中的事件。
Module
协议本身可以添加一些其他的方法,比如现在有load
,相应地还可以加一些其他的生命周期方法。其他更多的,这就需要根据不同业务的特点来设计了。
此外,业务模块也可以通过Module
协议来实现,将模块的一些公有内容放到这个模块类里供其他模块使用,其他模块便不需要再关注你的模块到底有哪些页面/功能。
上面所有的代码示例在。