swiftUI:检测嵌入数组中的两个视图的交集并交换它们的位置
swiftUI: detect intersection of two views embedded inside an array and swap their positions
1) 同一视图的多个实例嵌入在一个数组中。主 ContentView 的子视图通过 HStack 中的 ForEach 显示它们。每个实例都有一个几何体 reader 来检测拖动过程中的帧交叉点。目标是在检测到交集时交换视图在主要内容视图上的位置。使用下面的代码,它根本就没有检测到交叉点。
- 为什么?
- 是否有 better/more 简单的方法(比几何 reader)检测 collisions/intersections 的视图?
是否有更好的方法(比数组+foreach)来分组和显示某些视图的动态实例数?
var rects: [rect] = [rect(num:"0"),rect(num:"1"),rect(num:"2")]
struct rect: View {
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)
}
})
.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 = rects.first(where: {[=10=].id != self.id && [=10=].viewFrame.intersects(newFrame)}) {
print("intersects")
rects.swapAt(rects.firstIndex(of: self), rects.firstIndex(of: swap_rect))
} else {
print("nope")
}
self.viewOffset = .zero
}
)
}
}
struct field: View {
var body: some View {
HStack {
ForEach(self.field_env.rects, id:\.id) { rect in
rect
}
}
}
}
struct ContentView: View {
var body: some View {
VStack {
field()
}
}
}
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()
}
}
1) 同一视图的多个实例嵌入在一个数组中。主 ContentView 的子视图通过 HStack 中的 ForEach 显示它们。每个实例都有一个几何体 reader 来检测拖动过程中的帧交叉点。目标是在检测到交集时交换视图在主要内容视图上的位置。使用下面的代码,它根本就没有检测到交叉点。
- 为什么?
- 是否有 better/more 简单的方法(比几何 reader)检测 collisions/intersections 的视图?
是否有更好的方法(比数组+foreach)来分组和显示某些视图的动态实例数?
var rects: [rect] = [rect(num:"0"),rect(num:"1"),rect(num:"2")] struct rect: View { 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) } }) .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 = rects.first(where: {[=10=].id != self.id && [=10=].viewFrame.intersects(newFrame)}) { print("intersects") rects.swapAt(rects.firstIndex(of: self), rects.firstIndex(of: swap_rect)) } else { print("nope") } self.viewOffset = .zero } ) } } struct field: View { var body: some View { HStack { ForEach(self.field_env.rects, id:\.id) { rect in rect } } } } struct ContentView: View { var body: some View { VStack { field() } } }
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()
}
}