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 步:
- 正常挂载子控制器:先执行
addChild和didMove(toParent:),父子关系与不带内边距时完全一致; - 动态创建容器视图:当
edgeInsets非nil时,现场new一个containerView(一个纯UIView),并把子控制器的view作为子视图加进去,同时关闭双方的自动转换布局; - 用 4 条约束还原内边距:子视图的上边距绑
edgeInsets.top、左边距绑left,容器的下边距、右边距反向绑bottom和right——内边距本质上就是"容器比子视图大一圈"的距离; - 把容器(而不是子视图)放入 StackView:
stackView.insertArrangedSubview(containerView, at: index)。
如果edgeInsets是nil,则跳过整个包装过程,直接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(_:)即可。
五、三个实用小贴士 🧩
- 位置定位同样"容器感知":
insert支持.start/.end/.index(1)/.after(vc)/.before(vc)五种位置(Position 枚举定义)。定位.after/.before时用的arrangedViewOrContainerIndex(for:)会先查直接子视图、再查包裹容器,所以对包装过的区块也能精确定位; - 背景色画在"容器间隙":
spacingColor和borderColor/borderWidth作用在stackViewBackgroundView上(viewDidLoad 布局逻辑)。内边距区块周围露出的间距会显示spacingColor,可以借此做出"卡片悬浮"的视觉层次; - 子视图控制器必须自带高度约束: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),仅供参考