iOS VIPER架构实践(三):面向接口的路由设计

UML(二)component 组件图 组件图即是用来描述组件与组件之间关系的一种UML图。组件图在宏观层面上显示了构成系统某一个特定方面的实现结构。 组件图适用于基于组件的开发模式(Component-Based Development, CBD),它通过组件,及组件的接口、端口来表达组件的构成及其关系。当我们使用 CBD 进行开发时,其实是对行为进行了抽象,一个组件提供了若干的行为,组件图通过接口、端口的方式来表达组件间的连接,很形象的表达出组件是可被替换的概念,一个组件可以被另一个提供了相同接口的组件替换。因此,当我们通过组件进行建模时,能 阅读详情

路由是实现模块间解耦的一个有效工具。如果要进行组件化开发,路由是必不可少的一部分。目前iOS上绝大部分的路由工具都是基于URL匹配的,优缺点都很明显。这篇文章里将会给出一个更加原生和安全的设计,这个设计的特点是:

  • 路由时用protocol寻找模块
  • 可以对模块进行固定的依赖注入和运行时依赖注入
  • 支持不同模块间进行接口适配和转发,因此无需和某个固定的protocol关联
  • 充分解耦的同时,增加类型安全
  • 支持移除已执行的路由
  • 封装UIKit界面跳转方法,可以一键跳转和移除
  • 支持storyboard,支持其他任意模块
  • 可以检测界面跳转时的大部分错误

如果你想要一个能够充分解耦、类型安全、有依赖注入功能的路由器,那这个就是目前所能找到的最佳方案。

这个路由工具是为了实践VIPER模式而设计的,目的是为VIPER提供依赖注入功能,不过它也可以用于MVC、MVP、MVVM,没有任何限制。

工具和Demo地址:ZIKRouter。

目录

  • Router的作用
    • 路由缺失时的情况
    • 寻找模块
    • 声明依赖和接口
    • Builder和依赖注入
  • 现有的Router
    • URL Router
      • 优点
        • 极高的动态性
        • 统一多端路由规则
        • 适配URL scheme
      • 缺点
        • 不适合通用模块
        • 安全性差
        • 维护困难
    • Protocol Router
      • 优点
        • 安全性好,维护简单
        • 适用于所有模块
        • 优雅地声明依赖
      • 缺点
        • 动态性有限
        • 需要额外适配URL Scheme
      • Protocol是否会导致耦合?
        • 业务设计的互相关联
        • Required Interface 和 Provided Interface
    • Target-Action
      • 优点
      • 缺点
    • UIStoryboardSegue
    • 总结
  • ZIKRouter的特性
    • 离散式管理
    • 自由定义路由参数
    • 移除已执行的路由
    • 通过protocol获取对应模块
      • Protocol作为匹配标识
      • 多对一匹配
    • 依赖注入和依赖声明
      • 固定依赖和运行时依赖
      • 直接在头文件中声明
    • 使用泛型指明特定router
    • 类型安全
      • 传入正确的protocol
      • 泛型的协变和逆变
    • 用Adapter兼容接口
      • 为Provided模块添加Required Interface
      • 用中介者转发接口
    • 封装UIKit跳转和移除方法
      • 封装iOS的路由方法
      • 识别adaptative类型的路由
      • 支持自定义路由
      • 关于extension里的跳转方法
    • 支持storyboard
    • AOP
    • 路由错误检查
    • 支持任意模块
    • 性能
  • 项目地址和Demo

Router的作用

首先,我们需要梳理清楚,为什么我们需要Router,Router能带来什么好处,解决什么问题?我们需要一个什么样的Router?

路由缺失时的情况

没有路由时,界面跳转的代码就很容易产生模块间耦合。

iOS中执行界面跳转时,用的是UIViewController上提供的跳转方法:

[sourceViewController.navigationController pushViewController:destinationViewController animated:YES];
[sourceViewController presentViewController:destinationViewController animated:YES completion:nil];

如果是直接导入destinationViewController的头文件进行引用,就会导致和destinationViewController模块产生耦合。类似的,一个模块引用另一个模块时也会产生这样的耦合。因此我们需要一个方式来获取destinationViewController,但又不能对其产生直接引用。

这时候就需要路由提供的"寻找模块"的功能。以某种动态的方式获取目的模块。

那么路由是怎么解决模块耦合的呢?在上一篇VIPER讲解里,路由有这几个主要职责:

  • 寻找指定模块,执行具体的路由操作
  • 声明模块的依赖
  • 声明模块的对外接口
  • 对模块内各部分进行依赖注入

通过这几个功能,就能实现模块间的完全解耦。

寻找模块

路由最重要的功能就是给出一种寻找某个指定模块的方案。这个方案是松耦合的,获取到的模块在另一端可以随时被另一个相同功能的模块替换,从而实现两个模块之间的解耦。

寻找模块的实现方式其实只有有限的几种:

  • 用一个字符串identifier来标识某个对应的界面(URL Router、UIStoryboardSegue)
  • 利用Objective-C的runtime特性,直接调用目的模块的方法(CTMediator)
  • 用一个protocol来和某个界面进行匹配(蘑菇街的第二种路由和阿里的BeeHive),这样就可以更安全的对目的模块进行传参

这几种方案的优劣将在之后逐一细说。

声明依赖和接口

一个模块A有时候需要使用其他模块的功能,例如最通用的log功能,不同的app有不同的log模块,如果模块A对通用性要求很高,log方法就不能在模块A里写死,而是应该通过外部调用。这时这个模块A就依赖于一个log模块了。App在使用模块A的时候,需要知道它的依赖,从而在使用模块A之前,对其注入依赖。

当通过cocoapods这样的包管理工具来配置不同模块间的依赖时,一般模块之间是强耦合的,模块是一一对应的,当需要替换一个模块时会很麻烦,容易牵一发而动全身。如果是一个单一功能模块,的确需要依赖其他特定的各种库时,那这样做没有问题。但是如果是一个业务模块中引用了另一个业务模块,就应该尽量避免互相耦合。因为不同的业务模块一般是由不同的人负责,应该避免出现一个业务模块的简单修改(例如调整了方法或者属性的名字)导致引用了它的业务模块也必须修改的情况。

这时候,业务模块就需要在代码里声明自己需要依赖的模块,让app在使用时提供这些模块,从而充分解耦。

示例代码:

@protocol ZIKLoginServiceInput <NSObject>
- (void)loginWithAccount:(NSString *)account
                password:(NSString *)password
                 success:(void(^_Nullable)(void))successHandler
                   error:(void(^_Nullable)(void))errorHandler;
@end
@interface ZIKNoteListViewController ()
//笔记界面需要登录后才能查看,因此在头文件中声明,让外部在使用的时候设置此属性
@property (nonatomic, strong) id<ZIKLoginServiceInput> loginService;
@end

这个声明依赖的工作其实是模块的Builder的职责。一个界面模块大部分情况下都不止有一个UIViewController,也有其他一些Manager或者Service,而这些角色都是有各自的依赖的,都统一由模块的Builder声明,再在Builder内部设置依赖。不过在上一篇文章的VIPER讲解里,我们把Builder的职责也放到了Router里,让每个模块单独提供一个自己的Router。因此在这里,Router是一个离散的设计,而不是一个单例Router掌管所有的路由。这样的好处就是每个模块可以充分定制和控制自己的路由过程。

