iosswiftswiftui

Best way to handle mask and image


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)

enter image description here enter image description here enter image description here

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.

enter image description here enter image description here enter image description here

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

enter image description here


Solution

  • 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:

    1. Add a reference size for normalization
    let refSizeForOffset: CGFloat = 1000
    
    1. In the .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
    }
    
    1. Add a computed property for computing the scaled offset
    private var scaledOffset: CGSize {
        CGSize(
            width: offset.width * photoSize.width / refSizeForOffset,
            height: offset.height * photoSize.height / refSizeForOffset
        )
    }
    
    1. Apply the scaled offset to the image
    Image(uiImage: selectedPhoto)
        // ... other modifiers as before
        .offset(scaledOffset) // πŸ‘ˆ scaledOffset replaces offset from before
        .mask {
            // ...
        }