1) A number of instances of the same view are embedded inside an array. A Subview of a main ContentView displays them via ForEach in a HStack. Each instance has a geometry reader to detect frame intersections during drag. The goal is to swap views' positions on the main content view when intersection is detected. With the code below it's simply not detecting an intersection.
Is there a better way (than array+foreach) to group & display a dynamic number of instances of some view?
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: {$0.id != self.id && $0.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) The only way I was somewhat able to achieve this is by storing additional GR collision frame of each view inside a published array var of environmentObject/ObservableObject. However, once two instances of my view swap their positions on the main view, it seems GR collision frame CGRects are not getting updated (as GR only has onappear method) and next time I perform view drag & intersect wrong views are swapped (not the ones visually collided) e.g. the code below displays 3 rectangles labeled 0,1,2 (as their indexes in the array). First time you drag "2" onto "1" they swap correctly, but then if you drag "1" onto "2" the "1" gets swapped with "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: {$0.key != self.id && $0.value.intersects(newFrame)}) {
print("intersects")
withAnimation {
self.field_env.rects.swapAt(self.field_env.rects.firstIndex(of: self)!, self.field_env.rects.firstIndex(where: {$0.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()
}
}
}
```
UPDATE:
I've tried manually updating GR view frames after the swap is performed via notification center as I thought that GR was using initial frame positions (obtained during .onAppear) every time and things got confused after the swap, but it seems the problem is somewhere else - still wrong array indexes are getting swapped (not the ones that belong to the intersected views):
```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: {$0.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: {$0.id == swap_rect.key})!)
}
} else {
print("nope")
}
self.viewOffset = .zero
self.NC.post(name: .cardsSwapped, object: nil)
}
)
}
}
```
looks like this time it's working properly...however the solution itself and amount of overhead code far from being perfect, not sure maybe there's more easy way - please take a look, all ideas welcome
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: {$0.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()
}
}