How to Rearrange Views in Swiftui Zstack by Dragging

How to rearrange views in SwiftUI ZStack by dragging

check this out:

the "trick" is that you just need to reorder the z order of the items. therefore you have to "hold" the cards in an array.

let cardSpace:CGFloat = 10

struct Card : Identifiable, Hashable, Equatable {

static func == (lhs: Card, rhs: Card) -> Bool {
lhs.id == rhs.id
}

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

var id = UUID()

var intID : Int

static let cardColors: [Color] = [.orange, .green, .yellow, .purple, .red, .orange, .green, .yellow, .purple]

var zIndex : Int
var color : Color
}

class Data: ObservableObject {

@Published var cards : [Card] = []

init() {
for i in 0..<Card.cardColors.count {
cards.append(Card(intID: i, zIndex: i, color: Card.cardColors[i]))
}
}
}

struct ContentView: View {

@State var data : Data = Data()

var body: some View {
HStack {
VStack {
CardView().environmentObject(data)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// .position(x: 370, y: 300)
}
}

struct CardView: View {

@EnvironmentObject var data : Data

@State var offset = CGSize.zero
@State var dragging:Bool = false
@State var tapped:Bool = false
@State var tappedLocation:Int = -1
@State var locationDragged:Int = -1
var body: some View {
GeometryReader { reader in
ZStack {
ForEach(self.data.cards, id: \.self) { card in
ColorCard(card: card, reader:reader, offset: self.$offset, tappedLocation: self.$tappedLocation, locationDragged:self.$locationDragged, tapped: self.$tapped, dragging: self.$dragging)
.environmentObject(self.data)
.zIndex(Double(card.zIndex))
}
}
}
.animation(.spring())
}
}

struct ColorCard: View {

@EnvironmentObject var data : Data

var card: Card

var reader: GeometryProxy
@State var offsetHeightBeforeDragStarted: Int = 0
@Binding var offset: CGSize
@Binding var tappedLocation:Int
@Binding var locationDragged:Int
@Binding var tapped:Bool
@Binding var dragging:Bool
var body: some View {
VStack {
Group {
VStack {
card.color
}
.frame(width: 300, height: 400)
.cornerRadius(20).shadow(radius: 20)
.offset(
x: (self.locationDragged == card.intID) ? CGFloat(card.zIndex) * self.offset.width / 14
: 0,
y: (self.locationDragged == card.intID) ? CGFloat(card.zIndex) * self.offset.height / 4
: 0
)
.offset(
x: (self.tapped && self.tappedLocation != card.intID) ? 100 : 0,
y: (self.tapped && self.tappedLocation != card.intID) ? 0 : 0
)
.position(x: reader.size.width / 2, y: (self.tapped && self.tappedLocation == card.intID) ? -(cardSpace * CGFloat(card.zIndex)) + 0 : reader.size.height / 2)
}
.rotationEffect(
(card.zIndex % 2 == 0) ? .degrees(-0.2 * Double(arc4random_uniform(15)+1) ) : .degrees(0.2 * Double(arc4random_uniform(15)+1) )
)

.onTapGesture() { //Show the card
self.tapped.toggle()
self.tappedLocation = self.card.intID
}

.gesture(
DragGesture()
.onChanged { gesture in

self.locationDragged = self.card.intID
self.offset = gesture.translation

if self.offset.height > 60 ||
self.offset.height < -60 {
withAnimation {

if let index = self.data.cards.firstIndex(of: self.card) {
self.data.cards.remove(at: index)
self.data.cards.append(self.card)

for index in 0..<self.data.cards.count {
self.data.cards[index].zIndex = index
}
}
}
}

self.dragging = true
}
.onEnded { _ in
self.locationDragged = -1 //Reset
self.offset = .zero
self.dragging = false
self.tapped = false //enable drag to dismiss
self.offsetHeightBeforeDragStarted = Int(self.offset.height)
}
)
}.offset(y: (cardSpace * CGFloat(card.zIndex)))
}
}

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

Sample Image

Combining of views using coordinates and zstack

task was easy, instead of using regular solid shapes with "magic methods from black box" do it all manually

convert decart to iso and place it whatever you want together with text or anything else and offset will be fine

Adjusting the frame of a SwiftUI view with a DragGesture resizes it in all dimensions

You're right that the issue is caused by your view being center-aligned.

To fix this, you can wrap your view in a VStack with a different alignment applied, e.g. .topLeading if you want it to align to the top-left.

You also have to make sure this VStack is taking up the available space of the view. Otherwise, it will shrink to the size of your resizable box, causing the view to stay center-aligned. You can achieve this with .frame(maxWidth: .infinity, maxHeight: .infinity).

TL;DR

Place your resizable box in a VStack with a .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) modifier.

VStack { // <-- Wrapping VStack with alignment modifier
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width + value.translation.width)
height = max(100, height + value.translation.height)
}
)
}
.frame(width: width, height: height, alignment: .topLeading)
.border(.red, width: 5)
.background(.yellow)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)

Edit

With an additional position modifier on the ZStack, try offsetting the x and y values by half of the width and height to position relative to the top-left origin of the ZStack:

.position(x: 400 + width / 2, y: 400 + height / 2)

Custom Picker appearing causes other views to rearrange

Try using the CustomPicker as overlay instead.

It will be treated similar to the elements inside a ZStack container, so the views under the overlay keep their original position.

 Button("Show picker") {
showPicker.toggle()
}
.overlay {
CustomerPicker() //this part
}

SwiftUI: VStack/HStack/ZStack drag gesture not working

Actually, there's a SwiftUI feature meant to address just that issue with no need for a background.

Simply place .contentShape(Rectangle()) just before your gesture modifier and it will respond to the whole enclosing rectangle. :)

How to layout properly in ZStack (I have visibility problem)?

iOS 15.5: still valid

How can achieve my goal then, like dragging Element on different view (in this scenario Color.blue)

Actually we need to disable clipping by ScrollView.

Below is possible approach based on helper extensions from my other answers (https://stackoverflow.com/a/63322713/12299030 and https://stackoverflow.com/a/60855853/12299030)

demo

VStack {
Spacer()
ScrollView(.horizontal) {
HStack {
ForEach(1...15, id: \.self) { (idx) in
Element(index: idx)
}
}
.padding()
.background(ScrollViewConfigurator {
$0?.clipsToBounds = false // << here !!
})


}
.background(Color.secondary.opacity(0.3))
}


Related Topics



Leave a reply



Submit