基于树的iOS页面路径转换设计

需求场景

采集用户在购买VIP或购买商品时的操作路径,如:"首页" -> "搜题历史页" -> "题目详情页1" -> "名师大招"。

设计原则

  1. 易用性
  2. 可靠性

方案分析

考虑到上述两个设计原则,路径转换在每个页面中的使用都不应该受限于其相对于其他页面的位置或关系。简而言之,即具备独立性和通用性。以view controller为例,路径转换在特定的生命周期阶段使用,就是符合设计原则的。

以下有两种方案:基于栈的设计、基于多叉树的设计。下面依次进行分析。

1. 栈

下图所示为两个View Controller切换时,各个VC的生命周期的切换顺序,其中A为父VC,B为子VC。为了遵循上述设计原则,我们希望能够在特定生命周期进行push/pop操作。合法的push/pop组合应该遵循如下的调用顺序:

1
A push -> B push -> B pop -> A pop

然而,我们在下图中找不到一个合法的push/pop组合,除了【init/viewDidLoad时push,dealloc时pop】这种情况。

那么,在【init/viewDidLoad时push,dealloc时pop】是不是真能应对所有场景呢?

试想如下图所示这样一种场景,当一个VC包含多个VC对象时,其中一个VC再调用子VC。这时候,我们期望得到的路径应该是:

1
A -> D -> E

然而在很多情况下,A会在init/viewDidLoad阶段初始化多有VC对象。这时候如果使用的是【init/viewDidLoad时push,dealloc时pop】这种组合,在E中得到的路径会是:

1
A -> B -> C -> D -> E

综合上述,使用栈结构很难实现一种易用、可靠的设计方案。

2. 树

我们使用多叉树实现了一个易用、可靠的路径转换方案。如下左图所示,为一个app经常会面临的vc结构。我们使用多叉树来描述ap的vc结构。在任何时候,页面总是能够返回到根页面(tab页),所以树节点不会形成环,即可以使用树结构来进行描述,如右图所示。

其中,树节点包括四个属性:

1
2
3
4
5
6
7
@interface VGOKeyfromBaseNode : NSObject

@property (nonatomic, weak, nullable) __kindof VGOKeyfromBaseNode *parentNode; // 父节点
@property (nonatomic, strong) NSString *value; // 节点信息
@property (nonatomic, strong) NSMutableArray<__kindof VGOKeyfromBaseNode *> *subNodes; // 子节点

@end

为了能够维护多棵树,使用 VGOKeyfromTreeManager 进行管理,其包含两个属性:

1
2
@property (nonatomic, strong) NSMutableDictionary<NSString *, __kindof VGOKeyfromBaseNode *> *keyfromTrees;     // 存储各个树的根节点
@property (nonatomic, strong) NSMutableDictionary<NSString *, __kindof VGOKeyfromBaseNode *> *currentNodes; // 存储各个树的当前节点

keyfromNode 的类型名称作为 key,分别保存了树的根节点以及当前节点。

使用的方法是:

  1. viewDidLoad阶段,创建节点并加入树中。
  2. viewWillAppear/viewDidAppear阶段或者在进入新页面之前,将该节点设置为当前节点。
  3. dealloc阶段,将本节点的父节点设置为当前节点,并将该节点从树中删除。

树管理器的公有接口如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建根节点
- (void)createKeyfromTreeWithRootNode:(__kindof VGOKeyfromBaseNode *)node;

// 设置节点为当前节点
- (void)updateCurrentNodeWithNode:(__kindof VGOKeyfromBaseNode *)node;

// 向树中添加子节点
- (void)addNode:(__kindof VGOKeyfromBaseNode *)node;
// 从树中删除子节点
- (void)removeNode:(__kindof VGOKeyfromBaseNode *)node;

// 获取从根节点到指定节点的列表
- (NSArray<__kindof VGOKeyfromBaseNode *> *)nodePathToLastNode:(__kindof VGOKeyfromBaseNode *)node;
// 获取从根节点到指定节点的列表,并进行过滤
- (NSArray<__kindof VGOKeyfromBaseNode *> *)nodePathToLastNode:(__kindof VGOKeyfromBaseNode *)node exceptValue:(nullable NSString *)value;

// 获取从根节点到指定节点的路径
- (NSString *)keyfromPathToLastNode:(__kindof VGOKeyfromBaseNode *)node;
// 获取从根节点到指定节点的路径,并自定义联结符号、过滤内容
- (NSString *)keyfromPathToLastNode:(__kindof VGOKeyfromBaseNode *)node exceptValue:(nullable NSString *)value joinWith:(NSString *)join;

节点的公有接口如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 将本节点设置为根节点
- (void)setAsRootNode;
// 将本节点设置为当前节点
- (void)setAsCurrentNode;

// 将本节点添加至树中
- (void)addToKeyfromTree;
// 将本节点从树种删除
- (void)removeFromKeyfromTree;

// 清空本节点的所有子节点
- (void)clearSubNodes;

// 获取本节点类型的根节点
+ (__kindof VGOKeyfromBaseNode *)rootNode;
// 获取本节点类型的当前节点
+ (__kindof VGOKeyfromBaseNode *)currentNode;
// 获取本节点类型的 keyfrom 路径
+ (NSString *)keyfromPath;

实践证明,这种设计方案还能够非常简单地应用到一下这些场景之中。

  1. view Controller、View混合路径;
  2. 统跳;
  3. view Controller多处复用、节点信息不同;
  4. 忽略路径中特定节点