I'm having an issue that I can't figure out. I can't seem to figure out how to start the gesture where my last gesture position ended.
import SwiftUI
struct TestView: View {
#State var xPosition: CGFloat = 50
#State var yPosition: CGFloat = 50
var body: some View {
VStack{
Circle()
.frame(width: 100, height: 100)
.position(x: xPosition, y: yPosition)
.gesture(DragGesture()
.onChanged{gesture in
xPosition = gesture.translation.width
yPosition = gesture.translation.height
}
.onEnded{gesture in
xPosition = gesture.translation.width
yPosition = gesture.translation.height
}
)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
This is the code, let say I make a gesture and move my circle to any given position (works great) but when I try dragging it again the gesture start again from the first position (50,50). is there a way to fix it that with GestureState or updating or anything else?
Thanks for taking the time to help me out on this one.
The translation is a relative location (equivalent to location.{x,y} - startLocation.{x,y}).
You should store the position instead, i.e.
struct TestView: View {
#State var position: CGPoint = CGPoint(x: 50, y: 50)
var body: some View {
VStack{
Circle()
.frame(width: 100, height: 100)
.position(position)
.gesture(
DragGesture()
.onChanged { gesture in
position = gesture.location
}
.onEnded { gesture in
position = gesture.location
}
)
}
}
}
Related
I am creating a Cardview stored with user data for each user in the usersList. Now I want to offset each card for itself by pressing on either button. If I press the button now I will offset every card created by the foreach. How can I make the button only affect the first card? It is probably a better Idea to offset the card in the CardView itself, like I did with the Draggesture and not in the ContentView
struct ContentView: View {
#State var usersList: [User] = []
#State var fetchingComplete: Bool = false
#State var offset = CGSize.zero
#State var buttonPushed: Bool = false
var body: some View {
VStack {
NavBar().onAppear() {
fetchUsers()
}
if fetchingComplete {
ZStack {
ForEach(usersList, id: \.id) { user in
CardView(user: user).offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring(), value: buttonPushed)
}
}
}
ButtonsBar(offset: $offset, buttonPushed: $buttonPushed)
}
}
}
public func fetchUsers() -> [User]{
FirebaseManager.shared.firestore.collection("users").getDocuments { snapshot, error in
if let error = error {
print("Failed to fetch users: ", error)
return
}
for document in snapshot!.documents {
let user = User(dictionary: document.data())
usersList.append(user)
}
fetchingComplete.toggle()
}
return usersList
}
}
struct ButtonsBar:View {
#Binding var offset: CGSize
#Binding var buttonPushed: Bool
var body: some View {
HStack {
Button {
print("perform dislike")
offset = CGSize(width: -500, height: 0)
buttonPushed.toggle()
} label: {
Image("dismiss_circle")
}.frame(width: 65)
Button {
print("perform like")
offset = CGSize(width: 500, height: 0)
buttonPushed.toggle()
} label: {
Image("like_circle")
}.frame(width: 65)
}
}
}
struct CardView: View {
#State private var offset = CGSize.zero
var user: User
var body: some View {
VStack{
Image("lady")
.resizable()
}.cornerRadius(12)
.frame(width: 310, height: 392)
.scaledToFit()
.overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
} .onEnded { _ in
withAnimation {
swipeCard(width: offset.width)
}
}
)
}
func swipeCard(width: CGFloat) {
switch width {
case -500...(-150):
offset = CGSize(width: -500, height: 0)
case 150...500:
offset = CGSize(width: 500, height: 0)
default:
offset = .zero
}
}
}
Right now, your current design will be unable to achieve what you're saying. How you have designed offset makes it so there's only the single source of truth being a single var offset. Thus, there's no way to give an offset to each card uniquely. Likewise, your design of a single ButtonBar will also be unable to achieve the goal of modifying each unique entry. How would this ButtonBar as it is choose any particular CardView? I know how to implement this, but I think you can figure it out once I explain more on how to uniquely associate values to each CardView.
I know you asked for how to just indent the first card only, but I don't think the easiest answer will be your desired outcome.
What you should do is that you have to track an offset and/or a buttonPressed for EACH user in your UserList. The easiest way is to create a CardViewModel so at the top of your CardView add
#StateObject var cardVM = CardViewModel()
and for the CardViewModel,
class CardViewModel : ObservableObject {
#Published var offset = CGSize.zero
#Published var buttonPushed: Bool = false
}
Now we have a unique offset and buttonPushed for each CardView. From here I would remove the modifiers
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring(), value: buttonPushed)
and inside the CardView place your ButtonBar like so
struct CardView: View {
#StateObject var cardVM = CardViewModel()
var user: User
var body: some View {
VStack{
ButtonBar(offset: $cardVM.offset, buttonPushed: $cardVM.buttomPushed)
Image("lady")
.resizable()
}
.cornerRadius(12)
.frame(width: 310, height: 392)
.scaledToFit()
.overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded { _ in
withAnimation {
swipeCard(width: offset.width)
}
}
)
// the offset and effects based on button push
.offset(x: cardVM.offset.width, y: cardVM.offset.height * 0.4)
.rotationEffect(.degrees(Double(cardVM.offset.width / 40)))
.animation(.spring(), value: cardVM.buttonPushed)
}
func swipeCard(width: CGFloat) {
switch width {
case -500...(-150):
offset = CGSize(width: -500, height: 0)
case 150...500:
offset = CGSize(width: 500, height: 0)
default:
offset = .zero
}
}
}
Because this isn't an MRE, I'm not able to actually test this, but in general, this should follow the design of creating a CardViewModel for each CardView which contains unique to each view variables offset and buttonPressed and placing the ButtonBar in each CardView allows us to modify the CardViewModel uniquely too.
EDIT:
In your main View, create
#StateObject var buttonBarVM = ButtonBarViewModel()
which has a
#Published var cardVM = CardViewModel().
In CardView, do
#EnvironmentObject var buttonBarVM: ButtonBarViewModel and
.onTap of the CardView or whatever gesture to do self.ButtonBarVM.cardVM = self.cardVM
In ButtonBar, instantiate
#EnvironmentObject var buttonBarVM: ButtonBarViewModel
and change the buttons to modify self.buttonBarVM.cardVM.(states)
One thing I would do is also pass #Published var user: User into cardVM and so when everything is first instantiated, you can wrap the ButtonBar in an if to only show when the user in cardVM is not default because at the moment, unless you first set the ButtonBar, the actions won't do anything until you gesture a CardView.
Here, we create a barButtonsVM to be our controller of a cardVM that you need to select, and we can set that based on a gesture you do to a card. We must preserve #StateObject cardVM = CardViewModel() because that specifically creates a ViewModel for each CardView.
struct ContentView: View {
#State private var location: CGPoint = CGPoint(x: 150, y: 100)
#GestureState private var startLocation: CGPoint? = nil
#State var windowSize = CGSize(width: 300, height: 200)
var dragGesture: some Gesture {
DragGesture().onChanged { value in
location = (startLocation ?? location) + value.translation
}.updating($startLocation) {
(_, startLocation, _) in
startLocation = startLocation ?? location
}
}
var body: some View {
VStack {
//Title
ZStack {
Color.green
Text("Title bar")
}.frame(height: 30)
.gesture(dragGesture) // (1)
//Contents
Spacer()
Text("contents")
Spacer()
}.background(Color.gray)
.frame(width: windowSize.width, height: windowSize.height)
.position(location) // (3)
//.gesture(dragGesture) // (2)
}
}
func +(lhs: CGPoint, rhs: CGSize) -> CGPoint {
return CGPoint(
x: lhs.x + rhs.width,
y: lhs.y + rhs.height
)
}
The dragging effect works if (1) is commented out and (2) uncommented. But I want to recognize drag gestures only on the title bar to move the whole View. What I tried is to add a gesture to the ZStack (1) modifying the state of the VStack (3).
I got a solution later.
ZStack {
TitleBar() //some view
.position(/*position for title bar*/)
.gesture(dragGesture)
Contents() //some view
.position(/*position for contents below, which can be a computed variable from TitleBar's (state) position*/)
}
I have 25 buttons laid out in a grid for a game like this.
I'm generating these buttons like this:
VStack {
ForEach(1..<6) {_ in
HStack {
ForEach(1..<6) {_ in
Button(action: {
// Button clicked
doButton()
}) {
Rectangle()
.frame(width: 50, height: 50)
.border(Color.black, lineWidth: 1)
}
}
}
}
}
How can I make it so if the user drags their finger across multiple buttons, each one of the buttons are clicked, and doButton() is executed?
I tried using a DragGesture() but couldn't get it to work...
Swift 5.1, iOS 13.
I don't have a 100% solution for you, but I do have something to get you off the ground hopefully. These are not buttons, but simply squares. As I click/drag across them they^ll signal I passed by.
It isn't perfect, but should give you some thing more to work with.
I read a tutorial on medium on doing something similar with drag and drop. This one.
Can help but think you'll need to do something with drag here and the Geometry reader ultimately, detecting where they are and responding to that.
import SwiftUI
struct ContentView: View {
var body: some View {
return VStack {
ForEach(1..<6) {colY in
HStack {
ForEach(1..<6) {rowX in
boxView(row: rowX, col: colY)
}
}
}
}
}
}
struct boxView: View {
#State var row: Int
#State var col: Int
var body: some View {
let dragGesture = DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.global)
.onChanged { (value) in
print("trigger ",self.row,self.col)
}
return Rectangle()
.stroke(Color.black)
.frame(width: 50, height: 50)
.gesture(dragGesture)
}
}
import SwiftUI
class Container : ObservableObject {
#Published var location = CGPoint()
public func updateLocation(_ location: CGPoint) {
self.location = location
}
}
struct SliderGestureButtonsView: View {
#ObservedObject var container = Container()
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(1..<6) { colY in
HStack {
ForEach(1..<6) { rowX in
GeometryReader { gp in
let x = gp.frame(in:.named("hive")).midX
let y = gp.frame(in:.named("hive")).midY
let w = gp.frame(in:.named("hive")).size.width
let h = gp.frame(in:.named("hive")).size.height
let rect = CGRect(x: x, y: y, width: w, height: h)
boxView(row: rowX, col: colY, rect: rect, container: container)
}
}
}.frame(height: 60)
}
}.coordinateSpace(name: "hive").frame(width: 300)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.named("hive"))
.onChanged { (value) in
container.updateLocation(value.location)
}
)
}
}
struct boxView: View {
var row: Int
var col: Int
var rect: CGRect
#ObservedObject var container: Container
var body: some View {
let x = container.location.x
let y = container.location.y
let hw = rect.size.width / 2
let ox = rect.origin.x
let withinX = x > ox - hw && x < ox + hw
let hh = rect.size.height / 2
let oy = rect.origin.y
let withinY = y > oy - hh && y < oy + hh
var pressed = false
if(withinX && withinY) {
pressed = true
print("trigger ", self.row, self.col)
}
return Rectangle()
.stroke(Color.white)
.frame(width: 60, height: 60)
.background(pressed ? Color.green : Color.black)
}
}
I have a ZStack with an Image and a small circle, and some gestures for panning and zooming in/out on the Image. I want the small circle to change location based on where the user taps. Everything is working fine until I start zooming in with scaleEffect. How can I account for the current scale value when placing the circle? (See TODO: Fix tap point when scaled for where the issue is.)
struct PodMapTest: View {
#State var circlePosition = CGPoint.zero
#State var position = CGSize.zero
#State var scale: CGFloat = 1.0
#State var lastScaleValue: CGFloat = 1.0
#State var tapPoint: CGPoint = CGPoint.zero
#State var dragSize: CGSize = CGSize.zero
#State var lastDrag: CGSize = CGSize.zero
#GestureState private var dragOffset = CGSize.zero
var body: some View {
VStack {
Spacer()
ZStack {
Image("floorPlan")
.resizable()
self.circle
}
.scaledToFit()
.animation(.linear)
.scaleEffect(self.scale)
.offset(self.dragSize)
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.scale = self.scale * delta
}.onEnded { val in
// without this the next gesture will be broken
self.lastScaleValue = 1.0
}
)
.simultaneousGesture(DragGesture(minimumDistance: 1, coordinateSpace: .local).onChanged({ val in
print("dragging map")
self.tapPoint = val.startLocation
self.dragSize = CGSize(width: val.translation.width + self.lastDrag.width, height: val.translation.height + self.lastDrag.height)
})
.onEnded({ (val) in
self.dragSize = CGSize(width: val.translation.width + self.lastDrag.width, height: val.translation.height + self.lastDrag.height)
self.lastDrag = self.dragSize
}))
.simultaneousGesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({ (val) in
// TODO: Fix tap point when scaled
let tapPoint = CGPoint(x: val.startLocation.x - self.dragSize.width, y: val.startLocation.y - self.dragSize.height)
self.circlePosition = tapPoint
}))
Spacer()
}
}
var circle: some View {
Circle()
.frame(width: 20, height: 20, alignment: .center)
.position(circlePosition)
}
}
I have not been able to find an equivalent to the pan gesture in SwiftUI. I do see
and use magnify, tap, drag and rotate - but I do not see any built in pan. In the
following code snippet I add an image and allow the user to zoom - but I want the
user to also move the zoomed image to focus on the area of interest. Dragging, of
course does not do the job - it just moves the frame.
I tried layering a frame on top and moving the bottom image but could not make that
work either.
struct ContentView: View {
#State var scale: CGFloat = 1.0
#State var isScaled: Bool = false
#State private var dragOffset = CGSize.zero
var body: some View {
GeometryReader { geo in
VStack {
ZStack{
RoundedRectangle(cornerRadius: 40)
.foregroundColor(Color.white)
.frame(width: geo.size.width - 45, height: geo.size.width - 45)
.shadow(radius: 10)
Image("HuckALaHuckMedium")
.resizable()
.scaleEffect(self.scale)
.frame(width: geo.size.width - 60, height: geo.size.width - 60)
.cornerRadius(40)
.aspectRatio(contentMode: .fill)
.shadow(radius: 10, x: 20, y: 20)
//need pan not drag
.gesture(
DragGesture()
.onChanged { self.dragOffset = $0.translation }
.onEnded { _ in self.dragOffset = .zero }
)
//this works but you can't "zoom twice"
.gesture(MagnificationGesture()
.onChanged { value in
self.scale = self.isScaled ? 1.0 : value.magnitude
}
.onEnded({ value in
//self.scale = 1.0
self.isScaled.toggle()
})
)
.animation(.easeInOut)
.offset(self.dragOffset)
}//zstack
Spacer()
}
}
}
}
An original image example:
And that image after zoom - but with drag not pan:
Any guidance would be appreciated. Xcode 11.3 (11C29)
To make it simpler and more readable, I created an extension/modifier for that
struct DraggableView: ViewModifier {
#State var offset = CGPoint(x: 0, y: 0)
func body(content: Content) -> some View {
content
.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
self.offset.x += value.location.x - value.startLocation.x
self.offset.y += value.location.y - value.startLocation.y
})
.offset(x: offset.x, y: offset.y)
}
}
extension View {
func draggable() -> some View {
return modifier(DraggableView())
}
}
Now all you have to do is call the modifier:
Image(systemName: "plus")
.draggable()
For others. This is really simple. Ensure that the magnification is accomplished before the drag is allowed - it will work like a pan. First define the drag gesture:
let dragGesture = DragGesture()
.onChanged { (value) in
self.translation = value.translation
}
.onEnded { (value) in
self.viewState.width += value.translation.width
self.viewState.height += value.translation.height
self.translation = .zero
}
Then create an #State value that you toggle to true after the magnification gesture has been activated. When you attach the gesture to the image, do so conditionally:
.gesture(self.canBeDragged ? dragGesture : nil)
I'm a little late, but for anyone looking, please try this as it worked for me. What you want to do is change the offset of the image right after you zoom out in your MagnificationGesture.onChanged() lifecycle.
var magnification: some Gesture {
MagnificationGesture()
.updating($magnifyBy) {
.....
}
.onChanged() { _ in
//Zoom Out value of magnifyBy < 1.0
if self.magnifyBy <= 0.6{
//Change offset of image after zoom out to center of screen
self.currentPosition = CGSize(width: 1.0, height: 1.0)
}
self.newPosition = self.currentPosition
}
.onEnded{
.....
} }
var body: some View{
Image()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)}
Anyone questions please let me know