资讯动态

iOS毛玻璃效果全解析:UIVisualEffectView与UIBlurEffect实战指南

发布时间:2026/10/17 21:25:07 来源:尧图企业网站定制
1. 从一次视觉改版说起为什么iOS端毛玻璃效果值得单独拎出来讲前阵子帮一个朋友改他的独立App首页原本是纯色卡片堆叠数据一多就显得特别平层级感全靠阴影硬撑。他想要那种一眼看上去就很iOS原生的质感——背景图透出来、内容浮在上面、边缘有一层柔和的雾面过渡。这个需求翻译成技术语言就是高斯模糊Gaussian Blur在苹果的设计语言里叫毛玻璃Frosted Glass落到代码层面最省事的入口就是UIVisualEffectView配合UIBlurEffect。很多人第一次接触这个效果会下意识去找一个叫BlurView的控件。这里得先澄清一个容易踩的认知坑UIKit 里并没有一个叫BlurView的官方类。你搜到的BlurView绝大多数是第三方封装或者社区里对UIVisualEffectView的俗称。真正干活的是UIVisualEffectView它接收一个UIVisualEffect对象而UIBlurEffect是UIVisualEffect的子类专门负责模糊。搞不清这层关系后面调参数、做动画、处理层级的时候就会一头雾水。这篇内容我打算把毛玻璃这件事从头到尾讲透从UIBlurEffect的几种样式到底差在哪、UIVisualEffectView的视图层级为什么会让很多人加了模糊却看不到内容、到实际项目里怎么封装一个可复用的模糊视图、性能上什么时候该收手、以及 SwiftUI 时代这套东西又该怎么写。适合已经能写基础 UIKit 界面、但一碰到视觉效果就靠试参数的开发者也适合想把界面质感往上提一个档次的独立开发者。先说结论毛玻璃不是加个模糊就完事它本质上是一个材质Material系统牵扯到采样、混合、动态适配明暗模式、以及 GPU 合成开销。理解它的工作机制比记住几个枚举值重要得多。2. UIBlurEffect 的样式谱系五种枚举值背后的设计意图2.1 从 extraLight 到 systemMaterial 的演进逻辑UIBlurEffect.Style这个枚举早期只有三个值extraLight、light、dark。后来苹果一路加到了现在的十来个包括regular、prominent、systemUltraThinMaterial这一整族。很多人看到这么多选项直接懵其实它们可以按两个维度来理解模糊强度和明暗倾向。早期的extraLight、light、dark是固定色调思路——不管你的内容是什么颜色模糊层都往某个方向偏。extraLight最亮最透适合压在深色背景上dark最暗适合压在浅色内容上做遮罩。这套思路的问题在于它不感知系统是深色模式还是浅色模式你在浅色模式下调好的效果用户切到深色模式就崩了。于是苹果引入了systemMaterial系列这才是现在应该优先用的。systemUltraThinMaterial、systemThinMaterial、systemMaterial、systemThickMaterial、systemChromeMaterial从极薄到极厚模糊层的不透明度逐渐增加透出的背景越来越少。它们会自动跟随系统的明暗模式切换色调这才是原生感的来源。2.2 一张表看清各样式该用在哪样式模糊强度典型场景明暗自适应systemUltraThinMaterial极弱悬浮在图片上的小标签是systemThinMaterial弱顶部导航栏、工具栏是systemMaterial中弹出菜单、卡片背景是systemThickMaterial强模态弹窗背景是systemChromeMaterial强底部标签栏是extraLight弱旧代码兼容否light中旧代码兼容否dark中旧代码兼容否regular中通用场景部分prominent强需要强调的前景部分我个人的选型习惯是新项目一律用systemMaterial系列老代码里如果看到extraLight/light/dark能换就换。原因很简单前三个在深色模式下经常出现糊成一团黑或者亮得刺眼的问题而 system 系列是苹果根据当前系统外观动态计算的省心。2.3 为什么 regular 和 prominent 有点特殊regular和prominent这两个样式比较微妙。它们不像 system 系列那样明确绑定系统材质而是更偏向通用模糊。regular适合大多数需要模糊但不需要特定材质感的场景prominent则会在模糊基础上增加一点前景的对比度让压在上面的文字更清晰。实测下来prominent在模糊层上要放白色文字的场景里表现不错因为它会稍微压暗背景提升文字可读性。但如果你追求的是那种若有若无的轻盈感regular或者systemThinMaterial更合适。这里没有绝对的对错取决于你的设计稿想要什么调性。3. UIVisualEffectView 的视图层级陷阱为什么你的模糊层看不见内容3.1 contentView 不是可选项是必选项这是新手最容易踩的坑没有之一。UIVisualEffectView有一个contentView属性你所有想放在模糊层上面的子视图必须加到contentView上而不是直接加到UIVisualEffectView本身。let blurEffect UIBlurEffect(style: .systemMaterial) let blurView UIVisualEffectView(effect: blurEffect) blurView.frame view.bounds // 错误做法直接加到 blurView 上 // blurView.addSubview(label) // 这个 label 会被模糊效果吃掉 // 正确做法加到 contentView 上 let label UILabel() label.text 浮在毛玻璃上的文字 blurView.contentView.addSubview(label)为什么会有这个设计因为UIVisualEffectView内部其实有两层一层负责渲染模糊效果一层也就是contentView负责承载你的内容。如果你直接往UIVisualEffectView上加子视图这些视图会被插入到模糊渲染层和 contentView 之间的某个位置结果就是要么被模糊掉要么层级错乱。这个坑我见过太多人踩包括一些工作两三年的开发者。3.2 模糊层会穿透到它下面的所有内容UIVisualEffectView的模糊是实时采样它下方的内容。这意味着如果你把模糊视图加在一个纯色背景上你看到的就是纯色被模糊——几乎看不出效果。毛玻璃的精髓在于透出有细节的背景所以它下面最好是有图片、有渐变、有内容的视图。这就引出一个实践问题如果你想让整个页面背景模糊正确的做法是把模糊视图盖在背景图上面而不是把背景图设成模糊视图的子视图。顺序反了效果就没了。// 正确背景图在下模糊层在上 view.addSubview(backgroundImageView) view.addSubview(blurView) // 错误把背景图塞进模糊层它会被模糊两次或者根本不显示3.3 动画切换 effect 时的注意事项UIVisualEffectView的effect属性是可以动画切换的但有个细节你不能直接把effect从 nil 动画到某个 blurEffect那样会有一个突兀的跳变。正确的做法是用UIView.animate包裹并且确保起始和结束都是有效的 effect 对象。UIView.animate(withDuration: 0.3) { blurView.effect UIBlurEffect(style: .systemThickMaterial) }如果你需要从无模糊过渡到有模糊建议用一个透明度为 0 的模糊视图先占位然后动画它的alpha而不是动画effect本身。这个技巧在做弹出菜单、下拉刷新这类交互时特别有用。4. 封装一个能直接抄作业的毛玻璃视图4.1 基础封装BlurContainerView与其在每个页面重复写UIVisualEffectView的初始化代码不如封装一个轻量的容器。下面这个是我在多个项目里用过的版本支持圆角、边框、内容插入import UIKit final class BlurContainerView: UIView { private let blurView: UIVisualEffectView var contentView: UIView { return blurView.contentView } init(style: UIBlurEffect.Style .systemMaterial, cornerRadius: CGFloat 0) { let effect UIBlurEffect(style: style) self.blurView UIVisualEffectView(effect: effect) super.init(frame: .zero) blurView.translatesAutoresizingMaskIntoConstraints false addSubview(blurView) NSLayoutConstraint.activate([ blurView.topAnchor.constraint(equalTo: topAnchor), blurView.bottomAnchor.constraint(equalTo: bottomAnchor), blurView.leadingAnchor.constraint(equalTo: leadingAnchor), blurView.trailingAnchor.constraint(equalTo: trailingAnchor) ]) if cornerRadius 0 { layer.cornerRadius cornerRadius layer.masksToBounds true } } required init?(coder: NSCoder) { fatalError(init(coder:) has not been implemented) } func updateStyle(_ style: UIBlurEffect.Style, animated: Bool true) { let newEffect UIBlurEffect(style: style) if animated { UIView.animate(withDuration: 0.25) { self.blurView.effect newEffect } } else { blurView.effect newEffect } } }用的时候就很清爽let card BlurContainerView(style: .systemThinMaterial, cornerRadius: 16) card.frame CGRect(x: 20, y: 100, width: 300, height: 200) view.addSubview(card) let titleLabel UILabel() titleLabel.text 毛玻璃卡片 card.contentView.addSubview(titleLabel)4.2 圆角与 masksToBounds 的坑上面代码里用了layer.masksToBounds true来裁圆角。这里有个性能提醒masksToBounds会触发离屏渲染如果页面上有大量毛玻璃视图同时开圆角滚动时可能掉帧。替代方案是用layer.cornerRadius配合layer.cornerCurve .continuousiOS 13并且尽量让模糊视图本身不裁剪而是裁剪它的父容器。另一个细节是UIVisualEffectView自己也有layer.cornerRadius但直接设它有时候不生效因为模糊渲染层和 contentView 是分开的。稳妥的做法还是在外层容器上做圆角。4.3 给模糊层加边框和阴影的正确姿势毛玻璃卡片通常需要一点边框来强化轮廓。但如果你在BlurContainerView上直接加layer.borderWidth边框会被masksToBounds裁掉一部分。我的做法是再套一层let wrapper UIView() wrapper.layer.cornerRadius 16 wrapper.layer.cornerCurve .continuous wrapper.layer.borderWidth 0.5 wrapper.layer.borderColor UIColor.separator.cgColor wrapper.layer.masksToBounds true let blur BlurContainerView(style: .systemMaterial) wrapper.addSubview(blur) // blur 约束铺满 wrapper阴影则要加在 wrapper 的父层或者 wrapper 自己但不开masksToBounds。阴影和圆角裁剪天生冲突这是 UIKit 的老问题解决方式就是分层一层负责裁剪一层负责阴影。5. 性能账本毛玻璃什么时候会拖垮你的滚动帧率5.1 模糊是 GPU 实时合成不是静态图片很多人以为毛玻璃是一张预先渲染好的图其实不是。UIVisualEffectView每一帧都在采样它下方的内容并做高斯模糊计算这是实打实的 GPU 开销。单个模糊视图在 A12 以上的芯片上几乎无感但如果你在一个UITableView的每个 cell 里都塞一个毛玻璃背景滚动时帧率会肉眼可见地掉。我做过一个粗略的测试在 iPhone 上放 20 个带毛玻璃的卡片滚动时帧率从 60 掉到 40 左右放到 50 个直接掉到 30 以下。所以列表类界面慎用大面积毛玻璃尤其是 cell 复用频繁的场景。5.2 减少模糊视图数量的几个实用手段第一能合并就合并。如果多个卡片共享同一块背景考虑用一个大的模糊视图垫底卡片本身用半透明色而不是各自模糊。第二静态场景用截图替代。如果背景内容不会变比如一张固定的背景图可以预先用UIGraphicsImageRenderer渲染一张模糊图直接用UIImageView显示。这样运行时零 GPU 模糊开销。func blurredImage(from image: UIImage, radius: CGFloat) - UIImage? { guard let ciImage CIImage(image: image) else { return nil } let filter CIFilter(name: CIGaussianBlur) filter?.setValue(ciImage, forKey: kCIInputImageKey) filter?.setValue(radius, forKey: kCIInputRadiusKey) guard let output filter?.outputImage else { return nil } let context CIContext() guard let cgImage context.createCGImage(output, from: ciImage.extent) else { return nil } return UIImage(cgImage: cgImage) }第三降低模糊视图的层级复杂度。模糊视图下面如果还有复杂的图层树采样成本会上升。尽量让模糊视图直接盖在简单的背景上。5.3 用 Instruments 定位模糊相关的卡顿如果你怀疑卡顿来自毛玻璃打开 Instruments 的Core Animation模板勾选 Color Offscreen-Rendered Yellow 和 Color Blended Layers。离屏渲染的区域会标黄混合过多的区域会标红。毛玻璃视图通常会显示为黄色或红色这时候你就知道该优化哪里了。另一个指标是 Color Copied Images如果模糊采样触发了大量图片拷贝也会拖慢性能。这些工具用起来不复杂但能帮你把感觉卡变成知道哪里卡。6. SwiftUI 里的毛玻璃从 Material 到 toolbarBackground6.1 .background(.ultraThinMaterial) 的用法SwiftUI 把毛玻璃抽象成了Material类型用起来比 UIKit 简洁得多struct ContentView: View { var body: some View { ZStack { Image(background) .resizable() .scaledToFill() .ignoresSafeArea() VStack { Text(浮在毛玻璃上的内容) .padding() } .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) } } }.ultraThinMaterial、.thinMaterial、.regularMaterial、.thickMaterial、.bar这几个对应 UIKit 的 system 系列。SwiftUI 会自动处理明暗模式和层级省去了contentView那套心智负担。6.2 toolbarBackground 与 navigationBar 的材质控制iOS 16 之后SwiftUI 提供了.toolbarBackground修饰符可以精细控制导航栏和工具栏的材质NavigationStack { List { // ... } .toolbarBackground(.ultraThinMaterial, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) }这里有个坑.toolbarBackground默认是.automatic在某些滚动场景下导航栏会突然从透明变成有背景。如果你想要始终毛玻璃的效果必须显式设置.visible否则滚动到顶部时它会变透明视觉上会跳。6.3 UIKit 与 SwiftUI 混用时的材质一致性如果你的项目是 UIKit 为主、局部嵌 SwiftUI或者反过来要注意两边的材质命名虽然相似但渲染结果可能有细微差异。比如 UIKit 的.systemThinMaterial和 SwiftUI 的.thinMaterial在同一个界面上并排显示色调可能不完全一致。我的建议是同一个视觉区域内材质来源保持一致。要么全用 UIKit 的UIVisualEffectView要么全用 SwiftUI 的Material不要混着来。如果必须混用UIViewRepresentable把 UIKit 的模糊视图包进 SwiftUI这样至少渲染路径是统一的。7. 几个只有实际做过才会知道的细节7.1 深色模式下的对比度问题system 系列材质虽然会自动适配明暗模式但不代表你什么都不用管。在深色模式下.systemThinMaterial压在一张深色图片上模糊层会变得非常暗上面的深色文字可能完全看不清。这时候要么换更亮的材质.systemUltraThinMaterial要么给文字加一层浅色描边或阴影。我习惯在深色模式下面测一遍所有毛玻璃界面尤其是那些文字压在模糊层上的地方。设计稿通常只给浅色模式深色模式的问题得自己发现。7.2 模糊层上的分割线会消失如果你在毛玻璃卡片里放UITableView或者带分割线的列表会发现分割线在模糊背景上几乎看不见。原因是分割线通常是半透明的灰色叠在同样半透明的模糊层上对比度被进一步稀释。解决办法是给分割线用更实的颜色比如UIColor.separator在 iOS 14 已经考虑了材质背景但如果你用的是自定义颜色记得手动调高不透明度。7.3 键盘弹出时模糊层的布局抖动这个坑比较隐蔽。当键盘弹出、页面整体上移时如果模糊视图的约束是相对于view的bottomAnchor它可能会跟着键盘一起动导致模糊区域错位。正确的做法是把模糊视图的约束绑到安全区域或者一个固定的容器上而不是直接绑到会随键盘变化的锚点。// 容易出问题 blurView.bottomAnchor.constraint(equalTo: view.bottomAnchor) // 更稳 blurView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)具体用哪个取决于你的设计意图但心里要清楚键盘会改变什么。7.4 截图和录屏时毛玻璃的表现UIVisualEffectView在UIGraphicsImageRenderer截图时有时候会渲染成空白或者纯色因为它依赖实时合成。如果你需要把带毛玻璃的界面导出成图片稳妥的做法是先把模糊效果烘焙成一张静态图再合成。这个在生成分享图、海报的场景里会碰到。8. 从毛玻璃延伸出去材质系统的设计思维毛玻璃本质上只是材质这个更大概念的一个实现。苹果从 iOS 13 开始系统性地推 Material 设计语言核心思想是界面元素不应该是不透明的色块而应该像真实世界的玻璃、纸张、金属一样有透光性、有厚度、有层级关系。理解这一点你在选材质的时候就不会只盯着模糊强度看而会考虑这个元素在层级里处于什么位置。导航栏用薄材质因为它要透出内容但保持可读模态弹窗用厚材质因为它要阻断背景干扰悬浮按钮用极薄材质因为它要轻盈地浮在内容之上。这套思维迁移到其他平台也成立。Android 的 Material You 有类似的动态色彩和表面色调系统Web 端的backdrop-filter也是同一个思路。学会用材质而不是模糊来思考你的界面设计能力会上一个台阶。最后分享一个我自己的习惯每次做完一个毛玻璃界面我会在真机上用最深色的背景图和最浅色的背景图各测一遍再切一次深色模式。三遍下来没问题这个效果才算真正稳了。模拟器上的表现和真机经常有差异尤其是老设备别偷懒。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