可以声明依赖,也就可以同时声明模块的对外接口。这两者很相似,所以不再重复说明。

Builder和依赖注入

执行路由的同时用Builder进行模块构建,构建的时候就对模块内各个角色进行依赖注入。当你调用某个模块的时候,需要的不是某个简单的具体类,而是一个构建完毕的模块中的某个具体类。在使用这个模块前,模块需要做一些初始化的操作,比如VIPER里设置各个角色之间的依赖关系,就是一个初始化操作。因此使用路由去获取某个模块中的类,必定需要通过模块的Builder进行。很多路由工具都缺失了这部分功能。

你可以把依赖注入简单地看成对目的模块传参。在进行界面跳转和使用某个模块时,经常需要设置目的模块的一些参数,例如设置delegate回调。这时候就必须调用一些目的模块的方法,或者传递一些对象。由于每个模块需要的参数都不一样,目前大部分Router都是使用字典包裹参数进行传递。但其实还有更好、更安全的方案,下面将会进行详解。

你也可以把Router、Builder和Dependency Injector分开,不过如果Router是一个离散型的设计,那么都交给各自的Router去做也很合理,同时能够减少代码量,也能够提供细粒度的AOP。

现有的Router

梳理完了路由的职责,现在来比较一下现有的各种Router方案。关于各个方案的具体实现细节我就不再展开看,可以参考这篇详解的文章:iOS 组件化 —— 路由设计思路分析。

URL Router

目前绝大多数的Router都是用一串URL来表示需要打开的某个界面,代码上看来大概是这样:

//注册某个URL,和路由处理进行匹配保存
[URLRouter registerURL:@"settings" handler:^(NSDictionary *userInfo) {
	UIViewController *sourceViewController = userInfo[@"sourceViewController"];
	//获取其他参数
	id param = userInfo[@"param"];
	//获取需要的界面
	UIViewController *settingViewController = [[SettingViewController alloc] init];
	[sourceViewController.navigationController pushViewController: settingViewController animated:YES];
}];
//调用路由
[URLRouter openURL:@"myapp://noteList/settings?debug=true" userInfo:params completion:^(NSDictionary *info) {

}];

传递一串URL就能打开noteList界面的settings界面,用字典包裹需要传递的参数,有时候还会把UIKit的push、present等方法进行简单封装,提供给调用者。

这种方式的优点和缺点都很突出。

优点
极高的动态性

这是动态性最高的方案,甚至可以在运行时随时修改路由规则,指向不同的界面。也可以很轻松地支持多级页面的跳转。

如果你的app是电商类app,需要经常做活动,app内的跳转规则经常变动,那么就很适合使用URL的方案。

统一多端路由规则

URL的方案是最容易跨平台实现的,iOS、Andorid、web、PC都按照URL来进行路由时,也就可以统一管理多端的路由规则,降低多端各自维护和修改的成本,让不懂技术的运营人员也可以简单快速地修改路由。

和上一条一样,这也是一个和业务强相关的优点。如果你有统一多端的业务需求,使用URL也很合适。

适配URL scheme

iOS中的URL scheme可以跨进程通信,从app外打开app内的某个指定页面。当app内的页面都能使用URL打开时,也就直接兼容了URL scheme,无需再做额外的工作。

缺点
不适合通用模块

URL Router的设计只适合UI模块,不适合其他功能性模块的组件。功能性模块的调用并不需要如此强的动态特性,除非是有模块热更新的需求,否则一个模块的调用在一个版本里应该总是稳定不变的,即便要进行模块间解耦,也不应该用这种方式。

安全性差

字符串匹配的方式无法进行编译时检查,当页面配置出错时,只能在运行时才能发现。如果某个开发人员不小心在字符串里加了一个空格,编译时也无法发现。你可以用宏定义来减少这种出错的几率。

维护困难

没有高效地声明接口的方式,只能从文档里查找,编写时必须仔细对照字符串及其参数类型。

传参通过字典来进行,参数类型无法保证,而且也无法准确地知道所调用的接口需要哪些参数。当目的模块进行了接口升级,修改了参数类型和数量,那所有用到的地方都要一一修改,并且没有编译器的帮助,你无法知道是否遗漏了某些地方。这将会给维护和重构带来极大的成本。

针对这个问题,蘑菇街的选择是用另一个Router,用protocol来获取目的模块,再进行调用,增加安全性。

Protocol Router

这个方案也很容易理解。把之前的字符串匹配改成了protocol匹配,就能获取到一个实现了某个protocol的对象。

开源方案里只看到了BeeHive实现了这样的方式:

id<ZIKLoginServiceInput> loginService = [[BeeHive shareInstance] createService:@protocol(ZIKLoginServiceInput)];
优点
安全性好,维护简单

再对这个对象调用protocol中的方法,就十分安全了。在重构和修改时,有了编译器的类型检查,效率更高。

适用于所有模块

Protocol更加符合OC和Swift原生的设计思想,任何模块都可以使用,而不局限于UI模块。

优雅地声明依赖

模块A需要用到登录模块,但是它要怎么才能声明这种依赖关系呢?如果使用Protocol Router,那就只需要在头文件里定义一个属性:

@property (nonatomic, string) id<ZIKLoginServiceInput> *loginService;

如果这个依赖是必需依赖,而不是一个可选依赖,那就添加到初始化参数里:

@interface ModuleA ()
- (instancetype)initWithLoginService:(id<ZIKLoginServiceInput>)loginService;
@end

问题是,如果这样的依赖很多,那么初始化方法就会变得很长。因此更好的做法是由Builder进行固定的依赖注入,再提供给外部。目前BeeHive并没有提供依赖注入的功能。

缺点
动态性有限

你可以维护一份protocol和模块的对照表,使用动态的protocol来尝试动态地更改路由规则,也可以在Protocol Router之上封装一层URL Router专门用于动态性的需求。

需要额外适配URL Scheme

使用了Protocol Router就需要再额外处理URL Scheme了。不过这样也是正常的,解析URL Scheme本来就应该放到另一个单独的模块里。

Protocol是否会导致耦合?

很多谈到这种方案的文章都会指出,和URL Router相比,Protocol Router会导致调用者引用目的模块的protocol,因此会产生"耦合"。我认为这是对"解耦"的错误理解。

要想避免耦合,首先要弄清楚,我们需要什么程度的解耦。我的定义是:模块A调用了模块B,模块B的接口或者实现在做出简单的修改时,或者模块B被替换为相同功能的模块C时,模块A不需要进行任何修改。这时候就可以认为模块A和模块B是解耦的。

业务设计的互相关联

有些时候,表达出两个模块之间的关联是有意义的。

当一个界面A需要展示一个登录界面时,它可能需要向登录界面传递一个"提示语"参数,用于在登录界面显示一串提示。这时候,界面A在调用登录界面时,是要求登录界面能够显示这个自定义提示语的,在业务设计中就存在两个模块间的强关联性。这时候,URL Router和Protocol Router没有任何区别,包括下面将要提到的Target-Action路由方式,都存在耦合,但是Protocol Router通过简单地改善,是可以把这部分耦合去除的。

