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)
}
}
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'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
}
)
}
}
}
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)
}
}
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)
}
}
}
}
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
}
}
}
}