SwiftUI 将视图位置设置为不同视图的中心

11

我有两个不同的视图,一个是红色矩形,另一个是黑色矩形,始终位于屏幕底部。当我点击红色矩形时,它应该移动到黑色矩形内部。

click me

目前,红色矩形位置静态:.position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50)。是否有一种方法可以动态地将210和777替换为黑色矩形中心位置的位置?

我知道可以使用GeometryReader获取视图大小,但如何使用该大小定位不同的视图?这样做是否正确?

struct ContentView: View {

    @State private var tap = false

    var body: some View {
        ZStack {
            VStack {
                Spacer()
                RoundedRectangle(cornerRadius: 10)
                    .frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
            }
            .padding()

            VStack {
                ZStack {
                    Rectangle()
                        .foregroundColor(.red)
                    Text("Click me")
                        .fontWeight(.light)
                        .foregroundColor(.white)
                }
                .frame(width: 50, height: 50)
                .position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50)
                .onTapGesture {
                    withAnimation {
                        self.tap.toggle()
                    }
                }
            }
        }
    }
}

1
这篇文章可能会很有用:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/ - cbjeukendrup
2个回答

9
首先定义一些结构来存储某个视图的.center位置。
struct PositionData: Identifiable {
    let id: Int
    let center: Anchor<CGPoint>
}

内置的机制用于保存数据并将其暴露给父级视图的方法是设置/读取(或反应)符合PreferenceKey协议的值。
struct Positions: PreferenceKey {
    static var defaultValue: [PositionData] = []
    static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
        value.append(contentsOf: nextValue())
    }
}

为了能读取View的中心位置,我们可以使用广泛讨论过的GeometryReader。我将我的PositionReader定义为一个View,在这里,我们可以简单地将其中心位置保存在我们的preferences中以供进一步使用。无需将中心位置转换为不同的坐标系。为了识别View,必须也保存其标记值。
struct PositionReader: View {
    let tag: Int
    var body: some View {
        // we don't need geometry reader at all
        //GeometryReader { proxy in
            Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor)  in
                [PositionData(id: self.tag, center: anchor)]
            }
        //}
    }
}

为了演示如何将所有这些内容结合起来使用,请参见下一个简单的应用程序(复制-粘贴-运行)。
import SwiftUI

struct PositionData: Identifiable {
    let id: Int
    let center: Anchor<CGPoint>
}
struct Positions: PreferenceKey {
    static var defaultValue: [PositionData] = []
    static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
        value.append(contentsOf: nextValue())
    }
}

struct PositionReader: View {
    let tag: Int
    var body: some View {
        Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor)  in
                [PositionData(id: self.tag, center: anchor)]
        }
    }
}

struct ContentView: View {
    @State var tag = 0
    var body: some View {
        ZStack {
            VStack {
                Color.green.background(PositionReader(tag: 0))
                    .onTapGesture {
                    self.tag = 0
                }
                HStack {
                    Rectangle()
                        .foregroundColor(Color.red)
                        .aspectRatio(1, contentMode: .fit)
                        .background(PositionReader(tag: 1))
                        .onTapGesture {
                            self.tag = 1
                        }
                    Rectangle()
                        .foregroundColor(Color.red)
                        .aspectRatio(1, contentMode: .fit)
                        .background(PositionReader(tag: 2))
                        .onTapGesture {
                            self.tag = 2
                        }
                    Rectangle()
                        .foregroundColor(Color.red)
                        .aspectRatio(1, contentMode: .fit)
                        .background(PositionReader(tag: 3))
                        .onTapGesture {
                            self.tag = 3
                        }
                }
            }
        }.overlayPreferenceValue(Positions.self) { preferences in
            GeometryReader { proxy in
                Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
            }
        }
    }
    func getPosition(proxy: GeometryProxy, tag: Int, preferences: [PositionData])->CGPoint {
        let p = preferences.filter({ (p) -> Bool in
            p.id == tag
            })[0]
        return proxy[p.center]
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

代码几乎是自解释的,我们使用 .background(PositionReader(tag:)) 来保存视图的中心位置(这可以通过直接在视图上应用 .anchorPreference 来避免),并且...
.overlayPreferenceValue(Positions.self) { preferences in
    GeometryReader { proxy in
        Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
    }
}

此功能用于创建一个小黑色矩形,它将定位在其他视图的中心。只需轻触绿色或红色矩形的任何位置,黑色矩形即会立即移动 :-)

这是运行此示例应用程序的视图。

输入图像描述


7

下面是一种可能的方法(略微简化了您最初的快照并添加了一些方便的 View 扩展名)。

在Xcode 11.2 / iOS 13.2上测试过

enter image description here

extension View {
    func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
        self.background(GeometryReader { (geometry) -> AnyView in
            let rect = geometry.frame(in: space)
            DispatchQueue.main.async {
                binding.wrappedValue = rect
            }
            return AnyView(Rectangle().fill(Color.clear))
        })
    }
}

struct ContentView: View {

    @State private var tap = false
    @State private var bottomRect: CGRect = .zero

    var body: some View {
        ZStack(alignment: .bottom) {
            RoundedRectangle(cornerRadius: 10)
                .frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
                .padding()
                .rectReader($bottomRect, in: .named("board"))

            Rectangle()
                .foregroundColor(.red)
                .overlay(Text("Click me")
                    .fontWeight(.light)
                    .foregroundColor(.white)
                )
                .frame(width: 50, height: 50)
                .position(x: self.tap ? bottomRect.midX : 50,
                          y: self.tap ? bottomRect.midY : 50)
                .onTapGesture {
                    withAnimation {
                        self.tap.toggle()
                    }
                }
        }.coordinateSpace(name: "board")
    }
}

非常感谢,这是一个非常好的答案! - KevinP
2
@Mamaessen,我仍然建议在一些真实世界的应用中使用更通用的解决方案,就像我的答案所示。 - user3441734

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接