前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >在 SwiftUI 中用 zIndex 调整视图显示顺序

在 SwiftUI 中用 zIndex 调整视图显示顺序

作者头像
东坡肘子
发布于 2022-07-28 05:05:44
发布于 2022-07-28 05:05:44
1.9K00
代码可运行
举报
运行总次数:0
代码可运行

本文将对 SwiftUI 的 zIndex 修饰符做以介绍,包括:使用方法、zIndex 的作用域、通过 zIndex 避免动画异常、为什么 zIndex 需要设置稳定的值以及在多种布局容器内使用 zIndex 等内容。

访问我的博客 www.fatbobman.com[1] 可以获得更好的阅读体验

zIndex 修饰符

在 SwiftUI 中,开发者使用 zIndex 修饰符来控制重叠视图间的显示顺序,具有较大 zIndex 值的视图将显示在具有较小 zIndex 值的视图之上。在没有指定 zIndex 值的时候,SwiftUI 默认会给视图一个为 0 的 zIndex 值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ZStack {
    Text("Hello") // 默认 zIndex 值为 0 ,显示在最后面
    
    Text("World")
        .zIndex(3.5)  // 显示在最前面
    
    Text("Hi")
        .zIndex(3.0)  
    
    Text("Fat")
        .zIndex(3.0) // 显示在 Hi 之前, 相同 zIndex 值,按布局顺序显示
}

可以在此处获取本文的全部代码[2]

zIndex 的作用域

  • zIndex 的作用范围被限定在布局容器内 视图的 zIndex 值仅限于与处于同一个布局容器的其他视图进行比较( Group 不是布局容器)。处于不同的布局容器或父子容器之间的视图无法直接比较。
  • 当一个视图有多个 zIndex 修饰符时,视图将使用最内层的 zIndex 值
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)
    }
}

执行上面的代码,最终只能看到 ColorGroup

image-20220409170346551

设定 zIndex 避免动画异常

如果视图的 zIndex 值相同(比如全部使用默认值 0 ),SwiftUI 会按照布局容器的布局方向( 视图代码在闭包中的出现顺序 )对视图进行绘制。在视图没有增减变化的需求时,可以不必显式设置 zIndex 。但如果有动态的视图增减需求,如不显式设置 zIndex ,某些情况下会出现显示异常,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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()
    }
}

点击按钮,红色出现时没有渐变过场,隐藏时有渐变过场。

animationException20220409

如果我们显式地给每个视图设置了 zIndex 值,就可以解决这个显示异常。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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()
    }
}

zIndexAnimation2022-04-09 17.15.18.2022-04-09 17_17_08

zIndex是不可动画的

offsetrotationEffectopacity 等修饰符不同, zIndex 是不可动画的 ( 其内部对应的 _TraitWritingModifier 并不符合 Animatable 协议)。这意味着即使我们使用例如 withAnimation 之类的显式动画手段来改变视图的 zIndex 值,并不会出现预期中的平滑过渡,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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()
    }
}

swapWithzIndex2022-04-09 17.31.01.2022-04-09 17_33_07

因此在进行视图的显示切换时,最好通过 opacitytransition 等方式来处理(参阅下面的代码)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 使用 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() }
    }
}

swapWithTransition2022-04-09 17.36.08.2022-04-09 17_38_34

为 zIndex 设置稳定的值

由于 zIndex 是不可动画的,所以应尽量为视图设置稳定的 zIndex 值。

对于固定数量的视图,可以手动在代码中进行标注。对于可变数量的视图(例如使用了 ForEach),需要在数据中找到可作为 zIndex 值参考依据的稳定标识

例如下面的代码,尽管我们利用了 enumerated 为每个视图添加序号,并以此序号作为视图的 zIndex 值,但当视图发生增减时,由于序号的重组,就会有几率出现动画异常的情况。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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))
}

unStablezIndex2022-04-09 17.47.49.2022-04-09 17_49_14

删除第四个色块(紫色)时,显示异常。

通过为视图指定稳定的 zIndex 值,可以避免上述问题。下面的代码,为每个视图添加了稳定的 zIndex 值,该值并不会因为有视图被删除就发生变化。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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))
}

stableZindex2022-04-09 18.07.18.2022-04-09 18_09_12

并非一定要在数据结构中为 zIndex 预留独立的属性,下节中的范例代码则是利用了数据中的时间戳属性作为 zIndex 值的参照依据。

zIndex 并非 ZStack 的专利

尽管大多数人都是在 ZStack 中使用 zIndex ,但 zIndex 也同样可以使用在 VStack 和 HStack 中,且通过和 spacing 的配合,可以非常方便的实现某些特殊的效果。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 值,便可以实现对新增视图是出现在最上面还是最下面的控制。

zIndexInVStack2022-04-09 19.18.42.2022-04-09 19_20_20

