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
Related
I have created a slider view which consists of 2 images, a chevron pointer and a slider line. My ultimate goal is to offset the button in the opposite direction to the chevron image so that when the user clicks the empty space the button will slide to its location. Below is a gif of me attempting to achieve this. My approach is quite manual and I suspect there is a sophisticated way of achieving my goal. I have it working when I click the button once, I suspect that on the other side I have my numbers wrong so it doesn't work when the image is on the left side of the screen.
import SwiftUI
extension Animation {
static func smooth() -> Animation {
Animation.spring()
.speed(0.7)
}
}
struct SliderView: View {
//this works as a toggle you can implement other booleans with this
#State var partyType: Bool = false
#State private var isOn = false
#State var width: CGFloat = 0
#State var height: CGFloat = 0
let screen = UIScreen.main.bounds
var body: some View {
ZStack {
Toggle("",isOn: $isOn).toggleStyle(SliderToggle())
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
func hostParty() -> Bool {
if partyType {
return true
} else {
return false
}
}
}
struct SlideEffect: GeometryEffect {
var offset: CGSize
var animatableData: CGSize.AnimatableData {
get { CGSize.AnimatableData(offset.width, offset.height) }
set { offset = CGSize(width: newValue.first, height: newValue.second) }
}
public func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(
CGAffineTransform(
translationX: offset.width,
y: offset.height
)
)
}
}
struct SliderView_Previews: PreviewProvider {
static var previews: some View {
SliderView()
}
}
struct SliderToggle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
GeometryReader{ geo in
VStack (alignment: .leading,spacing: 0){
Button(action: {
withAnimation() {
configuration.isOn.toggle()
}
}) {
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
.offset(x: geo.size.width - 100, y: 0)
}
.modifier(
SlideEffect(
offset: CGSize(
width: configuration.isOn ? -geo.size.width + imageWidthX(name: "chevronPointer") : 0,
height: 0.0
)
)
)
Image("sliderLine")
.resizable()
.scaledToFit()
.frame(width: geo.size.width, alignment: .center)
}
}
.frame(height: 40)
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
I worded the question pretty poorly but ultimately the goal I wanted to achieve was to have 2 buttons on a stack that controlled the view under it. When the opposite side is clicked the slider icon will travel to that side and vice versa. The offset made things a lot more complicated than necessary so I removed this code and basically created invisible buttons. IF there is a more elegant way top achieve this I would be truly greatful
HStack {
ZStack {
Button (" ") {
withAnimation(){
configuration.isOn.toggle()
}}
.padding(.trailing).frame(width: 70, height: 25)
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
//.offset(x: geo.size.width - 100, y: 0)
.modifier(SlideEffect(offset: CGSize(width: configuration.isOn ? geo.size.width - imageWidthX(name: "chevronPointer"): 0, height: 0.0)))
}
Spacer()
Button(" "){
withAnimation(){
configuration.isOn.toggle()
}
}.padding(.trailing).frame(width: 70, height: 25)
}
I am learning SwiftUI and as an end goal of the current project, I would like to draw bezier paths, each on its own layer, which could be modified on interaction (e.g. change color on tap) and also be able to resize the whole group on magnification gesture.
For testing purpose, I wanted to create a grid of rounded squares (which are going to be replaced by arbitrary bezier path in the future) and display the grid on the screen.
The code is the following:
import SwiftUI
struct PathView: View {
#State var scale: CGFloat = 1.0
var body: some View {
let overlayContent = VStack(alignment: .leading) {
Text("Hello")
}
.padding()
GeometryReader {
geometry in
let rectangles: [GridItem] = getSquareGrid(width: geometry.size.width, height: geometry.size.height, count: 10)
var rects = ZStack {
ForEach(rectangles) { rectangle in
RoundedRectangle(cornerRadius: rectangle.cornerRadius, style: /*#START_MENU_TOKEN#*/.continuous/*#END_MENU_TOKEN#*/)
.fill(rectangle.color)
.frame(width: rectangle.size, height: rectangle.size, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.offset(x: rectangle.offsetX, y: rectangle.offsetY)
.onTapGesture {
print("rectangle \(rectangle.id) tapped")
}
}
}.scaledToFit()
.scaleEffect(scale)
.gesture(MagnificationGesture()
.onChanged({ (scale) in
self.scale = scale.magnitude
})
.onEnded({ (scaleFinal) in
self.scale = scaleFinal
}))
return rects.drawingGroup()
.overlay(overlayContent, alignment: .topLeading)
}
}
}
struct GridItem: Identifiable {
let size: CGFloat
let offsetX: CGFloat
let offsetY: CGFloat
let cornerRadius: CGFloat
let color: Color
let id = UUID()
init(size: CGFloat, offsetX: CGFloat, offsetY: CGFloat, cornerRadius: CGFloat, color: Color) {
self.size = size
self.offsetX = offsetX
self.offsetY = offsetY
self.cornerRadius = cornerRadius
self.color = color
}
}
func getSquareGrid(width: CGFloat, height: CGFloat, count: Int) -> [GridItem] {
var rectangleArray: [GridItem] = []
let size = min(width, height)
let rectangleSize = round(size/CGFloat(count))
for row in 0...count - 1 {
for column in 0...count - 1 {
let rect = GridItem(size: rectangleSize, offsetX: CGFloat(column) * rectangleSize, offsetY: CGFloat(row) * rectangleSize, cornerRadius: 25.0, color: Color.red)
rectangleArray.append(rect)
}
}
return rectangleArray
}
struct PathView_Previews: PreviewProvider {
static var previews: some View {
PathView()
}
}
This version is unable to build due to the following error:
Unable to infer complex closure return type; add explicit type to
disambiguate
In order to get the available space dimensions, I used GeometryReader and passed its dimensions to the function getSquareGrid().
Does anyone know how to appropriately use GeometryReader in this case or anyhow get the dimensions in order to fill the available screen with generated grid?
The code is not testable due to absent dependencies, but try the following
let drawnRects = rects.drawingGroup()
return drawnRects
.overlay(overlayContent, alignment: .topLeading)
replace with
rects.drawingGroup()
.overlay(overlayContent, alignment: .topLeading)
Update: actually if I replace overlayContent with just Text("test") it complied well.
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
If you have played around with Apple's Room Tutorial (link: ../WWDC2019/204/), I added a small touch-to-zoom gesture (thanks to #Alladinian and brar07), per the code below.
HOWEVER, the image when touched and panned, moves off the screen and does not return to its original position. You should be able to copy+paste this code into Apple's project with little modifications.
REQUIREMENTS: 1) The image should stay within the confines of the image frame, i.e. when zoomed, the edges of the image should not go beyond the edges of the defined frame (or screen if the frame is not defined). 2) The image should return to its original position.
This final result would be similar to how mouse-over works for product images on some websites.
import SwiftUI
struct RoomDetail: View {
let room : Room
#State var scale: CGFloat = 1.0
#State var isTouchingScreen = false
#State var isZoomedIn = false
#State var pointTouchedOnScreen: CGPoint = CGPoint.zero
#State var panSize: CGSize = CGSize.zero
#State var fingerState: String = "Finger is not touching the image"
var body: some View {
ZStack {
// Show the room selected by the user, implement zooming capabilities
GeometryReader { reader in
Image("\(self.room.name)" + "_Thumb")
.resizable()
.offset(x: self.panSize.width, y: self.panSize.height)
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { (value) in
self.fingerState = "Finger is touching the image" // for debug purpose only
self.isZoomedIn = true
self.isTouchingScreen = true
self.pointTouchedOnScreen = value.startLocation
self.scale = 1.1
let offsetWidth = (reader.frame(in: .global).maxX * self.scale - reader.frame(in: .global).maxX) / 2
let newDraggedWidth = self.panSize.width * self.scale
if (newDraggedWidth > offsetWidth) {
self.panSize = CGSize(width: (value.translation.width + self.panSize.width), height: (value.translation.height + self.panSize.height))
} else if (newDraggedWidth < -offsetWidth) {
self.panSize = CGSize(width: (value.translation.width + self.panSize.width), height: (value.translation.height + self.panSize.height))
} else {
self.panSize = CGSize(width: (value.translation.width + self.panSize.width), height: (value.translation.height + self.panSize.height))
}
}
.onEnded { _ in
self.fingerState = "Finger is not touching the image" // for debug purpose only
self.isZoomedIn = false
self.isTouchingScreen = false
})
.aspectRatio(contentMode: self.isZoomedIn ? .fill : .fit)
.scaleEffect(self.isTouchingScreen ? self.scale : 1, anchor: UnitPoint(x: self.pointTouchedOnScreen.x / reader.frame(in: .global).maxX, y: self.pointTouchedOnScreen.y / reader.frame(in: .global).maxY))
.animation(.easeInOut(duration: 1))
.frame(maxWidth: UIScreen.main.bounds.size.width - 50, maxHeight: UIScreen.main.bounds.size.height - 200, alignment: .center)
.clipped()
.offset(x: 0, y: -50)
}
}
}
struct RoomDetail_Previews: PreviewProvider {
static var previews: some View {
RoomDetail(room: testData[0])
}
}
I have no idea if this answers your question exactly, but I wanted to make this note:
The ORDER of modifiers MATTERS
I was trying to do a zoom effect and
it kept exceeding the frame of the image
However, when I set the FRAME AFTER the scale effect, it stayed within the bounds
This didn't work, the image wouldn't "crop" to the size of the frame for the image:
Image(uiImage: self.userData.image!)
.resizable()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.aspectRatio(contentMode: .fill)
.frame(maxWidth:metrics.size.width * 0.60, maxHeight: metrics.size.height * 0.60, alignment: .top)
.scaleEffect(self.scale)
.clipped()
.foregroundColor(.gray)
Whereas this DID work:
Image(uiImage: self.userData.image!)
.resizable()
.scaleEffect(self.scale)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.aspectRatio(contentMode: .fill)
.frame(maxWidth:metrics.size.width * 0.60, maxHeight: metrics.size.height * 0.60, alignment: .top)
.clipped()
.foregroundColor(.gray)
The only difference being applying the scaleEffect before the frame. (In this case, I have a drag gesture and a zoom gesture).
Spent a lot of time trying to sort this out and not sure it exactly solves your issue but perhaps useful to someone.
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
}
}
}
}