UIViewControllers的组成以及它们之间的导航(不仅如此)


在本文中,我想分享我们在iOS应用程序中已经成功使用了几年的经验,其中3个当前在Appstore中。 这种方法效果很好,我们最近将其与其余代码分开,并将其设计到一个单独的RouteComposer库中,实际上将对此进行讨论。


https://github.com/ekazaev/route-composer


但是,对于初学者来说,让我们尝试找出iOS中视图控制器的组成意味着什么。


在继续进行说明之前,我提醒您,在iOS中,它通常被理解为视图控制器或UIViewController 。 这是从标准UIViewController继承的类,该标准UIViewController是Apple建议用于开发iOS应用程序的基本MVC模式控制器。


您可以使用其他架构模式,例如MVVM,VIP,VIPER,但是其中的UIViewController将以一种或另一种方式涉及,这意味着该库可以与它们一起使用。 UIViewController的本质用于控制UIViewUIView通常代表一个屏幕或屏幕的重要部分,处理来自其中的事件并在其中显示一些数据。



所有UIViewController可以有条件地分为普通视图控制器 (负责在屏幕上的某些可见区域)和容器视图控制器 (除显示其自身及其某些控件外,还可以以一种或另一种方式显示集成在其中的子视图控制器) 。


Cocoa Touch随附的标准容器视图控制器包括: UINavigationConrollerUITabBarControllerUISplitControllerUIPageController等。 此外,用户可以按照Apple文档中描述的Cocoa Touch规则创建自己的自定义容器视图控制器。


在容器视图控制器中引入标准视图控制器的过程,以及将视图控制器集成到控制器堆栈中的过程,我们将在本文中称之为组合


那么,为什么对于视图控制器组成而言,标准解决方案对我们来说并不是最佳选择,我们开发了一个库来简化我们的工作。


让我们以一些标准容器视图控制器的组成为例:


标准容器中的成分示例


UINavigationController



 let tableViewController = UITableViewController(style: .plain) //        let navigationController = UINavigationController(rootViewController: tableViewController) // ... //        let detailViewController = UIViewController(nibName: "DetailViewController", bundle: nil) navigationController.pushViewController(detailViewController, animated: true) // ... //     navigationController.popToRootViewController(animated: true) 

UITabBarController



 let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() //   let tabBarController = UITabBarController() //         tabBarController.viewControllers = [firstViewController, secondViewController] //        tabBarController.selectedViewController = secondViewController 

UISplitViewController



 let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() //   let splitViewController = UISplitViewController() //        splitViewController.viewControllers = [firstViewController] //        splitViewController.showDetailViewController(secondViewController, sender: nil) 

堆栈上视图控制器的集成(组成)示例


安装视图控制器根目录


 let window: UIWindow = //... window.rootViewController = viewController window.makeKeyAndVisible() 

视图控制器的模态表示


 window.rootViewController.present(splitViewController, animated: animated, completion: nil) 

为什么我们决定创建一个合成库


从上面的示例中可以看到,没有将传统视图控制器集成到容器中的单一方法,就像没有构建视图控制器堆栈的唯一方法一样。 而且,如果您想稍微更改应用程序的布局或导航方式,则需要对应用程序代码进行重大更改,还需要指向容器对象的链接,以便您可以将视图控制器插入其中,等等。 也就是说,标准方法本身意味着大量工作,并且存在指向视图控制器的链接以生成其他控制器的动作和表示。


所有这些都使应用程序中的各种深层链接方法(例如,使用通用链接)更加头痛,因为您必须回答以下问题: 如果已经显示了控制器,因为用户已经单击了野生动物园中的链接,就需要向用户显示该控制器,或者我正在查看该控制器它应该显示尚未创建 ,迫使您浏览视图控制器树并编写代码,有时您的眼睛开始流血,任何iOS开发人员都试图隐藏该代码。 此外,与在iOS中为每个屏幕单独构建的Android体系结构不同,为了在启动后立即显示应用程序的某些部分,可能有必要构建相当大的控制器堆栈,这些控制器将隐藏在您应要求显示的控制器下。


