ARTICLE · 1167285
iOS两种模式之间的切换,也能做得这么细
点一下,选项变了,界面跟着更新。功能上,这已经说得过去。
但如果选中背景突然跳到另一边,上方画面瞬间替换,文字和底色又各变各的,即使每一步都没错,切换仍然会显得生硬。
Kavsoft 的这个 SwiftUI 主题选择器,把两种状态之间的过渡做得很细。

切换后,预览中的背景颜色逐渐变化,部分元素退出,另一部分进入;下方的选中背景也移动到新的位置,描边、图标颜色和说明文字一起回应这次选择。
画面使用什么图案,可以换。更值得拆解的是:这么多变化,怎样让人觉得它们属于同一次操作。
两张画面之间,还需要设计一个过程
看静态效果时,我们往往只准备两张图:模式 A 长什么样,模式 B 长什么样。
动画多出来的工作,是决定它们之间怎样衔接。
这段预览采用分层结构:底部是渐变背景,上面叠加不同的装饰元素,再覆盖前景形状。切换外观时,各层分别改变颜色、透明度或位置。
背景适合平滑变色,因为它承担着整块画面的基调。需要退出的元素,可以移动并淡出;需要进入的元素,则从对应的起始位置出现。一直存在的前景,保留轮廓,只调整配色。
这样,读者能在变化前后找到相同的结构,理解眼前仍然是同一个界面。
如果直接替换两张完整图片,也可以做出淡入淡出,但画面内部的元素只能一起交接。想让某个元素先退出、另一个元素沿自己的路径进入,就需要把它们拆成可控制的部分。
素材怎样组织,决定了动画能细到哪一步。
代价也在这里:分层越多,需要协调的参数和渲染工作就越多。一个只出现一次的小预览,不一定值得拆成几十层。先确定哪些变化能帮助用户理解切换,再决定细节做到哪里。
选中背景移动,按钮本身留在原处
下面的选择器同样有值得注意的地方。
几个选项的位置是固定的,移动的是它们背后的胶囊形选中背景。用户的视线可以一直跟着这块背景走,知道选择从哪里移到了哪里。
教程通过 matchedGeometryEffect,把不同选项下的选中背景关联起来。它们使用同一个命名空间和标识,选中项变化时,系统在旧位置与新位置之间插值,让视觉上看起来像同一块背景移动过去。
这里要分清两件事:@Namespace 提供用于匹配的命名空间,真正建立几何关联的是 matchedGeometryEffect。
它负责关联位置、尺寸等几何属性,不会替你决定所有视觉变化。描边怎样变色、阴影多浓、文字是否淡入,都仍然需要各自安排。
也不需要把整个按钮都拿去匹配。把效果集中在选中背景上,文字和触摸区域保持稳定,用户更容易连续操作。
迁移到自己的组件时,还应保证同一组匹配关系里的源视图明确,避免几个背景同时以相同标识充当源视图。动画看起来像一个对象在移动,代码里的对应关系也要清楚。
动画要跟着状态走
这种效果很容易写成一串时间安排:先改背景,过一会儿隐藏旧元素,再等一会儿显示新元素。
第一次点,可能很好看。用户在动画中途又点回去,问题就来了:前一次安排的任务还没结束,后一次切换已经开始,最终画面可能跟选中项对不上。
更稳妥的思路,是先确定当前目标状态,再让颜色、透明度、位置等属性朝着这个状态变化。
SwiftUI 的 animation(_:value:) 可以在指定值变化时为相关视图应用动画。关键是把动画作用范围放在需要过渡的区域,并选择真正驱动画面的状态作为触发值。
选中项变化,选中背景就移动;实际外观变化,预览画面才切换。
这两个变化经常同时发生,但也可能只有其中一个发生。把它们混成一个开关,后面处理“跟随系统”就容易出错。
对于装饰性的持续运动,教程另用了 KeyframeAnimator,让一个小元素按设定节奏移动、淡出和等待。它属于预览的氛围细节,和“用户刚刚切换了一次模式”是两种不同的动画任务。
将两者分开,既方便控制节奏,也方便在需要时关闭装饰动画,保留必要的操作反馈。
三个选项,只有两种实际外观
这个选择器有浅色、深色、跟随系统三个选项,预览却只需要呈现明、暗两种外观。
所以,用户选择的偏好,与当前实际显示的外观,应该分别理解。
选择浅色,实际外观为浅色。 选择深色,实际外观为深色。 选择跟随系统,实际外观由当前系统设置决定。
假设系统此刻就是深色,从“深色”切换到“跟随系统”,选中背景需要移动,说明文字需要更新,但预览不必为了表现“发生了点击”,强行播放一次明暗切换。
反过来,用户一直选着“跟随系统”,系统外观发生变化时,选中背景应该留在原处,预览则需要更新。

这也是为什么仅在点击按钮时更新一个 isDark 不够。实现还需要响应实际外观的变化,保证从设置返回、重新打开页面时,控件依然显示正确。
在 SwiftUI 中,可以通过 preferredColorScheme 表达外观偏好:浅色对应 .light,深色对应 .dark,不指定偏好则使用 nil。当前生效的外观可以从环境中的 colorScheme 读取。
不过,环境值表示的是当前视图收到的外观,它可能已经被上层覆盖,并不总等于设备原始设置。将偏好恢复为跟随系统后,要让实际生效的外观继续驱动预览,而不是拿之前缓存的值当作系统状态。
预览变了,不代表整个应用都设置好了
预览可以通过自己的状态控制颜色,但真正设置应用外观,还涉及作用范围。
preferredColorScheme 会影响最近的展示容器,例如窗口或弹层。把它放在一个弹层里,并不能据此认定所有窗口和页面都已经改好了。
如果这是应用级设置,就需要在合适的根层级应用偏好,并保存用户的选择。重新启动后,选中项、页面外观和预览应该对应同一份设置。
因此,验证这类组件时,除了看动画,还要检查几个实际操作:切换后关闭选择器,页面是否一致;重新打开,选择是否保留;选择跟随系统后改变系统外观,页面和预览是否一起响应。
这些关系正确,动画才是在解释设置结果,而不是用漂亮的画面掩盖状态不一致。
细节可以丰富,反馈不能拖慢
这种预览适合放在外观设置页,让用户在选择时看到差别。但每次切换都等待一段完整演出,就会影响操作。
我会优先保证选中状态及时更新、文字始终可读,再调整画面过渡。快速连续切换时,以最后一次选择为准,不让旧动画的收尾逻辑覆盖新状态。
还要照顾“减少动态效果”的设置。大幅移动和持续装饰动画可以减弱或关闭,必要的状态区别则通过文字、选中背景和颜色继续表达。装饰画面也不需要让 VoiceOver 把每个元素都读一遍。
这些机制不必等到最新系统才能使用。preferredColorScheme 在 iOS 13 就已可用,matchedGeometryEffect 从 iOS 14 开始支持;这里提到的 KeyframeAnimator 需要 iOS 17。完整复用教程时,其他样式和修饰符仍要分别检查,兼容旧系统时可以简化装饰部分。
两种模式的切换,最终还是一次设置操作。好的过渡会让这次操作更清楚:选择移动到了哪里,画面为什么变化,现在究竟处于哪种状态。那些细节,应该一起回答这几个问题。