swiftUI:检测嵌入数组中的两个视图的交集并交换它们的位置

swiftUI: detect intersection of two views embedded inside an array and swap their positions

1) 同一视图的多个实例嵌入在一个数组中。主 ContentView 的子视图通过 HStack 中的 ForEach 显示它们。每个实例都有一个几何体 reader 来检测拖动过程中的帧交叉点。目标是在检测到交集时交换视图在主要内容视图上的位置。使用下面的代码,它根本就没有检测到交叉点。

2) 我 有点 能够实现此目的的唯一方法是将每个视图的额外 GR 碰撞帧存储在 environmentObject/ObservableObject 的已发布数组 var 中。然而,一旦我的视图的两个实例交换了它们在主视图上的位置,似乎 GR 碰撞框架 CGRects 没有得到更新(因为 GR 只有 onappear 方法)并且下次我执行视图拖动并相交错误的视图被交换(不是视觉上发生碰撞的)例如下面的代码显示了 3 个标记为 0、1、2 的矩形(作为它们在数组中的索引)。第一次将“2”拖到“1”上时,它们会正确交换,但是如果你将“1”拖到“2”上,则“1”会与“0”交换。

```swift
class FieldEnv: ObservableObject {
    @Published var rect_frames: [UUID:CGRect] = [:]
    @Published var rects: [rect] = [rect(num:"0"),rect(num:"1"),rect(num:"2")]
}

struct rect: View {
    @EnvironmentObject var field_env: FieldEnv
    let id = UUID()
    let num: String
    @State var viewFrame: CGRect = .zero
    @State var viewOffset: CGSize = .zero
    var body: some View {
        Rectangle()
        .stroke(Color.blue, lineWidth: 4)
        .frame(width: 50, height: 100)
        .offset(viewOffset)
        .background(Text("\(num)"))
        .overlay(GeometryReader { r in
            Color.red
                .opacity(0.5)
                .onAppear {
                    self.viewFrame = r.frame(in: .global)
                    self.field_env.rect_frames[self.id] = r.frame(in: .global)
            }
        })
        .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
            .onChanged { value in
                self.viewOffset = value.translation
        }
            .onEnded { value in
                let newFrame = self.viewFrame.offsetBy(dx: self.viewOffset.width, dy: self.viewOffset.height)
                if let swap_rect = self.field_env.rect_frames.first(where: {[=11=].key != self.id && [=11=].value.intersects(newFrame)}) {
                    print("intersects")
                    withAnimation {
                        self.field_env.rects.swapAt(self.field_env.rects.firstIndex(of: self)!, self.field_env.rects.firstIndex(where: {[=11=].id == swap_rect.key})!)
                    }
                } else {
                    print("nope")
                }
                self.viewOffset = .zero
        }
        )
    }
}

struct field: View {
    let NC = NotificationCenter.default
    @EnvironmentObject var field_env: FieldEnv
    var body: some View {
        HStack {
            ForEach(self.field_env.rects, id:\.id) { rect in
                    rect
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            field()
        }
    }
}
```

更新:

我尝试在通过通知中心执行交换后手动更新 GR 视图帧,因为我认为 GR 每次都使用初始帧位置(在 .onAppear 期间获得)并且交换后事情变得混乱,但它似乎问题出在其他地方 - 仍然交换了错误的数组索引(不是属于相交视图的索引):

```swift
struct rect: View {
    @EnvironmentObject var field_env: FieldEnv
    let NC = NotificationCenter.default
    let id = UUID()
    let num: String
    @State var viewFrame: CGRect = .zero
    @State var viewOffset: CGSize = .zero
    var body: some View {
        Rectangle()
        .stroke(Color.blue, lineWidth: 4)
        .frame(width: 50, height: 100)
        .offset(viewOffset)
        .background(Text("\(num)"))
        .overlay(GeometryReader { r in
            Color.red
                .opacity(0.5)
                .onAppear {
                    self.viewFrame = r.frame(in: .global)
                    self.field_env.rect_frames[self.id] = r.frame(in: .global)
                    self.NC.addObserver(forName: .cardsSwapped, object: nil, queue: nil, using: {_ in self.field_env.rect_frames[self.id] = r.frame(in: .global)})
                }
        })
        .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
            .onChanged { value in
                self.viewOffset = value.translation
        }
            .onEnded { value in
                let newFrame = self.viewFrame.offsetBy(dx: self.viewOffset.width, dy: self.viewOffset.height)
                if let swap_rect = self.field_env.rect_frames.first(where: {[=12=].value.intersects(newFrame)}) {
                    print("intersects")
                    withAnimation {
                        self.field_env.rects.move(fromOffsets: [self.field_env.rects.firstIndex(of: self)!], toOffset: self.field_env.rects.firstIndex(where: {[=12=].id == swap_rect.key})!)
                    }
                } else {
                    print("nope")
                }
                self.viewOffset = .zero
                self.NC.post(name: .cardsSwapped, object: nil)
        }
        )
    }
}
```