当用户单击按钮时,或者当应用程序从另一个应用程序goToProduct(withId: "012345")通用链接并且不考虑将此视图控制器集成到堆栈时,最好调用诸如goToAccount()goToMenu()goToProduct(withId: "012345")该视图控制器的创建者已经提供了此实现。


此外,通常,我们的应用程序由不同团队开发的大量屏幕组成,为了在开发过程中进入其中一个屏幕,您需要浏览另一个可能尚未创建的屏幕。 在我们公司中,我们使用了称为培养皿的方法。 也就是说,在开发模式下,开发人员和测试人员可以访问所有应用程序屏幕的列表,并且他可以转到其中的任何一个(当然,其中一些可能需要一些输入参数)。



您可以与它们进行交互并单独进行测试,然后将它们组装到最终的生产应用程序中。 这种方法极大地促进了开发,但是,如您从上面的示例中看到的那样,当您需要在代码中保留几种将视图控制器集成到堆栈中的方式时,组合地狱就开始了。


仍然需要补充的是,一旦您的营销团队表示希望对实时用户进行A / B测试并检查哪种导航方法效果更好,例如选项卡栏或汉堡菜单,所有这些都将乘以N。


  • 让我们切掉苏珊宁的腿 让我们展示一下标签栏的50%用户,以及其他汉堡菜单,然后我们将在一个月内告诉您哪些用户看到了我们的更多优惠?

我将尝试告诉您我们如何解决该问题并将其最终分配给RouteComposer库。


苏珊宁 路线作曲家


在分析了组合和导航的所有场景之后,我们尝试对以上示例中给出的代码进行抽象,并确定RouteComposer运行的 3个主要实体-FactoryFinderAction 。 此外,该库包含3个辅助实体,它们负责导航过程中可能需要的一些微调RoutingInterceptorContextTaskPostRoutingTask 。 所有这些实体都必须按照一系列依赖关系进行配置,并转移到Router y(将建立您的控制器堆栈的对象)中。


但是,关于它们的顺序:


工厂工厂


顾名思义, Factory负责创建视图控制器。


 public protocol Factory { associatedtype ViewController: UIViewController associatedtype Context func build(with context: Context) throws -> ViewController } 

在这里,重要的是要保留上下文的概念。 在库中的上下文中,我们调用了查看器才能创建的所有内容。 例如,为了显示一个显示产品详细信息的视图控制器,您需要将某个productID传递给它,例如,以String的形式。 上下文的本质可以是任何东西:对象,结构,块或元组。 如果您的控制器不需要任何东西来创建-上下文可以指定为Any? 并安装在nil


例如:


 class ProductViewControllerFactory: Factory { func build(with productID: UUID) throws -> ProductViewController { let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) productViewController.productID = productID //  ,      `ContextAction`,     return productViewController } } 

从上面的实现中可以清楚地看出,该工厂将从XIB文件中加载控制器映像并将已安装的productID安装到该映像中。 除了标准的Factory协议之外,该库还提供了该协议的几种标准实现,从而使您不必编写普通代码(尤其是上面的示例)。


此外,我将避免提供协议的描述及其实现的示例,因为您可以通过下载库随附的示例来熟悉它们的详细信息。 对于传统的视图控制器和容器,工厂有多种实现方式,以及配置它们的方式。


动作片


Action实体描述如何将由工厂构建的视图控制器集成到堆栈中。 创建后的视图控制器不能简单地挂在空中,因此,每个工厂都应包含Action如上例所示)。


Action的最常见实现是控制器的模式表示:


 class PresentModally: Action { func perform(viewController: UIViewController, on existingController: UIViewController, animated: Bool, completion: @escaping (_: ActionResult) -> Void) { guard existingController.presentedViewController == nil else { completion(.failure("\(existingController) is already presenting a view controller.")) return } existingController.present(viewController, animated: animated, completion: { completion(.continueRouting) }) } } 

该库包含将视图控制器集成到堆栈中的大多数标准方法的实现,在使用某种自定义容器视图控制器或表示方法之前,您可能不必创建自己的视图控制器。 但是,如果您阅读示例,则创建自定义动作不会引起问题。


