【问题标题】:iOS Swift Coordinator pattern and back button of Navigation ControlleriOS Swift Coordinator 模式和导航控制器的后退按钮
【发布时间】:2019-06-06 22:59:26
【问题描述】:

我正在使用模式MVVM+Coordinator。我的每个控制器都是由coordinators 创建的。但是当点击Navigation Controller 的后退按钮时停止我的协调员的正确方法是什么?

class InStoreMainCoordinator: NavigationCoordinatorType, HasDisposeBag {

    let container: Container

    enum InStoreMainChildCoordinator: String {
        case menu = "Menu"
        case locations = "Locations"
    }

    var navigationController: UINavigationController
    var childCoordinators = [String: CoordinatorType]()

    init(navigationController: UINavigationController, container: Container) {
        self.navigationController = navigationController
        self.container = container
    }

    func start() {
        let inStoreMainViewModel = InStoreMainViewModel()
        let inStoreMainController = InStoreMainController()
        inStoreMainController.viewModel = inStoreMainViewModel

        navigationController.pushViewController(inStoreMainController, animated: true)
    }
}

【问题讨论】:

  • 让我们面对现实吧:在 UIKit 中,“协调器模式”在纸面上看起来不错,但要正确实现却非常非常困难。让我们从您的陈述开始:“我的每个控制器都是由协调员创建的”。这有效地产生了一个从不没有问题的实现,最终导致您的应用由于“致命错误”而终止或行为不正确的边缘情况;)

标签: ios swift mvvm rx-swift coordinator-pattern


【解决方案1】:

我的方法是使用根(父)协调器来管理子协调器,因此当用户完成流程或点击后退按钮时,会调用根协调器中的委托方法,它可以清理子协调器并创建一个新的如果需要,一个。

