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>


常用修饰符索引

修饰符代码示例 说明
设置前景色 .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)
叠加视图 .overlay( RoundedRectangle(cornerRadius: 40))
剪切形状 .clipShape(Rectangle().offset(y: 50)) clipShape 仅调整视图的外部形状,中间无法有洞
设置遮罩 .mask(Text("SWIFT!").font(.system(size: 72)) ) 使用闭包的视图作为遮罩,中间可以有洞
设置圆角 .cornerRadius(10)
设置边框 .border(Color.blue, width:1) 适用于视图类型;想要实现圆角边框,请用 overlay 实现
设置描边 .stroke(Color.red, lineWidth: 2) 适用于 shape;边框以视图边缘为中心,即一半在视图内,一半在视图外
设置描边 .strokeBorder(.blue, lineWidth: 50) 适用于 shape;视图会插入边框宽度的一半,描绘的整个边框都在视图内
图层混合模式 .blendMode(.hardLight)
饱和度 .saturation(0.3) 调整视图内饱和度。其中 0.0 是全灰色,1.0 是其原始颜色
颜色叠加效果 .colorMultiply(.red) 这将创建一个图像视图并将整个事物染成红色
对比度 .contrast(0.5) 值为 0.0 时不产生对比度,1.0 提供原始图像,高于 1.0 会增加对比度
色相旋转修改器 .hueRotation(Angle(degrees: 30.0))
设置框架尺寸 .frame(width: 10, height: 10, alignment: .top)
设置框架撑满 .frame(maxWidth: .infinity) 使用 maxWidth 才行
固定大小 .fixedSize(horizontal: false, vertical: true) 使两个视图具有相同的宽度或高度
设置间距 .padding()
位置偏移 .offset(x: 0, y: 87) 使视图相对于其自然位置偏移,但不会影响其他视图,也不会影响偏移后放置的其他修饰符的位置效果。需要注意确保视图不发生重叠
绝对定位 .position(x: 100, y: 100) 使用 position 会给原视图加上一个【扩张型】父视图,尽可能占据多的空间,然后在空间里定位子视图;它是根据中心点的位置进行设置
视图层级 .zIndex(2) 在单个 ZStack 内,zIndex 值高的视图,会排在低值视图的上方
放大效果 .scaleEffect(2, anchor: .bottomTrailing) scale 只是单纯拉伸,不会以新尺寸重新绘制,小图像可能会产生模糊
放大效果 .scaleEffect(x: 1, y: 5) 还可以独立控制不同轴的缩放效果
旋转(角度) .rotationEffect(.degrees(45)) 预设旋转会以视图的中心来旋转
旋转(弧度) .rotationEffect(.radians(.pi))
绕锚点旋转 .rotationEffect(.degrees(1), anchor: UnitPoint(x: 0, y: 0)) 將文字以特定點來旋轉(譬如左上角)
3D旋转 .rotation3DEffect(.degrees(60), axis: (x: 1, y: 0, z: 0)) 兩個參數:「旋轉角度」與「旋轉軸」
visualEffect
视觉效果 .visualEffect { content, proxy in
content
    .rotation3DEffect(.degrees(-proxy.frame(in: .global).minX) / 8, axis: (x: 0, y: 1, z: 0))

} | visualEffect() 有很多修饰符可供使用,包括 rotationEffect() 、 rotation3DEffect() 、offset() ;尽管它们影响视图的渲染方式,但它们不会改变视图的框架 用 visualEffect 也能获得 GeometryProxy | | | | |

修饰符代码示例 说明
隐藏 Label 标签 .labelsHidden() 当不希望展示 Label 时,应该将其隐藏,但还是要填写里面的信息;而不是将信息留空,因为 VoiceOver 可以读到 Label 的信息,还是有用的。该修饰符对 Picker、Stepper、Toggle 等都适用。
禁用控件交互 .disabled()
禁止点击交互 .allowsHitTesting(false)
指定元素可交互的区域 .contentShape(.rect) 见notion说明
阻止系统手势干扰 .defersSystemGestures(on: .vertical) 让自定义手势的优先级高于系统内置手势
修饰符代码示例 说明
安全区域扩张 .ignoresSafeArea(.all)
仅忽略底部安全区域 .ignoresSafeArea(.container, edges: .bottom)
忽略底部和两侧但不包括顶部 .ignoresSafeArea(.container, edges: [.bottom, .leading, .trailing])
安全区域收缩 .safeAreaPadding()
安全区域外插入内容 .safeAreaInset(edge: .bottom, spacing: 20)
隐藏状态栏 .statusBar(hidden: active ? true : false) 仅在 iOS 上可用
隐藏 Home indicator .persistentSystemOverlays(.hidden) 显示/隐藏 home indicator 和其他系统 UI