diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5865abc6..8fa42732 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: [push] jobs: build: - runs-on: macos-latest + runs-on: macos-26 steps: - uses: actions/checkout@v1 diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index df62e725..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,299 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - activesupport (7.1.3.2) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.4.0) - aws-partitions (1.1163.0) - aws-sdk-core (3.232.0) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - base64 - bigdecimal - jmespath (~> 1, >= 1.6.1) - logger - aws-sdk-kms (1.112.0) - aws-sdk-core (~> 3, >= 3.231.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.199.0) - aws-sdk-core (~> 3, >= 3.231.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.12.1) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - base64 (0.3.0) - bigdecimal (3.1.7) - claide (1.1.0) - cocoapods (1.15.2) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.15.2) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 2.1, < 3.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.6.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.15.2) - activesupport (>= 5.0, < 8) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (2.1) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - concurrent-ruby (1.2.3) - connection_pool (2.4.1) - declarative (0.0.20) - digest-crc (0.7.0) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) - dotenv (2.8.1) - drb (2.2.1) - emoji_regex (3.2.3) - escape (0.0.4) - ethon (0.16.0) - ffi (>= 1.15.0) - excon (0.112.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.1) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.1.1) - multipart-post (~> 2.0) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - fastimage (2.4.0) - fastlane (2.228.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored (~> 1.2) - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - http-cookie (~> 1.0.5) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (>= 2.0.0, < 3.0.0) - naturally (~> 2.2) - optparse (>= 0.1.1, < 1.0.0) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.4.1) - xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-sirp (1.0.0) - sysrandom (~> 1.0) - ffi (1.16.3) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.8.0) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.5.0) - google-cloud-storage (1.47.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.8) - domain_name (~> 0.5) - httpclient (2.9.0) - mutex_m - i18n (1.14.4) - concurrent-ruby (~> 1.0) - jmespath (1.6.2) - json (2.15.0) - jwt (2.10.2) - base64 - logger (1.7.0) - mini_magick (4.13.2) - mini_mime (1.1.5) - minitest (5.22.3) - molinillo (0.8.0) - multi_json (1.17.0) - multipart-post (2.4.1) - mutex_m (0.2.0) - nanaimo (0.4.0) - nap (1.1.0) - naturally (2.3.0) - netrc (0.11.0) - nkf (0.2.0) - optparse (0.6.0) - os (1.1.4) - plist (3.7.2) - public_suffix (4.0.7) - rake (13.3.0) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.4.4) - rouge (3.28.0) - ruby-macho (2.5.1) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - security (0.1.5) - signet (0.21.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - sysrandom (1.0.5) - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - typhoeus (1.4.1) - ethon (>= 0.9.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uber (0.1.0) - unicode-display_width (2.6.0) - word_wrap (1.0.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - rexml (>= 3.3.6, < 4.0) - xcpretty (0.4.1) - rouge (~> 3.28.0) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods - fastlane - -BUNDLED WITH - 2.5.7 diff --git a/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m b/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m index bd7edae6..d7068c82 100644 --- a/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m +++ b/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.m @@ -53,7 +53,9 @@ - (void)animateTransition:(id )transitionC if (self.isDismissing == NO) { [containerView addSubview:cropViewController.view]; - //Force a relayout now that the view is in the view hierarchy (so things like the safe area insets are now valid) + //Force a relayout now that the view is in the view hierarchy (so things like the safe area insets are now valid)] + [cropViewController.view setNeedsLayout]; + [cropViewController.view layoutIfNeeded]; [cropViewController viewDidLayoutSubviews]; } else { diff --git a/Objective-C/TOCropViewController/TOCropViewController.m b/Objective-C/TOCropViewController/TOCropViewController.m index 21a9d3fe..d26e1ba4 100755 --- a/Objective-C/TOCropViewController/TOCropViewController.m +++ b/Objective-C/TOCropViewController/TOCropViewController.m @@ -80,7 +80,11 @@ - (instancetype)initWithCroppingStyle:(TOCropViewCroppingStyle)style image:(UIIm // Init parameters _image = image; _croppingStyle = style; - + + if (@available(iOS 13.0, *)) { + self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; + } + // Set up base view controller behaviour self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; self.modalPresentationStyle = UIModalPresentationFullScreen; @@ -281,14 +285,48 @@ - (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.size.width = kTOCropViewControllerToolbarHeight; - frame.size.height = CGRectGetHeight(self.view.frame); + if (@available(iOS 26.0, *)) { + CGFloat minPadding = 8.0f; + frame.origin.x = insets.left + minPadding; + frame.origin.y = minPadding; + frame.size.width = kTOCropViewControllerToolbarHeight; + frame.size.height = CGRectGetHeight(self.view.frame) - (minPadding * 2.0f); + } else { + 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); + // On iOS 26, the safe area insets values are the same, however the default bottom value is so + // high that the buttons look incorrectly set. While you can try and hardcode a value lower to + // the bottom of the screen, trying to align the width with all the varied corner radii of modern iOS + // devices is also difficult. + + // For now, I've decided to use a private API to fetch the corner radius of the device so we can properly align + // the toolbar with the device's rounded corners. + // I've filed FB20413789 with Apple hoping that this can become a real solution in future. + if (@available(iOS 26.0, *)) { + if (insets.bottom > 0.0f) { + insets.bottom = 20.0f; + } else { + insets.bottom = 8.0f; + } + + const char* components[] = {"Radius", "Corner", "display", "_"}; + NSString *selectorName = @""; + for (NSInteger i = 3; i >= 0; i--) { + selectorName = [selectorName stringByAppendingString:[NSString stringWithCString:components[i] + encoding:NSUTF8StringEncoding]]; + } + const CGFloat cornerRadius = [[UIScreen.mainScreen valueForKey:selectorName] floatValue]; + frame.size.width = CGRectGetWidth(self.view.bounds) - MAX(cornerRadius, insets.bottom * 2.0f); + } else { + frame.size.width = CGRectGetWidth(self.view.bounds); + } + + frame.origin.x = (CGRectGetWidth(self.view.bounds) - frame.size.width) / 2.0f; frame.size.height = kTOCropViewControllerToolbarHeight; if (self.toolbarPosition == TOCropViewControllerToolbarPositionBottom) { @@ -314,6 +352,11 @@ - (CGRect)frameForCropViewWithVerticalLayout:(BOOL)verticalLayout view = self.parentViewController.view; } + // Always make the crop view edge-to-edge on iOS 26 and up + if (@available(iOS 26.0, *)) { + return view.bounds; + } + UIEdgeInsets insets = self.statusBarSafeInsets; CGRect bounds = view.bounds; @@ -368,18 +411,26 @@ - (void)adjustCropViewInsets { UIEdgeInsets insets = self.statusBarSafeInsets; + if (@available(iOS 26.0, *)) { + if (!self.verticalLayout) { + insets.left = CGRectGetMaxX(self.toolbar.frame); + } else { + insets.bottom = CGRectGetHeight(self.view.frame) - CGRectGetMinY(self.toolbar.frame); + } + } + // 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); - } + 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, insets.bottom, 0.0f); + } } else { - self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0f, 0.0f, insets.bottom, 0.0f); + self.cropView.cropRegionInsets = UIEdgeInsetsMake(0.0f, insets.left, insets.bottom, 0.0f); } return; @@ -394,7 +445,7 @@ - (void)adjustCropViewInsets CGFloat verticalInset = self.statusBarHeight; verticalInset += kTOCropViewControllerTitleTopPadding; verticalInset += self.titleLabel.frame.size.height; - self.cropView.cropRegionInsets = UIEdgeInsetsMake(verticalInset, 0, insets.bottom, 0); + self.cropView.cropRegionInsets = UIEdgeInsetsMake(verticalInset, insets.left, insets.bottom, 0); } - (void)adjustToolbarInsets @@ -611,12 +662,18 @@ - (void)setAspectRatioPreset:(CGSize)aspectRatioPreset animated:(BOOL)animated - (void)rotateCropViewClockwise { - [self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:YES]; + self.toolbar.disableRotationButtons = YES; + [self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:YES completion:^(BOOL success) { + self.toolbar.disableRotationButtons = NO; + }]; } - (void)rotateCropViewCounterclockwise { - [self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:NO]; + self.toolbar.disableRotationButtons = YES; + [self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:NO completion:^(BOOL success) { + self.toolbar.disableRotationButtons = NO; + }]; } #pragma mark - Crop View Delegates - @@ -1002,7 +1059,7 @@ - (TOCropView *)cropView _cropView = [[TOCropView alloc] initWithCroppingStyle:self.croppingStyle image:self.image]; _cropView.delegate = self; _cropView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self.view addSubview:_cropView]; + [self.view insertSubview:_cropView atIndex:0]; } return _cropView; } diff --git a/Objective-C/TOCropViewController/Views/TOCropToolbar.h b/Objective-C/TOCropViewController/Views/TOCropToolbar.h index bacc4341..94726be7 100644 --- a/Objective-C/TOCropViewController/Views/TOCropToolbar.h +++ b/Objective-C/TOCropViewController/Views/TOCropToolbar.h @@ -53,7 +53,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) NSString *cancelTextButtonTitle; @property (nullable, nonatomic, copy) UIColor *cancelButtonColor; -@property (nonatomic, assign) BOOL showOnlyIcons; +/* Show the tick and cross buttons instead of 'Done' and 'Cancel'. */ +@property (nonatomic, assign) BOOL showOnlyIcons API_DEPRECATED("iOS 26 uses icons only", ios(7.0, 18.0)); /* The cropper control buttons */ @property (nonatomic, strong, readonly) UIButton *rotateCounterclockwiseButton; @@ -61,6 +62,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, readonly) UIButton *clampButton; @property (nullable, nonatomic, strong, readonly) UIButton *rotateClockwiseButton; +/* Set the rotation buttons to be disabled while rotating is in progress */ +@property (nonatomic, assign) BOOL disableRotationButtons; + @property (nonatomic, readonly) UIButton *rotateButton; // Points to `rotateCounterClockwiseButton` /* Button feedback handler blocks */ diff --git a/Objective-C/TOCropViewController/Views/TOCropToolbar.m b/Objective-C/TOCropViewController/Views/TOCropToolbar.m index ff331f9c..7668d226 100644 --- a/Objective-C/TOCropViewController/Views/TOCropToolbar.m +++ b/Objective-C/TOCropViewController/Views/TOCropToolbar.m @@ -39,6 +39,8 @@ @interface TOCropToolbar() @property (nonatomic, strong) UIButton *rotateButton; // defaults to counterclockwise button for legacy compatibility +@property (nonatomic, strong) UIVisualEffectView *glassView; + @end @implementation TOCropToolbar @@ -54,57 +56,85 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)setup { self.backgroundView = [[UIView alloc] initWithFrame:self.bounds]; - self.backgroundView.backgroundColor = [UIColor colorWithWhite:0.12f alpha:1.0f]; - [self addSubview:self.backgroundView]; - + + UIView *containerView = self; + if (@available(iOS 26.0, *)) { + UIGlassEffect *glassEffect = [UIGlassEffect effectWithStyle:UIGlassEffectStyleClear]; + glassEffect.interactive = YES; + _glassView = [[UIVisualEffectView alloc] initWithEffect:glassEffect]; + _glassView.cornerConfiguration = [UICornerConfiguration capsuleConfiguration]; + _glassView.userInteractionEnabled = YES; + [self addSubview:_glassView]; + + containerView = _glassView.contentView; + _showOnlyIcons = YES; + } else { + self.backgroundView.backgroundColor = [UIColor colorWithWhite:0.12f alpha:1.0f]; + [self addSubview:self.backgroundView]; + } + // On iOS 9 and up, we can use the new layout features to determine whether we're in an 'Arabic' style language mode _reverseContentLayout = ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft); // Get the resource bundle depending on the framework/dependency manager we're using NSBundle *resourceBundle = TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(self); - - _doneTextButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [_doneTextButton setTitle: _doneTextButtonTitle ? - _doneTextButtonTitle : NSLocalizedStringFromTableInBundle(@"Done", - @"TOCropViewControllerLocalizable", - resourceBundle, - nil) - forState:UIControlStateNormal]; - [_doneTextButton setTitleColor:[UIColor colorWithRed:1.0f green:0.8f blue:0.0f alpha:1.0f] forState:UIControlStateNormal]; - if (@available(iOS 13.0, *)) { - [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f weight:UIFontWeightMedium]]; - } else { - [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f]]; + + if (@available(iOS 26.0, *)) {} + else { + _doneTextButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_doneTextButton setTitle: _doneTextButtonTitle ? + _doneTextButtonTitle : NSLocalizedStringFromTableInBundle(@"Done", + @"TOCropViewControllerLocalizable", + resourceBundle, + nil) + forState:UIControlStateNormal]; + [_doneTextButton setTitleColor:[UIColor colorWithRed:1.0f green:0.8f blue:0.0f alpha:1.0f] forState:UIControlStateNormal]; + if (@available(iOS 13.0, *)) { + [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f weight:UIFontWeightMedium]]; + } else { + [_doneTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f]]; + } + [_doneTextButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [_doneTextButton sizeToFit]; + [self addSubview:_doneTextButton]; } - [_doneTextButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [_doneTextButton sizeToFit]; - [self addSubview:_doneTextButton]; - + _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 addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + if (@available(iOS 26.0, *)) { + UIButtonConfiguration *configuration = [UIButtonConfiguration prominentGlassButtonConfiguration]; + configuration.baseForegroundColor = [UIColor blackColor]; + _doneIconButton.configuration = configuration; + } [self addSubview:_doneIconButton]; // Set the default color for the done buttons self.doneButtonColor = nil; - _cancelTextButton = [UIButton buttonWithType:UIButtonTypeSystem]; - - [_cancelTextButton setTitle: _cancelTextButtonTitle ? - _cancelTextButtonTitle : NSLocalizedStringFromTableInBundle(@"Cancel", - @"TOCropViewControllerLocalizable", - resourceBundle, - nil) - forState:UIControlStateNormal]; - [_cancelTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f]]; - [_cancelTextButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [_cancelTextButton sizeToFit]; - [self addSubview:_cancelTextButton]; - + if (@available(iOS 26.0, *)) {} + else { + _cancelTextButton = [UIButton buttonWithType:UIButtonTypeSystem]; + + [_cancelTextButton setTitle: _cancelTextButtonTitle ? + _cancelTextButtonTitle : NSLocalizedStringFromTableInBundle(@"Cancel", + @"TOCropViewControllerLocalizable", + resourceBundle, + nil) + forState:UIControlStateNormal]; + [_cancelTextButton.titleLabel setFont:[UIFont systemFontOfSize:17.0f]]; + [_cancelTextButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [_cancelTextButton sizeToFit]; + [self addSubview:_cancelTextButton]; + } + _cancelIconButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_cancelIconButton setImage:[TOCropToolbar cancelImage] forState:UIControlStateNormal]; [_cancelIconButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + if (@available(iOS 26.0, *)) { + _cancelIconButton.configuration = [UIButtonConfiguration clearGlassButtonConfiguration]; + } [self addSubview:_cancelIconButton]; _clampButton = [UIButton buttonWithType:UIButtonTypeSystem]; @@ -112,22 +142,22 @@ - (void)setup { _clampButton.tintColor = [UIColor whiteColor]; [_clampButton setImage:[TOCropToolbar clampImage] forState:UIControlStateNormal]; [_clampButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:_clampButton]; - + [containerView addSubview:_clampButton]; + _rotateCounterclockwiseButton = [UIButton buttonWithType:UIButtonTypeSystem]; _rotateCounterclockwiseButton.contentMode = UIViewContentModeCenter; _rotateCounterclockwiseButton.tintColor = [UIColor whiteColor]; [_rotateCounterclockwiseButton setImage:[TOCropToolbar rotateCCWImage] forState:UIControlStateNormal]; [_rotateCounterclockwiseButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:_rotateCounterclockwiseButton]; - + [containerView addSubview:_rotateCounterclockwiseButton]; + _rotateClockwiseButton = [UIButton buttonWithType:UIButtonTypeSystem]; _rotateClockwiseButton.contentMode = UIViewContentModeCenter; _rotateClockwiseButton.tintColor = [UIColor whiteColor]; [_rotateClockwiseButton setImage:[TOCropToolbar rotateCWImage] forState:UIControlStateNormal]; [_rotateClockwiseButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:_rotateClockwiseButton]; - + [containerView addSubview:_rotateClockwiseButton]; + _resetButton = [UIButton buttonWithType:UIButtonTypeSystem]; _resetButton.contentMode = UIViewContentModeCenter; _resetButton.tintColor = [UIColor whiteColor]; @@ -138,7 +168,7 @@ - (void)setup { @"TOCropViewControllerLocalizable", resourceBundle, nil); - [self addSubview:_resetButton]; + [containerView addSubview:_resetButton]; } - (void)layoutSubviews @@ -147,7 +177,7 @@ - (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); @@ -174,7 +204,10 @@ - (void)layoutSubviews if (verticalLayout == NO) { CGFloat insetPadding = 10.0f; - + if (@available(iOS 26.0, *)) { + insetPadding = 0.0f; + } + // Work out the cancel button frame CGRect frame = CGRectZero; frame.size.height = 44.0f; @@ -279,14 +312,54 @@ - (void)layoutSubviews } // The convenience method for calculating button's frame inside of the container rect -- (void)layoutToolbarButtons:(NSArray *)buttons withSameButtonSize:(CGSize)size inContainerRect:(CGRect)containerRect horizontally:(BOOL)horizontally +- (void)layoutToolbarButtons:(NSArray *)buttons withSameButtonSize:(CGSize)size inContainerRect:(CGRect)containerRect horizontally:(BOOL)horizontally { - if (buttons.count > 0){ + if (!buttons.count) { + return; + } + + const CGFloat buttonSize = 44.0f; + + if (@available(iOS 26.0, *)) { + CGFloat glassPadding = 6.0f; + CGFloat minSpacing = 16.0f; + CGFloat minPadding = 12.0f; + CGFloat maxExtent = 0.0f; + + // On iPad, we'll align in the middle. On iPhone, we'll stretch between Done and Cancel. + if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact || + self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) { + CGFloat containerExtent = horizontally ? containerRect.size.width : containerRect.size.height; + maxExtent = containerExtent - (minSpacing * 2.0f); + } else { + maxExtent = buttons.count * buttonSize + (minPadding * (buttons.count - 1)) + (glassPadding * 2.0f); + } + + CGRect glassFrame = CGRectZero; + glassFrame.size.width = horizontally ? maxExtent : buttonSize; + glassFrame.size.height = horizontally ? buttonSize : maxExtent; + glassFrame.origin.x = horizontally ? CGRectGetMidX(containerRect) - (glassFrame.size.width / 2.0f) : 0.0f; + glassFrame.origin.y = horizontally ? 0.0f : CGRectGetMidY(containerRect) - (glassFrame.size.height / 2.0f); + _glassView.frame = glassFrame; + + CGFloat buttonPadding = (maxExtent - (glassPadding * 2.0f) - (buttons.count * buttonSize)) / (buttons.count - 1); + CGFloat position = glassPadding; + for (UIButton *button in buttons) { + CGRect buttonFrame = CGRectMake(0.0, 0.0, buttonSize, buttonSize); + if (horizontally) { + buttonFrame.origin.x = position; + } else { + buttonFrame.origin.y = position; + } + button.frame = buttonFrame; + position += buttonSize + buttonPadding; + } + } else { NSInteger count = buttons.count; CGFloat fixedSize = horizontally ? size.width : size.height; CGFloat maxLength = horizontally ? CGRectGetWidth(containerRect) : CGRectGetHeight(containerRect); CGFloat padding = (maxLength - fixedSize * count) / (count + 1); - + for (NSInteger i = 0; i < count; i++) { UIButton *button = buttons[i]; CGFloat sameOffset = horizontally ? fabs(CGRectGetHeight(containerRect)-44.0f) : fabs(CGRectGetWidth(containerRect)-size.width); @@ -694,4 +767,14 @@ - (UIView *)visibleCancelButton return self.cancelTextButton; } +- (void)setDisableRotationButtons:(BOOL)disableRotationButtons +{ + if (_disableRotationButtons == disableRotationButtons) { + return; + } + _disableRotationButtons = disableRotationButtons; + _rotateClockwiseButton.enabled = !disableRotationButtons; + _rotateCounterclockwiseButton.enabled = !disableRotationButtons; +} + @end diff --git a/Objective-C/TOCropViewController/Views/TOCropView.h b/Objective-C/TOCropViewController/Views/TOCropView.h index 9708b07f..ad706472 100755 --- a/Objective-C/TOCropViewController/Views/TOCropView.h +++ b/Objective-C/TOCropViewController/Views/TOCropView.h @@ -265,7 +265,7 @@ The minimum croping aspect ratio. If set, user is prevented from setting croppin @param animated Whether the transition is animated */ -- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated; +- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated completion:(nullable void(^)(BOOL completed))completionHandler; /** Rotates the entire canvas to a 90-degree angle @@ -273,7 +273,7 @@ The minimum croping aspect ratio. If set, user is prevented from setting croppin @param animated Whether the transition is animated @param clockwise Whether the rotation is clockwise. Passing 'NO' means counterclockwise */ -- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwise; +- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwise completion:(nullable void(^)(BOOL completed))completionHandler; /** 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 b458f8ee..f31e8eb3 100755 --- a/Objective-C/TOCropViewController/Views/TOCropView.m +++ b/Objective-C/TOCropViewController/Views/TOCropView.m @@ -1218,12 +1218,12 @@ - (void)setAngle:(NSInteger)angle // on direction if (newAngle >= 0) { while (labs(self.angle) != labs(newAngle)) { - [self rotateImageNinetyDegreesAnimated:NO clockwise:YES]; + [self rotateImageNinetyDegreesAnimated:NO clockwise:YES completion:nil]; } } else { while (-labs(self.angle) != -labs(newAngle)) { - [self rotateImageNinetyDegreesAnimated:NO clockwise:NO]; + [self rotateImageNinetyDegreesAnimated:NO clockwise:NO completion:nil]; } } } @@ -1503,12 +1503,12 @@ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated completion:nil]; } -- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated +- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated completion:(void(^)(BOOL completed))completionHandler { - [self rotateImageNinetyDegreesAnimated:animated clockwise:NO]; + [self rotateImageNinetyDegreesAnimated:animated clockwise:NO completion:completionHandler]; } -- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwise +- (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwise completion:(void(^)(BOOL completed))completionHandler { //Only allow one rotation animation at a time if (self.rotateAnimationInProgress) @@ -1666,7 +1666,7 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis } completion:^(BOOL complete) { self.rotateAnimationInProgress = NO; [snapshotView removeFromSuperview]; - + // 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. @@ -1677,6 +1677,10 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis //This will animate the aspect ratio back to the desired locked ratio after the image is rotated. [self setAspectRatio:self.aspectRatio animated:animated]; } + + if (completionHandler) { + completionHandler(complete); + } }]; }]; } diff --git a/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample-Extension.entitlements b/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample-Extension.entitlements index ee95ab7e..0c67376e 100644 --- a/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample-Extension.entitlements +++ b/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample-Extension.entitlements @@ -1,10 +1,5 @@ - - com.apple.security.app-sandbox - - com.apple.security.network.client - - + diff --git a/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample.entitlements b/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample.entitlements index 3f1733d7..0c67376e 100644 --- a/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample.entitlements +++ b/Objective-C/TOCropViewControllerExample/TOCropViewControllerExample.entitlements @@ -1,14 +1,5 @@ - - com.apple.security.app-sandbox - - com.apple.security.device.camera - - com.apple.security.network.client - - com.apple.security.personal-information.photos-library - - + diff --git a/TOCropViewControllerExample.xcodeproj/project.pbxproj b/TOCropViewControllerExample.xcodeproj/project.pbxproj index a12092e9..b8226cf6 100644 --- a/TOCropViewControllerExample.xcodeproj/project.pbxproj +++ b/TOCropViewControllerExample.xcodeproj/project.pbxproj @@ -653,7 +653,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0910; - LastUpgradeCheck = 1520; + LastUpgradeCheck = 2610; ORGANIZATIONNAME = "Tim Oliver"; TargetAttributes = { 144B8CC81D22CAFF0085D774 = { @@ -1003,7 +1003,6 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6LF3GMKZAB; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1044,7 +1043,6 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6LF3GMKZAB; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1078,7 +1076,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 6LF3GMKZAB; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -1105,7 +1102,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 6LF3GMKZAB; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "Objective-C/TOCropViewControllerTests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -1154,6 +1150,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = 6LF3GMKZAB; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1177,6 +1174,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1213,6 +1211,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = 6LF3GMKZAB; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1228,6 +1227,7 @@ MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; @@ -1241,7 +1241,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Objective-C/TOCropViewControllerExample/TOCropViewControllerExample.entitlements"; - DEVELOPMENT_TEAM = 6LF3GMKZAB; + ENABLE_APP_SANDBOX = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_CAMERA = YES; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", @@ -1266,7 +1269,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Objective-C/TOCropViewControllerExample/TOCropViewControllerExample.entitlements"; - DEVELOPMENT_TEAM = 6LF3GMKZAB; + ENABLE_APP_SANDBOX = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_CAMERA = YES; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", @@ -1298,7 +1304,6 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 6LF3GMKZAB; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Swift/CropViewControllerExample/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -1326,7 +1331,6 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 6LF3GMKZAB; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Swift/CropViewControllerExample/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -1358,7 +1362,6 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6LF3GMKZAB; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1403,7 +1406,6 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6LF3GMKZAB; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1443,7 +1445,8 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 6LF3GMKZAB; + ENABLE_APP_SANDBOX = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", TARGET_APP_EXTENSION, @@ -1476,7 +1479,8 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 6LF3GMKZAB; + ENABLE_APP_SANDBOX = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO; INFOPLIST_FILE = "$(SRCROOT)/Objective-C/TOCropViewControllerExample-Extension/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; diff --git a/TOCropViewControllerExample.xcodeproj/xcshareddata/xcschemes/CropViewController.xcscheme b/TOCropViewControllerExample.xcodeproj/xcshareddata/xcschemes/CropViewController.xcscheme index b4ee5a45..2c1b9216 100644 --- a/TOCropViewControllerExample.xcodeproj/xcshareddata/xcschemes/CropViewController.xcscheme +++ b/TOCropViewControllerExample.xcodeproj/xcshareddata/xcschemes/CropViewController.xcscheme @@ -1,6 +1,6 @@