aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Package.swift4
-rw-r--r--Sources/SwiftyCrop/Models/CropViewModel.swift58
-rw-r--r--Sources/SwiftyCrop/SwiftyCrop.swift7
-rw-r--r--Sources/SwiftyCrop/View/CropView.swift38
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()
}