查找器


Finder的本质回答了路由器的问题-这样的控制器是否已经创建并且已经在堆栈中? 也许不需要创建任何东西,足以显示已经存在的内容了吗?


 public protocol Finder { associatedtype ViewController: UIViewController associatedtype Context func findViewController(with context: Context) -> ViewController? } 

如果存储指向创建的所有视图控制器的链接,则在Finder实现中,您可以简单地将链接返回至所需的视图控制器。 但是大多数情况并非如此,因为应用程序堆栈(尤其是大型应用程序堆栈)的变化非常动态。 此外,您可以在堆栈上有几个相同的视图控制器来显示不同的实体(例如,几个ProductViewControllers可以显示具有不同productID的不同产品),因此Finder实现可能需要自定义实现并在堆栈上搜索相应的视图控制器。 该库通过提供StackIteratingFinder作为Finder的扩展来简化此任务,该协议具有用于简化此任务的适当设置。 在StackIteratingFinder的实现中StackIteratingFinder您只需要回答问题-这个视图控制器是路由器根据您的请求寻找的那个控制器。


此类实现的示例:


 class ProductViewControllerFinder: StackIteratingFinder { let options: SearchOptions init(options: SearchOptions = .currentAndUp) { self.options = options } func isTarget(_ productViewController: ProductViewController, with productID: UUID) -> Bool { return productViewController.productID == productID } } 

辅助实体


RoutingInterceptor