URL Router:

[URLRouter openURL:@"login" userInfo:@{@"message":@"请登录查看笔记详情"}];

Protocol Router:

@protocol LoginViewInput <NSObject>
@property (nonatomic, copy) NSString *message;
@end

//获取登录界面进行设置
UIViewController<LoginViewInput> *loginViewController = [ProtocolRouter destinationForProtocol:@protocol(LoginViewInput)];
loginViewController.message = @"请登录查看笔记详情";

由于字典传参的原因,URL Router只不过是把这种接口上的关联隐藏到了字典key里,它在参数字典里使用@"message"时,就是在隐式地使用LoginViewInput的接口。

这种业务设计上导致的模块之间互相关联是不可避免的,也是不需要去隐藏的。隐藏了反而会引来麻烦。如果登录界面的属性名字变了,从NSString *message改成了NSString *notifyString,那么URL Router在register的时候也必须修改传参时的代码。如果register是由登录界面自己执行和处理的,而不是由App Context来处理的,那么此时参数key是固定为@"notifyString"的,那就会要求所有调用者的传参key也修改为notifyString,这种修改如果缺少编译器的帮助会很危险,目前是用宏来减少这种修改导致的工作量。而Protocol Router在修改时就能充分利用编译器进行检查,能够保证100%安全。

因此,URL Router并不能做到解耦,只是隐藏了接口关联而已。一旦遇到了需要修改或者重构的情况,麻烦就出现了,在替换宏的时候,你还必须仔细检查有没有哪里有直接使用字符串的key。只是简单地修改名字还是可控的,如果是需要增加参数呢?这时候就根本无法检查哪里遗漏了参数传递了。这就是字典传参的坏处。

关于这部分的讨论,也可以参考Peak大佬的文章:iOS组件化方案。

Protocol Router在这种情况下也需要作出修改,但是它能帮助你安全高效地进行重构。而且只要稍加改进,也可以完全无需修改。解决方法就是把Protocol分离为Required Interface和Provided Interface。

Required Interface 和 Provided Interface

模块的接口其实是有Required Interface和Provided Interface的区别的。Required Interface就是调用者需要用到的接口,Provided Interface就是实际的被调用者提供的接口。

在UML的组件图中,就很明确地表现出了这两者的概念。下图中的半圆就是Required Interface,框外的圆圈就是Provided Interface:

组件图

那么如何实施Required Interface和Provided Interface?上一篇文章里已经讨论过,应该由App Context在一个adapter里进行接口适配,从而使得调用者可以继续在内部使用Required Interface,adapter负责把Required Interface和修改后的Provided Interface进行适配。

示例代码:

@protocol ModuleARequiredLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *message;
@end

//Module A中的调用代码
UIViewController<ModuleARequiredLoginViewInput> *loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination];
loginViewController.message = @"请登录查看笔记详情";
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
//App Context 中的 Adapter,用Objective-C的category或者Swift的extension进行接口适配
@interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput>
@property (nonatomic, copy) NSString *message;
@end
@implementation LoginViewController (ModuleAAdapte)
- (void)setMessage:(NSString *)message {
	self.notifyString = message;
}
- (NSString *)message {
	return self.notifyString;
}
@end

用category、extension、NSProxy等技术兼容新旧接口,工作全部由模块的使用和装配者App Context完成。如果LoginViewController已经有了自己的message属性,这时候就说明新的登录模块是不可兼容的,必须有某一方做出修改。当然,接口适配能做的事情是有限的,例如一个接口从同步变成了异步,那么这时候两个模块也是不能兼容的。

因此,如果模块需要进行解耦,那么它的接口在设计的时候就应该十分仔细,尽量不要在参数中引入太多其他的模块依赖。

只有存在Required Interface和Provided Interface概念的设计,才能做到彻底的解耦。目前的路由方案都缺失了这一部分。

Target-Action

CTMediator的方案,把对模块的调用封装到Target-Action中,利用了Objective-C的runtime特性,省略了Target-Action的注册和绑定工作,直接通过CTMediator中介者调用目的模块的方法。

@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativFetchDetailViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去之后,可以由外界选择是push还是present
        return viewController;
    } else {
        // 这里处理异常场景,具体如何处理取决于产品
        return [[UIViewController alloc] init];
    }
}
@end

-performTarget:action:params:shouldCacheTarget:方法通过NSClassFromString,获取目的模块提供的Target类,再调用Target提供的Action,实现了方法调用:

@implementation CTMediator
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    Class targetClass;
    
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }
    
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 有可能target是Swift对象
        actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
        action = NSSelectorFromString(actionString);
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else {
                // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
                [self.cachedTarget removeObjectForKey:targetClassString];
                return nil;
            }
        }
    }
}
@end
优点
  • 实现简洁,整个实现的代码量很少
  • 省略了路由注册的步骤,可以减少一部分内存消耗和时间消耗,但是也略微降低了调用时的性能
  • 使用场景不局限于界面模块,所有模块都可以通过中介者调用
缺点
  • 在调用action时使用字典传参,无法保证类型安全,维护困难
  • 直接使用runtime互相调用,难以明确地区分Required Interface和Provided Interface,因此其实无法实现完全解耦。和URL Router一样,在目的模块变化时,调用模块也必须做出修改
  • 过于依赖runtime特性,和Swift的类型安全设计是不兼容的,也无法跨平台多端实现

UIStoryboardSegue

苹果的storyboard其实也有一套路由API,只不过它的局限性很大。在这里简单介绍一下:

@implementation SourceViewController

- (void)showLoginViewController {
	//调用在storyboard中定义好的segue identifier
	[self performSegueWithIdentifier:@"presentLoginViewController" sender:nil];
}

//perform segue时的回调
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(nullable id)sender {
    return YES;
}

//配置目的界面
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    //用[segue destinationViewController]获取目的界面,再对目的界面进行传参
}
@end

UIStoryboardSegue是和storyboard一起使用的,storyboard中定义好了一些界面跳转的参数,比如转场方式(push、present等),在执行路由前,执行路由的UIViewController会收到回调,让调用者配置目的界面的参数。

在storyboard中连接segue,其实是跳转到一个已经配置好界面的view controller,也就是和View相关的参数,都已经做好了依赖注入。但是自定义的依赖,却还是需要在代码中注入,所以又给了我们一个-prepareForSegue:sender:回调。

我不建议使用segue,因为它会导致强耦合。但是我们可以借鉴UIStoryboardSegue的sourceViewController、destinationViewController、封装跳转逻辑到segue子类、对页面执行依赖注入等设计。

总结

总结了几个路由工具之后,我的结论是:路由的选择还是以业务需求为先。当对动态性要求极高、或者需要多平台统一路由,则选择URL Router,其他情况下,我更倾向于使用Protocol Router。和Peak大大的结论一致。

Protocol Router目前并没有一个成熟的开源方案。因此我造了个轮子,增加了上面提到的一些需求。

ZIKRouter的特性

离散式管理

