I'm working on a crop view... I have a problem..
the first three photos that I show you represent the cropView
in its initial state (so not like this using the offset to move the photo)
as you can see if we take the girl's shoe as a reference point we notice that it is always outside the area of ββthe circular mask even if I use split view for 2/3, 1/2, 1/3 the image fits perfectly
If, however, we look at these other 3 photos in which I used the drag gesture, always taking the reference point, i.e. the girl's shoe, we notice that with each change of the split view it does not always remain in the same position. This certainly changes due to the image measurements but I just can't find a way to understand why it happens.
If I don't use the drag gesture the mask and the photo are perfect in all three first photos but as soon as the offset value changes with the dragGesture I get this difference in the last three photos
Can you help me understand? I hope I was able to explain myself correctly
public struct CropView: PlatformView {
var selectedPhoto: UIImage
let prefersShape: Mask
public init(selectedPhoto: UIImage, prefersShape: Mask = .circle) {
self.selectedPhoto = selectedPhoto
self.prefersShape = prefersShape
}
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
@State private var viewSize: CGSize = .zero
@State private var offset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
private var maskScale: CGFloat {
isCompact ? 0.6 : 0.65
}
@State private var photoSize: CGSize = .zero
private var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
let currentOffset = lastOffset + value.translation
offset = currentOffset
// updateOffsetLimits(currentOffset: currentOffset)
}
.onEnded { _ in lastOffset = offset }
}
public var body: some View {
ZStack {
Image(uiImage: selectedPhoto)
.resizable()
.aspectRatio( contentMode: .fill)
.frame(photoSize)
.offset(offset)
.mask {
Rectangle().opacity(0.5)
.overlay {
prefersShape.shape
.blendMode(.destinationOver)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.gesture(dragGesture)
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newValue in
viewSize = newValue
let isLandscape = newValue.width > newValue.height
let reference = isLandscape ? newValue.height : newValue
.width
photoSize = prefersShape.size(relativeTo: .init(width: reference, height: reference)).scaledBy(maskScale)
}
}
}
extension View {
func frame(_ size: CGSize) -> some View {
frame(width: size.width, height: size.height)
}
}
@MainActor
protocol PlatformView: View {
var horizontalSizeClass: UserInterfaceSizeClass? { get }
var verticalSizeClass: UserInterfaceSizeClass? { get }
var isCompact: Bool { get }
var isRegular: Bool { get }
}
extension PlatformView {
var isCompact: Bool { horizontalSizeClass == .compact || verticalSizeClass == .compact }
var isRegular: Bool { horizontalSizeClass == .regular && verticalSizeClass == .regular }
}
Update for Mask enum and Extensions
public enum Mask {
case circle, square,
var shape: AnyShape {
switch self {
case .circle: return AnyShape(.circle)
case .square, .rectangle : return AnyShape(.rect)
}
}
func size(relativeTo size: CGSize) -> CGSize {
let isLandscape = size.width > size.height
let reference = isLandscape ? size.height : size
.width
switch self {
case .circle, .square :
return .init(width: size.width, height: size.width)
}
}
extension CGSize {
static func + (lhs: CGSize, rhs: CGSize) -> CGSize {
.init(width: lhs.width + rhs.width, height: lhs.height + rhs.height)
}
func scaledBy(_ scale: CGFloat) -> CGSize {
.init(width: self.width * scale, height: self.height * scale)
}
}
Original Image of example
The explanation for why the image is shifted by different amounts when the view size changes is because the drag offset is being stored as an absolute size (= absolute number of points). This size relates to the size of the image at the time that drag was performed. When the image is shown on a smaller screen, the same offset will cause a larger movement of the image and vice versa.
To fix, the drag offset needs to be normalized. Then, when the offset is applied to the image, the normalized offset needs to be scaled in accordance with the current image size.
The following changes can be made to CropView
to get it to work this way:
let refSizeForOffset: CGFloat = 1000
.onChanged
closure, compute and save the normalized offset.onChanged { value in
let normalizedOffset = CGSize(
width: value.translation.width * refSizeForOffset / photoSize.width,
height: value.translation.height * refSizeForOffset / photoSize.height
)
let currentOffset = lastOffset + normalizedOffset
offset = currentOffset
}
private var scaledOffset: CGSize {
CGSize(
width: offset.width * photoSize.width / refSizeForOffset,
height: offset.height * photoSize.height / refSizeForOffset
)
}
Image(uiImage: selectedPhoto)
// ... other modifiers as before
.offset(scaledOffset) // π scaledOffset replaces offset from before
.mask {
// ...
}