Skip to content

Commit

Permalink
Merge pull request #2 from leoz/master
Browse files Browse the repository at this point in the history
Make SwiftLint happy
  • Loading branch information
benedom authored Jan 24, 2024
2 parents 5a5a745 + 2935379 commit 2d06f26
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 46 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,6 +20,6 @@ let package = Package(
name: "SwiftyCrop"),
.testTarget(
name: "SwiftyCropTests",
dependencies: ["SwiftyCrop"]),
dependencies: ["SwiftyCrop"])
]
)
58 changes: 33 additions & 25 deletions Sources/SwiftyCrop/Models/CropViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ 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
) {
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.
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
7 changes: 4 additions & 3 deletions Sources/SwiftyCrop/SwiftyCrop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,7 +27,7 @@ public struct SwiftyCropView: View {
self.configuration = configuration
self.onComplete = onComplete
}

public var body: some View {
CropView(
image: imageToCrop,
Expand Down
38 changes: 22 additions & 16 deletions Sources/SwiftyCrop/View/CropView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,15 +28,15 @@ struct CropView: View {
)
localizableTableName = "Localizable"
}

var body: some View {
VStack {
Text("interaction_instructions", tableName: localizableTableName, bundle: .module)
.font(.system(size: 16, weight: .regular))
.foregroundColor(.white)
.padding(.top, 30)
.zIndex(1)

ZStack {
Image(uiImage: image)
.resizable()
Expand All @@ -52,7 +52,7 @@ struct CropView: View {
}
}
)

Image(uiImage: image)
.resizable()
.scaledToFit()
Expand All @@ -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)
Expand All @@ -86,26 +86,32 @@ 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
viewModel.lastOffset = viewModel.offset
}
)
)

HStack {
Button {
dismiss()
} label: {
Text("cancel_button", tableName: localizableTableName, bundle: .module)
}
.foregroundColor(.white)

Spacer()

Button {
onComplete(cropImage())
dismiss()
Expand All @@ -119,24 +125,24 @@ struct CropView: View {
}
.background(.black)
}

private func cropImage() -> UIImage? {
if maskShape == .circle && configuration.cropImageCircular {
viewModel.cropToCircle(image)
} else {
viewModel.cropToSquare(image)
}
}

private struct MaskShapeView: View {
let maskShape: MaskShape

var body: some View {
Group {
switch maskShape {
case .circle:
Circle()

case .square:
Rectangle()
}
Expand Down

0 comments on commit 2d06f26

Please sign in to comment.