iOS底层实现九宫格菜单可配置、页面跳转不需要#import &
一、需求:
1、每新跳一个页面都需要导入其文件,导致头文件很多,先要删除这些头文件,还能实现页面跳转
2、项目后期做组件化,涉及到跨业务组件的话,需要解耦
3、在做个性化定制时,可根据后台返回,实现可配置(例如九宫格菜单可配置)。
当然,我先给出代码,附带案例, 框架(WGControllerPush)可直接拖到项目中使用
项目中常会有下图情况出现,现在做到去除这些都文件,依然可以做跳转:
二、实现步骤:
1、页面传值
页面跳转首先会遇到传值问题,大体分为四类:
a、不需要传值
b、只通过属性传值
例如:
@property (nonatomic, copy) NSString *name;
c、重写了init方法
例如:
- (instancetype)initWithDic:(NSDictionary *)dic model:(WGModel *)model school:(NSString *)school;
d、既有属性传值,重写了init来传值
例如:
@property (nonatomic, assign) BOOL isMale;
- (instancetype)initWithDic:(NSDictionary *)dic name:(NSString *) name;
2、根据上面传值分类,首先写一个传值类型的枚举
#pragma mark -传值类型
typedef NS_ENUM(NSInteger , WGPushControllerType) {
WGPushNoParam = 0, //不需要传值
WGPushProperty = 1, //只有属性传值
WGPushInit = 2, //重写了init方法传值
WGPushOther = 3, //其他,混合方式传值(既有属性又有initWith)等
};
3、因为没有导入头文件,所以不能再如下图这样
4、怎么办呢?
举个例子:
Person *per = [[Person alloc] init];
可以拆分为
Person *per = [Person alloc];
[per init];
(1)在知道一个类的字符串名字时,可以转为Class的
就是这句:
Class classCon = NSClassFromString(toCon);
(2)有这个类的Class,是不是就可以alloc init了
PS:要用id类型接收
id con = [[classCon alloc] init];
(3)初始化完成,然后判断是否需要传值:根据上面的枚举
//根据HuPushControllerType判断是否有值传到下一页
switch (type) {
case WGPushNoParam: {
//不需要传值
} break;
case WGPushProperty: {
//只属性传值
[self getToConFromProperty:paramDic toCon:con];
} break;
case WGPushInit: {
//只通过重写init传值
con = [self getToConFromInit:paramDic classCon:classCon];
} break;
case WGPushOther: {
//既有属性传值,还重写init传值
con = [self getToConFromInit:paramDic classCon:classCon];
[self getToConFromProperty:paramDic toCon:con];
} break;
default:
break;
}
(4)重点来了,要传值咯
1、属性传值
我这里简单说下思路:
1、把需要通过属性传值的值和属性名字放到一个字典中,值为字典的value,属性的字符串名字为字典的key
PS:这个字典再用一个写死的key(自己定义好一个宏就可以,我是写了一个@"property")拼成一个大的字典, 来让程序通过这个key拿到需要属性传值的字典
例如:
@property (nonatomic, copy) NSString *name;
组装成字典为:
//存储值的字典
NSDictionary *ParamDic = @{@"name": @"小明"};
//外层字典
NSDictionary *dic = @{@"property":ParamDic};
2、根据@"property"关键字,获取存值的字典,然后获取字典的所有key数组
3、遍历key数组。根据key的字符串生成对应属性的set方法
4、最后通过objc_msgSend完成赋值
:代码:
//属性传值
- (void)getToConFromProperty:(NSDictionary *)paramDic toCon:(id)toCon {
//需要属性传值,则通过运行时来解决
if (!toCon) {
return;
}
NSDictionary *propertyDic = [paramDic valueForKey:WGProperty];
NSArray *keyArr = [propertyDic allKeys];
for (int i = 0; i < keyArr.count; i++) {
NSString *key = [keyArr objectAtIndex:i];
id value = [propertyDic valueForKey:key];
//把key的首字母大写
NSString *firstStr = [key substringWithRange:NSMakeRange(0, 1)].uppercaseString;
NSString *restStr = [key substringFromIndex:1];
//生成对应属性的set方法
NSString *selName = [NSString stringWithFormat:@"set%@%@:", firstStr, restStr];
SEL method = NSSelectorFromString(selName);
if ([toCon respondsToSelector:method]) {
//等价于controller.shuxing = value;
//如果是数字则在字典中是NSNumber类型,需要把NSNumber类型转为NSInteger或者CGFloat
if ([value isKindOfClass:[NSNumber class]]) {
NSString *vale = [(NSNumber *) value stringValue];
if ([vale containsString:@"."]) {
CGFloat val = [vale doubleValue];
void (*action)(id, SEL, CGFloat) = (void (*)(id, SEL, CGFloat)) objc_msgSend;
action(toCon, method, val);
}else{
NSInteger val = [(NSNumber *) value integerValue];
void (*action)(id, SEL, NSInteger) = (void (*)(id, SEL, NSInteger)) objc_msgSend;
action(toCon, method, val);
}
} else {
void (*action)(id, SEL, id) = (void (*)(id, SEL, id)) objc_msgSend;
action(toCon, method, value);
}
}
}
}
2、重写init方法传值
我这里简单说下思路:
1、根据重写的init方法名字作为key,每一个要传的值按对应顺序放到数组中在作为value,放到一个字典中
PS:同上,写死一个key(我是用的@"initWith")拼成一个大的字典
例如:
- (instancetype)initWithAge:(NSInteger)age name:(NSString *) name;
组装称字典:
//存储值得字典
NSDictionary *ParamDic = @{@"initWithAge:name:":@[18, @"小明"]};
//外层字典
NSDictionary *dic = @{@"initWith":ParamDic};
2、根据@"initWith"关键字,获取存值的字典,然后根据字典获取第一个key(其实只有一个key 就是initWithAge:name:)
3、根据key获取一个数组,里面存的是参数的值
4、这时通过objc_msgSend先alloc,然后initwith···
5、要求按顺序放入数组中就是为了在init时值不会错乱
代码:
//init传值,类似于initWithDic
- (id)getToConFromInit:(NSDictionary *)paramDic classCon:(Class)classCon {
id toCon = nil;
NSDictionary *initDic = [paramDic valueForKey:WGInitWith];
NSString *key = [initDic allKeys].firstObject;
//把OC的字符串改成C语言的字符串
const char *ky = [key UTF8String];
NSArray *value = [initDic valueForKey:key];
//这里判断value数组元素个数是否和 key按:分割成数组后的个数相等
if ([key containsString:@":"] && value) {
if ([key componentsSeparatedByString:@":"].count == (value.count + 1)) {
switch (value.count) {
case 1: {
//先alloc
id classAlloc = objc_msgSend(classCon, sel_registerName("alloc"));
//sel_registerName(ky)等价于@selecter(ky)
if ([classAlloc respondsToSelector:sel_registerName(ky)]) {
id paramOne = [value objectAtIndex:0];
if ([paramOne isKindOfClass:[NSNumber class]]) {
NSString *val = [(NSNumber *) paramOne stringValue];
if ([val containsString:@"."]) {
CGFloat vale = [val doubleValue];
id (*action)(id, SEL, CGFloat) = (id(*)(id, SEL, CGFloat)) objc_msgSend;
//等价于[[class alloc] iniWith:]
toCon = action(classAlloc, sel_registerName(ky), vale);
}else{
id (*action)(id, SEL, NSInteger) = (id(*)(id, SEL, NSInteger)) objc_msgSend;
//等价于[[class alloc] iniWith:]
toCon = action(classAlloc, sel_registerName(ky), [val integerValue]);
}
}else{
id (*action)(id, SEL, id) = (id(*)(id, SEL, id)) objc_msgSend;
toCon = action(classAlloc, sel_registerName(ky), paramOne);
}
}
} break;
case 2: {
id classAlloc = objc_msgSend(classCon, sel_registerName("alloc"));
if ([classAlloc respondsToSelector:sel_registerName(ky)]) {
id paramOne = [value objectAtIndex:0];
id paramTwo = [value objectAtIndex:1];
id (*action)(id, SEL, id, id) = (id(*)(id, SEL, id, id)) objc_msgSend;
toCon = action(classAlloc, sel_registerName(ky), paramOne, paramTwo);
}
} break;
case 3: {
id classAlloc = objc_msgSend(classCon, sel_registerName("alloc"));
if ([classAlloc respondsToSelector:sel_registerName(ky)]) {
id (*action)(id, SEL, id, id, id) = (id(*)(id, SEL, id, id, id)) objc_msgSend;
toCon = action(classAlloc, sel_registerName(ky), [value objectAtIndex:0], [value objectAtIndex:1], [value objectAtIndex:2]);
}
} break;
case 4: {
id classAlloc = objc_msgSend(classCon, sel_registerName("alloc"));
if ([classAlloc respondsToSelector:sel_registerName(ky)]) {
id (*action)(id, SEL, id, id, id, id) = (id(*)(id, SEL, id, id, id, id)) objc_msgSend;
toCon = action(classAlloc, sel_registerName(ky), [value objectAtIndex:0], [value objectAtIndex:1], [value objectAtIndex:2], [value objectAtIndex:3]);
}
} break;
default:
break;
}
}
}
return toCon;
}
三:到这里已经完成了前两个需求了,还有一个九宫格菜单可配置
九宫格的demo我也写好了,可下载参考(代码在最上面)
1、把需要可配置的每个类的字符串名字和后台统一,用一个唯一标示pageId代替,例如@"1-1"代表@"HuInformationDetailViewController"
2、客户端本地用字典存储所有这些pageId
例如:
NSDictionary *ParamDic = @{@"1-1":@"FirstViewController",
@"1-2":@"FirstViewController"
};
3、这时后台只需要返给客户端一个pageId,客户端就可以根据这个id知道点击这个菜单按钮时,跳转到哪里了,这样是不是完成了点击功能
4、配置菜单按钮的图标和数量的话,本地给一个默认图标和数量,然后通过后台返回的最新图标和按钮数量来更新,这样就完成了图标的替换,菜单个数更新。
这是我项目中本地部分默认数组:可参考
defaultSkinArr = @[
@{
@"code" : @"1-2",
@"defaultImg" : @"",
@"name" : @"必修",
},
@{
@"code" : @"1-3",
@"defaultImg" : @"educate_share",
@"name" : @"选修",
},
@{
@"code" : @"1-6",
@"defaultImg" : @"peixun",
@"name" : @"分组培训",
}
]