The main reason is to apply magnify gesture to pinched part of the image. By default it works perfectly but always zoom in/out center of given image. How to manipulate it to zoom in actual part of the image? If I magnify left top corner of the image it should stay in the same place that part of the image I want to zoom in.
Where is the issue? While you pinch it, it scales the image incorrectly. It seem that offset is calculated correctly, but have no idea why it doesn’t work.
Dragging works perfectly, while zooming NOT.
To run the code you just need to deliver any UIImage. That is all.
Here is my code:
@available(iOS 17.0, *)
struct WrapperForCropView: View {
var image: UIImage //here you need to add any image from any source.
var body: some View {
ZStack {
Color.red
GeometryReader { proxy in
VStack {
DemoCroppingView(uiImage: image, bounds: proxy.size)
}
.frame(width: proxy.size.width, height: proxy.size.height)
.clipped()
}
}
}
}
@available(iOS 17.0, *)
struct DemoCroppingView: View {
var uiImage: UIImage
var bounds: CGSize
@State private var offsetLimit: CGSize = .zero
@State private var offset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
@State private var scale: CGFloat = 1
@State private var lastScale: CGFloat = 1
@State private var imageViewSize: CGSize = .zero
private let mask = CGSize(width: 300, height: 300)
private var dragGesture: some Gesture {
DragGesture()
.onChanged { gesture in
offsetLimit = getOffsetLimit()
let width = min(
max(-offsetLimit.width, lastOffset.width + gesture.translation.width),
offsetLimit.width
)
let height = min(
max(-offsetLimit.height, lastOffset.height + gesture.translation.height),
offsetLimit.height
)
offset = CGSize(width: width, height: height)
}
.onEnded { _ in
lastOffset = offset
}
}
private var scaleGesture: some Gesture {
MagnifyGesture()
.onChanged { gesture in
let startX = gesture.startLocation.x
let startY = gesture.startLocation.y
let currentScale = lastScale * gesture.magnification
scale = currentScale
let offsetWidth = (startX - (startX - lastOffset.width) * scale)
let offsetHeight = (startY - (startY - lastOffset.height) * scale)
offset = CGSize(width: offsetWidth, height: offsetHeight)
}
.onEnded { _ in
lastScale = scale
lastOffset = offset
}
}
var body: some View {
ZStack(alignment: .center) {
ZStack {
Rectangle()
.fill(.black)
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.scaleEffect(scale)
.offset(offset)
}
.overlay {
Color.black.opacity(0.5)
}
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.scaleEffect(scale)
.offset(offset)
.mask(
Circle()
.frame(width: mask.width, height: mask.height)
)
.overlay {
Circle()
.stroke(Color.red, lineWidth: 1)
.frame(width: mask.width, height: mask.height)
}
}
.simultaneousGesture(dragGesture)
.simultaneousGesture(scaleGesture)
.clipped()
.onChange(of: bounds) { _, _ in
calculateImageViewSize()
}
.onChange(of: uiImage) { _, _ in
calculateImageViewSize()
lastOffset = .zero
offset = .zero
offsetLimit = .zero
}
}
private func calculateImageViewSize() {
let viewRatio = bounds.width / bounds.height
let width = uiImage.size.width
let height = uiImage.size.height
let imageRatio = width / height
let factor = viewRatio < imageRatio ? bounds.height / height : bounds.width / width
imageViewSize.height = height * factor
imageViewSize.width = width * factor
}
private func getOffsetLimit() -> CGSize {
var offsetLimit: CGSize = .zero
offsetLimit.width = ((imageViewSize.width * scale) - mask.width) / 2
offsetLimit.height = ((imageViewSize.height * scale) - mask.height) / 2
return offsetLimit
}
}