Files
smart-city-digital-twin-mar…/smart-app-city/frontend/node_modules/expo-image-picker/ios/ImagePickerModule.swift
Eric FELIXINE e30ae8ed09 feat(smart-app): implement complete mobile app MVP
- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
2026-06-01 18:00:35 -04:00

254 lines
9.5 KiB
Swift

// Copyright 2022-present 650 Industries. All rights reserved.
import UIKit
import PhotosUI
import ExpoModulesCore
typealias MediaInfo = [UIImagePickerController.InfoKey: Any]
/**
Helper struct storing single picking operation context variables that have their own non-sharable state.
*/
struct PickingContext {
let promise: Promise
let options: ImagePickerOptions
let imagePickerHandler: ImagePickerHandler
}
enum OperationType {
case ask
case get
}
public class ImagePickerModule: Module, OnMediaPickingResultHandler {
public func definition() -> ModuleDefinition {
// TODO: (@bbarthec) change to "ExpoImagePicker" and propagate to other platforms
Name("ExponentImagePicker")
OnCreate {
self.appContext?.permissions?.register([
CameraPermissionRequester(),
MediaLibraryPermissionRequester(),
MediaLibraryWriteOnlyPermissionRequester()
])
}
AsyncFunction("getCameraPermissionsAsync", { (promise: Promise) in
self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .get, promise: promise)
})
AsyncFunction("getMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in
self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .get, promise: promise)
})
AsyncFunction("requestCameraPermissionsAsync", { (promise: Promise) in
self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .ask, promise: promise)
})
AsyncFunction("requestMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in
self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .ask, promise: promise)
})
AsyncFunction("launchCameraAsync", { (options: ImagePickerOptions, promise: Promise) -> Void in
guard let permissions = self.appContext?.permissions else {
return promise.reject(PermissionsModuleNotFoundException())
}
guard permissions.hasGrantedPermission(usingRequesterClass: CameraPermissionRequester.self) else {
return promise.reject(MissingCameraPermissionException())
}
self.launchImagePicker(sourceType: .camera, options: options, promise: promise)
})
.runOnQueue(DispatchQueue.main)
AsyncFunction("launchImageLibraryAsync", { (options: ImagePickerOptions, promise: Promise) in
self.launchImagePicker(sourceType: .photoLibrary, options: options, promise: promise)
})
.runOnQueue(DispatchQueue.main)
}
private var currentPickingContext: PickingContext?
private func handlePermissionRequest(requesterClass: AnyClass, operationType: OperationType, promise: Promise) {
guard let permissions = self.appContext?.permissions else {
return promise.reject(PermissionsModuleNotFoundException())
}
switch operationType {
case .get: permissions.getPermissionUsingRequesterClass(requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter)
case .ask: permissions.askForPermission(usingRequesterClass: requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter)
}
}
private func getMediaLibraryPermissionRequester(_ writeOnly: Bool) -> AnyClass {
return writeOnly ? MediaLibraryWriteOnlyPermissionRequester.self : MediaLibraryPermissionRequester.self
}
private func launchImagePicker(sourceType: UIImagePickerController.SourceType, options: ImagePickerOptions, promise: Promise) {
let imagePickerDelegate = ImagePickerHandler(onMediaPickingResultHandler: self, hideStatusBarWhenPresented: options.allowsEditing && !options.allowsMultipleSelection)
let pickingContext = PickingContext(promise: promise,
options: options,
imagePickerHandler: imagePickerDelegate)
if #available(iOS 14, *), !options.allowsEditing && sourceType != .camera {
self.launchMultiSelectPicker(pickingContext: pickingContext)
} else {
self.launchLegacyImagePicker(sourceType: sourceType, pickingContext: pickingContext)
}
}
private func launchLegacyImagePicker(sourceType: UIImagePickerController.SourceType, pickingContext: PickingContext) {
let options = pickingContext.options
let picker = UIImagePickerController()
picker.fixCannotMoveEditingBox()
if sourceType == .camera {
#if targetEnvironment(simulator)
return pickingContext.promise.reject(CameraUnavailableOnSimulatorException())
#else
picker.sourceType = .camera
picker.cameraDevice = options.cameraType == .front ? .front : .rear
#endif
}
if sourceType == .photoLibrary {
picker.sourceType = .photoLibrary
}
picker.mediaTypes = options.mediaTypes.toArray()
if options.mediaTypes.requiresMicrophonePermission() && sourceType == .camera {
do {
try checkMicrophonePermissions()
} catch {
pickingContext.promise.reject(error)
return
}
}
picker.videoExportPreset = options.videoExportPreset.toAVAssetExportPreset()
picker.videoQuality = options.videoQuality.toQualityType()
picker.videoMaximumDuration = options.videoMaxDuration
if options.allowsEditing {
picker.allowsEditing = options.allowsEditing
if options.videoMaxDuration > 600 {
return pickingContext.promise.reject(MaxDurationWhileEditingExceededException())
}
if options.videoMaxDuration == 0 {
picker.videoMaximumDuration = 600.0
}
}
presentPickerUI(picker, pickingContext: pickingContext)
}
private func checkMicrophonePermissions() throws {
guard Bundle.main.object(forInfoDictionaryKey: "NSMicrophoneUsageDescription") != nil else {
throw MissingMicrophonePermissionException()
}
}
@available(iOS 14, *)
private func launchMultiSelectPicker(pickingContext: PickingContext) {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
let options = pickingContext.options
// selection limit = 1 --> single selection, reflects the old picker behavior
configuration.selectionLimit = options.allowsMultipleSelection ? options.selectionLimit : SINGLE_SELECTION
configuration.filter = options.mediaTypes.toPickerFilter()
if #available(iOS 14, *) {
configuration.preferredAssetRepresentationMode = options.preferredAssetRepresentationMode.toAssetRepresentationMode()
}
if #available(iOS 15, *) {
configuration.selection = options.orderedSelection ? .ordered : .default
}
let picker = PHPickerViewController(configuration: configuration)
presentPickerUI(picker, pickingContext: pickingContext)
}
private func presentPickerUI(_ picker: PickerUIController, pickingContext context: PickingContext) {
guard let currentViewController = self.appContext?.utilities?.currentViewController() else {
return context.promise.reject(MissingCurrentViewControllerException())
}
picker.modalPresentationStyle = context.options.presentationStyle.toPresentationStyle()
if UIDevice.current.userInterfaceIdiom == .pad {
let viewFrame = currentViewController.view.frame
picker.popoverPresentationController?.sourceRect = CGRect(
x: viewFrame.midX,
y: viewFrame.maxY,
width: 0,
height: 0
)
picker.popoverPresentationController?.sourceView = currentViewController.view
}
picker.setResultHandler(context.imagePickerHandler)
// Store picking context as we're navigating to the different view controller (starting asynchronous flow)
self.currentPickingContext = context
currentViewController.present(picker, animated: true, completion: nil)
}
// MARK: - OnMediaPickingResultHandler
func didCancelPicking() {
self.currentPickingContext?.promise.resolve(ImagePickerResponse(assets: nil, canceled: true))
self.currentPickingContext = nil
}
@available(iOS 14, *)
func didPickMultipleMedia(selection: [PHPickerResult]) {
guard let options = self.currentPickingContext?.options,
let promise = self.currentPickingContext?.promise else {
log.error("Picking operation context has been lost.")
return
}
guard let fileSystem = self.appContext?.fileSystem else {
return promise.reject(FileSystemModuleNotFoundException())
}
let mediaHandler = MediaHandler(fileSystem: fileSystem,
options: options)
// Clean up the currently stored picking context
self.currentPickingContext = nil
mediaHandler.handleMultipleMedia(selection) { result -> Void in
switch result {
case .failure(let error): return promise.reject(error)
case .success(let response): return promise.resolve(response)
}
}
}
func didPickMedia(mediaInfo: MediaInfo) {
guard let options = self.currentPickingContext?.options,
let promise = self.currentPickingContext?.promise else {
log.error("Picking operation context has been lost.")
return
}
guard let fileSystem = self.appContext?.fileSystem else {
return promise.reject(FileSystemModuleNotFoundException())
}
// Clean up the currently stored picking context
self.currentPickingContext = nil
let mediaHandler = MediaHandler(fileSystem: fileSystem,
options: options)
mediaHandler.handleMedia(mediaInfo) { result -> Void in
switch result {
case .failure(let error): return promise.reject(error)
case .success(let response): return promise.resolve(response)
}
}
}
}