diff --git a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h index 1ae9e977..8d96a4f4 100644 --- a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h +++ b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h @@ -29,9 +29,11 @@ NS_ASSUME_NONNULL_BEGIN /// Crops a portion of an existing image object and returns it as a new image /// @param frame The region inside the image (In image pixel space) to crop /// @param angle If any, the angle the image is rotated at as well +/// @param flip Whether to flip (mirror) the image /// @param circular Whether the resulting image is returned as a square or a circle - (nonnull UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle + flip:(BOOL)flip circularClip:(BOOL)circular; @end diff --git a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m index b2cad97d..134a80ce 100644 --- a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m +++ b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m @@ -31,7 +31,7 @@ - (BOOL)hasAlpha alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaPremultipliedLast); } -- (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle circularClip:(BOOL)circular +- (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle flip:(BOOL)flip circularClip:(BOOL)circular { UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat new]; format.opaque = !self.hasAlpha && !circular; @@ -46,6 +46,10 @@ - (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle circular CGContextAddEllipseInRect(context, (CGRect){CGPointZero, frame.size}); CGContextClip(context); } + + // Flip image when applicable + CGContextTranslateCTM(context, flip ? frame.size.width : 0, 0); + CGContextScaleCTM(context, flip ? -1 : 1, 1); // Offset the origin (Which is the top left corner) to start where our cropping origin is CGContextTranslateCTM(context, -frame.origin.x, -frame.origin.y); @@ -53,7 +57,7 @@ - (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle circular // If an angle was supplied, rotate the entire canvas + coordinate space to match if (angle != 0) { // Rotation in radians - CGFloat rotation = angle * (M_PI/180.0f); + CGFloat rotation = angle * (M_PI/180.0); // Work out the new bounding size of the canvas after rotation CGRect imageBounds = (CGRect){CGPointZero, self.size}; diff --git a/Objective-C/TOCropViewController/Categories/UIView+Pixels.h b/Objective-C/TOCropViewController/Categories/UIView+Pixels.h new file mode 100644 index 00000000..eddff707 --- /dev/null +++ b/Objective-C/TOCropViewController/Categories/UIView+Pixels.h @@ -0,0 +1,41 @@ +// +// UIView+Pixels.h +// +// Copyright 2024 Jan de Vries. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIView(TOPixels) + +///Round point value to nearest physical pixel +- (CGFloat)roundToNearestPixel:(CGFloat)point NS_SWIFT_NAME(roundToNearestPixel(point:)); + +///Check if two CGFloats (points) round to the same number of physical pixels +- (BOOL)pixelCountOf:(CGFloat)point1 equals:(CGFloat)point2; + +///Works like CGRectIntegral() but rounds values to the nearest physical pixel +- (CGRect)CGRectIntegralRetina:(CGRect)rect; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Objective-C/TOCropViewController/Categories/UIView+Pixels.m b/Objective-C/TOCropViewController/Categories/UIView+Pixels.m new file mode 100644 index 00000000..f734b215 --- /dev/null +++ b/Objective-C/TOCropViewController/Categories/UIView+Pixels.m @@ -0,0 +1,52 @@ +// +// UIView+Pixels.m +// +// Copyright 2024 Jan de Vries. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "UIView+Pixels.h" + +@implementation UIView (TOPixels) + +- (CGFloat)roundToNearestPixel:(CGFloat)point { + CGFloat screenScale = 2.0; + if (self.window != nil && self.window.screen != nil) { + screenScale = self.window.screen.scale; + } + return round(point * screenScale) / screenScale; +} + +- (BOOL)pixelCountOf:(CGFloat)point1 equals:(CGFloat)point2 +{ + if (self.window == nil || self.window.screen == nil) { + return point1 == point2; + } + CGFloat screenScale = self.window.screen.scale; + return round(point1*screenScale) == round(point2*screenScale); +} + +- (CGRect)CGRectIntegralRetina:(CGRect)rect +{ + return CGRectMake([self roundToNearestPixel:rect.origin.x], + [self roundToNearestPixel:rect.origin.y], + [self roundToNearestPixel:rect.size.width], + [self roundToNearestPixel:rect.size.height]); +} + +@end diff --git a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h index f6199e47..58f20ecc 100644 --- a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h +++ b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h @@ -29,9 +29,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonnull, nonatomic, readonly) UIImage *image; @property (nonatomic, readonly) CGRect cropFrame; @property (nonatomic, readonly) NSInteger angle; +@property (nonatomic, readonly) BOOL flipped; @property (nonatomic, readonly) BOOL circular; -- (nonnull instancetype)initWithImage:(nonnull UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle circular:(BOOL)circular; +- (nonnull instancetype)initWithImage:(nonnull UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle flipped:(BOOL)flipped circular:(BOOL)circular; @end diff --git a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m index 578e4a24..76939b94 100644 --- a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m +++ b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m @@ -28,6 +28,7 @@ @interface TOActivityCroppedImageProvider () @property (nonatomic, strong, readwrite) UIImage *image; @property (nonatomic, assign, readwrite) CGRect cropFrame; @property (nonatomic, assign, readwrite) NSInteger angle; +@property (nonatomic, assign, readwrite) BOOL flipped; @property (nonatomic, assign, readwrite) BOOL circular; @property (atomic, strong) UIImage *croppedImage; @@ -36,12 +37,13 @@ @interface TOActivityCroppedImageProvider () @implementation TOActivityCroppedImageProvider -- (instancetype)initWithImage:(UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle circular:(BOOL)circular +- (instancetype)initWithImage:(UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle flipped:(BOOL)flipped circular:(BOOL)circular { if (self = [super initWithPlaceholderItem:[UIImage new]]) { _image = image; _cropFrame = cropFrame; _angle = angle; + _flipped = flipped; _circular = circular; } @@ -63,12 +65,12 @@ - (id)activityViewController:(UIActivityViewController *)activityViewController - (id)item { //If the user didn't touch the image, just forward along the original - if (self.angle == 0 && CGRectEqualToRect(self.cropFrame, (CGRect){CGPointZero, self.image.size})) { + if (self.angle == 0 && !self.flipped && CGRectEqualToRect(self.cropFrame, (CGRect){CGPointZero, self.image.size})) { self.croppedImage = self.image; return self.croppedImage; } - UIImage *image = [self.image croppedImageWithFrame:self.cropFrame angle:self.angle circularClip:self.circular]; + UIImage *image = [self.image croppedImageWithFrame:self.cropFrame angle:self.angle flip:self.flipped circularClip:self.circular]; self.croppedImage = image; return self.croppedImage; } diff --git a/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m b/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m index 3f79f023..732a2184 100644 --- a/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m +++ b/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m @@ -27,7 +27,7 @@ @implementation TOCropViewControllerTransitioning - (NSTimeInterval)transitionDuration:(id )transitionContext { - return 0.45f; + return 0.45; } - (void)animateTransition:(id )transitionContext @@ -86,13 +86,13 @@ - (void)animateTransition:(id )transitionC } } - cropViewController.view.alpha = (self.isDismissing ? 1.0f : 0.0f); + cropViewController.view.alpha = (self.isDismissing ? 1.0 : 0.0); if (imageView) { - [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.7f options:0 animations:^{ + [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:1.0 initialSpringVelocity:0.7 options:0 animations:^{ imageView.frame = self.toFrame; } completion:^(BOOL complete) { - [UIView animateWithDuration:0.25f animations:^{ - imageView.alpha = 0.0f; + [UIView animateWithDuration:0.25 animations:^{ + imageView.alpha = 0.0; }completion:^(BOOL complete) { [imageView removeFromSuperview]; }]; @@ -100,7 +100,7 @@ - (void)animateTransition:(id )transitionC } [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ - cropViewController.view.alpha = (self.isDismissing ? 0.0f : 1.0f); + cropViewController.view.alpha = (self.isDismissing ? 0.0 : 1.0); } completion:^(BOOL complete) { [self reset]; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; diff --git a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h index d7a8a8a9..b2bfd7cf 100644 --- a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h +++ b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h @@ -28,10 +28,11 @@ NS_ASSUME_NONNULL_BEGIN @interface TOCroppedImageAttributes : NSObject @property (nonatomic, readonly) NSInteger angle; +@property (nonatomic, readonly) BOOL flipped; @property (nonatomic, readonly) CGRect croppedFrame; @property (nonatomic, readonly) CGSize originalImageSize; -- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle originalImageSize:(CGSize)originalSize; +- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle flipped:(BOOL)flipped originalImageSize:(CGSize)originalSize; @end diff --git a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m index 97ddaf32..54a16378 100644 --- a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m +++ b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m @@ -25,6 +25,7 @@ @interface TOCroppedImageAttributes () @property (nonatomic, assign, readwrite) NSInteger angle; +@property (nonatomic, assign, readwrite) BOOL flipped; @property (nonatomic, assign, readwrite) CGRect croppedFrame; @property (nonatomic, assign, readwrite) CGSize originalImageSize; @@ -32,10 +33,11 @@ @interface TOCroppedImageAttributes () @implementation TOCroppedImageAttributes -- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle originalImageSize:(CGSize)originalSize +- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle flipped:(BOOL)flipped originalImageSize:(CGSize)originalSize { if (self = [super init]) { _angle = angle; + _flipped = flipped; _croppedFrame = croppedFrame; _originalImageSize = originalSize; } diff --git a/Objective-C/TOCropViewController/Resources/arrow.counterclockwise@2x.png b/Objective-C/TOCropViewController/Resources/arrow.counterclockwise@2x.png new file mode 100644 index 00000000..c1ecd2d1 Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/arrow.counterclockwise@2x.png differ diff --git a/Objective-C/TOCropViewController/Resources/arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png b/Objective-C/TOCropViewController/Resources/arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png new file mode 100644 index 00000000..f909052d Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png differ diff --git a/Objective-C/TOCropViewController/Resources/arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png b/Objective-C/TOCropViewController/Resources/arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png new file mode 100644 index 00000000..9bc7fe7f Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png differ diff --git a/Objective-C/TOCropViewController/Resources/aspectratio.fill@2x.png b/Objective-C/TOCropViewController/Resources/aspectratio.fill@2x.png new file mode 100644 index 00000000..1c233eb6 Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/aspectratio.fill@2x.png differ diff --git a/Objective-C/TOCropViewController/Resources/checkmark@2x.png b/Objective-C/TOCropViewController/Resources/checkmark@2x.png new file mode 100644 index 00000000..d34d5691 Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/checkmark@2x.png differ diff --git a/Objective-C/TOCropViewController/Resources/rotate.left.fill@2x.png b/Objective-C/TOCropViewController/Resources/rotate.left.fill@2x.png new file mode 100644 index 00000000..e8c986c2 Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/rotate.left.fill@2x.png differ diff --git a/Objective-C/TOCropViewController/Resources/rotate.right.fill@2x.png b/Objective-C/TOCropViewController/Resources/rotate.right.fill@2x.png new file mode 100644 index 00000000..d67ca52f Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/rotate.right.fill@2x.png differ diff --git a/Objective-C/TOCropViewController/Resources/xmark@2x.png b/Objective-C/TOCropViewController/Resources/xmark@2x.png new file mode 100644 index 00000000..8975a1f1 Binary files /dev/null and b/Objective-C/TOCropViewController/Resources/xmark@2x.png differ diff --git a/Objective-C/TOCropViewController/TOCropViewController.h b/Objective-C/TOCropViewController/TOCropViewController.h index fa658dae..62814453 100755 --- a/Objective-C/TOCropViewController/TOCropViewController.h +++ b/Objective-C/TOCropViewController/TOCropViewController.h @@ -45,33 +45,57 @@ Called when the user has committed the crop action, and provides just the cropping rectangle. - @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) + @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ +- (void)cropViewController:(nonnull TOCropViewController *)cropViewController + didCropImageToRect:(CGRect)cropRect + angle:(NSInteger)angle + flipped:(BOOL)flipped; + +/// Deprecated, use method with 'flipped' argument instead. +/// - Warning: Deprecated, add arg 'flipped: (BOOL) flipped' to this method. - (void)cropViewController:(nonnull TOCropViewController *)cropViewController didCropImageToRect:(CGRect)cropRect angle:(NSInteger)angle; /** Called when the user has committed the crop action, and provides - both the original image with crop co-ordinates. + both the original image with crop coordinates. @param image The newly cropped image. - @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) + @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ +- (void)cropViewController:(nonnull TOCropViewController *)cropViewController + didCropToImage:(nonnull UIImage *)image withRect:(CGRect)cropRect + angle:(NSInteger)angle + flipped:(BOOL)flipped; + +/// Deprecated, use method with 'flipped' argument instead. +/// - Warning: Deprecated, add arg 'flipped: (BOOL) flipped' to this method. - (void)cropViewController:(nonnull TOCropViewController *)cropViewController didCropToImage:(nonnull UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle; /** If the cropping style is set to circular, implementing this delegate will return a circle-cropped version of the selected - image, as well as it's cropping co-ordinates + image, as well as its cropping coordinates @param image The newly cropped image, clipped to a circle shape - @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) + @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ +- (void)cropViewController:(nonnull TOCropViewController *)cropViewController + didCropToCircularImage:(nonnull UIImage *)image withRect:(CGRect)cropRect + angle:(NSInteger)angle + flipped:(BOOL)flipped; + +/// Deprecated, use method with 'flipped' argument instead. +/// - Warning: Deprecated, add arg 'flipped: (BOOL) flipped' to this method. - (void)cropViewController:(nonnull TOCropViewController *)cropViewController didCropToCircularImage:(nonnull UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle; @@ -109,7 +133,7 @@ @property (nullable, nonatomic, weak) id delegate; /** - If true, when the user hits 'Done', a UIActivityController will appear + If YES, when the user hits 'Done', a UIActivityController will appear before the view controller ends. */ @property (nonatomic, assign) BOOL showActivitySheetOnDone; @@ -137,6 +161,18 @@ */ @property (nonatomic, assign) NSInteger angle; +/** + Use this and the ``angle`` property to flip (mirror) the image. + For horizontal flip set flipped to YES and angle to 0. + For vertical flip set flipped to YES and angle to 180. + For both horizontal and vertical flip set flipped to NO and angle to 180. + + This property can be set before the controller is presented to have + the image 'restored' to a previous cropping layout. + */ +@property (nonatomic, assign) BOOL flipped; +@property (nonatomic, assign) BOOL mirrored __attribute((unavailable("Use 'flipped' instead."))); + /** The toolbar view managed by this view controller. */ @@ -182,7 +218,7 @@ @property (nullable, nonatomic, copy) NSString *cancelButtonTitle; /** - If true, button icons are visible in portairt instead button text. + If YES, button icons are visible in portairt instead button text. Default is NO. */ @@ -207,8 +243,8 @@ @property (nonatomic, assign) BOOL showCancelConfirmationDialog; /** - If true, a custom aspect ratio is set, and the aspectRatioLockEnabled is set to YES, the crop box - will swap it's dimensions depending on portrait or landscape sized images. + If YES, a custom aspect ratio is set, and the aspectRatioLockEnabled is set to YES, the crop box + will swap its dimensions depending on portrait or landscape sized images. This value also controls whether the dimensions can swap when the image is rotated. Default is NO. @@ -216,7 +252,7 @@ @property (nonatomic, assign) BOOL aspectRatioLockDimensionSwapEnabled; /** - If true, while it can still be resized, the crop box will be locked to its current aspect ratio. + If YES, while it can still be resized, the crop box will be locked to its current aspect ratio. If this is set to YES, and `resetAspectRatioEnabled` is set to NO, then the aspect ratio button will automatically be hidden from the toolbar. @@ -226,7 +262,7 @@ @property (nonatomic, assign) BOOL aspectRatioLockEnabled; /** - If true, tapping the reset button will also reset the aspect ratio back to the image + If YES, tapping the reset button will also reset the aspect ratio back to the image default ratio. Otherwise, the reset will just zoom out to the current aspect ratio. If this is set to NO, and `aspectRatioLockEnabled` is set to YES, then the aspect ratio @@ -249,9 +285,23 @@ */ @property (nonatomic, assign) BOOL rotateClockwiseButtonHidden; +/** + When enabled, hides the flip horizontal button on the toolbar. + + Default is NO. + */ +@property (nonatomic, assign) BOOL flipHorizontalButtonHidden; + +/** + When enabled, hides the flip vertical button on the toolbar. + + Default is YES. + */ +@property (nonatomic, assign) BOOL flipVerticalButtonHidden; + /* If this controller is embedded in UINavigationController its navigation bar - is hidden by default. Set this property to false to show the navigation bar. + is hidden by default. Set this property to NO to show the navigation bar. This must be set before this controller is presented. */ @property (nonatomic, assign) BOOL hidesNavigationBar; @@ -296,26 +346,25 @@ Default is NO. */ -@property (nonatomic, assign) BOOL reverseContentLayout -; +@property (nonatomic, assign) BOOL reverseContentLayout; /** - If `showActivitySheetOnDone` is true, then these activity items will - be supplied to that UIActivityViewController in addition to the + If `showActivitySheetOnDone` is YES, then these activity items will + be supplied to that UIActivityViewController in addition to the `TOActivityCroppedImageProvider` object. */ @property (nullable, nonatomic, strong) NSArray *activityItems; /** - If `showActivitySheetOnDone` is true, then you may specify any - custom activities your app implements in this array. If your activity requires + If `showActivitySheetOnDone` is YES, then you may specify any + custom activities your app implements in this array. If your activity requires access to the cropping information, it can be accessed in the supplied `TOActivityCroppedImageProvider` object */ @property (nullable, nonatomic, strong) NSArray *applicationActivities; /** - If `showActivitySheetOnDone` is true, then you may expliclty + If `showActivitySheetOnDone` is YES, then you may expliclty set activities that won't appear in the share sheet here. */ @property (nullable, nonatomic, strong) NSArray *excludedActivityTypes; @@ -338,32 +387,35 @@ just the cropping rectangle. @param cropRect A rectangle indicating the crop region of the image the user chose - (In the original image's local co-ordinate space) + (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ -@property (nullable, nonatomic, strong) void (^onDidCropImageToRect)(CGRect cropRect, NSInteger angle); +@property (nullable, nonatomic, strong) void (^onDidCropImageToRect)(CGRect cropRect, NSInteger angle, BOOL flipped); /** Called when the user has committed the crop action, and provides - both the cropped image with crop co-ordinates. + both the cropped image with crop coordinates. @param image The newly cropped image. @param cropRect A rectangle indicating the crop region of the image the user chose - (In the original image's local co-ordinate space) + (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ -@property (nullable, nonatomic, strong) void (^onDidCropToRect)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle); +@property (nullable, nonatomic, strong) void (^onDidCropToRect)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle, BOOL flipped); /** If the cropping style is set to circular, this block will return a circle-cropped version of the selected - image, as well as it's cropping co-ordinates + image, as well as its cropping coordinates @param image The newly cropped image, clipped to a circle shape @param cropRect A rectangle indicating the crop region of the image the user chose - (In the original image's local co-ordinate space) + (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ -@property (nullable, nonatomic, strong) void (^onDidCropToCircleImage)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle); +@property (nullable, nonatomic, strong) void (^onDidCropToCircleImage)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle, BOOL flipped); ///------------------------------------------------ @@ -432,6 +484,7 @@ @param fromView A view that's frame will be used as the origin for this animation. Optional if `fromFrame` has a value. @param fromFrame In the screen's coordinate space, the frame from which the image should animate from. @param angle The rotation angle in which the image was rotated when it was originally cropped. + @param flipped Whether the image was flipped (mirrored) when it was originally cropped. @param toFrame In the image's coordinate space, the previous crop frame that created the previous crop @param setup A block that is called just before the transition starts. Recommended for hiding any necessary image views. @param completion A block that is called once the transition animation is completed. @@ -441,9 +494,10 @@ fromView:(nullable UIView *)fromView fromFrame:(CGRect)fromFrame angle:(NSInteger)angle + flipped:(BOOL)flipped toImageFrame:(CGRect)toFrame setup:(nullable void (^)(void))setup - completion:(nullable void (^)(void))completion NS_SWIFT_NAME(presentAnimatedFrom(_:fromImage:fromView:fromFrame:angle:toFrame:setup:completion:)); + completion:(nullable void (^)(void))completion NS_SWIFT_NAME(presentAnimatedFrom(_:fromImage:fromView:fromFrame:angle:flipped:toFrame:setup:completion:)); /** Play a custom animation of the supplied cropped image zooming out from diff --git a/Objective-C/TOCropViewController/TOCropViewController.m b/Objective-C/TOCropViewController/TOCropViewController.m index a8fb131f..6aa11ee5 100755 --- a/Objective-C/TOCropViewController/TOCropViewController.m +++ b/Objective-C/TOCropViewController/TOCropViewController.m @@ -25,10 +25,11 @@ #import "TOCropViewControllerTransitioning.h" #import "TOActivityCroppedImageProvider.h" #import "UIImage+CropRotate.h" +#import "UIView+Pixels.h" #import "TOCroppedImageAttributes.h" -static const CGFloat kTOCropViewControllerTitleTopPadding = 14.0f; -static const CGFloat kTOCropViewControllerToolbarHeight = 44.0f; +static const CGFloat kTOCropViewControllerTitleTopPadding = 14.0; +static const CGFloat kTOCropViewControllerToolbarHeight = 44.0; @interface TOCropViewController () @@ -84,7 +85,8 @@ - (instancetype)initWithCroppingStyle:(TOCropViewCroppingStyle)style image:(UIIm // Set up base view controller behaviour self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; self.modalPresentationStyle = UIModalPresentationFullScreen; - self.hidesNavigationBar = true; + self.hidesNavigationBar = YES; + self.flipVerticalButtonHidden = YES; // Controller object that handles the transition animation when presenting / dismissing this app _transitionController = [[TOCropViewControllerTransitioning alloc] init]; @@ -133,6 +135,8 @@ - (void)viewDidLoad self.toolbar.clampButtonTapped = ^{ [weakSelf showAspectRatioDialog]; }; self.toolbar.rotateCounterclockwiseButtonTapped = ^{ [weakSelf rotateCropViewCounterclockwise]; }; self.toolbar.rotateClockwiseButtonTapped = ^{ [weakSelf rotateCropViewClockwise]; }; + self.toolbar.flipHorizontalButtonTapped = ^{ [weakSelf flipCropViewHorizontally]; }; + self.toolbar.flipVerticalButtonTapped = ^{ [weakSelf flipCropViewVertically]; }; } - (void)viewWillAppear:(BOOL)animated @@ -165,7 +169,7 @@ - (void)viewWillAppear:(BOOL)animated [self.cropView setBackgroundImageViewHidden:YES animated:NO]; // The title label will fade - self.titleLabel.alpha = animated ? 0.0f : 1.0f; + self.titleLabel.alpha = animated ? 0.0 : 1.0; } // If an initial aspect ratio was set before presentation, set it now once the rest of @@ -191,11 +195,11 @@ - (void)viewDidAppear:(BOOL)animated #if TARGET_OS_IOS [self setNeedsStatusBarAppearanceUpdate]; #endif - self.titleLabel.alpha = 1.0f; + self.titleLabel.alpha = 1.0; }; if (animated) { - [UIView animateWithDuration:0.3f animations:updateContentBlock]; + [UIView animateWithDuration:0.3 animations:updateContentBlock]; } else { updateContentBlock(); @@ -219,7 +223,7 @@ - (void)viewWillDisappear:(BOOL)animated // Set the transition flag again so we can defer the status bar self.inTransition = YES; #if TARGET_OS_IOS - [UIView animateWithDuration:0.5f animations:^{ [self setNeedsStatusBarAppearanceUpdate]; }]; + [UIView animateWithDuration:0.5 animations:^{ [self setNeedsStatusBarAppearanceUpdate]; }]; #endif // Restore the navigation controller to its state before we were presented @@ -282,12 +286,12 @@ - (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout CGRect frame = CGRectZero; if (!verticalLayout) { // In landscape laying out toolbar to the left frame.origin.x = insets.left; - frame.origin.y = 0.0f; + frame.origin.y = 0.0; frame.size.width = kTOCropViewControllerToolbarHeight; frame.size.height = CGRectGetHeight(self.view.frame); } else { - frame.origin.x = 0.0f; + frame.origin.x = 0.0; frame.size.width = CGRectGetWidth(self.view.bounds); frame.size.height = kTOCropViewControllerToolbarHeight; @@ -345,7 +349,7 @@ - (CGRect)frameForTitleLabelWithSize:(CGSize)size verticalLayout:(BOOL)verticalL { CGRect frame = (CGRect){CGPointZero, size}; CGFloat viewWidth = self.view.bounds.size.width; - CGFloat x = 0.0f; // Additional X offset in landscape mode + CGFloat x = 0.0; // Additional X offset in landscape mode // Adjust for landscape layout if (!verticalLayout) { @@ -358,7 +362,7 @@ - (CGRect)frameForTitleLabelWithSize:(CGSize)size verticalLayout:(BOOL)verticalL } // Work out horizontal position - frame.origin.x = ceilf((viewWidth - frame.size.width) * 0.5f); + frame.origin.x = [self.view roundToNearestPixel:(viewWidth - frame.size.width) * 0.5]; if (!verticalLayout) { frame.origin.x += x; } // Work out vertical position @@ -380,14 +384,14 @@ - (void)adjustCropViewInsets if (!self.titleLabel.text.length) { if (self.verticalLayout) { if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) { - self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0f, 0.0f, insets.bottom, 0.0f); + self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0, 0.0, insets.bottom, 0.0); } else { // Add padding to the top otherwise - self.cropView.cropRegionInsets = UIEdgeInsetsMake(insets.top, 0.0f, 0.0, 0.0f); + self.cropView.cropRegionInsets = UIEdgeInsetsMake(insets.top, 0.0, 0.0, 0.0); } } else { - self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0f, 0.0f, insets.bottom, 0.0f); + self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0, 0.0, insets.bottom, 0.0); } return; @@ -494,7 +498,7 @@ - (void)_willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOri self.toolbar.frame = frame; [self.toolbar layoutIfNeeded]; - self.toolbar.alpha = 0.0f; + self.toolbar.alpha = 0.0; [self.cropView prepareforRotation]; self.cropView.frame = [self frameForCropViewWithVerticalLayout:!UIInterfaceOrientationIsPortrait(toInterfaceOrientation)]; @@ -514,7 +518,7 @@ - (void)_willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInt // On iOS 11, since these layout calls are done multiple times, if we don't aggregate from the // current state, the animation breaks. [UIView animateWithDuration:duration - delay:0.0f + delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations: ^{ @@ -523,8 +527,8 @@ - (void)_willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInt [self.cropView performRelayoutForRotation]; } completion:nil]; - self.toolbarSnapshotView.alpha = 0.0f; - self.toolbar.alpha = 1.0f; + self.toolbarSnapshotView.alpha = 0.0; + self.toolbar.alpha = 1.0; } - (void)_didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation @@ -651,25 +655,25 @@ - (void)setAspectRatioPreset:(TOCropViewControllerAspectRatioPreset)aspectRatioP aspectRatio = CGSizeZero; break; case TOCropViewControllerAspectRatioPresetSquare: - aspectRatio = CGSizeMake(1.0f, 1.0f); + aspectRatio = CGSizeMake(1.0, 1.0); break; case TOCropViewControllerAspectRatioPreset3x2: - aspectRatio = CGSizeMake(3.0f, 2.0f); + aspectRatio = CGSizeMake(3.0, 2.0); break; case TOCropViewControllerAspectRatioPreset5x3: - aspectRatio = CGSizeMake(5.0f, 3.0f); + aspectRatio = CGSizeMake(5.0, 3.0); break; case TOCropViewControllerAspectRatioPreset4x3: - aspectRatio = CGSizeMake(4.0f, 3.0f); + aspectRatio = CGSizeMake(4.0, 3.0); break; case TOCropViewControllerAspectRatioPreset5x4: - aspectRatio = CGSizeMake(5.0f, 4.0f); + aspectRatio = CGSizeMake(5.0, 4.0); break; case TOCropViewControllerAspectRatioPreset7x5: - aspectRatio = CGSizeMake(7.0f, 5.0f); + aspectRatio = CGSizeMake(7.0, 5.0); break; case TOCropViewControllerAspectRatioPreset16x9: - aspectRatio = CGSizeMake(16.0f, 9.0f); + aspectRatio = CGSizeMake(16.0, 9.0); break; case TOCropViewControllerAspectRatioPresetCustom: aspectRatio = self.customAspectRatio; @@ -704,6 +708,16 @@ - (void)rotateCropViewCounterclockwise [self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:NO]; } +- (void)flipCropViewHorizontally +{ + [self.cropView flipImageAnimated:YES horizontal:YES]; +} + +- (void)flipCropViewVertically +{ + [self.cropView flipImageAnimated:YES horizontal:NO]; +} + #pragma mark - Crop View Delegates - - (void)cropViewDidBecomeResettable:(TOCropView *)cropView { @@ -723,7 +737,7 @@ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewControll completion:(void (^)(void))completion { [self presentAnimatedFromParentViewController:viewController fromImage:nil fromView:fromView fromFrame:fromFrame - angle:0 toImageFrame:CGRectZero setup:setup completion:completion]; + angle:0 flipped:NO toImageFrame:CGRectZero setup:setup completion:completion]; } - (void)presentAnimatedFromParentViewController:(UIViewController *)viewController @@ -731,6 +745,7 @@ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewControll fromView:(UIView *)fromView fromFrame:(CGRect)fromFrame angle:(NSInteger)angle + flipped:(BOOL)flipped toImageFrame:(CGRect)toFrame setup:(void (^)(void))setup completion:(void (^)(void))completion @@ -759,6 +774,8 @@ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewControll if (!CGRectIsEmpty(fromFrame)) { [strongSelf.cropView setGridOverlayHidden:NO animated:YES]; } + + strongSelf.cropView.isFlippedHorizontally = flipped; }]; } @@ -915,11 +932,21 @@ - (void)doneButtonTapped { CGRect cropFrame = self.cropView.imageCropFrame; NSInteger angle = self.cropView.angle; + BOOL flipped = self.cropView.isFlippedHorizontally; + + //Convert vertical flip to a horizontal flip + 180 degree rotation + if (self.cropView.isFlippedVertically) { + angle += 180; + angle %= 360; + cropFrame.origin.x = self.cropView.image.size.width - cropFrame.size.width - cropFrame.origin.x; + cropFrame.origin.y = self.cropView.image.size.height - cropFrame.size.height - cropFrame.origin.y; + flipped ^= YES; //Flipped both ways equals a 180 rotation without flip + } //If desired, when the user taps done, show an activity sheet if (self.showActivitySheetOnDone) { - TOActivityCroppedImageProvider *imageItem = [[TOActivityCroppedImageProvider alloc] initWithImage:self.image cropFrame:cropFrame angle:angle circular:(self.croppingStyle == TOCropViewCroppingStyleCircular)]; - TOCroppedImageAttributes *attributes = [[TOCroppedImageAttributes alloc] initWithCroppedFrame:cropFrame angle:angle originalImageSize:self.image.size]; + TOActivityCroppedImageProvider *imageItem = [[TOActivityCroppedImageProvider alloc] initWithImage:self.image cropFrame:cropFrame angle:angle flipped:flipped circular:(self.croppingStyle == TOCropViewCroppingStyleCircular)]; + TOCroppedImageAttributes *attributes = [[TOCroppedImageAttributes alloc] initWithCroppedFrame:cropFrame angle:angle flipped:flipped originalImageSize:self.image.size]; NSMutableArray *activityItems = [@[imageItem, attributes] mutableCopy]; if (self.activityItems) { @@ -965,64 +992,76 @@ - (void)doneButtonTapped return; } else { - self.toolbar.doneTextButton.enabled = false; + self.toolbar.doneTextButton.enabled = NO; } BOOL isCallbackOrDelegateHandled = NO; //If the delegate/block that only supplies crop data is provided, call it + if ([self.delegate respondsToSelector:@selector(cropViewController:didCropImageToRect:angle:flipped:)]) { + [self.delegate cropViewController:self didCropImageToRect:cropFrame angle:angle flipped:flipped]; + isCallbackOrDelegateHandled = YES; + } + if ([self.delegate respondsToSelector:@selector(cropViewController:didCropImageToRect:angle:)]) { [self.delegate cropViewController:self didCropImageToRect:cropFrame angle:angle]; isCallbackOrDelegateHandled = YES; } if (self.onDidCropImageToRect != nil) { - self.onDidCropImageToRect(cropFrame, angle); + self.onDidCropImageToRect(cropFrame, angle, flipped); isCallbackOrDelegateHandled = YES; } // Check if the circular APIs were implemented - BOOL isCircularImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToCircularImage:withRect:angle:)]; + BOOL isCircularImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToCircularImage:withRect:angle:flipped:)]; + BOOL isCircularImageDelegateNoFlipAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToCircularImage:withRect:angle:)]; BOOL isCircularImageCallbackAvailable = self.onDidCropToCircleImage != nil; // Check if non-circular was implemented - BOOL isDidCropToImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToImage:withRect:angle:)]; + BOOL isDidCropToImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToImage:withRect:angle:flipped:)]; + BOOL isDidCropToImageDelegateNoFlipAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToImage:withRect:angle:)]; BOOL isDidCropToImageCallbackAvailable = self.onDidCropToRect != nil; //If cropping circular and the circular generation delegate/block is implemented, call it - if (self.croppingStyle == TOCropViewCroppingStyleCircular && (isCircularImageDelegateAvailable || isCircularImageCallbackAvailable)) { - UIImage *image = [self.image croppedImageWithFrame:cropFrame angle:angle circularClip:YES]; + if (self.croppingStyle == TOCropViewCroppingStyleCircular && (isCircularImageDelegateAvailable || isDidCropToImageDelegateNoFlipAvailable || isCircularImageCallbackAvailable)) { + UIImage *image = [self.image croppedImageWithFrame:cropFrame angle:angle flip:flipped circularClip:YES]; - //Dispatch on the next run-loop so the animation isn't interuppted by the crop operation - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + //Dispatch on the next run-loop so the animation isn't interrupted by the crop operation + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (isCircularImageDelegateAvailable) { + [self.delegate cropViewController:self didCropToCircularImage:image withRect:cropFrame angle:angle flipped:flipped]; + } + if (isCircularImageDelegateNoFlipAvailable) { [self.delegate cropViewController:self didCropToCircularImage:image withRect:cropFrame angle:angle]; } if (isCircularImageCallbackAvailable) { - self.onDidCropToCircleImage(image, cropFrame, angle); + self.onDidCropToCircleImage(image, cropFrame, angle, flipped); } }); isCallbackOrDelegateHandled = YES; } //If the delegate/block that requires the specific cropped image is provided, call it - else if (isDidCropToImageDelegateAvailable || isDidCropToImageCallbackAvailable) { + else if (isDidCropToImageDelegateAvailable || isDidCropToImageDelegateNoFlipAvailable || isDidCropToImageCallbackAvailable) { UIImage *image = nil; - if (angle == 0 && CGRectEqualToRect(cropFrame, (CGRect){CGPointZero, self.image.size})) { + if (angle == 0 && !flipped && CGRectEqualToRect(cropFrame, (CGRect){CGPointZero, self.image.size})) { image = self.image; } else { - image = [self.image croppedImageWithFrame:cropFrame angle:angle circularClip:NO]; + image = [self.image croppedImageWithFrame:cropFrame angle:angle flip:flipped circularClip:NO]; } //Dispatch on the next run-loop so the animation isn't interuppted by the crop operation - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (isDidCropToImageDelegateAvailable) { + [self.delegate cropViewController:self didCropToImage:image withRect:cropFrame angle:angle flipped:flipped]; + } + if (isDidCropToImageDelegateNoFlipAvailable) { [self.delegate cropViewController:self didCropToImage:image withRect:cropFrame angle:angle]; } - if (isDidCropToImageCallbackAvailable) { - self.onDidCropToRect(image, cropFrame, angle); + self.onDidCropToRect(image, cropFrame, angle, flipped); } }); @@ -1041,6 +1080,23 @@ - (void)commitCurrentCrop #pragma mark - Property Methods - +- (void)setDelegate:(id)delegate { + //Check for usage of outdated (pre-flip) delegate methods. + //When on a fresh build (less than 2 hours old) an exception is triggered telling you to update. + if ([delegate respondsToSelector:@selector(cropViewController:didCropImageToRect:angle:)] || + [delegate respondsToSelector:@selector(cropViewController:didCropToImage:withRect:angle:)] || + [delegate respondsToSelector:@selector(cropViewController:didCropToCircularImage:withRect:angle:)]) + { + NSString *bundleName = [[NSBundle mainBundle] infoDictionary][@"CFBundleExecutable"]; + NSString *infoPath = [[NSBundle mainBundle] pathForResource:bundleName ofType:nil]; + NSDate *date = [NSFileManager.defaultManager attributesOfItemAtPath:infoPath error:nil][NSFileCreationDate]; + if ([date timeIntervalSinceNow] > -7200) { + [NSException raise:@"TOCropViewController error" format:@"TOCropViewController has updated: add arg 'flipped:(BOOL)flipped' to your didCrop delegate method implementations."]; + } + } + _delegate = delegate; +} + - (void)setTitle:(NSString *)title { [super setTitle:title]; @@ -1147,6 +1203,26 @@ - (void)setRotateButtonsHidden:(BOOL)rotateButtonsHidden self.toolbar.rotateClockwiseButtonHidden = rotateButtonsHidden; } +- (BOOL)flipVerticalButtonHidden +{ + return self.toolbar.flipVerticalButtonHidden; +} + +- (void)setFlipVerticalButtonHidden:(BOOL)flipVerticalButtonHidden +{ + self.toolbar.flipVerticalButtonHidden = flipVerticalButtonHidden; +} + +- (BOOL)flipHorizontalButtonHidden +{ + return self.toolbar.flipHorizontalButtonHidden; +} + +- (void)setFlipHorizontalButtonHidden:(BOOL)flipHorizontalButtonHidden +{ + self.toolbar.flipHorizontalButtonHidden = flipHorizontalButtonHidden; +} + - (void)setResetButtonHidden:(BOOL)resetButtonHidden { self.toolbar.resetButtonHidden = resetButtonHidden; @@ -1236,6 +1312,16 @@ - (NSInteger)angle return self.cropView.angle; } +- (void)setFlipped:(BOOL)flipped +{ + self.cropView.isFlippedHorizontally = flipped; +} + +- (BOOL)flipped +{ + return self.cropView.isFlippedHorizontally ^ self.cropView.isFlippedVertically; +} + - (void)setImageCropFrame:(CGRect)imageCropFrame { self.cropView.imageCropFrame = imageCropFrame; @@ -1292,12 +1378,12 @@ - (BOOL)statusBarHidden - (CGFloat)statusBarHeight { - CGFloat statusBarHeight = 0.0f; + CGFloat statusBarHeight = 0.0; statusBarHeight = self.view.safeAreaInsets.top; // We do need to include the status bar height on devices // that have a physical hardware inset, like an iPhone X notch - BOOL hardwareRelatedInset = self.view.safeAreaInsets.bottom > FLT_EPSILON + BOOL hardwareRelatedInset = self.view.safeAreaInsets.bottom > DBL_EPSILON && UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; // Always have insetting on Mac Catalyst @@ -1308,7 +1394,7 @@ - (CGFloat)statusBarHeight // Unless the status bar is visible, or we need to account // for a hardware notch, always treat the status bar height as zero if (self.statusBarHidden && !hardwareRelatedInset) { - statusBarHeight = 0.0f; + statusBarHeight = 0.0; } return statusBarHeight; diff --git a/Objective-C/TOCropViewController/Views/TOCropOverlayView.m b/Objective-C/TOCropViewController/Views/TOCropOverlayView.m index 257432b0..34f59999 100644 --- a/Objective-C/TOCropViewController/Views/TOCropOverlayView.m +++ b/Objective-C/TOCropViewController/Views/TOCropOverlayView.m @@ -22,7 +22,7 @@ #import "TOCropOverlayView.h" -static const CGFloat kTOCropOverLayerCornerWidth = 20.0f; +static const CGFloat kTOCropOverLayerCornerWidth = 20.0; @interface TOCropOverlayView () @@ -93,10 +93,10 @@ - (void)layoutLines CGRect frame = CGRectZero; switch (i) { - case 0: frame = (CGRect){-1.0f,-1.0f,boundsSize.width+2.0f, 1.0f}; break; //top - case 1: frame = (CGRect){boundsSize.width,0.0f,1.0f,boundsSize.height}; break; //right - case 2: frame = (CGRect){-1.0f,boundsSize.height,boundsSize.width+2.0f,1.0f}; break; //bottom - case 3: frame = (CGRect){-1.0f,0,1.0f,boundsSize.height+1.0f}; break; //left + case 0: frame = (CGRect){-1.0, -1.0, boundsSize.width+2.0, 1.0}; break; //top + case 1: frame = (CGRect){boundsSize.width, 0.0, 1.0, boundsSize.height}; break; //right + case 2: frame = (CGRect){-1.0 ,boundsSize.height, boundsSize.width+2.0, 1.0}; break; //bottom + case 3: frame = (CGRect){-1.0, 0.0, 1.0, boundsSize.height+1.0}; break; //left } lineView.frame = frame; @@ -110,20 +110,20 @@ - (void)layoutLines CGRect verticalFrame = CGRectZero, horizontalFrame = CGRectZero; switch (i) { case 0: //top left - verticalFrame = (CGRect){-3.0f,-3.0f,3.0f,kTOCropOverLayerCornerWidth+3.0f}; - horizontalFrame = (CGRect){0,-3.0f,kTOCropOverLayerCornerWidth,3.0f}; + verticalFrame = (CGRect){-3.0, -3.0, 3.0, kTOCropOverLayerCornerWidth+3.0}; + horizontalFrame = (CGRect){0, -3.0, kTOCropOverLayerCornerWidth, 3.0}; break; case 1: //top right - verticalFrame = (CGRect){boundsSize.width,-3.0f,3.0f,kTOCropOverLayerCornerWidth+3.0f}; - horizontalFrame = (CGRect){boundsSize.width-kTOCropOverLayerCornerWidth,-3.0f,kTOCropOverLayerCornerWidth,3.0f}; + verticalFrame = (CGRect){boundsSize.width,-3.0, 3.0, kTOCropOverLayerCornerWidth+3.0}; + horizontalFrame = (CGRect){boundsSize.width-kTOCropOverLayerCornerWidth, -3.0, kTOCropOverLayerCornerWidth, 3.0}; break; case 2: //bottom right - verticalFrame = (CGRect){boundsSize.width,boundsSize.height-kTOCropOverLayerCornerWidth,3.0f,kTOCropOverLayerCornerWidth+3.0f}; - horizontalFrame = (CGRect){boundsSize.width-kTOCropOverLayerCornerWidth,boundsSize.height,kTOCropOverLayerCornerWidth,3.0f}; + verticalFrame = (CGRect){boundsSize.width,boundsSize.height-kTOCropOverLayerCornerWidth, 3.0, kTOCropOverLayerCornerWidth+3.0}; + horizontalFrame = (CGRect){boundsSize.width-kTOCropOverLayerCornerWidth, boundsSize.height, kTOCropOverLayerCornerWidth, 3.0}; break; case 3: //bottom left - verticalFrame = (CGRect){-3.0f,boundsSize.height-kTOCropOverLayerCornerWidth,3.0f,kTOCropOverLayerCornerWidth}; - horizontalFrame = (CGRect){-3.0f,boundsSize.height,kTOCropOverLayerCornerWidth+3.0f,3.0f}; + verticalFrame = (CGRect){-3.0, boundsSize.height-kTOCropOverLayerCornerWidth, 3.0, kTOCropOverLayerCornerWidth}; + horizontalFrame = (CGRect){-3.0, boundsSize.height, kTOCropOverLayerCornerWidth+3.0, 3.0}; break; } @@ -132,7 +132,7 @@ - (void)layoutLines } //grid lines - horizontal - CGFloat thickness = 1.0f / self.traitCollection.displayScale; + CGFloat thickness = 1.0 / self.traitCollection.displayScale; NSInteger numberOfLines = self.horizontalGridLines.count; CGFloat padding = (CGRectGetHeight(self.bounds) - (thickness*numberOfLines)) / (numberOfLines + 1); for (NSInteger i = 0; i < numberOfLines; i++) { @@ -163,22 +163,22 @@ - (void)setGridHidden:(BOOL)hidden animated:(BOOL)animated if (animated == NO) { for (UIView *lineView in self.horizontalGridLines) { - lineView.alpha = hidden ? 0.0f : 1.0f; + lineView.alpha = hidden ? 0.0 : 1.0; } for (UIView *lineView in self.verticalGridLines) { - lineView.alpha = hidden ? 0.0f : 1.0f; + lineView.alpha = hidden ? 0.0 : 1.0; } return; } - [UIView animateWithDuration:hidden?0.35f:0.2f animations:^{ + [UIView animateWithDuration:hidden? 0.35 : 0.2 animations:^{ for (UIView *lineView in self.horizontalGridLines) - lineView.alpha = hidden ? 0.0f : 1.0f; + lineView.alpha = hidden ? 0.0 : 1.0; for (UIView *lineView in self.verticalGridLines) - lineView.alpha = hidden ? 0.0f : 1.0f; + lineView.alpha = hidden ? 0.0 : 1.0; }]; } diff --git a/Objective-C/TOCropViewController/Views/TOCropToolbar.h b/Objective-C/TOCropViewController/Views/TOCropToolbar.h index ff621729..5827afc4 100644 --- a/Objective-C/TOCropViewController/Views/TOCropToolbar.h +++ b/Objective-C/TOCropViewController/Views/TOCropToolbar.h @@ -60,6 +60,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, readonly) UIButton *resetButton; @property (nonatomic, strong, readonly) UIButton *clampButton; @property (nullable, nonatomic, strong, readonly) UIButton *rotateClockwiseButton; +@property (nonatomic, strong, readonly) UIButton *flipHorizontalButton; +@property (nonatomic, strong, readonly) UIButton *flipVerticalButton; @property (nonatomic, readonly) UIButton *rotateButton; // Points to `rotateCounterClockwiseButton` @@ -70,6 +72,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, copy) void (^rotateClockwiseButtonTapped)(void); @property (nullable, nonatomic, copy) void (^clampButtonTapped)(void); @property (nullable, nonatomic, copy) void (^resetButtonTapped)(void); +@property (nullable, nonatomic, copy) void (^flipHorizontalButtonTapped)(void); +@property (nullable, nonatomic, copy) void (^flipVerticalButtonTapped)(void); /* State management for the 'clamp' button */ @property (nonatomic, assign) BOOL clampButtonGlowing; @@ -79,6 +83,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL clampButtonHidden; @property (nonatomic, assign) BOOL rotateCounterclockwiseButtonHidden; @property (nonatomic, assign) BOOL rotateClockwiseButtonHidden; +@property (nonatomic, assign) BOOL flipHorizontalButtonHidden; +@property (nonatomic, assign) BOOL flipVerticalButtonHidden; @property (nonatomic, assign) BOOL resetButtonHidden; @property (nonatomic, assign) BOOL doneButtonHidden; @property (nonatomic, assign) BOOL cancelButtonHidden; diff --git a/Objective-C/TOCropViewController/Views/TOCropToolbar.m b/Objective-C/TOCropViewController/Views/TOCropToolbar.m index cc5023d7..15aacc54 100644 --- a/Objective-C/TOCropViewController/Views/TOCropToolbar.m +++ b/Objective-C/TOCropViewController/Views/TOCropToolbar.m @@ -21,6 +21,7 @@ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #import "TOCropToolbar.h" +#import "UIView+Pixels.h" #define TOCROPTOOLBAR_DEBUG_SHOWING_BUTTONS_CONTAINER_RECT 0 // convenience debug toggle @@ -54,7 +55,7 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)setup { self.backgroundView = [[UIView alloc] initWithFrame:self.bounds]; - self.backgroundView.backgroundColor = [UIColor colorWithWhite:0.12f alpha:1.0f]; + self.backgroundView.backgroundColor = [UIColor colorWithWhite:0.12 alpha:1.0]; [self addSubview:self.backgroundView]; // On iOS 9, we can use the new layout features to determine whether we're in an 'Arabic' style language mode @@ -75,11 +76,11 @@ - (void)setup { resourceBundle, nil) forState:UIControlStateNormal]; - [_doneTextButton setTitleColor:[UIColor colorWithRed:1.0f green:0.8f blue:0.0f alpha:1.0f] forState:UIControlStateNormal]; + [_doneTextButton setTitleColor:[UIColor colorWithRed:1.0 green:0.8 blue:0.0 alpha:1.0] forState:UIControlStateNormal]; if (@available(iOS 13.0, *)) { - [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f weight:UIFontWeightMedium]]; + [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0 weight:UIFontWeightMedium]]; } else { - [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f]]; + [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0]]; } [_doneTextButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; [_doneTextButton sizeToFit]; @@ -87,7 +88,7 @@ - (void)setup { _doneIconButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_doneIconButton setImage:[TOCropToolbar doneImage] forState:UIControlStateNormal]; - [_doneIconButton setTintColor:[UIColor colorWithRed:1.0f green:0.8f blue:0.0f alpha:1.0f]]; + [_doneIconButton setTintColor:[UIColor colorWithRed:1.0 green:0.8 blue:0.0 alpha:1.0]]; [_doneIconButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:_doneIconButton]; @@ -102,7 +103,7 @@ - (void)setup { resourceBundle, nil) forState:UIControlStateNormal]; - [_cancelTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f]]; + [_cancelTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0]]; [_cancelTextButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; [_cancelTextButton sizeToFit]; [self addSubview:_cancelTextButton]; @@ -133,6 +134,20 @@ - (void)setup { [_rotateClockwiseButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:_rotateClockwiseButton]; + _flipHorizontalButton = [UIButton buttonWithType:UIButtonTypeSystem]; + _flipHorizontalButton.contentMode = UIViewContentModeCenter; + _flipHorizontalButton.tintColor = [UIColor whiteColor]; + [_flipHorizontalButton setImage:[TOCropToolbar flipHImage] forState:UIControlStateNormal]; + [_flipHorizontalButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_flipHorizontalButton]; + + _flipVerticalButton = [UIButton buttonWithType:UIButtonTypeSystem]; + _flipVerticalButton.contentMode = UIViewContentModeCenter; + _flipVerticalButton.tintColor = [UIColor whiteColor]; + [_flipVerticalButton setImage:[TOCropToolbar flipVImage] forState:UIControlStateNormal]; + [_flipVerticalButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_flipVerticalButton]; + _resetButton = [UIButton buttonWithType:UIButtonTypeSystem]; _resetButton.contentMode = UIViewContentModeCenter; _resetButton.tintColor = [UIColor whiteColor]; @@ -153,10 +168,10 @@ - (void)layoutSubviews BOOL verticalLayout = (CGRectGetWidth(self.bounds) < CGRectGetHeight(self.bounds)); CGSize boundsSize = self.bounds.size; - self.cancelIconButton.hidden = self.cancelButtonHidden || (_showOnlyIcons ? false : !verticalLayout); - self.cancelTextButton.hidden = self.cancelButtonHidden || (_showOnlyIcons ? true : verticalLayout); - self.doneIconButton.hidden = self.doneButtonHidden || (_showOnlyIcons ? false : !verticalLayout); - self.doneTextButton.hidden = self.doneButtonHidden || (_showOnlyIcons ? true : verticalLayout); + self.cancelIconButton.hidden = self.cancelButtonHidden || (_showOnlyIcons ? NO : !verticalLayout); + self.cancelTextButton.hidden = self.cancelButtonHidden || (_showOnlyIcons ? YES : verticalLayout); + self.doneIconButton.hidden = self.doneButtonHidden || (_showOnlyIcons ? NO : !verticalLayout); + self.doneTextButton.hidden = self.doneButtonHidden || (_showOnlyIcons ? YES : verticalLayout); CGRect frame = self.bounds; frame.origin.x -= self.backgroundViewOutsets.left; @@ -178,12 +193,12 @@ - (void)layoutSubviews #endif if (verticalLayout == NO) { - CGFloat insetPadding = 10.0f; + CGFloat insetPadding = 10.0; // Work out the cancel button frame CGRect frame = CGRectZero; - frame.size.height = 44.0f; - frame.size.width = _showOnlyIcons ? 44.0f : MIN(self.frame.size.width / 3.0, self.cancelTextButton.frame.size.width); + frame.size.height = 44.0; + frame.size.width = _showOnlyIcons ? 44.0 : MIN(self.frame.size.width / 3.0, self.cancelTextButton.frame.size.width); //If normal layout, place on the left side, else place on the right if (self.reverseContentLayout == NO) { @@ -195,7 +210,7 @@ - (void)layoutSubviews (_showOnlyIcons ? self.cancelIconButton : self.cancelTextButton).frame = frame; // Work out the Done button frame - frame.size.width = _showOnlyIcons ? 44.0f : MIN(self.frame.size.width / 3.0, self.doneTextButton.frame.size.width); + frame.size.width = _showOnlyIcons ? 44.0 : MIN(self.frame.size.width / 3.0, self.doneTextButton.frame.size.width); if (self.reverseContentLayout == NO) { frame.origin.x = boundsSize.width - (frame.size.width + insetPadding); @@ -207,7 +222,7 @@ - (void)layoutSubviews // Work out the frame between the two buttons where we can layout our action buttons CGFloat x = self.reverseContentLayout ? CGRectGetMaxX((_showOnlyIcons ? self.doneIconButton : self.doneTextButton).frame) : CGRectGetMaxX((_showOnlyIcons ? self.cancelIconButton : self.cancelTextButton).frame); - CGFloat width = 0.0f; + CGFloat width = 0.0; if (self.reverseContentLayout == NO) { width = CGRectGetMinX((_showOnlyIcons ? self.doneIconButton : self.doneTextButton).frame) - CGRectGetMaxX((_showOnlyIcons ? self.cancelIconButton : self.cancelTextButton).frame); @@ -215,68 +230,86 @@ - (void)layoutSubviews else { width = CGRectGetMinX((_showOnlyIcons ? self.cancelIconButton : self.cancelTextButton).frame) - CGRectGetMaxX((_showOnlyIcons ? self.doneIconButton : self.doneTextButton).frame); } - - CGRect containerRect = CGRectIntegral((CGRect){x,frame.origin.y,width,44.0f}); + + CGRect containerRect = CGRectInset([self CGRectIntegralRetina:(CGRect){x,frame.origin.y,width,44.0}], _showOnlyIcons ? 0 : insetPadding, 0); #if TOCROPTOOLBAR_DEBUG_SHOWING_BUTTONS_CONTAINER_RECT containerView.frame = containerRect; #endif - CGSize buttonSize = (CGSize){44.0f,44.0f}; + CGSize buttonSize = (CGSize){44.0,44.0}; NSMutableArray *buttonsInOrderHorizontally = [NSMutableArray new]; + + if (!self.clampButtonHidden) { + [buttonsInOrderHorizontally addObject:self.clampButton]; + } + if (!self.rotateCounterclockwiseButtonHidden) { [buttonsInOrderHorizontally addObject:self.rotateCounterclockwiseButton]; } - if (!self.resetButtonHidden) { - [buttonsInOrderHorizontally addObject:self.resetButton]; + if (!self.flipHorizontalButtonHidden) { + [buttonsInOrderHorizontally addObject:self.flipHorizontalButton]; } - - if (!self.clampButtonHidden) { - [buttonsInOrderHorizontally addObject:self.clampButton]; + + if (!self.flipVerticalButtonHidden) { + [buttonsInOrderHorizontally addObject:self.flipVerticalButton]; } - + if (!self.rotateClockwiseButtonHidden) { [buttonsInOrderHorizontally addObject:self.rotateClockwiseButton]; } + + if (!self.resetButtonHidden) { + [buttonsInOrderHorizontally addObject:self.resetButton]; + } [self layoutToolbarButtons:buttonsInOrderHorizontally withSameButtonSize:buttonSize inContainerRect:containerRect horizontally:YES]; } else { CGRect frame = CGRectZero; - frame.size.height = 44.0f; - frame.size.width = 44.0f; - frame.origin.y = CGRectGetHeight(self.bounds) - 44.0f; + frame.size.height = 44.0; + frame.size.width = 44.0; + frame.origin.y = CGRectGetHeight(self.bounds) - 44.0; self.cancelIconButton.frame = frame; frame.origin.y = self.statusBarHeightInset; - frame.size.width = 44.0f; - frame.size.height = 44.0f; + frame.size.width = 44.0; + frame.size.height = 44.0; self.doneIconButton.frame = frame; - CGRect containerRect = (CGRect){0,CGRectGetMaxY(self.doneIconButton.frame),44.0f,CGRectGetMinY(self.cancelIconButton.frame)-CGRectGetMaxY(self.doneIconButton.frame)}; + CGRect containerRect = (CGRect){0,CGRectGetMaxY(self.doneIconButton.frame),44.0,CGRectGetMinY(self.cancelIconButton.frame)-CGRectGetMaxY(self.doneIconButton.frame)}; #if TOCROPTOOLBAR_DEBUG_SHOWING_BUTTONS_CONTAINER_RECT containerView.frame = containerRect; #endif - CGSize buttonSize = (CGSize){44.0f,44.0f}; + CGSize buttonSize = (CGSize){44.0,44.0}; NSMutableArray *buttonsInOrderVertically = [NSMutableArray new]; + + if (!self.clampButtonHidden) { + [buttonsInOrderVertically addObject:self.clampButton]; + } + + if (!self.rotateClockwiseButtonHidden) { + [buttonsInOrderVertically addObject:self.rotateClockwiseButton]; + } + if (!self.rotateCounterclockwiseButtonHidden) { [buttonsInOrderVertically addObject:self.rotateCounterclockwiseButton]; } - if (!self.resetButtonHidden) { - [buttonsInOrderVertically addObject:self.resetButton]; + if (!self.flipHorizontalButtonHidden) { + [buttonsInOrderVertically addObject:self.flipHorizontalButton]; } - - if (!self.clampButtonHidden) { - [buttonsInOrderVertically addObject:self.clampButton]; + + if (!self.flipVerticalButtonHidden) { + [buttonsInOrderVertically addObject:self.flipVerticalButton]; } - if (!self.rotateClockwiseButtonHidden) { - [buttonsInOrderVertically addObject:self.rotateClockwiseButton]; + if (!self.resetButtonHidden) { + [buttonsInOrderVertically addObject:self.resetButton]; } [self layoutToolbarButtons:buttonsInOrderVertically withSameButtonSize:buttonSize inContainerRect:containerRect horizontally:NO]; @@ -301,7 +334,7 @@ - (void)layoutToolbarButtons:(NSArray *)buttons withSameButtonSize:(CGSize)size origin.x += CGRectGetMinX(containerRect); if (@available(iOS 13.0, *)) { UIImage *image = button.imageView.image; - button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, image.baselineOffsetFromBottom, 0); + button.imageEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, image.baselineOffsetFromBottom, 0.0); } } else { origin.y += CGRectGetMinY(containerRect); @@ -334,6 +367,14 @@ - (void)buttonTapped:(id)button self.clampButtonTapped(); return; } + else if (button == self.flipHorizontalButton && self.flipHorizontalButtonTapped) { + self.flipHorizontalButtonTapped(); + return; + } + else if (button == self.flipVerticalButton && self.flipVerticalButtonTapped) { + self.flipVerticalButtonTapped(); + return; + } } - (CGRect)clampButtonFrame @@ -447,7 +488,7 @@ - (void)setCancelButtonColor:(UIColor *)cancelButtonColor { - (void)setDoneButtonColor:(UIColor *)doneButtonColor { // Set the default color when nil is specified if (doneButtonColor == nil) { - doneButtonColor = [UIColor colorWithRed:1.0f green:0.8f blue:0.0f alpha:1.0f]; + doneButtonColor = [UIColor colorWithRed:1.0 green:0.8 blue:0.0 alpha:1.0]; } if (doneButtonColor == _doneButtonColor) { return; } @@ -465,19 +506,7 @@ + (UIImage *)doneImage return [UIImage systemImageNamed:@"checkmark" withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]]; } - - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){17,14}]; - UIImage *doneImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { - UIBezierPath* rectanglePath = UIBezierPath.bezierPath; - [rectanglePath moveToPoint: CGPointMake(1, 7)]; - [rectanglePath addLineToPoint: CGPointMake(6, 12)]; - [rectanglePath addLineToPoint: CGPointMake(16, 1)]; - [UIColor.whiteColor setStroke]; - rectanglePath.lineWidth = 2; - [rectanglePath stroke]; - }]; - - return doneImage; + return [UIImage imageNamed:@"checkmark"]; } + (UIImage *)cancelImage @@ -486,25 +515,7 @@ + (UIImage *)cancelImage return [UIImage systemImageNamed:@"xmark" withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]]; } - - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){16,16}]; - UIImage *cancelImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { - UIBezierPath* bezierPath = UIBezierPath.bezierPath; - [bezierPath moveToPoint: CGPointMake(15, 15)]; - [bezierPath addLineToPoint: CGPointMake(1, 1)]; - [UIColor.whiteColor setStroke]; - bezierPath.lineWidth = 2; - [bezierPath stroke]; - - UIBezierPath* bezier2Path = UIBezierPath.bezierPath; - [bezier2Path moveToPoint: CGPointMake(1, 15)]; - [bezier2Path addLineToPoint: CGPointMake(15, 1)]; - [UIColor.whiteColor setStroke]; - bezier2Path.lineWidth = 2; - [bezier2Path stroke]; - }]; - - return cancelImage; + return [UIImage imageNamed:@"xmark"]; } + (UIImage *)rotateCCWImage @@ -514,31 +525,7 @@ + (UIImage *)rotateCCWImage withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]] imageWithBaselineOffsetFromBottom:4]; } - - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){18,21}]; - UIImage *rotateImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { - UIBezierPath* rectangle2Path = [UIBezierPath bezierPathWithRect: CGRectMake(0, 9, 12, 12)]; - [UIColor.whiteColor setFill]; - [rectangle2Path fill]; - - UIBezierPath* rectangle3Path = UIBezierPath.bezierPath; - [rectangle3Path moveToPoint: CGPointMake(5, 3)]; - [rectangle3Path addLineToPoint: CGPointMake(10, 6)]; - [rectangle3Path addLineToPoint: CGPointMake(10, 0)]; - [rectangle3Path addLineToPoint: CGPointMake(5, 3)]; - [rectangle3Path closePath]; - [UIColor.whiteColor setFill]; - [rectangle3Path fill]; - - UIBezierPath* bezierPath = UIBezierPath.bezierPath; - [bezierPath moveToPoint: CGPointMake(10, 3)]; - [bezierPath addCurveToPoint: CGPointMake(17.5, 11) controlPoint1: CGPointMake(15, 3) controlPoint2: CGPointMake(17.5, 5.91)]; - [UIColor.whiteColor setStroke]; - bezierPath.lineWidth = 1; - [bezierPath stroke]; - }]; - - return rotateImage; + return [UIImage imageNamed:@"rotate.left.fill"]; } + (UIImage *)rotateCWImage @@ -548,17 +535,39 @@ + (UIImage *)rotateCWImage withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]] imageWithBaselineOffsetFromBottom:4]; } + return [UIImage imageNamed:@"rotate.right.fill"]; +} - UIImage *rotateCCWImage = [self.class rotateCCWImage]; - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:rotateCCWImage.size]; - UIImage *rotateCWImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { - CGContextRef context = rendererContext.CGContext; - CGContextTranslateCTM(context, rotateCCWImage.size.width, rotateCCWImage.size.height); - CGContextRotateCTM(context, M_PI); - CGContextDrawImage(context,CGRectMake(0,0,rotateCCWImage.size.width,rotateCCWImage.size.height),rotateCCWImage.CGImage); - }]; ++ (UIImage *)flipHImage +{ + UIImage* image; + if (@available(iOS 14.0, *)) { + image = [UIImage systemImageNamed:@"arrow.left.and.right.righttriangle.left.righttriangle.right.fill" + withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]]; + } else { + return [UIImage imageNamed:@"arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill"]; + } + if (@available(iOS 13.0, *)) { + return [image imageWithBaselineOffsetFromBottom:4]; + } else { + return image; + } +} - return rotateCWImage; ++ (UIImage *)flipVImage +{ + UIImage* image; + if (@available(iOS 14.0, *)) { + image = [UIImage systemImageNamed:@"arrow.up.and.down.righttriangle.up.fill.righttriangle.down.fill" + withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]]; + } else { + image = [UIImage imageNamed:@"arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill"]; + } + if (@available(iOS 13.0, *)) { + return [image imageWithBaselineOffsetFromBottom:4]; + } else { + return image; + } } + (UIImage *)resetImage @@ -566,81 +575,19 @@ + (UIImage *)resetImage if (@available(iOS 13.0, *)) { return [[UIImage systemImageNamed:@"arrow.counterclockwise" withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]] - imageWithBaselineOffsetFromBottom:0];; + imageWithBaselineOffsetFromBottom:1]; } - - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){22,18}]; - UIImage *resetImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { - UIBezierPath* bezier2Path = UIBezierPath.bezierPath; - [bezier2Path moveToPoint: CGPointMake(22, 9)]; - [bezier2Path addCurveToPoint: CGPointMake(13, 18) controlPoint1: CGPointMake(22, 13.97) controlPoint2: CGPointMake(17.97, 18)]; - [bezier2Path addCurveToPoint: CGPointMake(13, 16) controlPoint1: CGPointMake(13, 17.35) controlPoint2: CGPointMake(13, 16.68)]; - [bezier2Path addCurveToPoint: CGPointMake(20, 9) controlPoint1: CGPointMake(16.87, 16) controlPoint2: CGPointMake(20, 12.87)]; - [bezier2Path addCurveToPoint: CGPointMake(13, 2) controlPoint1: CGPointMake(20, 5.13) controlPoint2: CGPointMake(16.87, 2)]; - [bezier2Path addCurveToPoint: CGPointMake(6.55, 6.27) controlPoint1: CGPointMake(10.1, 2) controlPoint2: CGPointMake(7.62, 3.76)]; - [bezier2Path addCurveToPoint: CGPointMake(6, 9) controlPoint1: CGPointMake(6.2, 7.11) controlPoint2: CGPointMake(6, 8.03)]; - [bezier2Path addLineToPoint: CGPointMake(4, 9)]; - [bezier2Path addCurveToPoint: CGPointMake(4.65, 5.63) controlPoint1: CGPointMake(4, 7.81) controlPoint2: CGPointMake(4.23, 6.67)]; - [bezier2Path addCurveToPoint: CGPointMake(7.65, 1.76) controlPoint1: CGPointMake(5.28, 4.08) controlPoint2: CGPointMake(6.32, 2.74)]; - [bezier2Path addCurveToPoint: CGPointMake(13, 0) controlPoint1: CGPointMake(9.15, 0.65) controlPoint2: CGPointMake(11, 0)]; - [bezier2Path addCurveToPoint: CGPointMake(22, 9) controlPoint1: CGPointMake(17.97, 0) controlPoint2: CGPointMake(22, 4.03)]; - [bezier2Path closePath]; - [UIColor.whiteColor setFill]; - [bezier2Path fill]; - - UIBezierPath* polygonPath = UIBezierPath.bezierPath; - [polygonPath moveToPoint: CGPointMake(5, 15)]; - [polygonPath addLineToPoint: CGPointMake(10, 9)]; - [polygonPath addLineToPoint: CGPointMake(0, 9)]; - [polygonPath addLineToPoint: CGPointMake(5, 15)]; - [polygonPath closePath]; - [UIColor.whiteColor setFill]; - [polygonPath fill]; - }]; - - return resetImage; + return [UIImage imageNamed:@"arrow.counterclockwise"]; } + (UIImage *)clampImage { - if (@available(iOS 13.0, *)) { + if (@available(iOS 13.1, *)) { return [[UIImage systemImageNamed:@"aspectratio.fill" withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightSemibold]] imageWithBaselineOffsetFromBottom:0]; } - - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){22,16}]; - UIImage *clampImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { - //// Color Declarations - UIColor* outerBox = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 0.553]; - UIColor* innerBox = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 0.773]; - - //// Rectangle Drawing - UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRect: CGRectMake(0, 3, 13, 13)]; - [UIColor.whiteColor setFill]; - [rectanglePath fill]; - - //// Outer - { - //// Top Drawing - UIBezierPath* topPath = [UIBezierPath bezierPathWithRect: CGRectMake(0, 0, 22, 2)]; - [outerBox setFill]; - [topPath fill]; - - - //// Side Drawing - UIBezierPath* sidePath = [UIBezierPath bezierPathWithRect: CGRectMake(19, 2, 3, 14)]; - [outerBox setFill]; - [sidePath fill]; - } - - //// Rectangle 2 Drawing - UIBezierPath* rectangle2Path = [UIBezierPath bezierPathWithRect: CGRectMake(14, 3, 4, 13)]; - [innerBox setFill]; - [rectangle2Path fill]; - }]; - - return clampImage; + return [UIImage imageNamed:@"aspectratio.fill"]; } #pragma mark - Accessors - diff --git a/Objective-C/TOCropViewController/Views/TOCropView.h b/Objective-C/TOCropViewController/Views/TOCropView.h index be282964..c1ceee19 100755 --- a/Objective-C/TOCropViewController/Views/TOCropView.h +++ b/Objective-C/TOCropViewController/Views/TOCropView.h @@ -73,8 +73,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, weak) id delegate; /** - If false, the user cannot resize the crop box frame using a pan gesture from a corner. - Default vaue is YES. + If NO, the user cannot resize the crop box frame using a pan gesture from a corner. + Default value is YES. */ @property (nonatomic, assign) BOOL cropBoxResizeEnabled; @@ -120,8 +120,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL aspectRatioLockEnabled; /** - If true, a custom aspect ratio is set, and the aspectRatioLockEnabled is set to YES, - the crop box will swap it's dimensions depending on portrait or landscape sized images. + If YES, a custom aspect ratio is set, and the aspectRatioLockEnabled is set to YES, + the crop box will swap its dimensions depending on portrait or landscape sized images. This value also controls whether the dimensions can swap when the image is rotated. Default is NO. @@ -135,7 +135,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL resetAspectRatioEnabled; /** - True when the height of the crop box is bigger than the width + YES when the height of the crop box is bigger than the width */ @property (nonatomic, readonly) BOOL cropBoxAspectRatioIsPortrait; @@ -144,6 +144,16 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) NSInteger angle; +/** + Whether the canvas is flipped horizontally + */ +@property (nonatomic, assign) BOOL isFlippedHorizontally; + +/** + Whether the canvas is flipped vertically + */ +@property (nonatomic, assign) BOOL isFlippedVertically; + /** Hide all of the crop elements for transition animations */ @@ -160,12 +170,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL gridOverlayHidden; ///** -// Paddings of the crop rectangle. Default to 14.0 +// Paddings of the crop rectangle. Defaults to 14.0 // */ @property (nonatomic) CGFloat cropViewPadding; /** - Delay before crop frame is adjusted according new crop area. Default to 0.8 + Delay before crop frame is adjusted according new crop area. Defaults to 0.8 */ @property (nonatomic) NSTimeInterval cropAdjustingDelay; @@ -177,7 +187,7 @@ The minimum croping aspect ratio. If set, user is prevented from setting croppin /** The maximum scale that user can apply to image by pinching to zoom. Small values - are only recomended with aspectRatioLockEnabled set to true. Default to 15.0 + are only recommended with aspectRatioLockEnabled set to YES. Defaults to 15.0 */ @property (nonatomic, assign) CGFloat maximumZoomScale; @@ -275,6 +285,14 @@ The minimum croping aspect ratio. If set, user is prevented from setting croppin */ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwise; +/** + Flips (mirrors) the entire canvas + + @param animated Whether the transition is animated + @param horizontal Whether to flip horizontally (YES) or vertically (NO) + */ +- (void)flipImageAnimated:(BOOL)animated horizontal:(BOOL)horizontal; + /** Animate the grid overlay graphic to be visible */ diff --git a/Objective-C/TOCropViewController/Views/TOCropView.m b/Objective-C/TOCropViewController/Views/TOCropView.m index 8a03c976..16a12d87 100755 --- a/Objective-C/TOCropViewController/Views/TOCropView.m +++ b/Objective-C/TOCropViewController/Views/TOCropView.m @@ -23,13 +23,14 @@ #import "TOCropView.h" #import "TOCropOverlayView.h" #import "TOCropScrollView.h" +#import "UIView+Pixels.h" -#define TOCROPVIEW_BACKGROUND_COLOR [UIColor colorWithWhite:0.12f alpha:1.0f] +#define TOCROPVIEW_BACKGROUND_COLOR [UIColor colorWithWhite:0.12 alpha:1.0] -static const CGFloat kTOCropViewPadding = 14.0f; -static const NSTimeInterval kTOCropTimerDuration = 0.8f; -static const CGFloat kTOCropViewMinimumBoxSize = 42.0f; -static const CGFloat kTOMaximumZoomScale = 15.0f; +static const CGFloat kTOCropViewPadding = 14.0; +static const NSTimeInterval kTOCropTimerDuration = 0.8; +static const CGFloat kTOCropViewMinimumBoxSize = 42.0; +static const CGFloat kTOMaximumZoomScale = 15.0; /* When the user taps down to resize the box, this state is used to determine where they tapped and how to manipulate the box */ @@ -54,6 +55,7 @@ @interface TOCropView () @property (nonatomic, strong) UIImageView *backgroundImageView; /* The main image view, placed within the scroll view */ @property (nonatomic, strong) UIView *backgroundContainerView; /* A view which contains the background image view, to separate its transforms from the scroll view. */ @property (nonatomic, strong, readwrite) UIView *foregroundContainerView; +@property (nonatomic, strong) UIView *foregroundMaskView; @property (nonatomic, strong) UIImageView *foregroundImageView; /* A copy of the background image view, placed over the dimming views */ @property (nonatomic, strong) TOCropScrollView *scrollView; /* The scroll view in charge of panning/zooming the image. */ @property (nonatomic, strong) UIView *overlayView; /* A semi-transparent grey view, overlaid on top of the background image */ @@ -82,7 +84,7 @@ @interface TOCropView () /* View State information */ @property (nonatomic, readonly) CGRect contentBounds; /* Give the current screen real-estate, the frame that the scroll view is allowed to use */ @property (nonatomic, readonly) CGSize imageSize; /* Given the current rotation of the image, the size of the image */ -@property (nonatomic, readonly) BOOL hasAspectRatio; /* True if an aspect ratio was explicitly applied to this crop view */ +@property (nonatomic, readonly) BOOL hasAspectRatio; /* YES if an aspect ratio was explicitly applied to this crop view */ /* 90-degree rotation state data */ @property (nonatomic, assign) CGSize cropBoxLastEditedSize; /* When performing 90-degree rotations, remember what our last manual size was to use that as a base */ @@ -141,7 +143,7 @@ - (void)setup self.applyInitialCroppedImageFrame = NO; self.editing = NO; self.cropBoxResizeEnabled = !circularMode; - self.aspectRatio = circularMode ? (CGSize){1.0f, 1.0f} : CGSizeZero; + self.aspectRatio = circularMode ? (CGSize){1.0, 1.0} : CGSizeZero; self.resetAspectRatioEnabled = !circularMode; self.restoreImageCropFrame = CGRectZero; self.restoreAngle = 0; @@ -183,7 +185,7 @@ - (void)setup //Grey transparent overlay view self.overlayView = [[UIView alloc] initWithFrame:self.bounds]; self.overlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - self.overlayView.backgroundColor = [self.backgroundColor colorWithAlphaComponent:0.35f]; + self.overlayView.backgroundColor = [self.backgroundColor colorWithAlphaComponent:0.35]; self.overlayView.hidden = NO; self.overlayView.userInteractionEnabled = NO; [self addSubview:self.overlayView]; @@ -198,22 +200,27 @@ - (void)setup UIToolbar *toolbar = [[UIToolbar alloc] init]; toolbar.barStyle = UIBarStyleBlack; self.translucencyView = toolbar; - self.translucencyView.frame = CGRectInset(self.bounds, -1.0f, -1.0f); + self.translucencyView.frame = CGRectInset(self.bounds, -1.0, -1.0); } self.translucencyView.hidden = self.translucencyAlwaysHidden; self.translucencyView.userInteractionEnabled = NO; self.translucencyView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self addSubview:self.translucencyView]; - // The forground container that holds the foreground image view + // The foreground container that holds the foreground image view self.foregroundContainerView = [[UIView alloc] initWithFrame:(CGRect){0,0,200,200}]; - self.foregroundContainerView.clipsToBounds = YES; self.foregroundContainerView.userInteractionEnabled = NO; [self addSubview:self.foregroundContainerView]; + self.foregroundMaskView = [[UIView alloc] initWithFrame:self.foregroundContainerView.bounds]; + self.foregroundMaskView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.foregroundMaskView.clipsToBounds = YES; + self.foregroundMaskView.userInteractionEnabled = NO; + [self.foregroundContainerView addSubview:self.foregroundMaskView]; + self.foregroundImageView = [[UIImageView alloc] initWithImage:self.image]; self.foregroundImageView.layer.minificationFilter = kCAFilterTrilinear; - [self.foregroundContainerView addSubview:self.foregroundImageView]; + [self.foregroundMaskView addSubview:self.foregroundImageView]; // Disable colour inversion for the image views if (@available(iOS 11.0, *)) { @@ -282,11 +289,11 @@ - (void)layoutInitialImage CGSize boundsSize = bounds.size; //work out the minimum scale of the object - CGFloat scale = 0.0f; + CGFloat scale = 0.0; // Work out the size of the image to fit into the content bounds scale = MIN(CGRectGetWidth(bounds)/imageSize.width, CGRectGetHeight(bounds)/imageSize.height); - CGSize scaledImageSize = (CGSize){floorf(imageSize.width * scale), floorf(imageSize.height * scale)}; + CGSize scaledImageSize = (CGSize){imageSize.width * scale, imageSize.height * scale}; // If an aspect ratio was pre-applied to the crop view, use that to work out the minimum scale the image needs to be to fit CGSize cropBoxSize = CGSizeZero; @@ -300,7 +307,7 @@ - (void)layoutInitialImage } //Whether aspect ratio, or original, the final image size we'll base the rest of the calculations off - CGSize scaledSize = (CGSize){floorf(imageSize.width * scale), floorf(imageSize.height * scale)}; + CGSize scaledSize = (CGSize){imageSize.width * scale, imageSize.height * scale}; // Configure the scroll view self.scrollView.minimumZoomScale = scale; @@ -309,8 +316,8 @@ - (void)layoutInitialImage //Set the crop box to the size we calculated and align in the middle of the screen CGRect frame = CGRectZero; frame.size = self.hasAspectRatio ? cropBoxSize : scaledSize; - frame.origin.x = floorf(bounds.origin.x + floorf((CGRectGetWidth(bounds) - frame.size.width) * 0.5f)); - frame.origin.y = floorf(bounds.origin.y + floorf((CGRectGetHeight(bounds) - frame.size.height) * 0.5f)); + frame.origin.x = bounds.origin.x + (CGRectGetWidth(bounds) - frame.size.width) * 0.5; + frame.origin.y = bounds.origin.y + (CGRectGetHeight(bounds) - frame.size.height) * 0.5; self.cropBoxFrame = frame; //set the fully zoomed out state initially @@ -319,10 +326,10 @@ - (void)layoutInitialImage // If we ended up with a smaller crop box than the content, line up the content so its center // is in the center of the cropbox - if (frame.size.width < scaledSize.width - FLT_EPSILON || frame.size.height < scaledSize.height - FLT_EPSILON) { + if (frame.size.width < scaledSize.width - DBL_EPSILON || frame.size.height < scaledSize.height - DBL_EPSILON) { CGPoint offset = CGPointZero; - offset.x = -floorf(CGRectGetMidX(bounds) - (scaledSize.width * 0.5f)); - offset.y = -floorf(CGRectGetMidY(bounds) - (scaledSize.height * 0.5f)); + offset.x = -(CGRectGetMidX(bounds) - (scaledSize.width * 0.5)); + offset.y = -(CGRectGetMidY(bounds) - (scaledSize.height * 0.5)); self.scrollView.contentOffset = offset; } @@ -355,10 +362,10 @@ - (void)performRelayoutForRotation self.scrollView.zoomScale *= scale; //Work out the centered, upscaled version of the crop rectangle - cropFrame.size.width = floorf(cropFrame.size.width * scale); - cropFrame.size.height = floorf(cropFrame.size.height * scale); - cropFrame.origin.x = floorf(contentFrame.origin.x + ((contentFrame.size.width - cropFrame.size.width) * 0.5f)); - cropFrame.origin.y = floorf(contentFrame.origin.y + ((contentFrame.size.height - cropFrame.size.height) * 0.5f)); + cropFrame.size.width = cropFrame.size.width * scale; + cropFrame.size.height = cropFrame.size.height * scale; + cropFrame.origin.x = contentFrame.origin.x + ((contentFrame.size.width - cropFrame.size.width) * 0.5); + cropFrame.origin.y = contentFrame.origin.y + ((contentFrame.size.height - cropFrame.size.height) * 0.5); self.cropBoxFrame = cropFrame; [self captureStateForImageRotation]; @@ -380,8 +387,8 @@ - (void)performRelayoutForRotation translatedContentOffset.y = self.scrollView.contentSize.height * normalizedCenter.y; CGPoint offset = CGPointZero; - offset.x = floorf(translatedContentOffset.x - newMidPoint.x); - offset.y = floorf(translatedContentOffset.y - newMidPoint.y); + offset.x = translatedContentOffset.x - newMidPoint.x; + offset.y = translatedContentOffset.y - newMidPoint.y; //Make sure it doesn't overshoot the top left corner of the crop box offset.x = MAX(-self.scrollView.contentInset.left, offset.x); @@ -420,8 +427,8 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point point.y = MAX(contentFrame.origin.y - self.cropViewPadding, point.y); //The delta between where we first tapped, and where our finger is now - CGFloat xDelta = ceilf(point.x - self.panOriginPoint.x); - CGFloat yDelta = ceilf(point.y - self.panOriginPoint.y); + CGFloat xDelta = point.x - self.panOriginPoint.x; + CGFloat yDelta = point.y - self.panOriginPoint.y; //Current aspect ratio of the crop box in case we need to clamp it CGFloat aspectRatio = (originFrame.size.width / originFrame.size.height); @@ -441,7 +448,7 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point xDelta = MAX(xDelta, 0); CGPoint scaleOrigin = (CGPoint){CGRectGetMaxX(originFrame), CGRectGetMidY(originFrame)}; frame.size.height = frame.size.width / aspectRatio; - frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f); + frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5); } CGFloat newWidth = originFrame.size.width - xDelta; CGFloat newHeight = originFrame.size.height; @@ -458,7 +465,7 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point aspectHorizontal = YES; CGPoint scaleOrigin = (CGPoint){CGRectGetMinX(originFrame), CGRectGetMidY(originFrame)}; frame.size.height = frame.size.width / aspectRatio; - frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f); + frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5); frame.size.width = originFrame.size.width + xDelta; frame.size.width = MIN(frame.size.width, contentFrame.size.height * aspectRatio); } @@ -476,7 +483,7 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point aspectVertical = YES; CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMinY(originFrame)}; frame.size.width = frame.size.height * aspectRatio; - frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f); + frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5); frame.size.height = originFrame.size.height + yDelta; frame.size.height = MIN(frame.size.height, contentFrame.size.width / aspectRatio); } @@ -495,7 +502,7 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point yDelta = MAX(0,yDelta); CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMaxY(originFrame)}; frame.size.width = frame.size.height * aspectRatio; - frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f); + frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5); frame.origin.y = originFrame.origin.y + yDelta; frame.size.height = originFrame.size.height - yDelta; } @@ -518,13 +525,13 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point yDelta = MAX(yDelta, 0); CGPoint distance; - distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame)); - distance.y = 1.0f - (yDelta / CGRectGetHeight(originFrame)); + distance.x = 1.0 - (xDelta / CGRectGetWidth(originFrame)); + distance.y = 1.0 - (yDelta / CGRectGetHeight(originFrame)); - CGFloat scale = (distance.x + distance.y) * 0.5f; + CGFloat scale = (distance.x + distance.y) * 0.5; - frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale); - frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale); + frame.size.width = CGRectGetWidth(originFrame) * scale; + frame.size.height = CGRectGetHeight(originFrame) * scale; frame.origin.x = originFrame.origin.x + (CGRectGetWidth(originFrame) - frame.size.width); frame.origin.y = originFrame.origin.y + (CGRectGetHeight(originFrame) - frame.size.height); @@ -553,13 +560,13 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point yDelta = MAX(yDelta, 0); CGPoint distance; - distance.x = 1.0f - ((-xDelta) / CGRectGetWidth(originFrame)); - distance.y = 1.0f - ((yDelta) / CGRectGetHeight(originFrame)); + distance.x = 1.0 - ((-xDelta) / CGRectGetWidth(originFrame)); + distance.y = 1.0 - ((yDelta) / CGRectGetHeight(originFrame)); - CGFloat scale = (distance.x + distance.y) * 0.5f; + CGFloat scale = (distance.x + distance.y) * 0.5; - frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale); - frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale); + frame.size.width = CGRectGetWidth(originFrame) * scale; + frame.size.height = CGRectGetHeight(originFrame) * scale; frame.origin.y = originFrame.origin.y + (CGRectGetHeight(originFrame) - frame.size.height); aspectVertical = YES; @@ -582,13 +589,13 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point case TOCropViewOverlayEdgeBottomLeft: if (self.aspectRatioLockEnabled) { CGPoint distance; - distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame)); - distance.y = 1.0f - (-yDelta / CGRectGetHeight(originFrame)); + distance.x = 1.0 - (xDelta / CGRectGetWidth(originFrame)); + distance.y = 1.0 - (-yDelta / CGRectGetHeight(originFrame)); - CGFloat scale = (distance.x + distance.y) * 0.5f; + CGFloat scale = (distance.x + distance.y) * 0.5; - frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale); - frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale); + frame.size.width = CGRectGetWidth(originFrame) * scale; + frame.size.height = CGRectGetHeight(originFrame) * scale; frame.origin.x = CGRectGetMaxX(originFrame) - frame.size.width; aspectVertical = YES; @@ -612,13 +619,13 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point if (self.aspectRatioLockEnabled) { CGPoint distance; - distance.x = 1.0f - ((-1 * xDelta) / CGRectGetWidth(originFrame)); - distance.y = 1.0f - ((-1 * yDelta) / CGRectGetHeight(originFrame)); + distance.x = 1.0 - ((-1 * xDelta) / CGRectGetWidth(originFrame)); + distance.y = 1.0 - ((-1 * yDelta) / CGRectGetHeight(originFrame)); - CGFloat scale = (distance.x + distance.y) * 0.5f; + CGFloat scale = (distance.x + distance.y) * 0.5; - frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale); - frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale); + frame.size.width = CGRectGetWidth(originFrame) * scale; + frame.size.height = CGRectGetHeight(originFrame) * scale; aspectVertical = YES; aspectHorizontal = YES; @@ -679,12 +686,12 @@ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point frame.origin.y = MIN(frame.origin.y, CGRectGetMaxY(contentFrame) - minSize.height); //Once the box is completely shrunk, clamp its ability to move - if (clampMinFromLeft && frame.size.width <= minSize.width + FLT_EPSILON) { + if (clampMinFromLeft && frame.size.width <= minSize.width + DBL_EPSILON) { frame.origin.x = CGRectGetMaxX(originFrame) - minSize.width; } //Once the box is completely shrunk, clamp its ability to move - if (clampMinFromTop && frame.size.height <= minSize.height + FLT_EPSILON) { + if (clampMinFromTop && frame.size.height <= minSize.height + DBL_EPSILON) { frame.origin.y = CGRectGetMaxY(originFrame) - minSize.height; } @@ -702,12 +709,16 @@ - (void)resetLayoutToDefaultAnimated:(BOOL)animated _aspectRatio = CGSizeZero; } + //Reset any flips + self.isFlippedVertically = NO; + self.isFlippedHorizontally = NO; + if (animated == NO || self.angle != 0) { //Reset all of the rotation transforms _angle = 0; //Set the scroll to 1.0f to reset the transform scale - self.scrollView.zoomScale = 1.0f; + self.scrollView.zoomScale = 1.0; CGRect imageRect = (CGRect){CGPointZero, self.image.size}; @@ -740,8 +751,8 @@ - (void)resetLayoutToDefaultAnimated:(BOOL)animated [self setSimpleRenderMode:YES animated:NO]; //Perform an animation of the image zooming back out to its original size - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:1.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.5 delay:0.0 usingSpringWithDamping:1.0 initialSpringVelocity:1.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ [self layoutInitialImage]; } completion:^(BOOL complete) { [self setSimpleRenderMode:NO animated:YES]; @@ -752,7 +763,7 @@ - (void)resetLayoutToDefaultAnimated:(BOOL)animated - (void)toggleTranslucencyViewVisible:(BOOL)visible { if (self.dynamicBlurEffect == NO) { - self.translucencyView.alpha = visible ? 1.0f : 0.0f; + self.translucencyView.alpha = visible ? 1.0 : 0.0; } else { [(UIVisualEffectView *)self.translucencyView setEffect:visible ? self.translucencyEffect : nil]; @@ -774,21 +785,21 @@ - (void)updateToImageCropFrame:(CGRect)imageCropframe self.scrollView.zoomScale = self.scrollView.minimumZoomScale * scale; CGSize contentSize = self.scrollView.contentSize; - self.scrollView.contentSize = CGSizeMake(floorf(contentSize.width), floorf(contentSize.height)); + self.scrollView.contentSize = CGSizeMake(contentSize.width, contentSize.height); // Work out the size and offset of the upscaled crop box CGRect frame = CGRectZero; - frame.size = (CGSize){floorf(scaledCropSize.width * scale), floorf(scaledCropSize.height * scale)}; + frame.size = (CGSize){scaledCropSize.width * scale, scaledCropSize.height * scale}; //set the crop box CGRect cropBoxFrame = CGRectZero; cropBoxFrame.size = frame.size; - cropBoxFrame.origin.x = floorf(CGRectGetMidX(bounds) - (frame.size.width * 0.5f)); - cropBoxFrame.origin.y = floorf(CGRectGetMidY(bounds) - (frame.size.height * 0.5f)); + cropBoxFrame.origin.x = CGRectGetMidX(bounds) - (frame.size.width * 0.5); + cropBoxFrame.origin.y = CGRectGetMidY(bounds) - (frame.size.height * 0.5); self.cropBoxFrame = cropBoxFrame; - frame.origin.x = ceilf((scaledOffset.x * scale) - self.scrollView.contentInset.left); - frame.origin.y = ceilf((scaledOffset.y * scale) - self.scrollView.contentInset.top); + frame.origin.x = (scaledOffset.x * scale) - self.scrollView.contentInset.left; + frame.origin.y = (scaledOffset.y * scale) - self.scrollView.contentInset.top; self.scrollView.contentOffset = frame.origin; } @@ -828,8 +839,8 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer CGPoint tapPoint = [gestureRecognizer locationInView:self]; CGRect frame = self.gridOverlayView.frame; - CGRect innerFrame = CGRectInset(frame, 22.0f, 22.0f); - CGRect outerFrame = CGRectInset(frame, -22.0f, -22.0f); + CGRect innerFrame = CGRectInset(frame, 22.0, 22.0); + CGRect outerFrame = CGRectInset(frame, -22.0, -22.0); if (CGRectContainsPoint(innerFrame, tapPoint) || !CGRectContainsPoint(outerFrame, tapPoint)) return NO; @@ -872,20 +883,20 @@ - (TOCropViewOverlayEdge)cropEdgeForPoint:(CGPoint)point CGRect frame = self.cropBoxFrame; //account for padding around the box - frame = CGRectInset(frame, -32.0f, -32.0f); + frame = CGRectInset(frame, -32.0, -32.0); //Make sure the corners take priority - CGRect topLeftRect = (CGRect){frame.origin, {64,64}}; + CGRect topLeftRect = (CGRect){frame.origin, {64.0,64.0}}; if (CGRectContainsPoint(topLeftRect, point)) return TOCropViewOverlayEdgeTopLeft; CGRect topRightRect = topLeftRect; - topRightRect.origin.x = CGRectGetMaxX(frame) - 64.0f; + topRightRect.origin.x = CGRectGetMaxX(frame) - 64.0; if (CGRectContainsPoint(topRightRect, point)) return TOCropViewOverlayEdgeTopRight; CGRect bottomLeftRect = topLeftRect; - bottomLeftRect.origin.y = CGRectGetMaxY(frame) - 64.0f; + bottomLeftRect.origin.y = CGRectGetMaxY(frame) - 64.0; if (CGRectContainsPoint(bottomLeftRect, point)) return TOCropViewOverlayEdgeBottomLeft; @@ -895,21 +906,21 @@ - (TOCropViewOverlayEdge)cropEdgeForPoint:(CGPoint)point return TOCropViewOverlayEdgeBottomRight; //Check for edges - CGRect topRect = (CGRect){frame.origin, {CGRectGetWidth(frame), 64.0f}}; + CGRect topRect = (CGRect){frame.origin, {CGRectGetWidth(frame), 64.0}}; if (CGRectContainsPoint(topRect, point)) return TOCropViewOverlayEdgeTop; CGRect bottomRect = topRect; - bottomRect.origin.y = CGRectGetMaxY(frame) - 64.0f; + bottomRect.origin.y = CGRectGetMaxY(frame) - 64.0; if (CGRectContainsPoint(bottomRect, point)) return TOCropViewOverlayEdgeBottom; - CGRect leftRect = (CGRect){frame.origin, {64.0f, CGRectGetHeight(frame)}}; + CGRect leftRect = (CGRect){frame.origin, {64.0, CGRectGetHeight(frame)}}; if (CGRectContainsPoint(leftRect, point)) return TOCropViewOverlayEdgeLeft; CGRect rightRect = leftRect; - rightRect.origin.x = CGRectGetMaxX(frame) - 64.0f; + rightRect.origin.x = CGRectGetMaxX(frame) - 64.0; if (CGRectContainsPoint(rightRect, point)) return TOCropViewOverlayEdgeRight; @@ -975,29 +986,29 @@ - (void)setCropBoxFrame:(CGRect)cropBoxFrame // Upon init, sometimes the box size is still 0 (or NaN), which can result in CALayer issues CGSize frameSize = cropBoxFrame.size; - if (frameSize.width < FLT_EPSILON || frameSize.height < FLT_EPSILON) { return; } + if (frameSize.width < DBL_EPSILON || frameSize.height < DBL_EPSILON) { return; } if (isnan(frameSize.width) || isnan(frameSize.height)) { return; } //clamp the cropping region to the inset boundaries of the screen CGRect contentFrame = self.contentBounds; - CGFloat xOrigin = ceilf(contentFrame.origin.x); + CGFloat xOrigin = contentFrame.origin.x; CGFloat xDelta = cropBoxFrame.origin.x - xOrigin; - cropBoxFrame.origin.x = floorf(MAX(cropBoxFrame.origin.x, xOrigin)); - if (xDelta < -FLT_EPSILON) //If we clamp the x value, ensure we compensate for the subsequent delta generated in the width (Or else, the box will keep growing) + cropBoxFrame.origin.x = MAX(cropBoxFrame.origin.x, xOrigin); + if (xDelta < -DBL_EPSILON) //If we clamp the x value, ensure we compensate for the subsequent delta generated in the width (Or else, the box will keep growing) cropBoxFrame.size.width += xDelta; - CGFloat yOrigin = ceilf(contentFrame.origin.y); + CGFloat yOrigin = contentFrame.origin.y; CGFloat yDelta = cropBoxFrame.origin.y - yOrigin; - cropBoxFrame.origin.y = floorf(MAX(cropBoxFrame.origin.y, yOrigin)); - if (yDelta < -FLT_EPSILON) + cropBoxFrame.origin.y = MAX(cropBoxFrame.origin.y, yOrigin); + if (yDelta < -DBL_EPSILON) cropBoxFrame.size.height += yDelta; //given the clamped X/Y values, make sure we can't extend the crop box beyond the edge of the screen in the current state CGFloat maxWidth = (contentFrame.size.width + contentFrame.origin.x) - cropBoxFrame.origin.x; - cropBoxFrame.size.width = floorf(MIN(cropBoxFrame.size.width, maxWidth)); + cropBoxFrame.size.width = MIN(cropBoxFrame.size.width, maxWidth); CGFloat maxHeight = (contentFrame.size.height + contentFrame.origin.y) - cropBoxFrame.origin.y; - cropBoxFrame.size.height = floorf(MIN(cropBoxFrame.size.height, maxHeight)); + cropBoxFrame.size.height = MIN(cropBoxFrame.size.height, maxHeight); //Make sure we can't make the crop box too small cropBoxFrame.size.width = MAX(cropBoxFrame.size.width, kTOCropViewMinimumBoxSize); @@ -1005,13 +1016,14 @@ - (void)setCropBoxFrame:(CGRect)cropBoxFrame _cropBoxFrame = cropBoxFrame; - self.foregroundContainerView.frame = _cropBoxFrame; //set the clipping view to match the new rect - self.gridOverlayView.frame = _cropBoxFrame; //set the new overlay view to match the same region + CGRect pixelRoundedCropBoxFrame = [self CGRectIntegralRetina:cropBoxFrame]; + self.foregroundContainerView.frame = pixelRoundedCropBoxFrame; //set the clipping view to match the new rect + self.gridOverlayView.frame = pixelRoundedCropBoxFrame; //set the new overlay view to match the same region // If the mask layer is present, adjust its transform to fit the new container view size if (self.croppingStyle == TOCropViewCroppingStyleCircular) { - CGFloat halfWidth = self.foregroundContainerView.frame.size.width * 0.5f; - self.foregroundContainerView.layer.cornerRadius = halfWidth; + CGFloat halfWidth = self.foregroundMaskView.frame.size.width * 0.5; + self.foregroundMaskView.layer.cornerRadius = halfWidth; } //reset the scroll view insets to match the region of the new crop rect @@ -1025,12 +1037,6 @@ - (void)setCropBoxFrame:(CGRect)cropBoxFrame CGFloat scale = MAX(cropBoxFrame.size.height/imageSize.height, cropBoxFrame.size.width/imageSize.width); self.scrollView.minimumZoomScale = scale; - //make sure content isn't smaller than the crop box - CGSize size = self.scrollView.contentSize; - size.width = floorf(size.width); - size.height = floorf(size.height); - self.scrollView.contentSize = size; - //IMPORTANT: Force the scroll view to update its content after changing the zoom scale self.scrollView.zoomScale = self.scrollView.zoomScale; @@ -1065,21 +1071,21 @@ - (CGRect)imageCropFrame CGRect frame = CGRectZero; // Calculate the normalized origin - frame.origin.x = floorf((floorf(contentOffset.x) + edgeInsets.left) * (imageSize.width / contentSize.width)); + frame.origin.x = (contentOffset.x + edgeInsets.left) * (imageSize.width / contentSize.width); frame.origin.x = MAX(0, frame.origin.x); - frame.origin.y = floorf((floorf(contentOffset.y) + edgeInsets.top) * (imageSize.height / contentSize.height)); + frame.origin.y = (contentOffset.y + edgeInsets.top) * (imageSize.height / contentSize.height); frame.origin.y = MAX(0, frame.origin.y); // Calculate the normalized width - frame.size.width = ceilf(cropBoxFrame.size.width * scale); + frame.size.width = cropBoxFrame.size.width * scale; frame.size.width = MIN(imageSize.width, frame.size.width); // Calculate normalized height - if (floor(cropBoxFrame.size.width) == floor(cropBoxFrame.size.height)) { + if ([self pixelCountOf:cropBoxFrame.size.width equals:cropBoxFrame.size.height]) { frame.size.height = frame.size.width; } else { - frame.size.height = ceilf(cropBoxFrame.size.height * scale); + frame.size.height = cropBoxFrame.size.height * scale; } frame.size.height = MIN(imageSize.height, frame.size.height); @@ -1108,7 +1114,7 @@ - (void)setCroppingViewsHidden:(BOOL)hidden animated:(BOOL)animated _croppingViewsHidden = hidden; - CGFloat alpha = hidden ? 0.0f : 1.0f; + CGFloat alpha = hidden ? 0.0 : 1.0; if (animated == NO) { self.backgroundImageView.alpha = alpha; @@ -1123,7 +1129,7 @@ - (void)setCroppingViewsHidden:(BOOL)hidden animated:(BOOL)animated self.foregroundContainerView.alpha = alpha; self.backgroundImageView.alpha = alpha; - [UIView animateWithDuration:0.4f animations:^{ + [UIView animateWithDuration:0.4 animations:^{ [self toggleTranslucencyViewVisible:!hidden]; self.gridOverlayView.alpha = alpha; }]; @@ -1136,12 +1142,12 @@ - (void)setBackgroundImageViewHidden:(BOOL)hidden animated:(BOOL)animated return; } - CGFloat beforeAlpha = hidden ? 1.0f : 0.0f; - CGFloat toAlpha = hidden ? 0.0f : 1.0f; + CGFloat beforeAlpha = hidden ? 1.0 : 0.0; + CGFloat toAlpha = hidden ? 0.0 : 1.0; self.backgroundImageView.hidden = NO; self.backgroundImageView.alpha = beforeAlpha; - [UIView animateWithDuration:0.5f animations:^{ + [UIView animateWithDuration:0.5 animations:^{ self.backgroundImageView.alpha = toAlpha; }completion:^(BOOL complete) { if (hidden) { @@ -1172,10 +1178,10 @@ - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden animated:(BOOL)animated { _gridOverlayHidden = gridOverlayHidden; - self.gridOverlayView.alpha = gridOverlayHidden ? 1.0f : 0.0f; + self.gridOverlayView.alpha = gridOverlayHidden ? 1.0 : 0.0; - [UIView animateWithDuration:0.4f animations:^{ - self.gridOverlayView.alpha = gridOverlayHidden ? 0.0f : 1.0f; + [UIView animateWithDuration:0.4 animations:^{ + self.gridOverlayView.alpha = gridOverlayHidden ? 0.0 : 1.0; }]; } @@ -1208,13 +1214,14 @@ - (void)setCanBeReset:(BOOL)canReset - (void)setAngle:(NSInteger)angle { + // Round to the nearest 90 degrees and sanitize + angle = round(angle/90.0) * 90; + angle %= 360; + //The initial layout would not have been performed yet. //Save the value and it will be applied when it has NSInteger newAngle = angle; - if (angle % 90 != 0) { - newAngle = 0; - } - + if (!self.initialSetupPerformed) { self.restoreAngle = newAngle; return; @@ -1264,8 +1271,8 @@ - (void)setEditing:(BOOL)editing resetCropBox:(BOOL)resetCropbox animated:(BOOL) return; } - CGFloat duration = editing ? 0.05f : 0.35f; - CGFloat delay = editing? 0.0f : 0.35f; + CGFloat duration = editing ? 0.05 : 0.35; + CGFloat delay = editing? 0.0 : 0.35; if (self.croppingStyle == TOCropViewCroppingStyleCircular) { delay = 0.0f; @@ -1285,7 +1292,7 @@ - (void)moveCroppedContentToCenterAnimated:(BOOL)animated CGRect cropFrame = self.cropBoxFrame; // Ensure we only proceed after the crop frame has been setup for the first time - if (cropFrame.size.width < FLT_EPSILON || cropFrame.size.height < FLT_EPSILON) { + if (cropFrame.size.width < DBL_EPSILON || cropFrame.size.height < DBL_EPSILON) { return; } @@ -1295,10 +1302,10 @@ - (void)moveCroppedContentToCenterAnimated:(BOOL)animated CGPoint focusPoint = (CGPoint){CGRectGetMidX(cropFrame), CGRectGetMidY(cropFrame)}; CGPoint midPoint = (CGPoint){CGRectGetMidX(contentRect), CGRectGetMidY(contentRect)}; - cropFrame.size.width = ceilf(cropFrame.size.width * scale); - cropFrame.size.height = ceilf(cropFrame.size.height * scale); - cropFrame.origin.x = contentRect.origin.x + ceilf((contentRect.size.width - cropFrame.size.width) * 0.5f); - cropFrame.origin.y = contentRect.origin.y + ceilf((contentRect.size.height - cropFrame.size.height) * 0.5f); + cropFrame.size.width = cropFrame.size.width * scale; + cropFrame.size.height = cropFrame.size.height * scale; + cropFrame.origin.x = contentRect.origin.x + (contentRect.size.width - cropFrame.size.width) * 0.5; + cropFrame.origin.y = contentRect.origin.y + (contentRect.size.height - cropFrame.size.height) * 0.5; //Work out the point on the scroll content that the focusPoint is aiming at CGPoint contentTargetPoint = CGPointZero; @@ -1330,14 +1337,14 @@ - (void)moveCroppedContentToCenterAnimated:(BOOL)animated // in order for the crop view to resize itself during iPad split screen events. // On the first run, even though scale is exactly 1.0f, performing this multiplication introduces // a floating point noise that zooms the image in by about 5 pixels. This fixes that issue. - if (scale < 1.0f - FLT_EPSILON || scale > 1.0f + FLT_EPSILON) { + if (scale < 1.0 - DBL_EPSILON || scale > 1.0 + DBL_EPSILON) { strongSelf.scrollView.zoomScale *= scale; strongSelf.scrollView.zoomScale = MIN(strongSelf.scrollView.maximumZoomScale, strongSelf.scrollView.zoomScale); } - // If it turns out the zoom operation would have exceeded the minizum zoom scale, don't apply + // If it turns out the zoom operation would have exceeded the minimum zoom scale, don't apply // the content offset - if (strongSelf.scrollView.zoomScale < strongSelf.scrollView.maximumZoomScale - FLT_EPSILON) { + if (strongSelf.scrollView.zoomScale < strongSelf.scrollView.maximumZoomScale - DBL_EPSILON) { offset.x = MIN(-CGRectGetMaxX(cropFrame)+strongSelf.scrollView.contentSize.width, offset.x); offset.y = MIN(-CGRectGetMaxY(cropFrame)+strongSelf.scrollView.contentSize.height, offset.y); strongSelf.scrollView.contentOffset = offset; @@ -1358,11 +1365,11 @@ - (void)moveCroppedContentToCenterAnimated:(BOOL)animated [self matchForegroundToBackground]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [UIView animateWithDuration:0.5f - delay:0.0f - usingSpringWithDamping:1.0f - initialSpringVelocity:1.0f + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.5 + delay:0.0 + usingSpringWithDamping:1.0 + initialSpringVelocity:1.0 options:UIViewAnimationOptionBeginFromCurrentState animations:translateBlock completion:nil]; @@ -1384,7 +1391,7 @@ - (void)setSimpleRenderMode:(BOOL)simpleMode animated:(BOOL)animated return; } - [UIView animateWithDuration:0.25f animations:^{ + [UIView animateWithDuration:0.25 animations:^{ [self toggleTranslucencyViewVisible:!simpleMode]; }]; } @@ -1406,7 +1413,7 @@ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated BOOL zoomOut = NO; // Passing in an empty size will revert back to the image aspect ratio - if (aspectRatio.width < FLT_EPSILON && aspectRatio.height < FLT_EPSILON) { + if (aspectRatio.width < DBL_EPSILON && aspectRatio.height < DBL_EPSILON) { aspectRatio = (CGSize){self.imageSize.width, self.imageSize.height}; zoomOut = YES; // Prevent from steadily zooming in when cycling between alternate aspectRatios and original } @@ -1416,18 +1423,18 @@ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated CGPoint offset = self.scrollView.contentOffset; BOOL cropBoxIsPortrait = NO; - if ((NSInteger)aspectRatio.width == 1 && (NSInteger)aspectRatio.height == 1) + if (aspectRatio.width == aspectRatio.height) cropBoxIsPortrait = self.image.size.width > self.image.size.height; else cropBoxIsPortrait = aspectRatio.width < aspectRatio.height; if (cropBoxIsPortrait) { - CGFloat newWidth = floorf(cropBoxFrame.size.height * (aspectRatio.width/aspectRatio.height)); + CGFloat newWidth = cropBoxFrame.size.height * (aspectRatio.width/aspectRatio.height); CGFloat delta = cropBoxFrame.size.width - newWidth; cropBoxFrame.size.width = newWidth; - offset.x += (delta * 0.5f); + offset.x += (delta * 0.5); - if (delta < FLT_EPSILON) { + if (delta < DBL_EPSILON) { cropBoxFrame.origin.x = self.contentBounds.origin.x; //set to 0 to avoid accidental clamping by the crop frame sanitizer } @@ -1443,7 +1450,7 @@ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated cropBoxFrame.size.height = newHeight; // Offset the Y position so it stays in the middle - offset.y += (delta * 0.5f); + offset.y += (delta * 0.5); // Clamp the width to the bounds width cropBoxFrame.size.width = boundsWidth; @@ -1451,12 +1458,12 @@ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated } } else { - CGFloat newHeight = floorf(cropBoxFrame.size.width * (aspectRatio.height/aspectRatio.width)); + CGFloat newHeight = cropBoxFrame.size.width * (aspectRatio.height/aspectRatio.width); CGFloat delta = cropBoxFrame.size.height - newHeight; cropBoxFrame.size.height = newHeight; - offset.y += (delta * 0.5f); + offset.y += (delta * 0.5); - if (delta < FLT_EPSILON) { + if (delta < DBL_EPSILON) { cropBoxFrame.origin.y = self.contentBounds.origin.y; } @@ -1472,7 +1479,7 @@ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated cropBoxFrame.size.width = newWidth; // Offset the Y position so it stays in the middle - offset.x += (delta * 0.5f); + offset.x += (delta * 0.5); // Clamp the width to the bounds height cropBoxFrame.size.height = boundsHeight; @@ -1500,10 +1507,10 @@ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated return; } - [UIView animateWithDuration:0.5f + [UIView animateWithDuration:0.5 delay:0.0 - usingSpringWithDamping:1.0f - initialSpringVelocity:0.7f + usingSpringWithDamping:1.0 + initialSpringVelocity:0.7 options:UIViewAnimationOptionBeginFromCurrentState animations:translateBlock completion:nil]; @@ -1529,6 +1536,9 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis [self captureStateForImageRotation]; } + //When canvas is flipped either way (but not both), change rotation direction to compensate + clockwise ^= self.isFlippedHorizontally ^ self.isFlippedVertically; + //Work out the new angle, and wrap around once we exceed 360s NSInteger newAngle = self.angle; newAngle = clockwise ? newAngle + 90 : newAngle - 90; @@ -1539,7 +1549,7 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis _angle = newAngle; //Convert the new angle to radians - CGFloat angleInRadians = 0.0f; + CGFloat angleInRadians = 0.0; switch (newAngle) { case 90: angleInRadians = M_PI_2; break; case -90: angleInRadians = -M_PI_2; break; @@ -1571,15 +1581,15 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis self.scrollView.zoomScale = self.cropBoxLastEditedZoomScale; } else { - newCropFrame.size = (CGSize){floorf(self.cropBoxFrame.size.height * scale), floorf(self.cropBoxFrame.size.width * scale)}; + newCropFrame.size = (CGSize){self.cropBoxFrame.size.height * scale, self.cropBoxFrame.size.width * scale}; //Re-adjust the scrolling dimensions of the scroll view to match the new size self.scrollView.minimumZoomScale *= scale; self.scrollView.zoomScale *= scale; } - newCropFrame.origin.x = floorf(CGRectGetMidX(contentBounds) - (newCropFrame.size.width * 0.5f)); - newCropFrame.origin.y = floorf(CGRectGetMidY(contentBounds) - (newCropFrame.size.height * 0.5f)); + newCropFrame.origin.x = CGRectGetMidX(contentBounds) - (newCropFrame.size.width * 0.5); + newCropFrame.origin.y = CGRectGetMidY(contentBounds) - (newCropFrame.size.height * 0.5); //If we're animated, generate a snapshot view that we'll animate in place of the real view UIView *snapshotView = nil; @@ -1602,7 +1612,7 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis //Flip the content size of the scroll view to match the rotated bounds self.scrollView.contentSize = self.backgroundContainerView.frame.size; - + //assign the new crop box frame and re-adjust the content to fill it self.cropBoxFrame = newCropFrame; [self moveCroppedContentToCenterAnimated:NO]; @@ -1625,8 +1635,8 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis //reapply the translated scroll offset to the scroll view CGPoint midPoint = {CGRectGetMidX(newCropFrame), CGRectGetMidY(newCropFrame)}; CGPoint offset = CGPointZero; - offset.x = floorf(-midPoint.x + cropTargetPoint.x); - offset.y = floorf(-midPoint.y + cropTargetPoint.y); + offset.x = -midPoint.x + cropTargetPoint.x; + offset.y = -midPoint.y + cropTargetPoint.y; offset.x = MAX(-self.scrollView.contentInset.left, offset.x); offset.y = MAX(-self.scrollView.contentInset.top, offset.y); offset.x = MIN(self.scrollView.contentSize.width - (newCropFrame.size.width - self.scrollView.contentInset.right), offset.x); @@ -1634,10 +1644,11 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis //if the scroll view's new scale is 1 and the new offset is equal to the old, will not trigger the delegate 'scrollViewDidScroll:' //so we should call the method manually to update the foregroundImageView's frame - if (offset.x == self.scrollView.contentOffset.x && offset.y == self.scrollView.contentOffset.y && scale == 1) { + if (offset.x == self.scrollView.contentOffset.x && offset.y == self.scrollView.contentOffset.y && scale == 1.0) { [self matchForegroundToBackground]; } self.scrollView.contentOffset = offset; + [self moveCroppedContentToCenterAnimated:NO]; //Prevents uncropped rotation sometimes causing a black line on the edge. //If we're animated, play an animation of the snapshot view rotating, //then fade it out over the live content @@ -1650,7 +1661,7 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis self.translucencyView.hidden = YES; self.gridOverlayView.hidden = YES; - [UIView animateWithDuration:0.45f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.8f options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + [UIView animateWithDuration:0.45 delay:0.0 usingSpringWithDamping:1.0 initialSpringVelocity:0.8 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ CGAffineTransform transform = CGAffineTransformRotate(CGAffineTransformIdentity, clockwise ? M_PI_2 : -M_PI_2); transform = CGAffineTransformScale(transform, scale, scale); snapshotView.transform = transform; @@ -1660,15 +1671,15 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis self.translucencyView.hidden = self.translucencyAlwaysHidden; self.gridOverlayView.hidden = NO; - self.backgroundContainerView.alpha = 0.0f; - self.gridOverlayView.alpha = 0.0f; + self.backgroundContainerView.alpha = 0.0; + self.gridOverlayView.alpha = 0.0; - self.translucencyView.alpha = 1.0f; + self.translucencyView.alpha = 1.0; - [UIView animateWithDuration:0.45f animations:^{ - snapshotView.alpha = 0.0f; - self.backgroundContainerView.alpha = 1.0f; - self.gridOverlayView.alpha = 1.0f; + [UIView animateWithDuration:0.45 animations:^{ + snapshotView.alpha = 0.0; + self.backgroundContainerView.alpha = 1.0; + self.gridOverlayView.alpha = 1.0; } completion:^(BOOL complete) { self.rotateAnimationInProgress = NO; [snapshotView removeFromSuperview]; @@ -1690,6 +1701,59 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis [self checkForCanReset]; } +- (void)flipImageAnimated:(BOOL)animated horizontal:(BOOL)horizontal +{ + if (self.rotateAnimationInProgress) + return; + + //Stop any inertia first + [self.scrollView setContentOffset:self.scrollView.contentOffset animated:NO]; + + [self.resetTimer invalidate]; + [self setEditing:NO resetCropBox:NO animated:NO]; + + //Avoid setter by setting ivar directly, setter is triggered later + _isFlippedHorizontally ^= horizontal; + _isFlippedVertically ^= !horizontal; + [self checkForCanReset]; + + if (!animated) { + self.isFlippedHorizontally = self.isFlippedHorizontally; //Setter triggers canvas transform + } else { + self.rotateAnimationInProgress = YES; + self.backgroundContainerView.hidden = YES; + self.translucencyView.hidden = YES; + self.gridOverlayView.hidden = YES; + self.overlayView.hidden = YES; + + //first animate the flip of the foregroundContainerView + self.foregroundContainerView.transform = CGAffineTransformScale(self.foregroundContainerView.transform, horizontal ? -1 : 1, horizontal ? 1 : -1); + [UIView transitionWithView: self.foregroundContainerView + duration: 0.45 + options: horizontal ? UIViewAnimationOptionTransitionFlipFromRight : UIViewAnimationOptionTransitionFlipFromBottom + animations: nil + completion:^(BOOL finished) { + + //once flipped unflip it and flip the entire canvas instead + self.foregroundContainerView.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1); + self.isFlippedHorizontally = self.isFlippedHorizontally; //Setter triggers canvas transform + + self.backgroundContainerView.hidden = NO; + self.translucencyView.hidden = self.translucencyAlwaysHidden; + self.gridOverlayView.hidden = NO; + self.overlayView.hidden = NO; + self.backgroundContainerView.alpha = 0.0; + self.gridOverlayView.alpha = 0.0; + [UIView animateWithDuration: 0.45 animations:^{ + self.backgroundContainerView.alpha = 1.0; + self.gridOverlayView.alpha = 1.0; + } completion:^(BOOL complete) { + self.rotateAnimationInProgress = NO; + }]; + }]; + } +} + - (void)captureStateForImageRotation { self.cropBoxLastEditedSize = self.cropBoxFrame.size; @@ -1705,16 +1769,20 @@ - (void)checkForCanReset if (self.angle != 0) { //Image has been rotated canReset = YES; } - else if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale + FLT_EPSILON) { //image has been zoomed in + else if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale + DBL_EPSILON) { //image has been zoomed in canReset = YES; } - else if ((NSInteger)floorf(self.cropBoxFrame.size.width) != (NSInteger)floorf(self.originalCropBoxSize.width) || - (NSInteger)floorf(self.cropBoxFrame.size.height) != (NSInteger)floorf(self.originalCropBoxSize.height)) + else if (![self pixelCountOf:self.cropBoxFrame.size.width equals:self.originalCropBoxSize.width] || + ![self pixelCountOf:self.cropBoxFrame.size.height equals:self.originalCropBoxSize.height]) { //crop box has been changed canReset = YES; } - else if ((NSInteger)floorf(self.scrollView.contentOffset.x) != (NSInteger)floorf(self.originalContentOffset.x) || - (NSInteger)floorf(self.scrollView.contentOffset.y) != (NSInteger)floorf(self.originalContentOffset.y)) + else if (![self pixelCountOf:self.scrollView.contentOffset.x equals:self.originalContentOffset.x] || + ![self pixelCountOf:self.scrollView.contentOffset.y equals:self.originalContentOffset.y]) + { + canReset = YES; + } + else if (self.isFlippedHorizontally || self.isFlippedVertically) { canReset = YES; } @@ -1722,14 +1790,22 @@ - (void)checkForCanReset self.canBeReset = canReset; } -#pragma mark - Convienience Methods - +#pragma mark - Convenience Methods +- (void)setCropRegionInsets:(UIEdgeInsets)cropRegionInsets +{ + _cropRegionInsets = UIEdgeInsetsMake(self.isFlippedVertically ? cropRegionInsets.bottom : cropRegionInsets.top, + self.isFlippedHorizontally ? cropRegionInsets.right : cropRegionInsets.left, + self.isFlippedVertically ? cropRegionInsets.top : cropRegionInsets.bottom, + self.isFlippedHorizontally ? cropRegionInsets.left : cropRegionInsets.right); +} + - (CGRect)contentBounds { CGRect contentRect = CGRectZero; contentRect.origin.x = self.cropViewPadding + self.cropRegionInsets.left; contentRect.origin.y = self.cropViewPadding + self.cropRegionInsets.top; - contentRect.size.width = CGRectGetWidth(self.bounds) - ((self.cropViewPadding * 2) + self.cropRegionInsets.left + self.cropRegionInsets.right); - contentRect.size.height = CGRectGetHeight(self.bounds) - ((self.cropViewPadding * 2) + self.cropRegionInsets.top + self.cropRegionInsets.bottom); + contentRect.size.width = CGRectGetWidth(self.bounds) - ((self.cropViewPadding * 2.0) + self.cropRegionInsets.left + self.cropRegionInsets.right); + contentRect.size.height = CGRectGetHeight(self.bounds) - ((self.cropViewPadding * 2.0) + self.cropRegionInsets.top + self.cropRegionInsets.bottom); return contentRect; } @@ -1743,7 +1819,17 @@ - (CGSize)imageSize - (BOOL)hasAspectRatio { - return (self.aspectRatio.width > FLT_EPSILON && self.aspectRatio.height > FLT_EPSILON); + return (self.aspectRatio.width > DBL_EPSILON && self.aspectRatio.height > DBL_EPSILON); +} + +- (void)setIsFlippedHorizontally:(BOOL)isFlippedHorizontally { + _isFlippedHorizontally = isFlippedHorizontally; + self.transform = CGAffineTransformScale(CGAffineTransformIdentity, self.isFlippedHorizontally ? -1 : 1, self.isFlippedVertically ? -1 : 1); +} + +- (void)setIsFlippedVertically:(BOOL)isFlippedVertically { + _isFlippedVertically = isFlippedVertically; + self.transform = CGAffineTransformScale(CGAffineTransformIdentity, self.isFlippedHorizontally ? -1 : 1, self.isFlippedVertically ? -1 : 1); } @end diff --git a/Objective-C/TOCropViewController/include/UIView+Pixels.h b/Objective-C/TOCropViewController/include/UIView+Pixels.h new file mode 120000 index 00000000..be249e0b --- /dev/null +++ b/Objective-C/TOCropViewController/include/UIView+Pixels.h @@ -0,0 +1 @@ +../Categories/UIView+Pixels.h \ No newline at end of file diff --git a/Objective-C/TOCropViewControllerExample/ViewController.m b/Objective-C/TOCropViewControllerExample/ViewController.m index 3ff95c28..d349fcab 100644 --- a/Objective-C/TOCropViewControllerExample/ViewController.m +++ b/Objective-C/TOCropViewControllerExample/ViewController.m @@ -17,6 +17,7 @@ @interface ViewController () (Void))? { + public var onDidCropImageToRect: ((CGRect, NSInteger, Bool) -> (Void))? { set { toCropViewController.onDidCropImageToRect = newValue } get { return toCropViewController.onDidCropImageToRect } } /** Called when the user has committed the crop action, and provides - both the cropped image with crop co-ordinates. + both the cropped image with crop coordinates. @param image The newly cropped image. - @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) + @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ - public var onDidCropToRect: ((UIImage, CGRect, NSInteger) -> (Void))? { + public var onDidCropToRect: ((UIImage, CGRect, NSInteger, Bool) -> (Void))? { set { toCropViewController.onDidCropToRect = newValue } get { return toCropViewController.onDidCropToRect } } /** If the cropping style is set to circular, this block will return a circle-cropped version of the selected - image, as well as it's cropping co-ordinates + image, as well as its cropping coordinates @param image The newly cropped image, clipped to a circle shape - @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) + @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local coordinate space) @param angle The angle of the image when it was cropped + @param flipped Whether the image was flipped (mirrored) when it was cropped */ - public var onDidCropToCircleImage: ((UIImage, CGRect, NSInteger) -> (Void))? { + public var onDidCropToCircleImage: ((UIImage, CGRect, NSInteger, Bool) -> (Void))? { set { toCropViewController.onDidCropToCircleImage = newValue } get { return toCropViewController.onDidCropToCircleImage } } @@ -611,16 +675,17 @@ open class CropViewController: UIViewController, TOCropViewControllerDelegate { @param fromView A view that's frame will be used as the origin for this animation. Optional if `fromFrame` has a value. @param fromFrame In the screen's coordinate space, the frame from which the image should animate from. @param angle The rotation angle in which the image was rotated when it was originally cropped. + @param flipped Whether the image was flipped (mirrored) when it was originally cropped. @param toFrame In the image's coordinate space, the previous crop frame that created the previous crop @param setup A block that is called just before the transition starts. Recommended for hiding any necessary image views. @param completion A block that is called once the transition animation is completed. */ public func presentAnimatedFrom(_ viewController: UIViewController, fromImage image: UIImage?, - fromView: UIView?, fromFrame: CGRect, angle: Int, toImageFrame toFrame: CGRect, + fromView: UIView?, fromFrame: CGRect, angle: Int, flipped: Bool, toImageFrame toFrame: CGRect, setup: (() -> (Void))?, completion:(() -> (Void))?) { toCropViewController.presentAnimatedFrom(viewController, fromImage: image, fromView: fromView, - fromFrame: fromFrame, angle: angle, toFrame: toFrame, + fromFrame: fromFrame, angle: angle, flipped: flipped, toFrame: toFrame, setup: setup, completion: completion) } @@ -678,27 +743,48 @@ extension CropViewController { onDidFinishCancelled = nil return } - + if delegate.responds(to: #selector((any CropViewControllerDelegate).cropViewController(_:didCropImageToRect:angle:))) { - self.onDidCropImageToRect = {[weak self] rect, angle in + crashWhenFresh() + self.onDidCropImageToRect = {[weak self] rect, angle, flipped in guard let strongSelf = self else { return } delegate.cropViewController!(strongSelf, didCropImageToRect: rect, angle: angle) } } + if delegate.responds(to: #selector((any CropViewControllerDelegate).cropViewController(_:didCropImageToRect:angle:flipped:))) { + self.onDidCropImageToRect = {[weak self] rect, angle, flipped in + guard let strongSelf = self else { return } + delegate.cropViewController!(strongSelf, didCropImageToRect: rect, angle: angle, flipped: flipped) + } + } if delegate.responds(to: #selector((any CropViewControllerDelegate).cropViewController(_:didCropToImage:withRect:angle:))) { - self.onDidCropToRect = {[weak self] image, rect, angle in + crashWhenFresh() + self.onDidCropToRect = {[weak self] image, rect, angle, flipped in guard let strongSelf = self else { return } delegate.cropViewController!(strongSelf, didCropToImage: image, withRect: rect, angle: angle) } } - + if delegate.responds(to: #selector((any CropViewControllerDelegate).cropViewController(_:didCropToImage:withRect:angle:flipped:))) { + self.onDidCropToRect = {[weak self] image, rect, angle, flipped in + guard let strongSelf = self else { return } + delegate.cropViewController!(strongSelf, didCropToImage: image, withRect: rect, angle: angle, flipped: flipped) + } + } + if delegate.responds(to: #selector((any CropViewControllerDelegate).cropViewController(_:didCropToCircularImage:withRect:angle:))) { - self.onDidCropToCircleImage = {[weak self] image, rect, angle in + crashWhenFresh() + self.onDidCropToCircleImage = {[weak self] image, rect, angle, flipped in guard let strongSelf = self else { return } delegate.cropViewController!(strongSelf, didCropToCircularImage: image, withRect: rect, angle: angle) } } + if delegate.responds(to: #selector((any CropViewControllerDelegate).cropViewController(_:didCropToCircularImage:withRect:angle:flipped:))) { + self.onDidCropToCircleImage = {[weak self] image, rect, angle, flipped in + guard let strongSelf = self else { return } + delegate.cropViewController!(strongSelf, didCropToCircularImage: image, withRect: rect, angle: angle, flipped: flipped) + } + } if delegate.responds(to: #selector((any CropViewControllerDelegate).cropViewController(_:didFinishCancelled:))) { self.onDidFinishCancelled = {[weak self] finished in @@ -707,5 +793,19 @@ extension CropViewController { } } } + + //Fresh builds that are less than 2 hours old require you to update your delegate method implementations with the 'flipped' arg. + //This should make sure end users won't experience crashes but you will be motivated to update the delegate methods. + private func crashWhenFresh() { + let bundleName = Bundle.main.infoDictionary!["CFBundleExecutable"] as? String ?? "Info.plist" + + if let infoPath = Bundle.main.path(forResource: bundleName, ofType: nil), + let infoAttr = try? FileManager.default.attributesOfItem(atPath: infoPath), + let infoDate = infoAttr[FileAttributeKey.creationDate] as? Date { + if infoDate.timeIntervalSinceNow > -7200 { + fatalError("Please add arg 'flipped: Bool' your didCrop delegate method implementations.") + } + } + } } diff --git a/Swift/CropViewControllerExample/ViewController.swift b/Swift/CropViewControllerExample/ViewController.swift index 6bcba7e2..473bb6d6 100644 --- a/Swift/CropViewControllerExample/ViewController.swift +++ b/Swift/CropViewControllerExample/ViewController.swift @@ -17,6 +17,7 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke private var croppedRect = CGRect.zero private var croppedAngle = 0 + private var croppedFlipped = false func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { guard let image = (info[UIImagePickerController.InfoKey.originalImage] as? UIImage) else { return } @@ -30,6 +31,7 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke // -- Uncomment these if you want to test out restoring to a previous crop setting -- //cropController.angle = 90 // The initial angle in which the image will be rotated + //cropController.flipped = true // Whether to initially flip (mirror) the image //cropController.imageCropFrame = CGRect(x: 0, y: 0, width: 2848, height: 4288) //The initial frame that the crop controller will have visible. // -- Uncomment the following lines of code to test out the aspect ratio features -- @@ -43,6 +45,9 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke //cropController.rotateButtonsHidden = true //cropController.rotateClockwiseButtonHidden = true + + //cropController.flipHorizontalButtonHidden = true + //cropController.flipVerticalButtonHidden = false //cropController.doneButtonTitle = "Title" //cropController.cancelButtonTitle = "Title" @@ -77,16 +82,18 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke }) } } - - public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { + + public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int, flipped: Bool) { self.croppedRect = cropRect self.croppedAngle = angle + self.croppedFlipped = flipped updateImageViewWithImage(image, fromCropViewController: cropViewController) } - public func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int) { + public func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int, flipped: Bool) { self.croppedRect = cropRect self.croppedAngle = angle + self.croppedFlipped = flipped updateImageViewWithImage(image, fromCropViewController: cropViewController) } @@ -182,6 +189,7 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke fromView: nil, fromFrame: viewFrame, angle: self.croppedAngle, + flipped: self.croppedFlipped, toImageFrame: self.croppedRect, setup: { self.imageView.isHidden = true }, completion: nil) diff --git a/TOCropViewControllerExample.xcodeproj/project.pbxproj b/TOCropViewControllerExample.xcodeproj/project.pbxproj index 824b1d93..604b943d 100644 --- a/TOCropViewControllerExample.xcodeproj/project.pbxproj +++ b/TOCropViewControllerExample.xcodeproj/project.pbxproj @@ -98,6 +98,45 @@ 2F5062F31F53E31F00AA9F14 /* TOCropViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22DB4D831B234CFA008B8466 /* TOCropViewController.m */; }; 2F5062F41F53E32800AA9F14 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223424751ABBC0CD00BBC2B1 /* ViewController.m */; }; 2F5062F51F53E32D00AA9F14 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 223424771ABBC0CD00BBC2B1 /* Main.storyboard */; }; + 94189B992CC92BAA000C5263 /* UIView+Pixels.m in Sources */ = {isa = PBXBuildFile; fileRef = 94189B972CC92BAA000C5263 /* UIView+Pixels.m */; }; + 94189B9A2CC92BAA000C5263 /* UIView+Pixels.m in Sources */ = {isa = PBXBuildFile; fileRef = 94189B972CC92BAA000C5263 /* UIView+Pixels.m */; }; + 94189B9B2CC92BAA000C5263 /* UIView+Pixels.m in Sources */ = {isa = PBXBuildFile; fileRef = 94189B972CC92BAA000C5263 /* UIView+Pixels.m */; }; + 94189B9D2CC92BAA000C5263 /* UIView+Pixels.m in Sources */ = {isa = PBXBuildFile; fileRef = 94189B972CC92BAA000C5263 /* UIView+Pixels.m */; }; + 94189B9E2CC92BAA000C5263 /* UIView+Pixels.m in Sources */ = {isa = PBXBuildFile; fileRef = 94189B972CC92BAA000C5263 /* UIView+Pixels.m */; }; + 94776A0A2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A092CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png */; }; + 94776A0B2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A092CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png */; }; + 94776A0C2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A092CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png */; }; + 94776A0D2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A092CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png */; }; + 94776A0F2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A0E2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png */; }; + 94776A102CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A0E2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png */; }; + 94776A112CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A0E2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png */; }; + 94776A122CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A0E2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png */; }; + 94776A142CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A132CD4F3C6009566B1 /* rotate.right.fill@2x.png */; }; + 94776A152CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A132CD4F3C6009566B1 /* rotate.right.fill@2x.png */; }; + 94776A162CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A132CD4F3C6009566B1 /* rotate.right.fill@2x.png */; }; + 94776A172CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A132CD4F3C6009566B1 /* rotate.right.fill@2x.png */; }; + 94776A192CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A182CD4F492009566B1 /* rotate.left.fill@2x.png */; }; + 94776A1A2CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A182CD4F492009566B1 /* rotate.left.fill@2x.png */; }; + 94776A1B2CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A182CD4F492009566B1 /* rotate.left.fill@2x.png */; }; + 94776A1C2CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A182CD4F492009566B1 /* rotate.left.fill@2x.png */; }; + 94776A1E2CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A1D2CD5020D009566B1 /* arrow.counterclockwise@2x.png */; }; + 94776A1F2CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A1D2CD5020D009566B1 /* arrow.counterclockwise@2x.png */; }; + 94776A202CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A1D2CD5020D009566B1 /* arrow.counterclockwise@2x.png */; }; + 94776A212CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A1D2CD5020D009566B1 /* arrow.counterclockwise@2x.png */; }; + 94776A232CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A222CD5042B009566B1 /* aspectratio.fill@2x.png */; }; + 94776A242CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A222CD5042B009566B1 /* aspectratio.fill@2x.png */; }; + 94776A252CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A222CD5042B009566B1 /* aspectratio.fill@2x.png */; }; + 94776A262CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A222CD5042B009566B1 /* aspectratio.fill@2x.png */; }; + 94776A282CD508EE009566B1 /* checkmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A272CD508EE009566B1 /* checkmark@2x.png */; }; + 94776A292CD508EE009566B1 /* checkmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A272CD508EE009566B1 /* checkmark@2x.png */; }; + 94776A2A2CD508EE009566B1 /* checkmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A272CD508EE009566B1 /* checkmark@2x.png */; }; + 94776A2B2CD508EE009566B1 /* checkmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A272CD508EE009566B1 /* checkmark@2x.png */; }; + 94776A2D2CD509BB009566B1 /* xmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A2C2CD509BB009566B1 /* xmark@2x.png */; }; + 94776A2E2CD509BB009566B1 /* xmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A2C2CD509BB009566B1 /* xmark@2x.png */; }; + 94776A2F2CD509BB009566B1 /* xmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A2C2CD509BB009566B1 /* xmark@2x.png */; }; + 94776A302CD509BB009566B1 /* xmark@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 94776A2C2CD509BB009566B1 /* xmark@2x.png */; }; + 947A472E2CE7A905005A9628 /* UIView+Pixels.h in Headers */ = {isa = PBXBuildFile; fileRef = 94189B962CC92BAA000C5263 /* UIView+Pixels.h */; }; + 947A472F2CE7A905005A9628 /* UIView+Pixels.h in Headers */ = {isa = PBXBuildFile; fileRef = 94189B962CC92BAA000C5263 /* UIView+Pixels.h */; }; FE3E0E3A21098448004DAE93 /* TOCropViewConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 220C8E9F21062DD300A9B25D /* TOCropViewConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ @@ -217,6 +256,16 @@ 6AE313511C47DAA200894C5B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/TOCropViewControllerLocalizable.strings; sourceTree = ""; }; 72B9DCDC25763340006E160C /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/TOCropViewControllerLocalizable.strings; sourceTree = ""; }; 88BBE90E22C206340073D22A /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/TOCropViewControllerLocalizable.strings; sourceTree = ""; }; + 94189B962CC92BAA000C5263 /* UIView+Pixels.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+Pixels.h"; sourceTree = ""; }; + 94189B972CC92BAA000C5263 /* UIView+Pixels.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+Pixels.m"; sourceTree = ""; }; + 94776A092CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png"; sourceTree = ""; }; + 94776A0E2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png"; sourceTree = ""; }; + 94776A132CD4F3C6009566B1 /* rotate.right.fill@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "rotate.right.fill@2x.png"; sourceTree = ""; }; + 94776A182CD4F492009566B1 /* rotate.left.fill@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "rotate.left.fill@2x.png"; sourceTree = ""; }; + 94776A1D2CD5020D009566B1 /* arrow.counterclockwise@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "arrow.counterclockwise@2x.png"; sourceTree = ""; }; + 94776A222CD5042B009566B1 /* aspectratio.fill@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "aspectratio.fill@2x.png"; sourceTree = ""; }; + 94776A272CD508EE009566B1 /* checkmark@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "checkmark@2x.png"; sourceTree = ""; }; + 94776A2C2CD509BB009566B1 /* xmark@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "xmark@2x.png"; sourceTree = ""; }; A93782CF214A55F900CAE7EE /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/TOCropViewControllerLocalizable.strings; sourceTree = ""; }; A93782D3214A81A200CAE7EE /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/TOCropViewControllerLocalizable.strings; sourceTree = ""; }; CDC8C5751C667B9100BB86A4 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Main.strings; sourceTree = ""; }; @@ -308,6 +357,14 @@ 220C8EA021062E6D00A9B25D /* Resources */ = { isa = PBXGroup; children = ( + 94776A0E2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png */, + 94776A092CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png */, + 94776A132CD4F3C6009566B1 /* rotate.right.fill@2x.png */, + 94776A182CD4F492009566B1 /* rotate.left.fill@2x.png */, + 94776A1D2CD5020D009566B1 /* arrow.counterclockwise@2x.png */, + 94776A222CD5042B009566B1 /* aspectratio.fill@2x.png */, + 94776A272CD508EE009566B1 /* checkmark@2x.png */, + 94776A2C2CD509BB009566B1 /* xmark@2x.png */, 22A105B22BC134A200DB3A80 /* PrivacyInfo.xcprivacy */, 22C3C5491AC8CA0D00E86280 /* TOCropViewControllerLocalizable.strings */, ); @@ -335,6 +392,8 @@ children = ( 220C8EAB2106344D00A9B25D /* UIImage+CropRotate.h */, 220C8EAA2106344D00A9B25D /* UIImage+CropRotate.m */, + 94189B962CC92BAA000C5263 /* UIView+Pixels.h */, + 94189B972CC92BAA000C5263 /* UIView+Pixels.m */, ); path = Categories; sourceTree = ""; @@ -494,6 +553,7 @@ 2238CF451FC02AE00081B957 /* TOCropViewController.h in Headers */, 144B8CD71D22CD650085D774 /* TOCropToolbar.h in Headers */, 220C8EB22106344D00A9B25D /* UIImage+CropRotate.h in Headers */, + 947A472F2CE7A905005A9628 /* UIView+Pixels.h in Headers */, 144B8CD81D22CD650085D774 /* TOCropView.h in Headers */, 144B8CD11D22CD650085D774 /* TOCropViewControllerTransitioning.h in Headers */, 144B8CD21D22CD650085D774 /* TOActivityCroppedImageProvider.h in Headers */, @@ -510,6 +570,7 @@ files = ( 04262DA520F6FD1000024177 /* CropViewController.h in Headers */, 04262DA220F6FC4600024177 /* TOCropToolbar.h in Headers */, + 947A472E2CE7A905005A9628 /* UIView+Pixels.h in Headers */, 04262DA320F6FC4600024177 /* TOCropView.h in Headers */, 04262DA420F6FC4600024177 /* TOCropViewController.h in Headers */, 220C8EA82106341600A9B25D /* TOCropViewConstants.h in Headers */, @@ -730,8 +791,16 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 94776A262CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */, + 94776A292CD508EE009566B1 /* checkmark@2x.png in Resources */, + 94776A162CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */, + 94776A122CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */, 22A105B42BC134A200DB3A80 /* PrivacyInfo.xcprivacy in Resources */, + 94776A212CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */, 04262D8620F6F1C600024177 /* TOCropViewControllerLocalizable.strings in Resources */, + 94776A0D2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */, + 94776A2E2CD509BB009566B1 /* xmark@2x.png in Resources */, + 94776A1C2CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -750,7 +819,15 @@ 2234247E1ABBC0CD00BBC2B1 /* LaunchScreen.xib in Resources */, 2234247B1ABBC0CD00BBC2B1 /* Images.xcassets in Resources */, 22A105B52BC134A200DB3A80 /* PrivacyInfo.xcprivacy in Resources */, + 94776A232CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */, 22C3C5471AC8CA0D00E86280 /* TOCropViewControllerLocalizable.strings in Resources */, + 94776A202CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */, + 94776A192CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */, + 94776A142CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */, + 94776A2F2CD509BB009566B1 /* xmark@2x.png in Resources */, + 94776A102CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */, + 94776A2A2CD508EE009566B1 /* checkmark@2x.png in Resources */, + 94776A0A2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -762,7 +839,15 @@ 2238CF2A1FC0269C0081B957 /* Assets.xcassets in Resources */, 2238CF281FC0269C0081B957 /* Main.storyboard in Resources */, 22A105B62BC134A200DB3A80 /* PrivacyInfo.xcprivacy in Resources */, + 94776A252CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */, 220C8EA121062EFC00A9B25D /* TOCropViewControllerLocalizable.strings in Resources */, + 94776A1F2CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */, + 94776A1A2CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */, + 94776A152CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */, + 94776A302CD509BB009566B1 /* xmark@2x.png in Resources */, + 94776A112CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */, + 94776A2B2CD508EE009566B1 /* checkmark@2x.png in Resources */, + 94776A0C2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -770,8 +855,16 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 94776A242CD5042B009566B1 /* aspectratio.fill@2x.png in Resources */, + 94776A282CD508EE009566B1 /* checkmark@2x.png in Resources */, + 94776A172CD4F3C6009566B1 /* rotate.right.fill@2x.png in Resources */, + 94776A0F2CD4F28E009566B1 /* arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill@2x.png in Resources */, 22A105B32BC134A200DB3A80 /* PrivacyInfo.xcprivacy in Resources */, + 94776A1E2CD5020D009566B1 /* arrow.counterclockwise@2x.png in Resources */, 04262D8720F6F1D600024177 /* TOCropViewControllerLocalizable.strings in Resources */, + 94776A0B2CD4F185009566B1 /* arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill@2x.png in Resources */, + 94776A2D2CD509BB009566B1 /* xmark@2x.png in Resources */, + 94776A1B2CD4F492009566B1 /* rotate.left.fill@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -799,6 +892,7 @@ 144B8CE01D22CD730085D774 /* TOCropToolbar.m in Sources */, 144B8CE11D22CD730085D774 /* TOCropView.m in Sources */, 144B8CE21D22CD730085D774 /* TOCropViewController.m in Sources */, + 94189B9D2CC92BAA000C5263 /* UIView+Pixels.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -825,6 +919,7 @@ 22DB4D9E1B234D4F008B8466 /* TOActivityCroppedImageProvider.m in Sources */, 22DB4D961B234D07008B8466 /* TOCropViewControllerTransitioning.m in Sources */, 22DB4D991B234D07008B8466 /* TOCropScrollView.m in Sources */, + 94189B9B2CC92BAA000C5263 /* UIView+Pixels.m in Sources */, 223DCEB61FBAA85D00F99209 /* TOCropViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -844,6 +939,7 @@ 22B68F9D1FFB380700601B1A /* TOCropViewController.m in Sources */, 2238CF251FC0269C0081B957 /* ViewController.swift in Sources */, 2238CF231FC0269C0081B957 /* AppDelegate.swift in Sources */, + 94189B9E2CC92BAA000C5263 /* UIView+Pixels.m in Sources */, 2238CF361FC029880081B957 /* CropViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -853,6 +949,7 @@ buildActionMask = 2147483647; files = ( 220C8EAC2106344D00A9B25D /* UIImage+CropRotate.m in Sources */, + 94189B992CC92BAA000C5263 /* UIView+Pixels.m in Sources */, 22B68F9F1FFB3C0800601B1A /* TOCropViewControllerTransitioning.m in Sources */, 22B68FA01FFB3C0800601B1A /* TOActivityCroppedImageProvider.m in Sources */, 22B68FA11FFB3C0800601B1A /* TOCroppedImageAttributes.m in Sources */, @@ -872,6 +969,7 @@ 2236AB4A1F595960005C5098 /* ShareViewController.m in Sources */, 2F5062EC1F53E31F00AA9F14 /* TOActivityCroppedImageProvider.m in Sources */, 2F5062EF1F53E31F00AA9F14 /* TOCropOverlayView.m in Sources */, + 94189B9A2CC92BAA000C5263 /* UIView+Pixels.m in Sources */, 2F5062F21F53E31F00AA9F14 /* TOCropView.m in Sources */, 2F5062F41F53E32800AA9F14 /* ViewController.m in Sources */, 2F5062F31F53E31F00AA9F14 /* TOCropViewController.m in Sources */,