每个模块都对应一个或者多个router子类,在子类中管理各自的路由过程,包括对象的生成、模块的初始化、路由状态管理、AOP等。路由时,需要使用对应的router子类,而不是一个单例router掌管所有的路由。如果想要避免引用子类带来的耦合,可以用protocol动态获取router子类,或者用父类+泛型在调用者中代替子类。

采用离散式的设计的原因是想让各个模块对路由拥有充分的控制权。

一个router子类的简单实现如下:

@interface ZIKLoginViewRouter : ZIKViewRouter
@end

@implementation ZIKLoginViewRouter
//app启动时,注册对应的模块和Router
//不使用+load和+initialize方法,因为在Swift中已经不适用
+ (void)registerRoutableDestination {
    [self registerView:[ZIKLoginViewController class]];
    [self registerViewProtocol:ZIKRoutableProtocol(ZIKLoginViewProtocol)];
}
//执行路由时,返回对应的viewController或者UIView
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    ZIKLoginViewController *destination = [sb instantiateViewControllerWithIdentifier:@"ZIKLoginViewController"];
    return destination;
}
//检查来自storyboard的界面是否需要让外界进行配置
+ (BOOL)destinationPrepared:(UIViewController<ZIKLoginViewProtocol> *)destination {
    if (destination.loginService != nil) {
        return YES;
    }
    return NO;
}
//初始化工作
- (void)prepareDestination:(UIViewController<ZIKLoginViewProtocol> *)destination configuration:(__kindof ZIKViewRouteConfiguration *)configuration {
    if (destination.loginService == nil) {
        //ZIKLoginService也可以用ZIKServiceRouter动态获取
        destination.loginService = [ZIKLoginService new];
    }
}
//路由时的AOP回调
+ (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
@end

你甚至可以在不同情况下返回不同的destination,而调用者对此完全不知情。例如一个alertViewRouter为了兼容UIAlertView和UIAlertController,可以在router内部,iOS8以上使用UIAlertController,iOS8以下则使用UIAlertView。

一切路由的控制都在router类内部,不需要模块做出任何额外的修改。

自由定义路由参数

路由的配置信息都存储在configuration里,在调用者执行路由的时候传入。基本的跳转方法如下:

 //跳转到Login界面
 [ZIKLoginViewRouter
     performFromSource:self //界面跳转时的源界面
     configuring:^(ZIKViewRouteConfiguration *config) {
         //跳转类型,支持push、presentModally、presentAsPopover、performSegue、show、showDetail、addChild、addSubview、custom、getDestination
         config.routeType = ZIKViewRouteTypePush;
         config.animated = NO;
         config.prepareDestination = ^(id<ZIKLoginViewProtocol> destination) {
             //跳转前配置界面
         };
         config.routeCompletion = ^(id<NoteEditorProtocol> destination) {
	         //跳转成功并结束处理
	      };
	      config.performerErrorHandler = ^(ZIKRouteAction routeAction, NSError * error) {
	         //跳转失败处理,有失败的详细信息
	      };
 }];

Configuration只能在初始化block里配置,出了block以后就无法修改。你也可以用一个configuration子类添加更多自定义信息。

如果不需要复杂的配置,也可以只用最简单的跳转:

[ZIKLoginViewRouter performFromSource:self routeType:ZIKViewRouteTypePush];

移除已执行的路由

你可以先初始化一个router,再交给其他角色执行路由:

//初始化router
self.loginRouter = [[ZIKLoginViewRouter alloc] initWithConfiguring:^(ZIKViewRouteConfiguration * _Nonnull config) {
                               config.source = self;
                               config.routeType = ZIKViewRouteTypePush;
                           }];
                           
//执行路由
if ([self.loginRouter canPerform] == NO) {
    NSLog(@"此时无法执行路由:%@",self.loginRouter);
    return;
}
[self.loginRouter performRouteWithSuccessHandler:^{
    NSLog(@"performer: push success");
} performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) {
    NSLog(@"performer: push failed: %@",error);
}];

当你需要消除已经展示的界面,或者销毁一个模块时,可以调用移除路由方法一键移除:

if ([self.loginRouter canRemove] == NO) {
    NSLog(@"此时无法移除路由:%@", self.loginRouter);
    return;
}
[self.loginRouter removeRouteWithSuccessHandler:^{
    NSLog(@"performer: pop success");
} performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) {
    NSLog(@"performer: pop failed,error:%@",error);
}];

从而无需再区分调用pop、dismiss、removeFromParentViewController、removeFromSuperview等方法。

通过protocol获取对应模块

Protocol作为匹配标识

我们不想让外部引用ZIKLoginViewRouter头文件导致耦合,调用者只需要获取一个符合了ZIKLoginViewProtocol的view controller,因此只需要根据ZIKLoginViewProtocol获取到对应的router子类,然后在子类上调用父类ZIKViewRouter提供的路由方法即可,这样就可以做到隐藏子类。

使用ZIKViewRouterToView和ZIKViewRouterToModule宏,即可通过protocol获取到对应的router子类,并且子类返回的destination必定符合ZIKLoginViewProtocol:

[ZIKViewRouterToView(ZIKLoginViewProtocol)
    performFromSource:self
    configuring:^(ZIKViewRouteConfiguration *config) {
         config.routeType = ZIKViewRouteTypePush;
         config.prepareDestination = ^(id<ZIKLoginViewProtocol> destination) {
             //跳转前配置界面
         };
         config.routeCompletion = ^(id<ZIKLoginViewProtocol> destination) {
	         //跳转成功并结束处理
	      };
 }];

这时候ZIKLoginViewProtocol就相当于LoginView模块的唯一identifier,不能再用到其他view controller上。你可以用多个protocol注册同一个router,用于区分requiredProtocol和providedProtocol。

多对一匹配

有时候,一些第三方的模块或者系统模块并没有提供自己的router,你可以为其封装一个router,此时可以有多个不同的router管理同一个UIViewController或者UIView类。这些router可能提供了不同的功能,比如同样是alertRouter,routerA可能是用于封装UIAlertController,routerB可能是用于兼容UIAlertView和UIAlertController,这时候要如何区分并获取两个不同的router?

像这种提供了独特功能的router,需要你使用configuration的子类,然后让子类conform对应功能的protocol。于是就可以根据configuration的protocol来获取对应的router:

[ZIKViewRouterToModule(ZIKCompatibleAlertConfigProtocol)
    performFromSource:self
    configuring:^(ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> * _Nonnull config) {
 	config.routeType = ZIKViewRouteTypeCustom;
 	config.title = @"Compatible Alert";
 	config.message = @"Test custom route for alert with UIAlertView and UIAlertController";
 	[config addCancelButtonTitle:@"Cancel" handler:^{
	 	NSLog(@"Tap cancel alert");
 	}];
 	[config addOtherButtonTitle:@"Hello" handler:^{
	 	NSLog(@"Tap hello button");
 	}];
 	config.routeCompletion = ^(id _Nonnull destination) {
	 	NSLog(@"show custom alert complete");
 	};
}];

如果模块自己提供了router,并且这个router用于依赖注入,不能被其他router替代,可以声明本router为本模块的唯一指定router,当有多个router尝试管理此模块时,启动时就会产生断言错误。

依赖注入和依赖声明

固定依赖和运行时依赖

模块的依赖分为固定依赖和运行时参数依赖。

