SwiftUI如何在不重建视图的前提下实现条件隐藏视图
条件视图修饰符存在的问题
SwiftUI中如果用if/else分支给视图切分「带修饰符」和「不带修饰符」两种状态,会触发严重的视图重建问题。
最典型的就是条件隐藏的错误写法:
if condition { view } else { view.hidden() }
这种写法下SwiftUI会把两个分支里的view判定为完全无关的两个独立视图。分支切换时,原有视图会被直接销毁,新分支的视图会从头创建,会直接触发几个典型问题:
onAppear会重复触发- 视图内部的状态被完全重置
- 正在播放的动画会回到初始状态重放
- 分支间的过渡动画直接失效
- 哪怕手动给两个分支的视图加相同的
.id(),也没法解决这个类型判定问题
这种写法除了动画和状态问题,还会带来额外性能开销,如果onAppear里绑定了网络请求、本地数据加载这类重逻辑,还会触发重复请求、数据错乱等连锁bug。
常规无重建的条件修饰方案
对于本身支持传入参数的系统修饰符,直接用三元运算符把状态值传给修饰符即可,完全不会触发视图重建,比如控制内边距的写法:
view.padding(condition ? 10 : 0)
这种写法下SwiftUI会一直持有原视图实例,只是动态更新修饰符的参数值,视图生命周期、内部状态、动画上下文都会完整保留,运行表现完全正常。
不重建视图实现条件隐藏的方案
系统原生的.hidden()修饰符没有提供开关参数,没法直接套用三元传参的写法,要实现无重建的条件隐藏,直接用参数化的方式控制渲染属性即可,不要用分支切分视图。
推荐方案:自定义带开关的hidden修饰符
给View做个简单扩展,直接把隐藏状态作为参数传入,内部通过动态修改透明度、交互状态实现隐藏效果,逻辑和原生hidden完全一致,且不会触发任何重建:
extension View { func hidden(_ isHidden: Bool) -> some View { self .opacity(isHidden ? 0 : 1) .disabled(isHidden) .accessibilityHidden(isHidden) } }
使用时直接调用即可:
// 替代if/else分支写法,完全无重建 view.hidden(condition)
如果需要隐藏时不保留布局占位(原生.hidden()默认会保留视图的布局尺寸),可以给扩展加个参数控制,依然用参数化方式实现,不碰分支逻辑:
extension View { func hidden(_ isHidden: Bool, reserveLayout: Bool = true) -> some View { self .opacity(isHidden ? 0 : 1) .frame( width: !reserveLayout && isHidden ? 0 : nil, height: !reserveLayout && isHidden ? 0 : nil ) .disabled(isHidden) .accessibilityHidden(isHidden) } }
轻量方案:直接用opacity控制
如果不想写扩展,直接手动控制透明度即可,记得补上隐藏状态下的交互屏蔽,避免点到透明视图:
view .opacity(condition ? 0 : 1) .disabled(condition)
关键避坑点
不要为了省代码用@ViewBuilder写分支返回「带.hidden()的视图」和「不带.hidden()的视图」,这种写法和最开始提到的错误if/else逻辑本质完全一样,依然会触发视图重建。所有条件修饰的场景,核心原则都是 动态修改修饰符的参数,不要切分整个视图分支。
内容的提问来源于stack exchange,提问作者BlackWolf