【讨论】:

    【解决方案2】:

    协调器模式在原生后退按钮方面存在已知盲点。你主要有两种方法来解决它:

    • 重新实现您自己的后退按钮,尽管您松开了原生的向后滑动手势来导航。
    • 实现UINavigationControllerDelegate 以检测视图何时弹出以便能够释放匹配的协调器。

    关于第一种解决方案,我不推荐这个,用户会为你的代码架构付出代价,这听起来不公平。

    对于第二个,您可以按照@mosbah 的建议将其实现到协调器本身,但我建议您更进一步,使用NavigationControllerRouter 类将导航分离到协调器以隔离导航本身并保持清晰的关注点分离。

    我写了一些关于它的内容here,详细说明了主要步骤。

    【讨论】:

    • 还有第三种解决方案。使用函数而不是类来表示您的协调器。这样您就无需担心所有权问题。
    【解决方案3】:

    在阅读了许多关于协调器的文章并看到了一些复杂的想法(如路由器、一些令人费解的魔法和自定义导航控制器委托)之后,我现在要做的是:

    View Controller 强烈拥有 Coordinator,而 Coordinator 对 View Controller 的引用很弱。 Coordinator 对其父级的引用很弱,以支持 Coordinator 对象之间通信的责任链。

    (责任链设计模式的示例是 iOS 中的响应者链。)

    当您在某个协调器上调用 stop 时,视图控制器会从堆栈中弹出,释放并释放协调器。因此,当点击后退按钮并关闭视图控制器时,协调器将被释放。

    这对我有用,因为不需要构建额外的基础设施。

    最初我通过构建符合 UINavigationControllerDelegate 协议的 NavigationControllerMutliDelegate 类解决了 UINavigationControllerDelegate 问题。它具有注册/注销逻辑。然后这个对象被传递给每个协调器,以便在视图控制器关闭时通知协调器。 NavigationControllerMutliDelegate 是访问者设计模式的一个示例,它有一堆协调器,在 View Controller 出现/关闭时,它通过向每个协调器发送一个对象来通知所有协调器。

    但是,最后,当看到有多少代码和不必要的复杂性时,我只是选择了拥有 Coordinator 的 View Controller。我只希望对象位于 View Controller 之上,它保留数据提供者、服务、视图模型等,以便 View Controller 更干净。我不想重新发明协调器的推送弹出堆栈并处理这么多所有者问题。就像我想要一些东西来缓解我的生活而不是让它变得更加复杂......

    【讨论】:

      【解决方案4】:

      我的解决方案是使用函数而不是类作为我的协调器。这样我就完全没有所有权问题。当点击后退按钮时,来自视图控制器的视图会发出已完成的事件,一切都会自然展开,我不费吹灰之力。

      您在示例中显示的start() 可以更简单地表达为:

      func startInStore(navigationController: UINavigationController) {
          let inStoreMainViewModel = InStoreMainViewModel()
          let inStoreMainController = InStoreMainController()
          inStoreMainController.viewModel = inStoreMainViewModel
      
          navigationController.pushViewController(inStoreMainController, animated: true)
      }
      

      可以在此处找到使用此样式的示例应用程序:https://github.com/danielt1263/RxMyCoordinator

      【讨论】:

        【解决方案5】:

        您可以编写您的协调器类,而不是使用子协调器,以使它们根本不需要保留。实际上,在您给出的示例中,没有什么需要保留此类,您甚至可以将其最小化为以下形式:

        class InStoreMainCoordinator {
            func start(with navigationController: UINavigationController, container: Container) {
                let inStoreMainViewModel = InStoreMainViewModel()
                let inStoreMainController = InStoreMainController()
                inStoreMainController.viewModel = inStoreMainViewModel
                navigationController.pushViewController(inStoreMainController, animated: true)
            }
        }
        

        然后,当您想启动此屏幕时,只需调用 InStoreMainCoordinator().start(with: navigationController, container: container)。您根本不需要保留对此 InStoreMainCoordinator 的强引用。这样您就不会对后退按钮有任何问题,因为您不需要解除分配这些协调器。它们仅在您将屏幕切换到新屏幕时才存在。为了更好地理解此方法,假设您有另一个屏幕,例如 InStoreDetailsController 类,并且在单击 InStoreMainController 上的某些内容后应该启动详细信息屏幕。然后你可以像这样实现两个与这些视图控制器相关的协调器类:

        class InStoreMainCoordinator {
            func start(with navigationController: UINavigationController, container: Container) {
                let inStoreMainViewModel = InStoreMainViewModel(onStoreSelected: { storeId in
                    InStoreDetailsCoordinator().start(with: navigationController, container: container, dependencies: .init(storeId: storeId))
                })
                let inStoreMainController = InStoreMainController()
                inStoreMainController.viewModel = inStoreMainViewModel
                navigationController.pushViewController(inStoreMainController, animated: true)
            }
        }
        class InStoreDetailsCoordinator {
            struct Dependencies {
                var storeId: String
            }
            func start(with navigationController: UINavigationController, container: Container, dependencies: Dependencies) {
                let inStoreDetailsViewModel = InStoreDetailsViewModel(storeId: dependencies.storeId)
                let inStoreDetailsController = InStoreDetailsController()
                inStoreDetailsController.viewModel = inStoreDetailsViewModel
                navigationController.pushViewController(inStoreDetailsController, animated: true)
            }
        }
        

        如您所见,如果您使用闭包而不是委托模式,您可以在单个函数中编写与一个屏幕相关的所有内容(包括将其推送到屏幕上并处理与从该屏幕移动到另一个屏幕相关的事件) .这样,您在需要切换屏幕时调用的协调器中的每个屏幕只有一个方法,并且您不需要保留它们,因为需要保留的所有内容都由其他东西保留(在上面的示例中,视图模型保留onStoreSelected 参数中给出的处理程序,用于切换到另一个屏幕)。我认为这个解决方案比使用儿童协调员更简单。它工作正常,不需要对后退按钮进行任何额外的特殊处理。

        另一种非常有效的替代解决方案,特别是如果您的应用程序中没有大量屏幕,则为您的主视图控制器中的每个视图控制器创建 startNameOfYourScreen(...) 方法em>AppCoordinator 类或您命名的任何内容。正如您在上面看到的,如果您使用闭包而不是委托模式,您可以在一个函数中编写与一个屏幕相关的所有内容,这样可以保持非常简单。您可以选择将这些函数拆分为 AppCoordinator 类的扩展,并将它们放入单独的文件中,以便在您的项目中有更好的组织。在这个解决方案中,后退按钮也没有任何问题,因为您根本不需要实例化子协调器,也不需要释放它们。

        但是,如果由于某种原因您决定仍要使用子协调员的方式,那么这里有一些文章链接,这些链接指向有关使用子协调员时后退按钮问题的可能解决方案的文章:

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-07-10
          • 1970-01-01
          • 1970-01-01
          • 2011-11-29
          • 1970-01-01
          相关资源
          最近更新 更多