Nathan,我不确定这会有多大帮助,但是,对于它的价值,您可以看看 Paul Hudson 创建的名为 Switcheroo 的 Swift on Sundays 项目。在这个游戏中,您有一个字母托盘可供选择,您可以从托盘中拖出一个字母来替换上面 4 个字母中的一个来组成一个新单词。我假设这有可能对您有用的各种冲突。您可以在此处的 GiHub 上找到该项目:https://github.com/twostraws/SwiftOnSundays

好的,我终于成功了。我决定采用 Paul 的概念,即在数组中存储 strings 而不是完整的 views,并在 ForEach 循环期间使用实际创建视图那些字符串。不知何故,交换开始正常工作(即使用适当的索引)。为了提高精度,我还将 .intersects 替换为 .contains。所以感谢 Chrispy 为我指明了正确的方向!

看起来这次它工作正常......但是解决方案本身和开销代码量远非完美,不确定是否有更简单的方法 - 请看看,欢迎所有想法

import SwiftUI

extension Notification.Name {
    static let viewModified = Notification.Name("view_modified")

}

class FieldEnv: ObservableObject {
    @Published var rect_frames: [CGRect] = []
    @Published var rects: [Int] = [0,1,2]
}


struct rect: View {
    @EnvironmentObject var field_env: FieldEnv
    let num: Int
    @State var viewOffset: CGSize = .zero
    var body: some View {
        Rectangle()
        .stroke(Color.blue, lineWidth: 4)
        .frame(width: 50, height: 100)
        .overlay(Text("\(num)"))
        .offset(viewOffset)
        .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
            .onChanged { drag in
                self.viewOffset = drag.translation
        }
            .onEnded { drag in
                if let swap_rect_i = self.field_env.rect_frames.firstIndex(where: {[=10=].contains(drag.location)}) {
                    print("intersects")
                    withAnimation {
                        self.field_env.rects.swapAt(self.field_env.rects.firstIndex(of: self.num)!, swap_rect_i)
                        self.viewOffset = .zero
                    }

                } else {
                    print("nope")
                    self.viewOffset = .zero
                }

        }
        )
    }
}


struct field: View {
    @EnvironmentObject var field_env: FieldEnv
    let NC = NotificationCenter.default
    var body: some View {
        HStack {
            ForEach(self.field_env.rects, id:\.self) { num in
                rect(num: num)
                .background(GeometryReader { r in
                    Color.clear
                        .onAppear {
                            self.NC.post(name: .viewModified, object: nil)
                        }
                        .onDisappear {
                            self.NC.post(name: .viewModified, object: nil)
                        }
                    .onReceive(self.NC.publisher(for: .viewModified, object: nil), perform: {_ in
                        self.field_env.rect_frames = [CGRect](repeating: .zero, count: self.field_env.rects.count)
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                            self.field_env.rect_frames[self.field_env.rects.firstIndex(of: num)!] = r.frame(in: .global)
                        }
                    })
                })
            }
        }.onAppear {
            self.field_env.rect_frames = [CGRect](repeating: .zero, count: self.field_env.rects.count)
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var field_env: FieldEnv
    var body: some View {
        VStack {
            Spacer()
            field()
            Spacer()
            HStack {
                Button(action: {self.field_env.rects.append((self.field_env.rects.max() ?? 0)+1)}) {
                    Text("Add 1")
                }
                Button(action: {self.field_env.rects.removeLast()}) {
                    Text("Del 1")
                }
            }
            Spacer()
        }
    }
}

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