固定依赖就类似于VIPER各角色之间的依赖关系,是一个模块中固定不变的依赖。这种依赖只需要在router内部的-prepareDestination:configuration:固定配置即可。

运行时依赖就是外部传入的参数,由configuration负责传递,然后同样是在-prepareDestination:configuration:中,获取configuration并配置destination。你可以用一个configuration子类和router配对,这样就能添加更多自定义信息。

如果依赖参数很简单,也可以让router直接对destination进行配置,声明router的destination遵守ZIKLoginViewProtocol,让调用者在prepareDestination里设置destination。但是如果依赖涉及到了model对象的传递,并且由于需要隔离View和Model,destination不能接触到这些model对象,这时候还是需要让configuration传递依赖,在router内部再把model传给负责管理model的角色。

因此,configuration和destination的protocol就负责依赖声明和暴露接口。调用者只需要传入protocol里要求的参数或者调用一些初始化方法即可,至于router内部怎么使用和配置这些依赖,调用者就不用关心了。

直接在头文件中声明

声明一个protocol是一个router的config protocol或者view protocol时,只需要让这个protocol继承自ZIKViewConfigRoutable或者ZIKViewRoutable即可。这样,所有的依赖声明都可以在头文件里明确表示,不必再从文档中查找。

使用泛型指明特定router

一个模块可以直接在内部用ZIKViewRouterToModule和ZIKViewRouterToView动态获取router,也可以在头文件里添加一个router属性,让builder注入。

那么一个模块怎么向builder声明自己需要某个特定功能的router呢?答案是父类+泛型。

ZIKRouter支持用泛型指定参数类型。在OC中可以这样使用:

//注意这个示例代码只是用于演示泛型的意思,实际运行时必须要用一个ZIKViewRouter子类才可以
[ZIKViewRouter<UIViewController *,ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *>
  performFromSource:self
  configuring:^(ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *config) {
    config.routeType = ZIKViewRouteTypePerformSegue;
    config.configureSegue(^(ZIKViewRouteSegueConfiguration *segueConfig) {
    	segueConfig.identifier = @"showLoginViewController";
    );
}];

ZIKViewRouter<UIViewController *, ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *>就是一个指定了泛型的类,尖括号中指定了router的destination和configuration类型。这一串说明相当于是在声明:这是一个destination为UIViewController类型,用ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *作为执行路由时的configuration的router类。你可以对configuration再添加protocol,表明这个configuration必须遵守指定的protocol。

这时你就可以用父类+泛型来声明一个router类,这个router类的configuration符合特定的config protocol。而且在写的时候Xcode会给你自动补全。这是一种很好的隐藏子类的方式,而且符合原生的语法。

但是由于OC中的类都是Class类型,因此只能这样声明一个实例属性:

@property (nonatomic, strong) ZIKViewRouter<UIViewController *,ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *> *loginViewRouter;

Builder只能注入一个router实例,而不是一个router class。因此在OC里一般不这么使用。

但是在Swift这种类型安全语言中这种模式就能更好地发挥作用了,你可以直接注入一个符合某个泛型的router:

//在Builder中注入alertRouter
swiftSampleViewController.alertRouter = Router.to(RoutableViewModule<ZIKCompatibleAlertConfigProtocol>())
class SwiftSampleViewController: UIViewController {    
    //在Builder里注入alertRouterClass后,就可以直接用这个routerClass执行路由
    var alertRouter: ViewRouter<Any, ZIKCompatibleAlertConfigProtocol>!
    
    @IBAction func testInjectedRouter(_ sender: Any) {
        self.alertRouter.perform(
            from: self,
            configuring: { (config, prepareDestination, prepareModule) in
            prepareModule({ moduleConfig in
                //moduleConfig在类型推断时就是ZIKCompatibleAlertConfigProtocol,无需在判断后再强制转换
                moduleConfig.title = "Compatible Alert"
                moduleConfig.message = "Test custom route for alert with UIAlertView and UIAlertController"
                moduleConfig.addCancelButtonTitle("Cancel", handler: {
                print("Tap cancel alert")
                })
                moduleConfig.addOtherButtonTitle("Hello", handler: {
                    print("Tap Hello alert")
                })
            })
        }
    }
}

声明了ViewRouter<Any, ZIKCompatibleAlertConfigProtocol>的属性后,外部就可以直接注入一个对应的router。可以用这种设计模式来转移、集中获取router的职责。

Router可以在定义的时候限制自己的泛型:

Objective-C:

@interface ZIKCompatibleAlertViewRouter : ZIKViewRouter<UIViewController *, ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> *>

@end

Swift:

class ZIKCompatibleAlertViewRouter: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol> {

}

这样在传递的时候,就可以让编译器检查router是否正确。

调用安全和类型安全

上面的演示已经展示了类型安全的处理,由protocol和泛型共同完成了这个类型安全的设计。不过有一些问题还需要特别注意。

编译检查

使用ZIKViewRouterToModule和ZIKViewRouterToView时,会对传入的protocol进行编译检查。保证传入的protocol是可路由的protocol,不能随意滥用。具体用到的方式有些复杂,而且在Objective-C和Swift上使用了两种方式来实现编译检查,具体实现可以看源代码。

泛型的协变和逆变

Swift的自定义泛型不支持协变,所以使用起来有点奇怪。

let alertRouterClass: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration>.Type
 
 //编译错误
 //ZIKCompatibleAlertViewRouter.Type is ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol>.Type
 alertRouterClass = ZIKCompatibleAlertViewRouter.self

Swift的自定义泛型不支持子类型转为父类型,因此把ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol>.Type赋值给ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration>.Type类型时就会出现编译错误。奇怪的是反过来逆变反而没有编译错误。而Swift原生的集合类型是支持协变的。从2015年开始就有人提议Swift对自定义泛型加入协变,到现在也没支持。在Objective-C里自定义泛型是可以正常协变的。

因此在swift里,使用了另一个类来包裹真正的router,而这个类是可以随意指定泛型的。

用Adapter兼容接口

可以用不同的protocol获取到相同的router。也就是requiredProtocol和providedProtocol只要有声明,都可以获取到同一个router。

首先检查requiredProtocol和providedProtocol,确定两个接口提供的功能是一致的。否则无法兼容。

为Provided模块添加Required Interface

requiredProtocol是外部的要求目的模块额外兼容的,由App Context在ZIKViewAdapter的子类里进行接口兼容。

@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end

//Module A中的调用代码
UIViewController<ModuleARequiredLoginViewInput> *loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination];
loginViewController.message = @"请登录查看笔记详情";
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
//ZIKEditorAdapter.h,ZIKViewAdapter子类
@interface ZIKEditorAdapter : ZIKViewRouteAdapter
@end
//ZIKEditorAdapter.m
//用Objective-C的category、Swift的extension进行接口适配
@interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput>
@property (nonatomic, copy) NSString *message;
@end
@implementation LoginViewController (ModuleAAdapte)
- (void)setMessage:(NSString *)message {
	self.notifyString = message;
}
- (NSString *)message {
	return self.notifyString;
}
@end

@implementation ZIKEditorAdapter

+ (void)registerRoutableDestination {
	//注册NoteListRequiredNoteEditorProtocol和ZIKEditorViewRouter匹配
	[ZIKEditorViewRouter registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)];
}

