@onekeyfe/react-native-image-crop-picker 3.0.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/ReactNativeImageCropPicker.podspec +40 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +144 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +24 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerException.kt +45 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +308 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +297 -0
  12. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPicker.kt +69 -0
  13. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPickerPackage.kt +24 -0
  14. package/ios/ImageCropPickerError.swift +57 -0
  15. package/ios/ImageCropPickerImageProcessor.swift +343 -0
  16. package/ios/ImageCropPickerSession.swift +407 -0
  17. package/ios/ReactNativeImageCropPicker.swift +65 -0
  18. package/ios/TOCropViewController/Categories/UIImage+CropRotate.h +39 -0
  19. package/ios/TOCropViewController/Categories/UIImage+CropRotate.m +94 -0
  20. package/ios/TOCropViewController/Constants/TOCropViewConstants.h +63 -0
  21. package/ios/TOCropViewController/LICENSE +21 -0
  22. package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.h +38 -0
  23. package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.m +73 -0
  24. package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.h +64 -0
  25. package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.m +121 -0
  26. package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.h +49 -0
  27. package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.m +125 -0
  28. package/ios/TOCropViewController/Models/TOCroppedImageAttributes.h +38 -0
  29. package/ios/TOCropViewController/Models/TOCroppedImageAttributes.m +45 -0
  30. package/ios/TOCropViewController/Resources/Base.lproj/TOCropViewControllerLocalizable.strings +8 -0
  31. package/ios/TOCropViewController/Resources/PrivacyInfo.xcprivacy +14 -0
  32. package/ios/TOCropViewController/Resources/ar.lproj/TOCropViewControllerLocalizable.strings +9 -0
  33. package/ios/TOCropViewController/Resources/ca.lproj/TOCropViewControllerLocalizable.strings +8 -0
  34. package/ios/TOCropViewController/Resources/cs.lproj/TOCropViewControllerLocalizable.strings +8 -0
  35. package/ios/TOCropViewController/Resources/da-DK.lproj/TOCropViewControllerLocalizable.strings +8 -0
  36. package/ios/TOCropViewController/Resources/de.lproj/TOCropViewControllerLocalizable.strings +8 -0
  37. package/ios/TOCropViewController/Resources/en.lproj/TOCropViewControllerLocalizable.strings +8 -0
  38. package/ios/TOCropViewController/Resources/es.lproj/TOCropViewControllerLocalizable.strings +8 -0
  39. package/ios/TOCropViewController/Resources/fa-IR.lproj/TOCropViewControllerLocalizable.strings +8 -0
  40. package/ios/TOCropViewController/Resources/fa.lproj/TOCropViewControllerLocalizable.strings +8 -0
  41. package/ios/TOCropViewController/Resources/fi.lproj/TOCropViewControllerLocalizable.strings +8 -0
  42. package/ios/TOCropViewController/Resources/fr.lproj/TOCropViewControllerLocalizable.strings +8 -0
  43. package/ios/TOCropViewController/Resources/hu.lproj/TOCropViewControllerLocalizable.strings +9 -0
  44. package/ios/TOCropViewController/Resources/id.lproj/TOCropViewControllerLocalizable.strings +8 -0
  45. package/ios/TOCropViewController/Resources/it.lproj/TOCropViewControllerLocalizable.strings +8 -0
  46. package/ios/TOCropViewController/Resources/ja.lproj/TOCropViewControllerLocalizable.strings +8 -0
  47. package/ios/TOCropViewController/Resources/ko.lproj/TOCropViewControllerLocalizable.strings +8 -0
  48. package/ios/TOCropViewController/Resources/ms.lproj/TOCropViewControllerLocalizable.strings +8 -0
  49. package/ios/TOCropViewController/Resources/nl.lproj/TOCropViewControllerLocalizable.strings +8 -0
  50. package/ios/TOCropViewController/Resources/pl.lproj/TOCropViewControllerLocalizable.strings +8 -0
  51. package/ios/TOCropViewController/Resources/pt-BR.lproj/TOCropViewControllerLocalizable.strings +8 -0
  52. package/ios/TOCropViewController/Resources/pt.lproj/TOCropViewControllerLocalizable.strings +8 -0
  53. package/ios/TOCropViewController/Resources/ro.lproj/TOCropViewControllerLocalizable.strings +8 -0
  54. package/ios/TOCropViewController/Resources/ru.lproj/TOCropViewControllerLocalizable.strings +8 -0
  55. package/ios/TOCropViewController/Resources/sk.lproj/TOCropViewControllerLocalizable.strings +8 -0
  56. package/ios/TOCropViewController/Resources/tr.lproj/TOCropViewControllerLocalizable.strings +9 -0
  57. package/ios/TOCropViewController/Resources/uk.lproj/TOCropViewControllerLocalizable.strings +8 -0
  58. package/ios/TOCropViewController/Resources/vi.lproj/TOCropViewControllerLocalizable.strings +9 -0
  59. package/ios/TOCropViewController/Resources/zh-Hans.lproj/TOCropViewControllerLocalizable.strings +9 -0
  60. package/ios/TOCropViewController/Resources/zh-Hant.lproj/TOCropViewControllerLocalizable.strings +9 -0
  61. package/ios/TOCropViewController/TOCropViewController.h +479 -0
  62. package/ios/TOCropViewController/TOCropViewController.m +1355 -0
  63. package/ios/TOCropViewController/Views/TOCropOverlayView.h +43 -0
  64. package/ios/TOCropViewController/Views/TOCropOverlayView.m +240 -0
  65. package/ios/TOCropViewController/Views/TOCropScrollView.h +39 -0
  66. package/ios/TOCropViewController/Views/TOCropScrollView.m +48 -0
  67. package/ios/TOCropViewController/Views/TOCropToolbar.h +104 -0
  68. package/ios/TOCropViewController/Views/TOCropToolbar.m +783 -0
  69. package/ios/TOCropViewController/Views/TOCropView.h +311 -0
  70. package/ios/TOCropViewController/Views/TOCropView.m +1787 -0
  71. package/lib/module/ReactNativeImageCropPicker.nitro.js +4 -0
  72. package/lib/module/ReactNativeImageCropPicker.nitro.js.map +1 -0
  73. package/lib/module/index.js +83 -0
  74. package/lib/module/index.js.map +1 -0
  75. package/lib/module/package.json +1 -0
  76. package/lib/typescript/package.json +1 -0
  77. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +54 -0
  78. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -0
  79. package/lib/typescript/src/index.d.ts +31 -0
  80. package/lib/typescript/src/index.d.ts.map +1 -0
  81. package/nitro.json +23 -0
  82. package/nitrogen/generated/android/c++/JCropRect.hpp +69 -0
  83. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +125 -0
  84. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.hpp +66 -0
  85. package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +154 -0
  86. package/nitrogen/generated/android/c++/JPickedImage.hpp +88 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/CropRect.kt +66 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/HybridReactNativeImageCropPickerSpec.kt +69 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +171 -0
  90. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/PickedImage.kt +86 -0
  91. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/reactnativeimagecroppickerOnLoad.kt +35 -0
  92. package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.cmake +81 -0
  93. package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.gradle +27 -0
  94. package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.cpp +54 -0
  95. package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.hpp +34 -0
  96. package/nitrogen/generated/ios/ReactNativeImageCropPicker+autolinking.rb +62 -0
  97. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.cpp +57 -0
  98. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +221 -0
  99. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +55 -0
  100. package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.mm +33 -0
  101. package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.swift +26 -0
  102. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.cpp +11 -0
  103. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +116 -0
  104. package/nitrogen/generated/ios/swift/CropRect.swift +44 -0
  105. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  106. package/nitrogen/generated/ios/swift/Func_void_PickedImage.swift +46 -0
  107. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  108. package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec.swift +58 -0
  109. package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec_cxx.swift +202 -0
  110. package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +474 -0
  111. package/nitrogen/generated/ios/swift/PickedImage.swift +96 -0
  112. package/nitrogen/generated/shared/c++/CropRect.hpp +95 -0
  113. package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.cpp +24 -0
  114. package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.hpp +71 -0
  115. package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +180 -0
  116. package/nitrogen/generated/shared/c++/PickedImage.hpp +114 -0
  117. package/package.json +173 -0
  118. package/src/ReactNativeImageCropPicker.nitro.ts +72 -0
  119. package/src/index.tsx +134 -0
