SwiftUI - 从锚定到按钮框架的按钮点击到屏幕的动画视图

SwiftUI - Animate view onto screen from button tap anchored to button frame

我创建了一个示例项目来使用颜色显示我的视图布局。这是它的样子:

这是生成它的代码:

struct ContentView: View {
    @State private var isShowingLeftPopup: Bool = false
    @State private var isShowingRightPopup: Bool = false

    var body: some View {
        TabView {
            VStack {
                Spacer()
                ZStack {
                    Color.red
                        .frame(height: 200)
                    HStack(spacing: 15) {
                        Color.accentColor
                            .disabled(self.isShowingRightPopup)
                            .onTapGesture {
                                self.isShowingLeftPopup.toggle()
                            }
                        Color.accentColor
                            .disabled(self.isShowingLeftPopup)
                            .onTapGesture {
                                self.isShowingRightPopup.toggle()
                            }
                    }
                    .frame(height: 70)
                    .padding(.horizontal)
                }
                Color.purple
                    .frame(height: 300)
            }
        }
    }
}

当点击两个蓝色矩形中的任何一个时,我想在蓝色矩形正下方的屏幕上设置一个视图动画,填充蓝色矩形和底部标签栏之间的垂直 space。动画目前并不是那么重要 - 我无法弄清楚的是如何将条件视图锚定到蓝色矩形的底部并调整其大小以适应下面剩余的 space。

我制作了一个模型,展示了点击左侧蓝色矩形时的外观:

我在此示例中使用固定高度,但我正在寻找不依赖于固定值的解决方案。有谁知道如何将绿色矩形锚定到蓝色矩形的底部并动态调整其大小以填充垂直 space 一直到选项卡栏?

您可以受益于 GeometryReader、Preferences 和 AnchorPreferences。我写了很多关于它们的文章。要了解有关它们如何工作的更多信息,请参阅它们:

GeometryReader 文章: https://swiftui-lab.com/geometryreader-to-the-rescue/

首选项文章: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

具体来说,对于你想要完成的事情,你需要知道蓝色视图和紫色视图(表示绿色视图的下限)的大小和位置。一旦你获得了这些信息,剩下的就很容易了。以下代码将执行此操作:

import SwiftUI

struct MyData {
    let viewName: String
    let bounds: Anchor<CGRect>
}

struct MyPreferenceKey: PreferenceKey {
    static var defaultValue: [MyData] = []

    static func reduce(value: inout [MyData], nextValue: () -> [MyData]) {
        value.append(contentsOf: nextValue())
    }

    typealias Value = [MyData]
}

struct ContentView: View {
    @State private var isShowingLeftPopup: Bool = false
    @State private var isShowingRightPopup: Bool = false

    var body: some View {
        TabView {
            VStack {
                Spacer()
                ZStack {
                    Color.red
                        .frame(height: 200)
                    HStack(spacing: 15) {
                        Color.accentColor
                            .disabled(self.isShowingRightPopup)
                            .onTapGesture {
                                self.isShowingLeftPopup.toggle()
                            }
                            .anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
                                return [MyData(viewName: "leftView", bounds: [=10=])]
                            }

                        Color.accentColor
                            .disabled(self.isShowingLeftPopup)
                            .onTapGesture { self.isShowingRightPopup.toggle() }
                            .anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
                                return [MyData(viewName: "rightView", bounds: [=10=])]
                            }
                    }
                    .frame(height: 70)
                    .padding(.horizontal)
                }

                Color.purple
                    .frame(height: 300)
                    .anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
                        return [MyData(viewName: "purpleView", bounds: [=10=])]
                    }
            }.overlayPreferenceValue(MyPreferenceKey.self) { preferences in
                GeometryReader { proxy in
                    Group {
                        if self.isShowingLeftPopup {
                            ZStack(alignment: .topLeading) {
                                self.createRectangle(proxy, preferences)

                                HStack { Spacer() } // makes the ZStack to expand horizontally
                                VStack { Spacer() } // makes the ZStack to expand vertically
                            }.frame(alignment: .topLeading)
                        } else {
                            EmptyView()
                        }
                    }
                }
            }
        }
    }

    func createRectangle(_ geometry: GeometryProxy, _ preferences: [MyData]) -> some View {

        let l = preferences.first(where: { [=10=].viewName == "leftView" })
        let r = preferences.first(where: { [=10=].viewName == "rightView" })
        let p = preferences.first(where: { [=10=].viewName == "purpleView" })

        let bounds_l = l != nil ? geometry[l!.bounds] : .zero
        let bounds_r = r != nil ? geometry[r!.bounds] : .zero
        let bounds_p = p != nil ? geometry[p!.bounds] : .zero

        return RoundedRectangle(cornerRadius: 15)
            .fill(Color.green)
            .frame(width: bounds_r.maxX - bounds_l.minX, height: bounds_p.maxY - bounds_l.maxY)
            .fixedSize()
            .offset(x: bounds_l.minX, y: bounds_l.maxY)
    }
}