SwiftUI Overlay Container[3] 即是通过上述方式实现了在不改变数据源的情况下调整视图的显示顺序

总结

zIndex 使用简单,效果明显,为我们提供了从另一个维度来调度、组织视图的能力。

希望本文能够对你有所帮助。

参考资料

[1] www.fatbobman.com: https://www.fatbobman.com

[2] 此处获取本文的全部代码: https://github.com/fatbobman/BlogCodes/tree/main/ZIndexDemo

[3] SwiftUI Overlay Container: https://github.com/fatbobman/SwiftUIOverlayContainer

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 肘子的Swift记事本 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
SwiftUI 布局 —— 尺寸( 下 )
在 上篇[3] 中,我们对 SwiftUI 布局过程中涉及的众多尺寸概念进行了说明。本篇中,我们将通过对视图修饰器 frame 和 offset 的仿制进一步加深对 SwiftUI 布局机制的理解,并通过一些示例展示在布局时需要注意的问题。
东坡肘子
2022/07/28
2.7K0
SwiftUI 布局 —— 尺寸( 下 )
SwiftUI 的动画机制
大多初学者都会在第一时间惊叹于 SwiftUI 轻松实现各种动画效果的能力,但经过一段时间的使用后,他们会发现 SwiftUI 的动画并非像表面上看起来那样容易驾驭。开发者经常需要面对:如何动、怎么动、什么能动、为什么不动、为什么这么动、如何不让它动等等困扰。对 SwiftUI 的动画处理逻辑了解的不够深入是造成上述困扰的主要原因。本文将尝试对 SwiftUI 的动画机制做以介绍,以帮助大家更好地学习、掌握 SwiftUI 的动画,制作出满意的交互效果。
东坡肘子
2022/07/28
15K0
SwiftUI 的动画机制
SheetKit——SwiftUI模态视图扩展库
如果想获得更好的阅读体验,可以访问我的博客 www.fatbobman.com。[1]
东坡肘子
2022/07/28
3K0
SheetKit——SwiftUI模态视图扩展库
SwiftUI案例:自定义加载动画
案例通过在间隔时间内不断控制变量 animateBall:Bool 与 animateRotation:Bool 的值来间接地实现动画效果;
DioxideCN
2022/08/05
2.1K0
SwiftUI案例:自定义加载动画
掌握 Transaction,实现 SwiftUI 动画的精准控制
SwiftUI 因其简便的动画 API 与极低的动画设计门槛而广受欢迎。但是,随着应用程序复杂性的增加,开发者逐渐发现,尽管动画设计十分简单,但要实现精确细致的动画控制并非易事。同时,在 SwiftUI 的动画系统中,有关 Transaction 的解释很少,无论是官方资料还是第三方文章,都没有对其运作机制进行系统的阐述。
东坡肘子
2023/07/08
6090
掌握 Transaction,实现 SwiftUI 动画的精准控制
在 SwiftUI 中实现视图居中的若干种方法
将某个视图在父视图中居中显示是一个常见的需求,即使对于 SwiftUI 的初学者来说这也并非难事。在 SwiftUI 中,有很多手段可以达成此目的。本文将介绍其中的一些方法,并对每种方法背后的实现原理、适用场景以及注意事项做以说明。
东坡肘子
2022/12/16
7.2K0
在 SwiftUI 中实现视图居中的若干种方法
深入了解 SwiftUI 5 中 ScrollView 的新功能
为可滚动容器的内容或滚动指示器(Scroll Indicator)添加外边距(Margin)。
东坡肘子
2023/07/08
1.1K0
深入了解 SwiftUI 5 中 ScrollView 的新功能
SwiftUI 布局 —— 对齐
“对齐”是 SwiftUI 中极为重要的概念,然而相当多的开发者并不能很好地驾驭这个布局利器。在 WWDC 2022 中,苹果为 SwiftUI 增添了 Layout 协议,让我们有了更多的机会了解和验证 SwiftUI 的布局原理。本文将结合 Layout 协议的内容对 SwiftUI 的 “对齐” 进行梳理,希望能让读者对“对齐”有更加清晰地认识和掌握。
东坡肘子
2022/07/28
6.5K0
SwiftUI 布局 —— 对齐
用 SwiftUI 的方式进行布局
最近时常有朋友反映,尽管 SwiftUI 的布局系统学习门槛很低,但当真正面对要求较高的设计需求时,好像又无从下手。SwiftUI 真的具备创建复杂用户界面的能力吗?本文将通过用多种手段完成同一需求的方式,展示 SwiftUI 布局系统的强大与灵活,并通过这些示例让开发者对 SwiftUI 的布局逻辑有更多的认识和理解。
东坡肘子
2023/03/08
4.9K2
用 SwiftUI 的方式进行布局
掌握 SwiftUI 的 Safe Area
Safe Area(安全区域)是指不与导航栏、标签栏、工具栏或其他视图控制器提供的视图重叠的内容空间。
东坡肘子
2022/07/28
8K0
掌握 SwiftUI 的 Safe Area
SwiftUI 视图的生命周期研究
在 UIKit(AppKit)的世界中,通过框架提供的大量钩子(例如 viewDidLoad、viewWillLayoutSubviews 等),开发者可以将自己的意志注入视图控制器生命周期的各个节点之中,宛如神明。在 SwiftUI 中,系统收回了上述的权利,开发者基本丧失了对视图生命周期的掌控。不少 SwiftUI 开发者都碰到过视图生命周期的行为超出预期的状况(例如视图多次构造、onAppear 无从控制等)。
东坡肘子
2022/07/28
4.7K0
SwiftUI geometryGroup() 指南:从原理到实践
在 WWDC 2023 中,苹果为 SwiftUI 添加了一个新的修饰器:geometryGroup()。它可以解决一些之前无法处理或处理起来比较困难的动画异常。本文将介绍 geometryGroup() 的概念、用法,以及在低版本 SwiftUI 中,在不使用 geometryGroup() 的情况下如何处理异常。
东坡肘子
2023/11/30
3720
SwiftUI geometryGroup() 指南:从原理到实践
Ask Apple 2022 与 SwiftUI 有关的问答(下)
Ask Apple 为开发者与苹果工程师创造了在 WWDC 之外进行直接交流的机会。本文对本次活动中与 SwiftUI 有关的一些问答进行了整理,并添加了一点个人见解。本文为下篇。
东坡肘子
2022/12/16
15.3K0
Ask Apple 2022 与 SwiftUI 有关的问答(下)
SwiftUI:视图的显示和隐藏动画
SwiftUI最强大的功能之一是能够自定义视图的显示和隐藏方式。以前,您已经了解了如何使用常规if条件有条件地包含视图,这意味着当条件更改时,我们可以从视图层次结构中插入或移除视图。
韦弦zhy
2020/04/16
4.9K0
在 SwiftUI 中创建自适应的程序化导航方案
随着苹果对 iPadOS 的不断投入,越来越多的开发者都希望自己的应用能够在 iPad 中有更好的表现。尤其当用户开启了台前调度( Stage Manager )功能后,应用对不同视觉大小模式的兼容能力就越发显得重要。本文将就如何创建可自适应不同尺寸模式的程序化导航方案这一内容进行探讨。
东坡肘子
2022/12/16
4.4K0
在 SwiftUI 中创建自适应的程序化导航方案
探索 SwiftUI 基本手势
在 SwiftUI 中,我们可以通过添加不同的交互来使我们的应用程序更具交互性,这些交互可以响应我们的点击,点击和滑动。
Swift社区
2021/11/26
2.2K0
探索 SwiftUI 基本手势
SwiftUI 布局 —— 尺寸( 上 )
在 SwiftUI 中,尺寸这一布局中极为重要的概念,似乎变得有些神秘。无论是设置尺寸还是获取尺寸都不是那么地符合直觉。本文将从布局的角度入手,为你揭开盖在 SwiftUI 尺寸概念上面纱,了解并掌握 SwiftUI 中众多尺寸的含义与用法;并通过创建符合 Layout 协议的 frame 和 fixedSize 视图修饰器的复制品,让你对 SwiftUI 的布局机制有更加深入地理解。
东坡肘子
2022/07/28
4.9K0
SwiftUI 布局 —— 尺寸( 上 )
SwiftUI-ScrollView进化史
ScrollView 即滚动视图,在 iOS 开发中扮演着非常重要的角色。但在 SwiftUI 的发展史上,ScrollView 一直处于“残废”的状态,直到 SwiftUI 6.0 才逐渐补齐短板。下面详细讲解 SwiftUI 中 ScrollView 的进化史。
YungFan
2025/02/10
1060
在 SwiftUI 视图中打开 URL 的若干方法
本文将介绍在 SwiftUI 视图中打开 URL 的若干种方式,其他的内容还包括如何自动识别文本中的内容并为其转换为可点击链接,以及如何自定义打开 URL 前后的行为等。
东坡肘子
2022/07/28
7.9K1
在 SwiftUI 视图中打开 URL 的若干方法
高级 SwiftUI 动画 — Part 1:Paths
在本文中,我们将深入探讨一些创建 SwiftUI 动画的高级技术。我将广泛讨论 Animatable[1] 协议,它可靠的伙伴 animatableData[2],强大但经常被忽略的 GeometryEffect[3] 以及完全被忽视但全能的 AnimatableModifier[4] 协议。
Swift社区
2022/04/04
3.9K0
高级 SwiftUI 动画 — Part 1:Paths
相关推荐
SwiftUI 布局 —— 尺寸( 下 )
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验