0x1 前言

本文将共享运用Objective-C言语完成一种高可用、易扩展的App设置页。在demo中将会以淘宝、货拉拉的设置页面为例进行叙述。

0x2 剖析

如图所示,剖析比照以下三张图:

怎么规划一个易扩展的设置页
怎么规划一个易扩展的设置页
怎么规划一个易扩展的设置页
1.确认易变部分。
  • 每一行的cell内容与其点击事情归于易变部分,跟从事务需求进行变化
  • 整体设置页面的结构也归于易变部分,但不在本文评论规模。
2.根据页面结构确认数据结构
  • 该页面结构是一个列表页面,所以界说数据源类型为一个数组结构
  • 每个cell按功用大类进行了分区,所以数据源是二维数组结构,便于分区办理
  • 每个cell的款式、功用或许都不共同,所以二维数组中的元素类型界说为id类型
let dataSource = [
[id, id, id],
[id, id],
[id, id, id, id]
]
3.根据已有的UI结构,区分cell的品种。

按三张图中的cell,其实都能够区分为一类cell,可是会提高该cell的复杂度,不易保护与扩展,所以这儿按款式区分,将cell类型分成以下四种:

  • 头像cell(AvatarCell):头像、昵称、描绘、箭头 -> AvatarModel
  • 根底cell(SampleCell):标题、箭头>、箭头文本 -> SampleModel
  • 开关cell(SwitchCell):标题、开关、描绘 -> SwitchModel
  • 退出cell(ExitCell):单文本 -> ExitModel

每种cell对应的数据源根据cell展现所需进行规划,所以对应四种cell模型。

4.抹平cell model类型与cell点击事情。

从前面所述,设置页面的数据源界说为二维数组类型,数组中只能放同一类型的元素目标,且各个cell的数据结构不同。所以规划一个新的模型,携带cell数据与事情函数。

  • responseSelector为cell的点击事情处理函数,因为并不是每个cell都会有cell的点击事情,所以selector或许为nil。
  • model为每品种型cell所需求的数据源
  • cellClass为cell的详细类,与model相对应
@interface SettingModel : NSObject
@property (nonatomic, strong) Class cellClass;
@property (nonatomic, assign, nullable) SEL responseSelector;
@property (nonatomic, strong) id model;
@end

0x3 Coding

1.编写AvatarCell与其model的完成逻辑

该cell的UI元素为以下几点:

  • 头像
  • 昵称
  • 小标题(一般可有可无,所以需求规划成可认为nil)

所以AvatarModel代码界说如下:

@interface AvatarSettingModel: NSObject
@property (nonatomic, copy) NSString *avatar;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy, nullable) NSString *subtitle;
+ (AvatarSettingModel *)title:(NSString *)title avatar:(NSString *)avatar subtitle:(nullable NSString *)subtitle;
@end

界说cell遵从的协议,用来抹平各个cell的差异性

@protocol SettingCellProtocol <NSObject>
- (void)setDataSource:(id)dataSource;
@end

AvatarCell完成初始化代码与协议更新UI逻辑

@interface AvatarSettingCell : UITableViewCell <SettingCellProtocol>
@end
@implementation AvatarSettingCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
	//布局逻辑
	return self;
}
- (void)setDataSource:(id)dataSource {
    NSParameterAssert([dataSource isKindOfClass:AvatarSettingModel.class]);
    AvatarSettingModel *model = dataSource;
    //赋值逻辑
    //...
}
@end
2.根据其他cell页面元素完成其初始化与协议更新UI逻辑(不重复赘述相同操作,此处省掉)
3.将cell、model与设置页面串联起来

将model的界说也声明成协议,抹平不同model之间的差异性,并能够调用model指定的函数

@protocol SettingModelProtocol <NSObject>
- (Class)cellClass;
- (SEL)responseSelector;
- (id)model;
@end

创立tableView与其数据源

