@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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/ReactNativeImageCropPicker.podspec +40 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +144 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerException.kt +45 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +308 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +297 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPicker.kt +69 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPickerPackage.kt +24 -0
- package/ios/ImageCropPickerError.swift +57 -0
- package/ios/ImageCropPickerImageProcessor.swift +343 -0
- package/ios/ImageCropPickerSession.swift +407 -0
- package/ios/ReactNativeImageCropPicker.swift +65 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.h +39 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.m +94 -0
- package/ios/TOCropViewController/Constants/TOCropViewConstants.h +63 -0
- package/ios/TOCropViewController/LICENSE +21 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.h +38 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.m +73 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.h +64 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.m +121 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.h +49 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.m +125 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.h +38 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.m +45 -0
- package/ios/TOCropViewController/Resources/Base.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/PrivacyInfo.xcprivacy +14 -0
- package/ios/TOCropViewController/Resources/ar.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/ca.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/cs.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/da-DK.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/de.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/en.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/es.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa-IR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fi.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fr.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/hu.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/id.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/it.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ja.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ko.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ms.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/nl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt-BR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ro.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ru.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/sk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/tr.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/uk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/vi.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hans.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hant.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/TOCropViewController.h +479 -0
- package/ios/TOCropViewController/TOCropViewController.m +1355 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.h +43 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.m +240 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.h +39 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.m +48 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.h +104 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.m +783 -0
- package/ios/TOCropViewController/Views/TOCropView.h +311 -0
- package/ios/TOCropViewController/Views/TOCropView.m +1787 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js +4 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js.map +1 -0
- package/lib/module/index.js +83 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +54 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +31 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JCropRect.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +125 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +154 -0
- package/nitrogen/generated/android/c++/JPickedImage.hpp +88 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/CropRect.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/HybridReactNativeImageCropPickerSpec.kt +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +171 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/PickedImage.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/reactnativeimagecroppickerOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.cpp +54 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker+autolinking.rb +62 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +221 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +55 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +116 -0
- package/nitrogen/generated/ios/swift/CropRect.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PickedImage.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec_cxx.swift +202 -0
- package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +474 -0
- package/nitrogen/generated/ios/swift/PickedImage.swift +96 -0
- package/nitrogen/generated/shared/c++/CropRect.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +180 -0
- package/nitrogen/generated/shared/c++/PickedImage.hpp +114 -0
- package/package.json +173 -0
- package/src/ReactNativeImageCropPicker.nitro.ts +72 -0
- 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
|