@end
用中介者转发接口

如果遇到protocol里的一些delegate需要兼容:

@protocol ModuleARequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end

@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<ModuleARequiredLoginViewDelegate> delegate;
@end
@protocol LoginViewDelegate <NSObject>
- (void)didLogin;
@end

@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<LoginViewDelegate> delegate;
@end

这种情况在OC里可以hook-setDelegate:方法,用NSProxy来进行消息转发,把LoginViewDelegate的消息转发为对应的ModuleARequiredLoginViewDelegate中的消息。

不过Swift里就不适合这么干了,相同方法有不同参数类型时,可以用一个新的router代替真正的router,在新的router里插入一个中介者,负责转发接口:

@implementation ZIKEditorMediatorViewRouter
+ (void)registerRoutableDestination {
	//注册NoteListRequiredNoteEditorProtocol,和新的ZIKEditorMediatorViewRouter配对,而不是目的模块中的ZIKEditorViewRouter
	//新的ZIKEditorMediatorViewRouter负责调用ZIKEditorViewRouter,插入一个中介者
	[self registerView:/* mediator的类*/];	
	[self registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)];
}
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
   //用ZIKEditorViewRouter获取真正的destination
   id<ProvidedLoginViewInput> realDestination = [ZIKEditorViewRouter makeDestination];
    //获取一个负责转发ProvidedLoginViewInput和ModuleARequiredLoginViewInput的mediator
    id<ModuleARequiredLoginViewInput> mediator = MediatorForDestination(realDestination);
    return mediator;
}
@end

一般来说,并不需要立即把所有的protocol都分离为requiredProtocol和providedProtocol。调用模块和目的模块可以暂时共用protocol,或者只是简单地改个名字,在第一次需要替换模块的时候再对protocol进行分离。

封装UIKit跳转和移除方法

封装iOS的路由方法

ZIKViewRouter把UIKit中路由相关的方法:

  • -pushViewController:animated:
  • -presentViewController:animated:completion:
  • UIPopoverController的-presentPopoverFromRect:inView:permittedArrowDirections:animated:
  • UIPopoverPresentationController的配置
  • -performSegueWithIdentifier:sender:
  • -showViewController:sender:
  • -showDetailViewController:sender:
  • -addChildViewController:
  • -addSubview:

全都统一封装,可以用枚举一键切换:

[ZIKViewRouterToView(ZIKLoginViewProtocol)
    performFromSource:self routeType::ZIKViewRouteTypePush];

对应的枚举值:

  • ZIKViewRouteTypePush
  • ZIKViewRouteTypePresentModally
  • ZIKViewRouteTypePresentAsPopover
  • ZIKViewRouteTypePerformSegue
  • ZIKViewRouteTypeShow
  • ZIKViewRouteTypeShowDetail
  • ZIKViewRouteTypeAddAsChildViewController
  • ZIKViewRouteTypeAddAsSubview
  • ZIKViewRouteTypeCustom
  • ZIKViewRouteTypeGetDestination

移除路由时,也不必再判断不同情况分别调用-popViewControllerAnimated:、-dismissViewControllerAnimated:completion:、-dismissPopoverAnimated:、-removeFromParentViewController、-removeFromSuperview等方法。

ZIKViewRouter会在内部自动调用对应的方法。

识别adaptative类型的路由

-performSegueWithIdentifier:sender:、-showViewController:sender:、-showDetailViewController:sender:这些adaptative的路由方法,系统会根据不同的情况适配UINavigationController和UISplitViewController,选择调用push、present或者其他方式。直接调用时无法明确知道最终调用的是哪个方法,也就无法移除界面。

ZIKViewRouter可以识别这些路由方法在调用后真正执行的路由操作,所以你现在也可以在使用这些方法后移除界面。

支持自定义路由

ZIKViewRouter也支持在子类中提供自定义的路由和移除路由方法。只要写好对应的协议即可。

关于extension里的跳转方法

App extension里还有一些特有的跳转方法,比如Watch扩展里WKInterfaceController的-pushControllerWithName:context:和-popController,Share扩展里SLComposeServiceViewController的-pushConfigurationViewController:和-popConfigurationViewController。

看了一下extension的种类有十几个,懒得一个个去适配了。而且extension里的界面不会特别复杂,不是特别需要路由工具。如果你需要适配extension,可以自己增加,也可以用ZIKViewRouteTypeCustom来适配。

支持storyboard

ZIKViewRouter支持storyboard,这也是和其他Router相比更强的地方。毕竟storyboard有时候也是很好用的,当使用了storyboard的项目中途使用router的时候,总不能为了适配router,把所有使用storyboard的界面都重构吧?

适配storyboard的原理是hook了所有UIViewController的-prepareForSegue:sender:方法,检查destinationViewController是否遵守ZIKRoutableView协议,如果遵守,就说明是一个由router管理的界面,获取注册的对应router类,生成router实例,对其进行依赖注入。如果destination需要传入动态参数,就会调用sourceViewController的-prepareDestinationFromExternal:configuration:方法,让sourceViewController传参。如果有多个router类注册了同一个view controller,则取随机的一个router。

你不需要对现有的模块做任何修改,就可以直接兼容。而且原来view controller中的-prepareForSegue:sender:也能照常使用。

AOP

ZIKViewRouter会在一个界面执行路由和移除路由的时候,对所有注册了此界面的router回调4个方法:

+ (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}

你可以在这些方法中检查界面是否配置正确。也可以用于AOP记录。

例如,你可以为UIViewController这个所有view controller的父类注册一个router,这样就可以监控所有的UIViewController子类的路由事件。

路由错误检查

ZIKRouter会在启动时进行所有router的注册,这样就能检测出router是否有冲突、protocol是否和router正确匹配,保证所有router都能正确工作。当检测到错误时,断言将会失败。

ZIKViewRouter在执行界面路由时,会检测并报告路由时的错误。例如:

  • 使用了错误的protocol执行路由
  • 执行路由时configuration配置错误
  • 不支持的路由方式(router可以限制界面只能使用push、present等有限的跳转方式)
  • 在其他界面的跳转过程中,执行了另一个界面的跳转(unbalanced transition错误,会导致-viewWillAppear:、-viewDidAppear:、-viewWillDisAppear:、-viewDidDisappear:等事件的顺序发生错乱)
  • Source view controller此时的状态无法执行当前路由
  • 路由时container view controller配置错误
  • segue在代理方法中被取消,导致路由未执行
  • 重复执行路由

基本上包含了界面跳转时会发生的大部分错误事件。

支持任意模块

ZIKRouter包含ZIKViewRouter和ZIKServiceRouter。ZIKViewRouter专门用于界面跳转,ZIKServiceRouter则可以添加任意类进行实例获取。

你可以用ZIKServiceRouter管理需要的类,并且ZIKServiceRouter增添了和ZIKViewRouter相同的动态性和泛型支持。

性能

为了错误检查、支持storyboard和注册,ZIKViewRouter和ZIKServiceRouter会在app启动时遍历所有类,进行hook和注册的工作。注册时只是把view class、protocol和router class的地址加入字典,不会对内存有影响。