@interface SettingViewController ()
@property (nonatomic, copy) NSArray<NSArray<id<SettingModelProtocol>> *> *dataSource;
@end
@implementation SettingViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.tableView.estimatedRowHeight = UITableViewAutomaticDimension;
    self.dataSource = [self getDataSourceFromCode];
}
- (NSArray<NSArray<id<SettingModelProtocol>> *> *)getDataSourceFromCode {
    NSString *subtitle = @"账号名:酱鸭做吗";
    NSString *avatar = @"https://www.6hu.cc/files/2024/03/239795-vknh9V.jpg";
    return @[
        @[[SettingModel avaterCell:@"青藏澳元" avatar:avatar subtitle:subtitle selector:@selector(avatar)],
          [SettingModel sampleCell:@"我的收成地址" selector:@selector(address)],
          [SettingModel sampleCell:@"我的档案" accessory:@"添加档案,获得精准引荐" selector:@selector(myProfile)]],
        @[[SettingModel sampleCell:@"账号与安全" selector:@selector(account)],
          [SettingModel sampleCell:@"付出" accessory:@"付出宝账号、免密付出等" selector:@selector(pay)]],
        @[[SettingModel sampleCell:@"消息告诉" selector:@selector(messageNoti)],
          [SettingModel sampleCell:@"主题换肤" selector:@selector(themeSkinChanging)],
          [SettingModel sampleCell:@"图标切换" accessory:@"默认88/VIP" selector:@selector(changeIcon)],
          [SettingModel sampleCell:@"方法切换" accessory:@"标准/老一辈方法" selector:@selector(modeChange)],
          [SettingModel sampleCell:@"隐私" selector:@selector(privacy)],
          [SettingModel sampleCell:@"通用" accessory: @"区域、音效、辅佐等功用" selector:@selector(currency)]],
        @[[SettingModel sampleCell:@"协助与反应" selector:@selector(help)],
          [SettingModel sampleCell:@"关于淘宝" selector:@selector(about)]],
        @[[SettingModel sampleCell:@"商家入住" selector:@selector(merchantCheckIn)]],
        @[[SettingModel exitCell:@"切换账号" selector:@selector(changeAccount)]],
        @[[SettingModel exitCell:@"退出登录" selector:@selector(logout)]]
    ];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.dataSource.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataSource[section].count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    id<SettingModelProtocol> model = self.dataSource[indexPath.section][indexPath.row];
    NSString *identifier = NSStringFromClass(model.cellClass);
    UITableViewCell<SettingCellProtocol> *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        cell = [[model.cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    [cell setDataSource:model.model];
    return cell;
}
//事情处理
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (self.dataSource.count > indexPath.section && 
        self.dataSource[indexPath.section].count > indexPath.row) {
        id<SettingModelProtocol> model = self.dataSource[indexPath.section][indexPath.row];
        if ([self respondsToSelector:model.responseSelector]) {
        	//声明的函数能够承受/不承受参数,不影响调用
            [self performSelector:model.responseSelector withObject:model.model];
        }
    }
}
#pragma mark - Cell Click
//不承受参数
- (void)avatar {
    NSLog(@"avatar");
}
//也可改成承受参数(二选一,取决于创立模型时传入的@selector(avatar)是否带参数)
- (void)avatar:(id)model {
    NSLog(@"avatar");
}
- (void)address {
    NSLog(@"address");
}
- (void)myProfile {
    NSLog(@"myProfile");
}
//...
@end

代码写到这儿基本上已经完成了设置页面的数据源驱动化,经过传入不同的cell模型与完成事情详细逻辑即可便利的扩展设置页面的cell排序、款式与事情。

0x4 Cell内部事情

关于cell中有其他事情需求处理的部分,能够参照以下完成方法进行处理。

1.声明cell内部事情处理协议
  • cell: 当时响应事情的cell
  • indexPath: cell在tableView中的索引
  • parameters:cell中回调给外面的数据
@protocol SettingCellEventProtocol <NSObject>
@optional
- (void)cell:(UITableViewCell *)cell didSelected:(NSIndexPath *)indexPath parameters:(id)parameters;
@end
2.为完成通用性,给SettingCellProtocol扩大两个可选协议方法,搭配SettingCellEventProtocol协议运用
@protocol SettingCellProtocol <NSObject>
- (void)setDataSource:(id)dataSource;
@optional
- (void)setEventDelegate:(id<SettingCellEventProtocol>)eventDelegate;
- (void)setIndexPath:(NSIndexPath *)indexPath;
@end
3.以SwitchCell为例进行完成cell内部事情处理逻辑,完成协议:
@interface SwitchSettingCell () <SettingCellProtocol>
//...其他属性
//遵从协议
@property (nonatomic, weak) id<SettingCellEventProtocol> eventDelegate;
@property (nonatomic, strong) NSIndexPath *indexPath;
@end
@implementation SwitchSettingCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
	//...初始化代码
    return self;
}
- (void)setDataSource:(id)dataSource {
    NSParameterAssert([dataSource isKindOfClass:SwitchSettingModel.class]);
    SwitchSettingModel *model = dataSource;
    self.switchView.on = model.isOpen;
    //...
}
//switch切换事情回调
- (void)switchChange:(UISwitch *)sender {
    if ([self.eventDelegate respondsToSelector:@selector(cell:didSelected:parameters:)]) {
        [self.eventDelegate cell:self didSelected:self.indexPath parameters:@(sender.on)];
    }
}
@end
4.修正SettingViewController创立cell部分代码,以承受cell内部事情
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	//...其他代码
    if ([cell respondsToSelector:@selector(setEventDelegate:)] &&
        [cell respondsToSelector:@selector(setIndexPath:)]) {
        [cell setIndexPath:indexPath];
        [cell setEventDelegate:self];
    }
    return cell;
}
5.SettingViewController完成cell事情协议处理相关逻辑
#pragma mark - SettingCellEventProtocol
- (void)cell:(UITableViewCell *)cell didSelected:(NSIndexPath *)indexPath parameters:(id)parameters {
    if (self.models.count > indexPath.section &&
        self.models[indexPath.section].count > indexPath.row) {
        NSLog(@"%@:nmodel: %@nchange switch: %d", cell, self.models[indexPath.section][indexPath.row], [parameters boolValue]);
    }
}

