ios 关于VIPER -清洁架构的问题

13z8s7eq  于 2023-02-26  发布在  iOS
关注(0)|答案(3)|浏览(195)

我一直在阅读Robert Martin关于Clean Architecture的文章,更具体地说,是关于VIPER的。
然后我遇到了这篇文章/post Brigade’s Experience Using an MVC Alternative,它描述了我目前正在做的事情。
在一个新的iOS项目上实际尝试实现VIPER后,我遇到了一些问题:

  • 演示者可以在视图中查询信息吗?或者“信息传递”应该总是从视图开始吗?例如,如果视图触发了演示者中的某个操作,但随后,根据通过该操作传递的参数,演示者可能需要更多信息。我的意思是:用户点击“完成状态:“,如果状态==“某事”,从视图中获取信息以创建实体,如果状态==“某事”,在视图中动画化某事。2我应该如何处理这种情况?
  • 假设一个“模块”(VIPER组件组)决定以模态方式呈现另一个模块,那么谁应该负责决定第二个模块是否以模态方式呈现,是第一个模块的线框图还是第二个模块的线框图?
  • 另外,假设第二个模块的视图被推入导航控制器,那么“后退”操作应该如何处理呢?我应该手动设置一个“后退”按钮,并在第二个模块的视图控制器中执行一个操作吗?调用第二个模块的解除的线框,并告诉第一个模块的线框它已解除,以便第一个模块“的视图控制器可能要显示某些内容?
  • 不同的模块应该只通过线框图还是通过演示者之间的代理进行对话?例如,如果应用导航到不同的模块,但之后用户按下“取消”或“保存”,该选择需要返回并更改第一个模块中的某些内容(可能显示保存的动画或删除某些内容)。
  • 假设在Map上选择了一个管脚,然后显示PinEditViewController。返回时,所选管脚的颜色可能需要根据PinEditViewController上的使用操作进行更改。谁应该保持当前所选管脚的状态,MapViewController、MapPresenter还是MapWireframe,以便我在返回时知道哪个管脚应该更改颜色?
1u4esq0p

1u4esq0p1#

1.演示者是否可以从视图查询信息

为了让您满意地回答这个问题,我们需要关于这个特定案例的更多细节。为什么视图不能在回调时直接提供更多的上下文信息?
我建议你给Presenter传递一个Command对象,这样Presenter就不需要知道在哪种情况下该做什么,Presenter可以执行对象的方法,如果需要的话,它自己传递一些信息,而不需要知道视图的状态(从而引入高耦合)。

  • 视图处于一种称为 x 的状态(与 yz 相对),它知道自己的状态。
  • 用户完成操作。View通知其代理(演示者)操作已完成。由于它如此参与,因此它构造了一个数据传输对象来保存所有常规信息。此DTO的属性之一是id<FollowUpCommand> followUpCommand。View创建一个XFollowUpCommand(与YFollowUpCommandZFollowUpCommand相对)并相应地设置其参数,然后将其放入DTO中。
  • Presenter接收方法调用。它对数据做一些事情,不管那里有什么具体的FollowUpCommand。然后它执行协议的唯一方法,followUpCommand.followUp。具体的实现将知道要做什么。

如果必须对某个属性执行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;
}

如果您使用故事板Segues,则必须稍微不同地执行操作。

3.导航层次结构

另外,假设第二个模块的视图被推送到导航控制器中,应该如何处理“后退”操作?
如果你去“所有毒蛇”,是的,你必须从视图到它的线框,并路由到另一个线框。
要将数据从显示模块(“Second”)传递回显示模块(“First”),添加SecondDelegate并在FirstPresenter中实现它。在显示模块弹出之前,它向SecondDelegate发送消息以通知结果。
不过,“不要与框架对抗”。也许你可以牺牲VIPER的纯粹性来利用导航控制器的一些细节。Segues已经是向路由机制方向迈出的一步。Look at 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.消息流

不同的模块应该只通过线框对话,还是通过演示者之间的代理对话?
如果你直接从演示者A向另一个模块B的对象发送消息,那么会发生什么?
例如,由于接收方的视图不可见,动画无法启动。Presenter仍然必须等待线框/路由器。因此,它必须将动画排队,直到它再次变为活动状态。这使Presenter更具状态性,从而使其更难使用。
在体系结构方面,考虑模块所扮演的角色。在端口/适配器体系结构中,Clean Architecture挖掘了一些概念,这个问题更加明显。作为一个类比:计算机有许多端口。2 USB端口不能与LAN端口通信。3每一个信息流都必须通过核心路由。
你的应用的核心是什么?
你有一个域模型吗?你有一组服务,从不同的模块查询?VIPER模块围绕视图中心。填充模块共享,像数据访问机制,不属于一个特定的模块。这就是你可以称之为核心。在那里,你应该执行数据更改。如果另一个模块变得可见,它拉入更改的数据。
不过,仅出于动画的目的,让路由器知道要做什么,并根据模块更改向Presenter发出命令。
在VIPER Todo示例代码中:

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

