SwiftUI drag gesture across multiple subviews
Here is a demo of possible approach... (it is simplified version of your app data settings, but the idea and direction where to evolve should be clear)
The main idea that you capture drag not in item view but in the content view transferring needed states (or calculable dependent data) into item view when (or if) needed.
struct PlayerView: View {
var scaled: Bool = false
var player: Player = Player(name: "Phile", color: .green, age: 42)
var body: some View {
ZStack(alignment: .topLeading) {
Rectangle().frame(width: 100, height: 100).foregroundColor(player.color).cornerRadius(15.0).scaleEffect(scaled ? 1.5 : 1)
VStack {
Text(player.name)
Text("Age: \(player.age)")
}.padding([.top, .leading], 10)
}.zIndex(scaled ? 2 : 1)
}
}
struct ContentView: View {
@EnvironmentObject var data: PlayerData
@GestureState private var location: CGPoint = .zero
@State private var highlighted: Int? = nil
private var Content: some View {
VStack {
HStack {
ForEach(0..<3) { i in
PlayerView(scaled: self.highlighted == i, player: self.data.players[i])
.background(self.rectReader(index: i))
}
}
.zIndex((0..<3).contains(highlighted ?? -1) ? 2 : 1)
HStack {
ForEach(3..<6) { i in
PlayerView(scaled: self.highlighted == i, player: self.data.players[i])
.background(self.rectReader(index: i))
}
}
.zIndex((3..<6).contains(highlighted ?? -1) ? 2 : 1)
}
}
func rectReader(index: Int) -> some View {
return GeometryReader { (geometry) -> AnyView in
if geometry.frame(in: .global).contains(self.location) {
DispatchQueue.main.async {
self.highlighted = index
}
}
return AnyView(Rectangle().fill(Color.clear))
}
}
var body: some View {
Content
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.updating($location) { (value, state, transaction) in
state = value.location
}.onEnded {_ in
self.highlighted = nil
})
}
}
Is there a SwiftUI drag enter gesture?
I ended up intercepting touches in an overlay view, then highlight items when touch location is contained in its bounds. I tried both onDrop
and using UIKit
gestures/actions, but none of them came without drawbacks. So I opted to implement George's comment above instead.
I incorporated a model object where I store the frames on every update, so highlight effect can adopt dynamically. It makes the solution animation proof, while picker also can be displayed anywhere on the screen.
import SwiftUI
class Model: ObservableObject {
let coordinateSpace = "CoordinateSpace"
@Published var isDragged = false
@Published var highlightedNumber: Int? = nil
@Published var selectedNumber: Int? = nil
/// Frames for individual picker items (from their respective `GeometryReader`).
private var framesForNumbers: [Int: CGRect] = [:]
func update(frame: CGRect, for number: Int) {
framesForNumbers[number] = frame
}
/// Updates `highlightedNumber` according drag location.
func update(dragLocation: CGPoint) {
// Lookup if any frame contains drag location.
for (eachNumber, eachFrame) in framesForNumbers {
if eachFrame.contains(dragLocation) {
// Publish.
self.highlightedNumber = eachNumber
return
}
}
// Reset otherwise.
self.highlightedNumber = nil
}
/// Updates `highlightedNumber` and `selectedNumber` according drop location.
func update(isDragged: Bool) {
// Publish.
self.isDragged = isDragged
if isDragged == false,
let highlightedNumber = self.highlightedNumber {
// Publish.
self.selectedNumber = highlightedNumber
self.highlightedNumber = nil
}
}
}
struct ContentView: View {
@StateObject var model = Model()
var body: some View {
ZStack {
TouchesView(model: model, isDragged: $model.isDragged)
CanvasView(number: $model.selectedNumber)
.allowsHitTesting(false)
PickerView(model: model, highlightedNumber: $model.highlightedNumber)
.allowsHitTesting(false)
}
.ignoresSafeArea()
}
}
/// Handles drag interactions and updates model accordingly.
struct TouchesView: View {
var model: Model
@Binding var isDragged: Bool
var body: some View {
Rectangle()
.foregroundColor(isDragged ? .orange : .yellow)
.coordinateSpace(name: model.coordinateSpace)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
model.update(dragLocation: value.location)
model.update(isDragged: true)
}
.onEnded { state in
model.update(dragLocation: state.location)
model.update(isDragged: false)
}
)
}
}
/// Shows the selected number.
struct CanvasView: View {
@Binding var number: Int?
var body: some View {
VStack {
Text(number.string)
.font(.system(size: 100, weight: .bold))
.foregroundColor(.white)
.offset(y: -50)
}
}
}
/// Displays a picker to select number items from.
struct PickerView: View {
var model: Model
@Binding var highlightedNumber: Int?
var body: some View {
HStack(spacing: 5) {
PickerItemView(number: 1, model: model, highlightedNumber: $highlightedNumber)
PickerItemView(number: 2, model: model, highlightedNumber: $highlightedNumber)
PickerItemView(number: 3, model: model, highlightedNumber: $highlightedNumber)
}
.opacity(model.isDragged ? 1 : 0)
.scaleEffect(model.isDragged ? 1 : 0.5, anchor: .top)
.blur(radius: model.isDragged ? 0 : 10)
.animation(.spring(response: 0.15, dampingFraction: 0.4, blendDuration: 0.5))
}
}
/// Shows a number item (also highlights it when `highlightedNumber` matches).
struct PickerItemView: View {
let number: Int
var model: Model
@Binding var highlightedNumber: Int?
var body: some View {
Text(String(number))
.font(.system(size: 25, weight: .bold))
.foregroundColor(isHighlighted ? .orange : .white)
.frame(width: 50, height: 50)
.background(isHighlighted ? Color.white : Color.orange)
.cornerRadius(25)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white, lineWidth: 2)
)
.overlay(GeometryReader { geometry -> Color in
self.model.update(
frame: geometry.frame(in: .named(self.model.coordinateSpace)),
for: self.number
)
return Color.clear
})
.animation(.none)
}
}
extension PickerItemView {
var isHighlighted: Bool {
self.highlightedNumber == self.number
}
}
fileprivate extension Optional where Wrapped == Int {
var string: String {
if let number = self {
return String(number)
} else {
return ""
}
}
}
struct PrototypeView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Only thing I dislike about this is that this method is esentially the same I did 15 years ago in a Flash app. I was hoping to something less "manual".
Related Topics
Swift/Spritekit Multiple Collision Detection
Simulate Universal Gravitation for Two Sprite Kit Nodes
Xcode 6: Can't Connect Any Iboutlet to Viewcontroller
Combine Two String in Different Language Rtl & Ltr
Is This Code Drawing at the Point or Pixel Level? How to Draw Retina Pixels
Reading Text and Images from a PDF Document in iOS
Strange Constraints Behaviour on iPad
Ui Testing Failure - Neither Element Nor Any Descendant Has Keyboard Focus on Securetextfield
Cannot Assign to Property in Protocol - Swift Compiler Error
How to Do Weak Linking in Swift
Application(...Continue Useractivity...) Method Not Called in iOS 13
Uiimagepicker Cameraoverlayview Appears on Retake Screen
Determine User's "Temperature Unit" Setting on iOS 10 (Celsius/Fahrenheit)
iOS - Is Motion Activity Enabled in Settings > Privacy > Motion Activity
How to Broadcast Multiple Ibeacon Signals from Only One Bluetooth? and How
New Itunes Connect Interface -- Should It Immediately Be Seen on "Prerelease"