40

我一直在阅读 Robert Martin 的关于Clean Architecture的文章,尤其是关于VIPER的文章。

然后我遇到了这篇文章/post Brigade's Experience Using an MVC Alternative,它几乎描述了我目前正在做的事情。

在实际尝试在一个新的 iOS 项目上实现 VIPER 之后,我遇到了一些问题:

  • 演示者可以在视图中查询信息还是“信息传递”始终从视图开始?例如,如果视图在演示者中触发了某些操作,但是根据通过该操作传递的参数,演示者可能需要更多信息。我的意思是:用户点击“doneWithState:”,如果 state == “something”,从视图中获取信息以创建实体,如果 state == “something else”,则在视图中设置动画。我应该如何处理这种情况?
  • 假设一个“模块”(VIPER 组件组)决定以模态方式呈现另一个模块。谁应该负责决定是否以模态方式呈现第二个模块,第一个模块的线框还是第二个模块的线框?
  • 另外,假设第二个模块的视图被推入导航控制器,应该如何处理“后退”操作?我是否应该在第二个模块的视图控制器中手动设置一个带有动作的“后退”按钮,该按钮调用演示者,调用第二个模块的线框以解除并告诉第一个模块的线框它已被解除,以便第一个模块的视图控制器可能想要展示一些东西?
  • 不同的模块应该只通过线框对话还是通过演示者之间的委托进行对话?例如,如果应用程序导航到不同的模块,但之后用户按下“取消”或“保存”并且该选择需要返回并更改第一个模块中的某些内容(可能显示它已保存或删除某些内容的动画)。
  • 假设在地图上选择了一个图钉,而不是显示 PinEditViewController。返回时,所选引脚的颜色可能需要根据 PinEditViewController 上的使用操作进行更改。谁应该保留当前选择的 pin、MapViewController、MapPresenter 或 MapWireframe 的状态,以便我知道,当返回时,哪个 pin 应该改变颜色?
4

3 回答 3

19

1、Presenter可以从视图中查询信息

为了让您满意地回答这个问题,我们需要有关特定案例的更多详细信息。为什么视图不能在回调时直接提供更多上下文信息?

我建议您向 Presenter 传递一个Command对象,这样 Presenter 就不必知道在这种情况下要做什么。Presenter 可以执行对象的方法,在需要时自行传递一些信息,而无需了解视图的任何状态(因此引入了高度耦合)。

  • View 处于您称为x的状态(与yz相对)。无论如何,它都知道自己的状态。
  • 用户完成操作。View 通知它的委托人(Presenter)关于完成。因为它是如此复杂,所以它构造了一个数据传输对象来保存所有常用信息。此 DTO 的属性之一是id<FollowUpCommand> followUpCommand. View 创建一个XFollowUpCommand(与YFollowUpCommandand相对ZFollowUpCommand)并相应地设置其参数,然后将其放入 DTO。
  • Presenter 接收方法调用。无论那里有什么混凝土,它FollowUpCommand都会对数据做一些事情。然后它执行协议的唯一方法,followUpCommand.followUp. 具体的实现会知道该怎么做。

如果您必须对某些属性执行 switch-case/if-else,大多数情况下,将选项建模为从通用协议继承的对象并传递对象而不是状态会有所帮助。

2. 模态模块

呈现模块或被呈现模块是否应该决定它是否是模态的?-- 提出的模块(第二个)应该决定,只要它被设计为仅用于模态。将有关事物的知识放在事物本身中。如果它的表示模式取决于上下文,那么模块本身无法决定。

第二个模块的线框将收到如下消息:

[secondWireframe presentYourStuffIn:self.viewController]

参数是应该为其呈现的对象。asModal如果模块被设计为以两种方式使用,您也可以传递一个参数。如果只有一种方法可以做到这一点,请将此信息放入受影响的模块(提供的模块)本身。

然后它将执行以下操作:

- (void)presentYourStuffIn:(UIViewController)viewController {
    // set up module2ViewController

    [self.presenter configureUserInterfaceForPresentation:module2ViewController];

    // Assuming the modal transition is set up in your Storyboard
    [viewController presentViewController:module2ViewController animated:YES completion:nil];

    self.presentingViewController = viewController;
}

如果您使用 Storyboard Segues,您将不得不做一些不同的事情。

3.导航层次

另外,假设第二个模块的视图被推入导航控制器,应该如何处理“后退”操作?

如果你选择“all VIPER”,是的,你必须从视图到它的线框并路由到另一个线框。

要将数据从呈现模块(“第二”)传递回呈现模块(“第一”),请SecondDelegateFirstPresenter. 在呈现的模块弹出之前,它会发送一条消息SecondDelegate来通知结果。

但是,“不要与框架抗争”。也许你可以通过牺牲 VIPER 的纯粹性来利用一些导航控制器的细节。Segues 已经朝着路由机制的方向迈出了一步。查看 VTDAddWireframe以了解UIViewControllerTransitioningDelegate线框中引入自定义动画的方法。也许这是有帮助的:

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[VTDAddDismissalTransition alloc] init];
}


- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source
{
    return [[VTDAddPresentationTransition alloc] init];
}

我首先认为您需要保留一堆类似于导航堆栈的线框,并且所有“活动”模块的线框都相互链接。但事实并非如此。线框管理模块的内容,但导航堆栈是唯一代表哪个视图控制器可见的堆栈。

4. 消息流

不同的模块应该只通过线框对话还是通过演示者之间的委托进行对话?

