Modifier 修饰符的种类

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)

原地 modifier

像是 font,foregroundColor 这样定义在具体类型 (比如例中的 Text) 上,然后返回同样类型 (Text) 的,称为原地 modifier。

封装类 modifier

像是 padding,background 这样定义在 View extension 中,将原来的 View进行包装并返回新的 View 的,称为封装类 modifier。

<aside> 💡 原地 modifier 一般来说对顺序不敏感,对布局也不关心,它们更像是针对对象 View 本身的属性的修改。而与之相反,封装类的 modifier 的顺序十分重要。封装类修饰符的逻辑是:每个修饰符都创建一个应用了该修饰符的新 struct,而不是在视图上设置一个属性。所以修饰符的顺序非常关键。

</aside>


visualEffect 修饰符

visualEffect() 修饰符可以在不使用 GeometryReader 的情况下读取视图的几何代理,某种程度上可以代替 GeometryReader

// 例如,以下代码将滚动视图中的每个视图模糊一定的模糊量,该模糊量是根据视图距其滚动视图中心的距离计算的。这意味着垂直中心附近的视图很少或没有模糊,而外部的视图则严重模糊:
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>


Conditional modifier 判断修饰符

很多时候,我们希望满足某些条件才应用这个修饰符,不满足时就不应用。第一种方法是将我们的整个内容包装在 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 开销较大的修饰符,例如模糊视图。

解决修饰符参数类型不一致

有时候,当我们使用三元表达式去控制修饰符的参数时,会遇到想要的两种效果的类型不一致的报错情况,例如背景填充 colormaterial

这时我们可以通过给它们转成 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)

交互类

| --- | --- | --- |

系统类

| --- | --- | --- |