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>
修饰符代码示例 | 说明 | |
---|---|---|
设置前景色 | .foregroundStyle(.green) | .foregroundColor 逐渐要弃用 |
设置重点颜色 | .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) | 见 ‣ |
阻止系统手势干扰 | .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 |