在该回调中,能够经过判别cell类型/索引/参数等方法进行区分cell来处理相应事务逻辑。

0x5 MVP

在同一家公司下,有多个不同的app中需求相似的设置页面,如货拉拉的司机端、用户端、企业端等。或者其他app的设置页,如淘宝等。能够经过运用MVP规划方法切换presenter来完成不同事务线的设置页面。

经过构建不同的事务presenter为ViewController供给dataSource和完成cell、cell内部的点击事情。能够到达共用同一套流程代码,经过装备presenter的数据源和修正事情处理来处理不同事务。

1.声明presenter协议

将ViewController中的数据源结构与事情处理、整个设置页的通用装备抽到presenter进行完成。

  • dataSource: 所有cell的数据源
  • separatorInset: cell分割线的缩进
  • title: 设置页面的标题
@protocol SettingPresenterProtocol <SettingCellEventProtocol>
- (NSArray<NSArray<id<SettingModelProtocol>> *> *)dataSource;
- (UIEdgeInsets)separatorInset;
- (NSString *)title;
@end
2.构建淘宝的presenter

@interface TaobaoSettingPresenter : NSObject <SettingPresenterProtocol>
@end
@implementation TaobaoSettingPresenter
- (NSArray<NSArray<id<SettingModelProtocol>> *> *)dataSource {
   return [self getDataSourceFromCode];
}
- (UIEdgeInsets)separatorInset {
    return UIEdgeInsetsMake(0, 12, 0, 0);
}
- (NSString *)title {
    return @"淘宝-设置";
}
#pragma mark - Cell Click
- (void)avatar {
    NSLog(@"avatar");
}
- (void)address {
    NSLog(@"address");
}
- (void)myProfile {
    NSLog(@"myProfile");
}
//...
#pragma mark - SettingCellEventProtocol
- (void)cell:(UITableViewCell *)cell didSelected:(NSIndexPath *)indexPath parameters:(id)parameters {
}
@end
4.修正SettingViewController代码,将数据源获取与事情处理指向presenter
@interface SettingViewController : UITableViewController
@property (nonatomic, strong) id<SettingPresenterProtocol> presenter;
@end
@implementation SettingViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //...
    self.dataSource = self.presenter.dataSource;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    //...
    [cell setEventDelegate:self.presenter];
    return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (self.dataSource.count > indexPath.section && 
        self.dataSource[indexPath.section].count > indexPath.row) {
        id<SettingModelProtocol> model = self.dataSource[indexPath.section][indexPath.row];
        if ([self.presenter respondsToSelector:model.responseSelector]) {
            [self.presenter performSelector:model.responseSelector withObject:model.model];
        }
    }
}
@end
3.构建货拉拉的presenter:用户端、司机端(流程共同,不赘述)

0x6 数据源JSON化

在上述流程中,能够将构建数据源这块硬编码部分改为运用加载json文件的方法进行供给数据给presenter,这样能够经过后端下发装备的方法更灵敏的动态修正设置页面的款式和排序。

因为SettingModel中的model是id类型,与cellClass对应的cell类型对应,是在硬编码部分直接指定的model目标,其类型是从代码中确认的。

而从json中转为model时,也需求将其转为对应的model类型,而非dictionary类型。即硬编码指定model目标类型这步逻辑,需求下沉到json数据文件中,json文件中model.modelClass就是用来指定model目标的类型