如果直接向另一个模块 B 的对象发送来自 Presenter A 的消息,那会发生什么?

例如,由于接收者的视图不可见,因此无法启动动画。Presenter 仍然需要等待 Wireframe/Router。所以它必须将动画排入队列,直到它再次激活。这使得 Presenter 更有状态,这使得它更难使用。

在架构方面,请考虑模块所扮演的角色。在端口/适配器架构中,清洁架构从中挖掘了一些概念,问题更加明显。打个比方:一台计算机有很多端口。USB 端口无法与 LAN 端口通信。每个信息流都必须通过核心路由。

您的应用程序的核心是什么?

你有领域模型吗?您是否有一组从各种模块查询的服务?VIPER 模块以视图为中心。共享的东西模块,就像数据访问机制一样,不属于特定的模块。这就是你所说的核心。在那里,您应该执行数据更改。如果另一个模块变得可见,它会拉入更改的数据。

但是,仅出于动画目的,让路由器知道该做什么并根据模块更改向 Presenter 发出命令。

在 VIPER Todo 示例代码中:

  • “列表”是根视图。
  • “添加”视图显示在列表视图的顶部。
  • ListPresenter 实现了 AddModuleDelegate。如果“添加”模块完成,ListPresenter 会知道,而不是它的线框,因为视图已经在导航堆栈中。

5.保持状态

谁应该保留当前选择的引脚、MapViewController、MapPresenter 或 MapWireframe 的状态,以便我知道,当返回时,哪个引脚应该改变颜色?

没有任何。避免视图模块服务中的状态,以降低维护代码的成本。相反,请尝试弄清楚您是否可以在更改期间传递引脚更改的表示。

尝试获取实体以获取状态(通过 Presenter 和 Interactor 等等)。

这并不意味着您Pin在视图层中创建一个对象,将其从视图控制器传递到视图控制器,更改其属性,然后将其发送回以反映更改。NSDictionary带有序列化更改的会做吗?您可以将新颜色放在那里,然后将其从PinEditViewController背面发送给它的 Presenter,后者会在MapViewController.

现在我被骗了:MapViewController需要有状态。它需要知道所有引脚。然后我建议你传递一个更改字典,以便MapViewController知道该怎么做。

但是您如何识别受影响的引脚?

每个引脚都可能有自己的 ID。也许这个 ID 只是它在地图上的位置。也许它是它在 pin 数组中的索引。无论如何,您都需要某种标识符。或者,您创建一个可识别的包装对象,该对象在操作期间保持引脚本身。(不过,对于改变颜色的目的来说,这听起来太荒谬了。)

发送事件来改变状态

VIPER 非常基于服务。有许多大多是无状态的对象捆绑在一起来传递消息和转换数据。在 Brigade Engineering 的帖子中,也展示了一种以数据为中心的方法。

实体位于相当薄的层中。与我心目中的频谱相反的是域模型。并非每个应用都需要这种模式。不过,以类似的方式对应用程序的核心进行建模可能有助于回答您的一些问题。

与实体作为每个人都可以通过“数据管理器”访问的数据容器相反,域保护其实体。域也会主动通知更改。(通过NSNotificationCenter, 对于初学者来说。通过类似命令的直接消息调用就更少了。)

现在这可能也适合您的 Pin 盒:

  • PinEditViewController 更改引脚颜色。这是 UI 组件中的更改。
  • UI 组件更改对应于底层模型的更改。您通过 VIPER 模块堆栈执行更改。(你坚持颜色吗?如果不是,Pin实体总是短暂的,但它仍然是一个实体,因为它的身份很重要,而不仅仅是它的价值。)
  • 相应的Pin已经改变颜色并通过 发布通知NSNotificationCenter
  • 偶然(也就是说,Pin不知道),一些交互器订阅了这些通知并改变了它的视图的外观。

尽管这也可能适用于您的情况,但我认为将编辑绑定

于 2015-03-13T11:00:10.873 回答
8

这个答案可能有点无关,但我把它放在这里供参考。站点Clean Swift是 Bob 大叔的“干净架构”在 swift中的出色实现。所有者称它为 VIP(尽管它仍然包含“实体”和路由器/线框)。

该站点为您提供 XCode 模板。因此,假设您要创建一个新场景(他将 VIPER 模块称为“场景”),您所做的就是 File->new->sceneTemplate。

该模板创建了一批 7 个文件,其中包含您项目的样板代码的所有令人头疼的问题。它还对它们进行配置,以便它们开箱即用。该网站对每件事情如何组合在一起给出了非常详尽的解释。

排除所有样板代码后,找到您上面提出的问题的解决方案就容易一些了。此外,模板允许全面的一致性。

编辑->关于下面的评论,这里解释了为什么我支持这种方法-> http://stringerstheory.net/the-clean-er-architecture-for-ios-apps/

还有这个 -> iOS 中关于 VIPER 的好、坏和丑

于 2015-12-20T22:00:04.707 回答
3

这篇文章回答了您的大部分问题:https ://www.ckl.io/blog/best-practices-viper-architecture (包括示例项目)。我建议您特别注意模块初始化/演示的提示:这取决于源代码Router

关于后退按钮,您可以use delegates将此消息触发到所需的模块。我就是这样做的,而且效果很好(即使在您插入推送通知之后)。

是的,模块当然也可以相互using delegates通信。对于更复杂的项目来说,这是必须的。

于 2017-04-11T01:54:22.347 回答