Swift 项目的模块化

因为https://blog.csdn.net/urdfmqcul2/article/details/78788962
,博客搬家至https://juejin.im/user/59fd6315f265da4321536990

这篇博客是对最近在新启动的公司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协议来实现,将模块的一些公有内容放到这个模块类里供其他模块使用,其他模块便不需要再关注你的模块到底有哪些页面/功能。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容