Rocket.Chat.ReactNative/ios/Pods/TOCropViewController/Objective-C/TOCropViewController/TOCropViewController.m

1286 lines
48 KiB
Objective-C
Executable File

//
// TOCropViewController.m
//
// Copyright 2015-2018 Timothy Oliver. 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 "TOCropViewController.h"
#import "TOCropViewControllerTransitioning.h"
#import "TOActivityCroppedImageProvider.h"
#import "UIImage+CropRotate.h"
#import "TOCroppedImageAttributes.h"
static const CGFloat kTOCropViewControllerTitleTopPadding = 14.0f;
static const CGFloat kTOCropViewControllerToolbarHeight = 44.0f;
@interface TOCropViewController () <UIActionSheetDelegate, UIViewControllerTransitioningDelegate, TOCropViewDelegate>
/* The target image */
@property (nonatomic, readwrite) UIImage *image;
/* The cropping style of the crop view */
@property (nonatomic, assign, readwrite) TOCropViewCroppingStyle croppingStyle;
/* Views */
@property (nonatomic, strong) TOCropToolbar *toolbar;
@property (nonatomic, strong, readwrite) TOCropView *cropView;
@property (nonatomic, strong) UIView *toolbarSnapshotView;
@property (nonatomic, strong, readwrite) UILabel *titleLabel;
/* Transition animation controller */
@property (nonatomic, copy) void (^prepareForTransitionHandler)(void);
@property (nonatomic, strong) TOCropViewControllerTransitioning *transitionController;
@property (nonatomic, assign) BOOL inTransition;
/* If pushed from a navigation controller, the visibility of that controller's bars. */
@property (nonatomic, assign) BOOL navigationBarHidden;
@property (nonatomic, assign) BOOL toolbarHidden;
/* State for whether content is being laid out vertically or horizontally */
@property (nonatomic, readonly) BOOL verticalLayout;
/* Convenience method for managing status bar state */
@property (nonatomic, readonly) BOOL overrideStatusBar; // Whether the view controller needs to touch the status bar
@property (nonatomic, readonly) BOOL statusBarHidden; // Whether it should be hidden or visible at this point
@property (nonatomic, readonly) CGFloat statusBarHeight; // The height of the status bar when visible
/* Convenience method for getting the vertical inset for both iPhone X and status bar */
@property (nonatomic, readonly) UIEdgeInsets statusBarSafeInsets;
/* Flag to perform initial setup on the first run */
@property (nonatomic, assign) BOOL firstTime;
@end
@implementation TOCropViewController
- (instancetype)initWithCroppingStyle:(TOCropViewCroppingStyle)style image:(UIImage *)image
{
NSParameterAssert(image);
self = [super initWithNibName:nil bundle:nil];
if (self) {
// Init parameters
_image = image;
_croppingStyle = style;
// Set up base view controller behaviour
self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
self.modalPresentationStyle = UIModalPresentationFullScreen;
self.automaticallyAdjustsScrollViewInsets = NO;
self.hidesNavigationBar = true;
// Controller object that handles the transition animation when presenting / dismissing this app
_transitionController = [[TOCropViewControllerTransitioning alloc] init];
// Default initial behaviour
_aspectRatioPreset = TOCropViewControllerAspectRatioPresetOriginal;
_toolbarPosition = TOCropViewControllerToolbarPositionBottom;
}
return self;
}
- (instancetype)initWithImage:(UIImage *)image
{
return [self initWithCroppingStyle:TOCropViewCroppingStyleDefault image:image];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Set up view controller properties
self.transitioningDelegate = self;
self.view.backgroundColor = self.cropView.backgroundColor;
BOOL circularMode = (self.croppingStyle == TOCropViewCroppingStyleCircular);
// Layout the views initially
self.cropView.frame = [self frameForCropViewWithVerticalLayout:self.verticalLayout];
self.toolbar.frame = [self frameForToolbarWithVerticalLayout:self.verticalLayout];
// Set up toolbar default behaviour
self.toolbar.clampButtonHidden = self.aspectRatioPickerButtonHidden || circularMode;
self.toolbar.rotateClockwiseButtonHidden = self.rotateClockwiseButtonHidden;
// Set up the toolbar button actions
__weak typeof(self) weakSelf = self;
self.toolbar.doneButtonTapped = ^{ [weakSelf doneButtonTapped]; };
self.toolbar.cancelButtonTapped = ^{ [weakSelf cancelButtonTapped]; };
self.toolbar.resetButtonTapped = ^{ [weakSelf resetCropViewLayout]; };
self.toolbar.clampButtonTapped = ^{ [weakSelf showAspectRatioDialog]; };
self.toolbar.rotateCounterclockwiseButtonTapped = ^{ [weakSelf rotateCropViewCounterclockwise]; };
self.toolbar.rotateClockwiseButtonTapped = ^{ [weakSelf rotateCropViewClockwise]; };
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// If we're animating onto the screen, set a flag
// so we can manually control the status bar fade out timing
if (animated) {
self.inTransition = YES;
[self setNeedsStatusBarAppearanceUpdate];
}
// If this controller is pushed onto a navigation stack, set flags noting the
// state of the navigation controller bars before we present, and then hide them
if (self.navigationController) {
if (self.hidesNavigationBar) {
self.navigationBarHidden = self.navigationController.navigationBarHidden;
self.toolbarHidden = self.navigationController.toolbarHidden;
[self.navigationController setNavigationBarHidden:YES animated:animated];
[self.navigationController setToolbarHidden:YES animated:animated];
}
self.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
}
else {
// Hide the background content when transitioning for performance
[self.cropView setBackgroundImageViewHidden:YES animated:NO];
// The title label will fade
self.titleLabel.alpha = animated ? 0.0f : 1.0f;
}
// If an initial aspect ratio was set before presentation, set it now once the rest of
// the setup will have been done
if (self.aspectRatioPreset != TOCropViewControllerAspectRatioPresetOriginal) {
[self setAspectRatioPreset:self.aspectRatioPreset animated:NO];
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Disable the transition flag for the status bar
self.inTransition = NO;
// Re-enable translucency now that the animation has completed
self.cropView.simpleRenderMode = NO;
// Now that the presentation animation will have finished, animate
// the status bar fading out, and if present, the title label fading in
void (^updateContentBlock)(void) = ^{
[self setNeedsStatusBarAppearanceUpdate];
self.titleLabel.alpha = 1.0f;
};
if (animated) {
[UIView animateWithDuration:0.3f animations:updateContentBlock];
}
else {
updateContentBlock();
}
// Make the grid overlay view fade in
if (self.cropView.gridOverlayHidden) {
[self.cropView setGridOverlayHidden:NO animated:animated];
}
// Fade in the background view content
if (self.navigationController == nil) {
[self.cropView setBackgroundImageViewHidden:NO animated:animated];
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Set the transition flag again so we can defer the status bar
self.inTransition = YES;
[UIView animateWithDuration:0.5f animations:^{ [self setNeedsStatusBarAppearanceUpdate]; }];
// Restore the navigation controller to its state before we were presented
if (self.navigationController && self.hidesNavigationBar) {
[self.navigationController setNavigationBarHidden:self.navigationBarHidden animated:animated];
[self.navigationController setToolbarHidden:self.toolbarHidden animated:animated];
}
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
// Reset the state once the view has gone offscreen
self.inTransition = NO;
[self setNeedsStatusBarAppearanceUpdate];
}
#pragma mark - Status Bar -
- (UIStatusBarStyle)preferredStatusBarStyle
{
if (self.navigationController) {
return UIStatusBarStyleLightContent;
}
// Even though we are a dark theme, leave the status bar
// as black so it's not obvious that it's still visible during the transition
return UIStatusBarStyleDefault;
}
- (BOOL)prefersStatusBarHidden
{
// Disregard the transition animation if we're not actively overriding it
if (!self.overrideStatusBar) {
return self.statusBarHidden;
}
// Work out whether the status bar needs to be visible
// during a transition animation or not
BOOL hidden = YES; // Default is yes
hidden = hidden && !(self.inTransition); // Not currently in a presentation animation (Where removing the status bar would break the layout)
hidden = hidden && !(self.view.superview == nil); // Not currently waiting to be added to a super view
return hidden;
}
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
{
return UIRectEdgeAll;
}
- (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout
{
UIEdgeInsets insets = self.statusBarSafeInsets;
CGRect frame = CGRectZero;
if (!verticalLayout) { // In landscape laying out toolbar to the left
frame.origin.x = insets.left;
frame.origin.y = 0.0f;
frame.size.width = kTOCropViewControllerToolbarHeight;
frame.size.height = CGRectGetHeight(self.view.frame);
}
else {
frame.origin.x = 0.0f;
frame.size.width = CGRectGetWidth(self.view.bounds);
frame.size.height = kTOCropViewControllerToolbarHeight;
if (self.toolbarPosition == TOCropViewControllerToolbarPositionBottom) {
frame.origin.y = CGRectGetHeight(self.view.bounds) - (frame.size.height + insets.bottom);
} else {
frame.origin.y = insets.top;
}
}
return frame;
}
- (CGRect)frameForCropViewWithVerticalLayout:(BOOL)verticalLayout
{
//On an iPad, if being presented in a modal view controller by a UINavigationController,
//at the time we need it, the size of our view will be incorrect.
//If this is the case, derive our view size from our parent view controller instead
UIView *view = nil;
if (self.parentViewController == nil) {
view = self.view;
}
else {
view = self.parentViewController.view;
}
UIEdgeInsets insets = self.statusBarSafeInsets;
CGRect bounds = view.bounds;
CGRect frame = CGRectZero;
// Horizontal layout (eg landscape)
if (!verticalLayout) {
frame.origin.x = kTOCropViewControllerToolbarHeight + insets.left;
frame.size.width = CGRectGetWidth(bounds) - frame.origin.x;
frame.size.height = CGRectGetHeight(bounds);
}
else { // Vertical layout
frame.size.height = CGRectGetHeight(bounds);
frame.size.width = CGRectGetWidth(bounds);
// Set Y and adjust for height
if (self.toolbarPosition == TOCropViewControllerToolbarPositionBottom) {
frame.size.height -= (insets.bottom + kTOCropViewControllerToolbarHeight);
} else if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
frame.origin.y = kTOCropViewControllerToolbarHeight + insets.top;
frame.size.height -= frame.origin.y;
}
}
return frame;
}
- (CGRect)frameForTitleLabelWithSize:(CGSize)size verticalLayout:(BOOL)verticalLayout
{
CGRect frame = (CGRect){CGPointZero, size};
CGFloat viewWidth = self.view.bounds.size.width;
CGFloat x = 0.0f; // Additional X offset in landscape mode
// Adjust for landscape layout
if (!verticalLayout) {
x = kTOCropViewControllerTitleTopPadding;
if (@available(iOS 11.0, *)) {
x += self.view.safeAreaInsets.left;
}
viewWidth -= x;
}
// Work out horizontal position
frame.origin.x = ceilf((viewWidth - frame.size.width) * 0.5f);
if (!verticalLayout) { frame.origin.x += x; }
// Work out vertical position
if (@available(iOS 11.0, *)) {
frame.origin.y = self.view.safeAreaInsets.top + kTOCropViewControllerTitleTopPadding;
}
else {
frame.origin.y = self.statusBarHeight + kTOCropViewControllerTitleTopPadding;
}
return frame;
}
- (void)adjustCropViewInsets
{
UIEdgeInsets insets = self.statusBarSafeInsets;
// If there is no title text, inset the top of the content as high as possible
if (!self.titleLabel.text.length) {
if (self.verticalLayout) {
if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0f, 0.0f, insets.bottom, 0.0f);
}
else { // Add padding to the top otherwise
self.cropView.cropRegionInsets = UIEdgeInsetsMake(insets.top, 0.0f, 0.0, 0.0f);
}
}
else {
self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0f, 0.0f, insets.bottom, 0.0f);
}
return;
}
// Work out the size of the title label based on the crop view size
CGRect frame = self.titleLabel.frame;
frame.size = [self.titleLabel sizeThatFits:self.cropView.frame.size];
self.titleLabel.frame = frame;
// Set out the appropriate inset for that
CGFloat verticalInset = self.statusBarHeight;
verticalInset += kTOCropViewControllerTitleTopPadding;
verticalInset += self.titleLabel.frame.size.height;
self.cropView.cropRegionInsets = UIEdgeInsetsMake(verticalInset, 0, insets.bottom, 0);
}
- (void)adjustToolbarInsets
{
UIEdgeInsets insets = UIEdgeInsetsZero;
if (@available(iOS 11.0, *)) {
// Add padding to the left in landscape mode
if (!self.verticalLayout) {
insets.left = self.view.safeAreaInsets.left;
}
else {
// Add padding on top if in vertical and tool bar is at the top
if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
insets.top = self.view.safeAreaInsets.top;
}
else { // Add padding to the bottom otherwise
insets.bottom = self.view.safeAreaInsets.bottom;
}
}
}
else { // iOS <= 10
if (!self.statusBarHidden && self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
insets.top = self.statusBarHeight;
}
}
// Update the toolbar with these properties
self.toolbar.backgroundViewOutsets = insets;
self.toolbar.statusBarHeightInset = self.statusBarHeight;
[self.toolbar setNeedsLayout];
}
- (void)viewSafeAreaInsetsDidChange
{
[super viewSafeAreaInsetsDidChange];
[self adjustCropViewInsets];
[self adjustToolbarInsets];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.cropView.frame = [self frameForCropViewWithVerticalLayout:self.verticalLayout];
[self adjustCropViewInsets];
[self.cropView moveCroppedContentToCenterAnimated:NO];
if (self.firstTime == NO) {
[self.cropView performInitialSetup];
self.firstTime = YES;
}
if (self.title.length) {
self.titleLabel.frame = [self frameForTitleLabelWithSize:self.titleLabel.frame.size verticalLayout:self.verticalLayout];
[self.cropView moveCroppedContentToCenterAnimated:NO];
}
[UIView performWithoutAnimation:^{
self.toolbar.frame = [self frameForToolbarWithVerticalLayout:self.verticalLayout];
[self adjustToolbarInsets];
[self.toolbar setNeedsLayout];
}];
}
#pragma mark - Rotation Handling -
- (void)_willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
self.toolbarSnapshotView = [self.toolbar snapshotViewAfterScreenUpdates:NO];
self.toolbarSnapshotView.frame = self.toolbar.frame;
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
self.toolbarSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
}
else {
self.toolbarSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleRightMargin;
}
[self.view addSubview:self.toolbarSnapshotView];
// Set up the toolbar frame to be just off t
CGRect frame = [self frameForToolbarWithVerticalLayout:UIInterfaceOrientationIsPortrait(toInterfaceOrientation)];
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
frame.origin.x = -frame.size.width;
}
else {
frame.origin.y = self.view.bounds.size.height;
}
self.toolbar.frame = frame;
[self.toolbar layoutIfNeeded];
self.toolbar.alpha = 0.0f;
[self.cropView prepareforRotation];
self.cropView.frame = [self frameForCropViewWithVerticalLayout:!UIInterfaceOrientationIsPortrait(toInterfaceOrientation)];
self.cropView.simpleRenderMode = YES;
self.cropView.internalLayoutDisabled = YES;
}
- (void)_willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//Remove all animations in the toolbar
self.toolbar.frame = [self frameForToolbarWithVerticalLayout:!UIInterfaceOrientationIsLandscape(toInterfaceOrientation)];
[self.toolbar.layer removeAllAnimations];
for (CALayer *sublayer in self.toolbar.layer.sublayers) {
[sublayer removeAllAnimations];
}
// 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
options:UIViewAnimationOptionBeginFromCurrentState
animations:
^{
self.cropView.frame = [self frameForCropViewWithVerticalLayout:!UIInterfaceOrientationIsLandscape(toInterfaceOrientation)];
self.toolbar.frame = [self frameForToolbarWithVerticalLayout:UIInterfaceOrientationIsPortrait(toInterfaceOrientation)];
[self.cropView performRelayoutForRotation];
} completion:nil];
self.toolbarSnapshotView.alpha = 0.0f;
self.toolbar.alpha = 1.0f;
}
- (void)_didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self.toolbarSnapshotView removeFromSuperview];
self.toolbarSnapshotView = nil;
[self.cropView setSimpleRenderMode:NO animated:YES];
self.cropView.internalLayoutDisabled = NO;
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// If the size doesn't change (e.g, we did a 180 degree device rotation), don't bother doing a relayout
if (CGSizeEqualToSize(size, self.view.bounds.size)) { return; }
UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait;
CGSize currentSize = self.view.bounds.size;
if (currentSize.width < size.width) {
orientation = UIInterfaceOrientationLandscapeLeft;
}
[self _willRotateToInterfaceOrientation:orientation duration:coordinator.transitionDuration];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self _willAnimateRotationToInterfaceOrientation:orientation duration:coordinator.transitionDuration];
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self _didRotateFromInterfaceOrientation:orientation];
}];
}
#pragma mark - Reset -
- (void)resetCropViewLayout
{
BOOL animated = (self.cropView.angle == 0);
if (self.resetAspectRatioEnabled) {
self.aspectRatioLockEnabled = NO;
}
[self.cropView resetLayoutToDefaultAnimated:animated];
}
#pragma mark - Aspect Ratio Handling -
- (void)showAspectRatioDialog
{
if (self.cropView.aspectRatioLockEnabled) {
self.cropView.aspectRatioLockEnabled = NO;
self.toolbar.clampButtonGlowing = NO;
return;
}
//Depending on the shape of the image, work out if horizontal, or vertical options are required
BOOL verticalCropBox = self.cropView.cropBoxAspectRatioIsPortrait;
// Get the resource bundle depending on the framework/dependency manager we're using
NSBundle *resourceBundle = TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(self);
//Prepare the localized options
NSString *cancelButtonTitle = NSLocalizedStringFromTableInBundle(@"Cancel", @"TOCropViewControllerLocalizable", resourceBundle, nil);
NSString *originalButtonTitle = NSLocalizedStringFromTableInBundle(@"Original", @"TOCropViewControllerLocalizable", resourceBundle, nil);
NSString *squareButtonTitle = NSLocalizedStringFromTableInBundle(@"Square", @"TOCropViewControllerLocalizable", resourceBundle, nil);
//Prepare the list that will be fed to the alert view/controller
// Ratio titles according to the order of enum TOCropViewControllerAspectRatioPreset
NSArray<NSString *> *portraitRatioTitles = @[originalButtonTitle, squareButtonTitle, @"2:3", @"3:5", @"3:4", @"4:5", @"5:7", @"9:16"];
NSArray<NSString *> *landscapeRatioTitles = @[originalButtonTitle, squareButtonTitle, @"3:2", @"5:3", @"4:3", @"5:4", @"7:5", @"16:9"];
NSMutableArray *ratioValues = [NSMutableArray array];
NSMutableArray *itemStrings = [NSMutableArray array];
if (self.allowedAspectRatios == nil) {
for (NSInteger i = 0; i < TOCropViewControllerAspectRatioPresetCustom; i++) {
NSString *itemTitle = verticalCropBox ? portraitRatioTitles[i] : landscapeRatioTitles[i];
[itemStrings addObject:itemTitle];
[ratioValues addObject:@(i)];
}
}
else {
for (NSNumber *allowedRatio in self.allowedAspectRatios) {
TOCropViewControllerAspectRatioPreset ratio = allowedRatio.integerValue;
NSString *itemTitle = verticalCropBox ? portraitRatioTitles[ratio] : landscapeRatioTitles[ratio];
[itemStrings addObject:itemTitle];
[ratioValues addObject:allowedRatio];
}
}
// If a custom aspect ratio is provided, and a custom name has been given to it, add it as a visible choice
if (self.customAspectRatioName.length > 0 && !CGSizeEqualToSize(CGSizeZero, self.customAspectRatio)) {
[itemStrings addObject:self.customAspectRatioName];
[ratioValues addObject:@(TOCropViewControllerAspectRatioPresetCustom)];
}
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[alertController addAction:[UIAlertAction actionWithTitle:cancelButtonTitle style:UIAlertActionStyleCancel handler:nil]];
//Add each item to the alert controller
for (NSInteger i = 0; i < itemStrings.count; i++) {
id handlerBlock = ^(UIAlertAction *action) {
[self setAspectRatioPreset:[ratioValues[i] integerValue] animated:YES];
self.aspectRatioLockEnabled = YES;
};
UIAlertAction *action = [UIAlertAction actionWithTitle:itemStrings[i] style:UIAlertActionStyleDefault handler:handlerBlock];
[alertController addAction:action];
}
alertController.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *presentationController = [alertController popoverPresentationController];
presentationController.sourceView = self.toolbar;
presentationController.sourceRect = self.toolbar.clampButtonFrame;
[self presentViewController:alertController animated:YES completion:nil];
}
- (void)setAspectRatioPreset:(TOCropViewControllerAspectRatioPreset)aspectRatioPreset animated:(BOOL)animated
{
CGSize aspectRatio = CGSizeZero;
_aspectRatioPreset = aspectRatioPreset;
switch (aspectRatioPreset) {
case TOCropViewControllerAspectRatioPresetOriginal:
aspectRatio = CGSizeZero;
break;
case TOCropViewControllerAspectRatioPresetSquare:
aspectRatio = CGSizeMake(1.0f, 1.0f);
break;
case TOCropViewControllerAspectRatioPreset3x2:
aspectRatio = CGSizeMake(3.0f, 2.0f);
break;
case TOCropViewControllerAspectRatioPreset5x3:
aspectRatio = CGSizeMake(5.0f, 3.0f);
break;
case TOCropViewControllerAspectRatioPreset4x3:
aspectRatio = CGSizeMake(4.0f, 3.0f);
break;
case TOCropViewControllerAspectRatioPreset5x4:
aspectRatio = CGSizeMake(5.0f, 4.0f);
break;
case TOCropViewControllerAspectRatioPreset7x5:
aspectRatio = CGSizeMake(7.0f, 5.0f);
break;
case TOCropViewControllerAspectRatioPreset16x9:
aspectRatio = CGSizeMake(16.0f, 9.0f);
break;
case TOCropViewControllerAspectRatioPresetCustom:
aspectRatio = self.customAspectRatio;
break;
}
// If the aspect ratio lock is not enabled, allow a swap
// If the aspect ratio lock is on, allow a aspect ratio swap
// only if the allowDimensionSwap option is specified.
BOOL aspectRatioCanSwapDimensions = !self.aspectRatioLockEnabled ||
(self.aspectRatioLockEnabled && self.aspectRatioLockDimensionSwapEnabled);
//If the image is a portrait shape, flip the aspect ratio to match
if (self.cropView.cropBoxAspectRatioIsPortrait &&
aspectRatioCanSwapDimensions)
{
CGFloat width = aspectRatio.width;
aspectRatio.width = aspectRatio.height;
aspectRatio.height = width;
}
[self.cropView setAspectRatio:aspectRatio animated:animated];
}
- (void)rotateCropViewClockwise
{
[self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:YES];
}
- (void)rotateCropViewCounterclockwise
{
[self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:NO];
}
#pragma mark - Crop View Delegates -
- (void)cropViewDidBecomeResettable:(TOCropView *)cropView
{
self.toolbar.resetButtonEnabled = YES;
}
- (void)cropViewDidBecomeNonResettable:(TOCropView *)cropView
{
self.toolbar.resetButtonEnabled = NO;
}
#pragma mark - Presentation Handling -
- (void)presentAnimatedFromParentViewController:(UIViewController *)viewController
fromView:(UIView *)fromView
fromFrame:(CGRect)fromFrame
setup:(void (^)(void))setup
completion:(void (^)(void))completion
{
[self presentAnimatedFromParentViewController:viewController fromImage:nil fromView:fromView fromFrame:fromFrame
angle:0 toImageFrame:CGRectZero setup:setup completion:completion];
}
- (void)presentAnimatedFromParentViewController:(UIViewController *)viewController
fromImage:(UIImage *)image
fromView:(UIView *)fromView
fromFrame:(CGRect)fromFrame
angle:(NSInteger)angle
toImageFrame:(CGRect)toFrame
setup:(void (^)(void))setup
completion:(void (^)(void))completion
{
self.transitionController.image = image ? image : self.image;
self.transitionController.fromFrame = fromFrame;
self.transitionController.fromView = fromView;
self.prepareForTransitionHandler = setup;
if (self.angle != 0 || !CGRectIsEmpty(toFrame)) {
self.angle = angle;
self.imageCropFrame = toFrame;
}
__weak typeof (self) weakSelf = self;
[viewController presentViewController:self.parentViewController ? self.parentViewController : self
animated:YES
completion:^
{
typeof (self) strongSelf = weakSelf;
if (completion) {
completion();
}
[strongSelf.cropView setCroppingViewsHidden:NO animated:YES];
if (!CGRectIsEmpty(fromFrame)) {
[strongSelf.cropView setGridOverlayHidden:NO animated:YES];
}
}];
}
- (void)dismissAnimatedFromParentViewController:(UIViewController *)viewController
toView:(UIView *)toView
toFrame:(CGRect)frame
setup:(void (^)(void))setup
completion:(void (^)(void))completion
{
[self dismissAnimatedFromParentViewController:viewController withCroppedImage:nil toView:toView toFrame:frame setup:setup completion:completion];
}
- (void)dismissAnimatedFromParentViewController:(UIViewController *)viewController
withCroppedImage:(UIImage *)image
toView:(UIView *)toView
toFrame:(CGRect)frame
setup:(void (^)(void))setup
completion:(void (^)(void))completion
{
// If a cropped image was supplied, use that, and only zoom out from the crop box
if (image) {
self.transitionController.image = image ? image : self.image;
self.transitionController.fromFrame = [self.cropView convertRect:self.cropView.cropBoxFrame toView:self.view];
}
else { // else use the main image, and zoom out from its entirety
self.transitionController.image = self.image;
self.transitionController.fromFrame = [self.cropView convertRect:self.cropView.imageViewFrame toView:self.view];
}
self.transitionController.toView = toView;
self.transitionController.toFrame = frame;
self.prepareForTransitionHandler = setup;
[viewController dismissViewControllerAnimated:YES completion:^ {
if (completion) { completion(); }
}];
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
if (self.navigationController || self.modalTransitionStyle == UIModalTransitionStyleCoverVertical) {
return nil;
}
self.cropView.simpleRenderMode = YES;
__weak typeof (self) weakSelf = self;
self.transitionController.prepareForTransitionHandler = ^{
typeof (self) strongSelf = weakSelf;
TOCropViewControllerTransitioning *transitioning = strongSelf.transitionController;
transitioning.toFrame = [strongSelf.cropView convertRect:strongSelf.cropView.cropBoxFrame toView:strongSelf.view];
if (!CGRectIsEmpty(transitioning.fromFrame) || transitioning.fromView) {
strongSelf.cropView.croppingViewsHidden = YES;
}
if (strongSelf.prepareForTransitionHandler) {
strongSelf.prepareForTransitionHandler();
}
strongSelf.prepareForTransitionHandler = nil;
};
self.transitionController.isDismissing = NO;
return self.transitionController;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
if (self.navigationController || self.modalTransitionStyle == UIModalTransitionStyleCoverVertical) {
return nil;
}
__weak typeof (self) weakSelf = self;
self.transitionController.prepareForTransitionHandler = ^{
typeof (self) strongSelf = weakSelf;
TOCropViewControllerTransitioning *transitioning = strongSelf.transitionController;
if (!CGRectIsEmpty(transitioning.toFrame) || transitioning.toView) {
strongSelf.cropView.croppingViewsHidden = YES;
}
else {
strongSelf.cropView.simpleRenderMode = YES;
}
if (strongSelf.prepareForTransitionHandler) {
strongSelf.prepareForTransitionHandler();
}
};
self.transitionController.isDismissing = YES;
return self.transitionController;
}
#pragma mark - Button Feedback -
- (void)cancelButtonTapped
{
if (!self.showCancelConfirmationDialog) {
[self dismissCropViewController];
return;
}
// Get the resource bundle depending on the framework/dependency manager we're using
NSBundle *resourceBundle = TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(self);
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
alertController.popoverPresentationController.sourceView = self.toolbar.visibleCancelButton;
NSString *yesButtonTitle = NSLocalizedStringFromTableInBundle(@"Delete Changes", @"TOCropViewControllerLocalizable", resourceBundle, nil);
NSString *noButtonTitle = NSLocalizedStringFromTableInBundle(@"Cancel", @"TOCropViewControllerLocalizable", resourceBundle, nil);
__weak typeof (self) weakSelf = self;
UIAlertAction *yesAction = [UIAlertAction actionWithTitle:yesButtonTitle style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) {
[weakSelf dismissCropViewController];
}];
[alertController addAction:yesAction];
UIAlertAction *noAction = [UIAlertAction actionWithTitle:noButtonTitle style:UIAlertActionStyleCancel handler:nil];
[alertController addAction:noAction];
[weakSelf presentViewController:alertController animated:YES completion: nil];
}
- (void)dismissCropViewController
{
bool isDelegateOrCallbackHandled = NO;
// Check if the delegate method was implemented and call if so
if ([self.delegate respondsToSelector:@selector(cropViewController:didFinishCancelled:)]) {
[self.delegate cropViewController:self didFinishCancelled:YES];
isDelegateOrCallbackHandled = YES;
}
// Check if the block version was implemented and call if so
if (self.onDidFinishCancelled != nil) {
self.onDidFinishCancelled(YES);
isDelegateOrCallbackHandled = YES;
}
// If neither callbacks were implemented, perform a default dismissing animation
if (!isDelegateOrCallbackHandled) {
if (self.navigationController) {
[self.navigationController popViewControllerAnimated:YES];
}
else {
self.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
}
}
- (void)doneButtonTapped
{
CGRect cropFrame = self.cropView.imageCropFrame;
NSInteger angle = self.cropView.angle;
//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];
NSMutableArray *activityItems = [@[imageItem, attributes] mutableCopy];
if (self.activityItems) {
[activityItems addObjectsFromArray:self.activityItems];
}
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:self.applicationActivities];
activityController.excludedActivityTypes = self.excludedActivityTypes;
activityController.modalPresentationStyle = UIModalPresentationPopover;
activityController.popoverPresentationController.sourceView = self.toolbar;
activityController.popoverPresentationController.sourceRect = self.toolbar.doneButtonFrame;
[self presentViewController:activityController animated:YES completion:nil];
__weak typeof(activityController) blockController = activityController;
activityController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
if (!completed) {
return;
}
bool isCallbackOrDelegateHandled = NO;
if (self.onDidFinishCancelled != nil) {
self.onDidFinishCancelled(NO);
isCallbackOrDelegateHandled = YES;
}
if ([self.delegate respondsToSelector:@selector(cropViewController:didFinishCancelled:)]) {
[self.delegate cropViewController:self didFinishCancelled:NO];
isCallbackOrDelegateHandled = YES;
}
if (!isCallbackOrDelegateHandled) {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
blockController.completionWithItemsHandler = nil;
}
};
return;
}
BOOL isCallbackOrDelegateHandled = NO;
//If the delegate/block that only supplies crop data is provided, call it
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);
isCallbackOrDelegateHandled = YES;
}
// Check if the circular APIs were implemented
BOOL isCircularImageDelegateAvailable = [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 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];
//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(), ^{
if (isCircularImageDelegateAvailable) {
[self.delegate cropViewController:self didCropToCircularImage:image withRect:cropFrame angle:angle];
}
if (isCircularImageCallbackAvailable) {
self.onDidCropToCircleImage(image, cropFrame, angle);
}
});
isCallbackOrDelegateHandled = YES;
}
//If the delegate/block that requires the specific cropped image is provided, call it
else if (isDidCropToImageDelegateAvailable || isDidCropToImageCallbackAvailable) {
UIImage *image = nil;
if (angle == 0 && CGRectEqualToRect(cropFrame, (CGRect){CGPointZero, self.image.size})) {
image = self.image;
}
else {
image = [self.image croppedImageWithFrame:cropFrame angle:angle 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(), ^{
if (isDidCropToImageDelegateAvailable) {
[self.delegate cropViewController:self didCropToImage:image withRect:cropFrame angle:angle];
}
if (isDidCropToImageCallbackAvailable) {
self.onDidCropToRect(image, cropFrame, angle);
}
});
isCallbackOrDelegateHandled = YES;
}
if (!isCallbackOrDelegateHandled) {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
}
#pragma mark - Property Methods -
- (void)setTitle:(NSString *)title
{
[super setTitle:title];
if (self.title.length == 0) {
[_titleLabel removeFromSuperview];
_cropView.cropRegionInsets = UIEdgeInsetsMake(0, 0, 0, 0);
_titleLabel = nil;
return;
}
self.titleLabel.text = self.title;
[self.titleLabel sizeToFit];
self.titleLabel.frame = [self frameForTitleLabelWithSize:self.titleLabel.frame.size verticalLayout:self.verticalLayout];
}
- (void)setDoneButtonTitle:(NSString *)title {
self.toolbar.doneTextButtonTitle = title;
}
- (void)setCancelButtonTitle:(NSString *)title {
self.toolbar.cancelTextButtonTitle = title;
}
- (TOCropView *)cropView {
// Lazily create the crop view in case we try and access it before presentation, but
// don't add it until our parent view controller view has loaded at the right time
if (!_cropView) {
_cropView = [[TOCropView alloc] initWithCroppingStyle:self.croppingStyle image:self.image];
_cropView.delegate = self;
_cropView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_cropView];
}
return _cropView;
}
- (TOCropToolbar *)toolbar {
if (!_toolbar) {
_toolbar = [[TOCropToolbar alloc] initWithFrame:CGRectZero];
[self.view addSubview:_toolbar];
}
return _toolbar;
}
- (UILabel *)titleLabel
{
if (!self.title.length) { return nil; }
if (_titleLabel) { return _titleLabel; }
_titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
_titleLabel.backgroundColor = [UIColor clearColor];
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.numberOfLines = 1;
_titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignBaselines;
_titleLabel.clipsToBounds = YES;
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.text = self.title;
[self.view insertSubview:self.titleLabel aboveSubview:self.cropView];
return _titleLabel;
}
- (void)setAspectRatioLockEnabled:(BOOL)aspectRatioLockEnabled
{
self.toolbar.clampButtonGlowing = aspectRatioLockEnabled;
self.cropView.aspectRatioLockEnabled = aspectRatioLockEnabled;
if (!self.aspectRatioPickerButtonHidden) {
self.aspectRatioPickerButtonHidden = (aspectRatioLockEnabled && self.resetAspectRatioEnabled == NO);
}
}
- (void)setAspectRatioLockDimensionSwapEnabled:(BOOL)aspectRatioLockDimensionSwapEnabled
{
self.cropView.aspectRatioLockDimensionSwapEnabled = aspectRatioLockDimensionSwapEnabled;
}
- (BOOL)aspectRatioLockEnabled
{
return self.cropView.aspectRatioLockEnabled;
}
- (void)setRotateButtonsHidden:(BOOL)rotateButtonsHidden
{
self.toolbar.rotateCounterclockwiseButtonHidden = rotateButtonsHidden;
self.toolbar.rotateClockwiseButtonHidden = rotateButtonsHidden;
}
- (void)setResetButtonHidden:(BOOL)resetButtonHidden
{
self.toolbar.resetButtonHidden = resetButtonHidden;
}
- (BOOL)rotateButtonsHidden
{
return self.toolbar.rotateCounterclockwiseButtonHidden && self.toolbar.rotateClockwiseButtonHidden;
}
- (void)setRotateClockwiseButtonHidden:(BOOL)rotateClockwiseButtonHidden
{
self.toolbar.rotateClockwiseButtonHidden = rotateClockwiseButtonHidden;
}
- (BOOL)rotateClockwiseButtonHidden {
return self.toolbar.rotateClockwiseButtonHidden;
}
- (void)setAspectRatioPickerButtonHidden:(BOOL)aspectRatioPickerButtonHidden
{
self.toolbar.clampButtonHidden = aspectRatioPickerButtonHidden;
}
- (BOOL)aspectRatioPickerButtonHidden
{
return self.toolbar.clampButtonHidden;
}
- (void)setDoneButtonHidden:(BOOL)doneButtonHidden
{
self.toolbar.doneButtonHidden = doneButtonHidden;
}
- (BOOL)doneButtonHidden
{
return self.toolbar.doneButtonHidden;
}
- (void)setCancelButtonHidden:(BOOL)cancelButtonHidden
{
self.toolbar.cancelButtonHidden = cancelButtonHidden;
}
- (BOOL)cancelButtonHidden
{
return self.toolbar.cancelButtonHidden;
}
- (void)setResetAspectRatioEnabled:(BOOL)resetAspectRatioEnabled
{
self.cropView.resetAspectRatioEnabled = resetAspectRatioEnabled;
if (!self.aspectRatioPickerButtonHidden) {
self.aspectRatioPickerButtonHidden = (resetAspectRatioEnabled == NO && self.aspectRatioLockEnabled);
}
}
- (void)setCustomAspectRatio:(CGSize)customAspectRatio
{
_customAspectRatio = customAspectRatio;
[self setAspectRatioPreset:TOCropViewControllerAspectRatioPresetCustom animated:NO];
}
- (BOOL)resetAspectRatioEnabled
{
return self.cropView.resetAspectRatioEnabled;
}
- (void)setAngle:(NSInteger)angle
{
self.cropView.angle = angle;
}
- (NSInteger)angle
{
return self.cropView.angle;
}
- (void)setImageCropFrame:(CGRect)imageCropFrame
{
self.cropView.imageCropFrame = imageCropFrame;
}
- (CGRect)imageCropFrame
{
return self.cropView.imageCropFrame;
}
- (BOOL)verticalLayout
{
return CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds);
}
- (BOOL)overrideStatusBar
{
// If we're pushed from a navigation controller, we'll defer
// to its handling of the status bar
if (self.navigationController) {
return NO;
}
// If the view controller presenting us already hid it, we don't need to
// do anything ourselves
if (self.presentingViewController.prefersStatusBarHidden) {
return NO;
}
// We'll handle the status bar
return YES;
}
- (BOOL)statusBarHidden
{
// Defer behaviour to the hosting navigation controller
if (self.navigationController) {
return self.navigationController.prefersStatusBarHidden;
}
//If our presenting controller has already hidden the status bar,
//hide the status bar by default
if (self.presentingViewController.prefersStatusBarHidden) {
return YES;
}
// Our default behaviour is to always hide the status bar
return YES;
}
- (CGFloat)statusBarHeight
{
CGFloat statusBarHeight = 0.0f;
if (@available(iOS 11.0, *)) {
statusBarHeight = self.view.safeAreaInsets.top;
// On non-Face ID devices, always disregard the top inset
// unless we explicitly set the status bar to be visible.
if (self.statusBarHidden &&
self.view.safeAreaInsets.bottom <= FLT_EPSILON)
{
statusBarHeight = 0.0f;
}
}
else {
if (self.statusBarHidden) {
statusBarHeight = 0.0f;
}
else {
statusBarHeight = self.topLayoutGuide.length;
}
}
return statusBarHeight;
}
- (UIEdgeInsets)statusBarSafeInsets
{
UIEdgeInsets insets = UIEdgeInsetsZero;
if (@available(iOS 11.0, *)) {
insets = self.view.safeAreaInsets;
insets.top = self.statusBarHeight;
}
else {
insets.top = self.statusBarHeight;
}
return insets;
}
- (void)setMinimumAspectRatio:(CGFloat)minimumAspectRatio
{
self.cropView.minimumAspectRatio = minimumAspectRatio;
}
- (CGFloat)minimumAspectRatio
{
return self.cropView.minimumAspectRatio;
}
@end