本文将对 SwiftUI 的 zIndex 修饰符做以介绍,包括:运用方法、zIndex 的作用域、经过 zIndex 防止动画反常、为什么 zIndex 需求设置安稳的值以及在多种布局容器内运用 zIndex 等内容。
原文宣布在我的博客wwww.fatbobman.com
欢迎订阅我的公共号:【肘子的Swift记事本】
zIndex 修饰符
在 SwiftUI 中,开发者运用 zIndex 修饰符来控制堆叠视图间的显现次序,具有较大 zIndex 值的视图将显现在具有较小 zIndex 值的视图之上。在没有指定 zIndex 值的时分,SwiftUI 默许会给视图一个为 0 的 zIndex 值。
ZStack {
Text("Hello") // 默许 zIndex 值为 0 ,显现在最后面
Text("World")
.zIndex(3.5) // 显现在最前面
Text("Hi")
.zIndex(3.0)
Text("Fat")
.zIndex(3.0) // 显现在 Hi 之前, 相同 zIndex 值,按布局次序显现
}
能够在此处获取本文的悉数代码
zIndex 的作用域
-
zIndex 的作用范围被限定在布局容器内
视图的 zIndex 值仅限于与处于同一个布局容器的其他视图进行比较( Group 不是布局容器)。处于不同的布局容器或父子容器之间的视图无法直接比较。
-
当一个视图有多个 zIndex 修饰符时,视图将运用最内层的 zIndex 值
struct ScopeDemo: View {
var body: some View {
ZStack {
// zIndex = 1
Color.red
.zIndex(1)
// zIndex = 0.5
SubView()
.zIndex(0.5)
// zIndex = 0.5, 运用最内层的 zIndex 值
Text("abc")
.padding()
.zIndex(0.5)
.foregroundColor(.green)
.overlay(
Rectangle().fill(.green.opacity(0.5))
)
.padding(.top, 100)
.zIndex(1.3)
// zIndex = 1.5 ,Group 不是布局容器,运用最内层的 zIndex 值
Group {
Text("Hello world")
.zIndex(1.5)
}
.zIndex(0.5)
}
.ignoresSafeArea()
}
}
struct SubView: View {
var body: some View {
ZStack {
Text("Sub View1")
.zIndex(3) // zIndex = 3 ,仅在本 ZStack 中比较
Text("Sub View2") // zIndex = 3.5 ,仅在本 ZStack 中比较
.zIndex(3.5)
}
.padding(.top, 100)
}
}
履行上面的代码,终究只能看到 Color
和 Group
设定 zIndex 防止动画反常
假如视图的 zIndex 值相同(比方悉数运用默许值 0 ),SwiftUI 会按照布局容器的布局方向( 视图代码在闭包中的呈现次序 )对视图进行制作。在视图没有增减变化的需求时,能够不必显式设置 zIndex 。但假如有动态的视图增减需求,如不显式设置 zIndex ,某些情况下会呈现显现反常,例如:
struct AnimationWithoutZIndex: View {
@State var show = true
var body: some View {
ZStack {
Color.red
if show {
Color.yellow
}
Button(show ? "Hide" : "Show") {
withAnimation {
show.toggle()
}
}
.buttonStyle(.bordered)
.padding(.top, 100)
}
.ignoresSafeArea()
}
}
点击按钮,红色呈现时没有突变过场,隐藏时有突变过场。
假如咱们显式地给每个视图设置了 zIndex 值,就能够解决这个显现反常。
struct AnimationWithZIndex: View {
@State var show = true
var body: some View {
ZStack {
Color.red
.zIndex(1) // 按次序设置 zIndex 值
if show {
Color.yellow
.zIndex(2) // 取消或显现时,SwiftUI 将明确知道该视图在 Color 和 Button 之间
}
Button(show ? "Hide" : "Show") {
withAnimation {
show.toggle()
}
}
.buttonStyle(.bordered)
.padding(.top, 100)
.zIndex(3) // 最上层视图
}
.ignoresSafeArea()
}
}
zIndex是不行动画的
同 offset
、rotationEffect
、opacity
等修饰符不同, zIndex 是不行动画的 ( 其内部对应的 _TraitWritingModifier 并不符合 Animatable 协议)。这意味着即便咱们运用例如 withAnimation
之类的显式动画手法来改变视图的 zIndex 值,并不会呈现预期中的滑润过渡,例如:
struct SwapByZIndex: View {
@State var current: Current = .page1
var body: some View {
ZStack {
SubText(text: Current.page1.rawValue, color: .red)
.onTapGesture { swap() }
.zIndex(current == .page1 ? 1 : 0)
SubText(text: Current.page2.rawValue, color: .green)
.onTapGesture { swap() }
.zIndex(current == .page2 ? 1 : 0)
SubText(text: Current.page3.rawValue, color: .cyan)
.onTapGesture { swap() }
.zIndex(current == .page3 ? 1 : 0)
}
}
func swap() {
withAnimation {
switch current {
case .page1:
current = .page2
case .page2:
current = .page3
case .page3:
current = .page1
}
}
}
}
enum Current: String, Hashable, Equatable {
case page1 = "Page 1 tap to Page 2"
case page2 = "Page 2 tap to Page 3"
case page3 = "Page 3 tap to Page 1"
}
struct SubText: View {
let text: String
let color: Color
var body: some View {
ZStack {
color
Text(text)
}
.ignoresSafeArea()
}
}
因此在进行视图的显现切换时,最好经过 opacity
或 transition
等方式来处理(参阅下面的代码)。
// 运用 opacity
ZStack {
SubText(text: Current.page1.rawValue, color: .red)
.onTapGesture { swap() }
.opacity(current == .page1 ? 1 : 0)
SubText(text: Current.page2.rawValue, color: .green)
.onTapGesture { swap() }
.opacity(current == .page2 ? 1 : 0)
SubText(text: Current.page3.rawValue, color: .cyan)
.onTapGesture { swap() }
.opacity(current == .page3 ? 1 : 0)
}
// 经过 transition
VStack {
switch current {
case .page1:
SubText(text: Current.page1.rawValue, color: .red)
.onTapGesture { swap() }
case .page2:
SubText(text: Current.page2.rawValue, color: .green)
.onTapGesture { swap() }
case .page3:
SubText(text: Current.page3.rawValue, color: .cyan)
.onTapGesture { swap() }
}
}
为 zIndex 设置安稳的值
由于 zIndex 是不行动画的,所以应尽量为视图设置安稳的 zIndex 值。
关于固定数量的视图,能够手动在代码中进行标示。关于可变数量的视图(例如运用了 ForEach),需求在数据中找到可作为 zIndex 值参阅依据的安稳标识。
例如下面的代码,尽管咱们利用了 enumerated
为每个视图增加序号,并以此序号作为视图的 zIndex 值,但当视图产生增减时,由于序号的重组,就会有几率呈现动画反常的情况。
struct IndexDemo1: View {
@State var backgrounds = (0...10).map { _ in BackgroundWithoutIndex() }
var body: some View {
ZStack {
ForEach(Array(backgrounds.enumerated()), id: \.element.id) { item in
let background = item.element
background.color
.offset(background.offset)
.frame(width: 200, height: 200)
.onTapGesture {
withAnimation {
if let index = backgrounds.firstIndex(where: { $0.id == background.id }) {
backgrounds.remove(at: index)
}
}
}
.zIndex(Double(item.offset))
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.ignoresSafeArea()
}
}
struct BackgroundWithoutIndex: Identifiable {
let id = UUID()
let color: Color = {
[Color.orange, .green, .yellow, .blue, .cyan, .indigo, .gray, .pink].randomElement() ?? .red.opacity(Double.random(in: 0.8...0.95))
}()
let offset: CGSize = .init(width: CGFloat.random(in: -200...200), height: CGFloat.random(in: -200...200))
}
删去第四个色块(紫色)时,显现反常。
经过为视图指定安稳的 zIndex 值,能够防止上述问题。下面的代码,为每个视图增加了安稳的 zIndex 值,该值并不会因为有视图被删去就产生变化。
struct IndexDemo: View {
// 在创建时增加固定的 zIndex 值
@State var backgrounds = (0...10).map { i in BackgroundWithIndex(index: Double(i)) }
var body: some View {
ZStack {
ForEach(backgrounds) { background in
background.color
.offset(background.offset)
.frame(width: 200, height: 200)
.onTapGesture {
withAnimation {
if let index = backgrounds.firstIndex(where: { $0.id == background.id }) {
backgrounds.remove(at: index)
}
}
}
.zIndex(background.index)
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.ignoresSafeArea()
}
}
struct BackgroundWithIndex: Identifiable {
let id = UUID()
let index: Double // zIndex 值
let color: Color = {
[Color.orange, .green, .yellow, .blue, .cyan, .indigo, .gray, .pink].randomElement() ?? .red.opacity(Double.random(in: 0.8...0.95))
}()
let offset: CGSize = .init(width: CGFloat.random(in: -200...200), height: CGFloat.random(in: -200...200))
}
并非一定要在数据结构中为 zIndex 预留独立的特点,下节中的范例代码则是利用了数据中的时刻戳特点作为 zIndex 值的参照依据。
zIndex 并非 ZStack 的专利
尽管大多数人都是在 ZStack 中运用 zIndex ,但 zIndex 也同样能够运用在 VStack 和 HStack 中,且经过和 spacing 的配合,能够十分方便的完成某些特殊的作用。
struct ZIndexInVStack: View {
@State var cells: [Cell] = []
@State var spacing: CGFloat = -95
@State var toggle = true
var body: some View {
VStack {
Button("New Cell") {
newCell()
}
.buttonStyle(.bordered)
Slider(value: $spacing, in: -150...20)
.padding()
Toggle("新视图显现在最上面", isOn: $toggle)
.padding()
.onChange(of: toggle, perform: { _ in
withAnimation {
cells.removeAll()
spacing = -95
}
})
VStack(spacing: spacing) {
Spacer()
ForEach(cells) { cell in
cell
.onTapGesture { delCell(id: cell.id) }
.zIndex(zIndex(cell.timeStamp))
}
}
}
.padding()
}
// 利用时刻戳核算 zIndex 值
func zIndex(_ timeStamp: Date) -> Double {
if toggle {
return timeStamp.timeIntervalSince1970
} else {
return Date.distantFuture.timeIntervalSince1970 - timeStamp.timeIntervalSince1970
}
}
func newCell() {
let cell = Cell(
color: ([Color.orange, .green, .yellow, .blue, .cyan, .indigo, .gray, .pink].randomElement() ?? .red).opacity(Double.random(in: 0.9...0.95)),
text: String(Int.random(in: 0...1000)),
timeStamp: Date()
)
withAnimation {
cells.append(cell)
}
}
func delCell(id: UUID) {
guard let index = cells.firstIndex(where: { $0.id == id }) else { return }
withAnimation {
let _ = cells.remove(at: index)
}
}
}
struct Cell: View, Identifiable {
let id = UUID()
let color: Color
let text: String
let timeStamp: Date
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill(color)
.frame(width: 300, height: 100)
.overlay(Text(text))
.compositingGroup()
.shadow(radius: 3)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
在上面的代码中,咱们无需更改数据源,只需调整每个视图的 zIndex 值,便能够完成对新增视图是呈现在最上面仍是最下面的控制。
SwiftUI Overlay Container 便是经过上述方式完成了在不改变数据源的情况下调整视图的显现次序
总结
zIndex 运用简单,作用显着,为咱们提供了从另一个维度来调度、组织视图的才能。
希望本文能够对你有所协助。
原文宣布在我的博客wwww.fatbobman.com
欢迎订阅我的公共号:【肘子的Swift记事本】