在release模式下,iPhone6s机型上,测试了5000个UIViewController以及5000个对应的router,遍历所有类并且hook的耗时大约为15ms,注册router的耗时大约为50ms。基本上不会遇到性能问题。

如果你不需要支持storyboard,可以去掉view class和router class配对的注册,去掉以后就无法自动为storyboard里的view controller创建router。至于protocol和router的注册,目前似乎是无法避免的。

项目地址和Demo

简单来说,ZIKRouter就是一个用于模块间路由,基于接口进行模块发现和依赖注入的Router。它以原生的语法执行路由,在OC和Swift中都能使用。

项目地址在:ZIKRouter。里面包含了一个demo,用于演示iOS中大部分的界面路由场景,建议在横屏iPad上运行。

最后记得点个star~

Demo截图,控制台的输出就是界面路由时的AOP回调:

demo

参考


作者:黑超熊猫zuik
链接:https://juejin.im/post/59cb629c5188253cb5016322
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
仿真学习系列(四十二):Sigrity仿真之走线阻抗和耦合检查 在高速电路设计中,走线的阻抗和信号线之间的耦合是影响信号完整性的重要因素。设计中可能会出现多种情况,导致阻抗变化或不匹配,例如线宽的变化、跨层分割等。传统的人工检查方法常常不够高效且易忽视细节。借助仿真软件,可以快速定位问题区域,并进行精确分析,从而有效避免或减少信号反射和串扰带来的影响。 阅读详情

相关推荐

从入门到精通:MediaGo跨平台部署全攻略,Docker+NAS实战案例深度拆解!

本篇将深入解析开源媒体处理工具MediaGo的运作机制,该软件具备跨平台兼容性,支持Windows、MacOS及Linux三大主流操作系统。其核心创新在于采用模块化架构设计,用户无需额外依赖任何插件即可完成快速部署。作为GitHub平台上的开源项目,该工具特别注重用户体验设计,通过Docker容器化方案可无缝接入各类网络存储设备,显著优化系统资源占用并提升数据处理效率。以飞牛云NAS系统为实施案例,本文将详细展示本地化部署全流程。

Yawesh的博客 1954

iOS_Viper架构:提升iOS开发效率的利器

iOS_Viper架构:提升iOS开发效率的利器 项目介绍 在iOS开发领域,设计模式的选择对于项目的可维护性和扩展性至关重要。iOS_Viper项目正是为了解决这一问题而诞生的。Viper(View-Interactor-Presenter-Entity-Router)架构模式虽然在iOS开发中并不流行,但它提供了一种高度解耦的架构设计,能够显著提升大型项目的可维护性和开发效率。 项目技术分析 ...

gitblog_01087的博客 680

Rocky Linux 9 离线环境Docker部署实战:从依赖打包到服务启动

本文提供了一份详尽的Rocky Linux 9离线环境Docker部署实战指南。针对无法访问外网的生产或内网服务器,文章详细讲解了如何在联网跳板机上下载Docker二进制包及所有系统依赖RPM包,并完成打包。随后,手把手指导在离线服务器上进行依赖安装、二进制文件部署、systemd服务配置、SELinux与防火墙调优,最终实现Docker服务的成功启动与验证。

weixin_28363123的博客 138

UML2.0合成结构图简单说明与范例

什么是组件(Component)? 将某一个元素,视为 “整体(whole)” 时,观察该元素的外显行为与内在特征。组件 = 接口 + 实现接口的类别(不一定是只有一个)。组件的观点 外部观点(黑箱式) – 凸显该组件所提供(provide)与所需求(required)的界面(interface)。内部观点(白箱式) – 组件内部结构(internal structure) “组成元素

okman1214的专栏 1375

STM32—时钟树(结合系统时钟函数理解)

时钟树的概念: 我们可以把MCU的运行比作人体的运行一样,人最重要的是什么?是心跳! 心脏的周期性收缩将血液泵向身体各处。心脏对于人体好比时钟对于MCU,微控制器(MCU)的运行要靠周期性的时钟脉冲来驱动,而这个脉冲的始源往往是由外部晶体振荡器提供时钟输入,最终转换为多个外部设备的周期性运作。这种时钟“能量”的传递路径犹如大树的养分由主干流向个分支,因此称为时钟树。 STM32时钟: 在STM32...

肥宅快乐博客 1万+

英飞凌TC3xx之一起认识GTM(八)如何使用DTM实现死区时间配置

本文简要介绍英飞凌TC3xx系列芯片的DTM模块结构,以及有哪些相关寄存器可用于实现相关配置。

无所适从的我的博客 4647

iOS完整项目从需求到实现的全流程深度解析(45)

在项目启动阶段,需要与产品经理、客户等多方沟通,明确项目的核心功能与非功能需求。这一过程中,需要关注以下几个方面: 1. **功能需求**:明确项目需要实现的具体功能,例如用户登录、数据展示、社交分享等。这些需求通常以用户故事(User Story)的形式呈现,例如:"作为用户,我希望能够查看我的订单历史"。

Android开发领域创作者 1163

化工设计软件:AVEVA E3D二次开发_(8).管道系统设计

在 AVEVA E3D Design 中,管道规格的定义是确保设计一致性和标准化的关键。通过二次开发,可以自动生成并管理管道规格。下面是一个示例,展示如何通过编写脚本来定义和管理管道规格。# 导入必要的库 import clr。

kkchenjj的博客 971

iOS VIPER架构实践(二):VIPER详解与实现

第一篇文章对VIPER进行了简单的介绍,这篇文章将从VIPER的源头开始,比较现有的几种VIPER实现,对VIPER进行进一步的职责剖析,并对各种细节实现问题进行挖掘和探讨。最后给出两个完整的VIPER实现,并且提供快速生成VIPER代码的模板。 Demo和轮子的github地址是:ZIKViper,路由工具:ZIKRouter。有用请点个star~ 两个实现展示了以下问题的解决方案:

6511

Jetson Nano 实战指南:深度学习环境配置与 YOLOv5 边缘端部署解析

Jetson Nano是NVIDIA推出的嵌入式AI开发板,凭借其强大的GPU计算能力和完善的软件生态系统,成为边缘AI部署的理想平台。本教程源于作者团队参加2024中国机器人大赛暨RoboCup机器人世界杯中国赛机器人先进视觉3D识别赛道的实战经验,是一份经过验证的环境部署指南。

FJN110的博客 152

全球建筑物提取数据集(免费下载):微软/GlobalMLBuildingFootprints

这个是GitHub上的开源数据集: https://github.com/microsoft/GlobalMLBuildingFootprints 介绍 必应地图正在世界各地发布开放式建筑足迹。我们从 2014 年到 2021 年间的 Bing Maps 图像中检测到了 7.77 亿座建筑物,包括 Maxar 和 Airbus 的图像。数据可在 ODbL 下免费下载和使用。该数据集补充了我们的其他版本。 包括的地区 执照 此数据由 Microsoft 根据Open Dat..

此星光明博客 6289

iOS VIPER架构实践(一):从MVC到MVVM到VIPER