5.保持状态

谁应该保持当前所选管脚的状态,是MapViewController、MapPresenter还是MapWireframe,以便我在返回时知道哪个管脚应该改变颜色?
无。避免视图模块服务中的状态化以降低维护代码的成本。相反,尝试弄清楚是否可以在更改期间传递引脚更改的表示。
尝试访问实体以获得状态(通过Presenter和Interactor等)。
这并不意味着您在视图层中创建一个Pin对象,将其从视图控制器传递到视图控制器,更改其属性,然后将其发送回以反映更改。具有序列化更改的NSDictionary可以做到这一点吗?您可以将新颜色放入其中,并将其从PinEditViewController发送回其Presenter,后者在MapViewController中发布更改。
现在我作弊了:MapViewController需要有状态。它需要知道所有的引脚。然后我建议你传递一个更改字典,这样MapViewController就知道该怎么做了。
但如何识别受影响的引脚?
每个大头针可能都有自己的ID。这个ID可能只是它在Map上的位置。可能是它在大头针数组中的索引。在任何情况下都需要某种标识符。或者创建一个可识别的 Package 器对象,在操作期间保存大头针本身。(不过,对于改变颜色的目的来说,这听起来太荒谬了。)

发送事件以更改状态

VIPER是非常基于服务的。有很多无状态的对象捆绑在一起来传递消息和转换数据。在Brigade Engineering的文章中,也展示了一种以数据为中心的方法。
实体在一个相当薄的层中。与我所想到的相反的是Domain Model。这种模式并不是每个应用都需要的。但是,以类似的方式对应用的核心进行建模可能有助于回答你的一些问题。
与作为数据容器的实体不同,每个人都可以通过“数据管理器”进入实体,域保护它的实体。域也会主动通知更改。(首先通过NSNotificationCenter。通过类似命令的直接消息调用则较少。)
现在,这可能也适合您的Pin图案例:

  • PinEditViewController更改管脚颜色。这是UI组件中的更改。
  • UI组件的改变对应于底层模型的改变。你通过VIPER模块栈执行改变。(你持久化颜色了吗?如果没有,Pin实体总是短暂的,但它仍然是一个实体,因为它的身份很重要,而不仅仅是它的值。)
  • 对应的Pin已更改颜色,并通过NSNotificationCenter发布通知。
  • 由于偶然性(即Pin不知道),一些Interactor订阅了这些通知并更改了其视图的外观。

虽然这可能对你的情况也有用,但我认为

gc0ot86w

gc0ot86w2#

这个答案可能有点不相关,但我把它放在这里是为了参考。网站Clean Swift是Bob叔叔的“Clean Architecture“在swift中的一个很好的实现。所有者称它为VIP(尽管它仍然包含“实体”和路由器/线框)。
这个网站提供了XCode模板,假设你想创建一个新的场景(他称之为VIPER模块,“场景”),你所要做的就是文件-〉新建-〉场景模板。
这个模板创建了一批7个文件,包含了你的项目中所有令人头痛的样板代码。它还配置了这些文件,使它们可以开箱即用。该网站对每件事是如何组合在一起的给出了相当透彻的解释。
有了所有的样板代码,找到你上面问的问题的解决方案就容易多了。而且,模板允许在所有方面保持一致性。

EDIT-〉关于下面的注解,这里解释一下我为什么支持这种方法-〉http://stringerstheory.net/the-clean-er-architecture-for-ios-apps/

还有这个-〉The Good, the bad, and the Ugly about VIPER in iOS

6jygbczu

6jygbczu3#

你的大部分问题都在这篇文章中得到了回答:https://www.ckl.io/blog/best-practices-viper-architecture(包含示例项目)。我建议您特别注意模块初始化/表示的提示:就由源代码Router来完成。
关于后退按钮,您可以use delegates将此消息触发到所需的模块。这是我如何做到的,它工作得很好(即使在您插入推送通知之后)。
是的,模块之间也可以通过using delegates进行对话,这对于更复杂的项目来说是必须的。

相关问题