diff --git a/Demo/SwiftyCropDemo/ContentView.swift b/Demo/SwiftyCropDemo/ContentView.swift index 08b1060..489d35f 100644 --- a/Demo/SwiftyCropDemo/ContentView.swift +++ b/Demo/SwiftyCropDemo/ContentView.swift @@ -5,6 +5,7 @@ struct ContentView: View { @State private var showImageCropper: Bool = false @State private var selectedImage: UIImage? @State private var selectedShape: MaskShape = .square + @State private var rectAspectRatio: PresetAspectRatios = .fourToThree @State private var cropImageCircular: Bool @State private var rotateImage: Bool @State private var maxMagnificationScale: CGFloat @@ -12,6 +13,21 @@ struct ContentView: View { @State private var zoomSensitivity: CGFloat @FocusState private var textFieldFocused: Bool + enum PresetAspectRatios: String, CaseIterable { + case fourToThree = "4:3" + case sixteenToNine = "16:9" + + func getValue() -> CGFloat { + switch self { + case .fourToThree: + 4/3 + + case .sixteenToNine: + 16/9 + } + } + } + init() { let defaultConfiguration = SwiftyCropConfiguration() _cropImageCircular = State(initialValue: defaultConfiguration.cropImageCircular) @@ -59,7 +75,7 @@ struct ContentView: View { Text("Mask shape") .frame(maxWidth: .infinity, alignment: .leading) - Picker("maskShape", selection: $selectedShape) { + Picker("maskShape", selection: $selectedShape.animation()) { ForEach(MaskShape.allCases, id: \.self) { mask in Text(String(describing: mask)) } @@ -67,6 +83,21 @@ struct ContentView: View { .pickerStyle(.segmented) } + if selectedShape == .rectangle { + HStack { + Text("Rect aspect ratio") + .frame(maxWidth: .infinity, alignment: .leading) + + Picker("rectAspectRatio", selection: $rectAspectRatio) { + ForEach(PresetAspectRatios.allCases, id: \.self) { aspectRatio in + Text(aspectRatio.rawValue) + } + + } + .pickerStyle(.segmented) + } + } + Toggle("Crop image to circle", isOn: $cropImageCircular) Toggle("Rotate image", isOn: $rotateImage) @@ -129,7 +160,8 @@ struct ContentView: View { maskRadius: maskRadius, cropImageCircular: cropImageCircular, rotateImage: rotateImage, - zoomSensitivity: zoomSensitivity + zoomSensitivity: zoomSensitivity, + rectAspectRatio: rectAspectRatio.getValue() ) ) { croppedImage in // Do something with the returned, cropped image diff --git a/README.md b/README.md index c39b59d..d26ac2b 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,9 @@ If you want to display `SwiftyCrop` inside a sheet, use `NavigationView` instead SwiftyCrop supports two different mask shapes for cropping: - `circle` - `square` +- `rectangle` -This is only the shape of the mask the user will see when cropping the image. The resulting, cropped image will always be a square by default. You can override this using a configuration. +This is only the shape of the mask the user will see when cropping the image. The resulting, cropped image will always be a square by default when using `circle` or `square`. To get a circular cropped image, you can override this using a configuration. You can also configure `SwiftyCropView` by passing a `SwiftyCropConfiguration`. A configuration has the following properties: @@ -148,8 +149,9 @@ You can also configure `SwiftyCropView` by passing a `SwiftyCropConfiguration`. | `maxMagnificationScale` | `CGFloat`: The maximum scale factor that the image can be magnified while cropping. Defaults to `4.0`. | | `maskRadius` | `CGFloat`: The radius of the mask used for cropping. Defaults to `130`. A good way is to make it dependend on the screens size. | | `cropImageCircular` | `Bool`: When using the cropping mask `circle`, whether the resulting image should also be masked as circle. Defaults to `false`. | -| `rotateImage` | `Bool`: Whether the image can be rotated when cropping using pinch gestures. Defaults to `true`. | +| `rotateImage` | `Bool`: Whether the image can be rotated when cropping using pinch gestures. Defaults to `false`. | | `zoomSensitivity` | `CGFloat`: Zoom sensitivity when cropping. Increase to make zoom faster / less sensitive. Defaults to `1.0`. | +| `rectAspectRatio` | `CGFloat`: The aspect ratio to use when a rectangular mask shape is used. Defaults to `4:3`. | Create a configuration like this: ```swift @@ -158,7 +160,8 @@ let configuration = SwiftyCropConfiguration( maskRadius: 130, cropImageCircular: false, rotateImage: true, - zoomSensitivity = 1.0 + zoomSensitivity = 1.0, + rectAspectRatio = 4/3 ) ``` and use it like this: @@ -185,6 +188,8 @@ Thanks to [@leoz](https://github.com/leoz) for adding the circular crop mode, th Thanks to [@kevin-hv](https://github.com/kevin-hv) for adding the hungarian localization 🇭🇺 +Thanks to [@Festanny](https://github.com/Festanny) for helping with the recangular cropping functionality 🎉 + ## ✍️ Author Benedikt Betz & CHECK24 diff --git a/Sources/SwiftyCrop/Models/CropViewModel.swift b/Sources/SwiftyCrop/Models/CropViewModel.swift index 869d4f8..687e396 100644 --- a/Sources/SwiftyCrop/Models/CropViewModel.swift +++ b/Sources/SwiftyCrop/Models/CropViewModel.swift @@ -2,207 +2,195 @@ import SwiftUI import UIKit class CropViewModel: ObservableObject { - private let maxMagnificationScale: CGFloat - var imageSizeInView: CGSize = .zero { - didSet { - maskRadius = min(maskRadius, min(imageSizeInView.width, imageSizeInView.height) / 2) - } - } - @Published 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 angle: Angle = Angle(degrees: 0) - @Published var lastAngle: Angle = Angle(degrees: 0) - + private let maskRadius: CGFloat + private let maxMagnificationScale: CGFloat // The maximum allowed scale factor for image magnification. + private let maskShape: MaskShape // The shape of the mask used for cropping. + private let rectAspectRatio: CGFloat // The aspect ratio for rectangular masks. + + var imageSizeInView: CGSize = .zero // The size of the image as displayed in the view. + @Published var maskSize: CGSize = .zero // The size of the mask used for cropping. This is updated based on the mask shape and available space. + @Published var scale: CGFloat = 1.0 // The current scale factor of the image. + @Published var lastScale: CGFloat = 1.0 // The previous scale factor of the image. + @Published var offset: CGSize = .zero // The current offset of the image. + @Published var lastOffset: CGSize = .zero // The previous offset of the image. + @Published var angle: Angle = Angle(degrees: 0) // The current rotation angle of the image. + @Published var lastAngle: Angle = Angle(degrees: 0) // The previous rotation angle of the image. + init( maskRadius: CGFloat, - maxMagnificationScale: CGFloat + maxMagnificationScale: CGFloat, + maskShape: MaskShape, + rectAspectRatio: CGFloat ) { self.maskRadius = maskRadius self.maxMagnificationScale = maxMagnificationScale + self.maskShape = maskShape + self.rectAspectRatio = rectAspectRatio } - + + /** + Updates the mask size based on the given size and mask shape. + - Parameter size: The size to base the mask size calculations on. + */ + private func updateMaskSize(for size: CGSize) { + switch maskShape { + case .circle, .square: + let diameter = min(maskRadius * 2, min(size.width, size.height)) + maskSize = CGSize(width: diameter, height: diameter) + case .rectangle: + let maxWidth = min(size.width, maskRadius * 2) + let maxHeight = min(size.height, maskRadius * 2) + if maxWidth / maxHeight > rectAspectRatio { + maskSize = CGSize(width: maxHeight * rectAspectRatio, height: maxHeight) + } else { + maskSize = CGSize(width: maxWidth, height: maxWidth / rectAspectRatio) + } + } + } + /** - 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. + Updates the mask dimensions based on the size of the image in the view. + - Parameter imageSizeInView: The size of the image as displayed in the view. + */ + func updateMaskDimensions(for imageSizeInView: CGSize) { + self.imageSizeInView = imageSizeInView + updateMaskSize(for: imageSizeInView) + } + + /** + Calculates the maximum allowed offset for dragging the image. + - Returns: A CGPoint representing the maximum x and y offsets. */ func calculateDragGestureMax() -> CGPoint { - let yLimit = ((imageSizeInView.height / 2) * scale) - maskRadius - let xLimit = ((imageSizeInView.width / 2) * scale) - maskRadius + let xLimit = max(0, ((imageSizeInView.width / 2) * scale) - (maskSize.width / 2)) + let yLimit = max(0, ((imageSizeInView.height / 2) * scale) - (maskSize.height / 2)) 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 minimum and maximum allowed scale values for image magnification. + - Returns: A tuple containing the minimum and maximum scale values. */ func calculateMagnificationGestureMaxValues() -> (CGFloat, CGFloat) { - let minScale = (maskRadius * 2) / min(imageSizeInView.width, imageSizeInView.height) + let minScale = max(maskSize.width / imageSizeInView.width, maskSize.height / 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: - - image: The UIImage to crop - - Returns: A cropped UIImage if the cropping operation is successful; otherwise nil. + Crops the given image to a rectangle based on the current mask size and position. + - Parameter image: The UIImage to crop. + - Returns: A cropped UIImage, or nil if cropping fails. */ - func cropToSquare(_ image: UIImage) -> UIImage? { - guard let orientedImage = image.correctlyOriented else { + func cropToRectangle(_ image: UIImage) -> UIImage? { + 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 given image to a square based on the current mask size and position. + - Parameter image: The UIImage to crop. + - Returns: A cropped UIImage, or nil if cropping fails. + */ + func cropToSquare(_ image: UIImage) -> UIImage? { + 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: - - image: The UIImage to crop - - Returns: A cropped UIImage if the cropping operation is successful; otherwise nil. + Crops the given image to a circle based on the current mask size and position. + - Parameter image: The UIImage to crop. + - Returns: A cropped UIImage, or nil if cropping fails. */ func cropToCircle(_ image: UIImage) -> UIImage? { - guard let orientedImage = image.correctlyOriented else { - return nil - } - + 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 { _ 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 - // the UIBezierPath clips drawing to the circle - UIBezierPath(ovalIn: drawRect).addClip() - - // The drawImageRect is offsets the image’s bounds - // such that the circular clip is at the center of - // the image - let drawImageRect = CGRect( - origin: CGPoint( - x: -cropRect.origin.x, - y: -cropRect.origin.y - ), - size: orientedImage.size - ) - - // Draws the orientedImage inside of the - // circular clip - orientedImage.draw(in: drawImageRect) - } - + let drawRect = CGRect(origin: .zero, size: cropRect.size) + UIBezierPath(ovalIn: drawRect).addClip() + let drawImageRect = CGRect( + origin: CGPoint(x: -cropRect.origin.x, y: -cropRect.origin.y), + size: orientedImage.size + ) + orientedImage.draw(in: drawImageRect) + } + return circleCroppedImage } - + /** - Rotates the image to the angle that is rotated inside the view. - - Parameters: - - image: The UIImage to rotate - - angle: The Angle to rotate to - - Returns: A rotated UIImage if the rotating operation is successful; otherwise nil. + Rotates the given image by the specified angle. + - Parameter image: The UIImage to rotate. + - Parameter angle: The Angle to rotate the image by. + - Returns: A rotated UIImage, or nil if rotation fails. */ func rotate(_ image: UIImage, _ angle: Angle) -> UIImage? { - guard let orientedImage = image.correctlyOriented else { - return nil - } - - guard let cgImage = orientedImage.cgImage else { - return nil - } - + guard let orientedImage = image.correctlyOriented, + let cgImage = orientedImage.cgImage else { return nil } + let ciImage = CIImage(cgImage: cgImage) - - // Prepare filter - let filter = CIFilter.straightenFilter( - image: ciImage, - radians: angle.radians - ) - - // Get output image - guard let output = filter?.outputImage else { - return nil - } - - // Create resulting image + + guard let filter = CIFilter.straightenFilter(image: ciImage, radians: angle.radians), + let output = filter.outputImage else { return nil } + let context = CIContext() - guard let result = context.createCGImage( - output, - from: output.extent - ) else { - return nil - } - + guard let result = context.createCGImage(output, from: output.extent) else { return nil } + return UIImage(cgImage: result) } - + /** - Calculates the rectangle to crop. - - Parameters: - - image: The UIImage to calculate the rectangle to crop for - - Returns: A CGRect representing the rectangle to crop. + Calculates the rectangle to use for cropping the image based on the current mask size, scale, and offset. + - Parameter orientedImage: The correctly oriented UIImage to calculate the crop rect for. + - Returns: A CGRect representing the area to crop from the original image. */ 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) + (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 - // The x offset the image has by dragging - let offsetX = offset.width * factor - // The y offset the image has by dragging - let offsetY = offset.height * factor - // Calculates the x coordinate of the crop rectangle inside the original image - let cropRectX = (centerInOriginalImage.x - cropRadiusInOriginalImage) - (offsetX / scale) - // 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 - let cropRectDimension = cropRadiusInOriginalImage * 2 - - let cropRect = CGRect( - x: cropRectCoordinate.x, - y: cropRectCoordinate.y, - width: cropRectDimension, - height: cropRectDimension + let centerInOriginalImage = CGPoint( + x: orientedImage.size.width / 2, + y: orientedImage.size.height / 2 + ) + + let cropSizeInOriginalImage = CGSize( + width: (maskSize.width * factor) / scale, + height: (maskSize.height * factor) / scale + ) + + let offsetX = offset.width * factor / scale + let offsetY = offset.height * factor / scale + + let cropRectX = (centerInOriginalImage.x - cropSizeInOriginalImage.width / 2) - offsetX + let cropRectY = (centerInOriginalImage.y - cropSizeInOriginalImage.height / 2) - offsetY + + return CGRect( + origin: CGPoint(x: cropRectX, y: cropRectY), + size: cropSizeInOriginalImage ) - - return cropRect } } @@ -214,12 +202,12 @@ private extension UIImage { */ 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 } } @@ -228,8 +216,8 @@ private extension CIFilter { /** Creates the straighten filter. - Parameters: - - inputImage: The CIImage to use as an input image - - radians: An angle in radians + - inputImage: The CIImage to use as an input image + - radians: An angle in radians - Returns: A generated CIFilter. */ static func straightenFilter(image: CIImage, radians: Double) -> CIFilter? { diff --git a/Sources/SwiftyCrop/Models/MaskShape.swift b/Sources/SwiftyCrop/Models/MaskShape.swift index 38e5f20..15836b5 100644 --- a/Sources/SwiftyCrop/Models/MaskShape.swift +++ b/Sources/SwiftyCrop/Models/MaskShape.swift @@ -1,3 +1,3 @@ public enum MaskShape: CaseIterable { - case circle, square + case circle, square, rectangle } diff --git a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift index 3a3e06a..587e767 100644 --- a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift +++ b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift @@ -7,6 +7,7 @@ public struct SwiftyCropConfiguration { public let cropImageCircular: Bool public let rotateImage: Bool public let zoomSensitivity: CGFloat + public let rectAspectRatio: CGFloat /// Creates a new instance of `SwiftyCropConfiguration`. /// @@ -18,19 +19,23 @@ public struct SwiftyCropConfiguration { /// - cropImageCircular: Option to enable circular crop. /// Defaults to `false`. /// - rotateImage: Option to rotate image. - /// Defaults to `true`. + /// Defaults to `false`. /// - zoomSensitivity: Sensitivity when zooming. Default is `1.0`. Decrease to increase sensitivity. + /// + /// - rectAspectRatio: The aspect ratio to use when a `.rectangle` mask shape is used. Defaults to `4:3`. public init( maxMagnificationScale: CGFloat = 4.0, maskRadius: CGFloat = 130, cropImageCircular: Bool = false, - rotateImage: Bool = true, - zoomSensitivity: CGFloat = 1 + rotateImage: Bool = false, + zoomSensitivity: CGFloat = 1, + rectAspectRatio: CGFloat = 4/3 ) { self.maxMagnificationScale = maxMagnificationScale self.maskRadius = maskRadius self.cropImageCircular = cropImageCircular self.rotateImage = rotateImage self.zoomSensitivity = zoomSensitivity + self.rectAspectRatio = rectAspectRatio } } diff --git a/Sources/SwiftyCrop/View/CropView.swift b/Sources/SwiftyCrop/View/CropView.swift index b8ca961..c2349f6 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, @@ -23,31 +23,30 @@ struct CropView: View { _viewModel = StateObject( wrappedValue: CropViewModel( maskRadius: configuration.maskRadius, - maxMagnificationScale: configuration.maxMagnificationScale + maxMagnificationScale: configuration.maxMagnificationScale, + maskShape: maskShape, + rectAspectRatio: configuration.rectAspectRatio ) ) localizableTableName = "Localizable" } - + var body: some View { let magnificationGesture = MagnificationGesture() .onChanged { value in let sensitivity: CGFloat = 0.1 * configuration.zoomSensitivity 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) - viewModel.offset = CGSize(width: newX, height: newY) + viewModel.scale = min(max(scaledValue * viewModel.lastScale, maxScaleValues.0), maxScaleValues.1) + + updateOffset() } .onEnded { _ in viewModel.lastScale = viewModel.scale viewModel.lastOffset = viewModel.offset } - + let dragGesture = DragGesture() .onChanged { value in let maxOffsetPoint = viewModel.calculateDragGestureMax() @@ -64,7 +63,7 @@ struct CropView: View { .onEnded { _ in viewModel.lastOffset = viewModel.offset } - + let rotationGesture = RotationGesture() .onChanged { value in viewModel.angle = value @@ -72,14 +71,14 @@ struct CropView: View { .onEnded { _ in viewModel.lastAngle = viewModel.angle } - + 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() @@ -92,11 +91,11 @@ struct CropView: View { GeometryReader { geometry in Color.clear .onAppear { - viewModel.imageSizeInView = geometry.size + viewModel.updateMaskDimensions(for: geometry.size) } } ) - + Image(uiImage: image) .resizable() .scaledToFit() @@ -105,14 +104,14 @@ struct CropView: View { .offset(viewModel.offset) .mask( MaskShapeView(maskShape: maskShape) - .frame(width: viewModel.maskRadius * 2, height: viewModel.maskRadius * 2) + .frame(width: viewModel.maskSize.width, height: viewModel.maskSize.height) ) } .frame(maxWidth: .infinity, maxHeight: .infinity) .simultaneousGesture(magnificationGesture) .simultaneousGesture(dragGesture) .simultaneousGesture(configuration.rotateImage ? rotationGesture : nil) - + HStack { Button { dismiss() @@ -120,9 +119,9 @@ struct CropView: View { Text("cancel_button", tableName: localizableTableName, bundle: .module) } .foregroundColor(.white) - + Spacer() - + Button { onComplete(cropImage()) dismiss() @@ -136,7 +135,15 @@ struct CropView: View { } .background(.black) } - + + private func updateOffset() { + let maxOffsetPoint = viewModel.calculateDragGestureMax() + let newX = min(max(viewModel.offset.width, -maxOffsetPoint.x), maxOffsetPoint.x) + let newY = min(max(viewModel.offset.height, -maxOffsetPoint.y), maxOffsetPoint.y) + viewModel.offset = CGSize(width: newX, height: newY) + viewModel.lastOffset = viewModel.offset + } + private func cropImage() -> UIImage? { var editedImage: UIImage = image if configuration.rotateImage { @@ -149,21 +156,22 @@ struct CropView: View { } if configuration.cropImageCircular && maskShape == .circle { return viewModel.cropToCircle(editedImage) + } else if maskShape == .rectangle { + return viewModel.cropToRectangle(editedImage) } else { return viewModel.cropToSquare(editedImage) } } - + private struct MaskShapeView: View { let maskShape: MaskShape - + var body: some View { Group { switch maskShape { case .circle: Circle() - - case .square: + case .square, .rectangle: Rectangle() } } diff --git a/Tests/SwiftyCropTests/SwiftyCropTests.swift b/Tests/SwiftyCropTests/SwiftyCropTests.swift index d79f68c..a9c7f41 100644 --- a/Tests/SwiftyCropTests/SwiftyCropTests.swift +++ b/Tests/SwiftyCropTests/SwiftyCropTests.swift @@ -6,10 +6,13 @@ final class SwiftyCropTests: XCTestCase { let configuration = SwiftyCropConfiguration( maxMagnificationScale: 1.0, maskRadius: 1.0, - cropImageCircular: true + cropImageCircular: true, + rectAspectRatio: 4/3 ) + XCTAssertEqual(configuration.maxMagnificationScale, 1.0) XCTAssertEqual(configuration.maskRadius, 1.0) XCTAssertEqual(configuration.cropImageCircular, true) + XCTAssertEqual(configuration.rectAspectRatio, 4/3) } }