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())
}
}
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)
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
Swift + Nsviewcontroller Background Color (MAC App)
Swiftui - How to Set the Title of a Navigationview to Large Title (Or Small)
Checking If a Swift Class Conforms to a Protocol and Implements an Optional Function
How to Get Class Name of Parent Viewcontroller in Swift
Format String with Trailing Zeros Removed for X Decimal Places in Swift
If a Function Returns an Unsafemutablepointer Is It Our Responsibility to Destroy and Dealloc
How to Define an Enum as a Subset of Another Enum's Cases
Adding Nscoding as an Extension
Swift: How to Animate the Rowheight of a Uitableview
Cannot Assign to Property: 'Xxxx' Is a Get-Only Property
Swift: Force Show Navigation Bar in Modal
Variable P Passed by Reference Before Being Initialized
Swift3:How to Handle Precedencegroup Now Operator Should Be Declare with a Body
How to Quit Swift Repl Without Using Ctrl-D
Why Does a Public Class/Struct in Swift Require an Explicit Public Initializer