diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f15019c..5fc9f75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,17 +30,19 @@ jobs: lint: name: SwiftLint runs-on: macOS-latest - + steps: - name: Checkout uses: actions/checkout@v5 - - - name: SwiftLint + + - name: Install SwiftLint run: | if ! command -v swiftlint &> /dev/null; then brew install swiftlint - fi - swiftlint --strict + fi + + - name: SwiftLint + run: swiftlint --strict validate-podspec: name: Validate Podspec diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..81e1a64 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,60 @@ +# SwiftFormat configuration for IONCameraLib +# Targets iOS 14+ / Swift 5.9+ + +--swiftversion 5.9 +--minversion 0.54.0 + +# Indentation +--indent 4 +--tabwidth 4 +--smarttabs enabled +--indentcase false + +# Line endings +--linebreaks lf + +# Headers +--header strip + +# Line length — matches SwiftLint warning threshold +--maxwidth 150 +--wraparguments before-first +--wrapparameters before-first +--wrapcollections before-first +--closingparen balanced +--wrapreturntype if-multiline + +# Imports +--importgrouping testable-last + +# Self — remove redundant self outside init/deinit +--self init-only + +# Trailing items +--semicolons never +--commas always +--trimwhitespace always + +# Attributes +--funcattributes prev-line +--typeattributes prev-line +--varattributes same-line + +# Closures & functions +--trailingclosures always +--nospaceoperators ..<,... +--operatorfunc spaced + +# Braces +--guardelse auto + +# Redundant syntax +--redundanttype inferred +--stripunusedargs closure-only +--patternlet inline + +# Organise declarations (protocols, extensions, types) +--extensionacl on-declarations + +# Disable rules that conflict with SwiftLint opt-in rules +--disable blankLinesAtStartOfScope diff --git a/.swiftlint.yml b/.swiftlint.yml index 66bf042..6b2887b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,34 +1,61 @@ +included: + - Sources + - Tests + disabled_rules: -- trailing_whitespace -- switch_case_alignment + - trailing_whitespace # handled by SwiftFormat + - line_length # handled by SwiftFormat --maxwidth + opt_in_rules: -- empty_count -- empty_string -excluded: -- Carthage -- Pods -- vendor -- SwiftLint/Common/3rdPartyLib + # Code quality + - empty_count + - empty_string + - explicit_init + - first_where + - contains_over_filter_count + - unavailable_function + - force_unwrapping + - implicitly_unwrapped_optional + - redundant_nil_coalescing + - prefer_self_type_over_type_of_self + # Formatting & style + - closure_end_indentation + - closure_spacing + - collection_alignment + - literal_expression_end_indentation + - multiline_arguments + - multiline_parameters + - operator_usage_whitespace + - sorted_imports + - unneeded_parentheses_in_closure_argument + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + +# Severity overrides +force_cast: warning +force_try: warning + line_length: - warning: 150 - error: 200 - ignores_function_declarations: true - ignores_comments: true - ignores_urls: true + warning: 150 + error: 200 + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true function_body_length: - warning: 300 - error: 500 + warning: 40 + error: 60 function_parameter_count: - warning: 6 - error: 8 + warning: 6 + error: 8 + ignores_default_parameters: true type_body_length: - warning: 300 - error: 500 + warning: 200 + error: 300 file_length: - warning: 1000 - error: 1500 - ignore_comment_only_lines: true + warning: 400 + error: 600 + ignore_comment_only_lines: true cyclomatic_complexity: - warning: 15 - error: 25 -reporter: "xcode" \ No newline at end of file + warning: 10 + error: 15 +reporter: "xcode" diff --git a/Package.swift b/Package.swift index c42990f..55ffa95 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,19 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "IONCameraLib", platforms: [ - .iOS(.v14) + .iOS(.v14), ], products: [ .library( name: "IONCameraLib", - targets: ["IONCameraLib"]), + targets: ["IONCameraLib"] + ), ], dependencies: [ - .package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"), + .package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"), .package(url: "https://github.com/Quick/Quick.git", from: "7.0.0"), ], targets: [ diff --git a/Sources/IONCameraLib/Extensions/IONCAMRFlowResultsDelegate+Default.swift b/Sources/IONCameraLib/Extensions/IONCAMRFlowResultsDelegate+Default.swift index 069d39b..af59f79 100644 --- a/Sources/IONCameraLib/Extensions/IONCAMRFlowResultsDelegate+Default.swift +++ b/Sources/IONCameraLib/Extensions/IONCAMRFlowResultsDelegate+Default.swift @@ -9,16 +9,16 @@ extension IONCAMRFlowResultsHandler { switch result { case .success(let value): if let mediaResult = value as? IONCAMRMediaResult { - self.responseDelegate?.callback(result: mediaResult) + responseDelegate?.callback(result: mediaResult) } else if let mediaArray = value as? [IONCAMRMediaResult] { - self.responseDelegate?.callback(result: mediaArray) + responseDelegate?.callback(result: mediaArray) } case .failure(let error): - self.responseDelegate?.callback(error: error) + responseDelegate?.callback(error: error) } } - + func didCancel(_ error: IONCAMRError) { - self.responseDelegate?.callback(error: error) + responseDelegate?.callback(error: error) } } diff --git a/Sources/IONCameraLib/Extensions/IONCAMRMediaOptions+PickerAdapter.swift b/Sources/IONCameraLib/Extensions/IONCAMRMediaOptions+PickerAdapter.swift index 1efca84..d257e10 100644 --- a/Sources/IONCameraLib/Extensions/IONCAMRMediaOptions+PickerAdapter.swift +++ b/Sources/IONCameraLib/Extensions/IONCAMRMediaOptions+PickerAdapter.swift @@ -1,27 +1,27 @@ import Photos -import UniformTypeIdentifiers import UIKit +import UniformTypeIdentifiers /// Contains all method that converts `IONCAMRMediaOptions` properties into a native SDK equivalent extension IONCAMRMediaOptions { /// Converts the `direction` property into a `UIImagePickerController.CameraDevice`. /// - Returns: The resulting equivalent var cameraDevice: UIImagePickerController.CameraDevice { - switch self.direction { - case .front: return .front - case .back: return .rear + switch direction { + case .front: .front + case .back: .rear } } } extension IONCAMRMediaType: Hashable { var stringArray: [String] { - self.transform(basedOn: [IONCAMRMediaType.picture: UTType.image, .video: .movie]) + transform(basedOn: [IONCAMRMediaType.picture: UTType.image, .video: .movie]) .map(\.identifier) } - + var phAssetArray: [PHAssetMediaType] { - self.transform(basedOn: [IONCAMRMediaType.picture: PHAssetMediaType.image, .video: .video]) + transform(basedOn: [IONCAMRMediaType.picture: PHAssetMediaType.image, .video: .video]) } private func transform(basedOn map: [Self: T]) -> [T] { diff --git a/Sources/IONCameraLib/Extensions/UIImage+MediaOptions.swift b/Sources/IONCameraLib/Extensions/UIImage+MediaOptions.swift index e225b64..9c8f904 100644 --- a/Sources/IONCameraLib/Extensions/UIImage+MediaOptions.swift +++ b/Sources/IONCameraLib/Extensions/UIImage+MediaOptions.swift @@ -8,36 +8,36 @@ extension UIImage { func toData(with options: IONCAMRTakePhotoOptions? = nil) -> Data? { let data: Data? - if let options = options, options.encodingType == .jpeg { + if let options, options.encodingType == .jpeg { let quality = !options.allowEdit && !options.correctOrientation && options.quality == 100 ? 1.0 : CGFloat(options.quality) / 100 - data = self.jpegData(compressionQuality: quality) + data = jpegData(compressionQuality: quality) } else { - data = self.pngData() + data = pngData() } return data } - + /// Provides a couple of transformations (rotation and resize) to the `UIImage` object, based on the user defined options. /// - Parameter options: User defined options containing the transformations (if any) to apply. /// - Returns: The resulting image. It returns `nil` if some issue occured. func fix(with options: IONCAMRTakePhotoOptions) -> UIImage? { var image = self - + if options.correctOrientation, let orientedImage = image.fixOrientation() { image = orientedImage } if let targetSize = options.size, let resizedImage = image.resizeTo(CGSize(size: targetSize)) { image = resizedImage } - + return image } - + private func applyConfigurations(_ resolution: CGFloat, and quality: CGFloat) -> String? { var image = self let minimumSide = resolution - + var newSize: CGSize? if image.size.height > image.size.width { if image.size.width > minimumSide { @@ -48,8 +48,8 @@ extension UIImage { let ratio = image.size.width / image.size.height newSize = .init(width: minimumSide * ratio, height: minimumSide) } - - if let newSize = newSize, let resizedImage = image.resizeTo(newSize) { + + if let newSize, let resizedImage = image.resizeTo(newSize) { image = resizedImage } return image.jpegData(compressionQuality: quality)?.base64EncodedString() @@ -57,25 +57,28 @@ extension UIImage { } // MARK: - IONCAMRRecordVideoOptions extension + extension UIImage { func pictureThumbnailData( with originalResolution: IONCAMRSize? = nil, and originalQuality: Int = IONCAMRTakePhotoOptions.ThumbnailDefaultConfigurations.quality - ) -> String? { - guard let originalResolution = originalResolution ?? (try? .initSquare(with: IONCAMRTakePhotoOptions.ThumbnailDefaultConfigurations.resolution)) + ) + -> String? { + guard let originalResolution = originalResolution ?? + (try? .initSquare(with: IONCAMRTakePhotoOptions.ThumbnailDefaultConfigurations.resolution)) else { return nil } let resolution = CGFloat( min(originalResolution.height, originalResolution.width, IONCAMRTakePhotoOptions.ThumbnailDefaultConfigurations.resolution) ) let quality = CGFloat(originalQuality / 100) - return self.applyConfigurations(resolution, and: quality) + return applyConfigurations(resolution, and: quality) } - + var defaultVideoThumbnailData: String? { let resolution = CGFloat(IONCAMRRecordVideoOptions.ThumbnailDefaultConfigurations.resolution) let quality = CGFloat(IONCAMRRecordVideoOptions.ThumbnailDefaultConfigurations.quality) - return self.applyConfigurations(resolution, and: quality) + return applyConfigurations(resolution, and: quality) } } diff --git a/Sources/IONCameraLib/IONCAMRAssets.swift b/Sources/IONCameraLib/IONCAMRAssets.swift index 7a2b469..3267280 100644 --- a/Sources/IONCameraLib/IONCAMRAssets.swift +++ b/Sources/IONCameraLib/IONCAMRAssets.swift @@ -2,32 +2,31 @@ import SwiftUI /// Centralized asset management public enum IONCAMRAssets { - /// System icons used in the image editor public enum ImageEditor { /// Horizontal flip icon public static let flip = "flip" - - /// 90-degree rotation icon + + /// 90-degree rotation icon public static let rotate = "rotate" } - + /// Create an Image view from bundle asset name - /// + /// /// - Parameter name: The asset name in the bundle - /// - Returns: SwiftUI Image view + /// - Returns: SwiftUI Image view public static func bundleImage(_ name: String) -> Image { #if SWIFT_PACKAGE - return Image(name, bundle: .module) + return Image(name, bundle: .module) #else - if let bundle = resourceBundle { - return Image(name, bundle: bundle) - } else { - return Image(name) - } + if let bundle = resourceBundle { + return Image(name, bundle: bundle) + } else { + return Image(name) + } #endif } - + /// Helper to find the CocoaPods resource bundle private static var resourceBundle: Bundle? { if let url = Bundle.module.url(forResource: "IONCameraLibResources", withExtension: "bundle") { @@ -38,12 +37,13 @@ public enum IONCAMRAssets { } #if !SWIFT_PACKAGE -// MARK: - Bundle Extension for non-SPM builds -extension Bundle { - /// IONCameraLib module bundle for non-SPM builds - static var module: Bundle { - return Bundle(for: IONCAMRCameraManager.self) - } -} + // MARK: - Bundle Extension for non-SPM builds + + extension Bundle { + /// IONCameraLib module bundle for non-SPM builds + static var module: Bundle { + Bundle(for: IONCAMRCameraManager.self) + } + } #endif diff --git a/Sources/IONCameraLib/IONCAMRCameraManager.swift b/Sources/IONCameraLib/IONCAMRCameraManager.swift index 66f12ae..c6f2ca9 100644 --- a/Sources/IONCameraLib/IONCAMRCameraManager.swift +++ b/Sources/IONCameraLib/IONCAMRCameraManager.swift @@ -3,36 +3,38 @@ import UIKit final class IONCAMRCameraManager: NSObject { private weak var delegate: IONCAMRCallbackDelegate? private let flow: IONCAMRFlowDelegate - + init(delegate: IONCAMRCallbackDelegate, flow: IONCAMRFlowDelegate) { self.delegate = delegate self.flow = flow super.init() self.flow.delegate = self } - + convenience init(delegate: IONCAMRCallbackDelegate, viewController: UIViewController) { let coordinator = IONCAMRCoordinator(rootViewController: viewController) let flowBehaviour = IONCAMRFlowBehaviour(coordinator: coordinator) - + self.init(delegate: delegate, flow: flowBehaviour) } } extension IONCAMRCameraManager: IONCAMRCameraActionDelegate { func takePhoto(with options: IONCAMRTakePhotoOptions) { - self.flow.takePhoto(with: options) + flow.takePhoto(with: options) } - + func recordVideo(with options: IONCAMRRecordVideoOptions) { - self.flow.recordVideo(with: options) + flow.recordVideo(with: options) } - + func cleanTemporaryFiles() { - self.flow.cleanTemporaryFiles() + flow.cleanTemporaryFiles() } } extension IONCAMRCameraManager: IONCAMRFlowResultsHandler { - var responseDelegate: IONCAMRCallbackDelegate? { return delegate } + var responseDelegate: IONCAMRCallbackDelegate? { + delegate + } } diff --git a/Sources/IONCameraLib/IONCAMRCoordinator.swift b/Sources/IONCameraLib/IONCAMRCoordinator.swift index b16579e..247c4bb 100644 --- a/Sources/IONCameraLib/IONCAMRCoordinator.swift +++ b/Sources/IONCameraLib/IONCAMRCoordinator.swift @@ -6,36 +6,44 @@ class IONCAMRCoordinator { private let rootViewController: UIViewController /// Contains all the views that were added to the coordinator. private var currentlyPresentedViewControllerArray: [UIViewController] - + /// Indicates if the user is currently on a multiple step screen. var isSecondStep: Bool { - self.currentlyPresentedViewControllerArray.count > 1 + currentlyPresentedViewControllerArray.count > 1 } - + /// Constructor. /// - Parameter rootViewController: Root view of the plugin. init(rootViewController: UIViewController) { self.rootViewController = rootViewController self.currentlyPresentedViewControllerArray = [] } - + /// Presents the passed view controller, adding it to the currently presented view controller array. /// - Parameter viewController: New view controller to present. func present(_ viewController: UIViewController) { - let presentedViewController = self.currentlyPresentedViewControllerArray.last ?? self.rootViewController - if (viewController.modalPresentationStyle == UIModalPresentationStyle.popover) { - viewController.popoverPresentationController?.sourceRect = CGRectMake(presentedViewController.view.center.x, presentedViewController.view.center.y, 0, 0); - viewController.popoverPresentationController?.sourceView = presentedViewController.view; - viewController.popoverPresentationController?.permittedArrowDirections = []; - + let presentedViewController = currentlyPresentedViewControllerArray.last ?? rootViewController + if viewController.modalPresentationStyle == UIModalPresentationStyle.popover { + viewController.popoverPresentationController?.sourceRect = CGRect( + x: + presentedViewController.view.center.x, + y: + presentedViewController.view.center.y, + width: + 0, + height: + 0 + ) + viewController.popoverPresentationController?.sourceView = presentedViewController.view + viewController.popoverPresentationController?.permittedArrowDirections = [] } presentedViewController.present(viewController, animated: true) - self.currentlyPresentedViewControllerArray.append(viewController) + currentlyPresentedViewControllerArray.append(viewController) } - + /// Dismisses the currently presented view controllers. In case of a multiple step screen, all are dismissed. func dismiss() { - self.rootViewController.dismiss(animated: true) - self.currentlyPresentedViewControllerArray.removeAll() + rootViewController.dismiss(animated: true) + currentlyPresentedViewControllerArray.removeAll() } } diff --git a/Sources/IONCameraLib/IONCAMREditManager.swift b/Sources/IONCameraLib/IONCAMREditManager.swift index 870a27d..4385822 100644 --- a/Sources/IONCameraLib/IONCAMREditManager.swift +++ b/Sources/IONCameraLib/IONCAMREditManager.swift @@ -3,32 +3,34 @@ import UIKit final class IONCAMREditManager: NSObject { private weak var delegate: IONCAMRCallbackDelegate? private let flow: IONCAMRFlowDelegate - + init(delegate: IONCAMRCallbackDelegate, flow: IONCAMRFlowDelegate) { self.delegate = delegate self.flow = flow super.init() self.flow.delegate = self } - + convenience init(delegate: IONCAMRCallbackDelegate, viewController: UIViewController) { let coordinator = IONCAMRCoordinator(rootViewController: viewController) let flowBehaviour = IONCAMRFlowBehaviour(coordinator: coordinator) - + self.init(delegate: delegate, flow: flowBehaviour) } } -extension IONCAMREditManager: IONCAMREditActionDelegate { +extension IONCAMREditManager: IONCAMREditActionDelegate { func editPhoto(_ image: UIImage) { - self.flow.editPhoto(image) + flow.editPhoto(image) } - + func editPhoto(with options: IONCAMRPhotoEditOptions) { - self.flow.editPhoto(with: options) + flow.editPhoto(with: options) } } extension IONCAMREditManager: IONCAMRFlowResultsHandler { - var responseDelegate: IONCAMRCallbackDelegate? { return delegate } + var responseDelegate: IONCAMRCallbackDelegate? { + delegate + } } diff --git a/Sources/IONCameraLib/IONCAMRFactory.swift b/Sources/IONCameraLib/IONCAMRFactory.swift index 95bac06..702d815 100644 --- a/Sources/IONCameraLib/IONCAMRFactory.swift +++ b/Sources/IONCameraLib/IONCAMRFactory.swift @@ -1,25 +1,41 @@ import UIKit /// Factory structure that creates a Camera Wrapper. -public struct IONCAMRFactory { +public enum IONCAMRFactory { /// Method that creates a Camera Wrapper of type `IONCAMRCamera`. /// - Parameters: /// - delegate: Object responsible for the callback calls of the `IONCAMRCamera` class. /// - viewController: Root view controller, from whom every view gets pushed on top of. /// - Returns: An instance of the `IONCAMRCamera` class. - public static func createCameraManagerWrapper(withDelegate delegate: IONCAMRCallbackDelegate, and viewController: UIViewController) -> IONCAMRCameraActionDelegate { + public static func createCameraManagerWrapper( + withDelegate delegate: IONCAMRCallbackDelegate, + and viewController: UIViewController + ) + -> IONCAMRCameraActionDelegate { IONCAMRCameraManager(delegate: delegate, viewController: viewController) } - - public static func createGalleryManagerWrapper(withDelegate delegate: IONCAMRCallbackDelegate, and viewController: UIViewController) -> IONCAMRGalleryActionDelegate { + + public static func createGalleryManagerWrapper( + withDelegate delegate: IONCAMRCallbackDelegate, + and viewController: UIViewController + ) + -> IONCAMRGalleryActionDelegate { IONCAMRGalleryManager(delegate: delegate, viewController: viewController) } - public static func createEditManagerWrapper(withDelegate delegate: IONCAMRCallbackDelegate, and viewController: UIViewController) -> IONCAMREditActionDelegate { + public static func createEditManagerWrapper( + withDelegate delegate: IONCAMRCallbackDelegate, + and viewController: UIViewController + ) + -> IONCAMREditActionDelegate { IONCAMREditManager(delegate: delegate, viewController: viewController) } - - public static func createVideoManagerWrapper(withDelegate delegate: IONCAMRCallbackDelegate, and viewController: UIViewController) -> IONCAMRVideoActionDelegate { + + public static func createVideoManagerWrapper( + withDelegate delegate: IONCAMRCallbackDelegate, + and viewController: UIViewController + ) + -> IONCAMRVideoActionDelegate { IONCAMRVideoManager(delegate: delegate, viewController: viewController) } } diff --git a/Sources/IONCameraLib/IONCAMRFlowBehaviour.swift b/Sources/IONCameraLib/IONCAMRFlowBehaviour.swift index bd78589..c82082b 100644 --- a/Sources/IONCameraLib/IONCAMRFlowBehaviour.swift +++ b/Sources/IONCameraLib/IONCAMRFlowBehaviour.swift @@ -2,7 +2,6 @@ import Photos import UIKit final class IONCAMRFlowBehaviour: NSObject, IONCAMRFlowDelegate { - /// Responsible for handling and enabling the image picker behaviour. private let picker: IONCAMRPickerDelegate /// Responsible for handling and enabling the editing picker behaviour. @@ -15,15 +14,15 @@ final class IONCAMRFlowBehaviour: NSObject, IONCAMRFlowDelegate { private let metadataGetter: IONCAMRMetadataGetterDelegate private let imageFetcher: IONCAMRImageFetcherDelegate private let urlGenerator: IONCAMRURLGeneratorDelegate - + /// Handles the result of interacting with the flow interface. weak var delegate: IONCAMRFlowResultsDelegate? /// Object responsible for managing the user interface screens and respective flow. var coordinator: IONCAMRCoordinator var temporaryURLArray: [URL] = [] - + private var options: IONCAMRDefaultOptionsDelegate? - + /// Constructor method. /// - Parameters: /// - picker: Handles the picker behaviour. @@ -51,19 +50,19 @@ final class IONCAMRFlowBehaviour: NSObject, IONCAMRFlowDelegate { self.imageFetcher = imageFetcher self.urlGenerator = urlGenerator super.init() - + self.picker.delegate = self self.editorBehaviour.delegate = self self.galleryBehaviour.delegate = self } - + convenience init(coordinator: IONCAMRCoordinator) { let pickerBehaviour = IONCAMRPickerBehaviour() let editorBehaviour = IONCAMREditorBehaviour() let mediaResultGenerator = IONCAMRMediaResultGenerator() let galleryBehaviour = IONCAMRGalleryBehaviour(metadataGetter: mediaResultGenerator) let permissionsBehaviour = IONCAMRPermissionsBehaviour(coordinator: coordinator) - + self.init( picker: pickerBehaviour, editorBehaviour: editorBehaviour, @@ -76,144 +75,151 @@ final class IONCAMRFlowBehaviour: NSObject, IONCAMRFlowDelegate { coordinator: coordinator ) } - + func takePhoto(with options: IONCAMRTakePhotoOptions) { captureMedia(with: options) } - + func recordVideo(with options: IONCAMRRecordVideoOptions) { captureMedia(with: options) } - + private func captureMedia(with mediaOptions: IONCAMRMediaOptions) { - self.permissionsBehaviour.checkForCamera { [weak self] authorised in - guard let self = self else { return } + permissionsBehaviour.checkForCamera { [weak self] authorised in + guard let self else { return } guard authorised else { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: .cameraAccess) + delegate?.didFailed(type: IONCAMRMediaResult.self, with: .cameraAccess) return } - - guard self.picker.isCameraAvailable() else { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: .cameraAvailability) + + guard picker.isCameraAvailable() else { + delegate?.didFailed(type: IONCAMRMediaResult.self, with: .cameraAvailability) return } - - self.options = mediaOptions - self.picker.captureMedia(with: mediaOptions) { [weak self] viewController in + + options = mediaOptions + picker.captureMedia(with: mediaOptions) { [weak self] viewController in self?.present(viewController) } } } - + func editPhoto(_ image: UIImage) { - self.editorBehaviour.editPicture(image) { [weak self] viewController in + editorBehaviour.editPicture(image) { [weak self] viewController in self?.present(viewController) } } - + func editPhoto(with options: IONCAMRPhotoEditOptions) { - guard let image = self.imageFetcher.retrieveImage(from: options.uri) else { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: .fetchImageFromURLFailed) + guard let image = imageFetcher.retrieveImage(from: options.uri) else { + delegate?.didFailed(type: IONCAMRMediaResult.self, with: .fetchImageFromURLFailed) return } - + self.options = options - self.editorBehaviour.editPicture(image) { [weak self] viewController in + editorBehaviour.editPicture(image) { [weak self] viewController in self?.present(viewController) } } - + func chooseFromGallery(with options: IONCAMRGalleryOptions) { - self.permissionsBehaviour.checkForPhotoLibrary { [weak self] authorised in - guard let self = self else { return } - + permissionsBehaviour.checkForPhotoLibrary { [weak self] authorised in + guard let self else { return } + guard authorised else { // the type is indifferent as the flow will be the same - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: .photoLibraryAccess) + delegate?.didFailed(type: IONCAMRMediaResult.self, with: .photoLibraryAccess) return } - + self.options = options - self.galleryBehaviour.chooseFromGallery(with: options) { [weak self] viewController in + galleryBehaviour.chooseFromGallery(with: options) { [weak self] viewController in self?.present(viewController) } } } - + func cleanTemporaryFiles() { - self.temporaryURLArray.forEach { try? $0.deleteTemporaryPath() } - self.temporaryURLArray.removeAll() + temporaryURLArray.forEach { try? $0.deleteTemporaryPath() } + temporaryURLArray.removeAll() } } extension IONCAMRFlowBehaviour: IONCAMRCancelResultsDelegate { func didCancel(_ object: AnyObject) { - if object === self.picker, let mediaOptions = self.options as? IONCAMREditMediaTypeOptionsDelegate { + if object === picker, let mediaOptions = options as? IONCAMREditMediaTypeOptionsDelegate { switch mediaOptions.mediaType { case .picture: - self.delegate?.didCancel(.takePictureCancel) + delegate?.didCancel(.takePictureCancel) case .video: - self.delegate?.didCancel(.captureVideoCancel) + delegate?.didCancel(.captureVideoCancel) case .both: - self.delegate?.didCancel(.chooseMultimediaCancel) - default: break // not supposed to get here + delegate?.didCancel(.chooseMultimediaCancel) + default: break // not supposed to get here } - } else if object === self.editorBehaviour { - self.delegate?.didCancel(.editPictureCancel) - } else if object === self.galleryBehaviour { - self.delegate?.didCancel(.choosePictureCancel) + } else if object === editorBehaviour { + delegate?.didCancel(.editPictureCancel) + } else if object === galleryBehaviour { + delegate?.didCancel(.choosePictureCancel) } - self.coordinator.dismiss() - self.options = nil + coordinator.dismiss() + options = nil } } extension IONCAMRFlowBehaviour: IONCAMRResultsDelegate { func didReturn(_ object: AnyObject, with result: Result) { - if object === self.picker { - self.pickerDidReturn(result) - } else if object === self.editorBehaviour { - self.editorDidReturn(result) - } else if object === self.galleryBehaviour { - self.galleryDidReturnSingle(result) + if object === picker { + pickerDidReturn(result) + } else if object === editorBehaviour { + editorDidReturn(result) + } else if object === galleryBehaviour { + galleryDidReturnSingle(result) } } } extension IONCAMRFlowBehaviour: IONCAMRMultipleResultsDelegate { func didReturn(_ result: Result<[IONCAMRMediaResult], IONCAMRError>) { - self.galleryDidReturnMultiple(result) + galleryDidReturnMultiple(result) } } -private extension IONCAMRFlowBehaviour { +extension IONCAMRFlowBehaviour { /// Push a new view controller into the navigation stack, through the `coordinator` object. /// - Parameter viewController: View Controller to push. - func present(_ viewController: UIViewController) { + private func present(_ viewController: UIViewController) { DispatchQueue.main.async { self.coordinator.present(viewController) } } - + /// Enum containing error related with image transformation. - enum IONCAMRMultimediaError: Error { + fileprivate enum IONCAMRMultimediaError: Error { case mediaOptionsConversion, mediaResultCreation, stringConversion, thumbnailGeneratorIssue, treatmentIssue } - - func convertToMediaResult(_ image: UIImage, with options: IONCAMRTakePhotoOptions? = nil, separateReturnTypeBasedOn returnComplexVersion: Bool, and returnMetadata: Bool, savedToGallery: Bool? = nil) throws -> IONCAMRMediaResult { + + private func convertToMediaResult( + _ image: UIImage, + with options: IONCAMRTakePhotoOptions? = nil, + separateReturnTypeBasedOn returnComplexVersion: Bool, + and returnMetadata: Bool, + savedToGallery: Bool? = nil + ) throws + -> IONCAMRMediaResult { guard let imageResult = image.toData(with: options) else { throw IONCAMRMultimediaError.treatmentIssue } - + let result: IONCAMRMediaResult if returnComplexVersion { - guard let imageURL = self.urlGenerator.url(for: imageResult, withEncodingType: options?.encodingType), - let imageThumbnail = self.thumbnailGenerator.getBase64String(from: image, with: options?.size, and: options?.quality) + guard let imageURL = urlGenerator.url(for: imageResult, withEncodingType: options?.encodingType), + let imageThumbnail = thumbnailGenerator.getBase64String(from: image, with: options?.size, and: options?.quality) else { throw IONCAMRMultimediaError.mediaResultCreation } - self.temporaryURLArray += [imageURL] + temporaryURLArray += [imageURL] var metadata: IONCAMRMetadata? if returnMetadata { - metadata = try? self.metadataGetter.getImageMetadata(from: image, and: imageURL) + metadata = try? metadataGetter.getImageMetadata(from: image, and: imageURL) } result = IONCAMRMediaResult(pictureWith: imageURL.absoluteString, imageThumbnail, and: metadata, saved: savedToGallery) @@ -223,40 +229,46 @@ private extension IONCAMRFlowBehaviour { return result } - + /// Apply all the user defined transformations to the resulting image. /// - Parameters: /// - image: Image to treat. /// - options: User defined options with the transformations to apply to the image. - func treat(_ image: UIImage, with options: IONCAMRTakePhotoOptions) async throws -> IONCAMRMediaResult { + private func treat(_ image: UIImage, with options: IONCAMRTakePhotoOptions) async throws -> IONCAMRMediaResult { var savedToGallery = false if options.saveToGallery { - savedToGallery = await self.galleryBehaviour.saveToGallery(image) + savedToGallery = await galleryBehaviour.saveToGallery(image) } - return try self.convertToMediaResult(image, with: options, separateReturnTypeBasedOn: true, and: options.returnMetadata, savedToGallery: savedToGallery) + return try convertToMediaResult( + image, + with: options, + separateReturnTypeBasedOn: true, + and: options.returnMetadata, + savedToGallery: savedToGallery + ) } - + /// Return type allows us to return a single `IONCAMRMediaResult` (for `ChoosePictureGallery`) or an array of it (for `ChooseFromGallery`). - func treat(_ image: UIImage, with options: IONCAMRGalleryOptions) throws -> any Encodable { - let result = try self.convertToMediaResult(image, separateReturnTypeBasedOn: options.thumbnailAsData, and: options.returnMetadata) - + private func treat(_ image: UIImage, with options: IONCAMRGalleryOptions) throws -> any Encodable { + let result = try convertToMediaResult(image, separateReturnTypeBasedOn: options.thumbnailAsData, and: options.returnMetadata) + return options.thumbnailAsData ? [result] : result } - - func treat(_ url: URL, with options: IONCAMRRecordVideoOptions, _ completion: @escaping (IONCAMRMediaResult?) -> Void) { + + private func treat(_ url: URL, with options: IONCAMRRecordVideoOptions, _ completion: @escaping (IONCAMRMediaResult?) -> Void) { // Only add to temporaryURLArray if video is not persistent if !options.isPersistent { - self.temporaryURLArray += [url] + temporaryURLArray += [url] } - + if options.saveToGallery { Task { await self.galleryBehaviour.saveToGallery(url) } } - - self.thumbnailGenerator.getImage(from: url) { image in - guard let image = image, let data = image.defaultVideoThumbnailData + + thumbnailGenerator.getImage(from: url) { image in + guard let image, let data = image.defaultVideoThumbnailData else { return completion(nil) } - + if options.returnMetadata { Task { [url] in let metadata = try? await self.metadataGetter.getVideoMetadata(from: url) @@ -272,29 +284,30 @@ private extension IONCAMRFlowBehaviour { } // MARK: Picker Related Extension -private extension IONCAMRFlowBehaviour { - func imagePickerDidReturn(_ image: UIImage) async throws -> IONCAMRMediaResult? { + +extension IONCAMRFlowBehaviour { + private func imagePickerDidReturn(_ image: UIImage) async throws -> IONCAMRMediaResult? { var result: IONCAMRMediaResult? - - guard let pictureOptions = self.options as? IONCAMRTakePhotoOptions else { throw IONCAMRMultimediaError.mediaOptionsConversion } + + guard let pictureOptions = options as? IONCAMRTakePhotoOptions else { throw IONCAMRMultimediaError.mediaOptionsConversion } if !pictureOptions.allowEdit { - guard let treatedImage = try? await self.treat(image, with: pictureOptions) else { throw IONCAMRMultimediaError.treatmentIssue } + guard let treatedImage = try? await treat(image, with: pictureOptions) else { throw IONCAMRMultimediaError.treatmentIssue } result = treatedImage } - + return result } - - func videoPickerDidReturn(_ url: URL, _ completion: @escaping (IONCAMRMediaResult?) -> Void) { - guard let videoOptions = self.options as? IONCAMRRecordVideoOptions else { return completion(nil) } - self.treat(url, with: videoOptions, completion) + + private func videoPickerDidReturn(_ url: URL, _ completion: @escaping (IONCAMRMediaResult?) -> Void) { + guard let videoOptions = options as? IONCAMRRecordVideoOptions else { return completion(nil) } + treat(url, with: videoOptions, completion) } - + /// Method triggered when the user could finish, with or without success, the picker behaviour. /// - Parameter result: Returned object to who implements this object. It returns a base64 encoding text if successful or an error otherwise. - func pickerDidReturn(_ result: Result) { + private func pickerDidReturn(_ result: Result) { func didFailed(withError error: IONCAMRError) { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } defer { self.coordinator.dismiss() } @@ -317,53 +330,54 @@ private extension IONCAMRFlowBehaviour { } } case .video(let url): - self.videoPickerDidReturn(url) { [weak self] mediaResult in + videoPickerDidReturn(url) { [weak self] mediaResult in self?.options = nil - guard let mediaResult = mediaResult else { return didFailed(withError: .captureVideoIssue) } + guard let mediaResult else { return didFailed(withError: .captureVideoIssue) } self?.delegate?.didSucceed(with: mediaResult) } } case .failure(let error): - self.options = nil + options = nil didFailed(withError: error) } } } // MARK: - Editor Related Extension -private extension IONCAMRFlowBehaviour { - func imageEditorDidReturn(_ image: UIImage) async throws -> any Encodable { - if self.coordinator.isSecondStep { - if let pictureOptions = self.options as? IONCAMRTakePhotoOptions { - return try await self.treat(image, with: pictureOptions) + +extension IONCAMRFlowBehaviour { + private func imageEditorDidReturn(_ image: UIImage) async throws -> any Encodable { + if coordinator.isSecondStep { + if let pictureOptions = options as? IONCAMRTakePhotoOptions { + return try await treat(image, with: pictureOptions) } - if let galleryOptions = self.options as? IONCAMRGalleryOptions { - return try self.treat(image, with: galleryOptions) + if let galleryOptions = options as? IONCAMRGalleryOptions { + return try treat(image, with: galleryOptions) } throw IONCAMRMultimediaError.treatmentIssue } - var separator: Bool = false - var returnMetadata: Bool = false + var separator = false + var returnMetadata = false var savedToGallery = false - if let options = self.options as? IONCAMRSaveToGalleryOptionsDelegate { + if let options = options as? IONCAMRSaveToGalleryOptionsDelegate { separator = true returnMetadata = options.returnMetadata if options.saveToGallery { - savedToGallery = await self.galleryBehaviour.saveToGallery(image) + savedToGallery = await galleryBehaviour.saveToGallery(image) } } - return try self.convertToMediaResult(image, separateReturnTypeBasedOn: separator, and: returnMetadata, savedToGallery: savedToGallery) + return try convertToMediaResult(image, separateReturnTypeBasedOn: separator, and: returnMetadata, savedToGallery: savedToGallery) } - + /// Method triggered when the user could finish, with or without success, the editor behaviour. /// - Parameter result: Returned object to who implements this object. It returns a base64 encoding text if successful or an error otherwise. - func editorDidReturn(_ result: Result) { + private func editorDidReturn(_ result: Result) { func didFailed(with error: IONCAMRError = .editPictureIssue) { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } defer { self.coordinator.dismiss() } @@ -383,19 +397,20 @@ private extension IONCAMRFlowBehaviour { } } case .failure(let error): - self.options = nil + options = nil didFailed(with: error) } } } // MARK: - Gallery Related Extension -private extension IONCAMRFlowBehaviour { - func galleryDidReturnSingle(_ result: Result) { + +extension IONCAMRFlowBehaviour { + private func galleryDidReturnSingle(_ result: Result) { func didFailed(with error: IONCAMRError = .choosePictureIssue) { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } - + var canDismiss = true defer { if canDismiss { @@ -403,31 +418,31 @@ private extension IONCAMRFlowBehaviour { self.options = nil } } - + switch result { case .success(let item): if case .picture(let image) = item { - guard let options = self.options as? IONCAMREditMediaTypeOptionsDelegate else { return didFailed() } + guard let options = options as? IONCAMREditMediaTypeOptionsDelegate else { return didFailed() } if !options.allowEdit { guard let result = image.toData()?.base64EncodedString() else { return didFailed() } let mediaResult = IONCAMRMediaResult(pictureWith: result) - self.delegate?.didSucceed(with: mediaResult) + delegate?.didSucceed(with: mediaResult) } else { canDismiss = false - self.editPhoto(image) + editPhoto(image) } } case .failure(let error): didFailed(with: error) } } - - func galleryDidReturnMultiple(_ result: Result<[IONCAMRMediaResult], IONCAMRError>) { + + private func galleryDidReturnMultiple(_ result: Result<[IONCAMRMediaResult], IONCAMRError>) { func didFailed(with error: IONCAMRError = .chooseMultimediaIssue) { - self.delegate?.didFailed(type: [IONCAMRMediaResult].self, with: error) + delegate?.didFailed(type: [IONCAMRMediaResult].self, with: error) } - + var canDismiss = true defer { if canDismiss { @@ -435,23 +450,22 @@ private extension IONCAMRFlowBehaviour { self.options = nil } } - + switch result { case .success(let items): - guard let options = self.options as? IONCAMRGalleryOptions else { return didFailed() } + guard let options = options as? IONCAMRGalleryOptions else { return didFailed() } if options.allowEdit, options.mediaType == .picture, !options.allowMultipleSelection { - guard let mediaResult = items.first, let image = self.imageFetcher.retrieveImage(from: mediaResult.uri)?.fixOrientation() + guard let mediaResult = items.first, let image = imageFetcher.retrieveImage(from: mediaResult.uri)?.fixOrientation() else { return didFailed(with: .fetchImageFromURLFailed) } - + canDismiss = false - self.editPhoto(image) + editPhoto(image) } else { - self.delegate?.didSucceed(with: items) + delegate?.didSucceed(with: items) } case .failure(let error): didFailed(with: error) } - } } diff --git a/Sources/IONCameraLib/IONCAMRGalleryManager.swift b/Sources/IONCameraLib/IONCAMRGalleryManager.swift index 29039c6..ca9e5e6 100644 --- a/Sources/IONCameraLib/IONCAMRGalleryManager.swift +++ b/Sources/IONCameraLib/IONCAMRGalleryManager.swift @@ -3,28 +3,30 @@ import UIKit final class IONCAMRGalleryManager: NSObject { private weak var delegate: IONCAMRCallbackDelegate? private let flow: IONCAMRFlowDelegate - + init(delegate: IONCAMRCallbackDelegate, flow: IONCAMRFlowDelegate) { self.delegate = delegate self.flow = flow super.init() self.flow.delegate = self } - + convenience init(delegate: IONCAMRCallbackDelegate, viewController: UIViewController) { let coordinator = IONCAMRCoordinator(rootViewController: viewController) let flowBehaviour = IONCAMRFlowBehaviour(coordinator: coordinator) - + self.init(delegate: delegate, flow: flowBehaviour) } } extension IONCAMRGalleryManager: IONCAMRGalleryActionDelegate { func chooseFromGallery(with options: IONCAMRGalleryOptions) { - self.flow.chooseFromGallery(with: options) + flow.chooseFromGallery(with: options) } } extension IONCAMRGalleryManager: IONCAMRFlowResultsHandler { - var responseDelegate: IONCAMRCallbackDelegate? { return delegate } + var responseDelegate: IONCAMRCallbackDelegate? { + delegate + } } diff --git a/Sources/IONCameraLib/IONCAMRVideoManager.swift b/Sources/IONCameraLib/IONCAMRVideoManager.swift index 82f17f3..fe9d21c 100644 --- a/Sources/IONCameraLib/IONCAMRVideoManager.swift +++ b/Sources/IONCameraLib/IONCAMRVideoManager.swift @@ -2,22 +2,22 @@ import UIKit final class IONCAMRVideoManager: NSObject { private let videoPlayer: IONCAMRPlayerDelegate - + init(videoPlayer: IONCAMRPlayerDelegate) { self.videoPlayer = videoPlayer super.init() } - + convenience init(delegate: IONCAMRCallbackDelegate, viewController: UIViewController) { let coordinator = IONCAMRCoordinator(rootViewController: viewController) let videoPlayerBehaviour = IONCAMRPlayerBehaviour(coordinator: coordinator) - + self.init(videoPlayer: videoPlayerBehaviour) } } extension IONCAMRVideoManager: IONCAMRVideoActionDelegate { func playVideo(_ url: URL) async throws { - try await self.videoPlayer.playVideo(url) + try await videoPlayer.playVideo(url) } } diff --git a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropInternalView.swift b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropInternalView.swift index e9a5c72..b5e24cf 100644 --- a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropInternalView.swift +++ b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropInternalView.swift @@ -10,14 +10,14 @@ struct IONCAMRCropInternalView: View { var width: CGFloat /// Base height to draw the rectangles with. var height: CGFloat - + /// Size of the stroke line private let lineWidth: CGFloat = 1.0 /// Colour to use as foreground color for the rectangles. private let foregroundColour: Color = .white /// Opacity to apply to the internal rectangles. private let thinOpacity: CGFloat = 0.6 - + var body: some View { Group { // These views create the white grid @@ -27,20 +27,20 @@ struct IONCAMRCropInternalView: View { .frame(width: width, height: height) .foregroundColor(foregroundColour) .offset(offset) - + // This view creates a thin rectangle in the center that is 1/3 the outer square's width Rectangle() .stroke(lineWidth: lineWidth) - .frame(width: width/3, height: height) + .frame(width: width / 3, height: height) .foregroundColor(foregroundColour.opacity(thinOpacity)) - .offset(x: offset.width + width/3, y: offset.height) - + .offset(x: offset.width + width / 3, y: offset.height) + // This view creates a thin rectangle in the center that is 1/3 the outer square's height Rectangle() .stroke(lineWidth: lineWidth) - .frame(width: width, height: height/3) + .frame(width: width, height: height / 3) .foregroundColor(foregroundColour.opacity(thinOpacity)) - .offset(x: offset.width, y: offset.height + height/3) + .offset(x: offset.width, y: offset.height + height / 3) } } } diff --git a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickEdgesView.swift b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickEdgesView.swift index 3ab265e..58f8b1e 100644 --- a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickEdgesView.swift +++ b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickEdgesView.swift @@ -8,7 +8,7 @@ struct IONCAMRCropThickEdgesView: View { var width: CGFloat /// Base height to draw the lines with. var height: CGFloat - + var body: some View { // Top IONCAMRCropThickView( @@ -29,7 +29,7 @@ struct IONCAMRCropThickEdgesView: View { height: height, side: .init(vertical: .top, horizontal: .right) ) - + // Mid IONCAMRCropThickView( offset: offset, @@ -43,7 +43,7 @@ struct IONCAMRCropThickEdgesView: View { height: height, side: .init(vertical: .mid, horizontal: .right) ) - + // Bottom IONCAMRCropThickView( offset: offset, diff --git a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickView.swift b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickView.swift index befb6f6..d9d49fe 100644 --- a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickView.swift +++ b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropThickView.swift @@ -10,7 +10,7 @@ struct IONCAMRCropThickView: View { var height: CGFloat /// Side being drawn. var side: IONCAMRSideModel - + /// Width for the stroke of the line to be drawn. private let stroke: CGFloat = 3.0 /// Biggest width or height of the line to draw. The length to use depends on the side. @@ -19,7 +19,7 @@ struct IONCAMRCropThickView: View { private let smallestLength: CGFloat = 1.0 /// Colour to use as foreground color for the lines. private let foregroundColour: Color = .white - + var body: some View { Group { if side.vertical != .mid { @@ -29,7 +29,7 @@ struct IONCAMRCropThickView: View { .foregroundColor(foregroundColour) .offset(topBottomOffset) } - + if side.horizontal != .mid { Rectangle() .stroke(lineWidth: stroke) @@ -41,47 +41,43 @@ struct IONCAMRCropThickView: View { } } -private extension IONCAMRCropThickView { +extension IONCAMRCropThickView { /// Enumerator indicating which side to consider - enum Side { + fileprivate enum Side { case vertical case horizontal } - + /// Retrieves the offset for the biggest side. /// - Parameters: /// - extra: Value to add on top of the current offset. /// - sideValue: Value representation of the side being considered. /// - Returns: The calculated offset to use. - func getMajorExtraOffset(for extra: CGFloat, and sideValue: Int) -> CGFloat { - return sideValue != IONCAMRSideModel.lowerValue ? extra : 0.0 + private func getMajorExtraOffset(for extra: CGFloat, and sideValue: Int) -> CGFloat { + sideValue != IONCAMRSideModel.lowerValue ? extra : 0.0 } - + /// Retrieves the offset for the smallest side. /// - Parameters: /// - extra: Value to add on top of the current offset. /// - sideValue: Value representation of the side being considered. /// - Returns: The calculated offset to use. - func getMinorExtraOffset(for extra: CGFloat, and sideValue: Int) -> CGFloat { - let result: CGFloat - + private func getMinorExtraOffset(for extra: CGFloat, and sideValue: Int) -> CGFloat { if sideValue != IONCAMRSideModel.lowerValue { - result = extra / (sideValue != IONCAMRSideModel.lowerValue && sideValue != IONCAMRSideModel.upperValue ? 1.0 : 2.0) + extra / (sideValue != IONCAMRSideModel.lowerValue && sideValue != IONCAMRSideModel.upperValue ? 1.0 : 2.0) } else { - result = 0.0 + 0.0 } - - return result } - + /// Retrivies the offset for the line. /// - Parameters: /// - offsetSide: Indicates if its the horizontal or vertical side. /// - offset: The base offset to apply. /// - Returns: The calculated offset to use. - func getOffset(for offsetSide: Side, with offset: CGSize) -> CGSize { + private func getOffset(for offsetSide: Side, with offset: CGSize) -> CGSize { var result = offset - + if offsetSide == .vertical { result.width += getMinorExtraOffset(for: width - biggestLength, and: side.horizontal.rawValue) result.height += getMajorExtraOffset(for: height, and: side.vertical.rawValue) @@ -89,12 +85,17 @@ private extension IONCAMRCropThickView { result.width += getMajorExtraOffset(for: width, and: side.horizontal.rawValue) result.height += getMinorExtraOffset(for: height - biggestLength, and: side.vertical.rawValue) } - + return result } - + /// Vertical offset to apply. - var topBottomOffset: CGSize { getOffset(for: .vertical, with: offset) } + private var topBottomOffset: CGSize { + getOffset(for: .vertical, with: offset) + } + /// Horizontal offset to apply. - var leftRightOffset: CGSize { getOffset(for: .horizontal, with: offset) } + private var leftRightOffset: CGSize { + getOffset(for: .horizontal, with: offset) + } } diff --git a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropView.swift b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropView.swift index f6e789c..6988e9e 100644 --- a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropView.swift +++ b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRCropView.swift @@ -8,48 +8,47 @@ struct IONCAMRCropView: View { var imageHeight: CGFloat /// The dimensions and location of the image to crop. var imageRect: CGRect - + /// The In Progress offset value of the cropped image. @State private var activeOffset: CGSize = .zero /// The final offset value of the cropped image. @Binding var finalOffset: CGSize - + /// The In Progress width magnification value of the cropped image. @State private var activeWidthMagnification: CGFloat = 1.0 /// The final width magnification value of the cropped image. @Binding var finalWidthMagnification: CGFloat - + /// The In Progress height magnification value of the cropped image. @State private var activeHeightMagnification: CGFloat = 1.0 /// The final height magnification value of the cropped image. @Binding var finalHeightMagnification: CGFloat - + /// If a drag operation is in course, it indicates which corner, side or area is being dragged. @State private var currentDragState: DragState = .notDragging - + var body: some View { ZStack(alignment: .topLeading) { Color.clear - + // The black background view(s) Group { surroundingColour .frame(width: activeOffset.width, height: imageHeight) - + surroundingColour .frame(width: activeWidth, height: activeOffset.height) .offset(x: activeOffset.width) - + surroundingColour .frame(width: max(0, imageWidth - activeWidth - activeOffset.width), height: imageHeight) .offset(x: activeWidth + activeOffset.width) - + surroundingColour .frame(width: activeWidth, height: max(0, imageHeight - activeHeight - activeOffset.height)) .offset(x: activeOffset.width, y: activeHeight + activeOffset.height) - } - + Rectangle() .frame(width: activeWidth, height: activeHeight) .foregroundColor(.white.opacity(0.05)) @@ -58,7 +57,7 @@ struct IONCAMRCropView: View { DragGesture(coordinateSpace: .global) .onChanged { drag in let point = drag.location - + if currentDragState == .notDragging { currentDragState = isPanningACorner(point) if currentDragState == .notDragging { @@ -68,7 +67,7 @@ struct IONCAMRCropView: View { } } } - + performDrag(drag.translation) } .onEnded { _ in @@ -78,68 +77,84 @@ struct IONCAMRCropView: View { currentDragState = .notDragging } ) - + IONCAMRCropInternalView(offset: activeOffset, width: activeWidth, height: activeHeight) IONCAMRCropThickEdgesView(offset: activeOffset, width: activeWidth, height: activeHeight) } } } -private extension IONCAMRCropView { +extension IONCAMRCropView { /// Enumerator that indicates which corner, side or area is being dragged. - enum DragState { + fileprivate enum DragState { // Corners case draggingTopLeftCorner case draggingTopRightCorner case draggingBottomLeftCorner case draggingBottomRightCorner - + // Edges case draggingTopEdge case draggingBottomEdge case draggingLeftEdge case draggingRightEdge - + case draggingArea - + case notDragging - + /// Creates the related `IONCAMRSideModel` object. /// /// Returns `nil` if dragging is not in progress. var toSideModel: IONCAMRSideModel? { - switch self { // `mid` is assigned when the side doesn't impact the calculations - case .draggingTopLeftCorner: return .init(vertical: .top, horizontal: .left) - case .draggingTopRightCorner: return .init(vertical: .top, horizontal: .right) - case .draggingBottomLeftCorner: return .init(vertical: .bottom, horizontal: .left) - case .draggingBottomRightCorner: return .init(vertical: .bottom, horizontal: .right) - case .draggingTopEdge: return .init(vertical: .top, horizontal: .mid) - case .draggingBottomEdge: return .init(vertical: .bottom, horizontal: .mid) - case .draggingLeftEdge: return .init(vertical: .mid, horizontal: .left) - case .draggingRightEdge: return .init(vertical: .mid, horizontal: .right) - case .draggingArea: return .init(vertical: .mid, horizontal: .mid) - case .notDragging: return nil + switch self { // `mid` is assigned when the side doesn't impact the calculations + case .draggingTopLeftCorner: .init(vertical: .top, horizontal: .left) + case .draggingTopRightCorner: .init(vertical: .top, horizontal: .right) + case .draggingBottomLeftCorner: .init(vertical: .bottom, horizontal: .left) + case .draggingBottomRightCorner: .init(vertical: .bottom, horizontal: .right) + case .draggingTopEdge: .init(vertical: .top, horizontal: .mid) + case .draggingBottomEdge: .init(vertical: .bottom, horizontal: .mid) + case .draggingLeftEdge: .init(vertical: .mid, horizontal: .left) + case .draggingRightEdge: .init(vertical: .mid, horizontal: .right) + case .draggingArea: .init(vertical: .mid, horizontal: .mid) + case .notDragging: nil } } } - + /// The offset that helps identify which corner or edge is being selected. It helps create an invisible rectangle around the selectable object. - var minimumEdgesOffset: CGFloat { 20.0 } + private var minimumEdgesOffset: CGFloat { + 20.0 + } + /// The minimum height magnification that can be applied - var minimumHeightMagnification: CGFloat { max(70.0 / imageHeight, 0.4) } + private var minimumHeightMagnification: CGFloat { + max(70.0 / imageHeight, 0.4) + } + /// The minimum width magnifciation that can be applied - var minimumWidthMagnification: CGFloat { max(70.0 / imageWidth, 0.4) } - + private var minimumWidthMagnification: CGFloat { + max(70.0 / imageWidth, 0.4) + } + /// The current width of the cropped image. - var activeWidth: CGFloat { imageWidth * activeWidthMagnification } + private var activeWidth: CGFloat { + imageWidth * activeWidthMagnification + } + /// The current height of the cropped image. - var activeHeight: CGFloat { imageHeight * activeHeightMagnification } + private var activeHeight: CGFloat { + imageHeight * activeHeightMagnification + } + /// Colour to apply to the outer background of the crop view. - var surroundingColour: Color { .black.opacity(0.45) } - + private var surroundingColour: Color { + .black.opacity(0.45) + } + /// Adapts the crop view to the selected corner, edge or area, apply the translation passed. /// - Parameter translation: The total translation from the start of the drag gesture to its current event. - func performDrag(_ translation: CGSize) { + private func performDrag(_ translation: CGSize) { if let sideModel = currentDragState.toSideModel { dragging(sideModel, translation) } @@ -147,9 +162,10 @@ private extension IONCAMRCropView { } // MARK: - Corner Verification -private extension IONCAMRCropView { + +extension IONCAMRCropView { /// The Top Left Corner's location and dimensions. - var topLeftCornerRect: CGRect { + private var topLeftCornerRect: CGRect { .init( x: imageRect.minX + activeOffset.width - minimumEdgesOffset, y: imageRect.minY + activeOffset.height - minimumEdgesOffset, @@ -157,8 +173,9 @@ private extension IONCAMRCropView { height: minimumEdgesOffset * 2.0 ) } + /// The Top Right Corner's location and dimensions. - var topRightCornerRect: CGRect { + private var topRightCornerRect: CGRect { .init( x: imageRect.minX + activeOffset.width + activeWidth - minimumEdgesOffset, y: imageRect.minY + activeOffset.height - minimumEdgesOffset, @@ -166,8 +183,9 @@ private extension IONCAMRCropView { height: minimumEdgesOffset * 2.0 ) } + /// The Bottom Left Corner's location and dimensions. - var bottomLeftCornerRect: CGRect { + private var bottomLeftCornerRect: CGRect { .init( x: imageRect.minX + activeOffset.width - minimumEdgesOffset, y: imageRect.minY + activeOffset.height + activeHeight - minimumEdgesOffset, @@ -175,8 +193,9 @@ private extension IONCAMRCropView { height: minimumEdgesOffset * 2.0 ) } + /// The Bottom Right Corner's location and dimensions. - var bottomRightCornerRect: CGRect { + private var bottomRightCornerRect: CGRect { .init( x: imageRect.minX + activeOffset.width + activeWidth - minimumEdgesOffset, y: imageRect.minY + activeOffset.height + activeHeight - minimumEdgesOffset, @@ -184,15 +203,15 @@ private extension IONCAMRCropView { height: minimumEdgesOffset * 2.0 ) } - + /// Verifies if the user is currently dragging one of the crop view's corners. /// /// If the passed point doesn't belong to any of the corners, it returns a `notDragging` state. /// - Parameter point: The location of the drag gesture’s current event. /// - Returns: Returns the corner the location belongs to, or `notDragging` otherwise. - func isPanningACorner(_ point: CGPoint) -> DragState { + private func isPanningACorner(_ point: CGPoint) -> DragState { var result = DragState.notDragging - + if topLeftCornerRect.contains(point) { result = .draggingTopLeftCorner } else if topRightCornerRect.contains(point) { @@ -202,15 +221,16 @@ private extension IONCAMRCropView { } else if bottomRightCornerRect.contains(point) { result = .draggingBottomRightCorner } - + return result } } // MARK: - Edge Verification Logic -private extension IONCAMRCropView { + +extension IONCAMRCropView { /// The Top Edge Corner's location and dimensions. - var topEdgeRect: CGRect { + private var topEdgeRect: CGRect { .init( x: imageRect.minX + activeOffset.width, y: imageRect.minY + activeOffset.height - minimumEdgesOffset, @@ -218,8 +238,9 @@ private extension IONCAMRCropView { height: minimumEdgesOffset * 2 ) } + /// The Bottom Edge Corner's location and dimensions. - var bottomEdgeRect: CGRect { + private var bottomEdgeRect: CGRect { .init( x: imageRect.minX + activeOffset.width, y: imageRect.minY + activeOffset.height + activeHeight - minimumEdgesOffset, @@ -227,8 +248,9 @@ private extension IONCAMRCropView { height: minimumEdgesOffset * 2 ) } + /// The Left Edge Corner's location and dimensions. - var leftEdgeRect: CGRect { + private var leftEdgeRect: CGRect { .init( x: imageRect.minX + activeOffset.width - minimumEdgesOffset, y: imageRect.minY + activeOffset.height, @@ -236,8 +258,9 @@ private extension IONCAMRCropView { height: activeHeight ) } + /// The Right Edge Corner's location and dimensions. - var rightEdgeRect: CGRect { + private var rightEdgeRect: CGRect { .init( x: imageRect.minX + activeOffset.width + activeWidth - minimumEdgesOffset, y: imageRect.minY + activeOffset.height, @@ -245,15 +268,15 @@ private extension IONCAMRCropView { height: activeHeight ) } - + /// Verifies if the user is currently dragging one of the crop view's edges. /// /// If the passed point doesn't belong to any of the edges, it returns a `notDragging` state. /// - Parameter point: The location of the drag gesture’s current event. /// - Returns: Returns the edge the location belongs to, or `notDragging` otherwise. - func isPanningASide(_ point: CGPoint) -> DragState { + private func isPanningASide(_ point: CGPoint) -> DragState { var result: DragState = .notDragging - + if topEdgeRect.contains(point) { result = .draggingTopEdge } else if bottomEdgeRect.contains(point) { @@ -263,15 +286,16 @@ private extension IONCAMRCropView { } else if rightEdgeRect.contains(point) { result = .draggingRightEdge } - + return result } } // MARK: - Area Verification Logic -private extension IONCAMRCropView { + +extension IONCAMRCropView { /// The Area's location and dimensions. - var areaRect: CGRect { + private var areaRect: CGRect { .init( x: imageRect.minX + activeOffset.width, y: imageRect.minY + activeOffset.height, @@ -279,19 +303,20 @@ private extension IONCAMRCropView { height: activeHeight ) } - + /// Verifies if the user is currently dragging the crop view's area. /// /// If the passed point doesn't belong to the area, it returns a `notDragging` state. /// - Parameter point: The location of the drag gesture’s current event. /// - Returns: Returns the area if the point belongs to it, or `notDragging` otherwise. - func isPanningArea(_ point: CGPoint) -> DragState { + private func isPanningArea(_ point: CGPoint) -> DragState { areaRect.contains(point) ? .draggingArea : .notDragging } } // MARK: - Reusable Methods -private extension IONCAMRCropView { + +extension IONCAMRCropView { /// Verifies if the passed `currentValue` is within the optional range values. /// /// It can return one of the following values: @@ -304,19 +329,21 @@ private extension IONCAMRCropView { /// - minimumValue: The lower range value. It can be `nil` in order to ignore its comparison with `currentValue`. /// - maximumValue: The higher range value. It can be `nil` in order to ignore its comparison with `currentValue`. /// - Returns: Returns the value to use by the caller. - func setValueInRange(current currentValue: CGFloat, minimum minimumValue: CGFloat? = nil, andMaximum maximumValue: CGFloat? = nil) -> CGFloat { - let result: CGFloat - if let minimumValue = minimumValue, currentValue < minimumValue { - result = minimumValue - } else if let maximumValue = maximumValue, currentValue > maximumValue { - result = maximumValue + private func setValueInRange( + current currentValue: CGFloat, + minimum minimumValue: CGFloat? = nil, + andMaximum maximumValue: CGFloat? = nil + ) + -> CGFloat { + if let minimumValue, currentValue < minimumValue { + minimumValue + } else if let maximumValue, currentValue > maximumValue { + maximumValue } else { - result = currentValue + currentValue } - - return result } - + /// Calculates the offsets to be used to calculate the required transformation. /// - Parameters: /// - sideValue: Integer value associated to the which vertical or horizontal side is being dragged. @@ -324,10 +351,16 @@ private extension IONCAMRCropView { /// - transformationOffset: The offset to apply based on the transformation in progress. /// - remainingSize: The remaining width or height that can still be applied based on the current offset. /// - Returns: A tuple containing the current left/top and right/bottom offsets. - func calculateOffsets(sideValue: Int, _ activeOffset: CGFloat, transformationOffset: CGFloat, remainingSize: CGFloat) -> (workingOffset: CGFloat?, belowOffset: CGFloat?) { + private func calculateOffsets( + sideValue: Int, + _ activeOffset: CGFloat, + transformationOffset: CGFloat, + remainingSize: CGFloat + ) + -> (workingOffset: CGFloat?, belowOffset: CGFloat?) { var workingOffset: CGFloat? var belowOffset: CGFloat? - + if sideValue != IONCAMRSideModel.upperValue { var minimumValue: CGFloat? if sideValue == IONCAMRSideModel.lowerValue { @@ -336,22 +369,23 @@ private extension IONCAMRCropView { } workingOffset = setValueInRange(current: transformationOffset, minimum: minimumValue) } - + return (workingOffset, belowOffset) } - + /// Calculates the new magnification value to apply to the crop view. /// - Parameters: /// - sideValue: Integer value associated to the which vertical or horizontal side is being dragged. /// - workingOffset: The left/top offset being used during the current operation. - /// - belowOffset: The right/bottom offset being used during the current operation. It's basically the offset between the end of the crop view and of the original image. + /// - belowOffset: The right/bottom offset being used during the current operation. It's basically the offset between the end of the crop view + /// and of the original image. /// - currentSize: The original image's size. /// - translation: The total translation from the start of the drag gesture to its current event. /// - currentMagnification: The current magnification value. /// - currentOffset: The current offset value. /// - minimumMagnification: The minimum magnification value it can assume. /// - Returns: The value of the calculated magnification. - func calculateNewMagnification( // swiftlint:disable:this function_parameter_count + private func calculateNewMagnification( // swiftlint:disable:this function_parameter_count sideValue: Int, _ workingOffset: CGFloat?, _ belowOffset: CGFloat?, @@ -360,48 +394,50 @@ private extension IONCAMRCropView { currentMagnification: CGFloat, currentOffset: CGFloat, _ minimumMagnification: CGFloat - ) -> CGFloat? { + ) + -> CGFloat? { guard sideValue == IONCAMRSideModel.lowerValue || sideValue == IONCAMRSideModel.upperValue else { return nil } - + let workingMagnification: CGFloat let maximumMagnification: CGFloat - if let workingOffset = workingOffset, let belowOffset = belowOffset { + if let workingOffset, let belowOffset { workingMagnification = (currentSize - workingOffset - belowOffset) / currentSize maximumMagnification = 1.0 } else { workingMagnification = translation / currentSize + currentMagnification maximumMagnification = (currentSize - currentOffset) / currentSize } - + return setValueInRange(current: workingMagnification, minimum: minimumMagnification, andMaximum: maximumMagnification) } - + /// Calculates the new offset to apply based on the drag gesture and corner/edge/area selected. /// - Parameters: /// - sideValue: Integer value associated to the which vertical or horizontal side is being dragged. /// - workingOffset: The left/top offset being used during the current operation. - /// - belowOffset: The right/bottom offset being used during the current operation. It's basically the offset between the end of the crop view and of the original image. + /// - belowOffset: The right/bottom offset being used during the current operation. It's basically the offset between the end of the crop view + /// and of the original image. /// - remainingSize: The remaining width or height that can still be applied based on the current offset. /// - Returns: The new offset value to apply to the crop view. - func calculateNewOffset(sideValue: Int, _ workingOffset: CGFloat?, _ belowOffset: CGFloat?, remainingSize: CGFloat) -> CGFloat? { - guard sideValue != IONCAMRSideModel.upperValue, let workingOffset = workingOffset else { return nil } - + private func calculateNewOffset(sideValue: Int, _ workingOffset: CGFloat?, _ belowOffset: CGFloat?, remainingSize: CGFloat) -> CGFloat? { + guard sideValue != IONCAMRSideModel.upperValue, let workingOffset else { return nil } + var minimumValue: CGFloat? var maximumValue = remainingSize - if let belowOffset = belowOffset { + if let belowOffset { maximumValue -= belowOffset } else { minimumValue = 0.0 } - + return setValueInRange(current: workingOffset, minimum: minimumValue, andMaximum: maximumValue) } - + /// Updates the offset and magnification values on the crop view to incorporate the drag gesture performed by the user. /// - Parameters: /// - side: The corner/edge/area picker by the user's gesture. /// - translation: The total translation from the start of the drag gesture to its current event. - func dragging(_ side: IONCAMRSideModel, _ translation: CGSize) { + private func dragging(_ side: IONCAMRSideModel, _ translation: CGSize) { let xValues = calculateOffsets( sideValue: side.horizontal.rawValue, activeOffset.width, @@ -414,7 +450,17 @@ private extension IONCAMRCropView { transformationOffset: finalOffset.height + translation.height, remainingSize: imageHeight - activeHeight ) - + + updateActiveMagnification(side: side, xValues: xValues, yValues: yValues, translation: translation) + updateActiveOffset(side: side, xValues: xValues, yValues: yValues) + } + + private func updateActiveMagnification( + side: IONCAMRSideModel, + xValues: (workingOffset: CGFloat?, belowOffset: CGFloat?), + yValues: (workingOffset: CGFloat?, belowOffset: CGFloat?), + translation: CGSize + ) { if let newMagnification = calculateNewMagnification( sideValue: side.horizontal.rawValue, xValues.workingOffset, @@ -439,7 +485,13 @@ private extension IONCAMRCropView { ) { activeHeightMagnification = newMagnification } - + } + + private func updateActiveOffset( + side: IONCAMRSideModel, + xValues: (workingOffset: CGFloat?, belowOffset: CGFloat?), + yValues: (workingOffset: CGFloat?, belowOffset: CGFloat?) + ) { if let newOffset = calculateNewOffset( sideValue: side.horizontal.rawValue, xValues.workingOffset, xValues.belowOffset, remainingSize: imageWidth - activeWidth ) { diff --git a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRImageEditorView.swift b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRImageEditorView.swift index faec284..d644da1 100644 --- a/Sources/IONCameraLib/Interfaces/Editor/IONCAMRImageEditorView.swift +++ b/Sources/IONCameraLib/Interfaces/Editor/IONCAMRImageEditorView.swift @@ -8,24 +8,25 @@ struct IONCAMRImageEditorView: View { @State var image: UIImage /// Indicates if the device is in Portrait or Landspace mode. @State var isPortrait: Bool - + /// The original image's width dimension @State private var imageWidth: CGFloat = 0.0 /// The original image's height dimension @State private var imageHeight: CGFloat = 0.0 /// The original image's location and dimensions. @State private var imageRect: CGRect = .zero - + /// The offset to perform the crop on the image. @State private var croppingOffset: CGSize = .zero /// The width magnification value to perform the crop on the image. @State private var croppingWidthMagnification: CGFloat = 1.0 /// The height magnification value to perform the crop on the image. @State private var croppingHeightMagnification: CGFloat = 1.0 - - /// `IONCAMRCropView`'s ID. Its goal is to indicate that changes need to be propagated some changes are performed by the user (p.e., device or image rotation, mirroring, ...). + + /// `IONCAMRCropView`'s ID. Its goal is to indicate that changes need to be propagated some changes are performed by the user (p.e., device or + /// image rotation, mirroring, ...). @State private var viewID = 0 - + var body: some View { ZStack { Color.black.edgesIgnoringSafeArea(.all) @@ -34,7 +35,7 @@ struct IONCAMRImageEditorView: View { if !isPortrait { bar } - + DynamicStack(showHorizontalStack: !isPortrait) { Spacer() Image(uiImage: image) @@ -46,7 +47,7 @@ struct IONCAMRImageEditorView: View { imageHeight = geo.size.height imageRect = geo.frame(in: .global) } - + return AnyView(IONCAMRCropView( imageWidth: imageWidth, imageHeight: imageHeight, @@ -55,8 +56,8 @@ struct IONCAMRImageEditorView: View { finalWidthMagnification: $croppingWidthMagnification, finalHeightMagnification: $croppingHeightMagnification ).id(viewID)) - }) - + }) + Spacer() DynamicStack(showHorizontalStack: isPortrait, spacing: 50.0) { Button { @@ -66,7 +67,7 @@ struct IONCAMRImageEditorView: View { } label: { IONCAMRAssets.bundleImage(IONCAMRAssets.ImageEditor.flip) } - + Button { image = image.rotate(radians: CGFloat.pi / -2.0) croppingOffset = .zero @@ -78,7 +79,7 @@ struct IONCAMRImageEditorView: View { } } } - + // If device is in Portrait, the bar is shown on the bottom. if isPortrait { bar @@ -95,20 +96,20 @@ struct IONCAMRImageEditorView: View { viewID += 1 } } - + var bar: some View { HStack { Button { - self.delegate?.didCancelEdit() + delegate?.didCancelEdit() } label: { Text("Cancel") } - + Spacer() - + Button { guard let cgImage = image.cgImage else { - delegate?.finishEditing(with: .editPictureIssue) // it wasn't possible to retrieve the image, so an error is returned. + delegate?.finishEditing(with: .editPictureIssue) // it wasn't possible to retrieve the image, so an error is returned. return } let scaler = CGSize(width: CGFloat(cgImage.width) / imageWidth, height: CGFloat(cgImage.height) / imageHeight) @@ -119,12 +120,12 @@ struct IONCAMRImageEditorView: View { width: dim.width * croppingWidthMagnification, height: dim.height * croppingHeightMagnification ) - + if let cImage = cgImage.cropping(to: cropRect) { let croppedImage = UIImage(cgImage: cImage) - delegate?.finishEditing(with: croppedImage) // the resulting cropped image is returned. + delegate?.finishEditing(with: croppedImage) // the resulting cropped image is returned. } else { - delegate?.finishEditing(with: .editPictureIssue) // it wasn't possible to retrieve the image, so an error is returned. + delegate?.finishEditing(with: .editPictureIssue) // it wasn't possible to retrieve the image, so an error is returned. } } label: { Text("Done") @@ -136,11 +137,11 @@ struct IONCAMRImageEditorView: View { } } -private extension UIImage { +extension UIImage { /// Rotates the image with the passed radians. /// - Parameter radians: The radians value to rotate the image. /// - Returns: The rotated image. - func rotate(radians: CGFloat) -> UIImage { + fileprivate func rotate(radians: CGFloat) -> UIImage { let rotatedSize = CGRect(origin: .zero, size: size) .applying(CGAffineTransform(rotationAngle: CGFloat(radians))) .integral.size @@ -150,26 +151,29 @@ private extension UIImage { let origin = CGPoint(x: rotatedSize.width / 2.0, y: rotatedSize.height / 2.0) context.translateBy(x: origin.x, y: origin.y) context.rotate(by: radians) - self.draw(in: CGRect(x: -origin.y, y: -origin.x, width: size.width, height: size.height)) + draw(in: CGRect(x: -origin.y, y: -origin.x, width: size.width, height: size.height)) let rotatedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return rotatedImage ?? self } - + /// Flips the image horizontally. /// - Returns: The flipped image. - func flippedHorizontally() -> UIImage { - guard let cgImage = cgImage else { return self } - + fileprivate func flippedHorizontally() -> UIImage { + guard let cgImage else { return self } + UIGraphicsBeginImageContextWithOptions(size, false, scale) - let context = UIGraphicsGetCurrentContext()! + guard let context = UIGraphicsGetCurrentContext() else { + UIGraphicsEndImageContext() + return self + } context.translateBy(x: size.width, y: size.height) context.scaleBy(x: -scale, y: -scale) context.draw(cgImage, in: CGRect(origin: .zero, size: size)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - + return newImage ?? self } } @@ -182,7 +186,7 @@ private struct DynamicStack: View { var spacing: CGFloat? /// The content to display on the `HStack` or `VStack`. @ViewBuilder var content: () -> Content - + var body: some View { if showHorizontalStack { HStack(spacing: spacing, content: content) diff --git a/Sources/IONCameraLib/Interfaces/Editor/Model/IONCAMRSideModel.swift b/Sources/IONCameraLib/Interfaces/Editor/Model/IONCAMRSideModel.swift index b80434b..2f2b00c 100644 --- a/Sources/IONCameraLib/Interfaces/Editor/Model/IONCAMRSideModel.swift +++ b/Sources/IONCameraLib/Interfaces/Editor/Model/IONCAMRSideModel.swift @@ -1,24 +1,26 @@ /// Structure that manages the horizontal and vertical sides of the crop/drag gesture. struct IONCAMRSideModel { /// The value that represents the lowest value for a side (`left` for horizontal and `top` for vertical). - static var lowerValue: Int = 0 + static var lowerValue = 0 /// The value that represents the highest value for a side ( `right` for horizontal and `bottom` for vertical). - static var upperValue: Int { lowerValue + HorizontalSide.allCases.count - 1 } - + static var upperValue: Int { + lowerValue + HorizontalSide.allCases.count - 1 + } + /// Enumerator representing all the values for an horizontal side. enum HorizontalSide: Int, CaseIterable { case left = 0 case mid case right } - + /// Enumerator representing all the values for a vertical side. enum VerticalSide: Int, CaseIterable { case top = 0 case mid case bottom } - + /// The value representing the vertical side of the structure. let vertical: VerticalSide /// The value represeinting the horizontal side of the structure. diff --git a/Sources/IONCameraLib/Interfaces/Extensions/CGSize+Transformations.swift b/Sources/IONCameraLib/Interfaces/Extensions/CGSize+Transformations.swift index 9986592..f180ad8 100644 --- a/Sources/IONCameraLib/Interfaces/Extensions/CGSize+Transformations.swift +++ b/Sources/IONCameraLib/Interfaces/Extensions/CGSize+Transformations.swift @@ -7,7 +7,7 @@ extension CGSize { init(size: IONCAMRSize) { self.init(width: size.width, height: size.height) } - + init(resolution: Int) throws { let size = try IONCAMRSize(width: resolution, height: resolution) self.init(size: size) @@ -16,10 +16,10 @@ extension CGSize { extension CGSize { var resolution: String { - if self.height < self.width { - return "\(Int(self.width))x\(Int(self.height))" + if height < width { + "\(Int(width))x\(Int(height))" } else { - return "\(Int(self.height))x\(Int(self.width))" + "\(Int(height))x\(Int(width))" } } } diff --git a/Sources/IONCameraLib/Interfaces/Extensions/Data+Transformations.swift b/Sources/IONCameraLib/Interfaces/Extensions/Data+Transformations.swift index c082cdc..b1d6209 100644 --- a/Sources/IONCameraLib/Interfaces/Extensions/Data+Transformations.swift +++ b/Sources/IONCameraLib/Interfaces/Extensions/Data+Transformations.swift @@ -2,32 +2,32 @@ import Foundation extension Data { func createImageTemporaryPath(with pathExtension: String? = nil) throws -> URL { - let imageURL = URL.tempFilePath(for: .picture, with: pathExtension ?? self.fileExtension) - try self.write(to: imageURL) + let imageURL = URL.tempFilePath(for: .picture, with: pathExtension ?? fileExtension) + try write(to: imageURL) return imageURL } } -private extension Data { - static let jpegMimeTypeSignature: String = "image/jpeg" - static let pngMimeTypeSignature: String = "image/png" - - static let mimeTypeSignatures: [UInt8: String] = [ +extension Data { + fileprivate static let jpegMimeTypeSignature = "image/jpeg" + fileprivate static let pngMimeTypeSignature = "image/png" + + fileprivate static let mimeTypeSignatures: [UInt8: String] = [ 0xFF: Self.jpegMimeTypeSignature, 0x89: Self.pngMimeTypeSignature ] - - var mimeType: String { + + private var mimeType: String { var bytes: UInt8 = 0 copyBytes(to: &bytes, count: 1) return Self.mimeTypeSignatures[bytes] ?? "application/octet-stream" } - - var fileExtension: String { - switch self.mimeType { - case Self.jpegMimeTypeSignature: return IONCAMREncodingType.jpeg.description - case Self.pngMimeTypeSignature: return IONCAMREncodingType.png.description - default: return "unknown" + + private var fileExtension: String { + switch mimeType { + case Self.jpegMimeTypeSignature: IONCAMREncodingType.jpeg.description + case Self.pngMimeTypeSignature: IONCAMREncodingType.png.description + default: "unknown" } } } diff --git a/Sources/IONCameraLib/Interfaces/Extensions/UIImage+Transformations.swift b/Sources/IONCameraLib/Interfaces/Extensions/UIImage+Transformations.swift index 536d0a2..d6516dc 100644 --- a/Sources/IONCameraLib/Interfaces/Extensions/UIImage+Transformations.swift +++ b/Sources/IONCameraLib/Interfaces/Extensions/UIImage+Transformations.swift @@ -5,18 +5,18 @@ extension UIImage { /// Applies a set of transformations required to correct the image's orientation. It it's already set to `up`, the image is immediately returned. /// - Returns: The resulting image, after applying all transformations. `Nil` is returned if some issue occured. func fixOrientation() -> UIImage? { - guard self.imageOrientation != .up else { - return self.copy() as? UIImage // This is default orientation, nothing to do here + guard imageOrientation != .up else { + return copy() as? UIImage // This is default orientation, nothing to do here } - - guard let cgImage = self.cgImage else { - return nil // CGImage is not available + + guard let cgImage else { + return nil // CGImage is not available } - + guard let colorSpace = cgImage.colorSpace, let ctx = CGContext( data: nil, - width: Int(self.size.width), - height: Int(self.size.height), + width: Int(size.width), + height: Int(size.height), bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: colorSpace, @@ -24,79 +24,74 @@ extension UIImage { ) else { return nil // Not able to create CGContext } - - var transform: CGAffineTransform = .identity - + + ctx.concatenate(orientationTransform()) + + switch imageOrientation { + case .left, .leftMirrored, .right, .rightMirrored: + ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) + default: + ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + } + + guard let newCGImage = ctx.makeImage() else { return nil } + return UIImage(cgImage: newCGImage, scale: 1, orientation: .up) + } + + private func orientationTransform() -> CGAffineTransform { + applyMirror(to: applyRotation(to: .identity)) + } + + private func applyRotation(to transform: CGAffineTransform) -> CGAffineTransform { switch imageOrientation { case .down, .downMirrored: - transform = transform.translatedBy(x: self.size.width, y: self.size.height) - transform = transform.rotated(by: CGFloat.pi) + transform.translatedBy(x: size.width, y: size.height).rotated(by: .pi) case .left, .leftMirrored: - transform = transform.translatedBy(x: self.size.width, y: 0) - transform = transform.rotated(by: CGFloat.pi / 2.0) + transform.translatedBy(x: size.width, y: 0).rotated(by: .pi / 2) case .right, .rightMirrored: - transform = transform.translatedBy(x: 0, y: self.size.height) - transform = transform.rotated(by: CGFloat.pi / -2.0) - case .up, .upMirrored: - break - @unknown default: - break + transform.translatedBy(x: 0, y: size.height).rotated(by: .pi / -2) + default: + transform } - - // Flip image one more time if needed to, this is to prevent flipped image + } + + private func applyMirror(to transform: CGAffineTransform) -> CGAffineTransform { switch imageOrientation { case .upMirrored, .downMirrored: - transform = transform.translatedBy(x: self.size.width, y: 0) - transform = transform.scaledBy(x: -1, y: 1) + transform.translatedBy(x: size.width, y: 0).scaledBy(x: -1, y: 1) case .leftMirrored, .rightMirrored: - transform = transform.translatedBy(x: self.size.height, y: 0) - transform = transform.scaledBy(x: -1, y: 1) - case .up, .down, .left, .right: - break - @unknown default: - break - } - - ctx.concatenate(transform) - - switch imageOrientation { - case .left, .leftMirrored, .right, .rightMirrored: - ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: self.size.height, height: self.size.width)) + transform.translatedBy(x: size.height, y: 0).scaledBy(x: -1, y: 1) default: - ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)) + transform } - - guard let newCGImage = ctx.makeImage() else { return nil } - return UIImage(cgImage: newCGImage, scale: 1, orientation: .up) } - + /// Resizes the image to the `targetSize` passed by argument. /// - Parameter targetSize: The height and width to resize the image to /// - Returns: The resulting resized image. `Nil` is returned if some issue occured. func resizeTo(_ targetSize: CGSize) -> UIImage? { let sourceImage = self - let widthRatio = targetSize.width / sourceImage.size.width + let widthRatio = targetSize.width / sourceImage.size.width let heightRatio = targetSize.height / sourceImage.size.height - + guard widthRatio != 1.0, heightRatio != 1.0 else { return self } - + // Figure out what our orientation is, and use that to form the rectangle - var newSize: CGSize - if widthRatio > heightRatio { - newSize = CGSize(width: sourceImage.size.width * heightRatio, height: sourceImage.size.height * heightRatio) + let newSize = if widthRatio > heightRatio { + CGSize(width: sourceImage.size.width * heightRatio, height: sourceImage.size.height * heightRatio) } else { - newSize = CGSize(width: sourceImage.size.width * widthRatio, height: sourceImage.size.height * widthRatio) + CGSize(width: sourceImage.size.width * widthRatio, height: sourceImage.size.height * widthRatio) } - + // This is the rect that we've calculated out and this is what is actually used below let rect = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height) - + // Actually do the resizing to the rect using ImageContext UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) sourceImage.draw(in: rect) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - + return newImage } } diff --git a/Sources/IONCameraLib/Interfaces/Extensions/URL+Transformations.swift b/Sources/IONCameraLib/Interfaces/Extensions/URL+Transformations.swift index 6fe4945..c033eaf 100644 --- a/Sources/IONCameraLib/Interfaces/Extensions/URL+Transformations.swift +++ b/Sources/IONCameraLib/Interfaces/Extensions/URL+Transformations.swift @@ -4,15 +4,14 @@ let videosDirectoryName = "ion_ios_camera_videos" extension URL { func createVideoTemporaryPath(_ deleteTemporaryItem: Bool = true) throws -> URL { - let copyMovieURL = URL.tempFilePath(for: .video, with: self.pathExtension) + let copyMovieURL = URL.tempFilePath(for: .video, with: pathExtension) try FileManager.default.copyItem(at: self, to: copyMovieURL) if deleteTemporaryItem { - try? self.deleteTemporaryPath(alongWithThumbnail: true) + try? deleteTemporaryPath(alongWithThumbnail: true) } return copyMovieURL } - func createVideoPermanentPath(_ deleteTemporaryItem: Bool = true) throws -> URL { let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let videosDir = documentsURL.appendingPathComponent(videosDirectoryName, isDirectory: true) @@ -25,20 +24,20 @@ extension URL { let timestamp = NSInteger(Date().timeIntervalSince1970) let permanentURL = videosDir .appendingPathComponent("video_\(timestamp)") - .appendingPathExtension(self.pathExtension) + .appendingPathExtension(pathExtension) try FileManager.default.copyItem(at: self, to: permanentURL) if deleteTemporaryItem { - try? self.deleteTemporaryPath(alongWithThumbnail: true) + try? deleteTemporaryPath(alongWithThumbnail: true) } return permanentURL } - + func deleteTemporaryPath(alongWithThumbnail: Bool = false) throws { try FileManager.default.removeItem(at: self) if alongWithThumbnail { // recorded videos also generate a 'largeThumbnail' file that also needs to be removed - var thumbnailComponents = self.absoluteString.split(separator: ".") + var thumbnailComponents = absoluteString.split(separator: ".") thumbnailComponents.removeLast() thumbnailComponents.append("largeThumbnail") if let thumbnailURL = URL(string: thumbnailComponents.joined(separator: ".")) { @@ -46,24 +45,21 @@ extension URL { } } } - + static func tempFilePath(for mediaType: IONCAMRMediaType, with pathExtension: String) -> URL { let timestamp = NSInteger(Date().timeIntervalSince1970) - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + return URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .standardizedFileURL .appendingPathComponent("\(mediaType.description)_\(timestamp)") .appendingPathExtension(pathExtension) - return temporaryDirectoryURL - } } extension URL { var metadata: (date: Date, fileSize: UInt64)? { - guard let resources = try? self.resourceValues(forKeys: [.creationDateKey, .fileSizeKey]), + guard let resources = try? resourceValues(forKeys: [.creationDateKey, .fileSizeKey]), let date = resources.creationDate, let fileSize = resources.fileSize else { return nil } return (date, UInt64(fileSize)) } - } diff --git a/Sources/IONCameraLib/Interfaces/IONCAMREditorBehaviour.swift b/Sources/IONCameraLib/Interfaces/IONCAMREditorBehaviour.swift index fe9e966..184678b 100644 --- a/Sources/IONCameraLib/Interfaces/IONCAMREditorBehaviour.swift +++ b/Sources/IONCameraLib/Interfaces/IONCAMREditorBehaviour.swift @@ -3,7 +3,7 @@ import UIKit final class IONCAMREditorBehaviour: NSObject, IONCAMREditorDelegate { weak var delegate: IONCAMREditorResultsDelegate? - + func editPicture(_ image: UIImage, _ handler: @escaping (UIViewController) -> Void) { DispatchQueue.main.async { let isPortrait = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isPortrait ?? false @@ -11,7 +11,7 @@ final class IONCAMREditorBehaviour: NSObject, IONCAMREditorDelegate { let viewController = UIHostingController(rootView: imageEditorView) viewController.modalPresentationStyle = .fullScreen handler(viewController) - } + } } } @@ -22,15 +22,15 @@ extension IONCAMREditorBehaviour: IONCAMRImageEditorResultsDelegate { /// - error: Error occurred during the edit. func finishEditing(_ result: UIImage?, error: IONCAMRError?) { if let image = result { - self.delegate?.didReturn(self, with: .success(.picture(image))) + delegate?.didReturn(self, with: .success(.picture(image))) } else { - self.delegate?.didReturn(self, with: .failure(.editPictureIssue)) + delegate?.didReturn(self, with: .failure(.editPictureIssue)) } } - + /// Method triggered when the screen's interaction ended with the user cancelling it. func didCancelEdit() { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } @@ -41,10 +41,10 @@ protocol IONCAMRImageEditorResultsDelegate: AnyObject { extension IONCAMRImageEditorResultsDelegate { func finishEditing(with result: UIImage) { - self.finishEditing(result, error: nil) + finishEditing(result, error: nil) } - + func finishEditing(with error: IONCAMRError) { - self.finishEditing(nil, error: error) + finishEditing(nil, error: error) } } diff --git a/Sources/IONCameraLib/Interfaces/IONCAMRGalleryBehaviour.swift b/Sources/IONCameraLib/Interfaces/IONCAMRGalleryBehaviour.swift index dad4b29..ee6d3f5 100644 --- a/Sources/IONCameraLib/Interfaces/IONCAMRGalleryBehaviour.swift +++ b/Sources/IONCameraLib/Interfaces/IONCAMRGalleryBehaviour.swift @@ -4,16 +4,16 @@ import UIKit final class IONCAMRGalleryBehaviour: NSObject, IONCAMRGalleryDelegate { weak var delegate: IONCAMRGalleryResultsDelegate? - - var thumbnailAsData: Bool = false - var returnMetadata: Bool = false - + + var thumbnailAsData = false + var returnMetadata = false + var metadataGetter: IONCAMRMetadataGetterDelegate - + init(metadataGetter: IONCAMRMetadataGetterDelegate) { self.metadataGetter = metadataGetter } - + func saveToGallery(_ image: UIImage) async -> Bool { await withCheckedContinuation { continuation in PHPhotoLibrary.shared().performChanges { @@ -23,7 +23,7 @@ final class IONCAMRGalleryBehaviour: NSObject, IONCAMRGalleryDelegate { } } } - + func saveToGallery(_ fileURL: URL) async -> Bool { await withCheckedContinuation { continuation in PHPhotoLibrary.shared().performChanges { @@ -33,15 +33,15 @@ final class IONCAMRGalleryBehaviour: NSObject, IONCAMRGalleryDelegate { } } } - + func chooseFromGallery(with options: IONCAMRGalleryOptions, _ handler: @escaping (UIViewController) -> Void) { - self.thumbnailAsData = options.thumbnailAsData - self.returnMetadata = options.returnMetadata + thumbnailAsData = options.thumbnailAsData + returnMetadata = options.returnMetadata DispatchQueue.main.async { let viewController = self.displayPhotoLibraryView( - mediaTypes: options.mediaType.phAssetArray, - allowMultipleSelection: options.allowMultipleSelection, - limit: options.limit, + mediaTypes: options.mediaType.phAssetArray, + allowMultipleSelection: options.allowMultipleSelection, + limit: options.limit, thumbnailAsData: options.thumbnailAsData ) @@ -51,18 +51,25 @@ final class IONCAMRGalleryBehaviour: NSObject, IONCAMRGalleryDelegate { } extension IONCAMRGalleryBehaviour { - func displayPhotoLibraryView(mediaTypes: [PHAssetMediaType], allowMultipleSelection: Bool, limit: Int = 0, thumbnailAsData: Bool) -> UIViewController { + func displayPhotoLibraryView( + mediaTypes: [PHAssetMediaType], + allowMultipleSelection: Bool, + limit: Int = 0, + thumbnailAsData: Bool + ) + -> UIViewController { let photoLibraryService = IONCAMRPhotoLibraryService( delegate: self, - metadataGetter: self.metadataGetter, + metadataGetter: metadataGetter, mediaTypeArray: mediaTypes, thumbnailAsData: thumbnailAsData, - returnMetadata: self.returnMetadata + returnMetadata: returnMetadata ) - let photoLibraryView = IONCAMRPhotoLibraryView(allowMultipleSelection: allowMultipleSelection, limit: limit).environmentObject(photoLibraryService) + let photoLibraryView = IONCAMRPhotoLibraryView(allowMultipleSelection: allowMultipleSelection, limit: limit) + .environmentObject(photoLibraryService) let viewController = UIHostingController(rootView: photoLibraryView) viewController.navigationItem.title = "Photo Library" - + let navController = UINavigationController(rootViewController: viewController) navController.modalPresentationStyle = .fullScreen return navController @@ -71,22 +78,22 @@ extension IONCAMRGalleryBehaviour { extension IONCAMRGalleryBehaviour: IONCAMRPhotoLibraryViewDelegate { func didPickMultimedia(_ mediaResultArray: [IONCAMRMediaResult]?) { - if let mediaResultArray = mediaResultArray { - self.delegate?.didReturn(.success(mediaResultArray)) + if let mediaResultArray { + delegate?.didReturn(.success(mediaResultArray)) } else { - self.delegate?.didReturn(.failure(.chooseMultimediaIssue)) + delegate?.didReturn(.failure(.chooseMultimediaIssue)) } } - + func didPickPicture(_ item: IONCAMRResultItem?) { - if let item = item { - self.delegate?.didReturn(self, with: .success(item)) + if let item { + delegate?.didReturn(self, with: .success(item)) } else { - self.delegate?.didReturn(self, with: .failure(.choosePictureIssue)) + delegate?.didReturn(self, with: .failure(.choosePictureIssue)) } } - + func didCancel() { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } diff --git a/Sources/IONCameraLib/Interfaces/IONCAMRMediaResultGenerator.swift b/Sources/IONCameraLib/Interfaces/IONCAMRMediaResultGenerator.swift index 27f1be5..dae6f33 100644 --- a/Sources/IONCameraLib/Interfaces/IONCAMRMediaResultGenerator.swift +++ b/Sources/IONCameraLib/Interfaces/IONCAMRMediaResultGenerator.swift @@ -9,7 +9,7 @@ private enum IONCAMRMetadataError: Error { final class IONCAMRMediaResultGenerator: IONCAMRMetadataGetterDelegate { func getVideoMetadata(from url: URL) async throws -> IONCAMRMetadata { let asset = AVAsset(url: url) - + let durationProperty: CMTime let trackArray: [AVAssetTrack] if #available(iOS 15, *) { @@ -19,37 +19,34 @@ final class IONCAMRMediaResultGenerator: IONCAMRMetadataGetterDelegate { durationProperty = asset.duration trackArray = asset.tracks(withMediaType: .video) } - + guard let track = trackArray.first else { throw IONCAMRMetadataError.noVideoTrack } guard let urlMetadata = url.metadata else { throw IONCAMRMetadataError.urlConversionError } - - let naturalSize: CGSize - if #available(iOS 15, *) { - naturalSize = try await track.load(.naturalSize) + + let naturalSize: CGSize = if #available(iOS 15, *) { + try await track.load(.naturalSize) } else { - naturalSize = track.naturalSize + track.naturalSize } let duration = Int(CMTimeGetSeconds(durationProperty).rounded()) let format = url.pathExtension.lowercased() let creationDate = urlMetadata.date let size = urlMetadata.fileSize let resolution = naturalSize.resolution - - let result = IONCAMRMetadata(size: size, duration: duration, format: format, resolution: resolution, creationDate: creationDate) - return result + + return IONCAMRMetadata(size: size, duration: duration, format: format, resolution: resolution, creationDate: creationDate) } - + func getImageMetadata(from image: UIImage, and url: URL) throws -> IONCAMRMetadata { guard let urlMetadata = url.metadata else { throw IONCAMRMetadataError.urlConversionError } - + let naturalSize = image.size let format = url.pathExtension.lowercased() let creationDate = urlMetadata.date let size = urlMetadata.fileSize let resolution = naturalSize.resolution - - let result = IONCAMRMetadata(size: size, format: format, resolution: resolution, creationDate: creationDate) - return result + + return IONCAMRMetadata(size: size, format: format, resolution: resolution, creationDate: creationDate) } } @@ -73,7 +70,7 @@ extension IONCAMRMediaResultGenerator: IONCAMRThumbnailGeneratorDelegate { } } } - + func getBase64String(from image: UIImage, with originalSize: IONCAMRSize?, and originalQuality: Int?) -> String? { let size = originalSize ?? IONCAMRTakePhotoOptions.defaultSquare let quality = originalQuality ?? IONCAMRTakePhotoOptions.ThumbnailDefaultConfigurations.quality @@ -90,5 +87,11 @@ extension IONCAMRMediaResultGenerator: IONCAMRImageFetcherDelegate { } extension IONCAMRMediaResultGenerator: IONCAMRURLGeneratorDelegate { - func url(for imageData: Data, withEncodingType encodingType: IONCAMREncodingType?) -> URL? { try? imageData.createImageTemporaryPath(with: encodingType?.description) } + func url( + for imageData: Data, + withEncodingType encodingType: IONCAMREncodingType? + ) + -> URL? { + try? imageData.createImageTemporaryPath(with: encodingType?.description) + } } diff --git a/Sources/IONCameraLib/Interfaces/IONCAMRPermissionsBehaviour.swift b/Sources/IONCameraLib/Interfaces/IONCAMRPermissionsBehaviour.swift index aabf465..316fc7f 100644 --- a/Sources/IONCameraLib/Interfaces/IONCAMRPermissionsBehaviour.swift +++ b/Sources/IONCameraLib/Interfaces/IONCAMRPermissionsBehaviour.swift @@ -1,18 +1,20 @@ import AVFoundation -import UIKit import Photos +import UIKit -/// Object responsible to trigger the user interface and handle all user interactions required to validate if the device is ready for the Camera actions. +/// Object responsible to trigger the user interface and handle all user interactions required to validate if the device is ready for the Camera +/// actions. final class IONCAMRPermissionsBehaviour: IONCAMRPermissionsDelegate { var coordinator: IONCAMRCoordinator - + /// Constructor method. /// - Parameter coordinator: User interface flow coordinator. init(coordinator: IONCAMRCoordinator) { self.coordinator = coordinator } - - /// Checks if the device has authorisation to access its camera. On the first time, it requests the user for access, displaying an alert if not granted. + + /// Checks if the device has authorisation to access its camera. On the first time, it requests the user for access, displaying an alert if not + /// granted. /// - Parameter handler: Closure that indicates if permissions were granted or not. func checkForCamera(_ handler: @escaping (Bool) -> Void) { guard AVCaptureDevice.authorizationStatus(for: .video) != .authorized else { return handler(true) } @@ -23,7 +25,7 @@ final class IONCAMRPermissionsBehaviour: IONCAMRPermissionsDelegate { handler(granted) } } - + func checkForPhotoLibrary(_ handler: @escaping (Bool) -> Void) { func isAuthorised(_ authorisationStatus: PHAuthorizationStatus) -> Bool { authorisationStatus == .limited || authorisationStatus == .authorized @@ -44,9 +46,9 @@ final class IONCAMRPermissionsBehaviour: IONCAMRPermissionsDelegate { } } -private extension IONCAMRPermissionsBehaviour { +extension IONCAMRPermissionsBehaviour { /// Displays the alert controller when the camera access authorisation was not granted. - func showAlertViewController(with title: String, and message: String) { + private func showAlertViewController(with title: String, and message: String) { DispatchQueue.main.async { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let okAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default) { _ in @@ -60,28 +62,28 @@ private extension IONCAMRPermissionsBehaviour { } alertController.addAction(okAction) alertController.addAction(settingsAction) - + self.coordinator.present(alertController) } } - - func noAccessToCameraAlertViewController() { + + private func noAccessToCameraAlertViewController() { let title = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "" let message = NSLocalizedString( "Access to the camera has been prohibited. Please enable it in the Settings app to continue.", comment: "" ) - - self.showAlertViewController(with: title, and: message) + + showAlertViewController(with: title, and: message) } - - func noAccessToPhotoLibraryAlertViewController() { + + private func noAccessToPhotoLibraryAlertViewController() { let title = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "" let message = NSLocalizedString( "Access to the photos has been prohibited. Please enable it in the Settings app to continue.", comment: "" ) - - self.showAlertViewController(with: title, and: message) + + showAlertViewController(with: title, and: message) } } diff --git a/Sources/IONCameraLib/Interfaces/IONCAMRPickerBehaviour.swift b/Sources/IONCameraLib/Interfaces/IONCAMRPickerBehaviour.swift index 345d3ca..c8dff21 100644 --- a/Sources/IONCameraLib/Interfaces/IONCAMRPickerBehaviour.swift +++ b/Sources/IONCameraLib/Interfaces/IONCAMRPickerBehaviour.swift @@ -5,13 +5,13 @@ final class IONCAMRPickerBehaviour: NSObject, IONCAMRPickerDelegate { weak var delegate: IONCAMRPickerResultsDelegate? /// User defined options to apply to the picker and picture objects. private var mediaOptions: IONCAMRMediaOptions? - + /// Verifies if camera is available for usage. /// - Returns: The camera's availability func isCameraAvailable() -> Bool { UIImagePickerController.isSourceTypeAvailable(.camera) } - + func captureMedia(with mediaOptions: IONCAMRMediaOptions, _ handler: @escaping (UIViewController) -> Void) { self.mediaOptions = mediaOptions DispatchQueue.main.async { @@ -30,48 +30,48 @@ final class IONCAMRPickerBehaviour: NSObject, IONCAMRPickerDelegate { /// Extension that handles the responses obtained through the Image Picker user interaction. extension IONCAMRPickerBehaviour: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - guard let mediaType = self.mediaOptions?.mediaType else { - self.delegate?.didReturn(self, with: .failure(.generalIssue)) + guard let mediaType = mediaOptions?.mediaType else { + delegate?.didReturn(self, with: .failure(.generalIssue)) return } - + let result: Result switch mediaType { case .picture: let image = info[.originalImage] as? UIImage - result = self.fetchToReturn(image) + result = fetchToReturn(image) .flatMap { .success(.picture($0)) } .flatMapError { .failure($0) } - self.delegate?.didReturn(self, with: result) + delegate?.didReturn(self, with: result) case .video: let videoURL = info[.mediaURL] as? URL - result = self.fetchToReturn(videoURL) + result = fetchToReturn(videoURL) .flatMap { .success(.video($0)) } .flatMapError { .failure($0) } - self.delegate?.didReturn(self, with: result) - default: break // not supposed to get here + delegate?.didReturn(self, with: result) + default: break // not supposed to get here } } - + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } -private extension IONCAMRPickerBehaviour { - func fetchToReturn(_ picture: UIImage?) -> Result { +extension IONCAMRPickerBehaviour { + private func fetchToReturn(_ picture: UIImage?) -> Result { guard let originalImage = picture, - let pictureOptions = self.mediaOptions as? IONCAMRTakePhotoOptions, + let pictureOptions = mediaOptions as? IONCAMRTakePhotoOptions, let image = originalImage.fix(with: pictureOptions) else { return .failure(.takePictureIssue) } - + return .success(image) } - - func fetchToReturn(_ videoURL: URL?) -> Result { - guard let videoURL = videoURL else { return .failure(.captureVideoIssue) } - let isPersistent = (self.mediaOptions as? IONCAMRRecordVideoOptions)?.isPersistent ?? true + private func fetchToReturn(_ videoURL: URL?) -> Result { + guard let videoURL else { return .failure(.captureVideoIssue) } + + let isPersistent = (mediaOptions as? IONCAMRRecordVideoOptions)?.isPersistent ?? true let url: URL if isPersistent { diff --git a/Sources/IONCameraLib/Interfaces/IONCAMRPlayerBehaviour.swift b/Sources/IONCameraLib/Interfaces/IONCAMRPlayerBehaviour.swift index 7f111be..051b893 100644 --- a/Sources/IONCameraLib/Interfaces/IONCAMRPlayerBehaviour.swift +++ b/Sources/IONCameraLib/Interfaces/IONCAMRPlayerBehaviour.swift @@ -3,41 +3,40 @@ import AVKit final class IONCAMRPlayerBehaviour: NSObject, IONCAMRPlayerDelegate { /// Object responsible for managing the user interface screens and respective flow. var coordinator: IONCAMRCoordinator - + init(coordinator: IONCAMRCoordinator) { self.coordinator = coordinator super.init() - + NotificationCenter.default.addObserver(forName: .CAMRAVPlayerVCDismissNotification, object: nil, queue: nil) { _ in DispatchQueue.main.async { self.coordinator.dismiss() } } } - + deinit { NotificationCenter.default.removeObserver(self) } - + func playVideo(_ url: URL) async throws { // Resolve the URL in case the app sandbox path has changed - let resolvedURL = try self.resolveVideoURL(url) + let resolvedURL = try resolveVideoURL(url) let asset = AVAsset(url: resolvedURL) - - let isPlayable: Bool - if #available(iOS 15, *) { - isPlayable = try await asset.load(.isPlayable) + + let isPlayable: Bool = if #available(iOS 15, *) { + try await asset.load(.isPlayable) } else { - isPlayable = asset.isPlayable + asset.isPlayable } - + if isPlayable { DispatchQueue.main.async { let player = AVPlayer(url: resolvedURL) let playerViewController = AVPlayerViewController() playerViewController.player = player self.coordinator.present(playerViewController) - + player.play() } } else { @@ -69,7 +68,7 @@ final class IONCAMRPlayerBehaviour: NSObject, IONCAMRPlayerDelegate { } extension AVPlayerViewController { - open override func viewDidDisappear(_ animated: Bool) { + override open func viewDidDisappear(_ animated: Bool) { NotificationCenter.default.post(name: .CAMRAVPlayerVCDismissNotification, object: nil) } } diff --git a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPHFetchResultCollection.swift b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPHFetchResultCollection.swift index c638ba9..a4222d6 100644 --- a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPHFetchResultCollection.swift +++ b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPHFetchResultCollection.swift @@ -3,18 +3,28 @@ import Photos struct IONCAMRPHFetchResultCollection: RandomAccessCollection, Equatable { typealias Element = PHAsset typealias Index = Int - + var fetchResult: PHFetchResult - - var endIndex: Int { self.fetchResult.count } - var startIndex: Int { 0 } - + + var endIndex: Int { + fetchResult.count + } + + var startIndex: Int { + 0 + } + subscript(position: Int) -> PHAsset { - self.fetchResult.object(at: self.fetchResult.count - position - 1) + fetchResult.object(at: fetchResult.count - position - 1) } } extension IONCAMRPHFetchResultCollection { - var startElement: PHAsset { self.fetchResult[self.startIndex] } - var endElement: PHAsset { self.fetchResult[self.endIndex - 1] } + var startElement: PHAsset { + fetchResult[startIndex] + } + + var endElement: PHAsset { + fetchResult[endIndex - 1] + } } diff --git a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryService.swift b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryService.swift index 61d0b0b..0281d75 100644 --- a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryService.swift +++ b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryService.swift @@ -7,25 +7,32 @@ typealias PHAssetLocalIdentifier = String class IONCAMRPhotoLibraryService: NSObject, ObservableObject { /// A collection that allows subscript support to PHFetchResult /// - /// The results property will store all of the photo asset ids that we requested, and will be used by our views to request for a copy of the photo itself. + /// The results property will store all of the photo asset ids that we requested, and will be used by our views to request for a copy of the photo + /// itself. /// /// We don't want to store a copy of the actual photo as it would cost too much memory, especially if we show the photos in a grid. @Published var results = IONCAMRPHFetchResultCollection(fetchResult: .init()) - + private weak var delegate: IONCAMRPhotoLibraryViewDelegate? private let metadataGetter: IONCAMRMetadataGetterDelegate private let mediaTypeArray: [PHAssetMediaType] private let thumbnailAsData: Bool private let returnMetadata: Bool - + /// The manager that will fetch and cache photos for us var imageCachingManager = PHCachingImageManager() - + var hasAccessToFullAlbum: Bool { PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized } - - init(delegate: IONCAMRPhotoLibraryViewDelegate, metadataGetter: IONCAMRMetadataGetterDelegate, mediaTypeArray: [PHAssetMediaType], thumbnailAsData: Bool, returnMetadata: Bool) { + + init( + delegate: IONCAMRPhotoLibraryViewDelegate, + metadataGetter: IONCAMRMetadataGetterDelegate, + mediaTypeArray: [PHAssetMediaType], + thumbnailAsData: Bool, + returnMetadata: Bool + ) { self.delegate = delegate self.metadataGetter = metadataGetter self.mediaTypeArray = mediaTypeArray @@ -37,8 +44,10 @@ class IONCAMRPhotoLibraryService: NSObject, ObservableObject { } // MARK: - Multimedia Fetchers + extension IONCAMRPhotoLibraryService { - /// Function that will tell the image caching manager to fetch all photos from the user's photo library. We don't want to include hidden assets for obvious privacy reasons. + /// Function that will tell the image caching manager to fetch all photos from the user's photo library. We don't want to include hidden assets + /// for obvious privacy reasons. /// /// We also need to sort the photos being fetched by the most recent first, mimicking the behaviour of the Recents album from the Photos app. func fetchAllPhotos() { @@ -46,16 +55,22 @@ extension IONCAMRPhotoLibraryService { fetchOptions.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ] - fetchOptions.predicate = self.mediaTypePredicate + fetchOptions.predicate = mediaTypePredicate DispatchQueue.main.async { self.results.fetchResult = PHAsset.fetchAssets(with: fetchOptions) } } - + /// Requests an image copy given a photo asset id. /// - /// The image caching manager performs the fetching, and will cache the photo fetched for later use. Please know that the cache is temporary – all photos cached will be lost when the app is terminated. - func fetchImage(byLocalIdentifier localId: PHAssetLocalIdentifier, targetSize: CGSize = PHImageManagerMaximumSize, contentMode: PHImageContentMode = .default) async throws -> UIImage? { + /// The image caching manager performs the fetching, and will cache the photo fetched for later use. Please know that the cache is temporary – all + /// photos cached will be lost when the app is terminated. + func fetchImage( + byLocalIdentifier localId: PHAssetLocalIdentifier, + targetSize: CGSize = PHImageManagerMaximumSize, + contentMode: PHImageContentMode = .default + ) async throws + -> UIImage? { let assets = PHAsset.fetchAssets(withLocalIdentifiers: [localId], options: nil) guard let asset = assets.firstObject else { throw IONCAMRError.imageNotFound } let options = PHImageRequestOptions() @@ -66,14 +81,14 @@ extension IONCAMRPhotoLibraryService { options.version = .current return try await withCheckedThrowingContinuation { [weak self] continuation in - /// Use the imageCachingManager to fetch the image + // Use the imageCachingManager to fetch the image self?.imageCachingManager.requestImage( for: asset, targetSize: targetSize, contentMode: contentMode, options: options, resultHandler: { image, info in - /// image is of type UIImage + // image is of type UIImage if let error = info?[PHImageErrorKey] as? Error { continuation.resume(throwing: error) return @@ -83,7 +98,7 @@ extension IONCAMRPhotoLibraryService { ) } } - + private func fetchVideo(byLocalIdentifier localId: PHAssetLocalIdentifier) async throws -> URL? { let assets = PHAsset.fetchAssets(withLocalIdentifiers: [localId], options: nil) guard let asset = assets.firstObject else { throw IONCAMRError.videoNotFound } @@ -92,7 +107,7 @@ extension IONCAMRPhotoLibraryService { options.version = .original options.isNetworkAccessAllowed = true return try await withCheckedThrowingContinuation { [weak self] continuation in - /// Use the imageCachingManager to fetch the image + // Use the imageCachingManager to fetch the image self?.imageCachingManager.requestAVAsset(forVideo: asset, options: options, resultHandler: { asset, _, info in if let error = info?[PHImageErrorKey] as? Error { continuation.resume(throwing: error) @@ -109,7 +124,7 @@ extension IONCAMRPhotoLibraryService { }) } } - + private func fetchImageURL(for asset: PHAsset) async throws -> URL? { let options = PHContentEditingInputRequestOptions() options.isNetworkAccessAllowed = true @@ -126,58 +141,58 @@ extension IONCAMRPhotoLibraryService { } // MARK: - Returning delegates + extension IONCAMRPhotoLibraryService { private func fetchSingleResult(_ asset: PHAsset?) async -> IONCAMRResultItem? { - guard let asset = asset, let image = try? await self.fetchImage( - byLocalIdentifier: asset.localIdentifier, targetSize: CGSize(resolution: IONCAMRTakePhotoOptions.ThumbnailDefaultConfigurations.resolution) + guard let asset, let image = try? await fetchImage( + byLocalIdentifier: asset.localIdentifier, + targetSize: CGSize(resolution: IONCAMRTakePhotoOptions.ThumbnailDefaultConfigurations.resolution) ) else { return nil } return .picture(image) } - + private func fetchImage(from asset: PHAsset) async throws -> IONCAMRMediaResult { - guard let image = try await self.fetchImage(byLocalIdentifier: asset.localIdentifier), + guard let image = try await fetchImage(byLocalIdentifier: asset.localIdentifier), let imageData = image.pictureThumbnailData(), - let imageURL = try await self.fetchImageURL(for: asset) + let imageURL = try await fetchImageURL(for: asset) else { throw IONCAMRError.imageNotFound } - + var metadata: IONCAMRMetadata? - if self.returnMetadata { - metadata = try? self.metadataGetter.getImageMetadata(from: image, and: imageURL) + if returnMetadata { + metadata = try? metadataGetter.getImageMetadata(from: image, and: imageURL) } - - let result = IONCAMRMediaResult(pictureWith: imageURL.absoluteString, imageData, and: metadata) - return result + + return IONCAMRMediaResult(pictureWith: imageURL.absoluteString, imageData, and: metadata) } - + private func fetchVideo(from asset: PHAsset) async throws -> IONCAMRMediaResult { - guard let image = try await self.fetchImage( + guard let image = try await fetchImage( byLocalIdentifier: asset.localIdentifier, targetSize: CGSize(resolution: IONCAMRRecordVideoOptions.ThumbnailDefaultConfigurations.resolution) ), - let videoData = image.defaultVideoThumbnailData, - let videoURL = try await self.fetchVideo(byLocalIdentifier: asset.localIdentifier) + let videoData = image.defaultVideoThumbnailData, + let videoURL = try await fetchVideo(byLocalIdentifier: asset.localIdentifier) else { throw IONCAMRError.videoNotFound } - + var metadata: IONCAMRMetadata? - if self.returnMetadata { - metadata = try? await self.metadataGetter.getVideoMetadata(from: videoURL) + if returnMetadata { + metadata = try? await metadataGetter.getVideoMetadata(from: videoURL) } - - let result = IONCAMRMediaResult(videoWith: videoURL.absoluteString, videoData, and: metadata) - return result + + return IONCAMRMediaResult(videoWith: videoURL.absoluteString, videoData, and: metadata) } - + private func fetchMultipleResult(_ assetArray: [PHAsset]) async -> [IONCAMRMediaResult]? { do { var result = [IONCAMRMediaResult]() for asset in assetArray { switch asset.mediaType { case .image: - let imageAsset = try await self.fetchImage(from: asset) + let imageAsset = try await fetchImage(from: asset) result += [imageAsset] case .video: - let videoAsset = try await self.fetchVideo(from: asset) + let videoAsset = try await fetchVideo(from: asset) result += [videoAsset] default: return nil // not supposed to get here } @@ -187,7 +202,7 @@ extension IONCAMRPhotoLibraryService { return nil } } - + func didFinishPicking(_ assetArray: [PHAsset]) { Task { if self.thumbnailAsData { @@ -203,15 +218,15 @@ extension IONCAMRPhotoLibraryService { } } } - + func didCancelPicking() { - self.delegate?.didCancel() + delegate?.didCancel() } } extension IONCAMRPhotoLibraryService: PHPhotoLibraryChangeObserver { func photoLibraryDidChange(_ changeInstance: PHChange) { - if let changes = changeInstance.changeDetails(for: self.results.fetchResult) { + if let changes = changeInstance.changeDetails(for: results.fetchResult) { DispatchQueue.main.async { self.results.fetchResult = changes.fetchResultAfterChanges } @@ -219,10 +234,10 @@ extension IONCAMRPhotoLibraryService: PHPhotoLibraryChangeObserver { } } -private extension IONCAMRPhotoLibraryService { - var mediaTypePredicate: NSPredicate { - let formatArray = [String](repeating: "mediaType == %d", count: self.mediaTypeArray.count) - let typeArray = self.mediaTypeArray.map { $0.rawValue } +extension IONCAMRPhotoLibraryService { + private var mediaTypePredicate: NSPredicate { + let formatArray = [String](repeating: "mediaType == %d", count: mediaTypeArray.count) + let typeArray = mediaTypeArray.map(\.rawValue) return NSPredicate(format: formatArray.joined(separator: " || "), argumentArray: typeArray) } } diff --git a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryView.swift b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryView.swift index ac2f65c..f1bc345 100644 --- a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryView.swift +++ b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoLibraryView.swift @@ -4,35 +4,39 @@ import SwiftUI struct IONCAMRPhotoLibraryView: View { /// Photo library will ask for permission to ask the user for Photo access, and will provide the photos as well. @EnvironmentObject var photoLibraryService: IONCAMRPhotoLibraryService - + var allowMultipleSelection: Bool - var limit: Int = 0 - + var limit = 0 + @State var selectedAssetArray = [PHAsset]() - @State var showActionSheet: Bool = false - @State var showLimitedPicker: Bool = false - + @State var showActionSheet = false + @State var showLimitedPicker = false + var body: some View { VStack { - if !self.photoLibraryService.hasAccessToFullAlbum { + if !photoLibraryService.hasAccessToFullAlbum { Button { - self.showActionSheet = true + showActionSheet = true } label: { // swiftlint:disable:next line_length - (Text("You've given \(Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "") access to only a selected number of media. ") + ( + Text( + "You've given \(Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "") access to only a selected number of media. " + ) .foregroundColor(.gray) - + Text("Manage")) + + Text("Manage") + ) .padding() .multilineTextAlignment(.leading) .font(.subheadline) } .frame(maxWidth: .infinity) .background(Color(.systemGray6)) - .actionSheet(isPresented: self.$showActionSheet, content: { + .actionSheet(isPresented: $showActionSheet, content: { ActionSheet(title: Text("Please select your option"), buttons: [ .default(Text("Change multimedia selection")) { - self.showLimitedPicker = true - self.selectedAssetArray = [] + showLimitedPicker = true + selectedAssetArray = [] }, .default(Text("Change settings")) { if let url = URL(string: UIApplication.openSettingsURLString) { @@ -40,22 +44,25 @@ struct IONCAMRPhotoLibraryView: View { } }, .cancel { - self.showActionSheet = false - }]) + showActionSheet = false + } + ]) }) - - LimitedPicker(isPresented: self.$showLimitedPicker) + + LimitedPicker(isPresented: $showLimitedPicker) .frame(width: 0, height: 0) } - - if self.photoLibraryService.results.isEmpty { + + if photoLibraryService.results.isEmpty { VStack(spacing: 16) { Spacer() Text("No Content") .font(.title) // swiftlint:disable:next line_length - Text("Currently, \(Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "") doesn't have access to any media.") - .font(.body) + Text( + "Currently, \(Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "") doesn't have access to any media." + ) + .font(.body) Spacer() } .padding() @@ -65,30 +72,33 @@ struct IONCAMRPhotoLibraryView: View { ScrollViewReader { value in ScrollView { LazyVGrid( - /// We'll set a 3-column row with an adaptive width of 100 for each grid item, and give it a spacing of 1 pixel in between columns and in between rows + // We'll set a 3-column row with an adaptive width of 100 for each grid item, and give it a spacing of 1 pixel in between + // columns and in between rows columns: Array(repeating: .init(.adaptive(minimum: 100), spacing: 1), count: 3), spacing: 1 ) { - /// We'll go through the photo references fetched by the photo gallery and give a photo asset ID into the PhotoThumbnailView so it knows what image to load and show into the grid - ForEach(self.photoLibraryService.results, id: \.self) { asset in + // We'll go through the photo references fetched by the photo gallery and give a photo asset ID into the + // PhotoThumbnailView so it knows what image to load and show into the grid + ForEach(photoLibraryService.results, id: \.self) { asset in ZStack(alignment: .bottomTrailing) { - /// Wrap the PhotoThumbnailView into a button so we can tap on it without overlapping the tap area of each photo, as photos have their aspect ratios, and may go out of bounds of the square view. + // Wrap the PhotoThumbnailView into a button so we can tap on it without overlapping the tap area of each photo, + // as photos have their aspect ratios, and may go out of bounds of the square view. Button { - if let assetIndex = self.selectedAssetArray.firstIndex(of: asset) { - self.selectedAssetArray.remove(at: assetIndex) - } else if self.allowMultipleSelection { - if self.limit == 0 || self.selectedAssetArray.count < self.limit { - self.selectedAssetArray.append(asset) + if let assetIndex = selectedAssetArray.firstIndex(of: asset) { + selectedAssetArray.remove(at: assetIndex) + } else if allowMultipleSelection { + if limit == 0 || selectedAssetArray.count < limit { + selectedAssetArray.append(asset) } } else { - self.selectedAssetArray = [asset] + selectedAssetArray = [asset] } } label: { IONCAMRPhotoThumbnailView(assetLocalId: asset.localIdentifier, showVideoIcon: asset.mediaType == .video) - .opacity(self.selectedAssetArray.contains(asset) ? 0.5 : 1) + .opacity(selectedAssetArray.contains(asset) ? 0.5 : 1) } - - if self.selectedAssetArray.contains(asset) { + + if selectedAssetArray.contains(asset) { Image(systemName: "checkmark.circle") .resizable() .frame(width: 25, height: 25) @@ -101,7 +111,7 @@ struct IONCAMRPhotoLibraryView: View { } } .onAppear { - value.scrollTo(self.photoLibraryService.results.startElement, anchor: .bottom) + value.scrollTo(photoLibraryService.results.startElement, anchor: .bottom) } } } @@ -110,39 +120,39 @@ struct IONCAMRPhotoLibraryView: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { - self.photoLibraryService.didCancelPicking() + photoLibraryService.didCancelPicking() } label: { Text("Cancel") } } ToolbarItem(placement: .navigationBarTrailing) { Button { - self.photoLibraryService.didFinishPicking(self.selectedAssetArray) + photoLibraryService.didFinishPicking(selectedAssetArray) } label: { Text("Done") .bold() } - .disabled(self.selectedAssetArray.isEmpty) + .disabled(selectedAssetArray.isEmpty) } } .onAppear { - self.photoLibraryService.fetchAllPhotos() + photoLibraryService.fetchAllPhotos() } } } struct LimitedPicker: UIViewControllerRepresentable { @Binding var isPresented: Bool - + func makeUIViewController(context: Context) -> UIViewController { UIViewController() } - + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { - if self.isPresented { + if isPresented { PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: uiViewController) DispatchQueue.main.async { - self.isPresented = false + isPresented = false } } } diff --git a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoThumbnailView.swift b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoThumbnailView.swift index 0b0e56f..57d7f6f 100644 --- a/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoThumbnailView.swift +++ b/Sources/IONCameraLib/Interfaces/PhotoLibrary/IONCAMRPhotoThumbnailView.swift @@ -3,25 +3,29 @@ import SwiftUI /// The photo thumbnail view is responsible for showing a photo in the photo grid. struct IONCAMRPhotoThumbnailView: View { - /// The image view that will render the photo that we'll be fetching. It is set to optional since we don't have an actual photo when this view starts to render. + /// The image view that will render the photo that we'll be fetching. It is set to optional since we don't have an actual photo when this view + /// starts to render. /// - /// We need to give time for the photo library service to fetch a copy of the photo using the asset id, so we'll set the image with the fetched photo at a later time. + /// We need to give time for the photo library service to fetch a copy of the photo using the asset id, so we'll set the image with the fetched + /// photo at a later time. /// - /// Fetching make take time, especially if the photo has been requested initially. However, photos that were successfully fetched are cached, so any fetching from that point forward will be fast. + /// Fetching make take time, especially if the photo has been requested initially. However, photos that were successfully fetched are cached, so + /// any fetching from that point forward will be fast. /// /// Also, we would want to free up the image from the memory when this view disappears in order to save up memory. @State private var image: Image? - - /// We'll use the photo library service to fetch a photo given an asset id, and cache it for later use. If the photo is already cached, a cached copy will be provided instead. + + /// We'll use the photo library service to fetch a photo given an asset id, and cache it for later use. If the photo is already cached, a cached + /// copy will be provided instead. /// /// Ideally, we don't want to store a reference to an image itself and pass it around views as it would cost memory. /// We'll use the asset id instead as a reference, and allow the photo library's cache to handle any memory management for us. @EnvironmentObject var photoLibraryService: IONCAMRPhotoLibraryService - + /// The reference id of the selected photo private let assetLocalId: String private let showVideoIcon: Bool - + init(assetLocalId: String, showVideoIcon: Bool) { self.assetLocalId = assetLocalId self.showVideoIcon = showVideoIcon @@ -30,13 +34,13 @@ struct IONCAMRPhotoThumbnailView: View { var body: some View { Group { // Show the image if it's available - if let image = self.image { + if let image { ZStack(alignment: .bottomLeading) { image .resizable() .aspectRatio(contentMode: .fill) .clipped() - if self.showVideoIcon { + if showVideoIcon { Image(systemName: "video.fill") .resizable() .frame(width: 15, height: 10) @@ -56,26 +60,26 @@ struct IONCAMRPhotoThumbnailView: View { } // We need to use the task to work on a concurrent request to load the image from the photo library service, which is asynchronous work. .taskOperation { - await self.loadImageAsset() + await loadImageAsset() } // Finally, when the view disappears, we need to free it up from the memory .onDisappear { - self.image = nil + image = nil } } } extension IONCAMRPhotoThumbnailView { func loadImageAsset() async { - guard let uiImage = try? await self.photoLibraryService.fetchImage( + guard let uiImage = try? await photoLibraryService.fetchImage( byLocalIdentifier: assetLocalId, targetSize: CGSize(width: 150, height: 150), contentMode: .aspectFill ) else { - self.image = nil + image = nil return } - self.image = Image(uiImage: uiImage) + image = Image(uiImage: uiImage) } } @@ -87,10 +91,10 @@ extension View { return taskiOS14(priority: priority, action) } } - + @available(iOS, deprecated: 15.0, message: "This extension is no longer necessary. Use API built into SDK") func taskiOS14(priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async -> Void) -> some View { - self.onAppear { + onAppear { Task(priority: priority) { await action() } diff --git a/Sources/IONCameraLib/Models/IONCAMREditOptions.swift b/Sources/IONCameraLib/Models/IONCAMREditOptions.swift index 2e36bbb..aa75999 100644 --- a/Sources/IONCameraLib/Models/IONCAMREditOptions.swift +++ b/Sources/IONCameraLib/Models/IONCAMREditOptions.swift @@ -4,22 +4,22 @@ public class IONCAMRPhotoEditOptions: IONCAMRSaveToGalleryOptionsDelegate, Decod var saveToGallery: Bool /// Indicates if we should returns the media's metadata var returnMetadata: Bool - /// Sets default camera for capturing a picture. - + // Sets default camera for capturing a picture. + public init(uri: String, saveToGallery: Bool, returnMetadata: Bool) { self.uri = uri self.saveToGallery = saveToGallery self.returnMetadata = returnMetadata } - required public convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let uri = try container.decode(String.self, forKey: .uri) let saveToGallery = try container.decodeIfPresent(Bool.self, forKey: .saveToGallery) ?? false let returnMetadata = try container.decodeIfPresent(Bool.self, forKey: .returnMetadata) ?? false self.init(uri: uri, saveToGallery: saveToGallery, returnMetadata: returnMetadata) } - + private enum CodingKeys: String, CodingKey { case uri, saveToGallery, returnMetadata } diff --git a/Sources/IONCameraLib/Models/IONCAMREncodingType.swift b/Sources/IONCameraLib/Models/IONCAMREncodingType.swift index 00bd1e9..433ce41 100644 --- a/Sources/IONCameraLib/Models/IONCAMREncodingType.swift +++ b/Sources/IONCameraLib/Models/IONCAMREncodingType.swift @@ -2,11 +2,11 @@ public enum IONCAMREncodingType: Int, Decodable, CustomStringConvertible { case jpeg = 0 case png - + public var description: String { switch self { - case .jpeg: return "jpeg" - case .png: return "png" + case .jpeg: "jpeg" + case .png: "png" } } } diff --git a/Sources/IONCameraLib/Models/IONCAMRError.swift b/Sources/IONCameraLib/Models/IONCAMRError.swift index ea55c2e..60d9b0d 100644 --- a/Sources/IONCameraLib/Models/IONCAMRError.swift +++ b/Sources/IONCameraLib/Models/IONCAMRError.swift @@ -3,94 +3,95 @@ import Foundation /// All plugin errors that can be thrown public enum IONCAMRError: Int, CustomNSError, LocalizedError { // MARK: - Permissions Errors + case cameraAccess = 3 case cameraAvailability = 8 - + // MARK: - Take Pictures Errors + + case takePictureCancel = 7 case takePictureIssue = 11 case takePictureArguments = 15 - case takePictureCancel = 16 - + // MARK: - Edit Picture Errors + case invalidImageData = 9 case editPictureIssue = 10 case editPictureCancel = 14 - + // MARK: - Choose Picture Errors + case photoLibraryAccess = 6 case imageNotFound = 12 case choosePictureIssue = 13 case choosePictureCancel = 20 - + // MARK: - Capture Video Errors + case captureVideoIssue = 18 case captureVideoCancel = 19 - + // MARK: - Choose Multimedia Errors + case videoNotFound = 28 case chooseMultimediaIssue = 21 case chooseMultimediaCancel = 23 case fetchImageFromURLFailed = 31 - + // MARK: - Play Video Errors + case playVideoIssue = 26 - + // MARK: - General Errors + case invalidEncodeResultMedia = 22 case generalIssue = 29 - + /// Textual description public var errorDescription: String? { switch self { case .cameraAccess: - return "Couldn't access camera. Check your camera permissions and try again." - + "Couldn't access camera. Check your camera permissions and try again." case .cameraAvailability: - return "No camera available." + "No camera available." case .takePictureIssue: - return "Couldn't capture picture." + "Couldn't take photo." case .takePictureArguments: - return "Couldn't decode the 'Take Picture' action parameters." + "Couldn't decode the 'Take Picture' action parameters." case .takePictureCancel: - return "Couldn't capture picture because the process was canceled." - + "Couldn't take photo because the process was canceled." case .invalidImageData: - return "The selected file contains data that isn't valid." + "The selected file contains data that isn't valid." case .editPictureIssue: - return "Couldn't edit image." + "Couldn't edit image." case .editPictureCancel: - return "Couldn't edit picture because the process was canceled." - + "Couldn't edit photo because the process was canceled." case .photoLibraryAccess: - return "Couldn't access your photo gallery because access wasn't provided. Check its permissions and try again." + "Couldn't access your photo gallery because access wasn't provided. Check its permissions and try again." case .imageNotFound: - return "Couldn't get image from the gallery." + "Couldn't get image from the gallery." case .choosePictureIssue: - return "Couldn't process image." + "Couldn't process image." case .choosePictureCancel: - return "Couldn't choose picture because the process was canceled." - + "Couldn't choose picture because the process was canceled." case .captureVideoIssue: - return "Couldn't capture video." + "Couldn't record video." case .captureVideoCancel: - return "Couldn't capture video because the process was canceled." - + "Couldn't record video because the process was canceled." case .videoNotFound: - return "Couldn't get video from the gallery." + "Couldn't get video from the gallery." case .chooseMultimediaIssue: - return "Couldn't choose media from the gallery." + "Couldn't choose media from the gallery." case .chooseMultimediaCancel: - return "Couldn't choose media from the gallery because the process was canceled." + "Couldn't choose media from the gallery because the process was canceled." case .fetchImageFromURLFailed: - return "Couldn't retrieve image from the URI." - + "Couldn't retrieve image from the URI." case .playVideoIssue: - return "Couldn't play video." - + "Couldn't play video." case .invalidEncodeResultMedia: - return "Couldn't encode the media result." + "Couldn't encode the media result." case .generalIssue: - return "There's an issue with the plugin." + "There's an issue with the plugin." } } } diff --git a/Sources/IONCameraLib/Models/IONCAMRGalleryOptions.swift b/Sources/IONCameraLib/Models/IONCAMRGalleryOptions.swift index 22e1255..4362666 100644 --- a/Sources/IONCameraLib/Models/IONCAMRGalleryOptions.swift +++ b/Sources/IONCameraLib/Models/IONCAMRGalleryOptions.swift @@ -8,9 +8,10 @@ public class IONCAMRGalleryOptions: IONCAMREditMediaTypeOptionsDelegate, Decodab public let thumbnailAsData: Bool /// Indicates if the media's metadata should be returned public var returnMetadata: Bool - /// Indicates the maximum number of media items that can be selected when allowMultipleSelection is true. Ignored if allowMultipleSelection is false. 0 means no limit. + /// Indicates the maximum number of media items that can be selected when allowMultipleSelection is true. Ignored if allowMultipleSelection is + /// false. 0 means no limit. public let limit: Int - + init(mediaType: IONCAMRMediaType, allowEdit: Bool, allowMultipleSelection: Bool, andThumbnailAsData: Bool, returnMetadata: Bool, limit: Int = 0) { self.mediaType = mediaType self.allowEdit = allowEdit @@ -29,7 +30,14 @@ public class IONCAMRGalleryOptions: IONCAMREditMediaTypeOptionsDelegate, Decodab let thumbnailAsData = try container.decodeIfPresent(Bool.self, forKey: .thumbnailAsData) ?? true let returnMetadata = try container.decodeIfPresent(Bool.self, forKey: .includeMetadata) ?? false let limit = try container.decodeIfPresent(Int.self, forKey: .limit) ?? 0 - self.init(mediaType: mediaType, allowEdit: allowEdit, allowMultipleSelection: allowMultipleSelection, andThumbnailAsData: thumbnailAsData, returnMetadata: returnMetadata, limit: limit) + self.init( + mediaType: mediaType, + allowEdit: allowEdit, + allowMultipleSelection: allowMultipleSelection, + andThumbnailAsData: thumbnailAsData, + returnMetadata: returnMetadata, + limit: limit + ) } private enum CodingKeys: String, CodingKey { diff --git a/Sources/IONCameraLib/Models/IONCAMRMediaOptions.swift b/Sources/IONCameraLib/Models/IONCAMRMediaOptions.swift index 99ff999..24f565f 100644 --- a/Sources/IONCameraLib/Models/IONCAMRMediaOptions.swift +++ b/Sources/IONCameraLib/Models/IONCAMRMediaOptions.swift @@ -10,8 +10,15 @@ public class IONCAMRMediaOptions: IONCAMREditMediaTypeOptionsDelegate, IONCAMRSa var allowEdit: Bool /// Presentation style to use when showing the camera interface. Default is `.fullscreen`. var presentationStyle: IONCAMRPresentationStyle = .fullscreen - - init(mediaType: IONCAMRMediaType, saveToGallery: Bool, returnMetadata: Bool, direction: IONCAMRDirection, allowEdit: Bool, presentationStyle: IONCAMRPresentationStyle = .fullscreen) { + + init( + mediaType: IONCAMRMediaType, + saveToGallery: Bool, + returnMetadata: Bool, + direction: IONCAMRDirection, + allowEdit: Bool, + presentationStyle: IONCAMRPresentationStyle = .fullscreen + ) { self.mediaType = mediaType self.saveToGallery = saveToGallery self.returnMetadata = returnMetadata diff --git a/Sources/IONCameraLib/Models/IONCAMRMediaResult.swift b/Sources/IONCameraLib/Models/IONCAMRMediaResult.swift index be805f2..5e6761e 100644 --- a/Sources/IONCameraLib/Models/IONCAMRMediaResult.swift +++ b/Sources/IONCameraLib/Models/IONCAMRMediaResult.swift @@ -4,8 +4,8 @@ public struct IONCAMRMediaResult { public let thumbnail: String public let metadata: IONCAMRMetadata? public let saved: Bool? - - init(type: IONCAMRMediaType, uri: String, thumbnail: String, metadata: IONCAMRMetadata? = nil, saved: Bool? = nil ) { + + init(type: IONCAMRMediaType, uri: String, thumbnail: String, metadata: IONCAMRMetadata? = nil, saved: Bool? = nil) { self.type = type self.uri = uri self.thumbnail = thumbnail @@ -18,11 +18,11 @@ extension IONCAMRMediaResult { init(pictureWith uri: String, _ thumbnail: String, and metadata: IONCAMRMetadata? = nil, saved: Bool? = nil) { self.init(type: .picture, uri: uri, thumbnail: thumbnail, metadata: metadata, saved: saved) } - + init(pictureWith data: String, saved: Bool? = nil) { self.init(type: .picture, uri: "", thumbnail: data, saved: saved) } - + init(videoWith uri: String, _ thumbnail: String, and metadata: IONCAMRMetadata? = nil) { self.init(type: .video, uri: uri, thumbnail: thumbnail, metadata: metadata) } @@ -32,13 +32,13 @@ extension IONCAMRMediaResult: Encodable { enum CodingKeys: String, CodingKey { case type, uri, thumbnail, metadata, saved } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.type.enumerator.rawValue, forKey: .type) - try container.encode(self.uri, forKey: .uri) - try container.encode(self.thumbnail, forKey: .thumbnail) - try container.encodeIfPresent(self.metadata, forKey: .metadata) - try container.encodeIfPresent(self.saved, forKey: .saved) + try container.encode(type.enumerator.rawValue, forKey: .type) + try container.encode(uri, forKey: .uri) + try container.encode(thumbnail, forKey: .thumbnail) + try container.encodeIfPresent(metadata, forKey: .metadata) + try container.encodeIfPresent(saved, forKey: .saved) } } diff --git a/Sources/IONCameraLib/Models/IONCAMRMediaType.swift b/Sources/IONCameraLib/Models/IONCAMRMediaType.swift index 4d7e5d8..f7ad762 100644 --- a/Sources/IONCameraLib/Models/IONCAMRMediaType.swift +++ b/Sources/IONCameraLib/Models/IONCAMRMediaType.swift @@ -1,12 +1,12 @@ public struct IONCAMRMediaType: OptionSet { public typealias RawValue = Int - + public var rawValue: RawValue - + public init(rawValue: RawValue) { self.rawValue = rawValue } - + public static let picture = Self(rawValue: 1 << 0) public static let video = Self(rawValue: 1 << 1) public static let both: IONCAMRMediaType = [.picture, .video] @@ -18,11 +18,11 @@ extension IONCAMRMediaType { case video case both } - + enum IONCAMRMediaTypeError: Error { case unknownType } - + var enumerator: IONCAMRMediaTypeEnum { get throws { switch self { @@ -33,10 +33,10 @@ extension IONCAMRMediaType { } } } - + public init(from enumValue: Int) throws { guard let enumerator = IONCAMRMediaTypeEnum(rawValue: enumValue) else { throw IONCAMRMediaTypeError.unknownType } - + switch enumerator { case .picture: self = .picture case .video: self = .video @@ -48,9 +48,9 @@ extension IONCAMRMediaType { extension IONCAMRMediaType: CustomStringConvertible { public var description: String { switch self { - case .picture: return "image" - case .video: return "video" - default: return "" + case .picture: "image" + case .video: "video" + default: "" } } } diff --git a/Sources/IONCameraLib/Models/IONCAMRMetadata.swift b/Sources/IONCameraLib/Models/IONCAMRMetadata.swift index 9cf08e1..3a97b42 100644 --- a/Sources/IONCameraLib/Models/IONCAMRMetadata.swift +++ b/Sources/IONCameraLib/Models/IONCAMRMetadata.swift @@ -6,7 +6,7 @@ public struct IONCAMRMetadata { public var format: String public var resolution: String public var creationDate: Date - + init(size: UInt64, duration: Int? = nil, format: String, resolution: String, creationDate: Date) { self.size = size self.duration = duration @@ -20,13 +20,13 @@ extension IONCAMRMetadata: Encodable { enum CodingKeys: String, CodingKey { case size, duration, format, resolution, creationDate } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.size, forKey: .size) - try container.encodeIfPresent(self.duration, forKey: .duration) - try container.encode(self.format, forKey: .format) - try container.encode(self.resolution, forKey: .resolution) - try container.encode(self.creationDate, forKey: .creationDate) + try container.encode(size, forKey: .size) + try container.encodeIfPresent(duration, forKey: .duration) + try container.encode(format, forKey: .format) + try container.encode(resolution, forKey: .resolution) + try container.encode(creationDate, forKey: .creationDate) } } diff --git a/Sources/IONCameraLib/Models/IONCAMRPlayVideoOptions.swift b/Sources/IONCameraLib/Models/IONCAMRPlayVideoOptions.swift index 43b6f30..6f5fcad 100644 --- a/Sources/IONCameraLib/Models/IONCAMRPlayVideoOptions.swift +++ b/Sources/IONCameraLib/Models/IONCAMRPlayVideoOptions.swift @@ -8,15 +8,15 @@ extension IONCAMRPlayVideoOptions: Decodable { enum DecodeError: Error { case invalidURL } - + enum CodingKeys: String, CodingKey { case url = "uri" } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let urlString = try container.decode(String.self, forKey: .url) - + guard let url = URL(string: urlString) else { throw DecodeError.invalidURL } self.init(url: url) } diff --git a/Sources/IONCameraLib/Models/IONCAMRPresentationStyle.swift b/Sources/IONCameraLib/Models/IONCAMRPresentationStyle.swift index b0543a4..e83ce8c 100644 --- a/Sources/IONCameraLib/Models/IONCAMRPresentationStyle.swift +++ b/Sources/IONCameraLib/Models/IONCAMRPresentationStyle.swift @@ -1,15 +1,15 @@ import UIKit -public enum IONCAMRPresentationStyle : String, Codable { +public enum IONCAMRPresentationStyle: String, Codable { case fullscreen case popover var uiModalPresentationStyle: UIModalPresentationStyle { switch self { case .fullscreen: - return .fullScreen + .fullScreen case .popover: - return .popover + .popover } } } diff --git a/Sources/IONCameraLib/Models/IONCAMRRecordVideoOptions.swift b/Sources/IONCameraLib/Models/IONCAMRRecordVideoOptions.swift index 4f4f87b..0fcca16 100644 --- a/Sources/IONCameraLib/Models/IONCAMRRecordVideoOptions.swift +++ b/Sources/IONCameraLib/Models/IONCAMRRecordVideoOptions.swift @@ -28,7 +28,7 @@ public class IONCAMRRecordVideoOptions: IONCAMRMediaOptions, Decodable { } extension IONCAMRRecordVideoOptions { - struct ThumbnailDefaultConfigurations { + enum ThumbnailDefaultConfigurations { static let quality = 1.0 static let resolution = 480 } diff --git a/Sources/IONCameraLib/Models/IONCAMRSize.swift b/Sources/IONCameraLib/Models/IONCAMRSize.swift index 8fcd317..48ce825 100644 --- a/Sources/IONCameraLib/Models/IONCAMRSize.swift +++ b/Sources/IONCameraLib/Models/IONCAMRSize.swift @@ -8,7 +8,7 @@ public struct IONCAMRSize: Decodable { let width: Int /// Height for the image. let height: Int - + /// Constructor /// - Parameters: /// - width: Width to set. diff --git a/Sources/IONCameraLib/Models/IONCAMRTakePhotoOptions.swift b/Sources/IONCameraLib/Models/IONCAMRTakePhotoOptions.swift index 9cd312b..df1804e 100644 --- a/Sources/IONCameraLib/Models/IONCAMRTakePhotoOptions.swift +++ b/Sources/IONCameraLib/Models/IONCAMRTakePhotoOptions.swift @@ -17,19 +17,19 @@ public class IONCAMRTakePhotoOptions: IONCAMRMediaOptions, Decodable { case quality, width, height, correctOrientation, encodingType, saveToGallery, cameraDirection, allowEdit, includeMetadata, presentationStyle } - required public convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { func throwError(field: String) -> IONCAMRTakePhotoOptionsError { IONCAMRTakePhotoOptionsError.invalid(field: field) } - + let container = try decoder.container(keyedBy: CodingKeys.self) - + let quality = try container.decodeIfPresent(Int.self, forKey: .quality) ?? 90 if quality < 0 || quality > 100 { throw throwError(field: "quality") } - var size: IONCAMRSize? = nil + var size: IONCAMRSize? let width = try container.decodeIfPresent(Int.self, forKey: .width) let height = try container.decodeIfPresent(Int.self, forKey: .height) - if let width = width, let height = height { + if let width, let height { size = try IONCAMRSize(width: width, height: height) } let correctOrientation = try container.decodeIfPresent(Bool.self, forKey: .correctOrientation) ?? false @@ -78,21 +78,23 @@ public class IONCAMRTakePhotoOptions: IONCAMRMediaOptions, Decodable { self.correctOrientation = correctOrientation self.encodingType = encodingType super.init( - mediaType: .picture, - saveToGallery: saveToGallery, - returnMetadata: returnMetadata, - direction: cameraDirection, - allowEdit: allowEdit, + mediaType: .picture, + saveToGallery: saveToGallery, + returnMetadata: returnMetadata, + direction: cameraDirection, + allowEdit: allowEdit, presentationStyle: presentationStyle ) } } extension IONCAMRTakePhotoOptions { - struct ThumbnailDefaultConfigurations { + enum ThumbnailDefaultConfigurations { static let quality = 1 static let resolution = 1080 } - static var defaultSquare: IONCAMRSize? { try? .initSquare(with: ThumbnailDefaultConfigurations.resolution) } + static var defaultSquare: IONCAMRSize? { + try? .initSquare(with: ThumbnailDefaultConfigurations.resolution) + } } diff --git a/Sources/IONCameraLib/Protocols/IONCAMREditorDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMREditorDelegate.swift index c10b620..dd74796 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMREditorDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMREditorDelegate.swift @@ -1,10 +1,10 @@ import UIKit protocol IONCAMREditorDelegate: AnyObject { - typealias IONCAMREditorResultsDelegate = IONCAMRResultsDelegate & IONCAMRCancelResultsDelegate + typealias IONCAMREditorResultsDelegate = IONCAMRCancelResultsDelegate & IONCAMRResultsDelegate /// Handles the result of interacting with the editor interface. var delegate: IONCAMREditorResultsDelegate? { get set } - + /// Triggers the user interface that manages the editing a picture feature. /// - Parameters: /// - image: Image to edit. diff --git a/Sources/IONCameraLib/Protocols/IONCAMRFlowDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMRFlowDelegate.swift index 8ea59ab..a8cff60 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMRFlowDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMRFlowDelegate.swift @@ -2,21 +2,22 @@ import UIKit /// Interface that manages and handles all plugin actions flow, whether the operation as one or more steps. protocol IONCAMRFlowDelegate: AnyObject { - /// Handles the result of interacting with the flow interface. This is to be triggered by the result delegates from all the behaviours this object uses. + /// Handles the result of interacting with the flow interface. This is to be triggered by the result delegates from all the behaviours this object + /// uses. var delegate: IONCAMRFlowResultsDelegate? { get set } /// Object responsible for managing the user interface screens and respective flow. var coordinator: IONCAMRCoordinator { get set } - + var temporaryURLArray: [URL] { get set } - + func takePhoto(with options: IONCAMRTakePhotoOptions) func recordVideo(with options: IONCAMRRecordVideoOptions) - + /// Triggers the user interface that manages the editing a picture feature. /// - Parameter image: Image to be edited. func editPhoto(_ image: UIImage) func editPhoto(with options: IONCAMRPhotoEditOptions) func chooseFromGallery(with options: IONCAMRGalleryOptions) - + func cleanTemporaryFiles() } diff --git a/Sources/IONCameraLib/Protocols/IONCAMRFlowResultsDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMRFlowResultsDelegate.swift index 65a7ea8..b9e0146 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMRFlowResultsDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMRFlowResultsDelegate.swift @@ -11,10 +11,10 @@ protocol IONCAMRFlowResultsDelegate: AnyObject { extension IONCAMRFlowResultsDelegate { func didFailed(type: any Encodable.Type, with error: IONCAMRError) { let result: Result = .failure(error) - self.didReturn(result) + didReturn(result) } - + func didSucceed(with result: any Encodable) { - self.didReturn(.success(result)) + didReturn(.success(result)) } } diff --git a/Sources/IONCameraLib/Protocols/IONCAMRGalleryActionDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMRGalleryActionDelegate.swift index c6e2993..b42ea68 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMRGalleryActionDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMRGalleryActionDelegate.swift @@ -4,8 +4,8 @@ public protocol IONCAMRGalleryActionDelegate: AnyObject { func chooseFromGallery(with options: IONCAMRGalleryOptions) } -public extension IONCAMRGalleryActionDelegate { - func choosePicture(_ allowEdit: Bool) { +extension IONCAMRGalleryActionDelegate { + public func choosePicture(_ allowEdit: Bool) { let options = IONCAMRGalleryOptions( mediaType: .picture, allowEdit: allowEdit, @@ -13,6 +13,6 @@ public extension IONCAMRGalleryActionDelegate { andThumbnailAsData: false, returnMetadata: false ) - self.chooseFromGallery(with: options) + chooseFromGallery(with: options) } } diff --git a/Sources/IONCameraLib/Protocols/IONCAMRGalleryDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMRGalleryDelegate.swift index e04d444..58f2d80 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMRGalleryDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMRGalleryDelegate.swift @@ -2,15 +2,15 @@ import UIKit /// Interface that handles storing an image into the device's photo gallery. protocol IONCAMRGalleryDelegate: AnyObject { - typealias IONCAMRGalleryResultsDelegate = IONCAMRResultsDelegate & IONCAMRMultipleResultsDelegate & IONCAMRCancelResultsDelegate + typealias IONCAMRGalleryResultsDelegate = IONCAMRCancelResultsDelegate & IONCAMRMultipleResultsDelegate & IONCAMRResultsDelegate var delegate: IONCAMRGalleryResultsDelegate? { get set } - + /// Save image to the device's photo library. /// - Parameter image: Image to be saved. /// - Returns: `true` if the image was saved successfully, `false` otherwise. func saveToGallery(_ image: UIImage) async -> Bool /// - Returns: `true` if the video was saved successfully, `false` otherwise. func saveToGallery(_ fileURL: URL) async -> Bool - + func chooseFromGallery(with options: IONCAMRGalleryOptions, _ handler: @escaping (UIViewController) -> Void) } diff --git a/Sources/IONCameraLib/Protocols/IONCAMROptionsDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMROptionsDelegate.swift index c4334d3..e7470e7 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMROptionsDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMROptionsDelegate.swift @@ -2,7 +2,7 @@ protocol IONCAMRDefaultOptionsDelegate: AnyObject { var returnMetadata: Bool { get set } } -protocol IONCAMRSaveToGalleryOptionsDelegate: IONCAMRDefaultOptionsDelegate { +protocol IONCAMRSaveToGalleryOptionsDelegate: IONCAMRDefaultOptionsDelegate { var saveToGallery: Bool { get set } } diff --git a/Sources/IONCameraLib/Protocols/IONCAMRPermissionsDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMRPermissionsDelegate.swift index 1060a3b..22f3786 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMRPermissionsDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMRPermissionsDelegate.swift @@ -4,8 +4,9 @@ import UIKit protocol IONCAMRPermissionsDelegate: AnyObject { /// Object responsible for managing the user interface screens and respective flow. var coordinator: IONCAMRCoordinator { get set } - - /// Checks if the device has authorisation to access its camera. On the first time, it requests the user for access, displaying an alert if not granted. + + /// Checks if the device has authorisation to access its camera. On the first time, it requests the user for access, displaying an alert if not + /// granted. /// - Parameter handler: Closure that indicates if permissions were granted or not. func checkForCamera(_ handler: @escaping (Bool) -> Void) func checkForPhotoLibrary(_ handler: @escaping (Bool) -> Void) diff --git a/Sources/IONCameraLib/Protocols/IONCAMRPickerDelegate.swift b/Sources/IONCameraLib/Protocols/IONCAMRPickerDelegate.swift index cc1191f..06907c2 100644 --- a/Sources/IONCameraLib/Protocols/IONCAMRPickerDelegate.swift +++ b/Sources/IONCameraLib/Protocols/IONCAMRPickerDelegate.swift @@ -2,9 +2,9 @@ import UIKit /// Interface that manages and handles the Capture a Picture user interface and interaction. protocol IONCAMRPickerDelegate: AnyObject { - typealias IONCAMRPickerResultsDelegate = IONCAMRResultsDelegate & IONCAMRCancelResultsDelegate + typealias IONCAMRPickerResultsDelegate = IONCAMRCancelResultsDelegate & IONCAMRResultsDelegate var delegate: IONCAMRPickerResultsDelegate? { get set } - + /// Verifies if camera is available for usage. /// - Returns: The camera's availability func isCameraAvailable() -> Bool diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml new file mode 100644 index 0000000..70d53ac --- /dev/null +++ b/Tests/.swiftlint.yml @@ -0,0 +1,7 @@ +disabled_rules: + - implicitly_unwrapped_optional # common XCTest setUp/tearDown pattern + - cyclomatic_complexity # mocks and test helpers are legitimately complex + - force_unwrapping # tests should fail fast on unexpected nils + - type_body_length # test classes are naturally long + - file_length # test files are naturally long + - multiline_arguments # formatting handled by SwiftFormat diff --git a/Tests/IONCameraLibTests/CGSizeSpec.swift b/Tests/IONCameraLibTests/CGSizeSpec.swift index 4c1fe88..0657c85 100644 --- a/Tests/IONCameraLibTests/CGSizeSpec.swift +++ b/Tests/IONCameraLibTests/CGSizeSpec.swift @@ -1,18 +1,18 @@ +@testable import IONCameraLib import Nimble import Quick import UIKit -@testable import IONCameraLib class CGSizeSpec: QuickSpec { override class func spec() { var size: CGSize! - + describe("Given a size set on the picture options") { context("when setting the CGSize") { it("then all properties should match") { let optionsSize = IONCAMRSizeConfigurations.sizeSet! size = CGSize(size: optionsSize) - + expect(size.height).to(equal(CGFloat(optionsSize.height))) expect(size.width).to(equal(CGFloat(optionsSize.width))) } diff --git a/Tests/IONCameraLibTests/Configurations/IONCAMRFlowResultsDelegateMock.swift b/Tests/IONCameraLibTests/Configurations/IONCAMRFlowResultsDelegateMock.swift index dd01381..3153bff 100644 --- a/Tests/IONCameraLibTests/Configurations/IONCAMRFlowResultsDelegateMock.swift +++ b/Tests/IONCameraLibTests/Configurations/IONCAMRFlowResultsDelegateMock.swift @@ -4,26 +4,26 @@ class IONCAMRFlowResultsDelegateMock: IONCAMRFlowResultsDelegate { var resultArray: [IONCAMRMediaResult]? var resultSingle: IONCAMRMediaResult? var error: IONCAMRError? - var wasCancelled: Bool = false + var wasCancelled = false private var continuation: CheckedContinuation? func didReturn(_ result: Result) { switch result { case .success(let value): - self.resultArray = value as? [IONCAMRMediaResult] - self.resultSingle = value as? IONCAMRMediaResult + resultArray = value as? [IONCAMRMediaResult] + resultSingle = value as? IONCAMRMediaResult case .failure(let error): self.error = error } - self.continuation?.resume() - self.continuation = nil + continuation?.resume() + continuation = nil } func didCancel(_ error: IONCAMRError) { - self.wasCancelled = true - self.continuation?.resume() - self.continuation = nil + wasCancelled = true + continuation?.resume() + continuation = nil } func waitForResult() async { diff --git a/Tests/IONCameraLibTests/Configurations/IONCameraLib+Accelerators.swift b/Tests/IONCameraLibTests/Configurations/IONCameraLib+Accelerators.swift index fff0d73..be04685 100644 --- a/Tests/IONCameraLibTests/Configurations/IONCameraLib+Accelerators.swift +++ b/Tests/IONCameraLibTests/Configurations/IONCameraLib+Accelerators.swift @@ -5,10 +5,16 @@ extension IONCAMRFlowBehaviour { let options = IONCAMRGalleryOptions( mediaType: .picture, allowEdit: allowEdit, allowMultipleSelection: false, andThumbnailAsData: false, returnMetadata: false ) - self.chooseFromGallery(with: options) + chooseFromGallery(with: options) } - func chooseMultimedia(type mediaType: IONCAMRMediaType, allowEdit: Bool = false, allowMultipleSelection: Bool, returnMetadata: Bool, andThumbnailAsData: Bool = false) { + func chooseMultimedia( + type mediaType: IONCAMRMediaType, + allowEdit: Bool = false, + allowMultipleSelection: Bool, + returnMetadata: Bool, + andThumbnailAsData: Bool = false + ) { let options = IONCAMRGalleryOptions( mediaType: mediaType, allowEdit: allowEdit, @@ -16,7 +22,7 @@ extension IONCAMRFlowBehaviour { andThumbnailAsData: andThumbnailAsData, returnMetadata: returnMetadata ) - self.chooseFromGallery(with: options) + chooseFromGallery(with: options) } } @@ -29,7 +35,7 @@ extension IONCAMRMediaResult: Equatable { extension IONCAMRMetadata: Equatable { public static func == (lhs: IONCAMRMetadata, rhs: IONCAMRMetadata) -> Bool { lhs.size == rhs.size && lhs.resolution == rhs.resolution - && lhs.format == rhs.format - && lhs.duration == rhs.duration + && lhs.format == rhs.format + && lhs.duration == rhs.duration } } diff --git a/Tests/IONCameraLibTests/Configurations/MockConfigurations.swift b/Tests/IONCameraLibTests/Configurations/MockConfigurations.swift index 5e24663..e908d43 100644 --- a/Tests/IONCameraLibTests/Configurations/MockConfigurations.swift +++ b/Tests/IONCameraLibTests/Configurations/MockConfigurations.swift @@ -1,7 +1,7 @@ -import UIKit @testable import IONCameraLib +import UIKit -struct IONCAMRPictureOptionsConfigurations { +enum IONCAMRPictureOptionsConfigurations { static let jpegEncodingType = try? IONCAMRTakePhotoOptions( quality: 100, correctOrientation: true, @@ -121,11 +121,12 @@ struct IONCAMRPictureOptionsConfigurations { returnMetadata: true ) static let allowEditAndSaveToPhotoAlbum = try? IONCAMRTakePhotoOptions( - quality: 100, correctOrientation: true, encodingType: .jpeg, saveToGallery: true, cameraDirection: .back, allowEdit: true, returnMetadata: false + quality: 100, correctOrientation: true, encodingType: .jpeg, saveToGallery: true, cameraDirection: .back, allowEdit: true, + returnMetadata: false ) } -struct IONCAMRRecordVideoOptionsConfigurations { +enum IONCAMRRecordVideoOptionsConfigurations { static let video = IONCAMRRecordVideoOptions(saveToGallery: false, returnMetadata: false, isPersistent: true) static let saveToPhotosAlbum = IONCAMRRecordVideoOptions(saveToGallery: true, returnMetadata: false, isPersistent: true) static let withMetadata = IONCAMRRecordVideoOptions(saveToGallery: false, returnMetadata: true, isPersistent: true) @@ -133,23 +134,25 @@ struct IONCAMRRecordVideoOptionsConfigurations { static let saveToPhotosAlbumTemporary = IONCAMRRecordVideoOptions(saveToGallery: true, returnMetadata: false, isPersistent: false) } -struct IONCAMREditOptionsConfigurations { +enum IONCAMREditOptionsConfigurations { static func noSaveNorMetadata(uri: String = "") -> IONCAMRPhotoEditOptions { IONCAMRPhotoEditOptions(uri: uri, saveToGallery: false, returnMetadata: false) } + static func saveWithoutMetadata(uri: String = "") -> IONCAMRPhotoEditOptions { IONCAMRPhotoEditOptions(uri: uri, saveToGallery: true, returnMetadata: false) } + static func metadataWithoutSave(uri: String = "") -> IONCAMRPhotoEditOptions { IONCAMRPhotoEditOptions(uri: uri, saveToGallery: false, returnMetadata: true) } } -struct IONCAMRSizeConfigurations { +enum IONCAMRSizeConfigurations { static let sizeSet = try? IONCAMRSize(width: 50, height: 150) } -struct UIViewControllerConfigurations { +enum UIViewControllerConfigurations { static let `default` = UIViewController() static let takeMedia = UIViewController() static let editPicture = UIViewController() @@ -188,11 +191,16 @@ class IONCAMRPictureMock { metadata: IONCAMRMetadataOptions.photoMetadata ) - var toMediaResult: IONCAMRMediaResult { .init(pictureWith: self.url.absoluteString, self.thumbnail) } - var toMediaResultWithMetadata: IONCAMRMediaResult { .init(pictureWith: self.url.absoluteString, self.thumbnail, and: self.metadata) } + var toMediaResult: IONCAMRMediaResult { + .init(pictureWith: url.absoluteString, thumbnail) + } + + var toMediaResultWithMetadata: IONCAMRMediaResult { + .init(pictureWith: url.absoluteString, thumbnail, and: metadata) + } } -struct IONCAMRMetadataOptions { +enum IONCAMRMetadataOptions { static let videoMetadata = IONCAMRMetadata(size: 1200, duration: 20, format: "mp4", resolution: "1920x1080", creationDate: Date()) static let photoMetadata = IONCAMRMetadata(size: 120, format: "jpeg", resolution: "1920x1080", creationDate: Date()) } @@ -213,8 +221,13 @@ struct IONCAMRVideoMock { metadata: IONCAMRMetadataOptions.videoMetadata ) - var toMediaResult: IONCAMRMediaResult { .init(videoWith: self.url.absoluteString, self.thumbnail) } - var toMediaResultWithMetadata: IONCAMRMediaResult { .init(videoWith: self.url.absoluteString, self.thumbnail, and: self.metadata) } + var toMediaResult: IONCAMRMediaResult { + .init(videoWith: url.absoluteString, thumbnail) + } + + var toMediaResultWithMetadata: IONCAMRMediaResult { + .init(videoWith: url.absoluteString, thumbnail, and: metadata) + } } class IONCAMRCallbackMock: IONCAMRCallbackDelegate { @@ -227,24 +240,24 @@ class IONCAMRCallbackMock: IONCAMRCallbackDelegate { } func callback(result: IONCAMRMediaResult) { - self.singleResult = result + singleResult = result } func callback(result: [IONCAMRMediaResult]) { - self.arrayResult = result + arrayResult = result } } class IONCAMRPermissionsBehaviourMock: IONCAMRPermissionsDelegate { var coordinator: IONCAMRCoordinator = IONCAMRCoordinatorMock(rootViewController: UIViewControllerConfigurations.default) - var authorised: Bool = true + var authorised = true func checkForCamera(_ handler: @escaping (Bool) -> Void) { - handler(self.authorised) + handler(authorised) } func checkForPhotoLibrary(_ handler: @escaping (Bool) -> Void) { - handler(self.authorised) + handler(authorised) } } @@ -262,131 +275,129 @@ class IONCAMRFlowBehaviourMock: IONCAMRFlowDelegate { var coordinator: IONCAMRCoordinator = IONCAMRCoordinatorMock(rootViewController: UIViewControllerConfigurations.default) var delegate: IONCAMRFlowResultsDelegate? var temporaryURLArray: [URL] = [] - var triggeredTakePicture: Bool = false - var triggeredCancelTakePicture: Bool = false + var triggeredTakePicture = false + var triggeredCancelTakePicture = false - var triggeredChoosePicture: Bool = false - var triggeredCancelChoosePicture: Bool = false + var triggeredChoosePicture = false + var triggeredCancelChoosePicture = false - var triggeredCaptureVideo: Bool = false - var triggeredCancelVideo: Bool = false + var triggeredCaptureVideo = false + var triggeredCancelVideo = false - var triggeredChooseMultimedia: Bool = false - var triggeredCancelChooseMultimedia: Bool = false + var triggeredChooseMultimedia = false + var triggeredCancelChooseMultimedia = false - var triggeredEdit: Bool = true + var triggeredEdit = true var error: IONCAMRError? func takePhoto(with options: IONCAMRTakePhotoOptions) { - if self.triggeredCancelTakePicture { - self.delegate?.didCancel(.takePictureCancel) - } else if self.triggeredTakePicture { - if let error = error { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + if triggeredCancelTakePicture { + delegate?.didCancel(.takePictureCancel) + } else if triggeredTakePicture { + if let error { + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } else { let mediaResult = IONCAMRMediaResult(pictureWith: IONCAMRPictureMock.osLogo.image.toData(with: options)!.base64EncodedString()) - self.delegate?.didSucceed(with: mediaResult) + delegate?.didSucceed(with: mediaResult) } } } func recordVideo(with options: IONCAMRRecordVideoOptions) { - if self.triggeredCancelVideo { - self.delegate?.didCancel(.captureVideoCancel) - } else if self.triggeredCaptureVideo { - if let error = error { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + if triggeredCancelVideo { + delegate?.didCancel(.captureVideoCancel) + } else if triggeredCaptureVideo { + if let error { + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } else { let video = IONCAMRVideoMock.first - self.temporaryURLArray += [video.url] + temporaryURLArray += [video.url] let mediaResult = options.returnMetadata ? video.toMediaResultWithMetadata : video.toMediaResult - self.delegate?.didSucceed(with: mediaResult) + delegate?.didSucceed(with: mediaResult) } } } func editPhoto(_ image: UIImage) { - if self.triggeredEdit { - if let error = self.error { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + if triggeredEdit { + if let error { + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } else { let mediaResult = IONCAMRMediaResult(pictureWith: IONCAMRPictureMock.osLogoBlue.image.toData()!.base64EncodedString()) - self.delegate?.didSucceed(with: mediaResult) + delegate?.didSucceed(with: mediaResult) } } else { - self.delegate?.didCancel(.editPictureCancel) + delegate?.didCancel(.editPictureCancel) } } func editPhoto(with options: IONCAMRPhotoEditOptions) { - if self.triggeredEdit { - if let error = self.error { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + if triggeredEdit { + if let error { + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } else { let pictureToReturn = IONCAMRPictureMock.osLogoBlue - self.delegate?.didSucceed(with: options.returnMetadata ? pictureToReturn.toMediaResultWithMetadata : pictureToReturn.toMediaResult) + delegate?.didSucceed(with: options.returnMetadata ? pictureToReturn.toMediaResultWithMetadata : pictureToReturn.toMediaResult) } } else { - self.delegate?.didCancel(.editPictureCancel) + delegate?.didCancel(.editPictureCancel) } } func chooseFromGallery(with options: IONCAMRGalleryOptions) { - if self.triggeredCancelChoosePicture { - self.delegate?.didCancel(.choosePictureCancel) - } else if self.triggeredCancelChooseMultimedia { - self.delegate?.didCancel(.chooseMultimediaCancel) - } else if self.triggeredChoosePicture { - if let error = self.error { - self.delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) + if triggeredCancelChoosePicture { + delegate?.didCancel(.choosePictureCancel) + } else if triggeredCancelChooseMultimedia { + delegate?.didCancel(.chooseMultimediaCancel) + } else if triggeredChoosePicture { + if let error { + delegate?.didFailed(type: IONCAMRMediaResult.self, with: error) } else { let mediaResult = IONCAMRMediaResult(pictureWith: IONCAMRPictureMock.osLogo.image.toData()!.base64EncodedString()) - self.delegate?.didSucceed(with: mediaResult) + delegate?.didSucceed(with: mediaResult) } - } else if self.triggeredChooseMultimedia { - if let error = self.error { - self.delegate?.didFailed(type: [IONCAMRMediaResult].self, with: error) + } else if triggeredChooseMultimedia { + if let error { + delegate?.didFailed(type: [IONCAMRMediaResult].self, with: error) } else { - let mediaArray: [IONCAMRMediaResult] - switch options.mediaType { + let mediaArray: [IONCAMRMediaResult] = switch options.mediaType { case .picture: if options.returnMetadata { - mediaArray = [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRPictureMock.osLogoRotated.toMediaResultWithMetadata] + [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRPictureMock.osLogoRotated.toMediaResultWithMetadata] } else { - mediaArray = [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRPictureMock.osLogoRotated.toMediaResult] + [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRPictureMock.osLogoRotated.toMediaResult] } case .video: if options.returnMetadata { - mediaArray = [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata] + [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata] } else { - mediaArray = [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata] + [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata] } case .both: if options.returnMetadata { - mediaArray = [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRVideoMock.first.toMediaResultWithMetadata] + [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRVideoMock.first.toMediaResultWithMetadata] } else { - mediaArray = [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRVideoMock.first.toMediaResult] + [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRVideoMock.first.toMediaResult] } - - default: mediaArray = [] + default: [] } - self.delegate?.didSucceed(with: mediaArray) + delegate?.didSucceed(with: mediaArray) } } } func cleanTemporaryFiles() { - self.temporaryURLArray.removeAll() + temporaryURLArray.removeAll() } } class IONCAMRPickerBehaviourMock: IONCAMRPickerDelegate { weak var delegate: IONCAMRPickerResultsDelegate? - var cameraAvailable: Bool = true + var cameraAvailable = true func isCameraAvailable() -> Bool { - self.cameraAvailable + cameraAvailable } func captureMedia(with options: IONCAMRMediaOptions, _ handler: @escaping (UIViewController) -> Void) { @@ -394,188 +405,185 @@ class IONCAMRPickerBehaviourMock: IONCAMRPickerDelegate { } // MARK: Take Picture Handlers + func takePictureHandler(with success: Bool = true, and error: IONCAMRError? = nil) { if success { - if let error = error { - self.delegate?.didReturn(self, with: .failure(error)) + if let error { + delegate?.didReturn(self, with: .failure(error)) } else { - self.delegate?.didReturn(self, with: .success(.picture(IONCAMRPictureMock.osLogo.image))) + delegate?.didReturn(self, with: .success(.picture(IONCAMRPictureMock.osLogo.image))) } } else { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } func didCancelTakePicture() { - self.takePictureHandler(with: false, and: nil) + takePictureHandler(with: false, and: nil) } func didEndTakePictureHandler(with error: IONCAMRError) { - self.takePictureHandler(with: true, and: error) + takePictureHandler(with: true, and: error) } func didEndSuccessfullyTakePictureHandler() { - self.takePictureHandler(with: true, and: nil) + takePictureHandler(with: true, and: nil) } // MARK: Capture Video Handlers + func captureVideoHandler(with success: Bool = true, and error: IONCAMRError? = nil) { if success { - if let error = error { - self.delegate?.didReturn(self, with: .failure(error)) + if let error { + delegate?.didReturn(self, with: .failure(error)) } else { - self.delegate?.didReturn(self, with: .success(.video(IONCAMRVideoMock.first.url))) + delegate?.didReturn(self, with: .success(.video(IONCAMRVideoMock.first.url))) } } else { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } func didCancelCaptureVideo() { - self.captureVideoHandler(with: false, and: nil) + captureVideoHandler(with: false, and: nil) } func didEndCaptureVideoHandler(with error: IONCAMRError) { - self.captureVideoHandler(with: true, and: error) + captureVideoHandler(with: true, and: error) } func didEndSuccessfullyCaptureVideoHandler() { - self.captureVideoHandler(with: true, and: nil) + captureVideoHandler(with: true, and: nil) } } class IONCAMRGalleryBehaviourMock: IONCAMRGalleryDelegate { func saveToGallery(_ image: UIImage) async -> Bool { - self.isSaved = true + isSaved = true return true } - + func saveToGallery(_ fileURL: URL) async -> Bool { - self.isSaved = true + isSaved = true return true } - + weak var delegate: IONCAMRGalleryResultsDelegate? - var pleaseSave: Bool = true - var isSaved: Bool = false - var photoLibraryAvailable: Bool = true - var returnMetadata: Bool = false + var pleaseSave = true + var isSaved = false + var photoLibraryAvailable = true + var returnMetadata = false func saveToGallery(_ image: UIImage) { - self.isSaved = true + isSaved = true } func saveToGallery(_ fileURL: URL) { - self.isSaved = true + isSaved = true } func chooseFromGallery(with options: IONCAMRGalleryOptions, _ handler: @escaping (UIViewController) -> Void) { - self.returnMetadata = options.returnMetadata + returnMetadata = options.returnMetadata handler(UIViewControllerConfigurations.chooseFromGallery) } private func choosePictureHandler(with success: Bool, and error: IONCAMRError?) { if success { - if let error = error { - self.delegate?.didReturn(self, with: .failure(error)) + if let error { + delegate?.didReturn(self, with: .failure(error)) } else { - self.delegate?.didReturn(self, with: .success(.picture(IONCAMRPictureMock.osLogo.image))) + delegate?.didReturn(self, with: .success(.picture(IONCAMRPictureMock.osLogo.image))) } } else { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } func didCancelChoosePicture() { - self.choosePictureHandler(with: false, and: nil) + choosePictureHandler(with: false, and: nil) } func didEndChoosePictureHandler(with error: IONCAMRError) { - self.choosePictureHandler(with: true, and: error) + choosePictureHandler(with: true, and: error) } func didEndSuccessfullyChoosePictureHandler() { - self.choosePictureHandler(with: true, and: nil) + choosePictureHandler(with: true, and: nil) } private func chooseMultimediaHandler(withSuccess success: Bool, error: IONCAMRError?, andResult result: [IONCAMRMediaResult]?) { if success { - if let error = error { - self.delegate?.didReturn(.failure(error)) - } else if let result = result { - self.delegate?.didReturn(.success(result)) + if let error { + delegate?.didReturn(.failure(error)) + } else if let result { + delegate?.didReturn(.success(result)) } } else { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } func didCancelChooseMultimedia() { - self.chooseMultimediaHandler(withSuccess: false, error: nil, andResult: nil) + chooseMultimediaHandler(withSuccess: false, error: nil, andResult: nil) } func didEndChooseMultimediaHandler(with error: IONCAMRError) { - self.chooseMultimediaHandler(withSuccess: true, error: error, andResult: nil) + chooseMultimediaHandler(withSuccess: true, error: error, andResult: nil) } func didEndSuccessfullyChooseSinglePictureHandler() { - let result: [IONCAMRMediaResult] - if self.returnMetadata { - result = [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata] + let result: [IONCAMRMediaResult] = if returnMetadata { + [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata] } else { - result = [IONCAMRPictureMock.osLogo.toMediaResult] + [IONCAMRPictureMock.osLogo.toMediaResult] } - self.chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) + chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) } func didEndSuccessfullyChooseMultiplePicturesHandler() { - let result: [IONCAMRMediaResult] - if self.returnMetadata { - result = [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRPictureMock.osLogoRotated.toMediaResultWithMetadata] + let result: [IONCAMRMediaResult] = if returnMetadata { + [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRPictureMock.osLogoRotated.toMediaResultWithMetadata] } else { - result = [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRPictureMock.osLogoRotated.toMediaResult] + [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRPictureMock.osLogoRotated.toMediaResult] } - self.chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) + chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) } func didEndSuccessfullyChooseSingleVideoHandler() { - let result: [IONCAMRMediaResult] - if self.returnMetadata { - result = [IONCAMRVideoMock.first.toMediaResultWithMetadata] + let result: [IONCAMRMediaResult] = if returnMetadata { + [IONCAMRVideoMock.first.toMediaResultWithMetadata] } else { - result = [IONCAMRVideoMock.first.toMediaResult] + [IONCAMRVideoMock.first.toMediaResult] } - self.chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) + chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) } func didEndSuccessfullyChooseMultipleVideosHandler() { - let result: [IONCAMRMediaResult] - if self.returnMetadata { - result = [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata] + let result: [IONCAMRMediaResult] = if returnMetadata { + [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata] } else { - result = [IONCAMRVideoMock.first.toMediaResult, IONCAMRVideoMock.second.toMediaResult] + [IONCAMRVideoMock.first.toMediaResult, IONCAMRVideoMock.second.toMediaResult] } - self.chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) + chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) } func didEndSuccessfullyChoosePictureAndVideoHandler() { - let result: [IONCAMRMediaResult] - if self.returnMetadata { - result = [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRVideoMock.first.toMediaResultWithMetadata] + let result: [IONCAMRMediaResult] = if returnMetadata { + [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRVideoMock.first.toMediaResultWithMetadata] } else { - result = [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRVideoMock.first.toMediaResult] + [IONCAMRPictureMock.osLogo.toMediaResult, IONCAMRVideoMock.first.toMediaResult] } - self.chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) + chooseMultimediaHandler(withSuccess: true, error: nil, andResult: result) } func didEndSuccessfullyWithNoPicturesSelectedHandler() { - self.chooseMultimediaHandler(withSuccess: true, error: nil, andResult: []) + chooseMultimediaHandler(withSuccess: true, error: nil, andResult: []) } } class IONCAMREditorBehaviourMock: IONCAMREditorDelegate { weak var delegate: IONCAMREditorResultsDelegate? - var hasBeenEdited: Bool = false + var hasBeenEdited = false func editPicture(_ image: UIImage, _ handler: @escaping (UIViewController) -> Void) { hasBeenEdited = true @@ -584,33 +592,36 @@ class IONCAMREditorBehaviourMock: IONCAMREditorDelegate { func editPictureHandler(with success: Bool, and error: IONCAMRError?) { if success { - if let error = error { - self.delegate?.didReturn(self, with: .failure(error)) + if let error { + delegate?.didReturn(self, with: .failure(error)) } else { - self.delegate?.didReturn(self, with: .success(.picture(IONCAMRPictureMock.osLogoBlue.image))) + delegate?.didReturn(self, with: .success(.picture(IONCAMRPictureMock.osLogoBlue.image))) } } else { - self.delegate?.didCancel(self) + delegate?.didCancel(self) } } func didCancelEditPicture() { - self.editPictureHandler(with: false, and: nil) + editPictureHandler(with: false, and: nil) } func didEndEditPictureHandler(with error: IONCAMRError) { - self.editPictureHandler(with: true, and: error) + editPictureHandler(with: true, and: error) } func didEndSuccessfullyEditPictureHandler() { - self.editPictureHandler(with: true, and: nil) + editPictureHandler(with: true, and: nil) } } class IONCAMRCoordinatorMock: IONCAMRCoordinator { - var hasTwoSteps: Bool = false + var hasTwoSteps = false + + override var isSecondStep: Bool { + hasTwoSteps + } - override var isSecondStep: Bool { self.hasTwoSteps } override func present(_ viewController: UIViewController) {} override func dismiss() {} } @@ -619,7 +630,7 @@ class MirrorObject { let mirror: Mirror init(reflecting: Any) { - mirror = Mirror(reflecting: reflecting) + self.mirror = Mirror(reflecting: reflecting) } func extract(variableName: StaticString = #function) -> T? { @@ -627,7 +638,7 @@ class MirrorObject { } private func extract(variableName: StaticString, mirror: Mirror?) -> T? { - guard let mirror = mirror else { + guard let mirror else { return nil } @@ -653,7 +664,7 @@ final class IONCAMRCoordinatorMirror: MirrorObject { } var screenViewController: UIViewController? { - self.currentlyPresentedViewControllerArray?.last ?? self.rootViewController + currentlyPresentedViewControllerArray?.last ?? rootViewController } } @@ -667,8 +678,13 @@ final class IONCAMRThumbnailGeneratorMock: IONCAMRThumbnailGeneratorDelegate { ] } - func getImage(from videoURL: URL, _ completion: @escaping (UIImage?) -> Void) { completion(thumbnail) } - func getBase64String(from image: UIImage, with size: IONCAMRSize?, and quality: Int?) -> String? { imageMap[image] } + func getImage(from videoURL: URL, _ completion: @escaping (UIImage?) -> Void) { + completion(thumbnail) + } + + func getBase64String(from image: UIImage, with size: IONCAMRSize?, and quality: Int?) -> String? { + imageMap[image] + } } final class IONCAMRPlayerBehaviourMock: IONCAMRPlayerDelegate { @@ -679,7 +695,7 @@ final class IONCAMRPlayerBehaviourMock: IONCAMRPlayerDelegate { var isVideoPlayable = true func playVideo(_ url: URL) async throws { - if self.isVideoPlayable { + if isVideoPlayable { print("Video playing...") } else { throw MockBehaviourError.videoCantBePlayed @@ -688,7 +704,7 @@ final class IONCAMRPlayerBehaviourMock: IONCAMRPlayerDelegate { } final class IONCAMRImageFetcherBehaviourMock: IONCAMRImageFetcherDelegate { - var callShouldSucceed: Bool = true + var callShouldSucceed = true var urlMap: [String: UIImage] { [ IONCAMRPictureMock.osLogo.url.absoluteString: IONCAMRPictureMock.osLogo.image, @@ -697,7 +713,9 @@ final class IONCAMRImageFetcherBehaviourMock: IONCAMRImageFetcherDelegate { ] } - func retrieveImage(from urlString: String) -> UIImage? { callShouldSucceed ? urlMap[urlString] : nil } + func retrieveImage(from urlString: String) -> UIImage? { + callShouldSucceed ? urlMap[urlString] : nil + } } final class IONCAMRURLGeneratorMock: IONCAMRURLGeneratorDelegate { diff --git a/Tests/IONCameraLibTests/IONCAMRCameraTests.swift b/Tests/IONCameraLibTests/IONCAMRCameraTests.swift index 8f14b95..8892f15 100644 --- a/Tests/IONCameraLibTests/IONCAMRCameraTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRCameraTests.swift @@ -1,5 +1,5 @@ -import XCTest @testable import IONCameraLib +import XCTest extension IONCAMRMediaResult: Decodable { public init(from decoder: Decoder) throws { @@ -64,28 +64,28 @@ final class IONCAMRCameraTests: XCTestCase { // MARK: - Take Picture Tests - func test_whenUserPressesTakePictureButton_andCancels_returnError() { + func test_whenUserPressesTakePictureButton_andCancels_returnError() throws { mockFlow.triggeredCancelTakePicture = true - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.jpegEncodingType!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.jpegEncodingType)) XCTAssertNil(mockDelegate.singleResult) XCTAssertEqual(mockDelegate.error, IONCAMRError.takePictureCancel) } - func test_whenUserPressesTakePictureButton_andSomethingWrongHappens_returnError() { + func test_whenUserPressesTakePictureButton_andSomethingWrongHappens_returnError() throws { mockFlow.triggeredTakePicture = true mockFlow.error = .takePictureIssue - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.jpegEncodingType!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.jpegEncodingType)) XCTAssertNil(mockDelegate.singleResult) XCTAssertEqual(mockDelegate.error, mockFlow.error) } - func test_whenUserPressesTakePictureButton_withSuccess_returnJPEGsBase64String() { + func test_whenUserPressesTakePictureButton_withSuccess_returnJPEGsBase64String() throws { mockFlow.triggeredTakePicture = true - let pictureOptions = IONCAMRPictureOptionsConfigurations.jpegEncodingType! + let pictureOptions = try XCTUnwrap(IONCAMRPictureOptionsConfigurations.jpegEncodingType) sut.takePhoto(with: pictureOptions) @@ -93,9 +93,9 @@ final class IONCAMRCameraTests: XCTestCase { XCTAssertNil(mockDelegate.error) } - func test_whenUserPressesTakePictureButton_withSuccess_returnPNGsBase64String() { + func test_whenUserPressesTakePictureButton_withSuccess_returnPNGsBase64String() throws { mockFlow.triggeredTakePicture = true - let pictureOptions = IONCAMRPictureOptionsConfigurations.pngEncodingType! + let pictureOptions = try XCTUnwrap(IONCAMRPictureOptionsConfigurations.pngEncodingType) sut.takePhoto(with: pictureOptions) @@ -249,7 +249,13 @@ final class IONCAMRCameraTests: XCTestCase { func test_whenUserPressesChooseMultimediaButton_andCancels_returnError() { mockFlow.triggeredCancelChooseMultimedia = true - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .both, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: false)) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .both, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: false + )) XCTAssertNil(mockDelegate.arrayResult) XCTAssertEqual(mockDelegate.error, IONCAMRError.chooseMultimediaCancel) @@ -259,7 +265,13 @@ final class IONCAMRCameraTests: XCTestCase { mockFlow.triggeredChooseMultimedia = true mockFlow.error = .chooseMultimediaIssue - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .both, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: false)) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .both, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: false + )) XCTAssertNil(mockDelegate.arrayResult) XCTAssertEqual(mockDelegate.error, mockFlow.error) @@ -268,7 +280,13 @@ final class IONCAMRCameraTests: XCTestCase { func test_whenUserPressesChooseMultimediaButton_withSuccess_andBothFilesArePictures_returnPictures() { mockFlow.triggeredChooseMultimedia = true - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .picture, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: false)) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .picture, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: false + )) XCTAssertEqual(mockDelegate.arrayResult?.isEmpty, false) XCTAssertNil(mockDelegate.error) @@ -277,16 +295,31 @@ final class IONCAMRCameraTests: XCTestCase { func test_whenUserPressesChooseMultimediaButton_withSuccess_andBothFilesArePicture_andReturnMetadaIsTrue_returnPicturesWithMetadata() { mockFlow.triggeredChooseMultimedia = true - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .picture, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: true)) - - XCTAssertEqual(mockDelegate.arrayResult, [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRPictureMock.osLogoRotated.toMediaResultWithMetadata]) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .picture, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: true + )) + + XCTAssertEqual( + mockDelegate.arrayResult, + [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRPictureMock.osLogoRotated.toMediaResultWithMetadata] + ) XCTAssertNil(mockDelegate.error) } func test_whenUserPressesChooseMultimediaButton_withSuccess_andBothFilesAreVideo_returnVideos() { mockFlow.triggeredChooseMultimedia = true - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .video, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: false)) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .video, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: false + )) XCTAssertEqual(mockDelegate.arrayResult?.isEmpty, false) XCTAssertNil(mockDelegate.error) @@ -295,27 +328,52 @@ final class IONCAMRCameraTests: XCTestCase { func test_whenUserPressesChooseMultimediaButton_withSuccess_andBothFilesAreVideo_andReturnMetadataIsTrue_returnVideosWithMetadata() { mockFlow.triggeredChooseMultimedia = true - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .video, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: true)) - - XCTAssertEqual(mockDelegate.arrayResult, [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata]) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .video, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: true + )) + + XCTAssertEqual( + mockDelegate.arrayResult, + [IONCAMRVideoMock.first.toMediaResultWithMetadata, IONCAMRVideoMock.second.toMediaResultWithMetadata] + ) XCTAssertNil(mockDelegate.error) } func test_whenUserPressesChooseMultimediaButton_withSuccess_andFilesArePictureAndVideo_returnPictureAndVideo() { mockFlow.triggeredChooseMultimedia = true - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .both, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: false)) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .both, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: false + )) XCTAssertEqual(mockDelegate.arrayResult?.isEmpty, false) XCTAssertNil(mockDelegate.error) } - func test_whenUserPressesChooseMultimediaButton_withSuccess_andFilesArePictureAndVideo_andReturnMetadataIsTrue_returnPictureAndVideoWithMetadata() { + func test_whenUserPressesChooseMultimediaButton_withSuccess_andFilesArePictureAndVideo_andReturnMetadataIsTrue_returnPictureAndVideoWithMetadata( + ) { mockFlow.triggeredChooseMultimedia = true - gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions(mediaType: .both, allowEdit: true, allowMultipleSelection: true, andThumbnailAsData: false, returnMetadata: true)) - - XCTAssertEqual(mockDelegate.arrayResult, [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRVideoMock.first.toMediaResultWithMetadata]) + gallerySut.chooseFromGallery(with: IONCAMRGalleryOptions( + mediaType: .both, + allowEdit: true, + allowMultipleSelection: true, + andThumbnailAsData: false, + returnMetadata: true + )) + + XCTAssertEqual( + mockDelegate.arrayResult, + [IONCAMRPictureMock.osLogo.toMediaResultWithMetadata, IONCAMRVideoMock.first.toMediaResultWithMetadata] + ) XCTAssertNil(mockDelegate.error) } @@ -324,7 +382,7 @@ final class IONCAMRCameraTests: XCTestCase { func test_whenUserPressesPlayVideoButton_butVideoCantBePlayed_returnError() async { mockVideoPlayer.isVideoPlayable = false - await assertThrowsAsyncError(try await videoSut.playVideo(IONCAMRVideoMock.first.url)) + try await assertThrowsAsyncError(videoSut.playVideo(IONCAMRVideoMock.first.url)) } func test_whenUserPressesPlayVideoButton_withSuccess_videoIsPlayed() async throws { @@ -334,9 +392,9 @@ final class IONCAMRCameraTests: XCTestCase { } } -private extension IONCAMRCameraTests { - func assertThrowsAsyncError( - _ expression: @autoclosure () async throws -> T, +extension IONCAMRCameraTests { + private func assertThrowsAsyncError( + _ expression: @autoclosure () async throws -> some Any, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, diff --git a/Tests/IONCameraLibTests/IONCAMRCoordinatorTests.swift b/Tests/IONCameraLibTests/IONCAMRCoordinatorTests.swift index 379b391..dfc0be4 100644 --- a/Tests/IONCameraLibTests/IONCAMRCoordinatorTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRCoordinatorTests.swift @@ -1,5 +1,5 @@ -import XCTest @testable import IONCameraLib +import XCTest final class IONCAMRCoordinatorTests: XCTestCase { private var rootViewController: UIViewController! diff --git a/Tests/IONCameraLibTests/IONCAMRFactoryTests.swift b/Tests/IONCameraLibTests/IONCAMRFactoryTests.swift index 98b3d2d..8e6281c 100644 --- a/Tests/IONCameraLibTests/IONCAMRFactoryTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRFactoryTests.swift @@ -1,5 +1,5 @@ -import XCTest @testable import IONCameraLib +import XCTest final class IONCAMRFactoryTests: XCTestCase { func test_whenCreateWrapperIsTriggered_CreatesIONCAMRCameraObject() { diff --git a/Tests/IONCameraLibTests/IONCAMRFlowChooseFromGalleryTests.swift b/Tests/IONCameraLibTests/IONCAMRFlowChooseFromGalleryTests.swift index e227179..f57f389 100644 --- a/Tests/IONCameraLibTests/IONCAMRFlowChooseFromGalleryTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRFlowChooseFromGalleryTests.swift @@ -1,5 +1,5 @@ -import XCTest @testable import IONCameraLib +import XCTest /// This was introduced on May 18th 2023 on the scope of https://outsystemsrd.atlassian.net/browse/RMET-2494. /// Despite the name, it doesn't contain the whole `Choose from Gallery` client action tests @@ -30,7 +30,8 @@ final class IONCAMRFlowChooseFromGalleryTests: XCTestCase { metadataGetter: IONCAMRMetadataGetterMock(), imageFetcher: imageFetcher, urlGenerator: urlGenerator, - coordinator: coordinator) + coordinator: coordinator + ) sut.delegate = resultsDelegate coordinator.hasTwoSteps = true @@ -68,7 +69,8 @@ final class IONCAMRFlowChooseFromGalleryTests: XCTestCase { XCTAssertNil(resultsDelegate.error) } - func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_whenNoPictureIsReturned_returnError() { + func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_whenNoPictureIsReturned_returnError( + ) { sut.chooseMultimedia( type: .picture, allowEdit: true, allowMultipleSelection: false, returnMetadata: false, andThumbnailAsData: true ) @@ -79,7 +81,8 @@ final class IONCAMRFlowChooseFromGalleryTests: XCTestCase { XCTAssertEqual(resultsDelegate.error, .fetchImageFromURLFailed) } - func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_whenMediaResultURIDoesntContainAnImage_returnError() { + func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_whenMediaResultURIDoesntContainAnImage_returnError( + ) { sut.chooseMultimedia( type: .picture, allowEdit: true, allowMultipleSelection: false, returnMetadata: false, andThumbnailAsData: true ) @@ -91,7 +94,8 @@ final class IONCAMRFlowChooseFromGalleryTests: XCTestCase { XCTAssertEqual(resultsDelegate.error, .fetchImageFromURLFailed) } - func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_returnEditedPicture() async { + func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_returnEditedPicture( + ) async throws { sut.chooseMultimedia( type: .picture, allowEdit: true, allowMultipleSelection: false, returnMetadata: false, andThumbnailAsData: true ) @@ -104,10 +108,11 @@ final class IONCAMRFlowChooseFromGalleryTests: XCTestCase { XCTAssertTrue(editorBehaviour.hasBeenEdited) XCTAssertNil(resultsDelegate.error) XCTAssertEqual(resultsDelegate.resultArray, [IONCAMRPictureMock.osLogoBlue.toMediaResult]) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [resultsDelegate.resultArray!.first!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(resultsDelegate.resultArray?.first?.uri)]) } - func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_andReturnMetadataSetToTrue_returnEditedPicture() async { + func test_chooseFromGallery_withAllowEditSetToTrue_whenMediaTypeSetToPicture_andAllowMultipleSelectionSetToFalse_andReturnMetadataSetToTrue_returnEditedPicture( + ) async throws { urlGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url sut.chooseMultimedia( @@ -120,6 +125,6 @@ final class IONCAMRFlowChooseFromGalleryTests: XCTestCase { XCTAssertTrue(editorBehaviour.hasBeenEdited) XCTAssertNil(resultsDelegate.error) XCTAssertEqual(resultsDelegate.resultArray, [IONCAMRPictureMock.osLogoBlue.toMediaResultWithMetadata]) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [resultsDelegate.resultArray!.first!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(resultsDelegate.resultArray?.first?.uri)]) } } diff --git a/Tests/IONCameraLibTests/IONCAMRFlowEditPictureTests.swift b/Tests/IONCameraLibTests/IONCAMRFlowEditPictureTests.swift index 9c91413..e8cb9b7 100644 --- a/Tests/IONCameraLibTests/IONCAMRFlowEditPictureTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRFlowEditPictureTests.swift @@ -1,5 +1,5 @@ -import XCTest @testable import IONCameraLib +import XCTest final class IONCAMRFlowEditPictureTests: XCTestCase { private var resultsDelegate: IONCAMRFlowResultsDelegateMock! @@ -50,7 +50,7 @@ final class IONCAMRFlowEditPictureTests: XCTestCase { XCTAssertEqual(resultsDelegate.error, IONCAMRError.fetchImageFromURLFailed) } - func test_editPictureOnAURL_whenSaveToGallerySetToFalse_andReturnMetadataSetToFalse_whenSuccessful_returnEditedAsset() async { + func test_editPictureOnAURL_whenSaveToGallerySetToFalse_andReturnMetadataSetToFalse_whenSuccessful_returnEditedAsset() async throws { urlGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url sut.editPhoto(with: IONCAMREditOptionsConfigurations.noSaveNorMetadata(uri: IONCAMRPictureMock.osLogo.url.absoluteString)) @@ -60,11 +60,11 @@ final class IONCAMRFlowEditPictureTests: XCTestCase { XCTAssertNil(resultsDelegate.error) XCTAssertEqual(resultsDelegate.resultSingle, IONCAMRPictureMock.osLogoBlue.toMediaResult) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [resultsDelegate.resultSingle!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(resultsDelegate.resultSingle?.uri)]) XCTAssertFalse(galleryBehaviour.isSaved) } - func test_editPictureOnAURL_whenSaveToGallerySetToTrue_whenSuccessful_returnEditedAsset() async { + func test_editPictureOnAURL_whenSaveToGallerySetToTrue_whenSuccessful_returnEditedAsset() async throws { urlGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url sut.editPhoto(with: IONCAMREditOptionsConfigurations.saveWithoutMetadata(uri: IONCAMRPictureMock.osLogo.url.absoluteString)) @@ -74,11 +74,11 @@ final class IONCAMRFlowEditPictureTests: XCTestCase { XCTAssertNil(resultsDelegate.error) XCTAssertEqual(resultsDelegate.resultSingle, IONCAMRPictureMock.osLogoBlue.toMediaResult) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [resultsDelegate.resultSingle!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(resultsDelegate.resultSingle?.uri)]) XCTAssertTrue(galleryBehaviour.isSaved) } - func test_editPictureOnAURL_whenReturnMetadataSetToTrue_whenSuccessful_returnEditedAsset() async { + func test_editPictureOnAURL_whenReturnMetadataSetToTrue_whenSuccessful_returnEditedAsset() async throws { urlGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url sut.editPhoto(with: IONCAMREditOptionsConfigurations.metadataWithoutSave(uri: IONCAMRPictureMock.osLogo.url.absoluteString)) @@ -88,7 +88,7 @@ final class IONCAMRFlowEditPictureTests: XCTestCase { XCTAssertNil(resultsDelegate.error) XCTAssertEqual(resultsDelegate.resultSingle, IONCAMRPictureMock.osLogoBlue.toMediaResultWithMetadata) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [resultsDelegate.resultSingle!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(resultsDelegate.resultSingle?.uri)]) XCTAssertFalse(galleryBehaviour.isSaved) } } diff --git a/Tests/IONCameraLibTests/IONCAMRFlowTests.swift b/Tests/IONCameraLibTests/IONCAMRFlowTests.swift index 21e4627..c4a6a2e 100644 --- a/Tests/IONCameraLibTests/IONCAMRFlowTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRFlowTests.swift @@ -1,11 +1,11 @@ -import XCTest @testable import IONCameraLib +import XCTest final class IONCAMRFlowTests: XCTestCase { private var singleResult: IONCAMRMediaResult? private var multipleResult: [IONCAMRMediaResult]? private var error: IONCAMRError? - private var cancel: Bool = false + private var cancel = false private var mockPicker: IONCAMRPickerBehaviourMock! private var mockMetadata: IONCAMRMetadataGetterMock! @@ -66,37 +66,38 @@ final class IONCAMRFlowTests: XCTestCase { } // MARK: - Take Picture Tests + extension IONCAMRFlowTests { - func test_takePicture_butNoCameraAvailable_returnError() { + func test_takePicture_butNoCameraAvailable_returnError() throws { mockPicker.cameraAvailable = false - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.jpegEncodingType!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.jpegEncodingType)) XCTAssertNil(singleResult) XCTAssertEqual(error, .cameraAvailability) } - func test_takePicture_butNoCameraAccess_returnError() { + func test_takePicture_butNoCameraAccess_returnError() throws { mockPermissions.authorised = false - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.jpegEncodingType!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.jpegEncodingType)) XCTAssertNil(singleResult) XCTAssertEqual(error, .cameraAccess) } - func test_takePicture_butErrorOccurred_returnError() { + func test_takePicture_butErrorOccurred_returnError() throws { let pickerError = IONCAMRError.takePictureIssue - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.allowEdit!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.allowEdit)) mockPicker.didEndTakePictureHandler(with: pickerError) XCTAssertNil(singleResult) XCTAssertEqual(error, pickerError) } - func test_takePicture_butCancels_delegatesToDidCancel() { - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.jpegEncodingType!) + func test_takePicture_butCancels_delegatesToDidCancel() throws { + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.jpegEncodingType)) mockPicker.didCancelTakePicture() XCTAssertNil(singleResult) @@ -104,10 +105,10 @@ extension IONCAMRFlowTests { XCTAssertTrue(cancel) } - func test_takePicture_withAllowEditEnabled_butErrorOccurred_returnError() { + func test_takePicture_withAllowEditEnabled_butErrorOccurred_returnError() throws { let editorError = IONCAMRError.editPictureIssue - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.allowEdit!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.allowEdit)) mockPicker.didEndSuccessfullyTakePictureHandler() mockEditor.didEndEditPictureHandler(with: editorError) @@ -115,8 +116,8 @@ extension IONCAMRFlowTests { XCTAssertEqual(error, editorError) } - func test_takePicture_withAllowEditEnabled_butCancels_delegatesToDidCancel() { - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.allowEdit!) + func test_takePicture_withAllowEditEnabled_butCancels_delegatesToDidCancel() throws { + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.allowEdit)) mockPicker.didEndSuccessfullyTakePictureHandler() mockEditor.didCancelEditPicture() @@ -125,11 +126,11 @@ extension IONCAMRFlowTests { XCTAssertTrue(cancel) } - func test_takePicture_oldVersion_withAllowEditEnabled_isSuccessful_returnEditedImage() async { + func test_takePicture_oldVersion_withAllowEditEnabled_isSuccessful_returnEditedImage() async throws { mockCoordinator.hasTwoSteps = true mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.allowEdit!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.allowEdit)) mockPicker.didEndSuccessfullyTakePictureHandler() mockEditor.didEndSuccessfullyEditPictureHandler() await waitForResult() @@ -137,14 +138,14 @@ extension IONCAMRFlowTests { XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogoBlue.toMediaResult) XCTAssertNil(error) XCTAssertFalse(mockGallery.isSaved) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } - func test_takePicture_oldVersion_withAllowEditEnabled_isSuccessful_andSavedIntoPhotoLibrary_returnEditedImage() async { + func test_takePicture_oldVersion_withAllowEditEnabled_isSuccessful_andSavedIntoPhotoLibrary_returnEditedImage() async throws { mockCoordinator.hasTwoSteps = true mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.allowEditAndSaveToPhotoAlbum!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.allowEditAndSaveToPhotoAlbum)) mockPicker.didEndSuccessfullyTakePictureHandler() mockEditor.didEndSuccessfullyEditPictureHandler() await waitForResult() @@ -152,11 +153,11 @@ extension IONCAMRFlowTests { XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogoBlue.toMediaResult) XCTAssertNil(error) XCTAssertTrue(mockGallery.isSaved) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } - func test_takePicture_newVersion_withAllowEditEnabled_isSuccessful_returnEditedImage() async { - let options = IONCAMRPictureOptionsConfigurations.allowEditLatestVersion! + func test_takePicture_newVersion_withAllowEditEnabled_isSuccessful_returnEditedImage() async throws { + let options = try XCTUnwrap(IONCAMRPictureOptionsConfigurations.allowEditLatestVersion) mockCoordinator.hasTwoSteps = true mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url @@ -167,11 +168,11 @@ extension IONCAMRFlowTests { XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogoBlue.toMediaResult) XCTAssertNil(error) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } - func test_takePicture_newVersion_withAllowEditEnabled_isSuccessful_returnEditedImageWithMetadata() async { - let options = IONCAMRPictureOptionsConfigurations.allowEditLatestVersionWithMetadata! + func test_takePicture_newVersion_withAllowEditEnabled_isSuccessful_returnEditedImageWithMetadata() async throws { + let options = try XCTUnwrap(IONCAMRPictureOptionsConfigurations.allowEditLatestVersionWithMetadata) mockCoordinator.hasTwoSteps = true mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogoBlue.url @@ -182,37 +183,37 @@ extension IONCAMRFlowTests { XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogoBlue.toMediaResultWithMetadata) XCTAssertNil(error) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } - func test_takePicture_oldVersion_isSuccessful_returnImage() async { + func test_takePicture_oldVersion_isSuccessful_returnImage() async throws { mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogo.url - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.jpegEncodingType!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.jpegEncodingType)) mockPicker.didEndSuccessfullyTakePictureHandler() await waitForResult() XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogo.toMediaResult) XCTAssertNil(error) XCTAssertFalse(mockGallery.isSaved) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } - func test_takePicture_oldVersion_isSuccessful_andSavedIntoPhotoLibrary_returnImage() async { + func test_takePicture_oldVersion_isSuccessful_andSavedIntoPhotoLibrary_returnImage() async throws { mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogo.url - sut.takePhoto(with: IONCAMRPictureOptionsConfigurations.saveToPhotosAlbum!) + try sut.takePhoto(with: XCTUnwrap(IONCAMRPictureOptionsConfigurations.saveToPhotosAlbum)) mockPicker.didEndSuccessfullyTakePictureHandler() await waitForResult() XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogo.toMediaResult) XCTAssertNil(error) XCTAssertTrue(mockGallery.isSaved) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } - func test_takePicture_newVersion_isSuccessful_returnImage() async { - let options = IONCAMRPictureOptionsConfigurations.noEditEncodingTypeLatestVersion! + func test_takePicture_newVersion_isSuccessful_returnImage() async throws { + let options = try XCTUnwrap(IONCAMRPictureOptionsConfigurations.noEditEncodingTypeLatestVersion) mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogo.url sut.takePhoto(with: options) @@ -221,11 +222,11 @@ extension IONCAMRFlowTests { XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogo.toMediaResult) XCTAssertNil(error) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } - func test_takePicture_newVersion_isSuccessful_returnImageWithMetadata() async { - let options = IONCAMRPictureOptionsConfigurations.noEditLatestVersionWithMetadata! + func test_takePicture_newVersion_isSuccessful_returnImageWithMetadata() async throws { + let options = try XCTUnwrap(IONCAMRPictureOptionsConfigurations.noEditLatestVersionWithMetadata) mockURLGenerator.urlToReturn = IONCAMRPictureMock.osLogo.url sut.takePhoto(with: options) @@ -234,11 +235,12 @@ extension IONCAMRFlowTests { XCTAssertEqual(singleResult, IONCAMRPictureMock.osLogo.toMediaResultWithMetadata) XCTAssertNil(error) - XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), [singleResult!.uri]) + XCTAssertEqual(sut.temporaryURLArray.map(\.absoluteString), try [XCTUnwrap(singleResult?.uri)]) } } // MARK: - Edit Picture Tests + extension IONCAMRFlowTests { func test_editPicture_butErrorOccurs_returnError() { let editorError = IONCAMRError.editPictureIssue @@ -271,6 +273,7 @@ extension IONCAMRFlowTests { } // MARK: - Choose a Picture Tests + extension IONCAMRFlowTests { func test_choosePicture_butNoAccessToPhotoLibrary_returnError() { mockPermissions.authorised = false @@ -343,6 +346,7 @@ extension IONCAMRFlowTests { } // MARK: - Capture Video Tests + extension IONCAMRFlowTests { func test_captureVideo_butNoCameraAvailable_returnError() { mockPicker.cameraAvailable = false @@ -461,7 +465,7 @@ extension IONCAMRFlowTests { sut.recordVideo(with: IONCAMRRecordVideoOptionsConfigurations.withMetadata) mockPicker.didEndSuccessfullyCaptureVideoHandler() - // TODO: This is flaky. It's being done due to multithreading while generating video metadata. + // workaround for multithreading while generating video metadata. sleep(1) XCTAssertEqual(singleResult?.type, .video) @@ -473,6 +477,7 @@ extension IONCAMRFlowTests { } // MARK: - Choose Multimedia Tests + extension IONCAMRFlowTests { func test_chooseMultimedia_butNoPhotoLibraryAccess_returnError() { mockPermissions.authorised = false @@ -587,14 +592,14 @@ extension IONCAMRFlowTests { extension IONCAMRFlowTests: IONCAMRFlowResultsDelegate { func didReturn(_ result: Result) { - self.singleResult = nil - self.multipleResult = nil - self.error = nil + singleResult = nil + multipleResult = nil + error = nil switch result { case .success(let value): - self.singleResult = value as? IONCAMRMediaResult - self.multipleResult = value as? [IONCAMRMediaResult] + singleResult = value as? IONCAMRMediaResult + multipleResult = value as? [IONCAMRMediaResult] case .failure(let error): self.error = error } @@ -604,9 +609,9 @@ extension IONCAMRFlowTests: IONCAMRFlowResultsDelegate { } func didCancel(_ error: IONCAMRError) { - self.cancel = true - self.singleResult = nil - self.multipleResult = nil + cancel = true + singleResult = nil + multipleResult = nil self.error = nil continuation?.resume() diff --git a/Tests/IONCameraLibTests/IONCAMRMediaOptionsTests.swift b/Tests/IONCameraLibTests/IONCAMRMediaOptionsTests.swift index 507d444..eb725ad 100644 --- a/Tests/IONCameraLibTests/IONCAMRMediaOptionsTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRMediaOptionsTests.swift @@ -1,6 +1,6 @@ +@testable import IONCameraLib import UniformTypeIdentifiers import XCTest -@testable import IONCameraLib final class IONCAMRMediaOptionsTests: XCTestCase { func test_whenQualitySetWithValueUnderZero_returnNil() { diff --git a/Tests/IONCameraLibTests/IONCAMRMediaTypeTests.swift b/Tests/IONCameraLibTests/IONCAMRMediaTypeTests.swift index 9449a44..2f79b71 100644 --- a/Tests/IONCameraLibTests/IONCAMRMediaTypeTests.swift +++ b/Tests/IONCameraLibTests/IONCAMRMediaTypeTests.swift @@ -1,5 +1,5 @@ -import XCTest @testable import IONCameraLib +import XCTest final class IONCAMRMediaTypeTests: XCTestCase { func test_whenAPictureIsPassed_createIONCAMRMediaTypePictureObject() throws { diff --git a/package.json b/package.json index 86777c9..4f8a776 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ }, "private": true, "scripts": { + "format": "swiftformat Sources Tests && swiftlint --fix", + "lint": "swiftlint --strict", "semantic-release": "semantic-release" }, "devDependencies": {