简介最近半年在写app的时候,研究了一下各种iOS代码架构,最后选择了VIPER进行实践,在此对实践中遇到的各种设计问题做一番总结,并分享造出的轮子。VIPER演示demo:ZIKViper,模块路由工具地址:ZIKRouter。对代码风格和架构有兴趣的同学,肯定都已经在很多地方见过各种架构的介绍。MVC、MVP、MVVM、VIPER,细分程度逐渐上升。这些架构设计大部分都是来自MVC,只是各自用

1758

Dify智能体开发:发布嵌入网站

Dify 支持将你的 AI 应用嵌入到业务网站中,你可以使用该能力在几分钟内制作具有业务数据的官网 AI 客服、业务知识问答等应用。点击 WebApp 卡片上的嵌入按钮,复制嵌入代码,粘贴到你网站的目标位置。

王国平的博客 3243

iOS VIPER架构简析

在建筑学领域,有这样一句话:我们虽然在营造建筑,但建筑也在重新塑造我们。作为一名开发人员,如果你曾认真的去做过架构相关的东西,对这句话必然有着深刻体会。一个好的架构,不仅可以让一款产品成功的投入使用,还可以让产品具有良好的可维护性、可扩展性。这篇文章,主要介绍一种被称为VIPER的架构,并基于swift的特性去做相应的设计。 VIPER是视图(View)、交互器(Interactor)、展示器(...

buyu03 2701

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

iOS VIPER 架构解读

苹果官方推荐的 APP 架构是 MVC 架构,实际上它并不是传统的三层架构,而是两层架构,即整个 APP 由“模型”和“视图控制器”两层构成。因此视图控制器显得尤其重要,它的重要体现在两个方面: ViewController 是 iOS APP 中最重要的“基石”,没有 ViewController 就没有 APP——iOS 9 以后,要求 UIWindow 必须要有一个 rootViewCon...

kmyhy的专栏 3399

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

iOS设计模式:MVC、MVVM与VIPER架构对比

在iOS开发过程中,合理的架构设计对于提高代码的可维护性、可扩展性和可测试性至关重要。MVC、MVVM和VIPER是iOS开发中常用的三种架构模式,每种模式都有其独特的优势和适用场景。本文的目的是对这三种架构模式进行全面的对比分析,帮助开发者了解它们的特点、原理和应用场景,以便在实际项目中做出更合适的架构选择。本文的范围涵盖了这三种架构模式的核心概念、实现原理、代码示例、实际应用场景以及相关的工具和资源推荐等方面。背景介绍:介绍本文的目的、预期读者和文档结构概述,并对相关术语进行定义和解释。

操作系统内核探秘的博客 1317

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

VIPER架构学习

VIPER架构学习探索编程准则资源下载什么是VIPER功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 编程准则 代码...

木头书屋专栏 4937

iOS Viper架构实战教程

iOS Viper架构实战教程 1. 项目介绍 iOS_Viper 是一个基于VIPER架构的iOS项目实例。VIPER(View-Interactor-Presenter-Entity-Router)架构是一种设计模式,专为简化大型iOS项目开发而生。它强调通过清晰的责任分配,增强代码的可维护性和可测试性。本项目展示了如何在实际应用中部署VIPER架构,它通过模块化的各层(视图、交互器、呈现器、...

gitblog_00881的博客 640

iOS Viper架构实战教程:基于caojunhua/iOS_Viper

iOS Viper架构实战教程:基于caojunhua/iOS_Viper 本教程将引导您深入了解并实践一个典型的iOS Viper架构项目,该项目托管在GitHub,旨在教授您如何理解和运用Viper模式在您的iOS开发中。 1. 目录结构及介绍 iOS_Viper项目采用了精心设计的目录结构来体现Viper架构的各组件。以下是关键的目录组成部分: Assets: 包含项目所需的任何静态资源。...

gitblog_01174的博客 1394

ZIKRouter与VIPER架构:构建清晰分层的iOS应用案例分析

ZIKRouter是一款面向接口的路由框架,专为Objective-C和Swift设计,通过协议实现模块发现与依赖注入。作为VIPER架构的理想伴侣,它能帮助开发者构建高度解耦、易于维护的iOS应用。本文将深入探讨如何将ZIKRouter与VIPER架构结合,通过实际案例展示其在模块通信和依赖管理中的强大能力。 ## 为什么选择ZIKRouter搭配VIPER架构? VIPER架构通过将应用拆

gitblog_00282的博客 624

推荐iOS开发中的Viper架构实现:MindorksOpenSource/iOS-Viper-Architecture

是一个由[Mindorks OpenSource](https://mindorks.com/)团队维护的开源库,它为iOS应用开发提供了一种名为VIPER(View-Interactor-Presenter-Entity-Router)的架构模式实现。Viper是一种模块化、低耦合的开发方式,有助于提高代码的可读性和可维护性。 ## 技术分析 ### VIPER架构 在Viper架构中:

gitblog_00013的博客 656

Cleer Arc5耳机VIPER架构iOS项目适配

本文深入探讨Cleer Arc5开放式耳机基于自研VIPER架构在iOS系统中的适配方案,涵盖AVFoundation音频控制、CoreBluetooth指令通信、音频中断处理等关键技术,实现低延迟、高保真与智能交互的融合体验。

weixin_35755562的博客 865

iOS VIPER 框架的应用实践

MVC 的 "不实" 谈起iOS框架,我们会下意识的谈起MVC,这个经典框架。但是仔细想想我们并没有做到真正的MVC 的低耦合。 它们之间不断的混用,导致我们的代码不断的增加混乱一直调理不清。我们习惯在model上面写上属性,View上面只是视图控件放在上面。所有的交互基本全部放controller上面,这样会导致我们控制器代码量大大增加,并且逻辑混乱, 难以维护。 MVP MVP 架构拥有三个真正独立的分层,所以在组装的时候会有一些问题,而 MVP 也成了第一个披露了这种问题的架构。因为我们不想让

gcs的博客 513

iOS设计模式实践:MVC、MVVM与VIPER对比

在iOS开发中,选择合适的设计模式对于提高代码的可维护性、可扩展性和可测试性至关重要。MVC(Model-View-Controller)、MVVM(Model-View-ViewModel)和VIPER(View-Interactor-Presenter-Entity-Router)是iOS开发中常用的三种设计模式。本文的目的是对这三种设计模式进行详细的对比分析,包括它们的原理、优缺点、适用场景等,帮助开发者根据项目的需求选择最合适的设计模式。

操作系统内核探秘的博客 640

如何使用VIPER构建iOS应用

用VIPER构建iOS应用 为避免撕逼,提前声明:本文纯属翻译,仅仅是为了学习,加上水平有限,见谅! 【原文】https://www.objc.io/issues/13-architecture/singletons/ 用VIPER构建iOS应用 ——by Jeff Gilbert and Conrad Stoll 众所周知,在建筑领域,我们塑造我们的建筑,随后我们的建筑也塑造我们。正如程序员最终知道那样,这也适用于构建软件。 设计我们的代码很重要,这样每一个片段都很容易识别,

iOSjack的博客 621
上一篇: iOS多线程:GCD总结
下一篇: iOS 通俗易懂的HTTP网络
LIN1986LIN
博客等级 码龄19年 17粉丝 93原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值