@@ -0,0 +1,1355 @@
1
+ //
2
+ // TOCropViewController.m
3
+ //
4
+ // Copyright 2015-2026 Timothy Oliver. All rights reserved.
5
+ //
6
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ // of this software and associated documentation files (the "Software"), to
8
+ // deal in the Software without restriction, including without limitation the
9
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ // sell copies of the Software, and to permit persons to whom the Software is
11
+ // furnished to do so, subject to the following conditions:
12
+ //
13
+ // The above copyright notice and this permission notice shall be included in
14
+ // all copies or substantial portions of the Software.
15
+ //
16
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21
+ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ #import "TOCropViewController.h"
24
+
25
+ #import "TOActivityCroppedImageProvider.h"
26
+ #import "TOCroppedImageAttributes.h"
27
+ #import "TOCropViewControllerTransitioning.h"
28
+ #import "UIImage+CropRotate.h"
29
+
30
+ static const CGFloat kTOCropViewControllerTitleTopPadding = 14.0f;
31
+ static const CGFloat kTOCropViewControllerToolbarHeight = 44.0f;
32
+
33
+ @interface TOCropViewController () <UIActionSheetDelegate, UIViewControllerTransitioningDelegate, TOCropViewDelegate>
34
+
35
+ /* The target image */
36
+ @property (nonatomic, readwrite) UIImage *image;
37
+
38
+ /* The cropping style of the crop view */
39
+ @property (nonatomic, assign, readwrite) TOCropViewCroppingStyle croppingStyle;
40
+
41
+ /* Views */
42
+ @property (nonatomic, strong) TOCropToolbar *toolbar;
43
+ @property (nonatomic, strong, readwrite) TOCropView *cropView;
44
+ @property (nonatomic, strong) UIView *toolbarSnapshotView;
45
+ @property (nonatomic, strong, readwrite) UILabel *titleLabel;
46
+
47
+ /* Transition animation controller */
48
+ @property (nonatomic, copy) void (^prepareForTransitionHandler)(void);
49
+ @property (nonatomic, strong) TOCropViewControllerTransitioning *transitionController;
50
+ @property (nonatomic, assign) BOOL inTransition;
51
+
52
+ /* If pushed from a navigation controller, the visibility of that controller's bars. */
53
+ @property (nonatomic, assign) BOOL navigationBarHidden;
54
+ @property (nonatomic, assign) BOOL toolbarHidden;
55
+
56
+ /* State for whether content is being laid out vertically or horizontally */
57
+ @property (nonatomic, readonly) BOOL verticalLayout;
58
+
59
+ /* Convenience method for managing status bar state */
60
+ @property (nonatomic, readonly) BOOL overrideStatusBar; // Whether the view controller needs to touch the status bar
61
+ @property (nonatomic, readonly) BOOL statusBarHidden; // Whether it should be hidden or visible at this point
62
+ @property (nonatomic, readonly) CGFloat statusBarHeight; // The height of the status bar when visible
63
+
64
+ /* Convenience method for getting the vertical inset for both iPhone X and status bar */
65
+ @property (nonatomic, readonly) UIEdgeInsets statusBarSafeInsets;
66
+
67
+ /* Flag to perform initial setup on the first run */
68
+ @property (nonatomic, assign) BOOL firstTime;
69
+
70
+ @end
71
+
72
+ @implementation TOCropViewController
73
+
74
+ - (instancetype)initWithCroppingStyle:(TOCropViewCroppingStyle)style image:(UIImage *)image {
75
+ NSParameterAssert(image);
76
+
77
+ self = [super initWithNibName:nil bundle:nil];
78
+ if (self) {
79
+ // Init parameters
80
+ _image = image;
81
+ _croppingStyle = style;
82
+
83
+ if (@available(iOS 13.0, *)) {
84
+ self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
85
+ }
86
+
87
+ // Set up base view controller behaviour
88
+ self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
89
+ self.modalPresentationStyle = UIModalPresentationFullScreen;
90
+ self.hidesNavigationBar = true;
91
+
92
+ // Controller object that handles the transition animation when presenting / dismissing this app
93
+ _transitionController = [[TOCropViewControllerTransitioning alloc] init];
94
+
95
+ // Default initial behaviour
96
+ _aspectRatioPreset = CGSizeZero;
97
+
98
+ #if TARGET_OS_MACCATALYST
99
+ _toolbarPosition = TOCropViewControllerToolbarPositionTop;
100
+ #else
101
+ _toolbarPosition = TOCropViewControllerToolbarPositionBottom;
102
+ #endif
103
+ }
104
+
105
+ return self;
106
+ }
107
+
108
+ - (instancetype)initWithImage:(UIImage *)image {
109
+ return [self initWithCroppingStyle:TOCropViewCroppingStyleDefault image:image];
110
+ }
111
+
112
+ - (void)viewDidLoad {
113
+ [super viewDidLoad];
114
+
115
+ // Set up view controller properties
116
+ self.transitioningDelegate = self;
117
+ self.view.backgroundColor = self.cropView.backgroundColor;
118
+
119
+ BOOL circularMode = (self.croppingStyle == TOCropViewCroppingStyleCircular);
120
+
121
+ // Layout the views initially
122
+ self.cropView.frame = [self frameForCropViewWithVerticalLayout:self.verticalLayout];
123
+ self.toolbar.frame = [self frameForToolbarWithVerticalLayout:self.verticalLayout];
124
+
125
+ // Set up toolbar default behaviour
126
+ self.toolbar.clampButtonHidden = self.aspectRatioPickerButtonHidden || circularMode;
127
+ self.toolbar.rotateClockwiseButtonHidden = self.rotateClockwiseButtonHidden;
128
+
129
+ // Set up the toolbar button actions
130
+ __weak typeof(self) weakSelf = self;
131
+ self.toolbar.doneButtonTapped = ^{ [weakSelf doneButtonTapped]; };
132
+ self.toolbar.cancelButtonTapped = ^{ [weakSelf cancelButtonTapped]; };
133
+ self.toolbar.resetButtonTapped = ^{ [weakSelf resetCropViewLayout]; };
134
+ self.toolbar.clampButtonTapped = ^{ [weakSelf showAspectRatioDialog]; };
135
+ self.toolbar.rotateCounterclockwiseButtonTapped = ^{ [weakSelf rotateCropViewCounterclockwise]; };
136
+ self.toolbar.rotateClockwiseButtonTapped = ^{ [weakSelf rotateCropViewClockwise]; };
137
+ }
138
+
139
+ - (void)viewWillAppear:(BOOL)animated {
140
+ [super viewWillAppear:animated];
141
+
142
+ // If we're animating onto the screen, set a flag
143
+ // so we can manually control the status bar fade out timing
144
+ if (animated) {
145
+ self.inTransition = YES;
146
+ #if TARGET_OS_IOS
147
+ [self setNeedsStatusBarAppearanceUpdate];
148
+ #endif
149
+ }
150
+
151
+ // If this controller is pushed onto a navigation stack, set flags noting the
152
+ // state of the navigation controller bars before we present, and then hide them
153
+ if (self.navigationController) {
154
+ if (self.hidesNavigationBar) {
155
+ self.navigationBarHidden = self.navigationController.navigationBarHidden;
156
+ self.toolbarHidden = self.navigationController.toolbarHidden;
157
+ [self.navigationController setNavigationBarHidden:YES animated:animated];
158
+ [self.navigationController setToolbarHidden:YES animated:animated];
159
+ }
160
+
161
+ self.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
162
+ } else {
163
+ // Hide the background content when transitioning for performance
164
+ [self.cropView setBackgroundImageViewHidden:YES animated:NO];
165
+
166
+ // The title label will fade
167
+ self.titleLabel.alpha = animated ? 0.0f : 1.0f;
168
+ }
169
+
170
+ // If an initial aspect ratio was set before presentation, set it now once the rest of
171
+ // the setup will have been done
172
+ if (!CGSizeEqualToSize(self.aspectRatioPreset, CGSizeZero)) {
173
+ [self setAspectRatioPreset:self.aspectRatioPreset animated:NO];
174
+ }
175
+ }
176
+
177
+ - (void)viewDidAppear:(BOOL)animated {
178
+ [super viewDidAppear:animated];
179
+
180
+ // Disable the transition flag for the status bar
181
+ self.inTransition = NO;
182
+
183
+ // Re-enable translucency now that the animation has completed
184
+ self.cropView.simpleRenderMode = NO;
185
+
186
+ // Now that the presentation animation will have finished, animate
187
+ // the status bar fading out, and if present, the title label fading in
188
+ void (^updateContentBlock)(void) = ^{
189
+ #if TARGET_OS_IOS
190
+ [self setNeedsStatusBarAppearanceUpdate];
191
+ #endif
192
+ self.titleLabel.alpha = 1.0f;
193
+ };
194
+
195
+ if (animated) {
196
+ [UIView animateWithDuration:0.3f animations:updateContentBlock];
197
+ } else {
198
+ updateContentBlock();
199
+ }
200
+
201
+ // Make the grid overlay view fade in
202
+ if (self.cropView.gridOverlayHidden) {
203
+ [self.cropView setGridOverlayHidden:NO animated:animated];
204
+ }
205
+
206
+ // Fade in the background view content
207
+ if (self.navigationController == nil) {
208
+ [self.cropView setBackgroundImageViewHidden:NO animated:animated];
209
+ }
210
+ }
211
+
212
+ - (void)viewWillDisappear:(BOOL)animated {
213
+ [super viewWillDisappear:animated];
214
+
215
+ // Set the transition flag again so we can defer the status bar
216
+ self.inTransition = YES;
217
+ #if TARGET_OS_IOS
218
+ [UIView animateWithDuration:0.5f
219
+ animations:^{ [self setNeedsStatusBarAppearanceUpdate]; }];
220
+ #endif
221
+
222
+ // Restore the navigation controller to its state before we were presented
223
+ if (self.navigationController && self.hidesNavigationBar) {
224
+ [self.navigationController setNavigationBarHidden:self.navigationBarHidden animated:animated];
225
+ [self.navigationController setToolbarHidden:self.toolbarHidden animated:animated];
226
+ }
227
+ }
228
+
229
+ - (void)viewDidDisappear:(BOOL)animated {
230
+ [super viewDidDisappear:animated];
231
+
232
+ // Reset the state once the view has gone offscreen
233
+ self.inTransition = NO;
234
+ #if TARGET_OS_IOS
235
+ [self setNeedsStatusBarAppearanceUpdate];
236
+ #endif
237
+ }
238
+
239
+ #pragma mark - Status Bar -
240
+ - (UIStatusBarStyle)preferredStatusBarStyle {
241
+ if (self.navigationController) {
242
+ return UIStatusBarStyleLightContent;
243
+ }
244
+
245
+ // Even though we are a dark theme, leave the status bar
246
+ // as black so it's not obvious that it's still visible during the transition
247
+ if (@available(iOS 13.0, *)) {
248
+ return UIStatusBarStyleDarkContent;
249
+ }
250
+ return UIStatusBarStyleDefault;
251
+ }
252
+
253
+ - (BOOL)prefersStatusBarHidden {
254
+ // Disregard the transition animation if we're not actively overriding it
255
+ if (!self.overrideStatusBar) {
256
+ return self.statusBarHidden;
257
+ }
258
+
259
+ // Work out whether the status bar needs to be visible
260
+ // during a transition animation or not
261
+ BOOL hidden = YES; // Default is yes
262
+ hidden = hidden && !(self.inTransition); // Not currently in a presentation animation (Where removing the status bar would break the layout)
263
+ hidden = hidden && !(self.view.superview == nil); // Not currently waiting to be added to a super view
264
+ return hidden;
265
+ }
266
+
267
+ - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
268
+ if (self.navigationController) {
269
+ return super.preferredScreenEdgesDeferringSystemGestures;
270
+ }
271
+ return UIRectEdgeAll;
272
+ }
273
+
274
+ - (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout {
275
+ UIEdgeInsets insets = self.statusBarSafeInsets;
276
+
277
+ // fix: On iOS 26, overlay with iPadOS windowingControl area.
278
+ #if defined(__IPHONE_26_0)
279
+ if (@available(iOS 26.0, *)) {
280
+ if (!verticalLayout) {
281
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED < 180000
282
+ UIViewLayoutRegion *layoutRegion = [UIViewLayoutRegion safeAreaLayoutRegionWithCornerAdaptation: UIViewLayoutRegionAdaptivityAxisVertical];
283
+ UIEdgeInsets edgeInsets = [self.view edgeInsetsForLayoutRegion:layoutRegion];
284
+ insets.top = edgeInsets.top;
285
+ insets.left = edgeInsets.left;
286
+ insets.bottom = edgeInsets.bottom;
287
+ #endif
288
+ }
289
+ }
290
+ #endif
291
+
292
+ CGRect frame = CGRectZero;
293
+ if (!verticalLayout) { // In landscape laying out toolbar to the left
294
+ if (@available(iOS 26.0, *)) {
295
+ #if !TARGET_OS_VISION
296
+ CGFloat minPadding = 8.0f;
297
+ #else
298
+ CGFloat minPadding = 16.0f;
299
+ #endif
300
+ frame.origin.x = minPadding + insets.left;
301
+ frame.origin.y = minPadding + insets.top;
302
+ frame.size.width = kTOCropViewControllerToolbarHeight;
303
+ frame.size.height = CGRectGetHeight(self.view.frame) - (minPadding * 2.0f) - insets.top - insets.bottom;
304
+ } else {
305
+ frame.origin.x = insets.left;
306
+ frame.origin.y = 0.0f;
307
+ frame.size.width = kTOCropViewControllerToolbarHeight;
308
+ frame.size.height = CGRectGetHeight(self.view.frame);
309
+ }
310
+ } else {
311
+ if (@available(iOS 26.0, *)) {
312
+ CGRect frameInWindow = [self.view convertRect:self.view.bounds toView:nil];
313
+ BOOL isFullscreen = true;
314
+ if (self.view.window) {
315
+ isFullscreen = CGRectEqualToRect(frameInWindow, self.view.window.bounds);
316
+ }
317
+ if (isFullscreen) {
318
+ // On iOS 26, the safe area insets values are the same, however the default bottom value is so
319
+ // high that the buttons look incorrectly set. While you can try and hardcode a value lower to
320
+ // the bottom of the screen, trying to align the width with all the varied corner radii of modern iOS
321
+ // devices is also difficult.
322
+
323
+ // For now, I've decided to use a private API to fetch the corner radius of the device so we can properly align
324
+ // the toolbar with the device's rounded corners.
325
+ // I've filed FB20413789 with Apple hoping that this can become a real solution in future.
326
+ if (insets.bottom > 0.0f) {
327
+ insets.bottom = 20.0f;
328
+ } else {
329
+ insets.bottom = 8.0f;
330
+ }
331
+
332
+ #if !TARGET_OS_VISION
333
+ // Look the value up only once since it can't change for the life of the
334
+ // process, and fall back to a radius matching current-generation devices
335
+ // in case the private key is ever renamed or removed
336
+ static CGFloat cornerRadius = 44.0f;
337
+ static dispatch_once_t onceToken;
338
+ dispatch_once(&onceToken, ^{
339
+ const char *components[] = {"Radius", "Corner", "display", "_"};
340
+ NSString *selectorName = @"";
341
+ for (NSInteger i = 3; i >= 0; i--) {
342
+ selectorName = [selectorName stringByAppendingString:[NSString stringWithCString:components[i]
343
+ encoding:NSUTF8StringEncoding]];
344
+ }
345
+ @try {
346
+ cornerRadius = [[UIScreen.mainScreen valueForKey:selectorName] floatValue];
347
+ } @catch (NSException *exception) {
348
+ NSLog(@"TOCropViewController: Unable to read the display corner radius. Falling back to a default value.");
349
+ }
350
+ });
351
+ #else
352
+ const CGFloat cornerRadius = 64.0f;
353
+ #endif
354
+ frame.size.width = CGRectGetWidth(self.view.bounds) - MAX(cornerRadius, insets.bottom * 2.0f);
355
+ } else {
356
+ // Match system toolbar insets
357
+ insets.left += self.view.layoutMargins.left + 12;
358
+ insets.right += self.view.layoutMargins.right + 12;
359
+ insets.bottom = MAX(0, insets.bottom - 6);
360
+ frame.size.width = CGRectGetWidth(self.view.bounds) - insets.left - insets.right;
361
+ }
362
+ } else {
363
+ frame.size.width = CGRectGetWidth(self.view.bounds);
364
+ }
365
+
366
+ frame.origin.x = (CGRectGetWidth(self.view.bounds) - frame.size.width) / 2.0f;
367
+ frame.size.height = kTOCropViewControllerToolbarHeight;
368
+
369
+ if (self.toolbarPosition == TOCropViewControllerToolbarPositionBottom) {
370
+ frame.origin.y = CGRectGetHeight(self.view.bounds) - (frame.size.height + insets.bottom);
371
+ } else {
372
+ if (self.titleLabel.text.length) {
373
+ // Work out the size of the title label based on the crop view size
374
+ CGRect frame = self.titleLabel.frame;
375
+ frame.size = [self.titleLabel sizeThatFits:self.cropView.frame.size];
376
+ self.titleLabel.frame = frame;
377
+
378
+ // Set out the appropriate inset for that
379
+ insets.top = CGRectGetMaxY(self.titleLabel.frame);
380
+ insets.top += kTOCropViewControllerTitleTopPadding;
381
+ }
382
+ frame.origin.y = insets.top;
383
+ }
384
+ }
385
+
386
+ return frame;
387
+ }
388
+
389
+ - (CGRect)frameForCropViewWithVerticalLayout:(BOOL)verticalLayout {
390
+ // On an iPad, if being presented in a modal view controller by a UINavigationController,
391
+ // at the time we need it, the size of our view will be incorrect.
392
+ // If this is the case, derive our view size from our parent view controller instead
393
+ UIView *view = nil;
394
+ if (self.parentViewController == nil) {
395
+ view = self.view;
396
+ } else {
397
+ view = self.parentViewController.view;
398
+ }
399
+
400
+ // Always make the crop view edge-to-edge on iOS 26 and up
401
+ if (@available(iOS 26.0, *)) {
402
+ return view.bounds;
403
+ }
404
+
405
+ CGRect bounds = view.bounds;
406
+ CGRect frame = CGRectZero;
407
+
408
+ // Horizontal layout (eg landscape)
409
+ if (!verticalLayout) {
410
+ frame.origin.x = CGRectGetMaxX(self.toolbar.frame);
411
+ frame.size.width = CGRectGetWidth(bounds) - frame.origin.x;
412
+ frame.size.height = CGRectGetHeight(bounds);
413
+ } else { // Vertical layout
414
+ frame.size.width = CGRectGetWidth(bounds);
415
+
416
+ // Set Y and adjust for height
417
+ if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
418
+ frame.origin.y = CGRectGetMaxY(self.toolbar.frame);
419
+ frame.size.height = CGRectGetHeight(bounds) - frame.origin.y;
420
+ } else {
421
+ frame.size.height = CGRectGetMinY(self.toolbar.frame);
422
+ }
423
+ }
424
+
425
+ return frame;
426
+ }
427
+
428
+ - (CGRect)frameForTitleLabelWithSize:(CGSize)size verticalLayout:(BOOL)verticalLayout {
429
+ CGRect frame = (CGRect){CGPointZero, size};
430
+ CGFloat viewWidth = self.view.bounds.size.width;
431
+ CGFloat x = 0.0f; // Additional X offset in landscape mode
432
+
433
+ // Adjust for landscape layout
434
+ if (!verticalLayout) {
435
+ x = kTOCropViewControllerTitleTopPadding;
436
+ x += self.view.safeAreaInsets.left;
437
+ viewWidth -= x;
438
+ }
439
+
440
+ // Work out horizontal position
441
+ frame.origin.x = ceilf((viewWidth - frame.size.width) * 0.5f);
442
+ if (!verticalLayout) {
443
+ frame.origin.x += x;
444
+ }
445
+
446
+ // Work out vertical position
447
+ frame.origin.y = self.view.safeAreaInsets.top + kTOCropViewControllerTitleTopPadding;
448
+
449
+ return frame;
450
+ }
451
+
452
+ - (void)adjustCropViewInsets {
453
+ UIEdgeInsets insets = self.statusBarSafeInsets;
454
+
455
+ if (@available(iOS 26.0, *)) {
456
+ if (!self.verticalLayout) {
457
+ insets.left = CGRectGetMaxX(self.toolbar.frame);
458
+ } else {
459
+ if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
460
+ insets.top = CGRectGetMaxY(self.toolbar.frame);
461
+ } else {
462
+ insets.bottom = CGRectGetHeight(self.view.frame) - CGRectGetMinY(self.toolbar.frame);
463
+ }
464
+ }
465
+ } else {
466
+ if (!self.verticalLayout) {
467
+ insets.left = 0.0f;
468
+ } else {
469
+ if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
470
+ insets.top = 0.0f;
471
+ } else {
472
+ insets.bottom = 0.0f;
473
+ }
474
+ }
475
+ }
476
+
477
+ if (!self.verticalLayout || self.toolbarPosition == TOCropViewControllerToolbarPositionBottom) {
478
+ if (self.titleLabel.text.length) {
479
+ // Work out the size of the title label based on the crop view size
480
+ CGRect frame = self.titleLabel.frame;
481
+ frame.size = [self.titleLabel sizeThatFits:self.cropView.frame.size];
482
+ self.titleLabel.frame = frame;
483
+
484
+ // Set out the appropriate inset for that
485
+ insets.top += self.titleLabel.frame.size.height;
486
+ insets.top += kTOCropViewControllerTitleTopPadding;
487
+ }
488
+ }
489
+
490
+ self.cropView.cropRegionInsets = insets;
491
+ }
492
+
493
+ - (void)adjustToolbarInsets {
494
+ UIEdgeInsets insets = UIEdgeInsetsZero;
495
+
496
+ // Add padding to the left in landscape mode
497
+ if (!self.verticalLayout) {
498
+ insets.left = self.view.safeAreaInsets.left;
499
+ } else {
500
+ // Add padding on top if in vertical and tool bar is at the top
501
+ if (self.toolbarPosition == TOCropViewControllerToolbarPositionTop) {
502
+ insets.top = self.view.safeAreaInsets.top;
503
+ } else { // Add padding to the bottom otherwise
504
+ insets.bottom = self.view.safeAreaInsets.bottom;
505
+ }
506
+ }
507
+
508
+ // Update the toolbar with these properties
509
+ self.toolbar.backgroundViewOutsets = insets;
510
+ self.toolbar.statusBarHeightInset = self.statusBarHeight;
511
+ [self.toolbar setNeedsLayout];
512
+ }
513
+
514
+ - (void)viewSafeAreaInsetsDidChange {
515
+ [super viewSafeAreaInsetsDidChange];
516
+ [self adjustCropViewInsets];
517
+ [self adjustToolbarInsets];
518
+ }
519
+
520
+ - (void)viewDidLayoutSubviews {
521
+ [super viewDidLayoutSubviews];
522
+
523
+ [UIView performWithoutAnimation:^{
524
+ self.toolbar.frame = [self frameForToolbarWithVerticalLayout:self.verticalLayout];
525
+ [self adjustToolbarInsets];
526
+ [self.toolbar setNeedsLayout];
527
+ }];
528
+
529
+ self.cropView.frame = [self frameForCropViewWithVerticalLayout:self.verticalLayout];
530
+ [self adjustCropViewInsets];
531
+ [self.cropView moveCroppedContentToCenterAnimated:NO];
532
+
533
+ if (self.firstTime == NO) {
534
+ [self.cropView performInitialSetup];
535
+ self.firstTime = YES;
536
+ }
537
+
538
+ if (self.title.length) {
539
+ self.titleLabel.frame = [self frameForTitleLabelWithSize:self.titleLabel.frame.size verticalLayout:self.verticalLayout];
540
+ [self.cropView moveCroppedContentToCenterAnimated:NO];
541
+ }
542
+ }
543
+
544
+ #pragma mark - Rotation Handling -
545
+
546
+ - (void)_willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
547
+ self.toolbarSnapshotView = [self.toolbar snapshotViewAfterScreenUpdates:NO];
548
+ self.toolbarSnapshotView.frame = self.toolbar.frame;
549
+
550
+ if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
551
+ self.toolbarSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
552
+ } else {
553
+ self.toolbarSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleRightMargin;
554
+ }
555
+ [self.view addSubview:self.toolbarSnapshotView];
556
+
557
+ // Set up the toolbar frame to be just off t
558
+ CGRect frame = [self frameForToolbarWithVerticalLayout:UIInterfaceOrientationIsPortrait(toInterfaceOrientation)];
559
+ if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
560
+ frame.origin.x = -frame.size.width;
561
+ } else {
562
+ frame.origin.y = self.view.bounds.size.height;
563
+ }
564
+ self.toolbar.frame = frame;
565
+
566
+ [self.toolbar layoutIfNeeded];
567
+ self.toolbar.alpha = 0.0f;
568
+
569
+ [self.cropView prepareforRotation];
570
+ self.cropView.frame = [self frameForCropViewWithVerticalLayout:UIInterfaceOrientationIsPortrait(toInterfaceOrientation)];
571
+ self.cropView.simpleRenderMode = YES;
572
+ self.cropView.internalLayoutDisabled = YES;
573
+ }
574
+
575
+ - (void)_willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
576
+ // Remove all animations in the toolbar
577
+ self.toolbar.frame = [self frameForToolbarWithVerticalLayout:!UIInterfaceOrientationIsLandscape(toInterfaceOrientation)];
578
+ [self.toolbar.layer removeAllAnimations];
579
+ for (CALayer *sublayer in self.toolbar.layer.sublayers) {
580
+ [sublayer removeAllAnimations];
581
+ }
582
+
583
+ // On iOS 11 and up, since these layout calls are done multiple times, if we don't aggregate from the
584
+ // current state, the animation breaks.
585
+ [UIView animateWithDuration:duration
586
+ delay:0.0f
587
+ options:UIViewAnimationOptionBeginFromCurrentState
588
+ animations:
589
+ ^{
590
+ self.cropView.frame = [self frameForCropViewWithVerticalLayout:!UIInterfaceOrientationIsLandscape(toInterfaceOrientation)];
591
+ self.toolbar.frame = [self frameForToolbarWithVerticalLayout:UIInterfaceOrientationIsPortrait(toInterfaceOrientation)];
592
+ [self.cropView performRelayoutForRotation];
593
+ }
594
+ completion:nil];
595
+
596
+ self.toolbarSnapshotView.alpha = 0.0f;
597
+ self.toolbar.alpha = 1.0f;
598
+ }
599
+
600
+ - (void)_didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
601
+ [self.toolbarSnapshotView removeFromSuperview];
602
+ self.toolbarSnapshotView = nil;
603
+
604
+ [self.cropView setSimpleRenderMode:NO animated:YES];
605
+ self.cropView.internalLayoutDisabled = NO;
606
+ }
607
+
608
+ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
609
+ [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
610
+
611
+ // If the size doesn't change (e.g, we did a 180 degree device rotation), don't bother doing a relayout
612
+ if (CGSizeEqualToSize(size, self.view.bounds.size)) {
613
+ return;
614
+ }
615
+
616
+ #if !TARGET_OS_VISION
617
+ // Derive the layout from the aspect of the size we're transitioning to,
618
+ // matching the logic in -verticalLayout
619
+ UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait;
620
+ #if !TARGET_OS_MACCATALYST
621
+ // >= so a square size agrees with -verticalLayout (width < height)
622
+ if (size.width >= size.height) {
623
+ orientation = UIInterfaceOrientationLandscapeLeft;
624
+ }
625
+ #endif
626
+ #else
627
+ // On visionOS, this method is called on presentation with size=(0,0),
628
+ // which would set orientation incorrectly causing views to be misplaced.
629
+ UIInterfaceOrientation orientation = UIInterfaceOrientationLandscapeLeft;
630
+ #endif
631
+
632
+ [self _willRotateToInterfaceOrientation:orientation
633
+ duration:coordinator.transitionDuration];
634
+ [coordinator
635
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
636
+ [self _willAnimateRotationToInterfaceOrientation:orientation duration:coordinator.transitionDuration];
637
+ }
638
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
639
+ [self _didRotateFromInterfaceOrientation:orientation];
640
+ }];
641
+ }
642
+
643
+ #pragma mark - Reset -
644
+ - (void)resetCropViewLayout {
645
+ BOOL animated = (self.cropView.angle == 0);
646
+
647
+ if (self.resetAspectRatioEnabled) {
648
+ self.aspectRatioLockEnabled = NO;
649
+ }
650
+
651
+ [self.cropView resetLayoutToDefaultAnimated:animated];
652
+ }
653
+
654
+ #pragma mark - Aspect Ratio Handling -
655
+ - (void)showAspectRatioDialog {
656
+ if (self.cropView.aspectRatioLockEnabled) {
657
+ self.cropView.aspectRatioLockEnabled = NO;
658
+ self.toolbar.clampButtonGlowing = NO;
659
+ return;
660
+ }
661
+
662
+ // Depending on the shape of the image, work out if horizontal, or vertical options are required
663
+ BOOL verticalCropBox = self.cropView.cropBoxAspectRatioIsPortrait;
664
+
665
+ // Get the resource bundle depending on the framework/dependency manager we're using
666
+ NSBundle *resourceBundle = TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(self);
667
+
668
+ // Prepare the localized options
669
+ NSString *cancelButtonTitle = NSLocalizedStringFromTableInBundle(@"Cancel", @"TOCropViewControllerLocalizable", resourceBundle, nil);
670
+
671
+ // Prepare the list that will be fed to the alert view/controller
672
+
673
+ NSArray<TOCropViewControllerAspectRatioPreset *> *presets;
674
+
675
+ if (self.allowedAspectRatios == nil) {
676
+ presets = verticalCropBox ? [TOCropViewControllerAspectRatioPreset portraitPresets] : [TOCropViewControllerAspectRatioPreset landscapePresets];
677
+ } else {
678
+ presets = self.allowedAspectRatios;
679
+ }
680
+
681
+ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
682
+ [alertController addAction:[UIAlertAction actionWithTitle:cancelButtonTitle style:UIAlertActionStyleCancel handler:nil]];
683
+
684
+ // Add each item to the alert controller
685
+ for (NSInteger i = 0; i < presets.count; i++) {
686
+ id handlerBlock = ^(UIAlertAction *action) {
687
+ [self setAspectRatioPreset:presets[i].size animated:YES];
688
+ self.aspectRatioLockEnabled = YES;
689
+ };
690
+ UIAlertAction *action = [UIAlertAction actionWithTitle:presets[i].title style:UIAlertActionStyleDefault handler:handlerBlock];
691
+ [alertController addAction:action];
692
+ }
693
+
694
+ alertController.modalPresentationStyle = UIModalPresentationPopover;
695
+ UIPopoverPresentationController *presentationController = [alertController popoverPresentationController];
696
+ presentationController.sourceView = self.toolbar;
697
+ presentationController.sourceRect = self.toolbar.clampButtonFrame;
698
+ [self presentViewController:alertController animated:YES completion:nil];
699
+ }
700
+
701
+ - (void)setAspectRatioPreset:(CGSize)aspectRatioPreset animated:(BOOL)animated {
702
+ _aspectRatioPreset = aspectRatioPreset;
703
+ [self.cropView setAspectRatio:aspectRatioPreset animated:animated];
704
+ }
705
+
706
+ - (void)rotateCropViewClockwise {
707
+ self.toolbar.disableRotationButtons = YES;
708
+ [self.cropView rotateImageNinetyDegreesAnimated:YES
709
+ clockwise:YES
710
+ completion:^(BOOL success) {
711
+ self.toolbar.disableRotationButtons = NO;
712
+ }];
713
+ }
714
+
715
+ - (void)rotateCropViewCounterclockwise {
716
+ self.toolbar.disableRotationButtons = YES;
717
+ [self.cropView rotateImageNinetyDegreesAnimated:YES
718
+ clockwise:NO
719
+ completion:^(BOOL success) {
720
+ self.toolbar.disableRotationButtons = NO;
721
+ }];
722
+ }
723
+
724
+ #pragma mark - Crop View Delegates -
725
+ - (void)cropViewDidBecomeResettable:(TOCropView *)cropView {
726
+ self.toolbar.resetButtonEnabled = YES;
727
+ }
728
+
729
+ - (void)cropViewDidBecomeNonResettable:(TOCropView *)cropView {
730
+ self.toolbar.resetButtonEnabled = NO;
731
+ }
732
+
733
+ #pragma mark - Presentation Handling -
734
+ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewController
735
+ fromView:(UIView *)fromView
736
+ fromFrame:(CGRect)fromFrame
737
+ setup:(void (^)(void))setup
738
+ completion:(void (^)(void))completion {
739
+ [self presentAnimatedFromParentViewController:viewController
740
+ fromImage:nil
741
+ fromView:fromView
742
+ fromFrame:fromFrame
743
+ angle:0
744
+ toImageFrame:CGRectZero
745
+ setup:setup
746
+ completion:completion];
747
+ }
748
+
749
+ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewController
750
+ fromImage:(UIImage *)image
751
+ fromView:(UIView *)fromView
752
+ fromFrame:(CGRect)fromFrame
753
+ angle:(NSInteger)angle
754
+ toImageFrame:(CGRect)toFrame
755
+ setup:(void (^)(void))setup
756
+ completion:(void (^)(void))completion {
757
+ self.transitionController.image = image ? image : self.image;
758
+ self.transitionController.fromFrame = fromFrame;
759
+ self.transitionController.fromView = fromView;
760
+ self.prepareForTransitionHandler = setup;
761
+
762
+ if (angle != 0 || !CGRectIsEmpty(toFrame)) {
763
+ self.angle = angle;
764
+ self.imageCropFrame = toFrame;
765
+ }
766
+
767
+ __weak typeof(self) weakSelf = self;
768
+ [viewController presentViewController:self.parentViewController ? self.parentViewController : self
769
+ animated:YES
770
+ completion:^{
771
+ typeof(self) strongSelf = weakSelf;
772
+ if (completion) {
773
+ completion();
774
+ }
775
+
776
+ [strongSelf.cropView setCroppingViewsHidden:NO animated:YES];
777
+ if (!CGRectIsEmpty(fromFrame)) {
778
+ [strongSelf.cropView setGridOverlayHidden:NO animated:YES];
779
+ }
780
+ }];
781
+ }
782
+
783
+ - (void)dismissAnimatedFromParentViewController:(UIViewController *)viewController
784
+ toView:(UIView *)toView
785
+ toFrame:(CGRect)frame
786
+ setup:(void (^)(void))setup
787
+ completion:(void (^)(void))completion {
788
+ [self dismissAnimatedFromParentViewController:viewController withCroppedImage:nil toView:toView toFrame:frame setup:setup completion:completion];
789
+ }
790
+
791
+ - (void)dismissAnimatedFromParentViewController:(UIViewController *)viewController
792
+ withCroppedImage:(UIImage *)image
793
+ toView:(UIView *)toView
794
+ toFrame:(CGRect)frame
795
+ setup:(void (^)(void))setup
796
+ completion:(void (^)(void))completion {
797
+ // If a cropped image was supplied, use that, and only zoom out from the crop box
798
+ if (image) {
799
+ self.transitionController.image = image ? image : self.image;
800
+ self.transitionController.fromFrame = [self.cropView convertRect:self.cropView.cropBoxFrame toView:self.view];
801
+ } else { // else use the main image, and zoom out from its entirety
802
+ self.transitionController.image = self.image;
803
+ self.transitionController.fromFrame = [self.cropView convertRect:self.cropView.imageViewFrame toView:self.view];
804
+ }
805
+
806
+ self.transitionController.toView = toView;
807
+ self.transitionController.toFrame = frame;
808
+ self.prepareForTransitionHandler = setup;
809
+
810
+ [viewController dismissViewControllerAnimated:YES
811
+ completion:^{
812
+ if (completion) {
813
+ completion();
814
+ }
815
+ }];
816
+ }
817
+
818
+ - (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
819
+ if (self.navigationController || self.modalTransitionStyle == UIModalTransitionStyleCoverVertical) {
820
+ return nil;
821
+ }
822
+
823
+ self.cropView.simpleRenderMode = YES;
824
+
825
+ __weak typeof(self) weakSelf = self;
826
+ self.transitionController.prepareForTransitionHandler = ^{
827
+ typeof(self) strongSelf = weakSelf;
828
+ TOCropViewControllerTransitioning *transitioning = strongSelf.transitionController;
829
+
830
+ transitioning.toFrame = [strongSelf.cropView convertRect:strongSelf.cropView.cropBoxFrame toView:strongSelf.view];
831
+ if (!CGRectIsEmpty(transitioning.fromFrame) || transitioning.fromView) {
832
+ strongSelf.cropView.croppingViewsHidden = YES;
833
+ }
834
+
835
+ if (strongSelf.prepareForTransitionHandler) {
836
+ strongSelf.prepareForTransitionHandler();
837
+ }
838
+
839
+ strongSelf.prepareForTransitionHandler = nil;
840
+ };
841
+
842
+ self.transitionController.isDismissing = NO;
843
+ return self.transitionController;
844
+ }
845
+
846
+ - (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
847
+ if (self.navigationController || self.modalTransitionStyle == UIModalTransitionStyleCoverVertical) {
848
+ return nil;
849
+ }
850
+
851
+ __weak typeof(self) weakSelf = self;
852
+ self.transitionController.prepareForTransitionHandler = ^{
853
+ typeof(self) strongSelf = weakSelf;
854
+ TOCropViewControllerTransitioning *transitioning = strongSelf.transitionController;
855
+
856
+ if (!CGRectIsEmpty(transitioning.toFrame) || transitioning.toView) {
857
+ strongSelf.cropView.croppingViewsHidden = YES;
858
+ } else {
859
+ strongSelf.cropView.simpleRenderMode = YES;
860
+ }
861
+
862
+ if (strongSelf.prepareForTransitionHandler) {
863
+ strongSelf.prepareForTransitionHandler();
864
+ }
865
+ };
866
+
867
+ self.transitionController.isDismissing = YES;
868
+ return self.transitionController;
869
+ }
870
+
871
+ #pragma mark - Button Feedback -
872
+ - (void)cancelButtonTapped {
873
+ if (!self.showCancelConfirmationDialog) {
874
+ [self dismissCropViewController];
875
+ return;
876
+ }
877
+
878
+ // Get the resource bundle depending on the framework/dependency manager we're using
879
+ NSBundle *resourceBundle = TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(self);
880
+
881
+ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
882
+ message:nil
883
+ preferredStyle:UIAlertControllerStyleActionSheet];
884
+ alertController.popoverPresentationController.sourceView = self.toolbar.visibleCancelButton;
885
+
886
+ NSString *yesButtonTitle = NSLocalizedStringFromTableInBundle(@"Delete Changes", @"TOCropViewControllerLocalizable", resourceBundle, nil);
887
+ NSString *noButtonTitle = NSLocalizedStringFromTableInBundle(@"Cancel", @"TOCropViewControllerLocalizable", resourceBundle, nil);
888
+
889
+ __weak typeof(self) weakSelf = self;
890
+ UIAlertAction *yesAction = [UIAlertAction actionWithTitle:yesButtonTitle
891
+ style:UIAlertActionStyleDestructive
892
+ handler:^(UIAlertAction *action) {
893
+ [weakSelf dismissCropViewController];
894
+ }];
895
+ [alertController addAction:yesAction];
896
+
897
+ UIAlertAction *noAction = [UIAlertAction actionWithTitle:noButtonTitle style:UIAlertActionStyleCancel handler:nil];
898
+ [alertController addAction:noAction];
899
+
900
+ [weakSelf presentViewController:alertController animated:YES completion:nil];
901
+ }
902
+
903
+ - (void)dismissCropViewController {
904
+ bool isDelegateOrCallbackHandled = NO;
905
+
906
+ // Check if the delegate method was implemented and call if so
907
+ if ([self.delegate respondsToSelector:@selector(cropViewController:didFinishCancelled:)]) {
908
+ [self.delegate cropViewController:self didFinishCancelled:YES];
909
+ isDelegateOrCallbackHandled = YES;
910
+ }
911
+
912
+ // Check if the block version was implemented and call if so
913
+ if (self.onDidFinishCancelled != nil) {
914
+ self.onDidFinishCancelled(YES);
915
+ isDelegateOrCallbackHandled = YES;
916
+ }
917
+
918
+ // If neither callbacks were implemented, perform a default dismissing animation
919
+ if (!isDelegateOrCallbackHandled) {
920
+ if (self.navigationController && self.navigationController.viewControllers.count > 1) {
921
+ [self.navigationController popViewControllerAnimated:YES];
922
+ } else {
923
+ [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
924
+ }
925
+ }
926
+ }
927
+
928
+ - (void)doneButtonTapped {
929
+ CGRect cropFrame = self.cropView.imageCropFrame;
930
+ NSInteger angle = self.cropView.angle;
931
+
932
+ if (self.onDidTapDone) {
933
+ dispatch_async(dispatch_get_main_queue(), ^{
934
+ self.onDidTapDone();
935
+ });
936
+ }
937
+
938
+ // If desired, when the user taps done, show an activity sheet
939
+ if (self.showActivitySheetOnDone) {
940
+ TOActivityCroppedImageProvider *imageItem = [[TOActivityCroppedImageProvider alloc] initWithImage:self.image cropFrame:cropFrame angle:angle circular:(self.croppingStyle == TOCropViewCroppingStyleCircular)];
941
+ TOCroppedImageAttributes *attributes = [[TOCroppedImageAttributes alloc] initWithCroppedFrame:cropFrame angle:angle originalImageSize:self.image.size];
942
+
943
+ NSMutableArray *activityItems = [@[imageItem, attributes] mutableCopy];
944
+ if (self.activityItems) {
945
+ [activityItems addObjectsFromArray:self.activityItems];
946
+ }
947
+
948
+ UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:self.applicationActivities];
949
+ activityController.excludedActivityTypes = self.excludedActivityTypes;
950
+
951
+ activityController.modalPresentationStyle = UIModalPresentationPopover;
952
+ activityController.popoverPresentationController.sourceView = self.toolbar;
953
+ activityController.popoverPresentationController.sourceRect = self.toolbar.doneButtonFrame;
954
+ [self presentViewController:activityController animated:YES completion:nil];
955
+
956
+ __weak typeof(activityController) blockController = activityController;
957
+
958
+ activityController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
959
+ if (!completed) {
960
+ return;
961
+ }
962
+
963
+ bool isCallbackOrDelegateHandled = NO;
964
+
965
+ if (self.onDidFinishCancelled != nil) {
966
+ self.onDidFinishCancelled(NO);
967
+ isCallbackOrDelegateHandled = YES;
968
+ }
969
+ if ([self.delegate respondsToSelector:@selector(cropViewController:didFinishCancelled:)]) {
970
+ [self.delegate cropViewController:self didFinishCancelled:NO];
971
+ isCallbackOrDelegateHandled = YES;
972
+ }
973
+
974
+ if (!isCallbackOrDelegateHandled) {
975
+ if (self.navigationController != nil && self.navigationController.viewControllers.count > 1) {
976
+ [self.navigationController popViewControllerAnimated:YES];
977
+ } else {
978
+ [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
979
+ blockController.completionWithItemsHandler = nil;
980
+ }
981
+ }
982
+ };
983
+
984
+ return;
985
+ } else {
986
+ // Disable both variants; only the icon button exists on iOS 26,
987
+ // and it's also the visible one in landscape/icon-only mode
988
+ [self setDoneButtonsEnabled:NO];
989
+ }
990
+
991
+ BOOL isCallbackOrDelegateHandled = NO;
992
+ BOOL willRestoreDoneButtonsAfterCallback = NO;
993
+
994
+ // If the delegate/block that only supplies crop data is provided, call it
995
+ if ([self.delegate respondsToSelector:@selector(cropViewController:didCropImageToRect:angle:)]) {
996
+ [self.delegate cropViewController:self didCropImageToRect:cropFrame angle:angle];
997
+ isCallbackOrDelegateHandled = YES;
998
+ }
999
+
1000
+ if (self.onDidCropImageToRect != nil) {
1001
+ self.onDidCropImageToRect(cropFrame, angle);
1002
+ isCallbackOrDelegateHandled = YES;
1003
+ }
1004
+
1005
+ // Check if the circular APIs were implemented
1006
+ BOOL isCircularImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToCircularImage:withRect:angle:)];
1007
+ BOOL isCircularImageCallbackAvailable = self.onDidCropToCircleImage != nil;
1008
+
1009
+ // Check if non-circular was implemented
1010
+ BOOL isDidCropToImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToImage:withRect:angle:)];
1011
+ BOOL isDidCropToImageCallbackAvailable = self.onDidCropToRect != nil;
1012
+
1013
+ // If cropping circular and the circular generation delegate/block is implemented, call it
1014
+ if (self.croppingStyle == TOCropViewCroppingStyleCircular && (isCircularImageDelegateAvailable || isCircularImageCallbackAvailable)) {
1015
+ UIImage *image = [self.image croppedImageWithFrame:cropFrame angle:angle circularClip:YES];
1016
+
1017
+ // Dispatch on the next run-loop so the animation isn't interuppted by the crop operation
1018
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
1019
+ if (isCircularImageDelegateAvailable) {
1020
+ [self.delegate cropViewController:self didCropToCircularImage:image withRect:cropFrame angle:angle];
1021
+ }
1022
+ if (isCircularImageCallbackAvailable) {
1023
+ self.onDidCropToCircleImage(image, cropFrame, angle);
1024
+ }
1025
+
1026
+ // Let hosts that keep the controller on screen commit again
1027
+ [self setDoneButtonsEnabled:YES];
1028
+ });
1029
+
1030
+ isCallbackOrDelegateHandled = YES;
1031
+ willRestoreDoneButtonsAfterCallback = YES;
1032
+ }
1033
+ // If the delegate/block that requires the specific cropped image is provided, call it
1034
+ else if (isDidCropToImageDelegateAvailable || isDidCropToImageCallbackAvailable) {
1035
+ UIImage *image = nil;
1036
+ if (angle == 0 && CGRectEqualToRect(cropFrame, (CGRect){CGPointZero, self.image.size})) {
1037
+ image = self.image;
1038
+ } else {
1039
+ image = [self.image croppedImageWithFrame:cropFrame angle:angle circularClip:NO];
1040
+ }
1041
+
1042
+ // Dispatch on the next run-loop so the animation isn't interuppted by the crop operation
1043
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
1044
+ if (isDidCropToImageDelegateAvailable) {
1045
+ [self.delegate cropViewController:self didCropToImage:image withRect:cropFrame angle:angle];
1046
+ }
1047
+
1048
+ if (isDidCropToImageCallbackAvailable) {
1049
+ self.onDidCropToRect(image, cropFrame, angle);
1050
+ }
1051
+
1052
+ // Let hosts that keep the controller on screen commit again
1053
+ [self setDoneButtonsEnabled:YES];
1054
+ });
1055
+
1056
+ isCallbackOrDelegateHandled = YES;
1057
+ willRestoreDoneButtonsAfterCallback = YES;
1058
+ }
1059
+
1060
+ if (!isCallbackOrDelegateHandled) {
1061
+ [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
1062
+ } else if (!willRestoreDoneButtonsAfterCallback) {
1063
+ // Only the synchronous crop-data callbacks ran; re-enable immediately
1064
+ [self setDoneButtonsEnabled:YES];
1065
+ }
1066
+ }
1067
+
1068
+ - (void)setDoneButtonsEnabled:(BOOL)enabled {
1069
+ self.toolbar.doneTextButton.enabled = enabled;
1070
+ self.toolbar.doneIconButton.enabled = enabled;
1071
+ }
1072
+
1073
+ - (void)commitCurrentCrop {
1074
+ [self doneButtonTapped];
1075
+ }
1076
+
1077
+ #pragma mark - Property Methods -
1078
+
1079
+ - (void)setTitle:(NSString *)title {
1080
+ [super setTitle:title];
1081
+
1082
+ if (self.title.length == 0) {
1083
+ [_titleLabel removeFromSuperview];
1084
+ _cropView.cropRegionInsets = UIEdgeInsetsMake(0, 0, 0, 0);
1085
+ _titleLabel = nil;
1086
+ return;
1087
+ }
1088
+
1089
+ self.titleLabel.text = self.title;
1090
+ [self.titleLabel sizeToFit];
1091
+ self.titleLabel.frame = [self frameForTitleLabelWithSize:self.titleLabel.frame.size verticalLayout:self.verticalLayout];
1092
+ }
1093
+
1094
+ - (void)setDoneButtonTitle:(NSString *)title {
1095
+ self.toolbar.doneTextButtonTitle = title;
1096
+ }
1097
+
1098
+ - (NSString *)doneButtonTitle {
1099
+ return self.toolbar.doneTextButtonTitle;
1100
+ }
1101
+
1102
+ - (void)setCancelButtonTitle:(NSString *)title {
1103
+ self.toolbar.cancelTextButtonTitle = title;
1104
+ }
1105
+
1106
+ - (NSString *)cancelButtonTitle {
1107
+ return self.toolbar.cancelTextButtonTitle;
1108
+ }
1109
+
1110
+ - (void)setShowOnlyIcons:(BOOL)showOnlyIcons {
1111
+ self.toolbar.showOnlyIcons = showOnlyIcons;
1112
+ }
1113
+
1114
+ - (BOOL)showOnlyIcons {
1115
+ return self.toolbar.showOnlyIcons;
1116
+ }
1117
+
1118
+ - (void)setDoneButtonColor:(UIColor *)color {
1119
+ self.toolbar.doneButtonColor = color;
1120
+ }
1121
+
1122
+ - (UIColor *)doneButtonColor {
1123
+ return self.toolbar.doneButtonColor;
1124
+ }
1125
+
1126
+ - (void)setCancelButtonColor:(UIColor *)color {
1127
+ self.toolbar.cancelButtonColor = color;
1128
+ }
1129
+
1130
+ - (UIColor *)cancelButtonColor {
1131
+ return self.toolbar.cancelButtonColor;
1132
+ }
1133
+
1134
+ - (TOCropView *)cropView {
1135
+ // Lazily create the crop view in case we try and access it before presentation, but
1136
+ // don't add it until our parent view controller view has loaded at the right time
1137
+ if (!_cropView) {
1138
+ _cropView = [[TOCropView alloc] initWithCroppingStyle:self.croppingStyle image:self.image];
1139
+ _cropView.delegate = self;
1140
+ _cropView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
1141
+ [self.view insertSubview:_cropView atIndex:0];
1142
+ }
1143
+ return _cropView;
1144
+ }
1145
+
1146
+ - (TOCropToolbar *)toolbar {
1147
+ if (!_toolbar) {
1148
+ _toolbar = [[TOCropToolbar alloc] initWithFrame:CGRectZero];
1149
+ [self.view addSubview:_toolbar];
1150
+ }
1151
+ return _toolbar;
1152
+ }
1153
+
1154
+ - (UILabel *)titleLabel {
1155
+ if (!self.title.length) {
1156
+ return nil;
1157
+ }
1158
+ if (_titleLabel) {
1159
+ return _titleLabel;
1160
+ }
1161
+
1162
+ _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
1163
+ _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
1164
+ _titleLabel.backgroundColor = [UIColor clearColor];
1165
+ _titleLabel.textColor = [UIColor whiteColor];
1166
+ _titleLabel.numberOfLines = 0;
1167
+ _titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignBaselines;
1168
+ _titleLabel.clipsToBounds = YES;
1169
+ _titleLabel.textAlignment = NSTextAlignmentCenter;
1170
+ _titleLabel.text = self.title;
1171
+
1172
+ [self.view insertSubview:self.titleLabel aboveSubview:self.toolbar];
1173
+
1174
+ return _titleLabel;
1175
+ }
1176
+
1177
+ - (void)setAspectRatioLockEnabled:(BOOL)aspectRatioLockEnabled {
1178
+ self.toolbar.clampButtonGlowing = aspectRatioLockEnabled;
1179
+ self.cropView.aspectRatioLockEnabled = aspectRatioLockEnabled;
1180
+ if (!self.aspectRatioPickerButtonHidden) {
1181
+ self.aspectRatioPickerButtonHidden = (aspectRatioLockEnabled && self.resetAspectRatioEnabled == NO);
1182
+ }
1183
+ }
1184
+
1185
+ - (void)setAspectRatioLockDimensionSwapEnabled:(BOOL)aspectRatioLockDimensionSwapEnabled {
1186
+ _aspectRatioLockDimensionSwapEnabled = aspectRatioLockDimensionSwapEnabled;
1187
+ self.cropView.aspectRatioLockDimensionSwapEnabled = aspectRatioLockDimensionSwapEnabled;
1188
+ }
1189
+
1190
+ - (BOOL)aspectRatioLockEnabled {
1191
+ return self.cropView.aspectRatioLockEnabled;
1192
+ }
1193
+
1194
+ - (void)setRotateButtonsHidden:(BOOL)rotateButtonsHidden {
1195
+ self.toolbar.rotateCounterclockwiseButtonHidden = rotateButtonsHidden;
1196
+ self.toolbar.rotateClockwiseButtonHidden = rotateButtonsHidden;
1197
+ }
1198
+
1199
+ - (void)setResetButtonHidden:(BOOL)resetButtonHidden {
1200
+ self.toolbar.resetButtonHidden = resetButtonHidden;
1201
+ }
1202
+
1203
+ - (BOOL)resetButtonHidden {
1204
+ return self.toolbar.resetButtonHidden;
1205
+ }
1206
+
1207
+ - (BOOL)rotateButtonsHidden {
1208
+ return self.toolbar.rotateCounterclockwiseButtonHidden && self.toolbar.rotateClockwiseButtonHidden;
1209
+ }
1210
+
1211
+ - (void)setRotateClockwiseButtonHidden:(BOOL)rotateClockwiseButtonHidden {
1212
+ self.toolbar.rotateClockwiseButtonHidden = rotateClockwiseButtonHidden;
1213
+ }
1214
+
1215
+ - (BOOL)rotateClockwiseButtonHidden {
1216
+ return self.toolbar.rotateClockwiseButtonHidden;
1217
+ }
1218
+
1219
+ - (void)setAspectRatioPickerButtonHidden:(BOOL)aspectRatioPickerButtonHidden {
1220
+ self.toolbar.clampButtonHidden = aspectRatioPickerButtonHidden;
1221
+ }
1222
+
1223
+ - (BOOL)aspectRatioPickerButtonHidden {
1224
+ return self.toolbar.clampButtonHidden;
1225
+ }
1226
+
1227
+ - (void)setDoneButtonHidden:(BOOL)doneButtonHidden {
1228
+ self.toolbar.doneButtonHidden = doneButtonHidden;
1229
+ }
1230
+
1231
+ - (BOOL)doneButtonHidden {
1232
+ return self.toolbar.doneButtonHidden;
1233
+ }
1234
+
1235
+ - (void)setCancelButtonHidden:(BOOL)cancelButtonHidden {
1236
+ self.toolbar.cancelButtonHidden = cancelButtonHidden;
1237
+ }
1238
+
1239
+ - (BOOL)cancelButtonHidden {
1240
+ return self.toolbar.cancelButtonHidden;
1241
+ }
1242
+
1243
+ - (BOOL)reverseContentLayout {
1244
+ return self.toolbar.reverseContentLayout;
1245
+ }
1246
+ - (void)setReverseContentLayout:(BOOL)reverseContentLayout {
1247
+ self.toolbar.reverseContentLayout = reverseContentLayout;
1248
+ }
1249
+
1250
+ - (void)setResetAspectRatioEnabled:(BOOL)resetAspectRatioEnabled {
1251
+ self.cropView.resetAspectRatioEnabled = resetAspectRatioEnabled;
1252
+ if (!self.aspectRatioPickerButtonHidden) {
1253
+ self.aspectRatioPickerButtonHidden = (resetAspectRatioEnabled == NO && self.aspectRatioLockEnabled);
1254
+ }
1255
+ }
1256
+
1257
+ - (BOOL)resetAspectRatioEnabled {
1258
+ return self.cropView.resetAspectRatioEnabled;
1259
+ }
1260
+
1261
+ - (void)setAngle:(NSInteger)angle {
1262
+ self.cropView.angle = angle;
1263
+ }
1264
+
1265
+ - (NSInteger)angle {
1266
+ return self.cropView.angle;
1267
+ }
1268
+
1269
+ - (void)setImageCropFrame:(CGRect)imageCropFrame {
1270
+ self.cropView.imageCropFrame = imageCropFrame;
1271
+ }
1272
+
1273
+ - (CGRect)imageCropFrame {
1274
+ return self.cropView.imageCropFrame;
1275
+ }
1276
+
1277
+ - (BOOL)verticalLayout {
1278
+ #if TARGET_OS_MACCATALYST
1279
+ return YES;
1280
+ #endif
1281
+
1282
+ return CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds);
1283
+ }
1284
+
1285
+ - (BOOL)overrideStatusBar {
1286
+ // If we're pushed from a navigation controller, we'll defer
1287
+ // to its handling of the status bar
1288
+ if (self.navigationController) {
1289
+ return NO;
1290
+ }
1291
+
1292
+ // If the view controller presenting us already hid it, we don't need to
1293
+ // do anything ourselves
1294
+ if (self.presentingViewController.prefersStatusBarHidden) {
1295
+ return NO;
1296
+ }
1297
+
1298
+ // We'll handle the status bar
1299
+ return YES;
1300
+ }
1301
+
1302
+ - (BOOL)statusBarHidden {
1303
+ // Defer behaviour to the hosting navigation controller
1304
+ if (self.navigationController) {
1305
+ return self.navigationController.prefersStatusBarHidden;
1306
+ }
1307
+
1308
+ // If our presenting controller has already hidden the status bar,
1309
+ // hide the status bar by default
1310
+ if (self.presentingViewController.prefersStatusBarHidden) {
1311
+ return YES;
1312
+ }
1313
+
1314
+ // Our default behaviour is to always hide the status bar
1315
+ return YES;
1316
+ }
1317
+
1318
+ - (CGFloat)statusBarHeight {
1319
+ CGFloat statusBarHeight = 0.0f;
1320
+ statusBarHeight = self.view.safeAreaInsets.top;
1321
+
1322
+ // We do need to include the status bar height on devices
1323
+ // that have a physical hardware inset, like an iPhone X notch
1324
+ BOOL hardwareRelatedInset = self.view.safeAreaInsets.bottom > FLT_EPSILON && UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone;
1325
+
1326
+ // Always have insetting on Mac Catalyst
1327
+ #if TARGET_OS_MACCATALYST
1328
+ hardwareRelatedInset = YES;
1329
+ #endif
1330
+
1331
+ // Unless the status bar is visible, or we need to account
1332
+ // for a hardware notch, always treat the status bar height as zero
1333
+ if (self.statusBarHidden && !hardwareRelatedInset) {
1334
+ statusBarHeight = 0.0f;
1335
+ }
1336
+
1337
+ return statusBarHeight;
1338
+ }
1339
+
1340
+ - (UIEdgeInsets)statusBarSafeInsets {
1341
+ UIEdgeInsets insets = UIEdgeInsetsZero;
1342
+ insets = self.view.safeAreaInsets;
1343
+ insets.top = self.statusBarHeight;
1344
+ return insets;
1345
+ }
1346
+
1347
+ - (void)setMinimumAspectRatio:(CGFloat)minimumAspectRatio {
1348
+ self.cropView.minimumAspectRatio = minimumAspectRatio;
1349
+ }
1350
+
1351
+ - (CGFloat)minimumAspectRatio {
1352
+ return self.cropView.minimumAspectRatio;
1353
+ }
1354
+
1355
+ @end