在 SwiftUI 中使用 Metal Shader
简介
从 iOS 17/macOS 14 开始,SwiftUI 支持使用 Metal shader 来实现一些特效。主要提供三个 View Modifier:colorEffect、 distortionEffect 和 layerEffect 。每个 modifier 的第一个参数是传入的 Shader 实例。
此外,View 实例还新增了一个 visualEffect modifier,用于暴露修饰内容的布局信息。函数签名为 func visualEffect(_ effect: @escaping (EmptyVisualEffect, GeometryProxy) -> some VisualEffect) -> some View ,在这个闭包中给 EmptyVisualEffect 添加上面的三种 shader modifier,通过 GeometryProxy 参数来获取所修饰内容的 size 等信息,可以进一步传递给 shader function。
可惜的是,这些 modifier 只适用于 SwiftUI 的 View,不适用于 UIKit/AppKit 包的 View。
用法
Shader Function
Shader 构造函数为 init(function: ShaderFunction, arguments: [Shader.Argument],而 ShaderFunction 的构造函数为 init(library: ShaderLibrary, name: String)。ShaderLibrary 有一个 static 成员 default,表示 app 的 main bundle 中的 shader library。此外 ShaderLibrary 还提供了 static subscript(dynamicMember _: String) -> ShaderFunction 方法,返回 default shader library 中名字为 name 的 MSL function。
三个 View Modifier 分别操作不同的元素,实现不同的效果,也对 MSL 函数有着各自不同的要求,下面一一介绍。
colorEffect
签名如下:
func colorEffect(
_ shader: Shader,
isEnabled: Bool = true
) -> some View
该 modifier 用来操作每个单独的像素,要求提供的 MSL 函数的签名必须和下面的匹配:
[[ stitchable ]] half4 name(float2 position, half4 color, args...)
其中 position 和 color 参数在运行 shader 函数的时候会自动传入,position 表示像素在 user-space 坐标系下的坐标(相对的,Metal 的 clip-space 坐标系区域为 (-1.0, -1.0) 到 (1.0, 1.0)),color 是当前 position 对应像素的颜色。我们也可以通过 args… 可变参数传入自定义的数据。该 shader 函数返回处理后的像素颜色(Fragment shader)。
示例 Shader:
[[ stitchable ]] half4 colorCircle(float2 position, half4 currentColor, float2 size, float radius, half4 circleColor) {
float2 center = size / 2; // 计算 view 的中心点
if (length(position - center) < radius) {
return circleColor * currentColor.a;
} else {
return currentColor;
}
}
在上面的 shader 函数中,除了会默认提供的两个参数 position 和 currentColor 外,我们还额外提供了三个参数 size,radius,circleColor,这三个函数需要在SwiftUI 中进行指定,如下所示:
struct ContentView: View {
let start = Date()
var body: some View {
ZStack {
TimelineView(.animation) { _ in
Text("")
.font(.system(size: 80, weight: .black))
.visualEffect { content, geometryProxy in
content
.colorEffect(ShaderLibrary.colorCircle(
.float2(geometryProxy.size),
.float(abs(start.timeIntervalSinceNow) * 10),
.color(.purple)
))
}
}
}
.padding()
}
}
运行效果:

layerEffect
layerEffect 类似于 colorEffect,也是一个 fragment shader,返回处理后的像素颜色,但是不同于 colorEffect shader 函数参数只给我们提供 position 位置对应的单个像素的颜色,layerEffect 给我们提供了被修饰 View 的整个 layer,这样我们就可以实现一些上下文相关的效果,比如高斯模糊。该 modifier 签名如下:
func layerEffect(
_ shader: Shader,
maxSampleOffset: CGSize, // 该参数说明见下
isEnabled: Bool = true
) -> some View
要求提供的 MSL 函数的签名必须和下面的匹配:
[[ stitchable ]] half4 name(float2 position, SwiftUI::Layer layer, args...)
SwiftUI::Layer 只暴露出了一个 half4 sample(float2 p) 函数,返回的是被修饰内容里,坐标 p 处的线性插值颜色值,该函数的实现在头文件里给出了,代码如下:
half4 sample(float2 p) const {
p = metal::fma(p.x, info[0], metal::fma(p.y, info[1], info[2]));
p = metal::clamp(p, info[3], info[4]);
return tex.sample(metal::sampler(metal::filter::linear), p);
}
这里看起来会对传入的坐标 p 做 clamp,线下试过传越界值的时候返回的是透明色值,但是因为不知道 info 是什么数据,也没用找到明确的文档说明,如果比较谨慎的话可以自己对 p 做越界处理。
回过头来看 modifier 的 maxSampleOffset 参数,该参数是指在 shader 函数中,对 layer 调用 sample 取像素色值时,如果传入的坐标不是当前的坐标 position 而是其他坐标,则可以计算出一个相对当前左边的偏移距离 distance,maxSampleOffset 则是所有调用中的 distance 的最大值。(但是线下测试时传 .zero 却没有出现问题,比较奇怪)
Shader,需要引用相关头文件:
#include <SwiftUI/SwiftUI_Metal.h>
[[ stitchable ]] half4 gaussianBlur(float2 position, SwiftUI::Layer layer) {
return
layer.sample(position) * 0.0707355 +
layer.sample(position + float2(-1, -1)) * 0.0453542 +
layer.sample(position + float2(0, -1)) * 0.0566406 +
layer.sample(position + float2(1, -1)) * 0.0453542 +
layer.sample(position + float2(-1, 0)) * 0.0566406 +
layer.sample(position + float2(1, 0)) * 0.0566406 +
layer.sample(position + float2(-1, 1)) * 0.0453542 +
layer.sample(position + float2(0, 1)) * 0.0566406 +
layer.sample(position + float2(1, 1)) * 0.0453542;
}
示例:
struct ContentView: View {
var body: some View {
HStack {
Text("")
.font(.system(size: 80, weight: .black))
Text("")
.font(.system(size: 80, weight: .black))
.layerEffect(ShaderLibrary.gaussianBlur(),
maxSampleOffset: .init(width: 3, height: 3))
}
.padding()
}
}
运行效果:

distortionEffect
不同于前两者,distortionEffect 使用的是一个 vertex shader,即返回的不是一个 half4 类型的颜色值,而是一个 float2 类型的坐标值,即改变每一个像素的位置,从而实现一些扭曲变形的效果。该 modifier 的签名如下:
func distortionEffect(
_ shader: Shader,
maxSampleOffset: CGSize, // 该参数含义同 layerEffect
isEnabled: Bool = true
) -> some View
要求提供的 MSL 函数的签名必须和下面的匹配:
[[ stitchable ]] float2 name(float2 position, args...)
示例 Shader:
[[ stitchable ]] float2 stretch(float2 position, float2 size) {
float midY = size.y / 2;
return position + float2(30 * abs((position.y - midY) / midY), 0);
}
示例:
struct ContentView: View {
var body: some View {
ZStack {
Text("")
.font(.system(size: 80, weight: .black))
.visualEffect { context, proxy in
context
.distortionEffect(
ShaderLibrary.stretch(.float2(proxy.size)),
maxSampleOffset: .init(width: proxy.size.width / 2, height: proxy.size.height / 2))
}
}
.padding()
}
}
运行效果:

References
- How to add Metal shaders to SwiftUI views using layer effects
- https://developer.apple.com/documentation/swiftui/view/coloreffect(_:isenabled:)
- https://developer.apple.com/documentation/swiftui/view/distortioneffect(_:maxsampleoffset:isenabled:)
- https://developer.apple.com/documentation/swiftui/view/layereffect(_:maxsampleoffset:isenabled:)
在 SwiftUI 中使用 Metal Shader的更多相关文章
- 【玩转cocos2d-x之四十】怎样在Cocos2d-x 3.0中使用opengl shader?
有小伙伴提出了这个问题.事实上GLProgramCocos2d-x引擎自带了.全然能够直接拿来用. 先上图吧. 使用opengl前后的对照: watermark/2/text/aHR0cDovL2Js ...
- Unity5中新的Shader体系简析
一.Unity5中新的Shader体系简析 Unity5和之前的书写模式有了一定的改变.Unity5时代的Shader Reference官方文档也进一步地变得丰满. 主要需要了解到的是,在原来的Un ...
- iOS开发 - 在SwiftUI中显示模态视图
在SwiftUI中显示模态视图 简介 这里教大家如何弹出一个简单的模态视图.分别有两个页面,ContentView和GCPresentedView,以下对应简称为A和B.我们要做的是在A视图中点击按钮 ...
- 【原创翻译】初识Unity中的Compute Shader
一直以来都想试着自己翻译一些东西,现在发现翻译真的很不容易,如果你直接把作者的原文按照英文的思维翻译过来,你会发现中国人读起来很是别扭,但是如果你想完全利用中国人的语言方式来翻译,又怕自己理解的不到位 ...
- unity中使用自定义shader进行光照贴图烘培无法出现透明度的坑爹问题
最近开发中在对场景进行光照贴图烘焙时发现一个坑爹问题,在使用自定义shader的时候,shader命名中必须包含Transparent路径,否则烘焙的时候不对alpha通道进行计算,烘焙出来都是狗皮膏 ...
- 【日常记录】Unity3D 中的 Surface Shader 是不支持在 Pass中使用的,因为自动生成了 Pass
如题 搞了好久,一直报错: Shader error in 'custom_outline_effect': Parse error: syntax error, unexpected TOK_PAS ...
- SwiftUI 中一些和响应式状态有关的属性包装器的用途
SwiftUI 借鉴了 React 等 UI 框架的概念,通过 state 的变化,对 View 进行响应式的渲染.主要通过 @State, @StateObject, @ObservedObject ...
- Unity Diffuse Metal Shader Mod
Shader "MetalShader" { Properties { _MainTex ("Base (RGB) RefStrGloss (A)", 2D) ...
- cocos中lua使用shader实例
local prog = cc.GLProgram:create("res/shader/light2d.vsh","res/shader/light2d.fsh&quo ...
- SwiftUI中多设备运行方法
https://blog.csdn.net/weixin_42679753/article/details/94465674 https://www.jianshu.com/p/17fc7929fcb ...
随机推荐
- Django高级特性:django-apscheduler定时任务
前言: 在使用Django框架开发web项目时,很多时候需要设置定时任务或让用户手动在页面上设置定时任务 在Django中实现定时任务功能大概有以下三种方法: Celery 分布式任务队列.侧重实 ...
- NC16527 [NOIP2013]货车运输
题目链接 题目 题目描述 A 国有 n 座城市,编号从 1 到 n ,城市之间有 m 条双向道路.每一条道路对车辆都有重量限制,简称限重.现在有 q 辆货车在运输货物, 司机们想知道每辆车在不超过车辆 ...
- Git识别文件权限修改
刚打开IDE,工作区的代码状态全部变成修改未提交的状态了?这是这么回事?这是因为Git忽略文件权限或者拥有者改变导致的git状态变化.默认Git会记录文件的权限信息,如果文件的权限信息被修改,在Git ...
- Wireguard笔记(二) 命令行操作
目录 Wireguard笔记(一) 节点安装配置和参数说明 Wireguard笔记(二) 命令行操作 Wireguard笔记(三) lan-to-lan子网穿透和多网段并存 命令行操作 创建wg0网卡 ...
- ORA-12514问题解决
版本:11.2.0.1.0 - 64bit 本机安装Oracle后链接测试发现以下情况: sqlplus scott/tiger 正常登陆 sqlplus scott/tiger@orcl 登陆失败 ...
- HTML前置知识
1.概念 HTML:超文本标记语言 (英语:Hypertext Markup Language,简称:HTML ) 创建网页的标准标记语言 后缀:html,htm(两者没有区别) html语法对大小写 ...
- 神经网络优化篇:详解TensorFlow
TensorFlow 先提一个启发性的问题,假设有一个损失函数\(J\)需要最小化,在本例中,将使用这个高度简化的损失函数,\(Jw= w^{2}-10w+25\),这就是损失函数,也许已经注意到该函 ...
- Java 通过属性名称读取或者设置实体的属性值
原因 项目实战中有这个需求,数据库中配置对应的实体和属性名称,在代码中通过属性名称获取实体的对应的属性值. 解决方案 工具类,下面这个工具是辅助获取属性值 import com.alibaba.fas ...
- 【Azure Function】Function本地调试时遇见跨域问题(blocked by CORS policy)
问题描述 在本地调试Azure Function时,遇见了跨域问题: Access to XMLHttpRequest at 'http://localhost:7071/api/HttpTrigge ...
- 【Azure 应用服务】App Service 默认页面暴露Tomcat版本信息,存在安全风险
问题描述 在创建Azure App Service时,服务端的配置使用Java 8 + Tomcat 8.5.默认的根目录页面显示出App Service Tomcat版本信息,存在一定的安全隐患. ...