ScrollingStackViewController的edgeInsets秘密:容器视图自动包装机制揭秘
2026/8/23 12:35:58 网站建设 项目流程

ScrollingStackViewController的edgeInsets秘密:容器视图自动包装机制揭秘

【免费下载链接】ScrollingStackViewControllerA view controller that uses root views of child view controllers as views in a UIStackView.项目地址: https://gitcode.com/gh_mirrors/sc/ScrollingStackViewController

📦ScrollingStackViewController是 Just Eat 开源的 iOS 滚动容器控件,它用UIStackView+UIScrollView把多个子视图控制器纵向堆叠成一个可滚动的页面。它的edgeInsets内边距参数背后藏着一个巧妙的"容器视图自动包装"机制——今天我们就把这个秘密拆开来看。

一、edgeInsets 参数是什么?为什么新手容易踩坑?

普通的add(viewController:)会把子控制器的view直接塞进UIStackView。而带内边距的版本多了一个可选参数:

add(viewController: viewController, edgeInsets: insets)

一句话概括它的效果:让某个子控制器在页面中"缩进"显示(比如左右留白 40pt),常见于卡片式布局。示例工程里就给了 6~10 号区块加上了UIEdgeInsets(top: 20, left: 40, bottom: 20, right: 40)的内边距,效果就是页面下半部分的区块整体向里收缩,形成卡片感。

但很多新手遇到两个奇怪的现象:

  • ❓ 明明调用的是hide(viewController:),缩进的区块却没反应;
  • ❓ 明明调用的是scrollTo(viewController:),滚动定位却走了"别的分支"。

答案都藏在同一个秘密里:传了edgeInsets的子控制器,它的view并没有直接放进 StackView,而是被自动包进了一层新的容器视图。

二、自动包装机制:一行参数背后的四步操作

打开核心文件 ScrollingStackViewController.swift,insert(viewController:edgeInsets:at:)的逻辑可以拆成 4 步:

  1. 正常挂载子控制器:先执行addChilddidMove(toParent:),父子关系与不带内边距时完全一致;
  2. 动态创建容器视图:当edgeInsetsnil时,现场new一个containerView(一个纯UIView),并把子控制器的view作为子视图加进去,同时关闭双方的自动转换布局;
  3. 用 4 条约束还原内边距:子视图的上边距绑edgeInsets.top、左边距绑left,容器的下边距、右边距反向绑bottomright——内边距本质上就是"容器比子视图大一圈"的距离
  4. 把容器(而不是子视图)放入 StackViewstackView.insertArrangedSubview(containerView, at: index)

如果edgeInsetsnil,则跳过整个包装过程,直接insertArrangedSubview(viewController.view, at: index)

💡 换句话说,edgeInsets不是修改某个属性,而是悄悄地多插了一层视图层级

三、为什么必须用 show/hide 来操作它?

这是最容易被忽略的坑,README 里有一句简短提示:When a child view is added to a padding container view, the show/hide functions should be used.

原因很简单:StackView 的 arrangedSubview 是容器show/hide最终操作的是arrangedView(for:)返回的那个视图(见 arrangedView(for:) 实现)。它先查子视图本身,查不到就通过arrangedViewContainerIndex(for:)(L339-L345)向上查找"哪个 arrangedSubview 包含了我"。

所以正确的操作姿势是:

  • ✅ 插入时用add(viewController:edgeInsets:)
  • ✅ 之后对它的显隐、滚动都走show/hide/scrollTo这些"按控制器"的 API,它们会自动穿透容器找到正确对象;
  • ❌ 不要绕过这些 API 直接操作viewController.view的层级。

四、remove 时的"双重移除"细节

删除被包装的子控制器时,remove(arranged:) 方法 里有一行很关键的判断:

if arrangedView != viewController.view { viewController.view.removeFromSuperview() }

源码注释写得很直白:This happens when the view controller was added with edges.即"被内边距包装过"的子控制器,remove需要移除两次——先把容器视图从 StackView 摘掉,再把子视图从容器里摘出来,最后才走标准的willMove/removeFromParent流程。这套清理工作全部由框架自动完成,使用者只要调remove(_:)即可。

五、三个实用小贴士 🧩

  1. 位置定位同样"容器感知"insert支持.start/.end/.index(1)/.after(vc)/.before(vc)五种位置(Position 枚举定义)。定位.after/.before时用的arrangedViewOrContainerIndex(for:)会先查直接子视图、再查包裹容器,所以对包装过的区块也能精确定位;
  2. 背景色画在"容器间隙"spacingColorborderColor/borderWidth作用在stackViewBackgroundView上(viewDidLoad 布局逻辑)。内边距区块周围露出的间距会显示spacingColor,可以借此做出"卡片悬浮"的视觉层次;
  3. 子视图控制器必须自带高度约束:README 特别强调Make sure your child view controllers have constraints to self-size vertically。被容器包装后,容器的大小是由"子视图固有高度 + 四边内边距"推算出来的,若子视图不能自测量高,整条链路都会失效。

六、总结:一张图看懂机制

不带 edgeInsets: StackView ──┬── 子VC1.view └── 子VC2.view 带 edgeInsets: StackView ──┬── 子VC1.view └── containerView(容器) └── 子VC3.view(四周留出内边距)

edgeInsets的秘密并不复杂:传参 → 自动 new 容器 → 4 条约束留出内边距 → 容器进 StackView;而show/hide/scrollTo/remove全链路都做了"容器感知"的兼容,只要坚持用按控制器操作的公开 API,就能零成本享受卡片式布局。

📂 想动手试试,完整示例在 ViewController.swift(第 6~10 号区块都用了edgeInsets插入),单元测试见 ScrollingStackViewTests.swift,Pod 依赖声明见 ScrollingStackViewController.podspec。

【免费下载链接】ScrollingStackViewControllerA view controller that uses root views of child view controllers as views in a UIStackView.项目地址: https://gitcode.com/gh_mirrors/sc/ScrollingStackViewController

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询