diff options
-rw-r--r-- | Package.swift | 4 | ||||
-rw-r--r-- | Sources/SwiftyCrop/Models/CropViewModel.swift | 58 | ||||
-rw-r--r-- | Sources/SwiftyCrop/SwiftyCrop.swift | 7 | ||||
-rw-r--r-- | Sources/SwiftyCrop/View/CropView.swift | 38 |
4 files changed, 61 insertions, 46 deletions
diff --git a/Package.swift b/Package.swift index ca79f2e..546b8e4 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "SwiftyCrop", - targets: ["SwiftyCrop"]), + targets: ["SwiftyCrop"]) ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -20,6 +20,6 @@ let package = Package( name: "SwiftyCrop"), .testTarget( name: "SwiftyCropTests", - dependencies: ["SwiftyCrop"]), + dependencies: ["SwiftyCrop"]) ] ) diff --git a/Sources/SwiftyCrop/Models/CropViewModel.swift b/Sources/SwiftyCrop/Models/CropViewModel.swift index 761478a..d470d29 100644 --- a/Sources/SwiftyCrop/Models/CropViewModel.swift +++ b/Sources/SwiftyCrop/Models/CropViewModel.swift @@ -5,13 +5,13 @@ class CropViewModel: ObservableObject { private let maxMagnificationScale: CGFloat var imageSizeInView: CGSize = .zero var maskRadius: CGFloat - + @Published var scale: CGFloat = 1.0 @Published var lastScale: CGFloat = 1.0 @Published var offset: CGSize = .zero @Published var lastOffset: CGSize = .zero @Published var circleSize: CGSize = .zero - + init( maskRadius: CGFloat, maxMagnificationScale: CGFloat @@ -19,7 +19,7 @@ class CropViewModel: ObservableObject { self.maskRadius = maskRadius self.maxMagnificationScale = maxMagnificationScale } - + /** Calculates the max points that the image can be dragged to. - Returns: A CGPoint representing the maximum points to which the image can be dragged. @@ -29,16 +29,19 @@ class CropViewModel: ObservableObject { let xLimit = ((imageSizeInView.width / 2) * scale) - maskRadius return CGPoint(x: xLimit, y: yLimit) } - + /** - Calculates the maximum magnification values that are applied when zooming the image, so that the image can not be zoomed out of its own size. - - Returns: A tuple (CGFloat, CGFloat) representing the minimum and maximum magnification scale values. The first value is the minimum scale at which the image can be displayed without being smaller than its own size. The second value is the preset maximum magnification scale. + Calculates the maximum magnification values that are applied when zooming the image, + so that the image can not be zoomed out of its own size. + - Returns: A tuple (CGFloat, CGFloat) representing the minimum and maximum magnification scale values. + The first value is the minimum scale at which the image can be displayed without being smaller than its own size. + The second value is the preset maximum magnification scale. */ func calculateMagnificationGestureMaxValues() -> (CGFloat, CGFloat) { let minScale = (maskRadius * 2) / min(imageSizeInView.width, imageSizeInView.height) return (minScale, maxMagnificationScale) } - + /** Crops the image to the part that is dragged/zoomed inside the view. Cropped image will be a square. - Parameters: @@ -49,17 +52,17 @@ class CropViewModel: ObservableObject { guard let orientedImage = image.correctlyOriented else { return nil } - + let cropRect = calculateCropRect(orientedImage) - + guard let cgImage = orientedImage.cgImage, let result = cgImage.cropping(to: cropRect) else { return nil } - + return UIImage(cgImage: result) } - + /** Crops the image to the part that is dragged/zoomed inside the view. Cropped image will be a circle. - Parameters: @@ -70,29 +73,29 @@ class CropViewModel: ObservableObject { guard let orientedImage = image.correctlyOriented else { return nil } - + let cropRect = calculateCropRect(orientedImage) - + // A circular crop results in some transparency in the // cropped image, so set opaque to false to ensure the // cropped image does not include a background fill let imageRendererFormat = orientedImage.imageRendererFormat imageRendererFormat.opaque = false - + // UIGraphicsImageRenderer().image provides a block // interface to draw into in a new UIImage let circleCroppedImage = UIGraphicsImageRenderer( // The cropRect.size is the size of // the resulting circleCroppedImage size: cropRect.size, - format: imageRendererFormat).image { context in - + format: imageRendererFormat).image { _ in + // The drawRect is the cropRect starting at (0,0) let drawRect = CGRect( origin: .zero, size: cropRect.size ) - + // addClip on a UIBezierPath will clip all contents // outside of the UIBezierPath drawn after addClip // is called, in this case, drawRect is a circle so @@ -125,8 +128,11 @@ class CropViewModel: ObservableObject { - Returns: A CGRect representing the rectangle to crop. */ private func calculateCropRect(_ orientedImage: UIImage) -> CGRect { - // The relation factor of the originals image width/height and the width/height of the image displayed in the view (initial) - let factor = min((orientedImage.size.width / imageSizeInView.width), (orientedImage.size.height / imageSizeInView.height)) + // The relation factor of the originals image width/height + // and the width/height of the image displayed in the view (initial) + let factor = min( + (orientedImage.size.width / imageSizeInView.width), (orientedImage.size.height / imageSizeInView.height) + ) let centerInOriginalImage = CGPoint(x: orientedImage.size.width / 2, y: orientedImage.size.height / 2) // Calculate the crop radius inside the original image which based on the mask radius let cropRadiusInOriginalImage = (maskRadius * factor) / scale @@ -139,33 +145,35 @@ class CropViewModel: ObservableObject { // Calculates the y coordinate of the crop rectangle inside the original image let cropRectY = (centerInOriginalImage.y - cropRadiusInOriginalImage) - (offsetY / scale) let cropRectCoordinate = CGPoint(x: cropRectX, y: cropRectY) - // Cropped rects dimension is twice its radius (diameter), since it's always a square it's used both for width and height + // Cropped rects dimension is twice its radius (diameter), + // since it's always a square it's used both for width and height let cropRectDimension = cropRadiusInOriginalImage * 2 - + let cropRect = CGRect( x: cropRectCoordinate.x, y: cropRectCoordinate.y, width: cropRectDimension, height: cropRectDimension ) - + return cropRect } } private extension UIImage { /** - A UIImage instance with corrected orientation. If the instance's orientation is already `.up`, it simply returns the original. + A UIImage instance with corrected orientation. + If the instance's orientation is already `.up`, it simply returns the original. - Returns: An optional UIImage that represents the correctly oriented image. */ var correctlyOriented: UIImage? { if imageOrientation == .up { return self } - + UIGraphicsBeginImageContextWithOptions(size, false, scale) draw(in: CGRect(origin: .zero, size: size)) let normalizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - + return normalizedImage } } diff --git a/Sources/SwiftyCrop/SwiftyCrop.swift b/Sources/SwiftyCrop/SwiftyCrop.swift index 0de933d..22268a9 100644 --- a/Sources/SwiftyCrop/SwiftyCrop.swift +++ b/Sources/SwiftyCrop/SwiftyCrop.swift @@ -8,13 +8,14 @@ import SwiftUI /// - imageToCrop: The image to be cropped. /// - maskShape: The shape of the mask used for cropping. /// - configuration: The configuration for the cropping behavior. If nothing is specified, the default is used. -/// - onComplete: A closure that's called when the cropping is complete. This closure returns the cropped `UIImage?`. If an error occurs the return value is nil. +/// - onComplete: A closure that's called when the cropping is complete. This closure returns the cropped `UIImage?`. +/// If an error occurs the return value is nil. public struct SwiftyCropView: View { private let imageToCrop: UIImage private let maskShape: MaskShape private let configuration: SwiftyCropConfiguration private let onComplete: (UIImage?) -> Void - + public init( imageToCrop: UIImage, maskShape: MaskShape, @@ -26,7 +27,7 @@ public struct SwiftyCropView: View { self.configuration = configuration self.onComplete = onComplete } - + public var body: some View { CropView( image: imageToCrop, diff --git a/Sources/SwiftyCrop/View/CropView.swift b/Sources/SwiftyCrop/View/CropView.swift index d593c5d..389047e 100644 --- a/Sources/SwiftyCrop/View/CropView.swift +++ b/Sources/SwiftyCrop/View/CropView.swift @@ -3,13 +3,13 @@ import SwiftUI struct CropView: View { @Environment(\.dismiss) private var dismiss @StateObject private var viewModel: CropViewModel - + private let image: UIImage private let maskShape: MaskShape private let configuration: SwiftyCropConfiguration private let onComplete: (UIImage?) -> Void private let localizableTableName: String - + init( image: UIImage, maskShape: MaskShape, @@ -28,7 +28,7 @@ struct CropView: View { ) localizableTableName = "Localizable" } - + var body: some View { VStack { Text("interaction_instructions", tableName: localizableTableName, bundle: .module) @@ -36,7 +36,7 @@ struct CropView: View { .foregroundColor(.white) .padding(.top, 30) .zIndex(1) - + ZStack { Image(uiImage: image) .resizable() @@ -52,7 +52,7 @@ struct CropView: View { } } ) - + Image(uiImage: image) .resizable() .scaledToFit() @@ -69,10 +69,10 @@ struct CropView: View { .onChanged { value in let sensitivity: CGFloat = 0.2 let scaledValue = (value.magnitude - 1) * sensitivity + 1 - + let maxScaleValues = viewModel.calculateMagnificationGestureMaxValues() viewModel.scale = min(max(scaledValue * viewModel.scale, maxScaleValues.0), maxScaleValues.1) - + let maxOffsetPoint = viewModel.calculateDragGestureMax() let newX = min(max(viewModel.lastOffset.width, -maxOffsetPoint.x), maxOffsetPoint.x) let newY = min(max(viewModel.lastOffset.height, -maxOffsetPoint.y), maxOffsetPoint.y) @@ -86,8 +86,14 @@ struct CropView: View { with: DragGesture() .onChanged { value in let maxOffsetPoint = viewModel.calculateDragGestureMax() - let newX = min(max(value.translation.width + viewModel.lastOffset.width, -maxOffsetPoint.x), maxOffsetPoint.x) - let newY = min(max(value.translation.height + viewModel.lastOffset.height, -maxOffsetPoint.y), maxOffsetPoint.y) + let newX = min( + max(value.translation.width + viewModel.lastOffset.width, -maxOffsetPoint.x), + maxOffsetPoint.x + ) + let newY = min( + max(value.translation.height + viewModel.lastOffset.height, -maxOffsetPoint.y), + maxOffsetPoint.y + ) viewModel.offset = CGSize(width: newX, height: newY) } .onEnded { _ in @@ -95,7 +101,7 @@ struct CropView: View { } ) ) - + HStack { Button { dismiss() @@ -103,9 +109,9 @@ struct CropView: View { Text("cancel_button", tableName: localizableTableName, bundle: .module) } .foregroundColor(.white) - + Spacer() - + Button { onComplete(cropImage()) dismiss() @@ -119,7 +125,7 @@ struct CropView: View { } .background(.black) } - + private func cropImage() -> UIImage? { if maskShape == .circle && configuration.cropImageCircular { viewModel.cropToCircle(image) @@ -127,16 +133,16 @@ struct CropView: View { viewModel.cropToSquare(image) } } - + private struct MaskShapeView: View { let maskShape: MaskShape - + var body: some View { Group { switch maskShape { case .circle: Circle() - + case .square: Rectangle() } |