I want a view in which the user is shown an image. By dragging the corner points I want him to be capable of choosing the crop rectangle.
Because the input image can be of bigger dimensions than the screen, I want to use aspect fit as content mode of the image.
The problem I have is that I don't know how to take the displacement caused by the content mode into account when determining the measurements of the crop rectangle relative to the original size of the image.
This might be easier to explain in a video:
As you can see, the context for the positions of the circles is the whole view. I want to use the coordinate system of the resized image instead. For this transformation I would need the difference between the size of the outer view and the resized image.
So the question is: How can I get the correct measurements of the user-chosen rectangle with respect to the resized image?
import SwiftUI
struct CropImageViewTest: View {
var currentImage: Image
#State private var currentPositionTopLeft: CGPoint = .zero
#State private var newPositionTopLeft: CGPoint = .zero
#State private var currentPositionTopRight: CGPoint = .zero
#State private var newPositionTopRight: CGPoint = .zero
#State private var currentPositionBottomLeft: CGPoint = .zero
#State private var newPositionBottomLeft: CGPoint = .zero
#State private var currentPositionBottomRight: CGPoint = .zero
#State private var newPositionBottomRight: CGPoint = .zero
var body: some View {
ZStack {
VStack {
Text("Top left: \(currentPositionTopLeft.x) | \(currentPositionTopLeft.y)")
Text("Top right: \(currentPositionTopRight.x) | \(currentPositionTopRight.y)")
Text("Bottom left: \(currentPositionBottomLeft.x) | \(currentPositionBottomLeft.y)")
Text("Bottom right: \(currentPositionBottomRight.x) | \(currentPositionBottomRight.y)")
Spacer()
currentImage
.resizable()
.aspectRatio(1 , contentMode: .fit)
.background(Color.red)
Spacer()
Group {
Button(action: {
// TODO: Crop it
}) {
Image(systemName: "checkmark").resizable().frame(width: 24, height: 24)
.padding(20)
.background(Color(Colors.getColor(Colors.colorSboBlue)))
.foregroundColor(Color.white)
}.clipShape(Circle())
.shadow(radius: 4)
}
}
getCorners()
}
}
private func getCorners() -> some View{
return
HStack {
VStack {
ZStack {
GeometryReader { geometry in
Path { path in
path.move(to: self.currentPositionTopLeft)
path.addLine(
to: .init(
x: self.currentPositionTopRight.x + geometry.size.width,
y: self.currentPositionTopRight.y
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomRight.x + geometry.size.width,
y: self.currentPositionBottomRight.y + geometry.size.height
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomLeft.x,
y: self.currentPositionBottomLeft.y + geometry.size.height
)
)
path.addLine(
to: .init(
x: self.currentPositionTopLeft.x,
y: self.currentPositionTopLeft.y
)
)
}
.stroke(Color.blue, lineWidth: CGFloat(1))
}
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionTopLeft.x, y: self.currentPositionTopLeft.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionTopLeft = CGPoint(x: value.translation.width + self.newPositionTopLeft.x, y: value.translation.height + self.newPositionTopLeft.y)
}
.onEnded { value in
self.currentPositionTopLeft = CGPoint(x: value.translation.width + self.newPositionTopLeft.x, y: value.translation.height + self.newPositionTopLeft.y)
self.newPositionTopLeft = self.currentPositionTopLeft
print(self.currentPositionTopLeft)
print(self.newPositionTopLeft)
}
)
.opacity(0.5)
.position(CGPoint(x: 0, y: 0))
GeometryReader { geometry in
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionTopRight.x, y: self.currentPositionTopRight.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionTopRight = CGPoint(x: value.translation.width + self.newPositionTopRight.x, y: value.translation.height + self.newPositionTopRight.y)
}
.onEnded { value in
self.currentPositionTopRight = CGPoint(x: value.translation.width + self.newPositionTopRight.x, y: value.translation.height + self.newPositionTopRight.y)
self.newPositionTopRight = self.currentPositionTopRight
print(self.currentPositionTopRight)
print(self.newPositionTopRight)
}
)
.opacity(0.5)
.position(CGPoint(x: geometry.size.width, y: 0))
}
GeometryReader { geometry in
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionBottomLeft.x, y: self.currentPositionBottomLeft.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionBottomLeft = CGPoint(x: value.translation.width + self.newPositionBottomLeft.x, y: value.translation.height + self.newPositionBottomLeft.y)
}
.onEnded { value in
self.currentPositionBottomLeft = CGPoint(x: value.translation.width + self.newPositionBottomLeft.x, y: value.translation.height + self.newPositionBottomLeft.y)
self.newPositionBottomLeft = self.currentPositionBottomLeft
print(self.currentPositionBottomLeft)
print(self.newPositionBottomLeft)
}
)
.opacity(0.5)
.position(CGPoint(x: 0, y: geometry.size.height))
}
GeometryReader { geometry in
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionBottomRight.x, y: self.currentPositionBottomRight.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionBottomRight = CGPoint(x: value.translation.width + self.newPositionBottomRight.x, y: value.translation.height + self.newPositionBottomRight.y)
}
.onEnded { value in
self.currentPositionBottomRight = CGPoint(x: value.translation.width + self.newPositionBottomRight.x, y: value.translation.height + self.newPositionBottomRight.y)
self.newPositionBottomRight = self.currentPositionBottomRight
print(self.currentPositionBottomRight)
print(self.newPositionBottomRight)
}
)
.opacity(0.5)
.position(CGPoint(x: geometry.size.width, y: geometry.size.height))
}
}
Spacer()
}
Spacer()
}
}
}
If you don't have a sample image with you, you can just call the View this way:
CropImageViewTest(currentImage: Image(systemName: "camera.fill"))
I added a red background so that you can see the constraints of the image.
I am also open to completely different approaches if the current way is not the "swiftiest" way to go.
Thank you in advance!
Edit:
I have the UIImage available the (SwiftUI) Image originates from. If this is of any help in determining the correct measurements.
Update:
If I use the crop rectangle as an overlay of the image like so:
currentImage
.resizable()
.aspectRatio(1 , contentMode: .fit)
.overlay(getCorners())
it's actually working. Still, there is the problem that every corner defines its starting position is (0|0). I would like the position to be defined relative to the upper left corner of the image.
Okay, finally solved it.
1.) I used the view with the rectangle and the draggable corners as an overlay of the Image. This way, the origin of the rectangle and the corners is the image, not the surrounding view. Got the inspiration for that from here: https://swiftui-lab.com/geometryreader-to-the-rescue/
2.) There was still the problem that every corner defined it origin (0|0) as where it was initially positioned. I got around that by using
.position(CGPoint(x: 0, y: 0))
and using onAppear to place displace the coordinates.
This leads to the application correctly calculating the coordinates relative to the resized image:
I also encapsulated the rectangle and the corners in custom views resulting in this code:
The root view:
import SwiftUI
struct CropImageViewTest: View {
var currentImage: Image
#State private var currentPositionTopLeft: CGPoint = .zero
#State private var newPositionTopLeft: CGPoint = .zero
#State private var currentPositionTopRight: CGPoint = .zero
#State private var newPositionTopRight: CGPoint = .zero
#State private var currentPositionBottomLeft: CGPoint = .zero
#State private var newPositionBottomLeft: CGPoint = .zero
#State private var currentPositionBottomRight: CGPoint = .zero
#State private var newPositionBottomRight: CGPoint = .zero
var body: some View {
ZStack {
VStack {
Text("Top left: \(currentPositionTopLeft.x) | \(currentPositionTopLeft.y)")
Text("Top right: \(currentPositionTopRight.x) | \(currentPositionTopRight.y)")
Text("Bottom left: \(currentPositionBottomLeft.x) | \(currentPositionBottomLeft.y)")
Text("Bottom right: \(currentPositionBottomRight.x) | \(currentPositionBottomRight.y)")
Spacer()
currentImage
.resizable()
.aspectRatio(1 , contentMode: .fit)
.overlay(getCorners())
Spacer()
Group {
Button(action: {
// TODO: Crop it
}) {
Image(systemName: "checkmark").resizable().frame(width: 24, height: 24)
.padding(20)
.background(Color(Colors.getColor(Colors.colorSboBlue)))
.foregroundColor(Color.white)
}.clipShape(Circle())
.shadow(radius: 4)
}
}
}
}
private func getCorners() -> some View{
return
HStack {
VStack {
ZStack {
CropImageViewRectangle(
currentPositionTopLeft: self.$currentPositionTopLeft,
currentPositionTopRight: self.$currentPositionTopRight,
currentPositionBottomLeft: self.$currentPositionBottomLeft,
currentPositionBottomRight: self.$currentPositionBottomRight
)
GeometryReader { geometry in
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionTopLeft,
newPosition: self.$newPositionTopLeft,
displacementX: 0,
displacementY: 0
)
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionTopRight,
newPosition: self.$newPositionTopRight,
displacementX: geometry.size.width,
displacementY: 0
)
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionBottomLeft,
newPosition: self.$newPositionBottomLeft,
displacementX: 0,
displacementY: geometry.size.height
)
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionBottomRight,
newPosition: self.$newPositionBottomRight,
displacementX: geometry.size.width,
displacementY: geometry.size.height
)
}
}
Spacer()
}
Spacer()
}
}
}
The rectangle:
import SwiftUI
struct CropImageViewRectangle: View {
#Binding var currentPositionTopLeft: CGPoint
#Binding var currentPositionTopRight: CGPoint
#Binding var currentPositionBottomLeft: CGPoint
#Binding var currentPositionBottomRight: CGPoint
var body: some View {
GeometryReader { geometry in
Path { path in
path.move(to: self.currentPositionTopLeft)
path.addLine(
to: .init(
x: self.currentPositionTopRight.x,
y: self.currentPositionTopRight.y
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomRight.x,
y: self.currentPositionBottomRight.y
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomLeft.x,
y: self.currentPositionBottomLeft.y
)
)
path.addLine(
to: .init(
x: self.currentPositionTopLeft.x,
y: self.currentPositionTopLeft.y
)
)
}
.stroke(Color.blue, lineWidth: CGFloat(1))
}
}
}
The corner:
import SwiftUI
struct CropImageViewRectangleCorner: View {
#Binding var currentPosition: CGPoint
#Binding var newPosition: CGPoint
var displacementX: CGFloat
var displacementY: CGFloat
var body: some View {
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPosition.x, y: self.currentPosition.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGPoint(x: value.translation.width + self.newPosition.x, y: value.translation.height + self.newPosition.y)
}
.onEnded { value in
self.currentPosition = CGPoint(x: value.translation.width + self.newPosition.x, y: value.translation.height + self.newPosition.y)
self.newPosition = self.currentPosition
}
)
.opacity(0.5)
.position(CGPoint(x: 0, y: 0))
.onAppear() {
if self.displacementX > 0 || self.displacementY > 0 {
self.currentPosition = CGPoint(x: self.displacementX, y: self.displacementY)
self.newPosition = self.currentPosition
}
}
}
}
Related
I have tried to find the Before-After slider effect for images in SwiftUI. But I cannot find any libraries or solutions. And finally I came up with below solution myself.
Hope this help someone!!
I have added background color for images for to make differ if there is no images.
import SwiftUI
struct ContentView: View {
#State private var location: CGPoint = CGPoint(x: 0, y: 0)
#State private var maskWidth: CGFloat = 0.0
#State var startPoint: CGFloat = 0
#State var endPoint: CGFloat = 0
#State var yPoint: CGFloat = 0
var sliderWidth: CGFloat = 30
var containerWidth: CGFloat = 400
var containerHeight: CGFloat = 300
var body: some View {
ZStack {
ZStack() {
Image("before_img")
.resizable()
.frame(width: containerWidth, height: containerHeight)
.background(Color.red)
.clipped()
Image("after_img")
.resizable()
.frame(width: containerWidth, height: containerHeight)
.clipped()
.background(Color.green)
.mask(mask)
}
.clipped()
Slider
}
.clipped()
.frame(width: containerWidth, height: containerHeight)
.onAppear {
yPoint = containerHeight/2
location = CGPoint(x: containerWidth/2, y: yPoint)
maskWidth = containerWidth/2
endPoint = containerWidth
}
}
var dragAction: some Gesture {
DragGesture()
.onChanged { value in
updateDragView(point: value.location)
updateMaskView(point: value.translation)
}
.onEnded { value in
setInitialPosition()
}
}
var mask: some View {
HStack {
Spacer()
Rectangle()
.mask(Color.black)
.frame(width: maskWidth, height: containerHeight)
}
}
var Slider: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.white)
.frame(width: 4)
Image(systemName: "circle.circle.fill")
.foregroundColor(.white)
.frame(width: sliderWidth, height: sliderWidth)
.font(.system(size: sliderWidth))
Rectangle()
.fill(Color.white)
.frame(width: 4)
}
.position(location)
.gesture(dragAction)
.shadow(radius: 4)
}
func updateDragView(point: CGPoint) {
let locX = point.x
if locX > startPoint && locX < endPoint {
self.location = CGPoint(x: point.x, y: yPoint)
}
}
func updateMaskView(point: CGSize) {
let width = -(point.width)
let newWidth = ((containerWidth/2)+width)
if newWidth > 0 {
maskWidth = ((containerWidth/2)+width)
} else {
setInitialPosition()
}
}
func setInitialPosition() {
withAnimation {
location = CGPoint(x: containerWidth/2, y: yPoint)
maskWidth = containerWidth/2
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I would like to allow a user to crop images by drawing a closed shape on the original image. Sort of like Snapchat stickers. I was looking at this post from 2012 and was curious if there is an updated way to do this with SwiftUI.
To clarify, I would like to take user input (drawing) over a displayed image and crop the actual image (not the view) with a mask that is that shape.
Actually your question need to more specific.
but you can resize your image by following code in swift ui.
Image("Your Image name here!")
.resizable()
.frame(width: 300, height: 300)
if you want to crop at any position of the image you may try this one
import SwiftUI
struct ContentView: View {
var body: some View {
CropImage(imageName: "Your Image name here!")
}
}
// struct Shape
struct CropFrame: Shape {
let isActive: Bool
func path(in rect: CGRect) -> Path {
guard isActive else { return Path(rect) } // full rect for non active
let size = CGSize(width: UIScreen.main.bounds.size.width*0.7, height: UIScreen.main.bounds.size.height/5)
let origin = CGPoint(x: rect.midX - size.width/2, y: rect.midY - size.height/2)
return Path(CGRect(origin: origin, size: size).integral)
}
}
// struct image crop
struct CropImage: View {
let imageName: String
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var clipped = false
var body: some View {
VStack {
ZStack {
Image(imageName)
.resizable()
.scaledToFit()
.offset(x: currentPosition.width, y: currentPosition.height)
Rectangle()
.fill(Color.black.opacity(0.3))
.frame(width: UIScreen.main.bounds.size.width*0.7 , height: UIScreen.main.bounds.size.height/5)
.overlay(Rectangle().stroke(Color.white, lineWidth: 3))
}
.clipShape(
CropFrame(isActive: clipped)
)
.gesture(DragGesture()
.onChanged { value in
currentPosition = CGSize(width: value.translation.width + newPosition.width, height: value.translation.height + newPosition.height)
}
.onEnded { value in
currentPosition = CGSize(width: value.translation.width + newPosition.width, height: value.translation.height + newPosition.height)
newPosition = currentPosition
})
Button (action : { clipped.toggle() }) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
}
This is the full code of a view with image cropping.
else you may use a library from GitHub, see the GitHub demo here
I'm trying to make a view draggable and/or zoomable only within its clipping container view (otherwise it can run into and conflict with other views' gestures), but nothing I've tried so far keeps the gesture from extending outside the visible boundary of the container.
Here's a simplified demo of the behavior I don't want...
When the red Rectangle goes partially outside the green VStack area (clipped), it responds to drag gestures beyond the green area:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var position: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition: CGPoint = CGPoint(x: 100, y: 150)
var body: some View {
let drag = DragGesture()
.onChanged {
self.position = CGPoint(x: $0.translation.width + self.lastPosition.x, y: $0.translation.height + self.lastPosition.y)
}
.onEnded {_ in
self.lastPosition = self.position
}
return VStack {
Rectangle().foregroundColor(.red)
.frame(width: 150, height: 150)
.position(self.position)
.gesture(drag)
.clipped()
}
.background(Color.green)
.frame(width: 200, height: 300)
}
}
PlaygroundPage.current.setLiveView(ContentView())
How would you limit this gesture to only work inside the container (green area in the example above)?
UPDATE:
#Asperi's solution to the above works well, but when I add a second draggable container next to the one above, I get a "dead area" in the first container inside which I can't drag (it appears to be where the second square would cover the first one if it were not clipped). The problem only happens to the original/left side, not to the new one. I think that has to do with it having higher priority since it is drawn second.
Here's an illustration of the new issue:
And here's the updated code:
struct ContentView: View {
#State var position1: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition1: CGPoint = CGPoint(x: 100, y: 150)
let dragArea1: CGRect = CGRect(x: 0, y: 0, width: 200, height: 300)
#State var position2: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition2: CGPoint = CGPoint(x: 100, y: 150)
let dragArea2: CGRect = CGRect(x: 0, y: 0, width: 200, height: 300)
var body: some View {
let drag1 = DragGesture(coordinateSpace: .named("dragArea1"))
.onChanged {
guard self.dragArea1.contains($0.startLocation) else { return }
self.position1 = CGPoint(x: $0.translation.width + self.lastPosition1.x, y: $0.translation.height + self.lastPosition1.y)
}
.onEnded {_ in
self.lastPosition1 = self.position1
}
let drag2 = DragGesture(coordinateSpace: .named("dragArea2"))
.onChanged {
guard self.dragArea2.contains($0.startLocation) else { return }
self.position2 = CGPoint(x: $0.translation.width + self.lastPosition2.x, y: $0.translation.height + self.lastPosition2.y)
}
.onEnded {_ in
self.lastPosition2 = self.position2
}
return HStack {
VStack {
Rectangle().foregroundColor(.red)
.frame(width: 150, height: 150)
.position(self.position1)
.gesture(drag1)
.clipped()
}
.background(Color.green)
.frame(width: dragArea1.width, height: dragArea1.height)
VStack {
Rectangle().foregroundColor(.blue)
.frame(width: 150, height: 150)
.position(self.position2)
.gesture(drag2)
.clipped()
}
.background(Color.yellow)
.frame(width: dragArea2.width, height: dragArea2.height)
}
}
}
Any ideas of how to keep dragging disabled outside any containers, as already achieved, but also allow dragging within the full bounds of each container regardless of what happens with others?
Here is possible solution. The idea is to have drag coordinates in container coordinate space and ignore drag if start location is out of that named area.
Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
#State var position: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition: CGPoint = CGPoint(x: 100, y: 150)
var body: some View {
let area = CGRect(x: 0, y: 0, width: 200, height: 300)
let drag = DragGesture(coordinateSpace: .named("area"))
.onChanged {
guard area.contains($0.startLocation) else { return }
self.position = CGPoint(x: $0.translation.width + self.lastPosition.x, y: $0.translation.height + self.lastPosition.y)
}
.onEnded {_ in
self.lastPosition = self.position
}
return VStack {
Rectangle().foregroundColor(.red)
.frame(width: 150, height: 150)
.position(self.position)
.gesture(drag)
.clipped()
}
.background(Color.green)
.frame(width: area.size.width, height: area.size.height)
.coordinateSpace(name: "area")
}
}
Two days I was looking for a solution to a similar problem
#Asperi's solution helps, but it is not universal for 3 or more figures
My solution: I adding
.contentShape(Rectangle())
before
.gesture(DragGesture().onChanged {
this article helped me.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape
I hope it will be useful to someone.
Sample code:
var body: some View {
VStack {
Image("my image")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(200, 200)
.clipShape(Rectangle())
.contentShape(Rectangle()) // <== this code helped me
.gesture(
DragGesture()
.onChanged {
//
}
.onEnded {_ in
//
}
)
}
}
For the example above, the code could be like this:
struct ContentView: View {
#State var position1: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition1: CGPoint = CGPoint(x: 100, y: 150)
let dragArea1: CGSize = CGSize(width: 200, height: 300)
#State var position2: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition2: CGPoint = CGPoint(x: 100, y: 150)
let dragArea2: CGSize = CGSize(width: 200, height: 300)
var body: some View {
let drag = DragGesture()
.onChanged {
if $0.startLocation.x <= self.dragArea1.width {
self.position1 = CGPoint(x: $0.translation.width + self.lastPosition1.x, y: $0.translation.height + self.lastPosition1.y)
} else {
self.position2 = CGPoint(x: $0.translation.width + self.lastPosition2.x, y: $0.translation.height + self.lastPosition2.y)
}
}
.onEnded {_ in
self.lastPosition1 = self.position1
self.lastPosition2 = self.position2
}
return HStack {
VStack {
Rectangle().foregroundColor(.red)
.frame(width: 150, height: 150)
.position(self.position1)
.clipped()
}
.background(Color.green)
.frame(width: dragArea1.width, height: dragArea1.height)
VStack {
Rectangle().foregroundColor(.blue)
.frame(width: 150, height: 150)
.position(self.position2)
.clipped()
}
.background(Color.yellow)
.frame(width: dragArea2.width, height: dragArea2.height)
}
.clipShape(Rectangle()) //<=== This
.contentShape(Rectangle()) //<=== and this
.gesture(drag)
} }
For the 2nd part (i.e. update to the original question), here's what I ended up with. Basically, I combined the two separate drag gestures into one gesture that covers the whole HStack, and then directed the gesture to the appropriate #State variable depending on where in the HStack it started.
Demo of the Result:
Code:
struct ContentView: View {
#State var position1: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition1: CGPoint = CGPoint(x: 100, y: 150)
let dragArea1: CGSize = CGSize(width: 200, height: 300)
#State var position2: CGPoint = CGPoint(x: 100, y: 150)
#State var lastPosition2: CGPoint = CGPoint(x: 100, y: 150)
let dragArea2: CGSize = CGSize(width: 200, height: 300)
var body: some View {
let drag = DragGesture()
.onChanged {
guard $0.startLocation.y >= 0 && $0.startLocation.y <= self.dragArea1.height else { return }
if $0.startLocation.x <= self.dragArea1.width {
self.position1 = CGPoint(x: $0.translation.width + self.lastPosition1.x, y: $0.translation.height + self.lastPosition1.y)
} else {
self.position2 = CGPoint(x: $0.translation.width + self.lastPosition2.x, y: $0.translation.height + self.lastPosition2.y)
}
}
.onEnded {_ in
self.lastPosition1 = self.position1
self.lastPosition2 = self.position2
}
return HStack {
VStack {
Rectangle().foregroundColor(.red)
.frame(width: 150, height: 150)
.position(self.position1)
.clipped()
}
.background(Color.green)
.frame(width: dragArea1.width, height: dragArea1.height)
VStack {
Rectangle().foregroundColor(.blue)
.frame(width: 150, height: 150)
.position(self.position2)
.clipped()
}
.background(Color.yellow)
.frame(width: dragArea2.width, height: dragArea2.height)
}
.gesture(drag)
}
}
Notes:
As it is now, the gesture works anywhere in each container (i.e. green and yellow areas), even if you don't drag inside the red or blue square.
This could probably be more versatile and/or give a bit more control if you put the whole gesture code into the view and wrapped it in a GeometryReader so you could reference the local bounds in context inside ".onChanged".
Below is the code I wrote for dragging a rectangle. I would like to restrict its dragging to be within the border of the view given. ie if the user tries to move the rectangle outside the border of the view, it should stay within the border edges. I tried using GeometryReader to get the rect of both the rectangle and the outer view, but I am unable to restrict the drag. Please help.
import SwiftUI
struct DragView: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var parentRect: CGRect = .zero
#State private var childRect: CGRect = .zero
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100, alignment: .top)
.foregroundColor(.blue)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.background(GeometryGetter(rect: $childRect))
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.newPosition = self.currentPosition
}
)
}
.frame(width: 300, height: 500, alignment: .center)
.border(Color.black, width: 1)
.background(GeometryGetter(rect: $parentRect))
}
}
struct GeometryGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { geometry in
Group { () -> AnyView in
DispatchQueue.main.async {
self.rect = geometry.frame(in: .global)
}
return AnyView(Color.clear)
}
}
}
}
check this out:
struct ContentView: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var parentRect: CGRect = .zero
#State private var childRect: CGRect = .zero
func correctPostion() {
print(self.currentPosition)
if self.currentPosition.width > 100 {
self.currentPosition.width = 100
}
if self.currentPosition.height > 200 {
self.currentPosition.height = 200
}
if self.currentPosition.width < -100 {
self.currentPosition.width = -100
}
if self.currentPosition.height < -200 {
self.currentPosition.height = -200
}
}
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100, alignment: .top)
.foregroundColor(.blue)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.background(GeometryGetter(rect: $childRect))
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.correctPostion()
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.correctPostion()
self.newPosition = self.currentPosition
}
)
}
.frame(width: 300, height: 500, alignment: .center)
.border(Color.black, width: 1)
.background(GeometryGetter(rect: $parentRect))
}
}
struct GeometryGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { geometry in
Group { () -> AnyView in
DispatchQueue.main.async {
self.rect = geometry.frame(in: .global)
}
return AnyView(Color.clear)
}
}
}
}
So I've got the following code:
import SwiftUI
struct ContentView : View {
#State private var draggingLocation = CGPoint.zero
#State private var startLocation = CGPoint.zero
#State private var dragging = false
var body: some View {
let GR = DragGesture(minimumDistance: 10, coordinateSpace: .global)
.onEnded { value in
self.dragging = false
self.draggingLocation = CGPoint.zero
self.startLocation = CGPoint.zero
}
.onChanged { value in
if !self.dragging {
self.dragging = true
}
if self.startLocation == CGPoint.zero {
self.startLocation = value.startLocation
}
self.draggingLocation = value.location
}
return ZStack {
if self.dragging {
Path { path in
path.move(to: CGPoint(x: self.startLocation.x-5, y: self.startLocation.y-5))
path.addLine(to: CGPoint(x: self.draggingLocation.x-5, y: self.draggingLocation.y+5))
path.addLine(to: CGPoint(x: self.draggingLocation.x+5, y: self.draggingLocation.y-5))
path.addLine(to: CGPoint(x: self.startLocation.x+5, y: self.startLocation.y+5))
}
.fill(Color.black)
}
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.gesture(GR)
.offset(
x: 75,
y: 75
)
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.gesture(GR)
.offset(
x: -75,
y: -75
)
}
.frame(width: 400, height: 400)
.background(Color.gray)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Which results in this behavior:
I'd like to be able to drag the edge out from one circle and into the other, the problem of course is that the coordinate space of the Path is relative to the gray box (ContentView) and not global. A Path has a property coordinateSpace in the documentation but there's very little information how to use it, and googling the term with SwiftUI literally returns three results, all of which are really just links to Apple's currently sparse docs. Anyone have an idea on how to best approach this?
Coordinate spaces come in three flavours: .local, .global and .named. The first two are obvious. The third, named coordinate spaces, are extremely useful in cases like yours. They are also useful in combination with GeometryReader. For more details on that, check https://swiftui-lab.com/geometryreader-to-the-rescue/
Named coordinate spaces let you express a coordinate of one view, in the coordinate space of another. For that, SwiftUI let you specify a name for a view's coordinate space. Then, in other places of your code, you can make a reference of it. In your example, naming the coordinate of your ZStack is the way to go.
Here's the refactored code:
Note: I moved the Path below, only so that it draws in front of the circles. And also, pay attention to the first Path, which is only there to prevent what I think is a bug in ZStack.
import SwiftUI
struct ContentView : View {
#State private var draggingLocation = CGPoint.zero
#State private var startLocation = CGPoint.zero
#State private var dragging = false
var body: some View {
let GR = DragGesture(minimumDistance: 10, coordinateSpace: .named("myCoordinateSpace"))
.onEnded { value in
self.dragging = false
self.draggingLocation = CGPoint.zero
self.startLocation = CGPoint.zero
}
.onChanged { value in
if !self.dragging {
self.dragging = true
}
if self.startLocation == CGPoint.zero {
self.startLocation = value.startLocation
}
self.draggingLocation = value.location
}
return ZStack(alignment: .topLeading) {
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.overlay(Text("Circle 1"))
.gesture(GR)
.offset(x: 75, y: 75)
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.overlay(Text("Circle 2"))
.gesture(GR)
.offset(x: 200, y: 200)
if self.dragging {
Path { path in
path.move(to: CGPoint(x: self.startLocation.x-5, y: self.startLocation.y-5))
path.addLine(to: CGPoint(x: self.draggingLocation.x-5, y: self.draggingLocation.y+5))
path.addLine(to: CGPoint(x: self.draggingLocation.x+5, y: self.draggingLocation.y-5))
path.addLine(to: CGPoint(x: self.startLocation.x+5, y: self.startLocation.y+5))
}
.fill(Color.black)
}
}
.coordinateSpace(name: "myCoordinateSpace")
.frame(width: 400, height: 400, alignment: .topLeading)
.background(Color.gray)
}
}