RoutingInterceptor允许您在开始组合视图控制器之前执行一些操作,并告诉路由器是否可以在堆栈中集成视图控制器。 此类任务最常见的示例是身份验证(但在实现中一点都不常见)。 例如,您想显示一个具有用户帐户详细信息的视图控制器,但是为此,用户必须登录到系统。 您可以实现RoutingInterceptor并将其添加到用户详细信息控制器的视图配置中,并进行内部检查:如果用户已登录,则允许路由器继续导航;如果未登录,则显示提示用户登录的视图控制器;如果此操作成功,则允许路由器继续导航或取消如果用户拒绝登录,则为她。


 class LoginInterceptor: RoutingInterceptor { func execute(for destination: AppDestination, completion: @escaping (_: InterceptorResult) -> Void) { guard !LoginManager.sharedInstance.isUserLoggedIn else { // ... //  LoginViewController       completion(.success)  completion(.failure("User has not been logged in.")) // ... return } completion(.success) } } 

库随附的示例中包含带有注释的RoutingInterceptor的实现。


ContextTask


如果提供了ContextTask ,则可以将其单独应用于配置中的每个视图控制器,而不管它是由路由器创建的还是在堆栈中找到的,而您只想更新其中的数据或设置一些默认值参数(例如,显示关闭按钮或不显示)。


PostRoutingTask


成功完成请求的视图控制器到堆栈的集成后,路由器将调用PostRoutingTask的实现。 在其实现中,添加各种分析或提取各种服务非常方便。


有关所有描述实体的实现的详细信息,请参见该库的文档以及随附的示例。


PS:可以添加到配置中的辅助实体的数量不受限制。


构型


所有描述的实体都很好,因为它们可以将组成过程分为多个小的,可互换的和值得信赖的块。


现在,让我们继续最重要的事情-配置,即这些块之间的连接。 为了收集它们之间的这些块并将它们组合成一个步骤链,该库提供了一个构建器类StepAssembly (对于容器StepAssembly )。 它的实现使您可以将组合块字符串化为单个配置对象,例如字符串上的小珠,还可以指示对其他视图控制器的配置的依赖性。 将来如何配置取决于您。 您可以使用必要的参数将其馈送到路由器,它将为您构建一堆控制器,您可以将其保存到字典中,以后再通过按键使用-这取决于您的特定任务。


考虑一个简单的示例:假设,通过单击列表中的某个单元格,或者当应用程序从野生动物园或电子邮件客户端收到通用链接时,我们需要以模态方式显示具有特定productID的产品控制器。 在这种情况下,必须在UINavigationController内部构建产品控制器,以便它可以在其控制面板上显示其名称和关闭按钮。 此外,此产品只能显示给已登录的用户,否则,请邀请他们登录。


如果在不使用库的情况下分析此示例,则它将类似于以下内容:


 class ProductArrayViewController: UITableViewController { let products: [UUID]? let analyticsManager = AnalyticsManager.sharedInstance //  UITableViewControllerDelegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let productID = products[indexPath.row] else { return } //   LoginInterceptor guard !LoginManager.sharedInstance.isUserLoggedIn else { //    LoginViewController         `showProduct(with: productID)` return } showProduct(with: productID) } func showProduct(with productID: String) { //   ProductViewControllerFactory let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) //   ProductViewControllerContextTask productViewController.productID = productID //   NavigationControllerStep  PushToNavigationAction let navigationController = UINavigationController(rootViewController: productViewController) //   GenericActions.PresentModally present(alertController, animated: navigationController) { [weak self]   . ProductViewControllerPostTask self?.analyticsManager.trackProductView(productID: productID) } } } 

此示例不包括通用链接的实现,这将需要隔离授权代码,并在用户突然单击链接之后进行搜索以及搜索之后,应保持将用户定向到的上下文,并且已经向他展示了此产品,这最终将使代码非常实用。很难读。


考虑使用该库的示例配置:


 let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) //  : .adding(LoginInterceptor()) .adding(ProductViewControllerContextTask()) .adding(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) //  : .using(PushToNavigationAction()) .from(NavigationControllerStep()) // NavigationControllerStep -> StepAssembly(finder: NilFinder(), factory: NavigationControllerFactory()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() 

如果将其翻译成人类语言:


  • 检查用户是否已登录,如果没有提供输入
  • 如果用户已成功登录,请继续
  • Finder提供的搜索产品视图控制器
  • 如果找到-使可见并完成
  • 如果未找到-创建一个UINavigationController ,将ProductViewControllerFactory使用PushToNavigationAction创建的视图控制器集成到其中
  • 从当前视图控制器使用GenericActions.PresentModally UINavigationController

像许多复杂的解决方案一样,该配置需要进行一些研究,例如AutoLayout的概念,乍一看,它似乎很复杂且多余。 但是,给定代码片段要解决的任务数量涵盖了从授权到深度链接的所有方面,并且分成一系列动作可以轻松更改配置,而无需更改代码。 此外, StepAssembly的实现将帮助您避免步骤链不完整和类型控制的问题-不同视图控制器的输入参数不兼容的问题。


考虑一个完整的应用程序的伪代码,其中ProductArrayViewController在其中显示产品列表,如果用户选择此产品,则根据用户是否登录或者在成功登录后是否愿意登录并显示来显示该产品:


配置对象


 // `RoutingDestination`    .          . struct AppDestination: RoutingDestination { let finalStep: RoutingStep let context: Any? } struct Configuration { //     ,             static func productDestination(with productID: UUID) -> AppDestination { let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor()) .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(PushToNavigationAction()) .from(NavigationControllerStep()) .using(GenericActions.PresentModally()) .from(CurrentControllerStep()) .assemble() return AppDestination(finalStep: productScreen, context: productID) } } 


 class ProductArrayViewController: UITableViewController { let products: [UUID]? //... // DefaultRouter -  Router   ,   UIViewController   let router = DefaultRouter() override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let productID = products[indexPath.row] else { return } router.navigate(to: Configuration.productDestination(with: productID)) } } 


 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { //... func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { guard let productID = UniversalLinksManager.parse(url: url) else { return false } return DefaultRouter().navigate(to: Configuration.productDestination(with: productID)) == .handled } } 

.


. , , , — ProductArrayViewController, UINavigationController HomeViewController — StepAssembly from() . RouteComposer , ( ). , Configuration . , A/B , .


而不是结论


, 3 . , , . Fabric , Finder Action . , — , , . , .


, , objective c Cocoa Touch, . iOS 9 12.


UIViewController (MVC, MVVM, VIP, RIB, VIPER ..)


, , , . . .


.

Source: https://habr.com/ru/post/zh-CN421097/


All Articles