SwiftUI 通过 Modifiers 去修改各个 view 的样式,使用“点语法”呼叫方法,存取修饰器。它有以下特点:
// 同样的修饰符还可以添加多次(每个修饰符都只是添加到以前的任何内容上):
Text("Hello, world!")
.padding()
.background(.red)
.padding()
.background(.blue)
.padding()
.background(.green)
按照不同的维度,修饰符分为以下几种类别:
//例如:以下代码的 Gryffindor 字体就会覆盖父节点的 title
VStack {
Text("Gryffindor")
.font(.largeTitle)
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.font(.title)
但有些修改器就不是环境修饰符,对常规修饰符来说,应用于子视图的任何模糊都会添加到 VStack
的模糊中,而不是替换它。目前除了阅读每个修改器的单独文档并希望被提及之外,没有办法提前知道哪些修改器是环境修改器,哪些是常规修改器。
//例如 blur() ,以下代码中,子视图的 blur 就没有覆盖父视图的,反而是叠加上去
VStack {
Text("Gryffindor")
.blur(radius: 0)
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.blur(radius: 5)
像是 font,foregroundColor 这样定义在具体类型 (比如例中的 Text) 上,然后返回同样类型 (Text) 的,称为原地 modifier。
像是 padding,background 这样定义在 View extension 中,将原来的 View进行包装并返回新的 View 的,称为封装类 modifier。
<aside> 💡 原地 modifier 一般来说对顺序不敏感,对布局也不关心,它们更像是针对对象 View 本身的属性的修改。而与之相反,封装类的 modifier 的顺序十分重要。封装类修饰符的逻辑是:每个修饰符都创建一个应用了该修饰符的新 struct,而不是在视图上设置一个属性。所以修饰符的顺序非常关键。
</aside>
visualEffect()
修饰符可以在不使用 GeometryReader
的情况下读取视图的几何代理,某种程度上可以代替 GeometryReader
visualEffect()
允许改变某些东西的外观效果,它们影响视图的渲染方式,但无法影响视图的实际布局位置或框架。其名称应该清楚地表明所做的任何调整都只会改变视图外观;如果您想使用它来调整视图内容,那么就找错了地方visualEffect()
有很多修饰符可供使用,包括 rotationEffect()
、 rotation3DEffect()
、offset()
content
以及它的 GeometryProxy
content
就是添加了该修饰符的视图本身,但我们不能做任何影响视图布局位置的事情// 例如,以下代码将滚动视图中的每个视图模糊一定的模糊量,该模糊量是根据视图距其滚动视图中心的距离计算的。这意味着垂直中心附近的视图很少或没有模糊,而外部的视图则严重模糊:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(0..<100) { i in
Text("Row \\(i)")
.font(.largeTitle)
.frame(maxWidth: .infinity)
// 使用 visualEffect 可以自动获得 proxy 对象,传入方法
.visualEffect { content, proxy in
content.blur(radius: blurAmount(for: proxy))
}
}
}
}
func blurAmount(for proxy: GeometryProxy) -> Double {
let scrollViewHeight = proxy.bounds(of: .scrollView)?.height ?? 100
// 调用 proxy.frame(in: .scrollView) 可在包含该视图的最内层滚动视图中查找该视图的大小
let ourCenter = proxy.frame(in: .scrollView).midY
let distanceFromCenter = abs(scrollViewHeight / 2 - ourCenter)
return Double(distanceFromCenter) / 100
}
}
// 这些视觉效果适用于任何类型的位置,包括通过动画生成的位置。例如,这使得网格中的一系列圆圈旋转,每个圆圈根据色调旋转动态重新着色:
struct ContentView: View {
@State private var rotationAmount = 0.0
var body: some View {
Grid {
ForEach(0..<3) { _ in
GridRow {
ForEach(0..<3) { _ in
Circle()
.fill(.green)
.frame(width: 100, height: 100)
.visualEffect { content, proxy in
content.hueRotation(.degrees(proxy.frame(in: .global).midY / 2))
}
}
}
}
}
.rotationEffect(.degrees(rotationAmount))
.onAppear {
withAnimation(.linear(duration: 5).repeatForever(autoreverses: false)) {
rotationAmount = 360
}
}
}
}
// 例如使用 3D 旋转效果
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(1..<20) { num in
Text("Number \\(num)")
.font(.largeTitle)
.padding()
.background(.red)
.frame(width: 200, height: 200)
.visualEffect { content, proxy in
content
.rotation3DEffect(.degrees(-proxy.frame(in: .global).minX) / 8, axis: (x: 0, y: 1, z: 0))
}
}
}
}
和使用 GeometryReader 对比:
// 例如:创建一个简单的 CoverFlow 风格的效果,可以水平滑动来查看在 3D 空间中移动的视图
// 使用了 GeometryReader 读取滚动视图中每个 Text 视图在全局的位置,
// 重点是:需要添加显式的 frame 修饰符,指定宽度和高度(等于Text视图尺寸),以阻止 GeometryReader 自动扩展占据所有可用空间
// 如果没有给 GeometryReader 后面添加 frame 修饰符,则 GeometryReader 会撑满屏幕,那所有 Text 的坐标都靠左上角,就都一样
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(1..<20) { num in
GeometryReader {
proxy in
Text("Number \\(num)")
.font(.largeTitle)
.padding()
.background(.red)
.rotation3DEffect(.degrees(-proxy.frame(in: .global).minX) / 8, axis: (x: 0, y: 1, z: 0))
.frame(width: 200, height: 200)
}
.frame(width: 200, height: 200)
}
}
}
<aside> 💡 总结:用 visualEffect 也能获得 GeometryProxy。这种写法比使用 GeometryReader 更简洁,因为不再需要添加 frame() 修饰符来阻止 GeometryReader 内容占据全屏。
</aside>
很多时候,我们希望满足某些条件才应用这个修饰符,不满足时就不应用。第一种方法是将我们的整个内容包装在 if else 语句中。如果您的内容很少,这很好,但当您的内容变大时,它可能会变得难以阅读。此外,代码重复了两次,这并不好。
struct ContentView: View {
@State private var shouldBeRed: Bool = true
var body: some View {
if shouldBeRed {
Text("Hello, world!")
.foregroundColor(.red)
} else {
Text("Hello, world!")
.foregroundColor(.blue)
}
}
}
另一种方法是在修饰符内添加三元运算符。如果应应用修饰符,即使条件的计算结果为 false,这也是一个很好的方法。它更易于阅读,并且只有一行代码,这使得代码干净。
struct ContentView: View {
@State private var shouldBeRed: Bool = true
var body: some View {
Text("Hello, world!")
.foregroundColor(shouldBeRed ? .red : .blue)
}
}
但是有时候,光通过修改参数值并不能达到想要的效果,我们就希望条件不满足时,不要出现该修饰符,该如何实现呢?例如,如果 shouldAddShadow 的值为 true,则只想向 Text 添加阴影。如果为 false,则您不想添加任何阴影。这里可以设置一个 View
扩展来实现:
// 1.首先创建一个扩展文件
extension View {
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
// 2.调用时:用 if 函数调用,其中的 shouldAddShadow 是一个之前定义好的布尔值,通过它决定是否采用闭包中的修饰符;view 代表视图本身
struct ContentView: View {
@State private var shouldAddShadow: Bool = true
var body: some View {
Text("Hello, world!")
.if(shouldAddShadow) { view in
view.shadow(color: .black, radius: 10, x: 0.0, y: 0.0)
}
}
}
// 这对应用程序的性能也有好处,因为如果用户使用旧设备,您可以让用户禁用阴影和所有其他 CPU 开销较大的修饰符,例如模糊视图。
有时候,当我们使用三元表达式去控制修饰符的参数时,会遇到想要的两种效果的类型不一致的报错情况,例如背景填充 color
或 material
这时我们可以通过给它们转成 AnyView
类型来修复
.background(
hasSimpleWave || hasComplexWave ?
AnyView(Color(.secondarySystemBackground)) :
AnyView(Color.clear.background(.regularMaterial))
)
// material 转成 AnyView 时要用 color.clear.background 的写法
修饰符代码示例 | 说明 | |
---|---|---|
设置前景色 style | .foregroundStyle(.green, .white) | 可以设置多个值,支持 SF 图标的多颜色 |
设置前景色 color | .foregroundColor(.green) | 只能设置一个值,逐渐要弃用 |
设置重点颜色 | .accentColor(.green) | |
设置重点颜色 | .tint(.green) | 可以设置视图中激活按钮的颜色(子视图会覆盖父视图的) |
设置透明度 | .opacity(0.5) | |
设置背景 | .background(.red) | background 指定任何类型视图作为背景,比如可以使用文本视图 |
.background(Color.primary.opacity(0.1)) | 设置背景色并加上透明度 | |
.background(LinearGradient()) | 设置背景为渐变色 | |
.background(Image("pic"), alignment: .bottom) | 设置背景填充图像,选择对齐方式 | |
.background(.ultraThinMaterial) | 设置背景为材质 ultraThinMaterial、regularMaterial、thickMaterial | |
隐藏组件背景 | .scrollContentBackground(.hidden) | 适用于 List 、 TextEditor 和 Form |
填充 | .fill(.red) | 仅适用于 Shape |
填充+外阴影 | .fill(.red.shadow(.drop(color: .black, radius: 10))) | 仅适用于 Shape |
填充+内阴影 | .fill(.red.shadow(.inner(color: .black, radius: 10))) | 仅适用于 Shape |
设置阴影 | .shadow(radius: 5) | |
高斯模糊 | .blur(radius: 20) | |
设置阴影 | .shadow(color: .gray, radius: 2, x: 0, y: 15) | |
.shadow(color: .gray.opacity(0.5), radius: 2, x: 0, y: 15) | 可以给阴影加上透明度,让阴影效果更柔和 | |
叠加视图 | .overlay(RoundedRectangle(cornerRadius: 40)) | |
设置剪切形状 | .clipShape(Rectangle(cornerRadius: 40)) | 仅按指定形状裁剪视图,指定形状必须是符合 shape 协议(中间无法有洞) |
设置遮罩 | .mask(Text("SWIFT!").font(.system(size: 72))) | 使用闭包的视图作为遮罩,可以使用 Text 这样的视图(中间可以有洞) |
设置圆角 | .cornerRadius(10) |
| --- | --- | --- |
| --- | --- | --- |