1.编写json文件
{
    "data": [
        [
            {
                "cellClass": "AvatarSettingCell",
                "responseSelector": "avatar",
                "model": {
                    "modelClass": "AvatarSettingModel",
                    "title": "青藏澳元",
                    "subtitle": "账号名:酱鸭做吗",
                    "avatar": "https://www.6hu.cc/files/2024/03/239795-vknh9V.jpg"
                }
            },
            {
                "cellClass": "SampleSettingCell",
                "responseSelector": "address",
                "model": {
                    "modelClass": "SampleSettingModel",
                    "title": "我的收成地址",
                    "hideAccessory": false,
                    "accessoryTitle": ""
                }
            },
            {
                "cellClass": "SampleSettingCell",
                "responseSelector": "myProfile",
                "model": {
                    "modelClass": "SampleSettingModel",
                    "title": "我的档案",
                    "hideAccessory": false,
                    "accessoryTitle": "添加档案,获得精准引荐"
                }
            }
        ],
        [
            {
                "cellClass": "SampleSettingCell",
                "responseSelector": "account",
                "model": {
                    "modelClass": "SampleSettingModel",
                    "title": "账号与安全",
                    "hideAccessory": false,
                    "accessoryTitle": ""
                }
            },
            {
                "cellClass": "SampleSettingCell",
                "responseSelector": "pay",
                "model": {
                    "modelClass": "SampleSettingModel",
                    "title": "付出",
                    "hideAccessory": false,
                    "accessoryTitle": "付出宝账号、免密付出等"
                }
            }
        ]
    ]
}

将编写完成的json文件放入项目中。

2.结构新SettingJsonModel

⚠️也能够直接修正SettingModel中model类型id改为SettingJsonWrapperModel,无需结构SettingJsonModel

这儿运用SettingJsonWrapperModel覆写YYModel协议的+ (Class)modelCustomClassForDictionary:(NSDictionary *)dictionary函数来完成自动转为对应model类型的功用.该函数能够将dictionary映射成指定的类,即能够经过匹配逻辑将model转成cell对应的模型类。

当将SettingJsonModel调用YYModel转模型时,因为model的类型是SettingJsonWrapperModel类型,所以会走SettingJsonWrapperModel的modelCustomClassForDictionary:函数,在该函数中判别modelClass字符串,并返回modelClass对应的模型类,即可将SettingJsonWrapperModel转成modelClass指向的类型。

@interface SettingJsonWrapperModel : NSObject <YYModel>
@property (nonatomic, strong) Class modelClass;
@end
@interface SettingJsonModel : NSObject <SettingModelProtocol>
@property (nonatomic, strong) Class cellClass;
@property (nonatomic, assign, nullable) SEL responseSelector;
@property (nonatomic, strong) SettingJsonWrapperModel *model;
@end
@implementation SettingJsonWrapperModel
+ (Class)modelCustomClassForDictionary:(NSDictionary *)dictionary {
    if (dictionary[@"modelClass"]) {
        return NSClassFromString(dictionary[@"modelClass"]);
    }
    return nil;
}
@end
3.修正presenter代码以便运用json方法

@implementation TaobaoSettingPresenter
- (NSArray<NSArray<id<SettingModelProtocol>> *> *)dataSource {
    return [self getDataSourceFromJson];
}
- (NSArray<NSArray<id<SettingModelProtocol>> *> *)getDataSourceFromJson {
    NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"taobao_settings" ofType:@"json"];
    NSData *jsonData = [NSData dataWithContentsOfFile:jsonPath];
    id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
    NSArray<NSArray *> *items = jsonObject[@"data"];
    NSMutableArray *settingModels = [NSMutableArray array];
    [items enumerateObjectsUsingBlock:^(NSArray * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSArray *models = [NSArray yy_modelArrayWithClass:SettingJsonModel.class json:obj];
        [settingModels addObject:models];
    }];
    return settingModels.copy;
}
@end

0x7 总结

怎么规划一个易扩展的设置页

本篇文章对设置页面这个场景进行了剖析与代码结构规划,经过该规划,当该页面产生cell品种变更时,能够经过新增cell品种进行扩展,而不会影响原有逻辑。改动设置页整体风格时,能够新增presenter进行切换,完成共用一套cell与SettingViewController代码的才能,提高了代码的复用性与扩展性。

关于相似的页面,也能够套用这个流程:

  1. 剖析页面,确认易变部分与固定部分
  2. 确认数据结构
  3. 编写整体结构代码完成功用
  4. 优化
  5. 再优化