编写选项卡式容器视图控制器

我今年重点关注的事情之一是改善应用程序中的导航。 使类菜单更易于打开,为iPad添加一个拆分视图控制器以及简化应用程序启动期间的导航设置过程,为我们的用户和开发人员提供了改善的体验。 最近,我在一个视图控制器容器上工作,该容器可快速滑动即可在“消息”和“人员”屏幕之间切换,而无需敲击标题(然后突然更改视图)。 在视图之间滑动会感觉很好,并且可以按照用户期望的方式工作。

难题的核心部分是选项卡容器本身,滚动视图子类以及带有按钮的控件,用于显示子视图控制器标题。 在此处获取示例代码,然后继续。

当执行这样的任务时,从一个空项目开始,使所有工作正常运行,然后再集成回主应用程序将很有帮助。 这样可以通过减少编译时间来实现快速迭代,并避免无意中依赖于任何可能影响布局或外观的特定于应用程序的行为。

UITabBarController的界面中RDTopTabBarController灵感, RDTopTabBarController定义了方法setViewControllers(_:animated:) 。 它首先删除所有现有的子视图控制器,然后将新的子视图控制器添加为子视图控制器,并将其视图添加到滚动视图。 Apple的容器视图控制器指南提供了有关添加和删除子视图控制器的清晰说明,并提供了大量有用的信息。 RDMainViewController创建一个由三个表视图控制器组成的数组,以放入新的选项卡栏控制器中。 运行代码仅显示其中一个表-现在需要在滚动视图中正确布局它们。

滚动视图的职责是并排布置子视图控制器的视图,以便可以通过滑动来浏览它们。 为此,子类在其初始值设定isPagingEnabled中设置isPagingEnabled并覆盖layoutSubviews以将视图的大小调整为与其bounds相同的大小,然后按从左到右的顺序放置它们。 滚动视图还会在layoutSubviews更新其contentSize ,以便其可滚动区域的大小保持更新。 RDTabScrollView具有pagedViews属性,该属性仅用于跟踪子视图控制器的视图,而不依赖于subviews属性,因为滚动视图还包含滚动指示器和其他自定义视图。 此时,可以在表格视图控制器之间向左或向右滑动,这需要的是一个用于显示标题和切换视图的控件。

选项卡控件位于选项卡视图控制器的顶部,带有每个子视图控制器的按钮和一个指示器视图,该指示器视图在当前显示的视图控制器的标题下左右滑动。 它的setTitles(_:)方法创建带有传入标题的按钮。 要实现的第一项功能是点击一个按钮以滚动到相应的视图。

RDTabControl使用tabControl(_:didSelectButtonAt:)方法通知其委托人何时按下按钮。 RDTopTabBarController使用此方法来动画化滚动到相应的页面。 在实现中,使用索引检索子视图控制器,然后在调用scrollRectToVisible(_:animated:)时使用其视图的框架。

在滚动视图滚动时移动指示器视图需要信息从滚动视图到选项卡控件沿相反的方向流动。 滚动视图委托方法scrollViewDidScroll(_:)用于在滚动视图scrollViewDidScroll(_:)移动时更新指标。 选项卡控件中指示器的x位置与滚动视图的内容偏移量除以滚动视图内容的宽度成比例: scrollView.bounds.width * (scrollView.contentOffset.x / scrollView.contentSize.width) 。 当指示器移动时,最可见视图的按钮变暗。 使用mostVisibleIndex上的计算变量mostVisibleIndex来查找原点最接近内容偏移量的视图,从而更新相应按钮的状态。

为了使选项卡控件的显示更加灵活,它嵌入在UIStackView ,这使得在照顾所有调整大小的同时,易于显示或隐藏按钮。 堆栈视图还可以用于显示选项卡控件下方的视图,Remind应用程序将其用于横幅和Internet连接状态栏。

此时,当视图出现时,表视图看起来就像稍微向上滚动一样,因为选项卡控件覆盖了第一行。 尽管每个子视图控制器的内容插入都是在viewDidLayoutSubviews设置的,允许向上滚动表以查看其内容,但这不会将内容重新定位。 为了解决这个问题,当将每个子视图控制器的视图添加为子视图时,将设置它们的内容偏移量。

需要解决的另一个更加严重的问题是设备旋转时滚动视图和选项卡控件的行为。 在整个过渡过程中,内容偏移量保持不变,从而在页面之间保留滚动视图。 看起来不太好:

发生的情况是,在轮换之前,每个页面的宽度为320点, contentOffset.x为640点。 旋转后,水平偏移需要为1136点才能与第三页对齐,因为每个页面的宽度都会增加到568点。 最初,我使用viewWillTransition(size:coordinator:)进行一些混乱的布局操作,但经过进一步研究,设计了一个更简单的解决方案。 两个滚动视图委托方法用于使滚动视图的currentPage属性与可见视图保持同步。 每当滚动视图由于按钮而完成拖动或滚动动画完成时,请按下委托方法scrollViewDidEndDecelerating(_:)scrollViewDidEndScrollingAnimation(_:)更新页面。 在layoutSubviews使用currentPage属性设置内容偏移量,使其在可见页面的边缘对齐。 仅当滚动视图的宽度改变时才设置内容偏移量,否则滚动将不起作用,因为每当滚动开始时都会调用layoutSubviews 。 好多了:

Remind的应用程序在屏幕左侧有一个抽屉,可以将其滑动打开。 为了允许该手势识别器开始,需要禁用滚动视图的跳动,否则它将拦截平移手势,从不允许其到达抽屉。 RDTabScrollView使用UIScrollViewbounces属性来禁用所有弹跳, RDTabScrollView会覆盖gestureRecognizerShouldBegin(_:)并使用其allowedBounceEdges属性在边缘时禁用滚动手势,从而将手势传递到抽屉容器。 这样可以将弹跳保持在屏幕的另一侧。

最后,将制表符控制器添加到导航控制器是处理行选择,然后推送详细视图控制器的重要用例。 导航栏和标签栏按钮需要显示为单个视图。 Apple提供了一个出色的示例项目,其中包含各种导航栏自定义示例。 将导航栏的isTranslucent设置为false ,将其阴影图像设置为透明像素,将背景图像设置为纯白色像素,可以实现无缝外观。

在大多数情况下,编写此容器视图控制器非常简单。 但是,与任何通用组件一样,在保持简单接口和实现的同时正确获取细节可能是最耗时的工作。 希望通过记录我遇到的问题,可以为其他人节省一些时间,并可以一窥开发过程。 谢谢阅读!


最初 Phil Webster 发表在 engineering.remind.com上