@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,407 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import PhotosUI
|
|
3
|
+
import ReactNativeNativeLogger
|
|
4
|
+
import UIKit
|
|
5
|
+
import UniformTypeIdentifiers
|
|
6
|
+
|
|
7
|
+
// One picker or cropper flow. It owns the JS promise until the presented UI
|
|
8
|
+
// reports back, and settles it exactly once.
|
|
9
|
+
final class ImageCropPickerSession: NSObject {
|
|
10
|
+
enum Mode {
|
|
11
|
+
case picker
|
|
12
|
+
case cropper(path: String)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private let mode: Mode
|
|
16
|
+
private let config: ImageCropPickerConfig
|
|
17
|
+
private let promise: Promise<PickedImage>
|
|
18
|
+
private let onFinish: (ImageCropPickerSession) -> Void
|
|
19
|
+
|
|
20
|
+
// PHPickerViewController connects to its out-of-process service before it
|
|
21
|
+
// appears, so a requested presentation can stay pending for a moment.
|
|
22
|
+
private static let pendingPresentationTimeout: TimeInterval = 10
|
|
23
|
+
|
|
24
|
+
// Main thread state.
|
|
25
|
+
private var pickerController: PHPickerViewController?
|
|
26
|
+
private var cropController: TOCropViewController?
|
|
27
|
+
private var loadingView: UIView?
|
|
28
|
+
private var pickedFilename: String?
|
|
29
|
+
private var sourceScale: CGFloat = 1
|
|
30
|
+
private var presentationRequestedAt: Date?
|
|
31
|
+
private var hasPresented = false
|
|
32
|
+
private var isBusy = false
|
|
33
|
+
private var isFinished = false
|
|
34
|
+
|
|
35
|
+
init(
|
|
36
|
+
mode: Mode,
|
|
37
|
+
config: ImageCropPickerConfig,
|
|
38
|
+
promise: Promise<PickedImage>,
|
|
39
|
+
onFinish: @escaping (ImageCropPickerSession) -> Void
|
|
40
|
+
) {
|
|
41
|
+
self.mode = mode
|
|
42
|
+
self.config = config
|
|
43
|
+
self.promise = promise
|
|
44
|
+
self.onFinish = onFinish
|
|
45
|
+
super.init()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Whether the session still has UI on screen or work in flight.
|
|
49
|
+
var isActive: Bool {
|
|
50
|
+
guard !isFinished else {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
if isBusy {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
guard let rootController = rootController else {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
if rootController.presentingViewController != nil || rootController.isBeingPresented {
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
if !hasPresented, let presentationRequestedAt {
|
|
63
|
+
return Date().timeIntervalSince(presentationRequestedAt) < Self.pendingPresentationTimeout
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func start() {
|
|
69
|
+
switch mode {
|
|
70
|
+
case .picker:
|
|
71
|
+
presentPicker()
|
|
72
|
+
case .cropper(let path):
|
|
73
|
+
loadCropperImage(path: path)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Settles a session whose UI disappeared without calling back.
|
|
78
|
+
func abandon() {
|
|
79
|
+
finish(.failure(ImageCropPickerError.cancelled))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private var rootController: UIViewController? {
|
|
83
|
+
return pickerController ?? cropController
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private var isPickerMode: Bool {
|
|
87
|
+
if case .picker = mode {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// MARK: - Picker
|
|
94
|
+
|
|
95
|
+
private func presentPicker() {
|
|
96
|
+
guard let presenter = Self.topViewController() else {
|
|
97
|
+
finish(.failure(ImageCropPickerError.noViewController))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// PHPickerViewController runs out of process and needs no photo library
|
|
102
|
+
// permission, so a previously denied permission can't block the picker.
|
|
103
|
+
var configuration = PHPickerConfiguration()
|
|
104
|
+
configuration.filter = .images
|
|
105
|
+
configuration.selectionLimit = 1
|
|
106
|
+
configuration.preferredAssetRepresentationMode = .current
|
|
107
|
+
|
|
108
|
+
let picker = PHPickerViewController(configuration: configuration)
|
|
109
|
+
picker.delegate = self
|
|
110
|
+
picker.modalPresentationStyle = .fullScreen
|
|
111
|
+
pickerController = picker
|
|
112
|
+
present(picker, from: presenter)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private func loadPickedImage(_ result: PHPickerResult, in picker: PHPickerViewController) {
|
|
116
|
+
let provider = result.itemProvider
|
|
117
|
+
guard let typeIdentifier = Self.imageTypeIdentifier(of: provider) else {
|
|
118
|
+
dismissAll { self.finish(.failure(ImageCropPickerError.noImageData)) }
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
isBusy = true
|
|
123
|
+
let filename = provider.suggestedName
|
|
124
|
+
pickedFilename = filename
|
|
125
|
+
showLoading(in: picker.view)
|
|
126
|
+
|
|
127
|
+
provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { [self] url, _ in
|
|
128
|
+
// The file is removed once this handler returns, so decode it right here.
|
|
129
|
+
if let url, let decoded = ImageCropPickerImageProcessor.decodeImage(at: url) {
|
|
130
|
+
handlePickedImage(decoded, filename: filename)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { [self] data, _ in
|
|
134
|
+
handlePickedImage(
|
|
135
|
+
data.flatMap { ImageCropPickerImageProcessor.decodeImage(data: $0) },
|
|
136
|
+
filename: filename
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Runs on the item provider's background queue.
|
|
143
|
+
private func handlePickedImage(_ decoded: DecodedImage?, filename: String?) {
|
|
144
|
+
guard let decoded else {
|
|
145
|
+
DispatchQueue.main.async { [self] in
|
|
146
|
+
isBusy = false
|
|
147
|
+
hideLoading()
|
|
148
|
+
dismissAll { self.finish(.failure(ImageCropPickerError.noImageData)) }
|
|
149
|
+
}
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
guard !config.cropping else {
|
|
154
|
+
DispatchQueue.main.async { [self] in
|
|
155
|
+
isBusy = false
|
|
156
|
+
hideLoading()
|
|
157
|
+
guard !isFinished, let picker = pickerController else {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
sourceScale = decoded.sourceScale
|
|
161
|
+
presentCropper(image: decoded.image, from: picker)
|
|
162
|
+
}
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let result = Result {
|
|
167
|
+
try ImageCropPickerImageProcessor.makeResult(
|
|
168
|
+
image: decoded.image,
|
|
169
|
+
config: config,
|
|
170
|
+
cropRect: nil,
|
|
171
|
+
filename: filename
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
DispatchQueue.main.async { [self] in
|
|
175
|
+
isBusy = false
|
|
176
|
+
hideLoading()
|
|
177
|
+
dismissAll { self.finish(result) }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// MARK: - Cropper
|
|
182
|
+
|
|
183
|
+
private func loadCropperImage(path: String) {
|
|
184
|
+
isBusy = true
|
|
185
|
+
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
186
|
+
let loaded = Result { try ImageCropPickerImageProcessor.loadImage(fromPath: path) }
|
|
187
|
+
DispatchQueue.main.async { [self] in
|
|
188
|
+
isBusy = false
|
|
189
|
+
switch loaded {
|
|
190
|
+
case .success(let decoded):
|
|
191
|
+
guard let presenter = Self.topViewController() else {
|
|
192
|
+
finish(.failure(ImageCropPickerError.noViewController))
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
sourceScale = decoded.sourceScale
|
|
196
|
+
presentCropper(image: decoded.image, from: presenter)
|
|
197
|
+
case .failure(let error):
|
|
198
|
+
finish(.failure(error))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func presentCropper(image: UIImage, from presenter: UIViewController) {
|
|
205
|
+
let controller: TOCropViewController
|
|
206
|
+
if config.cropperCircleOverlay {
|
|
207
|
+
controller = TOCropViewController(croppingStyle: .circular, image: image)
|
|
208
|
+
} else {
|
|
209
|
+
controller = TOCropViewController(image: image)
|
|
210
|
+
if let targetSize = config.targetSize {
|
|
211
|
+
controller.aspectRatioPreset = targetSize
|
|
212
|
+
}
|
|
213
|
+
controller.aspectRatioLockEnabled = !config.freeStyleCropEnabled
|
|
214
|
+
controller.resetAspectRatioEnabled = !controller.aspectRatioLockEnabled
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
controller.title = config.cropperToolbarTitle
|
|
218
|
+
controller.delegate = self
|
|
219
|
+
if let color = ImageCropPickerImageProcessor.color(fromHex: config.cropperChooseColor) {
|
|
220
|
+
controller.doneButtonColor = color
|
|
221
|
+
}
|
|
222
|
+
if let color = ImageCropPickerImageProcessor.color(fromHex: config.cropperCancelColor) {
|
|
223
|
+
controller.cancelButtonColor = color
|
|
224
|
+
}
|
|
225
|
+
controller.doneButtonTitle = config.cropperChooseText
|
|
226
|
+
controller.cancelButtonTitle = config.cropperCancelText
|
|
227
|
+
controller.rotateButtonsHidden = config.cropperRotateButtonsHidden
|
|
228
|
+
controller.modalPresentationStyle = .fullScreen
|
|
229
|
+
controller.modalTransitionStyle = .coverVertical
|
|
230
|
+
|
|
231
|
+
cropController = controller
|
|
232
|
+
present(controller, from: presenter)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// MARK: - Completion
|
|
236
|
+
|
|
237
|
+
private func present(_ controller: UIViewController, from presenter: UIViewController) {
|
|
238
|
+
// UIKit silently ignores a presentation from a controller that is off
|
|
239
|
+
// screen or already presenting, and the promise would never settle.
|
|
240
|
+
guard presenter.viewIfLoaded?.window != nil, presenter.presentedViewController == nil else {
|
|
241
|
+
if controller === cropController {
|
|
242
|
+
cropController = nil
|
|
243
|
+
}
|
|
244
|
+
dismissAll { self.finish(.failure(ImageCropPickerError.noViewController)) }
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
presentationRequestedAt = Date()
|
|
248
|
+
presenter.present(controller, animated: true) { [weak self] in
|
|
249
|
+
self?.hasPresented = true
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private func dismissAll(completion: @escaping () -> Void) {
|
|
254
|
+
guard let presenting = rootController?.presentingViewController else {
|
|
255
|
+
completion()
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
presenting.dismiss(animated: true, completion: completion)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func finish(_ result: Result<PickedImage, Error>) {
|
|
262
|
+
guard !isFinished else {
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
isFinished = true
|
|
266
|
+
hideLoading()
|
|
267
|
+
pickerController = nil
|
|
268
|
+
cropController = nil
|
|
269
|
+
|
|
270
|
+
switch result {
|
|
271
|
+
case .success(let image):
|
|
272
|
+
promise.resolve(withResult: image)
|
|
273
|
+
case .failure(let error):
|
|
274
|
+
if let pickerError = error as? ImageCropPickerError,
|
|
275
|
+
pickerError.code != .pickerCancelled {
|
|
276
|
+
OneKeyLog.warn("ImageCropPicker", "\(pickerError)")
|
|
277
|
+
}
|
|
278
|
+
promise.reject(withError: error)
|
|
279
|
+
}
|
|
280
|
+
onFinish(self)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private func showLoading(in view: UIView) {
|
|
284
|
+
hideLoading()
|
|
285
|
+
let overlay = UIView(frame: view.bounds)
|
|
286
|
+
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
287
|
+
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.4)
|
|
288
|
+
|
|
289
|
+
let indicator = UIActivityIndicatorView(style: .large)
|
|
290
|
+
indicator.color = .white
|
|
291
|
+
indicator.center = CGPoint(x: overlay.bounds.midX, y: overlay.bounds.midY)
|
|
292
|
+
indicator.autoresizingMask = [
|
|
293
|
+
.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin,
|
|
294
|
+
]
|
|
295
|
+
indicator.startAnimating()
|
|
296
|
+
overlay.addSubview(indicator)
|
|
297
|
+
|
|
298
|
+
view.addSubview(overlay)
|
|
299
|
+
loadingView = overlay
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func hideLoading() {
|
|
303
|
+
loadingView?.removeFromSuperview()
|
|
304
|
+
loadingView = nil
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// MARK: - Helpers
|
|
308
|
+
|
|
309
|
+
private static func imageTypeIdentifier(of provider: NSItemProvider) -> String? {
|
|
310
|
+
let identifier = provider.registeredTypeIdentifiers.first { identifier in
|
|
311
|
+
UTType(identifier)?.conforms(to: .image) ?? false
|
|
312
|
+
}
|
|
313
|
+
if let identifier {
|
|
314
|
+
return identifier
|
|
315
|
+
}
|
|
316
|
+
return provider.hasItemConformingToTypeIdentifier(UTType.image.identifier)
|
|
317
|
+
? UTType.image.identifier
|
|
318
|
+
: nil
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
static func topViewController() -> UIViewController? {
|
|
322
|
+
let sceneWindows = UIApplication.shared.connectedScenes
|
|
323
|
+
.compactMap { $0 as? UIWindowScene }
|
|
324
|
+
.filter { $0.activationState != .unattached && $0.activationState != .background }
|
|
325
|
+
.flatMap { $0.windows }
|
|
326
|
+
let appDelegateWindow = UIApplication.shared.delegate?.window ?? nil
|
|
327
|
+
let window = sceneWindows.first { $0.isKeyWindow && $0.windowLevel == .normal && $0.rootViewController != nil }
|
|
328
|
+
?? appDelegateWindow
|
|
329
|
+
?? sceneWindows.first { $0.rootViewController != nil }
|
|
330
|
+
|
|
331
|
+
var controller = window?.rootViewController
|
|
332
|
+
while let presented = controller?.presentedViewController, !presented.isBeingDismissed {
|
|
333
|
+
controller = presented
|
|
334
|
+
}
|
|
335
|
+
return controller
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
extension ImageCropPickerSession: PHPickerViewControllerDelegate {
|
|
340
|
+
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
|
341
|
+
guard !isFinished, !isBusy, cropController == nil else {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
guard let result = results.first else {
|
|
345
|
+
dismissAll { self.finish(.failure(ImageCropPickerError.cancelled)) }
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
loadPickedImage(result, in: picker)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
extension ImageCropPickerSession: TOCropViewControllerDelegate {
|
|
353
|
+
func cropViewController(
|
|
354
|
+
_ cropViewController: TOCropViewController,
|
|
355
|
+
didCropTo image: UIImage,
|
|
356
|
+
with cropRect: CGRect,
|
|
357
|
+
angle: Int
|
|
358
|
+
) {
|
|
359
|
+
guard !isFinished, !isBusy else {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
isBusy = true
|
|
363
|
+
showLoading(in: cropViewController.view)
|
|
364
|
+
|
|
365
|
+
let config = self.config
|
|
366
|
+
let filename = pickedFilename
|
|
367
|
+
// Report the crop rect in the coordinates of the original, undownsampled image.
|
|
368
|
+
let sourceCropRect = CGRect(
|
|
369
|
+
x: (cropRect.origin.x * sourceScale).rounded(),
|
|
370
|
+
y: (cropRect.origin.y * sourceScale).rounded(),
|
|
371
|
+
width: (cropRect.width * sourceScale).rounded(),
|
|
372
|
+
height: (cropRect.height * sourceScale).rounded()
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
376
|
+
let result = Result {
|
|
377
|
+
try ImageCropPickerImageProcessor.makeResult(
|
|
378
|
+
image: ImageCropPickerImageProcessor.resizeCroppedImage(image, config: config),
|
|
379
|
+
config: config,
|
|
380
|
+
cropRect: sourceCropRect,
|
|
381
|
+
filename: filename
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
DispatchQueue.main.async { [self] in
|
|
385
|
+
isBusy = false
|
|
386
|
+
hideLoading()
|
|
387
|
+
dismissAll { self.finish(result) }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func cropViewController(
|
|
393
|
+
_ cropViewController: TOCropViewController,
|
|
394
|
+
didFinishCancelled cancelled: Bool
|
|
395
|
+
) {
|
|
396
|
+
guard !isFinished, !isBusy else {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
if isPickerMode, pickerController?.presentingViewController != nil {
|
|
400
|
+
// Go back to the photo picker, like react-native-image-crop-picker.
|
|
401
|
+
cropController = nil
|
|
402
|
+
cropViewController.dismiss(animated: true)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
dismissAll { self.finish(.failure(ImageCropPickerError.cancelled)) }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
class ReactNativeImageCropPicker: HybridReactNativeImageCropPickerSpec {
|
|
5
|
+
// Main thread only.
|
|
6
|
+
private var activeSession: ImageCropPickerSession?
|
|
7
|
+
|
|
8
|
+
func openPicker(options: ImageCropPickerOptions) throws -> Promise<PickedImage> {
|
|
9
|
+
return startSession(mode: .picker, config: ImageCropPickerConfig(options))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func openCropper(path: String, options: ImageCropPickerOptions) throws -> Promise<PickedImage> {
|
|
13
|
+
return startSession(
|
|
14
|
+
mode: .cropper(path: path),
|
|
15
|
+
config: ImageCropPickerConfig(options, forceCropping: true)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func clean() throws -> Promise<Void> {
|
|
20
|
+
return Promise.parallel {
|
|
21
|
+
try ImageCropPickerImageProcessor.cleanTemporaryDirectory()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func cleanSingle(path: String) throws -> Promise<Void> {
|
|
26
|
+
return Promise.parallel {
|
|
27
|
+
try ImageCropPickerImageProcessor.removeFile(atPath: path)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private func startSession(
|
|
32
|
+
mode: ImageCropPickerSession.Mode,
|
|
33
|
+
config: ImageCropPickerConfig
|
|
34
|
+
) -> Promise<PickedImage> {
|
|
35
|
+
let promise = Promise<PickedImage>()
|
|
36
|
+
DispatchQueue.main.async {
|
|
37
|
+
self.beginSession(mode: mode, config: config, promise: promise)
|
|
38
|
+
}
|
|
39
|
+
return promise
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func beginSession(
|
|
43
|
+
mode: ImageCropPickerSession.Mode,
|
|
44
|
+
config: ImageCropPickerConfig,
|
|
45
|
+
promise: Promise<PickedImage>
|
|
46
|
+
) {
|
|
47
|
+
if let activeSession {
|
|
48
|
+
if activeSession.isActive {
|
|
49
|
+
promise.reject(withError: ImageCropPickerError.inProgress)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
// Its UI went away without calling back; don't block new requests on it.
|
|
53
|
+
activeSession.abandon()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let session = ImageCropPickerSession(mode: mode, config: config, promise: promise) {
|
|
57
|
+
[weak self] finishedSession in
|
|
58
|
+
if self?.activeSession === finishedSession {
|
|
59
|
+
self?.activeSession = nil
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
activeSession = session
|
|
63
|
+
session.start()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// UIImage+CropRotate.h
|
|
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 <UIKit/UIKit.h>
|
|
24
|
+
|
|
25
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
26
|
+
|
|
27
|
+
@interface UIImage (TOCropRotate)
|
|
28
|
+
|
|
29
|
+
/// Crops a portion of an existing image object and returns it as a new image
|
|
30
|
+
/// @param frame The region inside the image to crop (in the image's point space, ie image.size)
|
|
31
|
+
/// @param angle If any, the angle the image is rotated at as well
|
|
32
|
+
/// @param circular Whether the resulting image is returned as a square or a circle
|
|
33
|
+
- (nonnull UIImage *)croppedImageWithFrame:(CGRect)frame
|
|
34
|
+
angle:(NSInteger)angle
|
|
35
|
+
circularClip:(BOOL)circular;
|
|
36
|
+
|
|
37
|
+
@end
|
|
38
|
+
|
|
39
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//
|
|
2
|
+
// UIImage+CropRotate.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 "UIImage+CropRotate.h"
|
|
24
|
+
|
|
25
|
+
@implementation UIImage (TOCropRotate)
|
|
26
|
+
|
|
27
|
+
- (BOOL)hasAlpha {
|
|
28
|
+
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(self.CGImage);
|
|
29
|
+
return (alphaInfo == kCGImageAlphaFirst || alphaInfo == kCGImageAlphaLast ||
|
|
30
|
+
alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaPremultipliedLast);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
- (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle circularClip:(BOOL)circular {
|
|
34
|
+
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat new];
|
|
35
|
+
|
|
36
|
+
#if defined(__IPHONE_17_0)
|
|
37
|
+
// When the source image is HDR, request an HDR-capable rendering format so its
|
|
38
|
+
// highlights aren't silently tone-mapped down to SDR. Falls back to the default
|
|
39
|
+
// format wherever HDR rendering isn't supported.
|
|
40
|
+
if (@available(iOS 17.0, *)) {
|
|
41
|
+
if (self.isHighDynamicRange) {
|
|
42
|
+
UITraitCollection *hdrTraits = [UITraitCollection traitCollectionWithImageDynamicRange:UIImageDynamicRangeHigh];
|
|
43
|
+
UIGraphicsImageRendererFormat *hdrFormat = [UIGraphicsImageRendererFormat formatForTraitCollection:hdrTraits];
|
|
44
|
+
if (hdrFormat.supportsHighDynamicRange) {
|
|
45
|
+
format = hdrFormat;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
#endif
|
|
50
|
+
|
|
51
|
+
format.opaque = !self.hasAlpha && !circular;
|
|
52
|
+
format.scale = self.scale;
|
|
53
|
+
|
|
54
|
+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:frame.size format:format];
|
|
55
|
+
UIImage *croppedImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
|
|
56
|
+
CGContextRef context = rendererContext.CGContext;
|
|
57
|
+
|
|
58
|
+
// If we're capturing a circular image, set the clip mask first
|
|
59
|
+
if (circular) {
|
|
60
|
+
CGContextAddEllipseInRect(context, (CGRect){CGPointZero, frame.size});
|
|
61
|
+
CGContextClip(context);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Offset the origin (Which is the top left corner) to start where our cropping origin is
|
|
65
|
+
CGContextTranslateCTM(context, -frame.origin.x, -frame.origin.y);
|
|
66
|
+
|
|
67
|
+
// If an angle was supplied, rotate the entire canvas + coordinate space to match
|
|
68
|
+
if (angle != 0) {
|
|
69
|
+
// Rotation in radians
|
|
70
|
+
CGFloat rotation = angle * (M_PI / 180.0f);
|
|
71
|
+
|
|
72
|
+
// Work out the new bounding size of the canvas after rotation
|
|
73
|
+
CGRect imageBounds = (CGRect){CGPointZero, self.size};
|
|
74
|
+
CGRect rotatedBounds = CGRectApplyAffineTransform(imageBounds,
|
|
75
|
+
CGAffineTransformMakeRotation(rotation));
|
|
76
|
+
// As we're rotating from the top left corner, and not the center of the canvas, the frame
|
|
77
|
+
// will have rotated out of our visible canvas. Compensate for this.
|
|
78
|
+
CGContextTranslateCTM(context, -rotatedBounds.origin.x, -rotatedBounds.origin.y);
|
|
79
|
+
|
|
80
|
+
// Perform the rotation transformation
|
|
81
|
+
CGContextRotateCTM(context, rotation);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Draw the image with all of the transformation parameters applied.
|
|
85
|
+
// We do not need to worry about specifying the size here since we're already
|
|
86
|
+
// constrained by the context image size
|
|
87
|
+
[self drawAtPoint:CGPointZero];
|
|
88
|
+
}];
|
|
89
|
+
|
|
90
|
+
// Re-apply the retina scale we originally had
|
|
91
|
+
return [UIImage imageWithCGImage:croppedImage.CGImage scale:self.scale orientation:UIImageOrientationUp];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TOCropViewConstants.h
|
|
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 <Foundation/Foundation.h>
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
The shape of the cropping region of this crop view controller
|
|
27
|
+
*/
|
|
28
|
+
typedef NS_ENUM(NSInteger, TOCropViewCroppingStyle) {
|
|
29
|
+
TOCropViewCroppingStyleDefault, // The regular, rectangular crop box
|
|
30
|
+
TOCropViewCroppingStyleCircular // A fixed, circular crop box
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
Whether the control toolbar is placed at the bottom or the top
|
|
35
|
+
*/
|
|
36
|
+
typedef NS_ENUM(NSInteger, TOCropViewControllerToolbarPosition) {
|
|
37
|
+
TOCropViewControllerToolbarPositionBottom, // Bar is placed along the bottom in portrait
|
|
38
|
+
TOCropViewControllerToolbarPositionTop // Bar is placed along the top in portrait (Respects the status bar)
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
static inline NSBundle *TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(NSObject *object) {
|
|
42
|
+
#if SWIFT_PACKAGE
|
|
43
|
+
// SPM is supposed to support the keyword SWIFTPM_MODULE_BUNDLE
|
|
44
|
+
// but I can't figure out how to make it work, so doing it manually
|
|
45
|
+
NSString *bundleName = @"TOCropViewController_TOCropViewController";
|
|
46
|
+
#else
|
|
47
|
+
NSString *bundleName = @"TOCropViewControllerBundle";
|
|
48
|
+
#endif
|
|
49
|
+
NSBundle *resourceBundle = nil;
|
|
50
|
+
NSBundle *classBundle = [NSBundle bundleForClass:object.class];
|
|
51
|
+
NSURL *resourceBundleURL = [classBundle URLForResource:bundleName withExtension:@"bundle"];
|
|
52
|
+
if (resourceBundleURL) {
|
|
53
|
+
resourceBundle = [[NSBundle alloc] initWithURL:resourceBundleURL];
|
|
54
|
+
#ifndef NDEBUG
|
|
55
|
+
if (resourceBundle == nil) {
|
|
56
|
+
@throw [[NSException alloc] initWithName:@"BundleAccessor" reason:[NSString stringWithFormat:@"unable to find bundle named %@", bundleName] userInfo:nil];
|
|
57
|
+
}
|
|
58
|
+
#endif
|
|
59
|
+
} else {
|
|
60
|
+
resourceBundle = classBundle;
|
|
61
|
+
}
|
|
62
|
+
return resourceBundle;
|
|
63
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2025 Tim Oliver
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|