diff --git a/.jazzy.yaml b/.jazzy.yaml index 86cde81..85b0627 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -5,7 +5,7 @@ xcodebuild_arguments: - "-scheme" - GiniCaptureSDK - "-destination" -- platform=iOS Simulator,OS=15.0,name=iPhone 13 +- platform=iOS Simulator,OS=16.2,name=iPhone 13 author: Gini GmbH author_url: https://gini.net module: GiniCaptureSDK @@ -19,9 +19,8 @@ abstract: CaptureSDK/GiniCaptureSDK/Documentation/Sections/*.md custom_categories: - name: Documentation children: - - Getting started + - Installation - Integration - - Features - Integration in a Xamarin Project - License @@ -34,13 +33,15 @@ custom_categories: - Event tracking guide - Error logging guide - Migration guide - - Migration to version 3.x.x - name: Classes children: + - AnalysisViewController - AnalysisResult + - CameraViewController - CustomDocumentValidationError - CustomDocumentValidationResult + - DocumentPickerCoordinator - GiniConfiguration - GiniImageDocument - GiniPDFDocument @@ -48,8 +49,13 @@ custom_categories: - GiniCapture - GiniCaptureDocumentBuilder - GiniCaptureDocumentValidator + - HelpMenuViewController + - ImageAnalysisNoResultsViewController + - MultipageReviewViewController - OnboardingPage - + - OnboardingViewController + - ReviewViewController + - name: Enums children: - AnalysisError @@ -62,6 +68,7 @@ custom_categories: - GiniCaptureDocumentType - ImageAnalysisNoResultsStrings - ImageAssetsStrings + - NoticeActionType - OpaqueViewStyle - ReviewError - OnboardingScreenEventType @@ -86,6 +93,7 @@ custom_categories: - GiniCaptureError - GiniCaptureResultsDelegate - HelpMenuViewControllerDelegate + - MultipageReviewViewControllerDelegate - ReviewViewControllerDelegate - UploadDelegate - GiniCaptureTrackingDelegate diff --git a/Documentation/Sections/Documentation.md b/Documentation/Sections/Documentation.md index ef834bf..bb67624 100644 --- a/Documentation/Sections/Documentation.md +++ b/Documentation/Sections/Documentation.md @@ -14,8 +14,28 @@ The Gini Capture SDK provides components for capturing, reviewing and analyzing By integrating this SDK into your application you can allow your users to easily take a picture of a document, review it and getting analysis results from the Gini backend. -We provide pre-defined screens that can be customized in a limited way. The screen and configuration design is based on our long-lasting experience with integration in customer apps. - -On *iPhone*, the Gini Capture SDK has been designed for portrait orientation. -On *iPad* we support portrait and landscape orientations. - +The Gini Capture SDK can be integrated in two ways, either by using the *Screen API* or the *Component API*. In the Screen API we provide pre-defined screens that can be customized in a limited way. The screen and configuration design is based on our long-lasting experience with integration in customer apps. In the Component API, we provide independent views so you can design your own application as you wish. We strongly recommend keeping in mind our UI/UX guidelines, however. + +On *iPhone*, the Gini Capture SDK has been designed for portrait orientation. In the Screen API, orientation is automatically forced to portrait when being displayed. In case you use the Component API, you should limit the view controllers orientation hosting the Component API's views to portrait orientation. This is specifically true for the camera view. + +## Contents + +* [Installation](installation.html) +* [Integration](integration.html) +* [Integration in a Xamarin Project](integration-in-a-xamarin-project.html) +* [Changelog](changelog.html) +* [License](license.html) +* Guides + - [Customization guide](customization-guide.html) + - [Import PDFs and Images guide](import-pdfs-and-images-guide.html) + - [Open with guide](open-with-guide.html) + - [QR Code scanning guide](qr-code-scanning-guide.html) + - [Event tracking guide](event-tracking-guide.html) + - [Error logging guide](error-logging-guide.html) + +## API + +* [Classes](Classes.html) +* [Enums](Enums.html) +* [Protocols](Protocols.html) +* [Typealiases](Typealiases.html) diff --git a/Documentation/Sections/Guides.md b/Documentation/Sections/Guides.md index e69de29..f067901 100644 --- a/Documentation/Sections/Guides.md +++ b/Documentation/Sections/Guides.md @@ -0,0 +1,6 @@ + - [Customization guide](customization-guide.html) + - [Import PDFs and Images guide](import-pdfs-and-images-guide.html) + - [Open with guide](open-with-guide.html) + - [QR Code scanning guide](qr-code-scanning-guide.html) + - [Event tracking guide](event-tracking-guide.html) + - [Error logging guide](error-logging-guide.html) diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Analysis.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Analysis.jpg new file mode 100644 index 0000000..fe5964b Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Analysis.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Camera.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Camera.jpg new file mode 100644 index 0000000..586541a Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Camera.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Gallery album.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Gallery album.jpg new file mode 100644 index 0000000..139dd05 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Gallery album.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Help screen.png b/Documentation/jazzy-theme/assets/img/Customization guide/Help screen.png new file mode 100644 index 0000000..7ba552e Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Help screen.png differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/MultipageReview.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/MultipageReview.jpg new file mode 100644 index 0000000..22f1f78 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/MultipageReview.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Navigation bar.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Navigation bar.jpg new file mode 100644 index 0000000..cf622c9 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Navigation bar.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/No results.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/No results.jpg new file mode 100644 index 0000000..8dd2bd0 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/No results.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Notice.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Notice.jpg new file mode 100644 index 0000000..2b49252 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Notice.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Onboarding.png b/Documentation/jazzy-theme/assets/img/Customization guide/Onboarding.png new file mode 100644 index 0000000..f13113b Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Onboarding.png differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Open with tutorial.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Open with tutorial.jpg new file mode 100644 index 0000000..8a49028 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Open with tutorial.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/QR code popup.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/QR code popup.jpg new file mode 100644 index 0000000..469e258 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/QR code popup.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Review.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Review.jpg new file mode 100644 index 0000000..b0967b4 Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Review.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Supported formats.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Supported formats.jpg new file mode 100644 index 0000000..63364ca Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Supported formats.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/Customization guide/Tooltip.jpg b/Documentation/jazzy-theme/assets/img/Customization guide/Tooltip.jpg new file mode 100644 index 0000000..eddd72b Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/Customization guide/Tooltip.jpg differ diff --git a/Documentation/jazzy-theme/assets/img/qr_code_popup.jpg b/Documentation/jazzy-theme/assets/img/qr_code_popup.jpg new file mode 100644 index 0000000..689e1ed Binary files /dev/null and b/Documentation/jazzy-theme/assets/img/qr_code_popup.jpg differ diff --git a/Documentation/source/Customization guide.md b/Documentation/source/Customization guide.md index 1b30fec..9fb5e1d 100644 --- a/Documentation/source/Customization guide.md +++ b/Documentation/source/Customization guide.md @@ -3,206 +3,320 @@ Customization guide The Gini Capture SDK components can be customized either through the `GiniConfiguration`, the `Localizable.string` file or through the assets. Here you can find a complete guide with the reference to every customizable item. -- [Customization guide](#customization-guide) - - [Colors](#colors) - - [Typography](#typography) - - [Images](#images) - - [Text](#text) - - [Generic components](#generic-components) - - [1. Top Navigation bar](#1-top-navigation-bar) - - [2. Bottom Navigation bar](#2-bottom-navigation-bar) - - [Onboarding screens](#onboarding-screens) - - [Camera screen](#camera-screen) - - [Single Page](#single-page) - - [Multi-Page](#multi-page) - - [Camera](#camera) - - [Camera access](#camera-access) - - [QR Code Scanning](#qr-code-scanning) - - [QR Code Only](#qr-code-only) - - [Document Import](#document-import) - - [Camera import error handling](#camera-import-error-handling) - - [Review screen](#review-screen) - - [Analysis screen](#analysis-screen) - - [Help screens](#help-screens) - - [Gallery album screen](#gallery-album-screen) - - [No result screen](#no-result-screen) - - [Error screen](#error-screen) +- [Generic components](#generic-components) +- [Camera screen](#camera-screen) +- [Review screen](#review-screen) +- [Multipage Review screen](#multipage-review-screen) +- [Analysis screen](#analysis-screen) +- [Supported formats screen](#supported-formats-screen) +- [Open with tutorial screen](#open-with-tutorial-screen) +- [Capturing tips screen](#capturing-tips-screen) +- [Gallery album screen](#gallery-album-screen) +- [Onboarding screens](#onboarding-screens) +- [Help screen](#help-screen) -## Colors -We are providing a global color palette `GiniColors.xcassets` which you are free to override. The custom colors will be then applied on all screens. -You can find the names of the colors in [GiniColors.xcassets](https://github.com/gini/gini-mobile-ios/tree/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets). +## Supporting dark mode - You can view our color palette here: - - - -## Typography - -We provide a global typography based on text appearance styles from `UIFont.TextStyle`. - - - -To override them in your application please use `GiniConfiguration.updateFont(_ font: UIFont, for textStyle: UIFont.TextStyle)`. For example: - -```swift - - // If you need to scale your font please use our method `scaledFont()`. Please, find the example below. - let configuration = GiniBankConfiguration.shared - let customFontToBeScaled = UIFont.scaledFont(UIFont(name: "Avenir", size: 20) ?? UIFont.systemFont(ofSize: 7, weight: .regular), textStyle: .caption1) - configuration.updateFont(customFontToBeScaled, for: .caption1) - - // If you would like to pass us already scaled font. - let customScaledFont = UIFontMetrics(forTextStyle: .caption2).scaledFont(for: UIFont.systemFont(ofSize: 28)) - configuration.updateFont(customScaledFont, for: .caption2) -``` - -## Images - -Images customization is done via overriding of [GiniImages.xcassets](https://github.com/gini/gini-mobile-ios/tree/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets) resources. - -## Text - - Text customization is done via overriding of string resources. - - You can find all the string resources in [Localizable.strings](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings). +Some background and text colors use the `GiniColor` type with which you can set colors for dark and light modes. Please make sure to set contrasting images to the background colors in your `.xcassets` for the Gini Capture SDK images you override (e.g. `onboardingPage1`). The text colors should also be set in contrast to the background colors. ## Generic components -##### 1. Top Navigation bar - -Colors, typography, texts can be customized as described above. - -To inject your own navigation bar view you need to pass your navigation view controller to -`GiniConfiguration.shared.customNavigationController`. -The view from the custom navigation view controller will then be displayed on all screens as the top navigation bar. - -##### 2. Bottom Navigation bar - -You can opt to show a bottom navigation bar. To enable it pass `true` to -`GiniConfiguration.shared.bottomNavigationBarEnabled`. - -**Note**: The top navigation bar will still be used, but its functionality will be limited to showing the screen's title and -an optional close button. -Please inject a custom top navigation bar if your design requires it even if you have enabled the bottom navigation bar. - -For each screen we provide a possibility to inject a custom bottom navigation bar. -More details will be added below during the specific screen customization. - -## Onboarding screens - - +##### 1. Navigation bar +
+- Tint color → `GiniConfiguration.navigationBarTintColor` +- Item tint color → `GiniConfiguration.navigationBarItemTintColor` +- Title color → `GiniConfiguration.navigationBarTitleColor` +- Item font → `GiniConfiguration.navigationBarItemFont` +- Title font → `GiniConfiguration.navigationBarTitleFont` + +##### 2. Notice +
+- Information background color → `GiniConfiguration.noticeInformationBackgroundColor` +- Information text color → `GiniConfiguration.noticeInformationTextColor` +- Error background → `GiniConfiguration.noticeErrorBackgroundColor` +- Error text color `GiniConfiguration.noticeErrorTextColor` + +##### 3. Tooltip +
+- Background color → `GiniConfiguration.fileImportToolTipBackgroundColor` +- Text color → `GiniConfiguration.fileImportToolTipTextColor` +- Close button color → `GiniConfiguration.fileImportToolTipCloseButtonColor` +- Text → + - *ginicapture.camera.fileImportTip* localized string for file import tooltip + - *ginicapture.camera.qrCodeTip* localized string for qr code tooltip + - *ginicapture.multipagereview.reorderContainerTooltipMessage* localized string for reorder tooltip + +##### 4. Gini Capture font + +- Font → `GiniConfiguration.customFont` ## Camera screen - - -### Single Page - -By default, the Gini Capture SDK is configured to capture single page documents. -No further configuration is required for this. - -### Multi-Page - -The multi-page feature allows the SDK to capture documents with multiple pages. - -To enable this feature simply pass `true` to `GiniConfiguration.shared.multipageEnabled`. - -### Camera - -- Enable the flash toggle button: -To allow users toggle the camera flash pass `true` to `GiniConfiguration.shared.flashToggleEnabled`. - -- Turn off flash by default: -Flash is on by default, and you can turn it off by passing `false` to `GiniConfiguration.shared.flashOnByDefault`. - -### Camera access - - - -### QR Code Scanning - -When a supported QR code is detected with valid payment data, the QR Code will be processed automatically without any further user interaction. -The QR Code scanning may be triggered directly without the need to analyze the document. - -You can show a custom loading indicator with custom animation support. -Your custom loading indicator should implement `CustomLoadingIndicatorAdapter` interface and be passed to `GiniConfiguration.shared.customLoadingIndicator`. - -If the QR code does not have a supported payment format then a popup informs the user that a QR code was detected, but it cannot be used. - -Please find more information in the [QR Code scanning guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/qr-code-scanning-guide.html). - -### QR Code Only - -During QR Code only mode the capture and import controls will be hidden from the camera screen. - -For enabling QR code only mode the both flags `GiniConfiguration.shared.qrCodeScanningEnabled` and `GiniConfiguration.shared.onlyQRCodeScanningEnabled` should be `true`. - -More information about the customization is available [here](https://www.figma.com/file/1985HMF83siAXmysSn3dC6/iOS-Gini-Capture-SDK-3.0.0-UI-Customisation?node-id=212%3A2331&t=cRAvcUKVlwGtGpuh-1) - -### Document Import - -This feature enables the Gini Capture SDK to import documents from the camera screen. When it's enabled an additional button is shown next to the camera trigger. Using this button allows the user to pick either an image or a pdf from the device. - -Please find more information in the [Import PDFs and images guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/import-pdfs-and-images-guide.html). - -### Camera import error handling - - +
+
+
+ +##### 1. Navigation bar +- Title → *ginicapture.navigationbar.camera.title* localized string +- Close button + - With image and title + - Image → *navigationCameraClose* image asset + - Title → *ginicapture.navigationbar.camera.close* localized string + - With title only + - Title → `GiniConfiguration.navigationBarCameraTitleCloseButton` +- Help button + - With image and title + - Image → *navigationCameraHelp* image asset + - Title → *ginicapture.navigationbar.camera.help* localized string + - With title only + - Title → `GiniConfiguration.navigationBarCameraTitleHelpButton` + +##### 2. Camera preview +- Preview frame color → `GiniConfiguration.cameraPreviewFrameColor` +- Guides color → `GiniConfiguration.cameraPreviewCornerGuidesColor` +- Focus large image → *cameraFocusLarge* image asset +- Focus large small → *cameraFocusSmall* image asset +- Opaque view style (when tool tip is shown) → `GiniConfiguration.toolTipOpaqueBackgroundStyle` + +##### 3. Camera buttons container +- Background color → `GiniConfiguration.cameraButtonsViewBackgroundColor` +- Container view background color under the home indicator → `GiniConfiguration.cameraContainerViewBackgroundColor` +- Capture button + - Image → *cameraCaptureButton* image asset +- Import button + - Image → *documentImportButton* image asset +- Captured images stack indicator color → `GiniConfiguration.imagesStackIndicatorLabelTextcolor` +- Flash toggle can be enabled through → `GiniConfiguration.flashToggleEnabled` +- Flash button + - Image → *flashOn* image asset + - Image → *flashOff* image asset + +##### 4. QR code popup +
+
+
+- Background color → `GiniConfiguration.qrCodePopupBackgroundColor` using `GiniColor` with dark mode and light mode colors +- Button color → `GiniConfiguration.qrCodePopupButtonColor` +- Text color → `GiniConfiguration.qrCodePopupTextColor` using `GiniColor` with dark mode and light mode colors +- Title → *ginicapture.camera.qrCodeDetectedPopup.buttonTitle* localized string +- Message → *ginicapture.camera.qrCodeDetectedPopup.message* localized string ## Review screen - +
+
+
+ +##### 1. Navigation bar +- Title → *ginicapture.navigationbar.review.title* localized string +- Back button + - With image and title + - Image → *navigationReviewBack* image asset + - Title → *ginicapture.navigationbar.review.back* localized string + - With title only + - Title → `GiniConfiguration.navigationBarReviewTitleBackButton` +- Next button + - Image → *navigationReviewContinue* image asset + - Title → *ginicapture.navigationbar.review.continue* localized string + +##### 2. Review top view +- Title → *ginicapture.review.top* localized string + +##### 3. Review bottom view +- Background color → `GiniConfiguration.reviewBottomViewBackgroundColor` +- Rotation button image → *reviewRotateButton* image asset +- Rotation message + - Text → *ginicapture.review.bottom* localized string + - Text color → `GiniConfiguration.reviewTextBottomColor` + +## Multipage Review screen + +
+
+
+ +##### 1. Navigation bar +- Back button + - With image and title + - Image → *navigationReviewBack* image asset + - Title → *ginicapture.navigationbar.review.back* localized string + - With title only + - Title → `GiniConfiguration.navigationBarReviewTitleBackButton` +- Next button + - Image → *navigationReviewContinue* image asset + - Title → *ginicapture.navigationbar.review.continue* localized string + +##### 2. Main collection +- Opaque view style (when tool tip is shown) → `GiniConfiguration.multipageToolTipOpaqueBackgroundStyle` + +##### 3. Page item +- Page upload state icon + - Successful upload → *successfullUploadIcon* image asset + - Failed upload → *failureUploadIcon* image asset +- Page upload state icon background color + - Successful upload → `GiniConfiguration.multipagePageSuccessfullUploadIconBackgroundColor` + - Failed upload → `GiniConfiguration.multipagePageFailureUploadIconBackgroundColor` +- Page circle indicator color → `GiniConfiguration.indicatorCircleColor` using `GiniColor` with dark mode and light mode colors +- Page indicator color → `GiniConfiguration.multipagePageIndicatorColor` +- Page background color → `GiniConfiguration.multipagePageBackgroundColor` using `GiniColor` with dark mode and light mode colors +- Page selected indicator color → `GiniConfiguration.multipagePageSelectedIndicatorColor` +- Page draggable icon tint color → `GiniConfiguration.multipageDraggableIconColor` + +##### 4. Bottom container +- Background color → `GiniConfiguration.multipagePagesContainerAndToolBarColor` using `GiniColor` with dark mode and light mode colors +- Rotation button image → *rotateImageIcon* image asset +- Delete button image → *trashIcon* image asset -You can show a custom loading indicator with custom animation support on the process button. -Your custom loading indicator should implement `OnButtonLoadingIndicatorAdapter` interface and be passed to `GiniConfiguration.shared.onButtonLoadingIndicator`. +## Analysis screen -The example implementation is available [here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/BankSDK/GiniBankSDKExample/GiniBankSDKExample/CustomLoadingIndicator.swift#L36). +
+
+
+ +##### 1. Navigation bar +- Cancel button + - With image and title + - Image → *navigationAnalysisBack* image asset + - Title → *ginicapture.navigationbar.analysis.back* localized string + - With title only + - Title → `GiniConfiguration.navigationBarAnalysisTitleBackButton` + +##### 2. PDF Information view +- Text color → `GiniConfiguration.analysisPDFInformationTextColor` +- Background color → `GiniConfiguration.analysisPDFInformationBackgroundColor` + +##### 3. Loading view +- Indicator color → `GiniConfiguration.analysisLoadingIndicatorColor` (Only with PDFs) +- Text → *ginicapture.analysis.loadingText* localized string + +## Supported formats screen + +
+
+
+ +##### Navigation bar +- Back button + - With image and title + - Image → *arrowBack* image asset + - Title → *ginicapture.navigationbar.help.backToMenu* localized string + - With title only + - Title → `GiniConfiguration.navigationBarHelpScreenTitleBackToMenuButton` + +##### 1. Supported format cells +- Supported fortmats icon → *supportedFormatsIcon* image asset +- Supported formats icon color → `GiniConfiguration.supportedFormatsIconColor` +- Non supported fortmats icon → *nonSupportedFormatsIcon* image asset +- Non supported formats icon color → `GiniConfiguration.nonSupportedFormatsIconColor` + +## Open with tutorial screen + +
+
+
+ +##### Navigation bar +- Back button + - With image and title + - Image → *arrowBack* image asset + - Title → *ginicapture.navigationbar.help.backToMenu* localized string + - With title only + - Title → `GiniConfiguration.navigationBarHelpScreenTitleBackToMenuButton` + +##### 1. Header +- Text → *ginicapture.help.openWithTutorial.collectionHeader* localized string + +##### 2. Open with steps +- App name → `GiniConfiguration.openWithAppNameForTexts` +- Step indicator color → `GiniConfiguration.stepIndicatorColor` +- Step 1 + - Title → *ginicapture.help.openWithTutorial.step1.title* localized string + - Subtitle → *ginicapture.help.openWithTutorial.step1.subtitle* localized string + - Image → *openWithTutorialStep1* (German) and *openWithTutorialStep1_en* (English) image assets +- Step 2 + - Title → *ginicapture.help.openWithTutorial.step2.title* localized string + - Subtitle → *ginicapture.help.openWithTutorial.step2.subtitle* localized string + - Image → *openWithTutorialStep2* (German) and *openWithTutorialStep2_en* (English) image assets +- Step 3 + - Title → *ginicapture.help.openWithTutorial.step3.title* localized string + - Subtitle → *ginicapture.help.openWithTutorial.step3.subtitle* localized string + - Image → *openWithTutorialStep3* (German) and *openWithTutorialStep3_en* (English) image assets + +## Capturing tips screen + +
+
+
+ +##### 1. Capturing tip images +- Tip 1 image → *captureSuggestion1* image asset +- Tip 2 image → *captureSuggestion2* image asset +- Tip 3 image → *captureSuggestion3* image asset +- Tip 4 image → *captureSuggestion4* image asset +- Tip 5 image → *captureSuggestion5* image asset + +##### 2. Go to camera button +- Background color → `GiniConfiguration.noResultsBottomButtonColor` +- Text color → `GiniConfiguration.noResultsBottomButtonTextColor` +- Corner radius → `GiniConfiguration.noResultsBottomButtonCornerRadius` -## Analysis screen +## Gallery album screen - +
+
+
-You can show a custom loading indicator with custom animation support. -Your custom loading indicator should implement `CustomLoadingIndicatorAdapter` interface and be passed to `GiniConfiguration.shared.customLoadingIndicator`. +##### 1. Selected image +- Selected item check color → `GiniConfiguration.galleryPickerItemSelectedBackgroundCheckColor` +- Background color → `GiniConfiguration.galleryScreenBackgroundColor` using `GiniColor` with dark mode and light mode colors -The example implementation is available [here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/BankSDK/GiniBankSDKExample/GiniBankSDKExample/CustomLoadingIndicator.swift). +## Onboarding screens -## Help screens +
+
+
- +##### 1. Background +- Background Color → `GiniConfiguration.onboardingScreenBackgroundColor` using `GiniColor` with dark mode and light mode colors -You can show your own help screens in the Gini Capture SDK. -You can pass the title and view controller for each screen to the -`GiniConfiguration.shared.customMenuItems` using a list of `HelpMenuItem` structs: +##### 2. Image +- Page image → *onboardingPage** image asset -``` swift +##### 3. Text +- Color → `GiniConfiguration.onboardingTextColor` using `GiniColor` with dark mode and light mode colors - let customMenuItem = HelpMenuItem.custom("Custom menu item", CustomMenuItemViewController()) +##### 4. Page indicator +- Color → `GiniConfiguration.onboardingPageIndicatorColor` using `GiniColor` with dark mode and light mode colors - configuration.customMenuItems = [customMenuItem] - ``` - -The example implementation is available [here](https://github.com/gini/gini-mobile-ios/tree/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDKExample/Example%20Swift). +##### 5. Current page indicator +- Color → `GiniConfiguration.onboardingCurrentPageIndicatorColor` using `GiniColor` with dark mode and light mode colors +- Alpha → `GiniConfiguration.onboardingCurrentPageIndicatorAlpha` sets alpha to the `GiniConfiguration.onboardingCurrentPageIndicatorColor` -You can also disable the supported formats help screen by passing `false` to -`GiniConfiguration.shared.shouldShowSupportedFormatsScreen`. +## Help screen -## Gallery album screen +
+
+
+ +##### 1. Navigation bar - +- Back button + - With image and title + - Image → *navigationHelpBack* image asset + - Title → *ginicapture.navigationbar.help.backToCamera* localized string + - With title only + - Title → `GiniConfiguration.navigationBarHelpMenuTitleBackToCameraButton` -## No result screen +##### 2. Table View Cells - +- Background color → `GiniConfiguration.helpScreenCellsBackgroundColor` using `GiniColor` with dark mode and light mode colors -You can show your own UI for data input if an error occurred and the user clicks the "Enter manually" button on the error screen. -For this you must to implement `GiniCaptureResultsDelegate.giniCaptureDidEnterManually() `. +##### 3. Background -You can find more details [here](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/features.html#no-result-screen-customization). +- Background color → `GiniConfiguration.helpScreenBackgroundColor` using `GiniColor` with dark mode and light mode colors -## Error screen +##### 4. Additional help menu items - +- Custom help menu items → `GiniConfiguration.customMenuItems` an array of `HelpMenuViewController.Item` objects -You can find more details [here](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/features.html#error-screen-customization). diff --git a/Documentation/source/Error logging guide.md b/Documentation/source/Error logging guide.md index 73c881e..1ef0a11 100644 --- a/Documentation/source/Error logging guide.md +++ b/Documentation/source/Error logging guide.md @@ -1,11 +1,11 @@ Error Logging ============================= -The Gini Capture SDK logs errors to the Gini Bank API when the default networking implementation is used (see the Default Networking (Recommended) section in Integration guide). We log only non-sensitive information like response status codes, headers and error messsages. +The Gini Capture SDK logs errors to the Gini Bank API when the default networking implementation is used (see the UI with Networking (Recommended) section in Integration guide). We log only non-sensitive information like response status codes, headers and error messsages. -You can disable the default error logging by passing false to `GiniConfiguration.shared.giniErrorLoggerIsOn`. +You can disable the default error logging by passing false to `GiniConfiguration().giniErrorLoggerIsOn`. -If you would like to get informed of error logging events you need to set `GiniConfiguration.shared.customGiniErrorLoggerDelegate` which confirms to `GiniCaptureErrorLoggerDelegate`: +If you would like to get informed of error logging events you need to set `GiniConfiguration().customGiniErrorLoggerDelegate` which confirms to `GiniCaptureErrorLoggerDelegate`: ```swift class CustomErrorLogger: GiniCaptureErrorLoggerDelegate { diff --git a/Documentation/source/Event tracking guide.md b/Documentation/source/Event tracking guide.md index 426e2ee..74615ee 100644 --- a/Documentation/source/Event tracking guide.md +++ b/Documentation/source/Event tracking guide.md @@ -34,3 +34,17 @@ Event types are partitioned into different domains according to the screens that | Analysis Screen | `cancel` || User canceled the process during analysis | ✅ | ❌ | | Analysis Screen | `error` | `"message"` | The analysis ended with an error. The error message is supplied under the "message" key. | ✅ | ✅ | | Analysis Screen | `retry` || The user decided to retry after an analysis error. | ✅ | ✅ | + +## Component API + +If you are using the Component API, you may want to implement the remaining events in your coordinator code. In order to report an event, call the `GiniCaptureTrackingDelegate` method relating to the event's domain area and pass the event. + +For instance to report user advancing from the Review Screen, call `onReviewScreenEvent(event:)` passing an `Event` struct. `ReviewScreenEventType` defines the event types available in the Review Screen domain. + +The call would look something like this: + +```swift +trackingDelegate?.onReviewScreenEvent(event: Event(type: .next)) +``` + + diff --git a/Documentation/source/Features.md b/Documentation/source/Features.md deleted file mode 100644 index 8469dc1..0000000 --- a/Documentation/source/Features.md +++ /dev/null @@ -1,161 +0,0 @@ -Features -========= - -The Gini Capture SDK provides various features you can enable and configure. -All the features are configured during through `GiniConfiguration.shared` instance. -Specifically the `GiniConfiguration` is used to configure the Gini Capture SDK. - -**Note**: Some features require additional contractual agreements and may not be used without prior confirmation. Please get in touch with us in case you are not sure which features your contract includes. - -The following sections list all the features along with the related configuration options. - -# Document Capture - -This is the core feature of the Gini Capture SDK. It enables your app to capture documents with the camera and prepares -them to be analyzed by the Gini Bank API. - -## Custom UI Elements - -Certain elements of the UI can now be fully customized via UI injection. It utilizes view adapter interfaces which you -can implement and pass to `GiniConfiguration` when configuring the SDK. These interfaces declare the contract the injected -view has to fulfill and allow the SDK to ask for your view instance when needed. - -### Top Navigation Bar - -To inject your own navigation bar view you need to pass your navigation view controller to -`GiniConfiguration.shared.customNavigationController`. -The view from the custom navigation view controller will then be displayed on all screens as the top navigation bar. - -### Bottom Navigation Bar - -You can opt to show a bottom navigation bar. To enable it pass `true` to -`GiniConfiguration.shared.bottomNavigationBarEnabled`. - -**Note**: The top navigation bar will still be used, but its functionality will be limited to showing the screen's title and -an optional close button. -Please inject a custom top navigation bar if your design requires it even if you have enabled the bottom navigation bar. - -## Onboarding - -The onboarding feature presents essential information to the user on how to best capture documents. - -You can customize the onboarding in the following ways: - -- Disable showing the onboarding at first run: -By default the onboarding is shown at first run. To disable this pass `false` to -`GiniConfiguration.shared.onboardingShowAtFirstLaunch`. - -- Customize the onboarding pages: -If you wish to show different onboarding pages then pass a list of `OnboardingPage` structs to `GiniConfiguration.shared.customOnboardingPages`. - -- Force show the onboarding: -If you wish to show the onboarding after the first run then pass `true` to -`GiniConfiguration.shared.onboardingShowAtLaunch`. - -- Animate illustrations by injecting custom views: -If you need to animate the illustrations on the onboarding pages implement the `OnboardingIllustrationAdapter` interface to inject a view that can animate images (e.g., `Lottie`) and pass it to the relevant onboarding illustration adapter setters (e.g., `onboardingAlignCornersIllustrationAdapter`, -`onboardingLightingIllustrationAdapter`, -`onboardingMultiPageIllustrationAdapter`, -`onboardingQRCodeIllustrationAdapter`) - when configuring the `GiniConfiguration.shared` instance. - -## Single Page - -By default, the Gini Capture SDK is configured to capture single page documents. -No further configuration is required for this. - -## Multi-Page - -The multi-page feature allows the SDK to capture documents with multiple pages. - -To enable this simply pass `true` to `GiniConfiguration.shared.multipageEnabled`. - -`Add pages button` will be shown on the Review screen only if multi-page is enabled. - -## Camera - -- Enable the flash toggle button: -To allow users toggle the camera flash pass `true` to `GiniConfiguration.shared.flashToggleEnabled`. - -- Turn off flash by default: -Flash is on by default, and you can turn it off by passing `false` to `GiniConfiguration.shared.flashOnByDefault`. - - # QR Code Scanning - -When a supported QR code is detected with valid payment data, the QR Code will be processed automatically without any further user interaction. -The QR Code scanning may be triggered directly without the need to analyze the document. - -If the QR code does not have a supported payment format then a popup informs the user that a QR code was detected, but it cannot be used. - -Please find more information in the [QR Code scanning guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/qr-code-scanning-guide.html). - -## QR Code Only - -During QR Code only mode the capture and import controls will be hidden from the camera screen. - -For enabling QR code only mode the both flags `GiniConfiguration.shared.qrCodeScanningEnabled` and `GiniConfiguration.shared.onlyQRCodeScanningEnabled` should be `true`. - -# Document Import - -This feature enables the Gini Capture SDK to import documents from the camera screen. When it's enabled an additional button is shown next to the camera trigger. Using this button allows the user to pick either an image or a pdf from the device. - -Please find more information in the [Import PDFs and images guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/import-pdfs-and-images-guide.html). - -# Open with - -The `Open with` feature allows importing of files from other apps via iOS `share` functionality. - -Please find more information in the [Open with guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/open-with-guide.html). - -# Help screen customization - -You can show your own help screens in the Gini Capture SDK. -You can pass the title and view controller for each screen to the -`GiniConfiguration.shared.customMenuItems` using a list of `HelpMenuItem` structs: - -``` swift - - let customMenuItem = HelpMenuItem.custom("Custom menu item", CustomMenuItemViewController()) - - configuration.customMenuItems = [customMenuItem] - ``` -The example implementation is available [here](https://github.com/gini/gini-mobile-ios/tree/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDKExample/Example%20Swift). - -You can also disable the supported formats help screen by passing `false` to -`GiniConfiguration.shared.shouldShowSupportedFormatsScreen`. - -# Review screen customization - -You can show a custom loading indicator with custom animation support on the process button. -Your custom loading indicator should implement `OnButtonLoadingIndicatorAdapter` interface and be passed to `GiniConfiguration.shared.onButtonLoadingIndicator`. - -The example implementation is available [here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/BankSDK/GiniBankSDKExample/GiniBankSDKExample/CustomLoadingIndicator.swift#L36). - -# Analysis screen customization - -You can show a custom loading indicator with custom animation support. -Your custom loading indicator should implement `CustomLoadingIndicatorAdapter` interface and be passed to `GiniConfiguration.shared.customLoadingIndicator`. - -The example implementation is available [here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/BankSDK/GiniBankSDKExample/GiniBankSDKExample/CustomLoadingIndicator.swift). - -# No result screen customization - -You can show your own UI if an error occured and the user chooses to enter details manually. For this you must to implement `GiniCaptureResultsDelegate.giniCaptureDidEnterManually() `. - -The buttom "Retake images" will be shown only if you took or imported images. - -# Error screen customization - -You can show your own UI if an error occured and the user chooses to enter details manually. For this you must to implement `GiniCaptureResultsDelegate.giniCaptureDidEnterManually() `. - -The buttom "Retake images" will be shown only if you took or imported images. - -# Event Tracking - -You have the possibility to track various events which occur during the usage of the Gini Capture SDK. - -Please find more information in the [Event tracking guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/event-tracking-guide.html). - -# Error Logging - -The SDK logs errors to the Gini Bank API when the default networking implementation is used (see the `Default networking` implementation in the [Integration](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/integration.html). diff --git a/Documentation/source/Getting started.md b/Documentation/source/Getting started.md deleted file mode 100644 index 38a4f9b..0000000 --- a/Documentation/source/Getting started.md +++ /dev/null @@ -1,50 +0,0 @@ -Getting started -============================= - -## Requirements - -- The minimum iOS version supported 12+. - -In addition to the minimum iOS version we also have hardware requirements to ensure the best possible analysis -results: - -- Back-facing camera with auto-focus and flash (iPhone). -- 8MP+ camera resolution. You can find the specification [here](https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Cameras/Cameras.html) - -## Installation - -Gini Capture SDK can either be installed by using `Swift Package Manager` or by manually dragging the required `XCFrameworks` to your project. - -### Swift Package Manager - -The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. -Once you have your Swift package set up, adding `GiniCaptureSDK` as a dependency is as easy as adding it to the dependencies value of your `Package.swift` - -```swift -dependencies: [ - .package(url: "https://github.com/gini/capture-sdk-ios.git", .exact("3.0.0-beta07")) -] -``` - -In case that you want to use the certificate pinning in the library, add `GiniCaptureSDKPinning`: -```swift -dependencies: [ - .package(url: "https://github.com/gini/capture-sdk-pinning-ios.git", .exact("3.0.0-beta07")) -] -``` - -### XCFrameworks - -If you prefer not to use a dependency management tool, you can integrate the Gini Capture SDK into your project manually. -To do so add the following frameworks into your project: -- `GiniBankAPILibrary.xcframework` -- `GiniCaptureSDK.xcframework`. - -In case that you want to use the certificate pinning you need to add the following frameworks: - - `GiniBankAPILibrary.xcframework` - - `GiniBankAPILibraryPinning.xcframework` - - `GiniCaptureSDK.xcframework` - - `GiniCaptureSDKPinning.xcframework` - - `TrustKit.xcframework` - - The latest version of the frameworks is availiable on [github](https://github.com/gini/gini-mobile-ios/releases/). diff --git a/Documentation/source/Import PDFs and images guide.md b/Documentation/source/Import PDFs and images guide.md index 2ef667d..3d7f845 100644 --- a/Documentation/source/Import PDFs and images guide.md +++ b/Documentation/source/Import PDFs and images guide.md @@ -5,7 +5,7 @@ Import PDFs and images If you want to add the _File import_ feature on your app, first you need to specify the supported types (`fileImportSupportedTypes `) on the `GiniConfiguration` instance. ```swift -let giniConfiguration = GiniConfiguration.shared +let giniConfiguration = GiniConfiguration() giniConfiguration.fileImportSupportedTypes = .pdf_and_images ``` @@ -26,16 +26,23 @@ giniConfiguration.customDocumentValidations = { document in } } ``` +#### Only Component API + +Additionaly - when using the Component API - you have to use the `DocumentPickerCoordinator` to present both the Photo Gallery and the File Explorer and to handle all the interaction with them. +To enable _Drag&Drop_, just call the `DocumentPickerCoordinator.setupDragAndDrop(in:)` method, passing the view that will handle the drop interaction (we recommend to pass the `CameraViewController.view`). + +Also, with the addition of a custom image picker to support multiple selection, you can start caching the album images by calling the `DocumentPickerCoordinator.startCaching()` method when creating the coordinator, but only if the gallery access permission is granted before (`DocumentPickerCoordinator.isGalleryPermissionGranted`). + Import images from camera roll -------------------------------- +---------------------- To enable your app to import images from **Photo Gallery** and also support **iOS 10** you need to specify the `NSPhotoLibraryUsageDescription ` key in your `Info.plist` file. This key is mandatory for all apps since iOS 10 when accessing the **Photo Gallery**. Import images and PDFs from other apps --------------------------------------- +------------------------------------ -In order to enable your app to import PDFs and images from other apps like *Files*, *Dropbox* or *Drive*, you need to enable _iCloud document support_ in your app. +In order to enable your app to import PDFs and images from other apps like *Dropbox*, *iCloud* or *Drive*, you need to enable _iCloud document support_ in your app.
diff --git a/Documentation/source/Installation.md b/Documentation/source/Installation.md new file mode 100644 index 0000000..bc2caa3 --- /dev/null +++ b/Documentation/source/Installation.md @@ -0,0 +1,36 @@ +Installation +============================= + +Gini Capture SDK can either be installed by using Swift Package Manager or by manually dragging the required files to your project. + +## Swift Package Manager + +The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. +Once you have your Swift package set up, adding `GiniCaptureSDK` as a dependency is as easy as adding it to the dependencies value of your `Package.swift` + +```swift +dependencies: [ + .package(url: "https://github.com/gini/capture-sdk-ios.git", .exact("1.11.1")) +] +``` + +In case that you want to use the certificate pinning in the library, add `GiniCaptureSDKPinning`: +```swift +dependencies: [ + .package(url: "https://github.com/gini/capture-sdk-pinning-ios.git", .exact("1.11.1")) +] +``` + +## Manually + +If you prefer not to use a dependency management tool, you can integrate the Gini Capture SDK into your project manually. +To do so drop the GiniCapture (classes and assets) folder into your project and add the files to your target. + +Xcode will automatically check your project for swift files and will create an autogenerated import header for you. +Use this header in an Objective-C project by adding + +```Obj-C +#import "YourProjectName-Swift.h" +``` + +to your implementation or header files. Note that spaces in your project name result in underscores. So `Your Project` becomes `Your_Project-Swift.h`. diff --git a/Documentation/source/Integration.md b/Documentation/source/Integration.md index 5d9d63d..643902b 100644 --- a/Documentation/source/Integration.md +++ b/Documentation/source/Integration.md @@ -1,20 +1,16 @@ Integration ============================= -## Request camera access -**Note**: You need to specify the `NSCameraUsageDescription` key in your `Info.plist` file. -This key is mandatory for all apps since iOS 10 when using the `Camera` framework. +The Gini Capture SDK provides two integration options. A [Screen API](#screen-api) that is easy to implement and a more complex, but also more flexible [Component API](#component-api). Both APIs can access the complete functionality of the SDK. -Also if you're using the [Gini Bank API Library](https://github.com/gini/bank-api-library-ios) you need to add support for "Keychain Sharing" in your entitlements by adding a `keychain-access-groups` value to your entitlements file. -For more information see the [Integration Guide](https://developer.gini.net/gini-mobile-ios/GiniBankAPILibrary/getting-started.html) of the Gini Bank API Library. +**Note**: You need to specify the `NSCameraUsageDescription` key in your `Info.plist` file. This key is mandatory for all apps since iOS 10 when using the `Camera` framework. Also if you're using the [Gini Bank API Library](https://github.com/gini/bank-api-library-ios) you need to add support for "Keychain Sharing" in your entitlements by adding a `keychain-access-groups` value to your entitlements file. For more information see the [Integration Guide](https://developer.gini.net/gini-mobile-ios/GiniBankAPILibrary/getting-started.html) of the Gini Bank API Library. -## Start SDK +## Screen API -The SDK provides a custom `UIViewController` object, which can be presented modally. It handles the complete process from showing the onboarding until providing a UI for the analysis. -Gini Capture SDK offers two different ways of the networking implementation: - -### Default Networking (Recommended) +The Screen API provides a custom `UIViewController` object, which can be presented modally. It handles the complete process from showing the onboarding until providing a UI for the analysis. +The Screen API, in turn, offers two different ways of implementation: +### UI with Default Networking (Recommended) Using this method you don't need to care about handling the analysis process with the [Gini Bank API Library](https://github.com/gini/bank-api-library-ios), you only need to provide your API credentials and a delegate to get the analysis results. ```swift @@ -34,7 +30,7 @@ import TrustKit let yourPublicPinningConfig = [ kTSKPinnedDomains: [ - "pay-api.gini.net": [ + "api.gini.net": [ kTSKPublicKeyHashes: [ // old *.gini.net public key "cNzbGowA+LNeQ681yMm8ulHxXiGojHE8qAjI+M7bIxU=", @@ -67,14 +63,14 @@ present(viewController, animated: true, completion:nil) #### Retrieve the Analyzed Document -The `AnalysisResult` returned in `GiniCaptureResultsDelegate.giniCaptureAnalysisDidFinishWith(result:)` +The `AnalysisResult` returned in `GiniCaptureResultsDelegate.giniCaptureAnalysisDidFinishWith(result:, sendFeedbackBlock:)` will return the analyzed Gini Bank API document in its `document` property. -When extractions were retrieved without using the Gini Bank API, then the `AnalysisResult.document` will be `nil`. For example when the extractions came from an EPS QR Code. - -### Custom Networking +When extractions were retrieved without using the Gini Bank API, then the `AnalysisResult.document` will be `nil`. For example when the +extractions came from an EPS QR Code. -You can also provide your own networking by implementing the `GiniCaptureNetworkService` and `GiniCaptureResultsDelegate` protocols. Pass your instances to the `UIViewController` initialiser of `GiniCapture` as shown below. +### UI with Custom Networking +You can also provide your own networking by implementing the `GiniCaptureNetworkService` and `GiniCaptureResultsDelegate` protocols. Pass your instances to the UIViewController initialiser of GiniCapture as shown below. ```swift let viewController = GiniCapture.viewController(importedDocuments: visionDocuments, @@ -87,21 +83,41 @@ let viewController = GiniCapture.viewController(importedDocuments: visionDocumen present(viewController, animated: true, completion: nil) ``` - You may also use the [Gini Bank API Library](https://github.com/gini/bank-api-library-ios) or implement communication with the Gini Bank API yourself. +## Component API + +The Component API provides a custom `UIViewController` for each screen. This allows a maximum of flexibility, as the screens can be presented modally, used in a container view or pushed to a navigation view controller. Make sure to add your own navigational elements around the provided views. -## Sending Feedback - TODO +To also use the `GiniConfiguration` with the Component API just use the `GiniCapture.setConfiguration(_:)` as follows: + +```swift +let giniConfiguration = GiniConfiguration() +. +. +. +GiniCapture.setConfiguration(giniConfiguration) +``` + +The components that can be found in the SDK are: +* **Camera**: The actual camera screen to capture the image of the document, to import a PDF or an image or to scan a QR Code (`CameraViewController`). +* **Review**: Offers the opportunity to the user to check the sharpness of the image and eventually to rotate it into reading direction (`ReviewViewController`). +* **Multipage Review**: Allows to check the quality of one or several images and the possibility to rotate and reorder them (`MultipageReviewViewController`). +* **Analysis**: Provides a UI for the analysis process of the document by showing the user capture tips when an image is analyzed or the document information when it is a PDF. In both cases an image preview of the document analyzed will be shown (`AnalysisViewController`). +* **Help**: Helpful tutorials indicating how to use the open with feature, which are the supported file types and how to capture better photos for a good analysis (`HelpMenuViewController`). +* **No results**: Shows some suggestions to capture better photos when there are no results after an analysis (`ImageAnalysisNoResultsViewController`). + +## Sending Feedback Your app should send feedback for the extractions the Gini Bank API delivered. Feedback should be sent only for the extractions the user has seen and accepted (or corrected). -We provide a sample test case [here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK;3.0.0-beta07/CaptureSDK/GiniCaptureSDKExample/Tests/ExtractionFeedbackIntegrationTest.swift) to verify that extraction feedback sending works. +We provide a sample test case [here](https://github.com/gini/gini-mobile-ios/blob/main/CaptureSDK/GiniCaptureSDKExample/Tests/ExtractionFeedbackIntegrationTest.swift) to verify that extraction feedback sending works. You may use it along with the example pdf and json files as a starting point to write your own test case. The sample test case is based on the Bank API documentation's [recommended steps](https://pay-api.gini.net/documentation/#test-example) for testing extraction feedback sending. For additional information about feedback see the [Gini Bank API documentation](https://pay-api.gini.net/documentation/#send-feedback-and-get-even-better-extractions-next-time). -### Default networking implementation - TODO +### Default networking implementation The example below shows how to correct extractions and send feedback using the default networking implementation: @@ -127,47 +143,8 @@ updatedExtractions.map{$0.value}.first(where: {$0.name == "amountToPay"})?.value sendFeedbackBlock(updatedExtractions) ``` -### Custom networking implementation - TODO +### Custom networking implementation If you use your own networking implementation and directly communicate with the Gini Bank API then see [this section](https://pay-api.gini.net/documentation/#submitting-feedback-on-extractions) in its documentation on how to send feedback. -In case you use the [Gini Bank API Library](https://developer.gini.net/gini-mobile-ios/GiniBankAPILibrary/) then see [this section](https://developer.gini.net/gini-mobile-ios/GiniBankAPILibrary/getting-started.html) in its documentation for details. - -## Capturing documents - -To launch the Gini Capture SDK you only need to: - -1. Request camera access via configuring `Info.plist` in your project. - -2. Configure `GiniConfiguration.shared`. The implementation example can be found [here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDKExample/Example%20Swift/AppCoordinator.swift#L32) - -3. Present the `UIViewController`. - -You can find the example [here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDKExample/Example%20Swift/ScreenAPICoordinator.swift#L44) - -4. Handle the extraction results - -For handling the extraction results you need to implement `GiniCaptureResultsDelegate `. -[Here](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDKExample/Example%20Swift/ScreenAPICoordinator.swift#L128) you can find the implementation example. - -5. Cleanup configuration and resources. - -The cleanup step includes the previously called `feedback sending` method. You don't need to implement any extra steps, just follow the recommendations below: - - - Please do cleanup always for all necessary fields, including those that were not extracted. - - - Please do cleanup with final data approved by the user (and not initially extracted only). - - - Please do cleanup after TAN verification. - -```swift -GiniConfiguration.shared.cleanup(paymentRecipient: "Payment Recipient", - paymentReference: "Payment Reference", - paymentPurpose: "Payment Purpose", - iban: "IBAN", - bic: "BIC", - amountToPay: ExtractionAmount(value: 10.242, currency: .EUR)) -``` - -Check out the [example app](https://github.com/gini/gini-mobile-ios/tree/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDKExample/Example%20Swift) to see how an integration could look like. -The following example shows how to launch the Gini Capture SDK and how to handle the extraction results. +In case you use the [Gini Bank API Library](https://developer.gini.net/gini-mobile-ios/GiniBankAPILibrary/) then see [this section](https://developer.gini.net/gini-mobile-ios/GiniBankAPILibrary/getting-started.html) in its documentation for details. \ No newline at end of file diff --git a/Documentation/source/License.md b/Documentation/source/License.md index 4e0c7b3..ef0ac79 100644 --- a/Documentation/source/License.md +++ b/Documentation/source/License.md @@ -3,7 +3,7 @@ License ## Gini Capture SDK for iOS is licensed under a Private License. - Copyright (c) 2014-2023, Gini GmbH + Copyright (c) 2014-2021, Gini GmbH All rights reserved. The Gini Capture SDK is licensed through Gini GmbH ("Gini") and may not be @@ -23,7 +23,7 @@ The Gini Capture SDK uses code from the following libraries: The MIT License (MIT) - Copyright (c) 2014 - 2023 Gini GmbH + Copyright (c) 2014 - 2021 Gini GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -45,7 +45,7 @@ The Gini Capture SDK uses code from the following libraries: ## Gini Capture SDK Pinning for iOS is licensed under a Private License. - Copyright (c) 2014-2023, Gini GmbH + Copyright (c) 2014-2021, Gini GmbH All rights reserved. The Gini Capture SDK is licensed through Gini GmbH ("Gini") and may not be @@ -65,7 +65,7 @@ The Gini Capture SDK Pinning uses code from the following libraries: The MIT License (MIT) - Copyright (c) 2014 - 2023 Gini GmbH + Copyright (c) 2014 - 2021 Gini GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Documentation/source/Migration guide.md b/Documentation/source/Migration guide.md index 23697c7..cb65c99 100644 --- a/Documentation/source/Migration guide.md +++ b/Documentation/source/Migration guide.md @@ -1,4 +1,4 @@ Migrate from the old Gini Capture SDK ===================================== -We've migrated from CocoaPods to Swift Package Manager. Please, find more details in [Getting started](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/getting-started.html). +We've migrated from CocoaPods to Swift Package Manager. Please, find more details in [Installation](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/installation.html). diff --git a/Documentation/source/Migration to version 3.x.x.md b/Documentation/source/Migration to version 3.x.x.md deleted file mode 100644 index 9df2f35..0000000 --- a/Documentation/source/Migration to version 3.x.x.md +++ /dev/null @@ -1,295 +0,0 @@ -Migrate from the Gini Capture SDK 1.x.x to 3.x.x -================================================= - -# Breaking changes - -In version 3.0.0 we modernized our UI. In addition, we simplified how the UI -is customized, introduced centralized customization for the color pallete, typograhy. -We also removed the Component API integration option and unified the public API of the SDK and introduced an easier way to customize certain parts of the UI. - -`GiniConfiguration` is a singleton now. -You don't need to create a new instance of `GiniConfiguration` just use `GiniConfiguration.shared` instead. - -Please, find more details in [Getting started](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/getting-started.html). - -# Migrate from Compoment API - -The Component API allowed more UI customization options at the cost of a more difficult integration and maintenance. It -was based on the view controllers, and you had to manage navigation between them and also update the navigation whenever we introduced -breaking changes. - -Maintaining the Component API along with the simpler Screen API required an increasing amount of effort as we added new -features. We decided therefore to unify both APIs and introduce the ability to inject fully custom UI elements. - -The major benefit of the Component API was the ability to use a custom navigation bar. Via -`GiniConfiguration.shared.customNavigationController` that is still possible with the new public API. - -The following steps will help you migrate to the new public API: - -* Configure the SDK the same way as before by using `GiniConfiguration`. -* If you used a custom navigation bar, then you can now pass `UINavigationViewController` to `GiniConfiguration.shared.customNavigationController`. -* The SDK provides a custom `UIViewController` object, which can be presented modally. It handles the complete process from showing the onboarding until providing a UI for the analysis. - -```swift -// MARK: - Default networking - let viewController = GiniCapture.viewController(withClient: client, - importedDocuments: visionDocuments, - configuration: visionConfiguration, - resultsDelegate: self, - documentMetadata: documentMetadata, - api: .default, - userApi: .default, - trackingDelegate: self) -// MARK: - Custom networking - let viewController = GiniCapture.viewController(importedDocuments: visionDocuments, - configuration: visionConfiguration, - resultsDelegate: self, - documentMetadata: documentMetadata, - trackingDelegate: trackingDelegate, - networkingService: self) -``` - -* Handling the analysis results and receiving the extracted information (error or cancellation) can be handled though `GiniCaptureResultsDelegate` protocol implementation. -* You can also provide your own networking by implementing the `GiniCaptureNetworkService` and `GiniCaptureResultsDelegate` protocols. Pass your instances to the `UIViewController` initialiser of GiniCapture as shown below. -* Remove all code related to interacting with the SDK's specific view controllers. From now on the entry point is the `UIViewController` and customization happens through `GiniConfiguration` and via overriding of images and color resources. -* Use the new UI customization options and follow the [Customization guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/customization-guide.html) to adapt the look of the new UI. - -# Migrate from Screen API - -The new public API is based on the Screen API, so you only need to use the new UI customization options and follow the [Customization guide](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/customization-guide.html) to adapt the look of the new UI. - -# Overview of New UI Customization Options - -To simplify UI customization we introduced global customization options. There is no need to customize each screen separately anymore. - -## Colors - -We are providing a global color palette `GiniColors.xcassets` which you are free to override. The custom colors will be then applied on all screens. -You can find the names of the colors in [GiniColors.xcassets](https://github.com/gini/gini-mobile-ios/tree/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets). - - You can view our color palette here: - - - -## Typography - -We provide a global typography based on text appearance styles from `UIFont.TextStyle`. - - - -To override them in your application please use `GiniConfiguration.updateFont(_ font: UIFont, for textStyle: UIFont.TextStyle)`. For example: - -```swift - // If you need to scale your font please use our method `scaledFont()`. Please, find the example below. - let configuration = GiniBankConfiguration.shared - let customFontToBeScaled = UIFont.scaledFont(UIFont(name: "Avenir", size: 20) ?? UIFont.systemFont(ofSize: 7, weight: .regular), textStyle: .caption1) - configuration.updateFont(customFontToBeScaled, for: .caption1) - - // If you would like to pass us already scaled font. - let customScaledFont = UIFontMetrics(forTextStyle: .caption2).scaledFont(for: UIFont.systemFont(ofSize: 28)) - configuration.updateFont(customScaledFont, for: .caption2) - -``` - -## Images - -Images customization is done via overriding of [GiniImages.xcassets](https://github.com/gini/gini-mobile-ios/tree/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets) resources. - -## Text - - Text customization is done via overriding of string resources. - - You can find all the string resources in [Localizable.strings](https://github.com/gini/gini-mobile-ios/blob/GiniCaptureSDK%3B3.0.0-beta07/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings). - -# UI Elements - -Certain elements of the UI can now be fully customized via UI injection. It utilizes view adapter interfaces which you -can implement and pass to `GiniConfiguration` when configuring the SDK. These interfaces declare the contract the injected -view has to fulfill and allow the SDK to ask for your view instance when needed. - -## Top Navigation Bar - -To inject your own navigation bar view you need to pass your navigation view controller to -`GiniConfiguration.shared.customNavigationController`. -The view from the custom navigation view controller will then be displayed on all screens as the top navigation bar. - -## Bottom Navigation Bar - -You can opt to show a bottom navigation bar. To enable it pass `true` to -`GiniConfiguration.shared.bottomNavigationBarEnabled`. - -**Note**: The top navigation bar will still be used, but its functionality will be limited to showing the screen's title and -an optional close button. Please inject a custom top navigation bar if your design requires it even if you have enabled the bottom navigation bar. - -# Migrate to the new UI - -## Onboarding screens - -The new onboarding screen uses the global UI customization options. You can discard the old screen specific -customizations. - -Images and text are onboarding page specific and need to be customized for each page. - -[here][https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/features.html#onboarding] and [here](https://www.figma.com/file/1985HMF83siAXmysSn3dC6/iOS-Gini-Capture-SDK-3.0.0-UI-Customisation?node-id=243%3A3305&t=6sAk7LGf1mi3zV9L-1). - -### Breaking Changes - -#### Setting Custom Onboarding Pages - -The `OnboardingPage` struct was changed to also allow setting a title for the page and inject a view for the -illustration. - -If you are setting custom onboarding pages, then you have to create the `OnboardingPage` as shown in the example -below: - -```swift - configuration.customOnboardingPages = [OnboardingPage(imageName: "captureSuggestion1", title: "Page 1", description: "Description for page 1")] -``` - -### New Features - -#### Custom Illustration Views - -By implementing the `OnboardingIllustrationAdapter` interface and passing it to either `GiniCapture` or the -`OnboardingPage` constructor you can inject any custom view for the illustration. - -If you need to animate the illustrations on the onboarding pages implement the `OnboardingIllustrationAdapter` interface to inject a view that can animate images (e.g., `Lottie`) and pass it to the relevant onboarding illustration adapter setters (e.g., -`onboardingAlignCornersIllustrationAdapter`, -`onboardingLightingIllustrationAdapter`, -`onboardingMultiPageIllustrationAdapter`, -`onboardingQRCodeIllustrationAdapter`) - when configuring the `GiniConfiguration.shared` instance. - -## Camera screen - -The new camera screen uses the global UI customization options. You can discard the old screen specific -customizations. - -### New Features - -#### QR Code only - -During QR Code only mode the capture and import controls will be hidden from the camera screen. - -For enabling QR code only mode the both flags `GiniConfiguration.shared.qrCodeScanningEnabled` and `GiniConfiguration.shared.onlyQRCodeScanningEnabled` should be `true`. - -## Help screens - -The new help screens use the global UI customization options. You can discard the old screen specific customizations. - -### Breaking Changes - -String keys changed: -`ginicapture.help.menu.firstItem` → `ginicapture.help.menu.tips` -`ginicapture.help.menu.secondItem`→ `ginicapture.help.menu.formats` -`ginicapture.help.menu.thirdItem` → `ginicapture.help.menu.import` - -### New Features - -#### Bottom navigation bar - -You can show a bottom navigation bar by passing true to `GiniConfiguration.shared.bottomNavigationBarEnabled`. There is a default implementation, but you can also use -your own by implementing the `HelpBottomNavigationBarAdapter` interface and passing it to `GiniConfiguration.shared.helpNavigationBarBottomAdapter`. - -You can find more details [here][https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/features.html#help-screen-customization] and [here](https://www.figma.com/file/1985HMF83siAXmysSn3dC6/iOS-Gini-Capture-SDK-3.0.0-UI-Customisation?node-id=141%3A2328&t=6sAk7LGf1mi3zV9L-1). - -## Analysis screen - -The new analysis screen uses the global UI customization options. You can discard the old screen specific customizations. - -### Breaking Changes - -String keys removed: -`ginicapture.analysis.suggestion.header` - -The following string keys now represent suggestion titles with new keys added for describing the tips. -`ginicapture.analysis.suggestion.1` -`ginicapture.analysis.suggestion.2` -`ginicapture.analysis.suggestion.3` -`ginicapture.analysis.suggestion.4` -`ginicapture.analysis.suggestion.5` - -### New Features - -#### Custom loading indicator - -You can show a custom loading indicator with custom animation support on the center of the screen. -Your custom loading indicator should implement `CustomLoadingIndicatorAdapter` interface and be passed to `GiniConfiguration.shared.customLoadingIndicator`. -This loading indicator is also used on the `Camera screen` when loading data for a valid QR code. - -You can find more details [here](https://www.figma.com/file/1985HMF83siAXmysSn3dC6/iOS-Gini-Capture-SDK-3.0.0-UI-Customisation?node-id=501%3A7494&t=6sAk7LGf1mi3zV9L-1). - -## Review screen - -The new review screen uses the global UI customization options. You can discard the old screen specific customizations. - -### Breaking Changes - -We unified UI for the single page and multi pages options. -We removed rotation and reorder functionalities. -Tips view was removed as well. - -### New Features - -#### Custom loading indicator - -You can show a custom loading indicator with custom animation support on the process button of the screen. -Your custom loading indicator should implement `OnButtonLoadingIndicatorAdapter` interface and be passed to `GiniConfiguration.shared.onButtonLoadingIndicator`. - -#### Bottom navigation bar - -You can show a bottom navigation bar by passing true to `GiniConfiguration.shared.bottomNavigationBarEnabled`. There is a default implementation, but you can also use -your own by implementing the `ReviewScreenBottomNavigationBarAdapter` interface and passing it to `GiniConfiguration.shared.reviewNavigationBarBottomAdapter`. - -You can find more details [here](https://www.figma.com/file/1985HMF83siAXmysSn3dC6/iOS-Gini-Capture-SDK-3.0.0-UI-Customisation?node-id=261%3A8256&t=6sAk7LGf1mi3zV9L-1). - -## No results screen - -The new no results screen uses the global UI customization options. You can discard the old screen specific -customizations. - -### Breaking Changes - -#### Removed localization keys: - -`ginicapture.noresults.warning` -`ginicapture.noresults.collection.header` -`ginicapture.noresults.gotocamera` -`ginicapture.noresults.warningHelpMenu` - -### New features - -#### New localization keys: - -`ginicapture.noresult.enterManually` -`ginicapture.noresult.retakeImages` - -#### Option to enter details manually - -You can show your own UI for data input if an error occurred and the user clicks the "Enter manually" button on the error screen. -For this you must to implement `GiniCaptureResultsDelegate.giniCaptureDidEnterManually() `. - -You can find more details [here](https://www.figma.com/file/1985HMF83siAXmysSn3dC6/iOS-Gini-Capture-SDK-3.0.0-UI-Customisation?node-id=263%3A6989&t=7wXW9XyhTUcmp5sk-1) and [here](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/customization-guide.html#no-result-screen). - -## Error screen - -The new error screen uses the global UI customization options. - -### Breaking Changes - -Showing errors during usage of the SDK was changed from snackbar to a whole new screen. - -### New Features - -#### New UI - -The new error screen gives options to retake photos or enter details manually and displays errors with more detailed description. - -You can find more details [here](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/customization-guide.html#error-screen). - -#### Option to enter details manually - -You can show your own UI for data input if an error occured and the user clicks the "Enter manually" button on the error screen. -For this you must to implement `GiniCaptureResultsDelegate.giniCaptureDidEnterManually() `. - -You can find more details [here](https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/features.html#error-screen-customization) and [here]((https://developer.gini.net/gini-mobile-ios/GiniCaptureSDK/3.0.0-beta07/customization-guide.html#error-screen). diff --git a/Documentation/source/Open with guide.md b/Documentation/source/Open with guide.md index 92cde4b..f8f513a 100644 --- a/Documentation/source/Open with guide.md +++ b/Documentation/source/Open with guide.md @@ -58,7 +58,7 @@ You can also add these by going to your target’s *Info* tab and enter the valu In order to allow GiniCapture library to handle files imported from other apps and to show the _Open With tutorial_ in the _Help_ menu, it is necessary to indicate it in the `GiniConfiguration`. ```swift - let giniConfiguration = GiniConfiguration.shared + let giniConfiguration = GiniConfiguration() ... ... giniConfiguration.openWithEnabled = true diff --git a/Documentation/source/QR Code scanning guide.md b/Documentation/source/QR Code scanning guide.md index 6ac8c08..7c3a1ce 100644 --- a/Documentation/source/QR Code scanning guide.md +++ b/Documentation/source/QR Code scanning guide.md @@ -9,7 +9,7 @@ Enable QR code scanning The QR code scanning feature is disabled by default, so in case that you what to use it you just need to enable it in the `GiniConfiguration`, like so: ```swift -let giniConfiguration = GiniConfiguration.shared +let giniConfiguration = GiniConfiguration() ... ... ... diff --git a/Package.swift b/Package.swift index bd1d223..ae123b9 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), - .package(name: "GiniBankAPILibrary", url: "https://github.com/gini/bank-api-library-ios.git", .exact("3.0.0-beta07")), + .package(name: "GiniBankAPILibrary", url: "https://github.com/gini/bank-api-library-ios.git", .exact("1.5.0")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/README.md b/README.md index 546cbd9..e8f301b 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,13 @@ To inject your API credentials into the Example app, just add to the Example dir ## Requirements -- iOS 12+ +- iOS 11+ - Xcode 12+ **Note:** In order to have better analysis results it is highly recommended to enable only devices with 8MP camera and flash. These devices would be: -* iPhones with iOS 12 or higher. +* iPhones with iOS 11 or higher. * iPad Pro devices (iPad Air 2 and iPad Mini 4 have 8MP camera but no flash). ## Author diff --git a/Sources/GiniCaptureSDK/Core/Custom views/BackButtonBottomNavigationBar.swift b/Sources/GiniCaptureSDK/Core/Custom views/BackButtonBottomNavigationBar.swift deleted file mode 100644 index a261e2f..0000000 --- a/Sources/GiniCaptureSDK/Core/Custom views/BackButtonBottomNavigationBar.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// BackButtonBottomNavigationBar.swift -// -// -// Created by Krzysztof Kryniecki on 04/10/2022. -// - -import UIKit - -/** -A custom view that displays a back button on the bottom navigation bar. - -This class is a subclass of UIView. -*/ - -final class BackButtonBottomNavigationBar: UIView { - - /// The button that displays the back arrow icon. - @IBOutlet public weak var backButton: UIButton! - - public override func awakeFromNib() { - super.awakeFromNib() - setupView() - } - - func setupView() { - backButton.setTitle("", for: .normal) - let image = UIImageNamedPreferred(named: "arrowBack") ?? UIImage() - backButton.setImage( - image.tintedImageWithColor(.GiniCapture.accent1), - for: .normal) - backgroundColor = GiniColor( - light: UIColor.GiniCapture.light1, - dark: UIColor.GiniCapture.dark1 - ).uiColor() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/BottomLabelButton.swift b/Sources/GiniCaptureSDK/Core/Custom views/BottomLabelButton.swift deleted file mode 100644 index 5da19f2..0000000 --- a/Sources/GiniCaptureSDK/Core/Custom views/BottomLabelButton.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// BottomLabelButton.swift -// -// -// Created by Krzysztof Kryniecki on 06/09/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -final class BottomLabelButton: UIView { - var didTapButton: (() -> Void)? - - lazy var actionButton: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(didPressButton(_:)), for: .touchUpInside) - return button - }() - - lazy var actionLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = true - label.textAlignment = .center - label.adjustsFontSizeToFitWidth = true - label.numberOfLines = 1 - label.minimumScaleFactor = 2 / label.font.pointSize - label.sizeToFit() - return label - }() - - lazy var iconView: UIImageView = { - let image = UIImageView() - image.contentMode = .scaleAspectFit - image.translatesAutoresizingMaskIntoConstraints = false - return image - }() - - private lazy var contentView: UIView = { - let contentView = UIView() - contentView.translatesAutoresizingMaskIntoConstraints = false - - return contentView - }() - - init() { - super.init(frame: .zero) - addSubview(contentView) - contentView.addSubview(actionLabel) - contentView.addSubview(iconView) - addSubview(actionButton) - setupConstraints() - } - - required init?(coder aDecoder: NSCoder) { - super.init(frame: .zero) - addSubview(contentView) - contentView.addSubview(actionLabel) - contentView.addSubview(iconView) - addSubview(actionButton) - } - - override func awakeFromNib() { - super.awakeFromNib() - isAccessibilityElement = true - actionLabel.isAccessibilityElement = false - actionButton.isAccessibilityElement = false - iconView.isAccessibilityElement = false - accessibilityTraits = .button - setupConstraints() - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - actionButton.topAnchor.constraint(equalTo: topAnchor), - actionButton.bottomAnchor.constraint(equalTo: bottomAnchor), - actionButton.trailingAnchor.constraint(equalTo: trailingAnchor), - actionButton.leadingAnchor.constraint(equalTo: leadingAnchor), - - contentView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 5), - contentView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -5), - contentView.centerYAnchor.constraint(equalTo: centerYAnchor), - contentView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentView.leadingAnchor.constraint(equalTo: leadingAnchor), - - iconView.topAnchor.constraint(equalTo: contentView.topAnchor), - iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - iconView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - iconView.heightAnchor.constraint(equalToConstant: 20), - - actionLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 5), - actionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - actionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - actionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } - - func setupButton(with image: UIImage, name: String) { - iconView.image = image - actionLabel.text = name - accessibilityValue = name - } - - @objc fileprivate func didPressButton(_ sender: UIButton) { - didTapButton?() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/ButtonsView.swift b/Sources/GiniCaptureSDK/Core/Custom views/ButtonsView.swift deleted file mode 100644 index bffcdd8..0000000 --- a/Sources/GiniCaptureSDK/Core/Custom views/ButtonsView.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// ButtonsView.swift -// -// -// Created by Krzysztof Kryniecki on 21/11/2022. -// - -import UIKit - -class ButtonsView: UIView { - var giniConfiguration = GiniConfiguration.shared - lazy var enterButton: MultilineTitleButton = { - let button = MultilineTitleButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(firstButtonTitle, for: .normal) - button.accessibilityLabel = firstButtonTitle - return button - }() - - lazy var retakeButton: MultilineTitleButton = { - let button = MultilineTitleButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(secondButtonTitle, for: .normal) - button.accessibilityLabel = secondButtonTitle - return button - }() - - lazy var buttonsView: UIStackView = { - let stackView = UIStackView() - stackView.addArrangedSubview(enterButton) - stackView.addArrangedSubview(retakeButton) - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.distribution = .fillEqually - stackView.axis = .vertical - stackView.spacing = 12 - return stackView - }() - - let firstButtonTitle: String - let secondButtonTitle: String - - init(firstTitle: String, secondTitle: String) { - firstButtonTitle = firstTitle - secondButtonTitle = secondTitle - super.init(frame: CGRect.zero) - addSubview(buttonsView) - configureButtons() - NSLayoutConstraint.activate([ - buttonsView.leadingAnchor.constraint(equalTo: leadingAnchor), - buttonsView.trailingAnchor.constraint(equalTo: trailingAnchor), - buttonsView.topAnchor.constraint(equalTo: topAnchor), - buttonsView.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - } - - private func configureButtons() { - retakeButton.titleLabel?.font = giniConfiguration.textStyleFonts[.bodyBold] - retakeButton.configure(with: giniConfiguration.primaryButtonConfiguration) - enterButton.titleLabel?.font = giniConfiguration.textStyleFonts[.bodyBold] - enterButton.configure(with: giniConfiguration.secondaryButtonConfiguration) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/ContainerNavigationController.swift b/Sources/GiniCaptureSDK/Core/Custom views/ContainerNavigationController.swift index c8f9df2..65c2945 100644 --- a/Sources/GiniCaptureSDK/Core/Custom views/ContainerNavigationController.swift +++ b/Sources/GiniCaptureSDK/Core/Custom views/ContainerNavigationController.swift @@ -14,47 +14,49 @@ import UIKit to keep a strong reference outside of the Gini Capture SDK. */ final class ContainerNavigationController: UIViewController { - + var rootViewController: UINavigationController var coordinator: Coordinator? var giniConfiguration: GiniConfiguration - + override var shouldAutorotate: Bool { return true } - + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return UIDevice.current.isIpad ? .all : .portrait } - + override var preferredStatusBarStyle: UIStatusBarStyle { return giniConfiguration.statusBarStyle } - + init(rootViewController: UINavigationController, parent: Coordinator? = nil, giniConfiguration: GiniConfiguration = GiniConfiguration.shared) { self.rootViewController = rootViewController self.coordinator = parent self.giniConfiguration = giniConfiguration + setStatusBarStyle(to: giniConfiguration.statusBarStyle) super.init(nibName: nil, bundle: nil) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(rootViewController:parent:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = rootViewController.navigationBar.backgroundColor + view.backgroundColor = .white addChild(rootViewController) view.addSubview(rootViewController.view) rootViewController.willMove(toParent: self) rootViewController.didMove(toParent: self) } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() rootViewController.view.frame = self.view.bounds } } + diff --git a/Sources/GiniCaptureSDK/Core/Custom views/ContainerViewController.swift b/Sources/GiniCaptureSDK/Core/Custom views/ContainerViewController.swift index be3dd06..1c3ce6f 100644 --- a/Sources/GiniCaptureSDK/Core/Custom views/ContainerViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Custom views/ContainerViewController.swift @@ -9,21 +9,21 @@ import UIKit protocol ContainerViewController: AnyObject { - + var containerView: UIView { get } var contentController: UIViewController { get } - + func displayContent(_ controller: UIViewController) - + } extension ContainerViewController where Self: UIViewController { - + func displayContent(_ controller: UIViewController) { self.addChild(controller) controller.view.frame = self.containerView.bounds self.containerView.addSubview(controller.view) controller.didMove(toParent: self) } - + } diff --git a/Sources/GiniCaptureSDK/Core/Custom views/FileImportButtonView.swift b/Sources/GiniCaptureSDK/Core/Custom views/FileImportButtonView.swift index 79fd417..1e5d507 100644 --- a/Sources/GiniCaptureSDK/Core/Custom views/FileImportButtonView.swift +++ b/Sources/GiniCaptureSDK/Core/Custom views/FileImportButtonView.swift @@ -8,13 +8,14 @@ import UIKit final class FileImportButtonView: UIView { - + var didTapButton: (() -> Void)? + fileprivate let giniConfiguration: GiniConfiguration fileprivate var documentImportButtonImage: UIImage? { return UIImageNamedPreferred(named: "documentImportButton") } - + lazy var importFileButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false @@ -24,30 +25,26 @@ final class FileImportButtonView: UIView { button.imageEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) return button }() - + lazy var importFileSubtitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = .localized(resource: CameraStrings.importFileButtonLabel) - label.font = GiniConfiguration.shared.customFont.with(weight: .regular, size: 12, style: .footnote) + label.font = giniConfiguration.customFont.with(weight: .regular, size: 12, style: .footnote) label.textAlignment = .center label.textColor = .white label.minimumScaleFactor = 10 / label.font.pointSize label.adjustsFontSizeToFitWidth = true - + return label }() - + init(giniConfiguration: GiniConfiguration = .shared) { - super.init(frame: .zero) - - } - - required init?(coder aDecoder: NSCoder) { + self.giniConfiguration = giniConfiguration super.init(frame: .zero) addSubview(importFileButton) addSubview(importFileSubtitleLabel) - + importFileButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true importFileButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true importFileButton.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true @@ -59,10 +56,15 @@ final class FileImportButtonView: UIView { importFileSubtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true importFileSubtitleLabel.topAnchor.constraint(equalTo: importFileButton.bottomAnchor, constant: 0).isActive = true - } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + @objc fileprivate func showImportFileSheet(_ sender: UIButton) { didTapButton?() } - + } diff --git a/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButton.swift b/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButton.swift deleted file mode 100644 index 3fb6d60..0000000 --- a/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButton.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// GiniBarButton.swift -// -// -// Created by David Vizaknai on 15.03.2023. -// - -import UIKit - -/// - Note: internal only - -/** - An enum representing the types of buttons that can appear in a navigation bar. - - Use the `BarButtonType` enum to specify the type of button to display in a navigation bar. - - - cancel: A cancel button. Used to cancel or dismiss the current view controller. - - - help: A help button. Used to open the help screen. - - - back(title: String): A back button. Used to navigate back to the previous view controller. - The `title` parameter is a string that specifies the title to display on the button. - - - done: A done button. Used to open the selected images from the gallery picker. - */ -public enum BarButtonType { - case cancel - case help - case back(title: String) - case done -} - -/** - A custom navigation bar button that displays a stack of subviews. - - Use the `GiniBarButton` class to create a custom toolbar button with a stack of subviews. This allows you to customize the appearance and behavior of the button by adding and configuring subviews within the stack view. The `GiniBarButton` class provides a convenient interface for creating a `UIBarButtonItem` with a custom view, and includes several helper methods for managing the subviews of the stack view. - */ - -public final class GiniBarButton { - private let configuration = GiniConfiguration.shared - private let titleLabel = UILabel() - private let imageView = UIImageView() - private let stackView = UIStackView() - - // MARK: - Public methods - - /** - Adds a tap gesture recognizer to the stack view with the specified target and action. - - Use the `addAction(_ target: Any?, _ action: Selector)` function to call the specified action. When the tap gesture is recognized, the action method specified by the `action` parameter will be called on the `target` object. - - - Parameters: - - target: The object that will handle the action message. If this parameter is `nil`, the action message will be sent to the first responder. - - action: The action method to call on the target object when the tap gesture is recognized. - */ - public func addAction(_ target: Any?, _ action: Selector) { - let tapRecognizer = UITapGestureRecognizer(target: target, action: action) - stackView.addGestureRecognizer(tapRecognizer) - } - - /** - A computed property that returns a `UIBarButtonItem` with the `stackView` as its custom view. - - Use the `barButton` property to create a `UIBarButtonItem` with the `stackView` as its custom view. This allows you to customize the appearance and behavior of the button by adding and configuring subviews within the stack view. - - - Returns: A `UIBarButtonItem` object with the `stackView` as its custom view. - */ - public var barButton: UIBarButtonItem { - return UIBarButtonItem(customView: stackView) - } - - /** - Initializes a new `GiniBarButton` object with the specified button type. - - Use the `init(ofType:)` initializer to create a new `GiniBarButton` object with the specified `BarButtonType`. The `BarButtonType` parameter determines the appearance and behavior of the button. - - - Parameter type: The `BarButtonType` that determines the appearance and behavior of the button. - */ - public init(ofType type: BarButtonType) { - setupContent(basedOnType: type) - setupViews() - } - - // MARK: - Private methods - private func setupViews() { - stackView.distribution = .fill - stackView.axis = .horizontal - stackView.spacing = Constants.spacing - stackView.backgroundColor = .clear - - imageView.contentMode = .scaleAspectFit - titleLabel.textColor = .GiniCapture.accent1 - titleLabel.adjustsFontForContentSizeCategory = true - - if imageView.image != nil { - stackView.addArrangedSubview(imageView) - } - - if titleLabel.text != nil { - stackView.addArrangedSubview(titleLabel) - } - } - - private func setupContent(basedOnType type: BarButtonType) { - var buttonTitle: String? - var icon: UIImage? - - switch type { - case .cancel: - buttonTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.analysis.back", - comment: "Cancel") - icon = UIImageNamedPreferred(named: "barButton_cancel") - case .help: - buttonTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.camera.help", - comment: "Help") - icon = UIImageNamedPreferred(named: "barButton_help") - case .back(title: let title): - buttonTitle = title - icon = UIImageNamedPreferred(named: "barButton_back") - case .done: - buttonTitle = NSLocalizedStringPreferredFormat("ginicapture.imagepicker.openbutton", - comment: "Done") - icon = UIImageNamedPreferred(named: "barButton_done") - } - - let buttonTitleIsEmpty = buttonTitle == nil || buttonTitle!.isEmpty - - // If there is no image nor any text for the button, the app will crash in debug mode. - if buttonTitleIsEmpty && icon == nil { - assertionFailure("You need to provide at least a valid string or an icon" + - " for the navigation bar button of type: \(type)") - } - - imageView.image = icon - - if let buttonTitle = buttonTitle, buttonTitle.isNotEmpty { - titleLabel.attributedText = NSAttributedString(string: buttonTitle, - attributes: textAttributes()) - stackView.accessibilityValue = buttonTitle - } - } - - private func textAttributes() -> [NSAttributedString.Key: Any] { - var attributes: [NSAttributedString.Key: Any] - if let font = configuration.textStyleFonts[.bodyBold] { - if font.pointSize > Constants.maximumFontSize { - attributes = [NSAttributedString.Key.font: font.withSize(Constants.maximumFontSize)] - } else { - attributes = [NSAttributedString.Key.font: font] - } - } else { - let font = configuration.textStyleFonts[.bodyBold] as Any - attributes = [NSAttributedString.Key.font: font] - } - - return attributes - } -} - -private extension GiniBarButton { - enum Constants { - static let maximumFontSize: CGFloat = 24 - static let spacing: CGFloat = 4 - } -} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButtonItem.swift b/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButtonItem.swift new file mode 100644 index 0000000..a341654 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButtonItem.swift @@ -0,0 +1,53 @@ +// +// GiniBarButtonItem.swift +// GiniCapture +// +// Created by Peter Pult on 13/07/16. +// Copyright © 2016 Gini GmbH. All rights reserved. +// + +import UIKit + +public final class GiniBarButtonItem: UIBarButtonItem { + + public init(image: UIImage?, title: String?, style: UIBarButtonItem.Style, target: AnyObject?, action: Selector) { + super.init() + + let button = UIButton(type: .system) + + if let image = image { + button.setImage(image, for: .normal) + } + + if let title = title { + let font = GiniConfiguration.shared.navigationBarItemFont + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font] + button.setAttributedTitle(NSAttributedString(string: title, attributes: attributes), for: .normal) + } + + button.sizeToFit() + + button.imageEdgeInsets = UIEdgeInsets(top: button.imageEdgeInsets.top, + left: button.imageEdgeInsets.left - 10, + bottom: button.imageEdgeInsets.bottom, + right: button.imageEdgeInsets.right) + + button.addTarget(target, action: action, for: .touchUpInside) + button.titleLabel?.textColor = GiniConfiguration.shared.navigationBarItemTintColor + button.titleLabel?.tintColor = GiniConfiguration.shared.navigationBarItemTintColor + + customView = button + + self.style = style + + // Set accessibility label on all elements + self.accessibilityLabel = title + + + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/MultilineTitleButton.swift b/Sources/GiniCaptureSDK/Core/Custom views/MultilineTitleButton.swift deleted file mode 100644 index 728daa0..0000000 --- a/Sources/GiniCaptureSDK/Core/Custom views/MultilineTitleButton.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// MultilineTitleButton.swift -// -// -// Created by Krzysztof Kryniecki on 29/08/2022. -// - -import UIKit - - -/** -- internal only - - `MultilineTitleButton` is a subclass of `UIButton`and it allows for multiple lines of text - to be displayed in the button's title, and the button's size is adjusted to fit the text. The content hugging priority is set s - o that the button's width and height can be adjusted by the Auto Layout system. - **/ - -public class MultilineTitleButton: UIButton { - - /** - `init?(coder:)` is used for creating an instance of the class from a nib file - **/ - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - commonInit() - } - - /** - `init(frame:)` is used for creating an instance of the class programmatically - */ - public override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - private func commonInit() { - contentEdgeInsets = UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4) - titleLabel?.adjustsFontForContentSizeCategory = true - titleLabel?.numberOfLines = 0 - titleLabel?.textAlignment = .center - setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .vertical) - setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .horizontal) - } - - /** - The class overrides the `intrinsicContentSize` property to adjust the size of the button - based on the intrinsic size of the title label and the content edge insets. - */ - public override var intrinsicContentSize: CGSize { - let size = titleLabel!.intrinsicContentSize - return CGSize( - width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, - height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom) - } - - /** - It also overrides the `layoutSubviews()` function to set the preferred maximum - layout width of the title label to the width of the button. - */ - public override func layoutSubviews() { - super.layoutSubviews() - titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width - } -} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/NoticeView.swift b/Sources/GiniCaptureSDK/Core/Custom views/NoticeView.swift new file mode 100644 index 0000000..9484912 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Custom views/NoticeView.swift @@ -0,0 +1,174 @@ +// +// NoticeView.swift +// GiniCapture +// +// Created by Peter Pult on 01/07/16. +// Copyright © 2016 Gini. All rights reserved. +// + +import UIKit + +@objc public enum NoticeActionType: Int { + case retry, retake + + var title: String { + switch self { + case .retry: + return .localized(resource: MultipageReviewStrings.retryActionButton) + case .retake: + return .localized(resource: MultipageReviewStrings.retakeActionButton) + } + } +} + +/** + Block that will be executed when a notice is tapped. Can be used to restart a + process or to give the user further guidance. + + - note: Screen API only. + */ + +struct NoticeAction { + let title: String + let action: () -> Void +} + +enum NoticeType { + case information, error +} + +final class NoticeView: UIView { + + // User interface + lazy var textLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 2 + label.textAlignment = .left + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 12 / 14 + label.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .horizontal) + label.font = giniConfiguration.customFont.with(weight: .regular, size: 14, style: .body) + + return label + }() + + lazy var actionButton: UIButton = { + let button = UIButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: .horizontal) + + button.titleLabel?.textColor = giniConfiguration.noticeErrorTextColor + button.titleLabel?.font = giniConfiguration.customFont.with(weight: .regular, size: 16, style: .caption1) + button.addTarget(self, action: #selector(self.didTapActionButton), for: .touchUpInside) + return button + }() + + // Properties + fileprivate let giniConfiguration: GiniConfiguration + var userAction: NoticeAction? { + didSet { + actionButton.setTitle(userAction?.title, for: .normal) + } + } + + init(text: String, + type: NoticeType = .information, + giniConfiguration: GiniConfiguration = .shared, + noticeAction: NoticeAction? = nil) { + self.giniConfiguration = giniConfiguration + super.init(frame: CGRect.zero) + + let textColor: UIColor + switch type { + case .information: + textColor = giniConfiguration.noticeInformationTextColor + backgroundColor = giniConfiguration + .noticeInformationBackgroundColor + .withAlphaComponent(0.8) + case .error: + textColor = giniConfiguration.noticeErrorTextColor + backgroundColor = giniConfiguration + .noticeErrorBackgroundColor + .withAlphaComponent(0.9) + } + + if let noticeAction = noticeAction { + userAction = noticeAction + actionButton.titleLabel?.textColor = textColor + addSubview(actionButton) + } + + textLabel.text = text + textLabel.textColor = textColor + addSubview(textLabel) + + addConstraints() + alpha = 0.0 + } + + @objc func didTapActionButton() { + UIImpactFeedbackGenerator().impactOccurred() + self.userAction?.action() + } + + /** + Returns an object initialized from data in a given unarchiver. + + - warning: Not implemented. + */ + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func addConstraints() { + Constraints.active(item: self, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 70) + // Text label + Constraints.active(item: textLabel, attr: .top, relatedBy: .equal, to: self, attr: .top, constant: 16) + Constraints.active(item: textLabel, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, constant: -16) + Constraints.active(item: textLabel, attr: .leading, relatedBy: .equal, to: self, attr: .leading, constant: 20, + priority: 999) + + if userAction != nil { + Constraints.active(item: actionButton, attr: .leading, relatedBy: .equal, to: textLabel, attr: .trailing, + constant: 16) + Constraints.active(item: actionButton, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, + constant: -16) + Constraints.active(item: actionButton, attr: .top, relatedBy: .equal, to: self, attr: .top) + Constraints.active(item: actionButton, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom) + } else { + Constraints.active(item: textLabel, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, + constant: -16) + } + } + +} + +// MARK: - Toggle options + +extension NoticeView { + func show(_ animated: Bool = true) { + if animated { + UIView.animate(withDuration: 0.5) { + self.alpha = 1.0 + } + } else { + self.alpha = 1.0 + } + } + + func hide(_ animated: Bool = true, completion: (() -> Void)?) { + if animated { + UIView.animate(withDuration: 0.5, animations: { + self.alpha = 0.0 + }, completion: { _ in + completion?() + }) + } else { + self.alpha = 0.0 + completion?() + } + + } +} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/OpaqueViewFactory.swift b/Sources/GiniCaptureSDK/Core/Custom views/OpaqueViewFactory.swift new file mode 100644 index 0000000..ef34fbc --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Custom views/OpaqueViewFactory.swift @@ -0,0 +1,27 @@ +// +// OpaqueViewFactory.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 6/13/18. +// + +import UIKit + +public enum OpaqueViewStyle { + case blurred(style: UIBlurEffect.Style) + case dimmed +} + +struct OpaqueViewFactory { + + static func create(with style: OpaqueViewStyle) -> UIView { + switch style { + case .blurred(let blurStyle): + return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) + case .dimmed: + let view = UIView() + view.backgroundColor = UIColor.black.withAlphaComponent(0.8) + return view + } + } +} diff --git a/Sources/GiniCaptureSDK/Core/Custom views/ToolTipView.swift b/Sources/GiniCaptureSDK/Core/Custom views/ToolTipView.swift new file mode 100644 index 0000000..92adf19 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Custom views/ToolTipView.swift @@ -0,0 +1,442 @@ +// +// ToolTipView.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 9/21/17. +// + +import Foundation +import UIKit + +final class ToolTipView: UIView { + + enum ToolTipPosition { + case above + case below + case left + case right + } + + fileprivate var arrowWidth: CGFloat = 30 + fileprivate var arrowHeight: CGFloat = 20 + fileprivate var closeButtonWidth: CGFloat = 20 + fileprivate var closeButtonHeight: CGFloat = 20 + fileprivate var itemSeparation: CGFloat = 16 + fileprivate var minimunDistanceToRefView: UIEdgeInsets + fileprivate var margin:(top: CGFloat, left: CGFloat, right: CGFloat, bottom: CGFloat) = (20, 20, 20, 20) + fileprivate var maxWidth: CGFloat = 414 + fileprivate var padding:(top: CGFloat, left: CGFloat, right: CGFloat, bottom: CGFloat) = (16, 16, 16, 16) + + fileprivate var textWidth: CGFloat { + guard let superview = superview else { return 0 } + let width: CGFloat + if superview.frame.width > maxWidth && superview.frame.height > maxWidth { + width = maxWidth + } else { + width = min(superview.frame.width, superview.frame.height) + } + return width - padding.left - padding.right - margin.left - margin.right - closeButtonWidth - itemSeparation + } + + fileprivate var text: String + fileprivate var toolTipPosition: ToolTipPosition + fileprivate var textSize: CGSize = .zero + + fileprivate var arrowView: UIView + fileprivate var closeButton: UIButton + fileprivate let referenceView: UIView + fileprivate var textLabel: UILabel + fileprivate var tipContainer: UIView + + var willDismiss: (() -> Void)? + var willDismissOnCloseButtonTap: (() -> Void)? + + + init(text: String, + giniConfiguration: GiniConfiguration, + referenceView: UIView, + superView: UIView, + position: ToolTipPosition, + distanceToRefView: UIEdgeInsets = .zero) { + + self.text = text + self.referenceView = referenceView + self.toolTipPosition = position + self.textLabel = UILabel() + self.closeButton = UIButton() + self.tipContainer = UIView() + self.minimunDistanceToRefView = distanceToRefView + self.arrowView = ToolTipView.arrow(withHeight: arrowHeight, + width: arrowWidth, + color: .white, + position: position) + + super.init(frame: .zero) + superView.addSubview(self) + alpha = 0 + + let font = giniConfiguration.customFont.with(weight: .regular, size: 14, style: .body) + self.textSize = size(forText: text, withFont: font) + self.addTipContainer(backgroundColor: giniConfiguration.fileImportToolTipBackgroundColor) + self.addTextLabel(withText: text, textColor: giniConfiguration.fileImportToolTipTextColor, font: font) + self.addCloseButton(withColor: giniConfiguration.fileImportToolTipCloseButtonColor) + self.addArrow() + self.addShadow() + + self.arrangeFrame(withSuperView: superView) + self.arrangeArrow(withSuperView: superView) + self.setupConstraints() + } + + override func layoutSubviews() { + super.layoutSubviews() + self.arrangeArrow(withSuperView: superview) + } + + func arrangeViews() { + self.arrangeFrame(withSuperView: superview) + self.arrangeArrow(withSuperView: superview) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("NSCoding not supported. Use init(text) instead!") + } + + // MARK: Add views + fileprivate func addTipContainer(backgroundColor color: UIColor) { + self.addSubview(tipContainer) + self.tipContainer.backgroundColor = color + self.tipContainer.layer.cornerRadius = 2.0 + } + + fileprivate func addTextLabel(withText text: String, textColor: UIColor, font: UIFont) { + textLabel.text = text + textLabel.textColor = textColor + textLabel.font = font + textLabel.numberOfLines = 0 + tipContainer.addSubview(textLabel) + } + + fileprivate func addArrow() { + arrowView.frame.origin = CGPoint(x: 0, y: self.frame.height) + self.addSubview(arrowView) + } + + fileprivate func addCloseButton(withColor color: UIColor) { + let image = UIImageNamedPreferred(named: "toolTipCloseButton") + closeButton.setImage(image, for: .normal) + closeButton.tintColor = color + closeButton.addTarget(self, action: #selector(closeAction), for: .touchUpInside) + tipContainer.addSubview(closeButton) + } + + fileprivate func addShadow() { + self.layer.shadowOffset = CGSize(width: 0, height: 2) + self.layer.shadowRadius = 0.8 + self.layer.shadowOpacity = 0.2 + self.layer.shadowColor = UIColor.black.cgColor + self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath + } + + // MARK: Actions + @objc fileprivate func closeAction() { + self.dismissOnCloseButtonTap(withCompletion: nil) + } + + // MARK: Frame and size calculations + fileprivate func size(forText text: String, withFont font: UIFont) -> CGSize { + let attributes = [NSAttributedString.Key.font: font] + + var textSize = text.boundingRect(with: CGSize(width: textWidth, + height: CGFloat.greatestFiniteMagnitude), + options: NSStringDrawingOptions.usesLineFragmentOrigin, + attributes: attributes, context: nil).size + + textSize.width = textWidth + textSize.height = ceil(textSize.height) + + return textSize + } + + fileprivate func absoluteFrame(for view: UIView, inside superView: UIView?) -> CGRect? { + guard let superView = superView, let referenceViewParent = referenceView.superview else { return nil } + + return referenceViewParent.convert(referenceView.frame, to: superView) + } + + // MARK: Constraints + fileprivate func setupConstraints() { + textLabel.translatesAutoresizingMaskIntoConstraints = false + closeButton.translatesAutoresizingMaskIntoConstraints = false + tipContainer.translatesAutoresizingMaskIntoConstraints = false + + // tipContainer + Constraints.active(item: self, attr: .top, relatedBy: .equal, to: tipContainer, attr: .top, + constant: -margin.top) + Constraints.active(item: self, attr: .bottom, relatedBy: .equal, to: tipContainer, attr: .bottom, + constant: margin.bottom) + Constraints.active(item: self, attr: .leading, relatedBy: .equal, to: tipContainer, attr: .leading, + constant: -margin.left) + Constraints.active(item: self, attr: .trailing, relatedBy: .equal, to: tipContainer, attr: .trailing, + constant: margin.right) + + // textLabel + Constraints.active(item: tipContainer, attr: .top, relatedBy: .equal, to: textLabel, attr: .top, + constant: -padding.top) + Constraints.active(item: tipContainer, attr: .bottom, relatedBy: .equal, to: textLabel, attr: .bottom, + constant: padding.bottom) + Constraints.active(item: tipContainer, attr: .leading, relatedBy: .equal, to: textLabel, attr: .leading, + constant: -padding.left) + + // closeButton + Constraints.active(item: closeButton, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: closeButtonWidth) + Constraints.active(item: closeButton, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: closeButtonHeight) + Constraints.active(item: closeButton, attr: .centerY, relatedBy: .equal, to: tipContainer, attr: .centerY) + Constraints.active(item: closeButton, attr: .leading, relatedBy: .equal, to: textLabel, attr: .trailing, + constant: itemSeparation) + Constraints.active(item: tipContainer, attr: .trailing, relatedBy: .equal, to: closeButton, attr: .trailing, + constant: padding.right) + + self.setNeedsLayout() + } + + // MARK: Draw arrow + class fileprivate func arrow(withHeight height: CGFloat, + width: CGFloat, + color: UIColor, + position: ToolTipPosition) -> UIView { + let bezierPath = UIBezierPath() + let arrowView: UIView + switch position { + case .above: + arrowView = UIView(frame: CGRect(x: 0, y: 0, width: width, height: height)) + bezierPath.move(to: CGPoint(x: 0, y: 0)) + bezierPath.addLine(to: CGPoint(x: width, y: 0)) + bezierPath.addLine(to: CGPoint(x: width / 2, y: height)) + bezierPath.addLine(to: CGPoint(x: 0, y: 0)) + case .below: + arrowView = UIView(frame: CGRect(x: 0, y: 0, width: width, height: height)) + bezierPath.move(to: CGPoint(x: 0, y: height)) + bezierPath.addLine(to: CGPoint(x: width, y: height)) + bezierPath.addLine(to: CGPoint(x: width / 2, y: 0)) + bezierPath.addLine(to: CGPoint(x: 0, y: height)) + case .left: + arrowView = UIView(frame: CGRect(x: 0, y: 0, width: height, height: width)) + bezierPath.move(to: CGPoint(x: 0, y: 0)) + bezierPath.addLine(to: CGPoint(x: height, y: width / 2)) + bezierPath.addLine(to: CGPoint(x: 0, y: width)) + bezierPath.addLine(to: CGPoint(x: 0, y: 0)) + case .right: + arrowView = UIView(frame: CGRect(x: 0, y: 0, width: height, height: width)) + bezierPath.move(to: CGPoint(x: height, y: 0)) + bezierPath.addLine(to: CGPoint(x: 0, y: width / 2)) + bezierPath.addLine(to: CGPoint(x: height, y: width)) + bezierPath.addLine(to: CGPoint(x: height, y: 0)) + } + bezierPath.close() + arrowView.backgroundColor = color + + let shapeLayer = CAShapeLayer() + shapeLayer.path = bezierPath.cgPath + arrowView.layer.mask = shapeLayer + return arrowView + } +} + +// MARK: - Arrange views + +extension ToolTipView { + + fileprivate func arrangeFrame(withSuperView superView: UIView?) { + guard let superview = superView, + let referenceViewAbsoluteFrame = absoluteFrame(for: referenceView, inside: superView) else { return } + let frameHeight = max(textSize.height, closeButtonHeight) + + padding.top + + padding.bottom + + margin.top + + margin.bottom + let frameWidth = textSize.width + + closeButtonWidth + + padding.left + + padding.right + + itemSeparation + + margin.left + + margin.right + + let size = CGSize(width: frameWidth, height: frameHeight) + let origin: CGPoint + if referenceViewAbsoluteFrame == .zero { + origin = .zero + } else { + origin = frameOrigin(forRefViewAbsFrame: referenceViewAbsoluteFrame, + withSize: size, + onSuperview: superview) + } + + self.frame = CGRect(origin: origin, size: size) + } + + fileprivate func frameOrigin(forRefViewAbsFrame referenceViewAbsoluteFrame: CGRect, + withSize size: CGSize, + onSuperview superview: UIView) -> CGPoint { + var x: CGFloat = 0 + var y: CGFloat = 0 + var refViewAbsFrame = referenceViewAbsoluteFrame + + switch toolTipPosition { + case .above: + x = refViewAbsFrame.midX - (size.width / 2) + + refViewAbsFrame.origin.y -= minimunDistanceToRefView.top + if refViewAbsFrame.origin.y - size.height < 0 { + y = refViewAbsFrame.origin.y + refViewAbsFrame.height - size.height + } else { + y = refViewAbsFrame.origin.y - size.height + } + case .below: + x = refViewAbsFrame.midX - (size.width / 2) + + refViewAbsFrame.origin.y += minimunDistanceToRefView.bottom + if refViewAbsFrame.origin.y + referenceView.frame.height + size.height > superview.frame.height { + y = refViewAbsFrame.origin.y + refViewAbsFrame.height - size.height + } else { + y = refViewAbsFrame.origin.y + referenceView.frame.height + } + case .left: + y = refViewAbsFrame.midY - size.height / 2 + + refViewAbsFrame.origin.x -= minimunDistanceToRefView.left + + if refViewAbsFrame.origin.x - size.width < 0 { + x = superview.frame.width - size.width + } else { + x = refViewAbsFrame.origin.x - size.width + } + case .right: + y = refViewAbsFrame.origin.y - margin.top + + refViewAbsFrame.origin.x += minimunDistanceToRefView.right + + if refViewAbsFrame.origin.x + referenceView.frame.width + size.width > superview.frame.width { + x = superview.frame.width - size.width + } else { + x = refViewAbsFrame.origin.x - size.width + } + } + + if x < 0 || superview.frame.width - x < size.width { + x = superview.frame.width - size.width + } + + if superview.frame.height - y < size.height { + y = refViewAbsFrame.origin.y + refViewAbsFrame.height - size.height + } + + return CGPoint(x: x, y: y) + } + + fileprivate func arrangeArrow(withSuperView superView: UIView?) { + guard let referenceViewAbsoluteFrame = absoluteFrame(for: referenceView, inside: superView) else { return } + + let x: CGFloat + let y: CGFloat + switch toolTipPosition { + case .above: + x = referenceViewAbsoluteFrame.origin.x + + referenceView.frame.width / 2 - + self.frame.origin.x - + arrowView.frame.width / 2 + y = tipContainer.frame.height + tipContainer.frame.origin.y + case .below: + x = referenceViewAbsoluteFrame.origin.x + + referenceView.frame.width / 2 - + self.frame.origin.x - + arrowView.frame.width / 2 + y = 0 + case .left: + x = tipContainer.frame.width + tipContainer.frame.origin.x + y = referenceViewAbsoluteFrame.origin.y + + referenceView.frame.height / 2 - + self.frame.origin.y - + arrowView.frame.height / 2 + case .right: + x = 0 + y = referenceViewAbsoluteFrame.origin.y + + referenceView.frame.height / 2 - + self.frame.origin.y - + arrowView.frame.height / 2 + } + arrowView.frame.origin = CGPoint(x: x, y: y) + } +} + +// MARK: - Show and hide tip methods + +extension ToolTipView { + + func show(alongsideAnimations:(() -> Void)? = nil) { + UIView.animate(withDuration: 0.5) { + self.alpha = 1 + alongsideAnimations?() + } + } + + func dismiss(withCompletion completion: (() -> Void)? = nil) { + willDismiss?() + self.removeFromSuperview() + completion?() + } + + func dismissOnCloseButtonTap(withCompletion completion: (() -> Void)? = nil) { + willDismissOnCloseButtonTap?() + self.removeFromSuperview() + completion?() + } +} + +// MARK: - UserDefaults flags + +extension ToolTipView { + private static let shouldShowFileImportToolTipKey = "ginicapture.defaults.shouldShowFileImportToolTip" + private static let shouldShowQRCodeToolTipKey = "ginicapture.defaults.shouldShowQRCodeToolTip" + + static var shouldShowFileImportToolTip: Bool { + set { + UserDefaults.standard.set(newValue, forKey: ToolTipView.shouldShowFileImportToolTipKey) + } + get { + let defaultsValue = UserDefaults + .standard + .object(forKey: ToolTipView.shouldShowFileImportToolTipKey) as? Bool + return defaultsValue ?? true + } + } + + private static let shouldShowReorderPagesButtonToolTipKey = + "ginicapture.defaults.shouldShowReorderPagesButtonToolTip" + static var shouldShowReorderPagesButtonToolTip: Bool { + set { + UserDefaults.standard.set(newValue, forKey: ToolTipView.shouldShowReorderPagesButtonToolTipKey) + } + get { + let defaultsValue = UserDefaults + .standard + .object(forKey: ToolTipView.shouldShowReorderPagesButtonToolTipKey) as? Bool + return defaultsValue ?? true + } + } + + static var shouldShowQRCodeToolTip: Bool { + set { + UserDefaults.standard.set(newValue, forKey: ToolTipView.shouldShowQRCodeToolTipKey) + } + get { + let defaultsValue = UserDefaults + .standard + .object(forKey: ToolTipView.shouldShowQRCodeToolTipKey) as? Bool + return defaultsValue ?? true + } + } +} diff --git a/Sources/GiniCaptureSDK/Core/Extensions/CGRect.swift b/Sources/GiniCaptureSDK/Core/Extensions/CGRect.swift index 8e2ef09..b9fa12c 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/CGRect.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/CGRect.swift @@ -12,11 +12,4 @@ extension CGRect { var center: CGPoint { return CGPoint(x: midX, y: midY) } - - func scaled(for scaleFactor: CGFloat) -> CGRect { - return CGRect(x: self.minX * scaleFactor, - y: self.minY * scaleFactor, - width: self.width * scaleFactor, - height: self.height * scaleFactor) - } } diff --git a/Sources/GiniCaptureSDK/Core/Extensions/Decimal.swift b/Sources/GiniCaptureSDK/Core/Extensions/Decimal.swift deleted file mode 100644 index 2f45e2f..0000000 --- a/Sources/GiniCaptureSDK/Core/Extensions/Decimal.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Decimal.swift -// -// -// Created by David Vizaknai on 14.12.2022. -// - -import Foundation - -public extension Decimal { - /// This function is an extension to the Decimal type in Swift. It converts a Decimal value to a Double value, rounding the result to the specified number of decimal places. - /// - Parameter decimalPoint: specifies the number of decimal places to which the result should be cut off - /// - Returns: returns the double value with the specified decimal places - /// - Note: internal only - func convertToDouble(withDecimalPoint decimalPoint: Int) -> Double { - let divisor = Double(truncating: pow(10.0, decimalPoint) as NSNumber) - let doubleValue = Double(truncating: self as NSNumber) - let doubleValueTruncated = (doubleValue * divisor).rounded(.towardZero) / divisor - - return doubleValueTruncated - } -} diff --git a/Sources/GiniCaptureSDK/Core/Extensions/ReuseIdentifier.swift b/Sources/GiniCaptureSDK/Core/Extensions/ReuseIdentifier.swift deleted file mode 100644 index 21ac77e..0000000 --- a/Sources/GiniCaptureSDK/Core/Extensions/ReuseIdentifier.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ReuseIdentifier.swift -// -// -// Created by David Vizaknai on 13.09.2022. -// - -import UIKit - -public protocol Reusable { - static var reuseIdentifier: String { get } -} - -public extension Reusable { - static var reuseIdentifier: String { - String(describing: self) - } -} - -extension UICollectionReusableView: Reusable {} - -extension UITableViewCell: Reusable {} - -extension UITableViewHeaderFooterView: Reusable {} diff --git a/Sources/GiniCaptureSDK/Core/Extensions/String.swift b/Sources/GiniCaptureSDK/Core/Extensions/String.swift index 89a6257..6341a16 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/String.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/String.swift @@ -8,14 +8,15 @@ import Foundation extension String { + /* + In this implementation, the regular expression pattern "\r\r?\n" matches one or more consecutive occurrences of the line separator (\r\n) or double line separator (\r\r\n). The replacingOccurrences(of:with:options:) method is then used to replace all occurrences of this pattern with a single new line character (\n), and the resulting string is split into an array of lines using the components(separatedBy:) method with the newlines delimiter. + */ var splitlines: [String] { - var lines: [String] = [] - enumerateLines(invoking: { line, _ in - lines.append(line) - }) - return lines + let pattern = #"\r\r?\n"# + return self.replacingOccurrences(of: pattern, with: "\n", options: .regularExpression) + .components(separatedBy: .newlines) } - + public static func localized(resource: T, args: CVarArg...) -> String { if args.isEmpty { return resource.localizedFormat diff --git a/Sources/GiniCaptureSDK/Core/Extensions/UIColor.swift b/Sources/GiniCaptureSDK/Core/Extensions/UIColor.swift index 02bd56a..9c75b48 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/UIColor.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/UIColor.swift @@ -7,7 +7,24 @@ import UIKit -extension UIColor { +extension UIColor { + public static func from(giniColor: GiniColor) -> UIColor { + if #available(iOS 13, *) { + return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in + if UITraitCollection.userInterfaceStyle == .dark { + /// Return the color for Dark Mode + return giniColor.darkModeColor + } else { + /// Return the color for Light Mode + return giniColor.lightModeColor + } + } + } else { + /// Return a fallback color for iOS 12 and lower. + return giniColor.lightModeColor + } + } + public static func from(hex: UInt) -> UIColor { return UIColor( red: CGFloat((hex & 0xFF0000) >> 16) / 255.0, diff --git a/Sources/GiniCaptureSDK/Core/Extensions/UIFont.swift b/Sources/GiniCaptureSDK/Core/Extensions/UIFont.swift deleted file mode 100644 index e58a00b..0000000 --- a/Sources/GiniCaptureSDK/Core/Extensions/UIFont.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// UIFont.swift -// -// -// Created by Nadya Karaban on 09.08.22. -// - -import UIKit - -extension UIFont { - /** - - parameter font: The font to be scaled. Do not specify a font that has already been scaled; doing so results in an exception - - parameter textStyle: Constants that describe the preferred styles for fonts. Please, find additional information [here](https://developer.apple.com/documentation/uikit/uifont/textstyle) - - - returns: A scaled font for a specific text style. - */ - public static func scaledFont(_ font: UIFont, textStyle: UIFont.TextStyle) -> UIFont { - return UIFontMetrics(forTextStyle: textStyle).scaledFont(for: font) - } -} - -extension UIFont.TextStyle { - - public static let bodyBold: UIFont.TextStyle = .init(rawValue: "kBodyBold") - public static let calloutBold: UIFont.TextStyle = .init(rawValue: "kCalloutBold") - public static let footnoteBold: UIFont.TextStyle = .init(rawValue: "kFootnoteBold") - public static let title2Bold: UIFont.TextStyle = .init(rawValue: "kTitle2Bold") - public static let title1Bold: UIFont.TextStyle = .init(rawValue: "kTitle1Bold") -} diff --git a/Sources/GiniCaptureSDK/Core/Extensions/UIImage.swift b/Sources/GiniCaptureSDK/Core/Extensions/UIImage.swift index d5de1b3..d1ed6fd 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/UIImage.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/UIImage.swift @@ -81,79 +81,5 @@ extension UIImage { UIGraphicsEndImageContext() return imageColored } - - /// Extension to fix orientation of an UIImage without EXIF - func fixOrientation() -> UIImage { - - guard let cgImage = cgImage else { return self } - - // If the image is already in '.up' orientation, just return - if imageOrientation == .up { return self } - - // Create a transform to apply on the image to redraw it in '.up' orientation - var transform = CGAffineTransform.identity - - switch imageOrientation { - - case .down, .downMirrored: - transform = transform.translatedBy(x: size.width, y: size.height) - transform = transform.rotated(by: CGFloat(Double.pi)) - - case .left, .leftMirrored: - transform = transform.translatedBy(x: size.width, y: 0) - transform = transform.rotated(by: CGFloat(Double.pi / 2)) - - case .right, .rightMirrored: - transform = transform.translatedBy(x: 0, y: size.height) - transform = transform.rotated(by: CGFloat(-Double.pi / 2)) - - case .up, .upMirrored: - break - @unknown default: - break - } - - // For mirrored orientations, mirror the transform as well - switch imageOrientation { - - case .upMirrored, .downMirrored: - transform = transform.translatedBy(x: size.width, y: 0) - transform = transform.scaledBy(x: -1, y: 1) - - case .leftMirrored, .rightMirrored: - transform = transform.translatedBy(x: size.height, y: 0) - transform = transform.scaledBy(x: -1, y: 1) - - case .up, .down, .left, .right: - break - @unknown default: - break - } - - // Create a context where the image will be drawn - if let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), - bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, - space: cgImage.colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) { - - ctx.concatenate(transform) - - // Draw the image in the rect defined above - 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)) - } - - // Return the redrawn image in '.up' orientation - if let finalImage = ctx.makeImage() { - return (UIImage(cgImage: finalImage)) - } - } - - // If something failed -- return original - return self - } + } diff --git a/Sources/GiniCaptureSDK/Core/Extensions/UINavigationController.swift b/Sources/GiniCaptureSDK/Core/Extensions/UINavigationController.swift index 09b7416..17d7f7d 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/UINavigationController.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/UINavigationController.swift @@ -2,30 +2,28 @@ // UINavigationController.swift // GiniCapture // -// Created by Nadzeya Karaban on 07/11/22. -// Copyright © 2022 Gini GmbH. All rights reserved. +// Created by Enrique del Pozo Gómez on 12/19/17. +// Copyright © 2017 Gini GmbH. All rights reserved. // import UIKit extension UINavigationController { public func applyStyle(withConfiguration configuration: GiniConfiguration) { - let titleTextAttrubutes = [NSAttributedString.Key.font: - configuration.textStyleFonts[.bodyBold] as Any, NSAttributedString.Key.foregroundColor: GiniColor(light: UIColor.GiniCapture.dark1, dark: UIColor.GiniCapture.light1).uiColor()] - let navigationBackgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - if #available(iOS 13.0, *) { + let titleTextAttrubutes = [NSAttributedString.Key.font: configuration.customFont.isEnabled ? + configuration.customFont.with(weight: .light, size: 16, style: .title2) : + configuration.navigationBarTitleFont as Any, NSAttributedString.Key.foregroundColor: configuration.navigationBarTitleColor] + if #available(iOS 15.0, *) { let appearance = UINavigationBarAppearance() appearance.configureWithOpaqueBackground() - appearance.backgroundColor = navigationBackgroundColor + appearance.backgroundColor = configuration.navigationBarTintColor appearance.titleTextAttributes = titleTextAttrubutes - appearance.shadowColor = navigationBackgroundColor navigationBar.standardAppearance = appearance navigationBar.scrollEdgeAppearance = navigationBar.standardAppearance } else { - navigationBar.barTintColor = navigationBackgroundColor - navigationBar.titleTextAttributes = titleTextAttrubutes - navigationBar.setValue(true, forKey: "hidesShadow") + self.navigationBar.barTintColor = configuration.navigationBarTintColor + self.navigationBar.titleTextAttributes = titleTextAttrubutes } - navigationBar.tintColor = UIColor.GiniCapture.accent1 + self.navigationBar.tintColor = configuration.navigationBarItemTintColor } } diff --git a/Sources/GiniCaptureSDK/Core/Extensions/UIView.swift b/Sources/GiniCaptureSDK/Core/Extensions/UIView.swift index d4f33aa..69e6f0a 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/UIView.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/UIView.swift @@ -14,24 +14,4 @@ extension UIView { let nib = UINib(nibName: nibName, bundle: bundle) return nib.instantiate(withOwner: self, options: nil).first as! UIView } - - func viewFromNibForClass() -> UIView { - let bundle = Bundle(for: type(of: self)) - let nibName = type(of: self).description().components(separatedBy: ".").last! - let nib = UINib(nibName: nibName, bundle: bundle) - guard let view = nib.instantiate(withOwner: self, options: nil).first, view is UIView else { - fatalError("View initializing with nib failed while casting") - } - return view as? UIView ?? UIView() - } - - func fixInView(_ container: UIView!) { - self.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(self) - let leadingConstraint = self.leadingAnchor.constraint(equalTo: container.leadingAnchor) - let trailingConstraint = self.trailingAnchor.constraint(equalTo: container.trailingAnchor) - let topConstraint = self.topAnchor.constraint(equalTo: container.topAnchor) - let bottomConstraint = self.bottomAnchor.constraint(equalTo: container.bottomAnchor) - NSLayoutConstraint.activate([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint]) - } } diff --git a/Sources/GiniCaptureSDK/Core/Extensions/UIViewController.swift b/Sources/GiniCaptureSDK/Core/Extensions/UIViewController.swift index 588852e..7c41a15 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/UIViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/UIViewController.swift @@ -8,14 +8,36 @@ import UIKit extension UIViewController { - /** - A UIViewcontroller extension that shows an alert based on the Error it gets as the parameter. It can also add an extra option as a closure to be executed. - Use this when drag and dropping files into the SDK. - - - parameter error: The error to be shown - - parameter positiveAction: The closure to be executed. If nil, the extra option won't be added. - */ - public func showErrorDialog(for error: Error, positiveAction: (() -> Void)?) { + + enum NavBarItemPosition { + case left, right + } + + func setupNavigationItem(usingResources preferredResources: PreferredButtonResource, + selector: Selector, + position: NavBarItemPosition, + target: AnyObject?) { + + let buttonText = preferredResources.preferredText ?? "" + + if !buttonText.isEmpty || preferredResources.preferredImage != nil { + let navButton = GiniBarButtonItem( + image: preferredResources.preferredImage, + title: preferredResources.preferredText, + style: .plain, + target: target, + action: selector + ) + switch position { + case .right: + navigationItem.setRightBarButton(navButton, animated: false) + case .left: + navigationItem.setLeftBarButton(navButton, animated: false) + } + } + } + + func showErrorDialog(for error: Error, positiveAction: (() -> Void)?) { let message: String var cancelActionTitle: String = .localized(resource: CameraStrings.errorPopupCancelButton) var confirmActionTitle: String? = .localized(resource: CameraStrings.errorPopupPickAnotherFileButton) @@ -37,8 +59,6 @@ extension UIViewController { confirmActionTitle = .localized(resource: CameraStrings.mixedArraysPopupUsePhotosButton) case .failedToOpenDocument: break - case .multiplePdfsUnsupported: - confirmActionTitle = .localized(resource: CameraStrings.errorConfirmButton) } default: message = DocumentValidationError.unknown.message @@ -59,8 +79,6 @@ extension UIViewController { confirmAction: (() -> Void)? = nil) -> UIAlertController { let alertViewController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - - alertViewController.view.tintColor = .GiniCapture.accent1 alertViewController.addAction(UIAlertAction(title: cancelActionTitle, style: .cancel, diff --git a/Sources/GiniCaptureSDK/Core/GiniCapture.swift b/Sources/GiniCaptureSDK/Core/GiniCapture.swift index 97cf1c2..9726808 100644 --- a/Sources/GiniCaptureSDK/Core/GiniCapture.swift +++ b/Sources/GiniCaptureSDK/Core/GiniCapture.swift @@ -13,9 +13,11 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate /** Delegate to inform the reveiver about the current status of the Gini Capture SDK. Makes use of callbacks for handling incoming data and to control view controller presentation. + + - note: Screen API only. */ @objc public protocol GiniCaptureDelegate { - + /** Called when the user has taken a picture or imported a file (image or PDF) from camera roll or document explorer @@ -26,6 +28,25 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate */ func didCapture(document: GiniCaptureDocument, networkDelegate: GiniCaptureNetworkDelegate) + + /** + Called when the user has taken a picture or imported a file (image or PDF) from camera roll or document explorer + + - parameter document: `GiniCaptureDocument` + */ + + @available(*, unavailable, + message: "Use didCapture(document: GiniCaptureDocument, networkDelegate: GiniCaptureNetworkDelegate) instead") + func didCapture(document: GiniCaptureDocument) + + /** + Called when the user has taken an image. + + - parameter imageData: JPEG image data including meta information or PDF data + */ + @available(*, unavailable, + message: "Use didCapture(document: GiniCaptureDocument, uploadDelegate: UploadDelegate) instead") + func didCapture(_ imageData: Data) /** Called when the user has reviewed one or several documents. @@ -38,6 +59,39 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate */ func didReview(documents: [GiniCaptureDocument], networkDelegate: GiniCaptureNetworkDelegate) + /** + Called when the user has reviewed the image and potentially rotated it to the correct orientation. + + - parameter document: `GiniCaptureDocument` + - parameter changes: Indicates whether `imageData` was altered. + */ + @available(*, unavailable, + message: "Use didReview(documents: [GiniCaptureDocument]) instead") + func didReview(document: GiniCaptureDocument, withChanges changes: Bool) + + /** + Called when the user has reviewed the image and potentially rotated it to the correct orientation. + + - parameter imageData: JPEG image data including eventually updated meta information or PDF Data + - parameter changes: Indicates whether `imageData` was altered. + */ + @available(*, unavailable, + message: "Use didReview(documents: [GiniCaptureDocument]) instead") + func didReview(_ imageData: Data, withChanges changes: Bool) + + /** + Called when the user is presented with the analysis screen. Use the `analysisDelegate` + object to inform the user about the current status of the analysis task. + + - parameter analysisDelegate: The analysis delegate to send updates to. + */ + @available(*, unavailable, + message: """ + This method is no longer needed since the analysis should start + always in the didReview(documents:networkDelegate:) method + """) + @objc optional func didShowAnalysis(_ analysisDelegate: AnalysisDelegate) + /** Called when the user cancels capturing on the camera screen. Should be used to dismiss the presented view controller. @@ -50,17 +104,19 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate */ func didCancelReview(for document: GiniCaptureDocument) + /** + Called when the user navigates back from the review screen to the camera potentially to + retake an image. Should be used to cancel any ongoing analysis task on the image. + */ + @available(*, unavailable, message: "Use didCancelReview(for: GiniCaptureDocument) instead") + func didCancelReview() + /** Called when the user navigates back from the analysis screen to the review screen. It is used to cancel any ongoing analysis task on the image. */ func didCancelAnalysis() - /** - Called when the 'Enter Manually' was pressed within No Result screen - */ - func didPressEnterManually() - } /** @@ -99,7 +155,9 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate /** Returns a view controller which will handle the analysis process. - + + - note: Screen API only. + - parameter delegate: An instance conforming to the `GiniCaptureDelegate` protocol. - parameter importedDocuments: Documents that come from a source different than `CameraViewController`. There should be either images or one PDF, and they should be validated before calling this method. @@ -117,6 +175,8 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate /** Returns a view controller which will handle the analysis process. + - note: Screen API only. + - parameter delegate: An instance conforming to the `GiniCaptureDelegate` protocol. - parameter importedDocuments: Documents that come from a source different than `CameraViewController`. There should be either images or one PDF, and they should be validated before calling this method. @@ -135,6 +195,8 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate /** Returns a view controller which will handle the analysis process. + + - note: Screen API only. - parameter delegate: An instance conforming to the `GiniCaptureDelegate` protocol. - parameter importedDocument: Documents that come from a source different than CameraViewController. @@ -155,6 +217,8 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate /** Returns a view controller which will handle the analysis process. + + - note: Screen API only. - parameter delegate: An instance conforming to the `GiniCaptureDelegate` protocol. - parameter importedDocument: Documents that come from a source different than CameraViewController. @@ -178,6 +242,8 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate Returns a view controller which will handle the analysis process. Allows to set a custom configuration to change the look and feel of the Gini Capture SDK. + - note: Screen API only. + - parameter delegate: An instance conforming to the `GiniCaptureDelegate` protocol. - parameter configuration: The configuration to set. - parameter importedDocument: Documents that come from a source different than CameraViewController. @@ -196,6 +262,8 @@ public typealias GiniCaptureNetworkDelegate = AnalysisDelegate & UploadDelegate Returns a view controller which will handle the analysis process. Allows to set a custom configuration to change the look and feel of the Gini Capture SDK. + - note: Screen API only. + - parameter delegate: An instance conforming to the `GiniCaptureDelegate` protocol. - parameter configuration: The configuration to set. - parameter importedDocument: Documents that come from a source different than CameraViewController. diff --git a/Sources/GiniCaptureSDK/Core/GiniConfiguration.swift b/Sources/GiniCaptureSDK/Core/GiniConfiguration.swift index ce2dd08..62bee2b 100644 --- a/Sources/GiniCaptureSDK/Core/GiniConfiguration.swift +++ b/Sources/GiniCaptureSDK/Core/GiniConfiguration.swift @@ -7,40 +7,22 @@ // import UIKit -import GiniBankAPILibrary /** The `GiniColor` class allows to customize color for the light and the dark modes. */ @objc public class GiniColor : NSObject { - var light: UIColor - var dark: UIColor + var lightModeColor: UIColor + var darkModeColor: UIColor /** Creates a GiniColor with the colors for the light and dark modes. - - parameter light: color for the light mode - - parameter dark: color for the dark mode + - parameter lightModeColor: color for the light mode + - parameter darkModeColor: color for the dark mode */ - public init(light: UIColor, dark: UIColor) { - self.light = light - self.dark = dark - } - - public func uiColor() -> UIColor { - if #available(iOS 13, *) { - return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in - if UITraitCollection.userInterfaceStyle == .dark { - /// Return the color for Dark Mode - return self.dark - } else { - /// Return the color for Light Mode - return self.light - } - } - } else { - /// Return a fallback color for iOS 12 and lower. - return self.light - } + public init(lightModeColor: UIColor, darkModeColor: UIColor) { + self.lightModeColor = lightModeColor + self.darkModeColor = darkModeColor } } @@ -63,7 +45,7 @@ import GiniBankAPILibrary /** Singleton to make configuration internally accessible in all classes of the Gini Capture SDK. */ - public static var shared = GiniConfiguration() + static var shared = GiniConfiguration() /** Supported document types by Gini Capture SDK. @@ -80,10 +62,19 @@ import GiniBankAPILibrary - returns: Instance of `GiniConfiguration`. */ - override init() {} + public override init() {} // MARK: General options + /** + Sets the background color in all screens of the Gini Capture SDK to the specified color. + + - note: Screen API only. + */ + @available(*, unavailable, + message: "Use the screen specific background color instead e.g. onboardingScreenBackgroundColor") + @objc public var backgroundColor: UIColor = UIColor.black + /** Sets custom validations that can be done apart from the default ones (file size, file type...). It should throw a `CustomDocumentValidationError` error. @@ -91,62 +82,7 @@ import GiniBankAPILibrary @objc public var customDocumentValidations: ((GiniCaptureDocument) -> CustomDocumentValidationResult) = { _ in return CustomDocumentValidationResult.success() } - - // MARK: Button configuration options - - public lazy var primaryButtonConfiguration: ButtonConfiguration = - ButtonConfiguration(backgroundColor: .GiniCapture.accent1, - borderColor: .clear, - titleColor: .GiniCapture.light1, - shadowColor: .clear, - cornerRadius: 16, - borderWidth: 0, - shadowRadius: 0, - withBlurEffect: false) - - public lazy var secondaryButtonConfiguration: ButtonConfiguration = - ButtonConfiguration(backgroundColor: .GiniCapture.dark4, - borderColor: GiniColor(light: UIColor.GiniCapture.light6, - dark: UIColor.clear).uiColor(), - titleColor: GiniColor(light: UIColor.GiniCapture.dark6, - dark: UIColor.GiniCapture.light1).uiColor(), - shadowColor: .clear, - cornerRadius: 16, - borderWidth: 2, - shadowRadius: 14, - withBlurEffect: true) - - public lazy var transparentButtonConfiguration: ButtonConfiguration = - ButtonConfiguration(backgroundColor: .clear, - borderColor: .clear, - titleColor: .GiniCapture.accent1, - shadowColor: .clear, - cornerRadius: 16, - borderWidth: 0, - shadowRadius: 0, - withBlurEffect: false) - - public lazy var cameraControlButtonConfiguration: ButtonConfiguration = - ButtonConfiguration(backgroundColor: .clear, - borderColor: .clear, - titleColor: .GiniCapture.light1, - shadowColor: .clear, - cornerRadius: 0, - borderWidth: 0, - shadowRadius: 0, - withBlurEffect: false) - - public lazy var addPageButtonConfiguration: ButtonConfiguration = - ButtonConfiguration(backgroundColor: .clear, - borderColor: .clear, - titleColor: GiniColor(light: .GiniCapture.dark2, dark: .GiniCapture.light2).uiColor(), - shadowColor: .clear, - cornerRadius: 0, - borderWidth: 0, - shadowRadius: 0, - withBlurEffect: false) - - // MARK: - TODO DELETE + /** Sets the font used in the GiniCapture library by default. */ @@ -159,7 +95,7 @@ import GiniBankAPILibrary thin: UIFont.systemFont(ofSize: 14, weight: .thin), isEnabled: false) - + /** Can be turned on during development to unlock extra information and to save captured images to camera roll. @@ -179,14 +115,56 @@ import GiniBankAPILibrary @objc public var multipageEnabled = false /** - Sets the custom navigation view controller as a root view controller for Gini Capture SDK screens. - */ - @objc public var customNavigationController : UINavigationController? = nil + Sets the tint color of the navigation bar in all screens of the Gini Capture SDK to + the globally specified color or to a default color. + + - note: Screen API only. + */ + @objc public var navigationBarTintColor = UINavigationBar.appearance().barTintColor ?? Colors.Gini.blue + + /** + Sets the tint color of all navigation items in all screens of the Gini Capture SDK to + the globally specified color. + + - note: Screen API only. + */ + @objc public var navigationBarItemTintColor = UINavigationBar.appearance().tintColor + + /** + Sets the font of all navigation items in all screens of the Gini Capture SDK to + the globally specified font or a default font. + + - note: Screen API only. + */ + @objc public var navigationBarItemFont = UIBarButtonItem.appearance() + .titleTextAttributes(for: .normal).dictionary?[NSAttributedString.Key.font.rawValue] as? UIFont ?? + UIFont.systemFont(ofSize: 16, weight: .bold) + + /** + Sets the title color in the navigation bar in all screens of the Gini Capture SDK to + the globally specified color or to a default color. + + - note: Screen API only. + */ + @objc public var navigationBarTitleColor = UINavigationBar + .appearance() + .titleTextAttributes?[NSAttributedString.Key.foregroundColor] as? UIColor ?? .white + + /** + Sets the title font in the navigation bar in all screens of the Gini Capture SDK to + the globally specified font or to a default font. + + - note: Screen API only. + */ + @objc public var navigationBarTitleFont = UINavigationBar + .appearance() + .titleTextAttributes?[NSAttributedString.Key.font] as? UIFont ?? UIFont.systemFont(ofSize: 16, weight: .regular) /** Sets the tint color of the UIDocumentPickerViewController navigation bar. - note: Use only if you have a custom `UIAppearance` for your UINavigationBar + - note: Only iOS >= 11.0 */ @objc public var documentPickerNavigationBarTintColor: UIColor? @@ -224,11 +202,6 @@ import GiniBankAPILibrary Indicates whether the QR Code scanning feature is enabled or not. */ @objc public var qrCodeScanningEnabled = false - - /** - Indicates whether only the QR Code scanning feature is enabled or not. - */ - @objc public var onlyQRCodeScanningEnabled = false /** Indicates the status bar style in the Gini Capture SDK. @@ -240,17 +213,98 @@ import GiniBankAPILibrary @objc public var statusBarStyle = UIStatusBarStyle.lightContent // MARK: Camera options - + + /** + Sets the text color of the descriptional text when camera access was denied. + */ + @objc public var cameraNotAuthorizedTextColor = UIColor.white + + /** + Sets the text color of the button title when camera access was denied. + */ + @objc public var cameraNotAuthorizedButtonTitleColor = UIColor.white + /** Sets the color of the loading indicator on the camera screen to the specified color. */ @objc public var cameraSetupLoadingIndicatorColor = UIColor.white + /** + Sets the color of camera preview corner guides. + */ + @objc public var cameraPreviewCornerGuidesColor = UIColor.white + + /** + Sets the background color of camera container view. + */ + @objc public var cameraContainerViewBackgroundColor = GiniColor(lightModeColor: .black, darkModeColor: .black) + + /** + Sets the color of camera preview frame. + */ + @objc public var cameraPreviewFrameColor = GiniColor(lightModeColor: UIColor(white: 0.0, alpha: 0.7), darkModeColor: UIColor(white: 0.0, alpha: 0.7)) + + /** + Sets the background color of camera buttons view. + */ + @objc public var cameraButtonsViewBackgroundColor = GiniColor(lightModeColor: .black, darkModeColor: .black) + /** Set the types supported by the file import feature. `GiniCaptureImportFileTypes.none` by default. */ @objc public var fileImportSupportedTypes: GiniCaptureImportFileTypes = .none + /** + Sets the background color of the new file import button hint. + */ + @objc public var fileImportToolTipBackgroundColor = UIColor.white + + /** + Sets the text color of the new file import button hint. + */ + @objc public var fileImportToolTipTextColor = UIColor.black + + /** + Sets the color of the close button for the file import button hint. + */ + @objc public var fileImportToolTipCloseButtonColor = Colors.Gini.grey + + /** + Sets the background style when the tooltip is shown. + */ + public var toolTipOpaqueBackgroundStyle: OpaqueViewStyle { + + set { + _toolTipOpaqueBackgroundStyle = newValue + } + + get { + + if let setValue = _toolTipOpaqueBackgroundStyle { + return setValue + } else { + + if #available(iOS 13.0, *) { + return .blurred(style: .regular) + } else { + return .blurred(style: .dark) + } + } + } + } + + private var _toolTipOpaqueBackgroundStyle: OpaqueViewStyle? + + /** + Sets the text color of the item selected background check. + */ + @objc public var galleryPickerItemSelectedBackgroundCheckColor = Colors.Gini.blue + + /** + Sets the background color for gallery screen. + */ + @objc public var galleryScreenBackgroundColor = GiniColor(lightModeColor: .black, darkModeColor: .black) + /** Indicates whether the flash toggle should be shown in the camera screen. */ @@ -261,26 +315,83 @@ import GiniBankAPILibrary */ @objc public var flashOnByDefault = true + /** + Sets the color of the captured images stack indicator label. + */ + @objc public var imagesStackIndicatorLabelTextcolor: UIColor = Colors.Gini.blue + /** Sets the close button text in the navigation bar on the camera screen. - */ + + - note: Screen API only. + */ @objc public var navigationBarCameraTitleCloseButton = "" /** Sets the help button text in the navigation bar on the camera screen. + + - note: Screen API only. */ @objc public var navigationBarCameraTitleHelpButton = "" + /** + Sets the text color of the QR Code popup button. + */ + @objc public var qrCodePopupButtonColor = Colors.Gini.blue + + /** + Sets the text color of the QR Code popup label. + */ + @objc public var qrCodePopupTextColor = GiniColor(lightModeColor: .black, darkModeColor: .white) + + /** + Sets the text color of the QR Code popup background. + */ + @objc public var qrCodePopupBackgroundColor = GiniColor(lightModeColor: .white, darkModeColor: UIColor.from(hex: 0x1c1c1e)) + + /** + Sets the button color of the unsupported QR Code popup. + */ + @objc public var unsupportedQrCodePopupButtonColor : UIColor = .red + + /** + Sets the text color of the unsupported QR Code popup. + */ + @objc public var unsupportedQrCodePopupTextColor = GiniColor(lightModeColor: .red, darkModeColor: .red) + + /** + Sets the background color of the unsupported QR Code popup. + */ + @objc public var unsupportedQrCodePopupBackgroundColor = GiniColor(lightModeColor: .white, darkModeColor: UIColor.from(hex: 0x1c1c1e)) // MARK: Onboarding screens /** - * Sets the continue button text in the navigation bar on the onboarding screen. + Sets the continue button text in the navigation bar on the onboarding screen. + + - note: Screen API only. */ @objc public var navigationBarOnboardingTitleContinueButton = "" /** - * Indicates whether the onboarding screen should be presented at each start of the Gini Capture SDK. + Sets the color of the page controller's page indicator items. + */ + @objc public var onboardingPageIndicatorColor = GiniColor(lightModeColor: .white, darkModeColor: .white) + + /** + Sets the color of the page controller's current page indicator item. + */ + @objc public var onboardingCurrentPageIndicatorColor = GiniColor(lightModeColor: .white, darkModeColor: .white) + + /** + Sets alpha to the color of the page controller's current page indicator item. + */ + @objc public var onboardingCurrentPageIndicatorAlpha: CGFloat = 0.2 + + /** + Indicates whether the onboarding screen should be presented at each start of the Gini Capture SDK. + + - note: Screen API only. */ @objc public var onboardingShowAtLaunch = false @@ -289,107 +400,200 @@ import GiniBankAPILibrary start of the Gini Capture SDK. It is advised to do so. - note: Overwrites `onboardingShowAtLaunch` for the first launch. + - note: Screen API only. */ @objc public var onboardingShowAtFirstLaunch = true - + /** - Set custom onboarding pages - - note: For your convenience we provide the `OnboardingPage` struct. + Sets the color ot the text for all onboarding pages. */ - public var customOnboardingPages: [OnboardingPage]? - + @objc public var onboardingTextColor = GiniColor(lightModeColor: .white, darkModeColor: .white) + /** - * Enable/disable the bottom navigation bar. + Sets the background color for all onboarding pages. */ - public var bottomNavigationBarEnabled: Bool = false + @objc public var onboardingScreenBackgroundColor = GiniColor(lightModeColor: .black, darkModeColor: .black) /** - * Set an adapter implementation to show a custom bottom navigation bar on the help screens. + All onboarding pages which will be presented in a horizontal scroll view to the user. + By default the Gini Capture SDK comes with three pages advising the user to keep the + document flat, hold the device parallel and capture the whole document. + + - note: Any array of views can be passed, but for your convenience we provide the `GINIOnboardingPage` class. */ - public var helpNavigationBarBottomAdapter: HelpBottomNavigationBarAdapter? + @objc public var onboardingPages: [UIView] { + get { + if let pages = onboardingCustomPages { + return pages + } + guard let page1 = OnboardingPage(imageNamed: "onboardingPage1", + text: .localized(resource: OnboardingStrings.onboardingFirstPageText), + rotateImageInLandscape: true), + let page2 = OnboardingPage(imageNamed: "onboardingPage2", + text: .localized(resource: OnboardingStrings.onboardingSecondPageText)), + let page3 = OnboardingPage(imageNamed: "onboardingPage3", + text: .localized(resource: OnboardingStrings.onboardingThirdPageText)), + let page4 = OnboardingPage(imageNamed: "onboardingPage5", + text: .localized(resource: OnboardingStrings.onboardingFifthPageText)) else { + return [UIView]() + } + + onboardingCustomPages = [page1, page2, page3, page4] + if let ipadTipPage = OnboardingPage(imageNamed: "onboardingPage4", + text: .localized(resource: OnboardingStrings.onboardingFourthPageText)), + UIDevice.current.isIpad { + onboardingCustomPages?.insert(ipadTipPage, at: 0) + } + return onboardingCustomPages! + } + set { + self.onboardingCustomPages = newValue + } + } + fileprivate var onboardingCustomPages: [UIView]? /** - * Set an adapter implementation to show a custom bottom navigation bar on the camera screen. + Sets the back button text in the navigation bar on the review screen. Use this if you only want to show the title. + + - note: Screen API only. */ - public var cameraNavigationBarBottomAdapter: CameraBottomNavigationBarAdapter? - + @objc public var navigationBarReviewTitleBackButton = "" + /** - * Set an adapter implementation to show a custom bottom navigation bar on the review screen. + Sets the close button text in the navigation bar on the review screen. Use this if you only want to show the title. + + - note: Screen API only. */ - public var reviewNavigationBarBottomAdapter: ReviewScreenBottomNavigationBarAdapter? - + @objc public var navigationBarReviewTitleCloseButton = "" + /** - * Set an adapter implementation to show a custom bottom navigation bar on the image picker screen. + Sets the continue button text in the navigation bar on the review screen. + + - note: Screen API only. */ - public var imagePickerNavigationBarBottomAdapter: ImagePickerBottomNavigationBarAdapter? + @objc public var navigationBarReviewTitleContinueButton = "" /** - * Set an adapter implementation to show a custom bottom navigation bar on the onboarding screen. + Sets the background color of the bottom section on the review screen containing the rotation button. + + - note: Background will have a 20% transparency, to have enough space for the document image on smaller devices. */ - public var onboardingNavigationBarBottomAdapter: OnboardingNavigationBarBottomAdapter? + @objc public var reviewBottomViewBackgroundColor = UIColor.black /** - * Set an adapter implementation to show a custom illustration on the "align corners" onboarding page. + Sets the font of the text appearing at the bottom of the review screen. */ - public var onboardingAlignCornersIllustrationAdapter: OnboardingIllustrationAdapter? - - /** - * Set an adapter implementation to show a custom illustration on the "lighting" onboarding page. - */ - public var onboardingLightingIllustrationAdapter: OnboardingIllustrationAdapter? - - /** - * Set an adapter implementation to show a custom illustration on the "multi-page" onboarding page. - */ - public var onboardingMultiPageIllustrationAdapter: OnboardingIllustrationAdapter? - - /** - * Set an adapter implementation to show a custom illustration on the "QR code" onboarding page. - */ - public var onboardingQRCodeIllustrationAdapter: OnboardingIllustrationAdapter? - + @objc public var reviewTextBottomFont = UIFont.systemFont(ofSize: 12, weight: .thin) + /** - * Set an adapter implementation to show a custom loading indicator on the document analysis screen. + Sets the color of the text appearing at the bottom of the review screen. */ - public var customLoadingIndicator: CustomLoadingIndicatorAdapter? - + @objc public var reviewTextBottomColor = UIColor.white + + // MARK: Multipage options + /** - * Set an adapter implementation to show a custom loading indicator on the buttons which support loading. + Sets the color of the pages container and toolbar. */ - public var onButtonLoadingIndicator: OnButtonLoadingIndicatorAdapter? + @objc public var multipagePagesContainerAndToolBarColor = GiniColor(lightModeColor: Colors.Gini.pearl, darkModeColor: UIColor.from(hex: 0x1c1c1c)) + + @objc private var _multipagePagesContainerAndToolBarColor: UIColor? /** - Sets the back button text in the navigation bar on the review screen. Use this if you only want to show the title. + Sets the color of the circle indicator. */ - @objc public var navigationBarReviewTitleBackButton = "" + @objc public var indicatorCircleColor = GiniColor(lightModeColor: Colors.Gini.pearl, darkModeColor: .lightGray) /** - Sets the close button text in the navigation bar on the review screen. Use this if you only want to show the title. + Sets the tint color of the toolbar items. */ - @objc public var navigationBarReviewTitleCloseButton = "" + @objc public var multipageToolbarItemsColor = Colors.Gini.blue /** - * Sets the continue button text in the navigation bar on the review screen. + Sets the tint color of the page indicator. */ - @objc public var navigationBarReviewTitleContinueButton = "" + @objc public var multipagePageIndicatorColor = Colors.Gini.blue + + /** + Sets the background color of the page selected indicator. + */ + @objc public var multipagePageSelectedIndicatorColor = Colors.Gini.blue + + /** + Sets the background color of the page background. + */ + @objc public var multipagePageBackgroundColor = GiniColor(lightModeColor: .white, darkModeColor: UIColor.from(hex: 0x1c1c1e)) + + @objc private var _multipagePageBackgroundColor: UIColor? + + /** + Sets the tint color of the draggable icon in the page collection cell. + */ + @objc public var multipageDraggableIconColor = Colors.Gini.veryLightGray + + /** + Sets the background style when the tooltip is shown in the multipage screen. + */ + public var multipageToolTipOpaqueBackgroundStyle: OpaqueViewStyle = .blurred(style: .light) + + /** + Sets the background color for the successfull upload icon. + */ + public var multipagePageSuccessfullUploadIconBackgroundColor = Colors.Gini.springGreen + + /** + Sets the background color for the failed upload icon. + */ + public var multipagePageFailureUploadIconBackgroundColor = Colors.Gini.springGreen // MARK: Analysis options /** - * Sets the back button text in the navigation bar on the analysis screen. Use this if you only want to show the title. + Sets the color of the loading indicator on the analysis screen to the specified color. + */ + @objc public var analysisLoadingIndicatorColor = Colors.Gini.blue + + /** + Sets the color of the PDF information view on the analysis screen to the specified color. + */ + @objc public var analysisPDFInformationBackgroundColor = Colors.Gini.bluishGreen + + /** + Sets the color of the PDF information view on the analysis screen to the specified color. + */ + @objc public var analysisPDFInformationTextColor = UIColor.white + + /** + Sets the back button text in the navigation bar on the analysis screen. Use this if you only want to show the title. + + - note: Screen API only. */ @objc public var navigationBarAnalysisTitleBackButton = "" // MARK: Help screens + /** + Sets the background color for all help screens. + */ + @objc public var helpScreenBackgroundColor = GiniColor(lightModeColor: Colors.Gini.pearl, darkModeColor: UIColor.from(hex: 0x1C1C1C)) + + /** + Sets the background color for the cells on help screen. + */ + @objc public var helpScreenCellsBackgroundColor = GiniColor(lightModeColor: Colors.Gini.pearl, darkModeColor: UIColor.from(hex: 0x1C1C1C)) + /** Sets the back button text in the navigation bar on the help menu screen. Use this if you only want to show the title. + + - note: Screen API only. */ @objc public var navigationBarHelpMenuTitleBackToCameraButton = "" /** Sets the back button text in the navigation bar on the help screen. Use this if you only want to show the title. - */ + + - note: Screen API only. + */ @objc public var navigationBarHelpScreenTitleBackToMenuButton = "" /** @@ -417,15 +621,49 @@ import GiniBankAPILibrary */ @objc public var openWithAppNameForTexts = Bundle.main.appName + /** + Sets the color of the step indicator for the Open with tutorial. + */ + @objc public var stepIndicatorColor = Colors.Gini.blue + + // MARK: No results options + + /** + Sets the color of the bottom button to the specified color. + */ + @objc public var noResultsBottomButtonColor = Colors.Gini.blue + + /** + Sets the text color of the bottom button to the specified color. + */ + @objc public var noResultsBottomButtonTextColor = GiniColor.init(lightModeColor: .white, darkModeColor: .white) + + /** + Sets the corner radius of the bottom button. + */ + @objc public var noResultsBottomButtonCornerRadius: CGFloat = 0.0 + + /** + Sets the color of the warning container background to the specified color. + */ + @objc public var noResultsWarningContainerIconColor = Colors.Gini.rose + /** Sets if the Drag&Drop step should be shown in the "Open with" tutorial. */ @objc public var shouldShowDragAndDropTutorial = true + // MARK: Albums screen + + /** + Sets the text color for the select more photos button on the albums screen. + */ + @objc public var albumsScreenSelectMorePhotosTextColor = GiniColor(lightModeColor: Colors.Gini.blue, darkModeColor: Colors.Gini.blue) + /** Set an array of additional custom help menu items . Those items will be presented as table view cells on the help menu screen. By selecting the cell the user will be redirected to the page, which represented by viewController provided by customer during the `HelpMenuViewController.Item` initialization. */ - public var customMenuItems: [HelpMenuItem] = [] + public var customMenuItems: [HelpMenuViewController.Item] = [] /** Sets the default error logger. It is only used when giniErrorLoggerIsOn is true. @@ -479,96 +717,4 @@ import GiniBankAPILibrary @objc public var backToMenuButtonResource: PreferredButtonResource? @objc public var nextButtonResource: PreferredButtonResource? @objc public var cancelButtonResource: PreferredButtonResource? - - /** - Set dictionary of fonts for available text styles. Used internally. - */ - var textStyleFonts: [UIFont.TextStyle: UIFont] = [ - .largeTitle: UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.systemFont(ofSize: 34)), - .title1: UIFontMetrics(forTextStyle: .title1).scaledFont(for: UIFont.systemFont(ofSize: 28)), - .title1Bold: UIFontMetrics(forTextStyle: .title1).scaledFont(for: UIFont.boldSystemFont(ofSize: 28)), - .title2: UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22)), - .title3: UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20)), - .caption1: UIFontMetrics(forTextStyle: .caption1).scaledFont(for: UIFont.systemFont(ofSize: 12)), - .caption2: UIFontMetrics(forTextStyle: .caption2).scaledFont(for: UIFont.systemFont(ofSize: 11)), - .headline: UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 17)), - .subheadline: UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: UIFont.systemFont(ofSize: 15)), - .body: UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.systemFont(ofSize: 17)), - .bodyBold: UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.boldSystemFont(ofSize: 17)), - .callout: UIFontMetrics(forTextStyle: .callout).scaledFont(for: UIFont.systemFont(ofSize: 16)), - .calloutBold: UIFontMetrics(forTextStyle: .callout).scaledFont(for: UIFont.boldSystemFont(ofSize: 16)), - .footnote: UIFontMetrics(forTextStyle: .footnote).scaledFont(for: UIFont.systemFont(ofSize: 13)), - .footnoteBold: UIFontMetrics(forTextStyle: .footnote).scaledFont(for: UIFont.boldSystemFont(ofSize: 13)), - ] - - /** - Allows setting a custom font for specific text styles. The change will affect all screens where a specific text style was used. - - - parameter font: Font that is going to be assosiated with specific text style. You can use scaled font or scale your font with our util method `UIFont.scaledFont(_ font: UIFont, textStyle: UIFont.TextStyle)` - - parameter textStyle: Constants that describe the preferred styles for fonts. Please, find additional information [here](https://developer.apple.com/documentation/uikit/uifont/textstyle) - */ - public func updateFont(_ font: UIFont, for textStyle: UIFont.TextStyle) { - textStyleFonts[textStyle] = font - } - - var documentService: DocumentServiceProtocol? - - /// Function for clean up - /// - Parameters: - /// - paymentRecipient: paymentRecipient description - /// - paymentReference: paymentReference description - /// - iban: iban description - /// - bic: bic description - /// - amountToPay: amountToPay description - public func cleanup(paymentRecipient: String, paymentReference: String, paymentPurpose: String, iban: String, bic: String, amountToPay: ExtractionAmount) { - guard let documentService = documentService else { return } - - // Convert amount object to string - // Cut off decimals after the first 2 - let truncatedAmountValue = amountToPay.value.convertToDouble(withDecimalPoint: 2) - let amountToPayString = "\(truncatedAmountValue)" + ":" + amountToPay.currency.rawValue - - let paymentRecipientExtraction = Extraction(box: nil, - candidates: nil, - entity: "companyname", - value: paymentRecipient, - name: "paymentRecipient") - let paymentReferenceExtraction = Extraction(box: nil, - candidates: nil, - entity: "reference", - value: paymentRecipient, - name: "paymentReference") - let paymentPurposeExtraction = Extraction(box: nil, - candidates: nil, - entity: "text", - value: paymentPurpose, - name: "paymentPurpose") - let ibanExtraction = Extraction(box: nil, - candidates: nil, - entity: "iban", - value: iban, - name: "iban") - let bicExtraction = Extraction(box: nil, - candidates: nil, - entity: "bic", - value: bic, - name: "bic") - let amountExtraction = Extraction(box: nil, - candidates: nil, - entity: "amount", - value: amountToPayString, - name: "amountToPay") - - let updatedExtractions: [Extraction] = [paymentRecipientExtraction, - paymentReferenceExtraction, - paymentPurposeExtraction, - ibanExtraction, - bicExtraction, - amountExtraction] - - documentService.sendFeedback(with: updatedExtractions, updatedCompoundExtractions: nil) - - documentService.resetToInitialState() - self.documentService = nil - } } diff --git a/Sources/GiniCaptureSDK/Core/Helpers/ButtonConfiguration.swift b/Sources/GiniCaptureSDK/Core/Helpers/ButtonConfiguration.swift deleted file mode 100644 index 116914a..0000000 --- a/Sources/GiniCaptureSDK/Core/Helpers/ButtonConfiguration.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// ButtonConfiguration.swift -// -// -// Created by David Vizaknai on 30.11.2022. -// - -import UIKit - -public struct ButtonConfiguration { - let backgroundColor: UIColor - let borderColor: UIColor - let titleColor: UIColor - let shadowColor: UIColor - let cornerRadius: CGFloat - let borderWidth: CGFloat - let shadowRadius: CGFloat - - let withBlurEffect: Bool - - /// Button configuration initalizer - /// - Parameters: - /// - backgroundColor: the button's background color - /// - borderColor: the button's border color - /// - titleColor: the button's title color - /// - shadowColor: the button's color of the shadow - /// - cornerRadius: the button's corner radius - /// - borderWidth: the button's border width - /// - shadowRadius: the button's shadow radius - /// - withBlurEffect: adds a blur effect on the button ignoring the background color and making it translucent - public init(backgroundColor: UIColor, borderColor: UIColor, titleColor: UIColor, shadowColor: UIColor, cornerRadius: CGFloat, borderWidth: CGFloat, shadowRadius: CGFloat, withBlurEffect: Bool) { - self.backgroundColor = backgroundColor - self.borderColor = borderColor - self.titleColor = titleColor - self.shadowColor = shadowColor - self.cornerRadius = cornerRadius - self.borderWidth = borderWidth - self.shadowRadius = shadowRadius - self.withBlurEffect = withBlurEffect - } -} - -extension BottomLabelButton { - func configure(with configuration: ButtonConfiguration) { - self.backgroundColor = configuration.backgroundColor - self.layer.borderColor = configuration.borderColor.cgColor - self.layer.shadowColor = configuration.shadowColor.cgColor - - self.actionLabel.textColor = configuration.titleColor - - self.layer.cornerRadius = configuration.cornerRadius - self.layer.borderWidth = configuration.borderWidth - self.layer.shadowRadius = configuration.shadowRadius - } -} - -public extension UIButton { - func configure(with configuration: ButtonConfiguration) { - self.backgroundColor = configuration.backgroundColor - self.layer.borderColor = configuration.borderColor.cgColor - self.layer.shadowColor = configuration.shadowColor.cgColor - self.setTitleColor(configuration.titleColor, for: .normal) - self.setTitleColor(configuration.titleColor, for: .highlighted) - self.setTitleColor(configuration.titleColor, for: .selected) - - self.layer.cornerRadius = configuration.cornerRadius - self.layer.borderWidth = configuration.borderWidth - self.layer.shadowRadius = configuration.shadowRadius - - if configuration.withBlurEffect { - self.addBlurEffect(cornerRadius: configuration.cornerRadius) - } else { - self.removeBlurEffect() - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureDocumentValidator.swift b/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureDocumentValidator.swift index 45ba1da..9e778bf 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureDocumentValidator.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureDocumentValidator.swift @@ -6,7 +6,6 @@ // import Foundation -import CoreGraphics public final class GiniCaptureDocumentValidator { @@ -62,11 +61,6 @@ fileprivate extension GiniCaptureDocumentValidator { try validate(qrCode: document) case let pdfDocument as GiniPDFDocument: if pdfDocument.data.isPDF { - if let dataProvider = CGDataProvider(data: pdfDocument.data as CFData), let pdfDocument = CGPDFDocument(dataProvider) { - if !pdfDocument.isUnlocked { - throw DocumentValidationError.pdfPasswordProtected - } - } if case 1...maxPagesCount = pdfDocument.numberPages { return } else { diff --git a/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureUtils.swift b/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureUtils.swift index aedbadf..02b11fe 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureUtils.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/GiniCaptureUtils.swift @@ -26,25 +26,6 @@ public func UIImageNamedPreferred(named name: String) -> UIImage? { return UIImage(named: name, in: giniCaptureBundle(), compatibleWith: nil) } -/** - Returns an optional `UIColor` instance with the given `name` preferably from the client's bundle. - - - parameter name: The name of the UIColor from `GiniColors` asset catalog. - - - returns: color if found with name. - */ -public func UIColorPreferred(named name: String) -> UIColor { - if let clientColor = UIColor(named: name) { - return clientColor - } - - if let color = UIColor(named: name, in: giniCaptureBundle(), compatibleWith: nil) { - return color - } else { - fatalError("The color named '\(name)' does not exist.") - } -} - /** Returns a localized string resource preferably from the client's bundle. @@ -255,7 +236,3 @@ extension Foundation.Bundle { return Bundle(for: GiniCapture.self) }() } - -public struct RoundedCorners { - static let cornerRadius: CGFloat = 8 -} diff --git a/Sources/GiniCaptureSDK/Core/Helpers/ImageMetaInformationManager.swift b/Sources/GiniCaptureSDK/Core/Helpers/ImageMetaInformationManager.swift index 0464da0..b5f2108 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/ImageMetaInformationManager.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/ImageMetaInformationManager.swift @@ -130,8 +130,8 @@ final class ImageMetaInformationManager { } // Returns the current image but with all meta information added - func imageByAddingMetadata(to processedImageData: Data? = nil, withCompression compression: CGFloat = JPEGDefaultCompression) -> Data? { - guard let image = processedImageData ?? imageData, + func imageByAddingMetadata(withCompression compression: CGFloat = JPEGDefaultCompression) -> Data? { + guard let image = imageData, let information = metaInformation, (image.isJPEG || image.isTIFF) else { return nil } let targetData = NSMutableData() diff --git a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/AnalysisStrings.swift b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/AnalysisStrings.swift index 1339480..d75272c 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/AnalysisStrings.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/AnalysisStrings.swift @@ -8,9 +8,11 @@ import Foundation public enum AnalysisStrings: LocalizableStringResource { - - case analysisErrorMessage, documentCreationErrorMessage, cancelledMessage - + + case analysisErrorMessage, documentCreationErrorMessage, cancelledMessage, loadingText, pdfPages, + suggestion1Text, suggestion2Text, suggestion3Text, suggestion4Text, suggestion5Text, suggestionHeader, + defaultPdfDokumentTitle + public var tableName: String { return "analysis" } @@ -23,6 +25,26 @@ public enum AnalysisStrings: LocalizableStringResource { return ("error.documentCreation", "This message is shown when there is an error creating the document") case .cancelledMessage: return ("error.cancelled", "This message is shown when the analysis was cancelled") + case .loadingText: + return ("loadingText", "Text appearing at the center of the analysis screen " + + "indicating that the document is being analysed") + case .pdfPages: + return ("pdfpages", + "Text appearing at the top of the analysis screen indicating pdf number of pages") + case .suggestion1Text: + return ("suggestion.1", "First suggestion text for analysis screen") + case .suggestion2Text: + return ("suggestion.2", "Second suggestion text for analysis screen") + case .suggestion3Text: + return ("suggestion.3", "Third suggestion text for analysis screen") + case .suggestion4Text: + return ("suggestion.4", "Fourth suggestion text for analysis screen") + case .suggestion5Text: + return ("suggestion.5", "Fifth suggestion text for analysis screen") + case .suggestionHeader: + return ("suggestion.header", "Fourth suggestion text for analysis screen") + case .defaultPdfDokumentTitle: + return ("defaultPdfDokumentTitle", "Default PDF document title") } } diff --git a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/CameraStrings.swift b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/CameraStrings.swift index 2963a31..0424446 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/CameraStrings.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/CameraStrings.swift @@ -10,12 +10,13 @@ import Foundation enum CameraStrings: LocalizableStringResource { case captureButton, captureFailedMessage, capturedImagesStackSubtitleLabel, errorPopupCancelButton, - errorPopupGrantAccessButton, errorPopupPickAnotherFileButton, errorPopupReviewPagesButton, - importFileButtonLabel, tooManyPagesErrorMessage, mixedArraysPopupCancelButton, - mixedArraysPopupUsePhotosButton, mixedDocumentsErrorMessage, notAuthorizedButton, - notAuthorizedMessage, photoLibraryAccessDeniedMessage, qrCodeDetectedPopupMessage, qrCodeDetectedPopupButton, - popupTitleImportPDF, popupOptionPhotos, popupOptionFiles, popupTitleImportPDForPhotos, popupCancel, - unsupportedQrCodeDetectedPopupMessage, unknownErrorMessage, failedToOpenDocumentErrorMessage, multiplePdfErrorMessage, errorConfirmButton + errorPopupGrantAccessButton, errorPopupPickAnotherFileButton, errorPopupReviewPagesButton, + exceededFileSizeErrorMessage, documentValidationGeneralErrorMessage, fileImportTipLabel, importFileButtonLabel,qrCodeTipLabel, + mixedArraysPopupCancelButton, mixedArraysPopupUsePhotosButton, mixedDocumentsErrorMessage, notAuthorizedButton, + notAuthorizedMessage, photoLibraryAccessDeniedMessage, qrCodeDetectedPopupMessage, qrCodeDetectedPopupButton, + tooManyPagesErrorMessage, unknownErrorMessage, wrongFormatErrorMessage, popupTitleImportPDF, popupOptionPhotos, + popupOptionFiles, popupTitleImportPDForPhotos, popupCancel, unsupportedQrCodeDetectedPopupMessage, + failedToOpenDocumentErrorMessage var tableName: String { return "camera" @@ -38,6 +39,14 @@ enum CameraStrings: LocalizableStringResource { return ("errorPopup.pickAnotherFileButton", "pick another file button title") case .errorPopupReviewPagesButton: return ("errorPopup.reviewPages", "review pages button title") + case .exceededFileSizeErrorMessage: + return ("documentValidationError.excedeedFileSize", + "Message text error shown in camera screen when a file size is higher than 10MB") + case .documentValidationGeneralErrorMessage: + return ("documentValidationError.general", + "Message text of a general document validation error shown in camera screen") + case .fileImportTipLabel: + return ("fileImportTip", "tooltip text indicating new file import feature") case .mixedArraysPopupCancelButton: return ("mixedarrayspopup.cancel", "cancel button text for popup") case .mixedArraysPopupUsePhotosButton: @@ -58,9 +67,16 @@ enum CameraStrings: LocalizableStringResource { return ("qrCodeDetectedPopup.message", "Proceed button message") case .qrCodeDetectedPopupButton: return ("qrCodeDetectedPopup.buttonTitle", "Proceed button title") + case .tooManyPagesErrorMessage: + return ("documentValidationError.tooManyPages", + "Message text error shown in camera screen when a pdf length is higher than 10 pages" ) case .unknownErrorMessage: return ("unknownError", "This message is shown when" + "there is an unknown error in the camera") + case .wrongFormatErrorMessage: + return ("documentValidationError.wrongFormat", + "Message text error shown in camera screen when a file " + + "has a wrong format (neither PDF, JPEG, GIF, TIFF or PNG)") case .popupTitleImportPDF: return ("popupTitleImportPDF", "File picker popup title for only PDFs") case .popupTitleImportPDForPhotos: @@ -73,17 +89,11 @@ enum CameraStrings: LocalizableStringResource { return ("popupCancel", "File picker popup cancel option") case .unsupportedQrCodeDetectedPopupMessage: return ("unsupportedQrCodeDetectedPopup.message", "Popup message") + case .qrCodeTipLabel: + return ("qrCodeTip", "tooltip text indicating new qr code feature") case .failedToOpenDocumentErrorMessage: return ("filepicker.failedToOpenDocument", "Error message when the the picked document couldn't be opened") - case .tooManyPagesErrorMessage: - return ("documentValidationError.tooManyPages", - "Message text error shown in camera screen when a pdf length is higher than 10 pages" ) - case .multiplePdfErrorMessage: - return ("filepicker.multiplePdfErrorMessage", - "Error message when the user selects multiple PDFs") - case .errorConfirmButton: - return ("filepicker.errorPopup.confirmButton", "Ok button title") } } diff --git a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/GalleryStrings.swift b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/GalleryStrings.swift new file mode 100644 index 0000000..47e0aef --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/GalleryStrings.swift @@ -0,0 +1,42 @@ +// +// GalleryStrings.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 8/1/18. +// + +import Foundation + +enum GalleryStrings: LocalizableStringResource { + + case albumsTitle, imagePickerOpenButton + + var tableName: String { + switch self { + case .albumsTitle: + return "albums" + case .imagePickerOpenButton: + return "imagepicker" + } + } + + var tableEntry: LocalizationEntry { + switch self { + case .albumsTitle: + return ("title", "title for the albums picker view controller") + case .imagePickerOpenButton: + return ("openbutton", "Open button title") + } + } + + var isCustomizable: Bool { + return true + } + + var fallbackTableEntry: String { + switch self { + default: + return "" + } + } +} diff --git a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/HelpStrings.swift b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/HelpStrings.swift index 755e2bc..ff98117 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/HelpStrings.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/HelpStrings.swift @@ -15,9 +15,7 @@ enum HelpStrings: LocalizableStringResource { openWithTutorialStep3Title, openWithTutorialStep3Subtitle, supportedFormatsTitle, supportedFormatsSection1Title, supportedFormatsSection1Item1Text, supportedFormatsSection1Item2Text, supportedFormatsSection1Item3Text, supportedFormatsSection1Item4Text, - supportedFormatsSection2Title, supportedFormatsSection2Item1Text, supportedFormatsSection2Item2Text, - formatsTitle - + supportedFormatsSection2Title, supportedFormatsSection2Item1Text, supportedFormatsSection2Item2Text var tableName: String { return "help" } @@ -66,8 +64,6 @@ enum HelpStrings: LocalizableStringResource { return ("supportedFormats.section.2.item.2", "message for second item on supported formats section") case .supportedFormatsSection1Item4Text: return ("supportedFormats.section.1.item.4", "message for fouth item on supported formats section") - case .formatsTitle: - return ("Fomrats", "message for Formats screen") } } diff --git a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/MultipageReviewStrings.swift b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/MultipageReviewStrings.swift new file mode 100644 index 0000000..1e203e1 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/MultipageReviewStrings.swift @@ -0,0 +1,47 @@ +// +// MultipageReviewStrings.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 7/31/18. +// + +import Foundation + +enum MultipageReviewStrings: LocalizableStringResource { + + case addButtonLabel, dragAndDropTipMessage, reorderContainerTooltipMessage, retakeActionButton, retryActionButton, + titleMessage + + var tableName: String { + return "multipagereview" + } + + var tableEntry: LocalizationEntry { + switch self { + case .addButtonLabel: + return ("addButtonLabel", "label shown below add button") + case .dragAndDropTipMessage: + return ("dragAndDropTip", "drag and drop tip shown below pages collection") + case .reorderContainerTooltipMessage: + return ("reorderContainerTooltipMessage", "reorder button tooltip message") + case .retakeActionButton: + return ("error.retakeAction", "button title for retake action") + case .retryActionButton: + return ("error.retryAction", "button title for retry action") + case .titleMessage: + return ("title", "title with the page indicator") + } + } + + var isCustomizable: Bool { + return true + } + + var fallbackTableEntry: String { + switch self { + default: + return "" + } + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/ReviewStrings.swift b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/ReviewStrings.swift new file mode 100644 index 0000000..316b0c4 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Helpers/Localization/String Resources/ReviewStrings.swift @@ -0,0 +1,48 @@ +// +// ReviewStrings.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 7/31/18. +// + +import Foundation + +enum ReviewStrings: LocalizableStringResource { + + case bottomText, documentImageTitle, rotateButton, topText, unknownErrorMessage + + var tableName: String { + return "review" + } + + var tableEntry: LocalizationEntry { + switch self { + case .bottomText: + return ("bottom", "Text at the bottom of the review screen encouraging the " + + "user to check sharpness by double-tapping the image") + case .documentImageTitle: + return ("documentImageTitle", + "Title for document image in review screen will be used exclusively for accessibility label") + case .rotateButton: + return ("rotateButton", + "Title for rotate button in review screen will be used exclusively for accessibility label") + case .topText: + return ("top", "Text at the top of the review screen asking the user if " + + "the full document is sharp and in the correct orientation") + case .unknownErrorMessage: + return ("unknownError", "This message is shown when Photo library permission is denied") + } + } + + var isCustomizable: Bool { + return true + } + + var fallbackTableEntry: String { + switch self { + default: + return "" + } + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Helpers/QRCodesExtractor.swift b/Sources/GiniCaptureSDK/Core/Helpers/QRCodesExtractor.swift index 1155f1b..66c556b 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/QRCodesExtractor.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/QRCodesExtractor.swift @@ -63,26 +63,42 @@ public final class QRCodesExtractor { let lines = string.splitlines var parameters: [String: String] = [:] - if !lines[4].isEmpty { + if lines.indices.contains(4) && !lines[4].isEmpty { parameters["bic"] = lines[4] + } else { + parameters["bic"] = "" } - if !lines[5].isEmpty { + if lines.indices.contains(5) && !lines[5].isEmpty { parameters["paymentRecipient"] = lines[5] + } else { + parameters["paymentRecipient"] = "" } - - if !lines[9].isEmpty { + + if lines.indices.contains(9) && !lines[9].isEmpty { parameters["paymentReference"] = lines[9] + } else { + parameters["paymentReference"] = "" } - - if IBANValidator().isValid(iban: lines[6]) { + + // if index out of range, return empty string + + if lines.indices.contains(6) && IBANValidator().isValid(iban: lines[6]) { parameters["iban"] = lines[6] + } else { + parameters["iban"] = "" } - - if let amountToPay = normalize(amount: lines[7], currency: nil) { - parameters["amountToPay"] = amountToPay + + if lines.indices.contains(7) { + if let amountToPay = normalize(amount: lines[7], currency: nil) { + parameters["amountToPay"] = amountToPay + } else { + parameters["amountToPay"] = "" + } + } else { + parameters["amountToPay"] = "" } - + return parameters } diff --git a/Sources/GiniCaptureSDK/Core/Helpers/Utils.swift b/Sources/GiniCaptureSDK/Core/Helpers/Utils.swift deleted file mode 100644 index 56f9f40..0000000 --- a/Sources/GiniCaptureSDK/Core/Helpers/Utils.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Utils.swift -// -// -// Created by Krzysztof Kryniecki on 01/08/2022. -// - -import UIKit - -extension UITableViewCell { - func round(corners: UIRectCorner, withRadius radius: CGFloat) { - let mask = UIBezierPath( - roundedRect: bounds, - byRoundingCorners: corners, - cornerRadii: CGSize(width: radius, height: radius)) - let shape = CAShapeLayer() - shape.frame = bounds - shape.path = mask.cgPath - layer.mask = shape - clipsToBounds = true - } - - func reset() { - layer.mask = nil - clipsToBounds = true - } - - open override func layoutSubviews() { - super.layoutSubviews() - layer.mask?.frame = bounds - } -} - -public struct GiniMargins { - public static let margin: CGFloat = 16 - public static let iPadAspectScale: CGFloat = 0.7 -} - -extension UIButton { - func addBlurEffect(cornerRadius: CGFloat) { - backgroundColor = .clear - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - blurView.isUserInteractionEnabled = false - blurView.backgroundColor = .clear - if cornerRadius > 0 { - blurView.layer.cornerRadius = cornerRadius - blurView.layer.masksToBounds = true - } - insertSubview(blurView, at: 0) - blurView.translatesAutoresizingMaskIntoConstraints = false - leadingAnchor.constraint(equalTo: blurView.leadingAnchor).isActive = true - trailingAnchor.constraint(equalTo: blurView.trailingAnchor, constant: -0).isActive = true - topAnchor.constraint(equalTo: blurView.topAnchor).isActive = true - bottomAnchor.constraint(equalTo: blurView.bottomAnchor).isActive = true - if let imageView = imageView { - imageView.backgroundColor = .clear - bringSubviewToFront(imageView) - } - } - - func removeBlurEffect() { - let effectView = self.subviews.first(where: { $0 is UIVisualEffectView }) - effectView?.removeFromSuperview() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Models/ExtractionAmount.swift b/Sources/GiniCaptureSDK/Core/Models/ExtractionAmount.swift deleted file mode 100644 index 750d5a1..0000000 --- a/Sources/GiniCaptureSDK/Core/Models/ExtractionAmount.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ExtractionAmount.swift -// -// -// Created by David Vizaknai on 08.12.2022. -// - -import Foundation - -public struct ExtractionAmount { - public let value: Decimal - public let currency: AmountCurrency - - public init(value: Decimal, currency: AmountCurrency) { - self.value = value - self.currency = currency - } -} - -public enum AmountCurrency: String { - case EUR, GBP, USD, CHF -} diff --git a/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift b/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift index 1885dfe..77657ce 100644 --- a/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift +++ b/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift @@ -72,9 +72,9 @@ public class GiniCaptureDocumentBuilder: NSObject { - Returns: A `GiniCaptureDocument` if `data` has a valid type or `nil` if it hasn't. */ - public func build(with data: Data, fileName: String?) -> GiniCaptureDocument? { + public func build(with data: Data) -> GiniCaptureDocument? { if data.isPDF { - return GiniPDFDocument(data: data, fileName: fileName) + return GiniPDFDocument(data: data) } else if data.isImage { return GiniImageDocument(data: data, imageSource: documentSource, @@ -100,7 +100,7 @@ public class GiniCaptureDocumentBuilder: NSObject { return } - completion(self.build(with: data, fileName: openURL.lastPathComponent)) + completion(self.build(with: data)) } } } diff --git a/Sources/GiniCaptureSDK/Core/Models/GiniCaptureFont.swift b/Sources/GiniCaptureSDK/Core/Models/GiniCaptureFont.swift index 94a231e..c79ea61 100644 --- a/Sources/GiniCaptureSDK/Core/Models/GiniCaptureFont.swift +++ b/Sources/GiniCaptureSDK/Core/Models/GiniCaptureFont.swift @@ -6,8 +6,6 @@ // Copyright © 2017 Gini GmbH. All rights reserved. // -// MARK: - TODO DELETE - import UIKit /** @@ -38,7 +36,11 @@ public class GiniCaptureFont: NSObject { } public func with(weight: UIFont.Weight, size: CGFloat, style: UIFont.TextStyle) -> UIFont { - return UIFontMetrics(forTextStyle: style).scaledFont(for: font(for: weight).withSize(size)) + if #available(iOS 11.0, *) { + return UIFontMetrics(forTextStyle: style).scaledFont(for: font(for: weight).withSize(size)) + } else { + return font(for: weight).withSize(size) + } } private func font(for weight: UIFont.Weight) -> UIFont { diff --git a/Sources/GiniCaptureSDK/Core/Models/GiniError.swift b/Sources/GiniCaptureSDK/Core/Models/GiniError.swift index 4cbd5f2..d501705 100644 --- a/Sources/GiniCaptureSDK/Core/Models/GiniError.swift +++ b/Sources/GiniCaptureSDK/Core/Models/GiniError.swift @@ -53,7 +53,7 @@ public protocol GiniCaptureError: Error { public var message: String { switch self { case .unknown: - return NSLocalizedStringPreferredFormat("ginicapture.review.unknownError", comment: "Unknown error") + return .localized(resource: ReviewStrings.unknownErrorMessage) } } } @@ -75,9 +75,6 @@ public protocol GiniCaptureError: Error { /// Could not open the document (data could not be read or unsupported file type or some other issue) case failedToOpenDocument - - /// MultiplePDFs unsupported - case multiplePdfsUnsupported public var message: String { switch self { @@ -89,8 +86,6 @@ public protocol GiniCaptureError: Error { return .localized(resource: CameraStrings.mixedDocumentsErrorMessage) case .failedToOpenDocument: return .localized(resource: CameraStrings.failedToOpenDocumentErrorMessage) - case .multiplePdfsUnsupported: - return .localized(resource: CameraStrings.multiplePdfErrorMessage) } } } @@ -140,50 +135,28 @@ public protocol GiniCaptureError: Error { /// PDF length exceeded case pdfPageLengthExceeded - // PDF password protected - case pdfPasswordProtected - /// QR Code formar not valid case qrCodeFormatNotValid public var message: String { switch self { case .exceededMaxFileSize: - return NSLocalizedStringPreferredFormat( - "ginicapture.camera.documentValidationError.excedeedFileSize", - comment: "Message text error shown in camera screen when a file size is higher than 10MB") + return .localized(resource: CameraStrings.exceededFileSizeErrorMessage) case .imageFormatNotValid: - return NSLocalizedStringPreferredFormat( - "ginicapture.camera.documentValidationError.wrongFormat", - comment: "Message text error shown in camera screen when a file " + - "has a wrong format (neither PDF, JPEG, GIF, TIFF or PNG)") + return .localized(resource: CameraStrings.wrongFormatErrorMessage) case .fileFormatNotValid: - return NSLocalizedStringPreferredFormat( - "ginicapture.camera.documentValidationError.wrongFormat", - comment: "Message text error shown in camera screen when a file " + - "has a wrong format (neither PDF, JPEG, GIF, TIFF or PNG)") + return .localized(resource: CameraStrings.wrongFormatErrorMessage) case .pdfPageLengthExceeded: - return NSLocalizedStringPreferredFormat( - "ginicapture.camera.documentValidationError.tooManyPages", - comment: "Message text error shown in camera screen when a pdf length is higher than 10 pages") - case .pdfPasswordProtected: - return NSLocalizedStringPreferredFormat( - "ginicapture.camera.documentValidationError.pdfPasswordProtected", - comment: "Message text error shown when there pdf uplaoded is password protected") + return .localized(resource: CameraStrings.tooManyPagesErrorMessage) case .qrCodeFormatNotValid: - return NSLocalizedStringPreferredFormat( - "ginicapture.camera.documentValidationError.wrongFormat", - comment: "Message text error shown in camera screen when a file " + - "has a wrong format (neither PDF, JPEG, GIF, TIFF or PNG)") + return .localized(resource: CameraStrings.wrongFormatErrorMessage) case .unknown: - return NSLocalizedStringPreferredFormat( - "ginicapture.camera.documentValidationError.general", - comment: "Message text of a general document validation error shown in camera screen") + return .localized(resource: CameraStrings.documentValidationGeneralErrorMessage) } } public static func == (lhs: DocumentValidationError, rhs: DocumentValidationError) -> Bool { - return lhs.rawValue == rhs.rawValue + return lhs.message == rhs.message } } diff --git a/Sources/GiniCaptureSDK/Core/Models/GiniImageDocument.swift b/Sources/GiniCaptureSDK/Core/Models/GiniImageDocument.swift index a643e0c..7bd4ff6 100644 --- a/Sources/GiniCaptureSDK/Core/Models/GiniImageDocument.swift +++ b/Sources/GiniCaptureSDK/Core/Models/GiniImageDocument.swift @@ -25,9 +25,7 @@ final public class GiniImageDocument: NSObject, GiniCaptureDocument { public var rotationDelta: Int { // Should be normalized to be in [0, 360) return self.metaInformationManager.imageRotationDeltaDegrees() } - - // A flag to determine if the document is opened from another app or from the SDK - public var isFromOtherApp: Bool + fileprivate let metaInformationManager: ImageMetaInformationManager /** @@ -40,37 +38,35 @@ final public class GiniImageDocument: NSObject, GiniCaptureDocument { */ init(data: Data, - processedImageData: Data? = nil, imageSource: DocumentSource, imageImportMethod: DocumentImportMethod? = nil, deviceOrientation: UIInterfaceOrientation? = nil) { - self.previewImage = UIImage(data: processedImageData ?? data) + self.previewImage = UIImage(data: data) self.isReviewable = true self.id = UUID().uuidString - - switch imageSource { - case .appName(name: _) : - isFromOtherApp = true - default: - isFromOtherApp = false - } - self.isImported = imageSource != DocumentSource.camera self.metaInformationManager = ImageMetaInformationManager(imageData: data, deviceOrientation: deviceOrientation, imageSource: imageSource, imageImportMethod: imageImportMethod) - - // The processed image data is assumed to be always in the correct orientation - if processedImageData != nil { - self.metaInformationManager.update(imageOrientation: .up) - } - if let dataWithMetadata = metaInformationManager.imageByAddingMetadata(to: processedImageData) { + if let dataWithMetadata = metaInformationManager.imageByAddingMetadata() { self.data = dataWithMetadata } else { self.data = data - } + } + + } + + func rotatePreviewImage90Degrees() { + guard let rotatedImage = self.previewImage?.rotated90Degrees() else { return } + metaInformationManager.rotate(degrees: 90, imageOrientation: rotatedImage.imageOrientation) + + if let data = metaInformationManager.imageByAddingMetadata() { + self.previewImage = UIImage(data: data) + } else { + self.previewImage = rotatedImage + } } } diff --git a/Sources/GiniCaptureSDK/Core/Models/GiniPDFDocument.swift b/Sources/GiniCaptureSDK/Core/Models/GiniPDFDocument.swift index 0a1b4a4..1c022d3 100644 --- a/Sources/GiniCaptureSDK/Core/Models/GiniPDFDocument.swift +++ b/Sources/GiniCaptureSDK/Core/Models/GiniPDFDocument.swift @@ -27,27 +27,20 @@ final public class GiniPDFDocument: NSObject, GiniCaptureDocument { Initializes a GiniPDFDocument with a preview image (from the first page) - Parameter data: PDF data - - Parameter fileName: PDF file name */ - init(data: Data, fileName: String?) { + init(data: Data) { self.data = data self.isReviewable = false self.id = UUID().uuidString self.isImported = true super.init() - + if let dataProvider = CGDataProvider(data: data as CFData), let pdfDocument = CGPDFDocument(dataProvider) { self.numberPages = pdfDocument.numberOfPages self.previewImage = renderFirstPage(fromPdf: pdfDocument) - - if let fileName = fileName { - self.pdfTitle = fileName - } else { - self.pdfTitle = getKey("Title", from: pdfDocument) - } - + self.pdfTitle = getKey("Title", from: pdfDocument) } } @@ -121,7 +114,7 @@ extension GiniPDFDocument: NSItemProviderReading { } static public func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self { - return self.init(data: data, fileName: nil) + return self.init(data: data) } } diff --git a/Sources/GiniCaptureSDK/Core/Models/GiniQRCodeDocument.swift b/Sources/GiniCaptureSDK/Core/Models/GiniQRCodeDocument.swift index 1ac64ef..e0736ef 100644 --- a/Sources/GiniCaptureSDK/Core/Models/GiniQRCodeDocument.swift +++ b/Sources/GiniCaptureSDK/Core/Models/GiniQRCodeDocument.swift @@ -43,15 +43,16 @@ import UIKit return .bezahl } else if self.scannedString.starts(with: "epspayment://") { return .eps4mobile - } else if let lines = Optional(self.scannedString.splitlines), - lines.count > 9 && - (lines[1] == "001" || lines[1] == "002") { - + } else if let lines = Optional(self.scannedString.splitlines), lines.count > 0 && lines[0] == "BCD" { if !(lines[2] == "1" || lines[2] == "2") { print("WARNING: Character set \(lines[2]) is unknown. Expected version 1 or 2.") } - - return .epc06912 + + if IBANValidator().isValid(iban: lines[6]) { + return .epc06912 + } else { + return nil + } } else { return nil } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift index eaac10f..84d109b 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift @@ -10,25 +10,27 @@ import UIKit /** Delegate which can be used to communicate back to the analysis screen allowing to display custom messages on screen. + + - note: Screen API only. */ @objc public protocol AnalysisDelegate { - + /** - Will display an error screen with predefined type. + Will display an error view on the analysis screen with a custom message. + The provided action will be called, when the user taps on the error view. - - parameter message: The error type to be displayed. + - parameter message: The error message to be displayed. + - parameter action: The action to be performed after the user tapped the error view. */ - func displayError( - errorType: ErrorType, - animated: Bool - ) - + func displayError(withMessage message: String?, andAction action: (() -> Void)?) + /** In case that the `GiniCaptureDocument` analysed is an image it will display a no results screen - with some capture suggestions. - + with some capture suggestions. It won't show any screen if it is not an image, return `false` in that case. + + - returns: `true` if the screen was shown or `false` if it wasn't. */ - func tryDisplayNoResultsScreen() + func tryDisplayNoResultsScreen() -> Bool } /** @@ -38,69 +40,63 @@ import UIKit - note: Component API only. */ @objcMembers public final class AnalysisViewController: UIViewController { - + var didShowAnalysis: (() -> Void)? - private let document: GiniCaptureDocument - private let giniConfiguration: GiniConfiguration - private static let loadingIndicatorContainerHeight: CGFloat = 60 - + fileprivate let document: GiniCaptureDocument + fileprivate let giniConfiguration: GiniConfiguration + fileprivate static let loadingIndicatorContainerHeight: CGFloat = 60 + public weak var trackingDelegate: AnalysisScreenTrackingDelegate? + // User interface - private var imageView: UIImageView = { + fileprivate var imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit return imageView }() - - private var loadingIndicatorView: UIActivityIndicatorView = { + + fileprivate var loadingIndicatorView: UIActivityIndicatorView = { let indicatorView = UIActivityIndicatorView() indicatorView.hidesWhenStopped = true indicatorView.style = .whiteLarge indicatorView.startAnimating() return indicatorView }() - - private lazy var loadingIndicatorText: UILabel = { + + fileprivate lazy var loadingIndicatorText: UILabel = { var loadingText = UILabel() - loadingText.font = giniConfiguration.textStyleFonts[.bodyBold] + loadingText.text = .localized(resource: AnalysisStrings.loadingText) + loadingText.font = giniConfiguration.customFont.with(weight: .regular, size: 18, style: .body) loadingText.textAlignment = .center - loadingText.adjustsFontForContentSizeCategory = true - loadingText.textColor = GiniColor(light: .GiniCapture.dark1, dark: .GiniCapture.light1).uiColor() - loadingText.isAccessibilityElement = true - loadingText.numberOfLines = 0 - - if document.type == .pdf { - if let documentTitle = (document as? GiniPDFDocument)?.pdfTitle { - let titleString = NSLocalizedStringPreferredFormat("ginicapture.analysis.loadingText.pdf", - comment: "Analysis screen loading text for PDF") - - loadingText.text = String(format: titleString, documentTitle) - } else { - loadingText.text = NSLocalizedStringPreferredFormat("ginicapture.analysis.loadingText", - comment: "Analysis screen loading text for images") - } - } else { - loadingText.text = NSLocalizedStringPreferredFormat("ginicapture.analysis.loadingText", - comment: "Analysis screen loading text for images") - } - + loadingText.textColor = .white return loadingText }() - - private lazy var loadingIndicatorContainer: UIView = { + + fileprivate lazy var loadingIndicatorContainer: UIView = { + let size = CGSize(width: AnalysisViewController.loadingIndicatorContainerHeight, + height: AnalysisViewController.loadingIndicatorContainerHeight) let loadingIndicatorContainer = UIView(frame: CGRect(origin: .zero, - size: .zero)) + size: size)) + loadingIndicatorContainer.backgroundColor = .white + loadingIndicatorContainer.layer.cornerRadius = AnalysisViewController.loadingIndicatorContainerHeight / 2 return loadingIndicatorContainer }() - - private lazy var overlayView: UIView = { + + fileprivate lazy var overlayView: UIView = { let overlayView = UIView() - overlayView.backgroundColor = GiniColor(light: .GiniCapture.light1, - dark: .GiniCapture.dark1).uiColor().withAlphaComponent(0.6) + overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.6) return overlayView }() - + + fileprivate lazy var errorView: NoticeView = { + let errorView = NoticeView(text: "", + type: .error, + noticeAction: NoticeAction(title: "", action: {})) + errorView.translatesAutoresizingMaskIntoConstraints = false + return errorView + }() + /** Designated intitializer for the `AnalysisViewController`. @@ -114,7 +110,7 @@ import UIKit self.giniConfiguration = giniConfiguration super.init(nibName: nil, bundle: nil) } - + /** Convenience intitializer for the `AnalysisViewController`. @@ -125,7 +121,7 @@ import UIKit public convenience init(document: GiniCaptureDocument) { self.init(document: document, giniConfiguration: GiniConfiguration.shared) } - + /** Returns an object initialized from data in a given unarchiver. @@ -134,157 +130,168 @@ import UIKit public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - public override func viewDidLoad() { - super.viewDidLoad() - + + public override func loadView() { + super.loadView() + imageView.image = document.previewImage + edgesForExtendedLayout = [] + view.backgroundColor = .black + // Configure view hierachy - setupView() + addImageView() + + if let document = document as? GiniPDFDocument { + addLoadingView(intoContainer: loadingIndicatorContainer) + loadingIndicatorView.color = giniConfiguration.analysisLoadingIndicatorColor + + showPDFInformationView(withDocument: document, + giniConfiguration: giniConfiguration) + } else { + addLoadingView() + addLoadingText(below: loadingIndicatorView) + addOverlay() + + if document.type == .image { + showCaptureSuggestions(giniConfiguration: giniConfiguration) + } + } + + addErrorView() } - + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) didShowAnalysis?() } - + // MARK: Toggle animation - - /// Displays a loading activity indicator. Should be called when document analysis is started. + /** + Displays a loading activity indicator. Should be called when document analysis is started. + */ public func showAnimation() { - if let loadingIndicator = giniConfiguration.customLoadingIndicator { - loadingIndicator.startAnimation() - } else { - loadingIndicatorView.startAnimating() - } + loadingIndicatorView.startAnimating() } - - /// Hides the loading activity indicator. Should be called when document analysis is finished. + + /** + Hides the loading activity indicator. Should be called when document analysis is finished. + */ public func hideAnimation() { - if let loadingIndicator = giniConfiguration.customLoadingIndicator { - loadingIndicator.stopAnimation() - } else { - loadingIndicatorView.stopAnimating() - } + loadingIndicatorView.stopAnimating() } - + /** - Set up the view elements on the screen + Shows an error when there was an error with either the analysis or document upload */ - - private func setupView() { - addImageView() - edgesForExtendedLayout = [] - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - title = NSLocalizedStringPreferredFormat("ginicapture.analysis.screenTitle", comment: "Analysis screen title") - - if let document = document as? GiniPDFDocument { - imageView.image = document.previewImage - } - - configureLoadingIndicator() - addOverlay() - - if document is GiniImageDocument { - showCaptureSuggestions(giniConfiguration: giniConfiguration) - } + public func showError(with message: String, action: @escaping () -> Void ) { + + trackingDelegate?.onAnalysisScreenEvent(event: Event(type: .error, info: ["message" : message])) + + errorView.textLabel.text = message + errorView.userAction = NoticeAction(title: NoticeActionType.retry.title, action: { [weak self] in + guard let self = self else { return } + self.trackingDelegate?.onAnalysisScreenEvent(event: Event(type: .retry)) + self.errorView.hide(true, completion: action) + }) + errorView.show() } - - private func addImageView() { - view.addSubview(imageView) + + /** + Hide the error view + */ + public func hideError(animated: Bool = false) { + errorView.hide(animated, completion: nil) + } + + fileprivate func addImageView() { + self.view.addSubview(imageView) imageView.translatesAutoresizingMaskIntoConstraints = false - + Constraints.active(item: imageView, attr: .top, relatedBy: .equal, to: view.safeAreaLayoutGuide, attr: .top, priority: 999) - Constraints.active(item: imageView, attr: .bottom, relatedBy: .equal, to: view.safeAreaLayoutGuide, - attr: .bottom, priority: 999) - Constraints.active(item: imageView, attr: .centerX, relatedBy: .equal, to: view, attr: .centerX) - Constraints.active(item: imageView, attr: .width, relatedBy: .equal, to: view, attr: .width, multiplier: 0.9) + Constraints.active(item: imageView, attr: .bottom, relatedBy: .equal, to: view.safeAreaLayoutGuide, attr: .bottom, + priority: 999) + Constraints.active(item: imageView, attr: .trailing, relatedBy: .equal, to: self.view, attr: .trailing) + Constraints.active(item: imageView, attr: .leading, relatedBy: .equal, to: self.view, attr: .leading) } - - private func addOverlay() { - view.insertSubview(overlayView, aboveSubview: imageView) + + fileprivate func addOverlay() { + self.view.insertSubview(overlayView, aboveSubview: imageView) overlayView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([overlayView.topAnchor.constraint(equalTo: view.topAnchor), - overlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - overlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]) + + Constraints.active(item: overlayView, attr: .top, relatedBy: .equal, to: imageView, attr: .top) + Constraints.active(item: overlayView, attr: .trailing, relatedBy: .equal, to: imageView, attr: .trailing) + Constraints.active(item: overlayView, attr: .bottom, relatedBy: .equal, to: imageView, attr: .bottom) + Constraints.active(item: overlayView, attr: .leading, relatedBy: .equal, to: imageView, attr: .leading) } - - private func configureLoadingIndicator() { - loadingIndicatorView.color = GiniColor(light: .GiniCapture.dark1, dark: .GiniCapture.light1).uiColor() - - addLoadingContainer() - addLoadingView(intoContainer: loadingIndicatorContainer) - - if let loadingIndicator = giniConfiguration.customLoadingIndicator { - addLoadingText(below: loadingIndicator.injectedView()) - loadingIndicator.startAnimation() - } else { - addLoadingText(below: loadingIndicatorView) - loadingIndicatorView.startAnimating() - } - } - - private func addLoadingText(below loadingIndicator: UIView) { - loadingIndicatorContainer.addSubview(loadingIndicatorText) + + fileprivate func addLoadingText(below: UIView) { + self.view.addSubview(loadingIndicatorText) loadingIndicatorText.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - loadingIndicatorText.topAnchor.constraint(equalTo: loadingIndicator.bottomAnchor, constant: 16), - loadingIndicatorText.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), - loadingIndicatorText.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), - loadingIndicatorText.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)]) + + Constraints.active(item: loadingIndicatorText, attr: .trailing, relatedBy: .equal, to: imageView, + attr: .trailing) + Constraints.active(item: loadingIndicatorText, attr: .top, relatedBy: .equal, to: below, attr: .bottom, + constant: 16) + Constraints.active(item: loadingIndicatorText, attr: .leading, relatedBy: .equal, to: imageView, attr: .leading) } - - private func addLoadingView(intoContainer container: UIView? = nil) { - let loadingIndicator: UIView - - if let customLoadingIndicator = giniConfiguration.customLoadingIndicator?.injectedView() { - loadingIndicator = customLoadingIndicator - } else { - loadingIndicator = loadingIndicatorView - } - + + fileprivate func addLoadingView(intoContainer container: UIView? = nil) { + loadingIndicatorView.translatesAutoresizingMaskIntoConstraints = false + if let container = container { container.translatesAutoresizingMaskIntoConstraints = false - loadingIndicator.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(container) - container.addSubview(loadingIndicator) - - NSLayoutConstraint.activate([ - container.centerXAnchor.constraint(equalTo: view.centerXAnchor), - container.centerYAnchor.constraint(equalTo: view.centerYAnchor), - container.heightAnchor.constraint(equalToConstant: - AnalysisViewController.loadingIndicatorContainerHeight), - container.widthAnchor.constraint(equalTo: container.heightAnchor), - loadingIndicator.centerXAnchor.constraint(equalTo: container.centerXAnchor), - loadingIndicator.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) + self.view.addSubview(container) + container.addSubview(loadingIndicatorView) + + Constraints.active(item: container, attr: .centerX, relatedBy: .equal, to: self.view, attr: .centerX) + Constraints.active(item: container, attr: .centerY, relatedBy: .equal, to: self.view, attr: .centerY) + Constraints.active(item: container, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: AnalysisViewController.loadingIndicatorContainerHeight) + Constraints.active(item: container, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: AnalysisViewController.loadingIndicatorContainerHeight) + Constraints.active(item: loadingIndicatorView, attr: .centerX, relatedBy: .equal, to: container, + attr: .centerX, constant: 1.5) + Constraints.active(item: loadingIndicatorView, attr: .centerY, relatedBy: .equal, to: container, + attr: .centerY, constant: 1.5) + } else { - view.addSubview(loadingIndicatorView) - - NSLayoutConstraint.activate([ - loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), - loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) + self.view.addSubview(loadingIndicatorView) + Constraints.active(item: loadingIndicatorView, attr: .centerX, relatedBy: .equal, to: self.view, + attr: .centerX) + Constraints.active(item: loadingIndicatorView, attr: .centerY, relatedBy: .equal, to: self.view, + attr: .centerY) } } - - private func addLoadingContainer() { - loadingIndicatorContainer.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(loadingIndicatorContainer) - NSLayoutConstraint.activate([ - loadingIndicatorContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), - loadingIndicatorContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor), - loadingIndicatorContainer.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor), - loadingIndicatorContainer.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, - constant: 16)]) + + fileprivate func addErrorView() { + view.addSubview(errorView) + + Constraints.pin(view: errorView, toSuperView: view, positions: [.left, .right, .top]) } - - private func showCaptureSuggestions(giniConfiguration: GiniConfiguration) { - let captureSuggestions = CaptureSuggestionsView(superView: view, - bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor) + + fileprivate func showPDFInformationView(withDocument document: GiniPDFDocument, + giniConfiguration: GiniConfiguration) { + let title: String = document.pdfTitle ?? .localized(resource: AnalysisStrings.defaultPdfDokumentTitle) + let pdfView = PDFInformationView(title: title, + subtitle: .localized(resource: AnalysisStrings.pdfPages, + args: document.numberPages), + textColor: giniConfiguration.analysisPDFInformationTextColor, + textFont: giniConfiguration.customFont.with(weight: .regular, + size: 16, + style: .body), + backgroundColor: giniConfiguration.analysisPDFInformationBackgroundColor, + superView: self.view, + viewBelow: self.imageView) + + pdfView.show() + } + + fileprivate func showCaptureSuggestions(giniConfiguration: GiniConfiguration) { + let captureSuggestions = CaptureSuggestionsView(superView: self.view, + bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor, + font: giniConfiguration.customFont.with(weight: .regular, + size: 16, + style: .body)) captureSuggestions.start() } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Analysis/CaptureSuggestionsView.swift b/Sources/GiniCaptureSDK/Core/Screens/Analysis/CaptureSuggestionsView.swift index 7b3725b..9e8242e 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Analysis/CaptureSuggestionsView.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Analysis/CaptureSuggestionsView.swift @@ -9,111 +9,134 @@ import UIKit final class CaptureSuggestionsView: UIView { - - private enum CaptureSuggestionsState { + + fileprivate enum CaptureSuggestionsState { case shown case hidden } - - private let suggestionContainer: CaptureSuggestionsViewContainer? - private let containerHeight: CGFloat = 96 - private var itemSeparationConstraint: NSLayoutConstraint = NSLayoutConstraint() - private var bottomConstraint: NSLayoutConstraint = NSLayoutConstraint() - private let repeatInterval: TimeInterval = 5 - private let superViewBottomAnchor: NSLayoutYAxisAnchor - - private var suggestionIconImages = [ - UIImageNamedPreferred(named: "captureSuggestion1"), - UIImageNamedPreferred(named: "captureSuggestion2"), - UIImageNamedPreferred(named: "captureSuggestion3"), - UIImageNamedPreferred(named: "captureSuggestion4") - ] - - private var suggestionTitle: [String] = [ - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.1", - comment: "First suggestion title for analysis screen"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.2", - comment: "Second suggestion title for analysis screen"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.3", - comment: "Third suggestion title for analysis screen"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.4", - comment: "Fourth suggestion title for analysis screen") - ] - - private var suggestionDescription: [String] = [ - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.1.details", - comment: "First suggestion description for analysis screen"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.2.details", - comment: "Second suggestion description for analysis screen"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.3.details", - comment: "Third suggestion description for analysis screen"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.4.details", - comment: "Fourth suggestion description for analysis screen") + + fileprivate let suggestionIcon: UIImageView + fileprivate let suggestionText: UILabel + fileprivate let suggestionContainer: UIView + fileprivate let suggestionTitle: UILabel + fileprivate let containerHeight: CGFloat = 135 + fileprivate let suggestionTitleHeight: CGFloat = 20 + fileprivate var itemSeparationConstraint: NSLayoutConstraint = NSLayoutConstraint() + fileprivate var bottomConstraint: NSLayoutConstraint = NSLayoutConstraint() + fileprivate let repeatInterval: TimeInterval = 5 + fileprivate let superViewBottomAnchor: NSLayoutYAxisAnchor + fileprivate let suggestionIconImage = UIImage(named: "analysisSuggestionsIcon", + in: giniCaptureBundle(), compatibleWith: nil) + fileprivate var suggestionTexts: [String] = [ + .localized(resource: AnalysisStrings.suggestion1Text), + .localized(resource: AnalysisStrings.suggestion2Text), + .localized(resource: AnalysisStrings.suggestion3Text), + .localized(resource: AnalysisStrings.suggestion4Text) ] - - init(superView: UIView, bottomAnchor: NSLayoutYAxisAnchor) { - if GiniConfiguration.shared.multipageEnabled { - suggestionIconImages.append(UIImageNamedPreferred(named: "captureSuggestion5")) - suggestionTitle.append(NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.5", - comment: "Fifth suggestion for analysis screen")) - suggestionDescription.append( - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.5.details", - comment: "Fifth suggestion description for analysis screen")) - } - - suggestionContainer = CaptureSuggestionsViewContainer().loadNib() as? CaptureSuggestionsViewContainer - + + init(superView: UIView, bottomAnchor: NSLayoutYAxisAnchor, font: UIFont) { + suggestionContainer = UIView() + suggestionTitle = UILabel() + suggestionText = UILabel() superViewBottomAnchor = bottomAnchor - let randomIndex = Int.random(in: 0...suggestionTitle.count - 1) - suggestionContainer?.configureContent(with: suggestionIconImages[randomIndex], - title: suggestionTitle[randomIndex], - description: suggestionDescription[randomIndex]) + suggestionIcon = UIImageView(image: suggestionIconImage) + suggestionIcon.contentMode = .scaleAspectFit + + suggestionTitle.textColor = .white + suggestionTitle.font = font + suggestionTitle.numberOfLines = 1 + suggestionTitle.text = .localized(resource: AnalysisStrings.suggestionHeader) + suggestionTitle.textAlignment = .center + suggestionTitle.adjustsFontSizeToFitWidth = true + suggestionTitle.minimumScaleFactor = 14/16 + + suggestionText.textColor = .white + suggestionText.font = font + suggestionText.numberOfLines = 0 + suggestionTexts.shuffle() + suggestionText.text = suggestionTexts.first! + super.init(frame: .zero) alpha = 0 - guard let suggestionContainer = suggestionContainer else { return } + + suggestionContainer.addSubview(suggestionIcon) + suggestionContainer.addSubview(suggestionText) self.addSubview(suggestionContainer) + self.addSubview(suggestionTitle) superView.addSubview(self) - + translatesAutoresizingMaskIntoConstraints = false suggestionContainer.translatesAutoresizingMaskIntoConstraints = false - + suggestionTitle.translatesAutoresizingMaskIntoConstraints = false + suggestionIcon.translatesAutoresizingMaskIntoConstraints = false + suggestionText.translatesAutoresizingMaskIntoConstraints = false + addConstraints() layoutIfNeeded() } - + required init?(coder aDecoder: NSCoder) { fatalError("You should use init() initializer") } - - private func addConstraints() { - guard let superview = superview, let suggestionContainer = suggestionContainer else { return } - + + fileprivate func addConstraints() { + guard let superview = superview else { return } + // self bottomConstraint = self.bottomAnchor.constraint(equalTo: superViewBottomAnchor, constant: containerHeight) Constraints.active(item: self, attr: .leading, relatedBy: .equal, to: superview, attr: .leading) Constraints.active(item: self, attr: .trailing, relatedBy: .equal, to: superview, attr: .trailing) - Constraints.active(item: self, attr: .height, relatedBy: .greaterThanOrEqual, - to: nil, attr: .notAnAttribute, constant: containerHeight, priority: 250) + Constraints.active(item: self, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: containerHeight) Constraints.active(constraint: bottomConstraint) + // suggestionTitle + Constraints.active(item: suggestionTitle, attr: .top, relatedBy: .equal, to: self, attr: .top) + Constraints.active(item: suggestionTitle, attr: .leading, relatedBy: .equal, to: self, attr: .leading, + constant: 8) + Constraints.active(item: suggestionTitle, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, + constant: -8, priority: 999) + Constraints.active(item: suggestionTitle, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: suggestionTitleHeight) + // suggestionContainer - itemSeparationConstraint = NSLayoutConstraint(item: suggestionContainer, attribute: .bottom, relatedBy: .equal, - toItem: self, attribute: .bottom, multiplier: 1, constant: 0) - Constraints.active(item: suggestionContainer, attr: .height, relatedBy: .greaterThanOrEqual, - to: nil, attr: .notAnAttribute, constant: containerHeight) + itemSeparationConstraint = NSLayoutConstraint(item: suggestionContainer, attribute: .top, relatedBy: .equal, + toItem: suggestionTitle, attribute: .bottom, multiplier: 1, + constant: 0) + Constraints.active(item: suggestionContainer, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: containerHeight - suggestionTitleHeight) Constraints.active(constraint: itemSeparationConstraint) - + + // suggestionIcon + Constraints.active(item: suggestionIcon, attr: .leading, relatedBy: .equal, to: suggestionContainer, + attr: .leading) + Constraints.active(item: suggestionIcon, attr: .height, relatedBy: .lessThanOrEqual, to: nil, + attr: .notAnAttribute, constant: 48) + Constraints.active(item: suggestionIcon, attr: .width, relatedBy: .equal, to: suggestionIcon, attr: .height) + Constraints.active(item: suggestionIcon, attr: .centerY, relatedBy: .equal, to: suggestionContainer, + attr: .centerY) + Constraints.active(item: suggestionIcon, attr: .trailing, relatedBy: .equal, to: suggestionText, attr: .leading, + constant: -16) + + // suggestionText + Constraints.active(item: suggestionText, attr: .top, relatedBy: .equal, to: suggestionContainer, attr: .top, + constant: 16, priority: 999) + Constraints.active(item: suggestionText, attr: .trailing, relatedBy: .equal, to: suggestionContainer, + attr: .trailing, priority: 999) + Constraints.active(item: suggestionText, attr: .bottom, relatedBy: .equal, to: suggestionContainer, + attr: .bottom, constant: -16, priority: 999) + // Center on align to margins depending on device if UIDevice.current.isIpad { - Constraints.active(item: suggestionContainer, attr: .width, relatedBy: .equal, to: self, - attr: .width, multiplier: 0.7) - Constraints.active(item: suggestionContainer, attr: .centerX, relatedBy: .equal, to: self, attr: .centerX) + Constraints.active(item: suggestionContainer, attr: .width, relatedBy: .lessThanOrEqual, to: self, + attr: .width, multiplier: 0.9) + Constraints.active(item: suggestionText, attr: .centerX, relatedBy: .equal, to: self, attr: .centerX) } else { Constraints.active(item: suggestionContainer, attr: .leading, relatedBy: .equal, to: self, attr: .leading, constant: 20) Constraints.active(item: suggestionContainer, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, - constant: -20) + constant: -20, priority: 999) } } } @@ -121,11 +144,11 @@ final class CaptureSuggestionsView: UIView { // MARK: Animations extension CaptureSuggestionsView { - + func start(after seconds: TimeInterval = 4) { DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: { [weak self] in guard let self = self, let superview = self.superview else { return } - self.bottomConstraint.constant = UIDevice.current.isIpad ? -28 : -24 + self.bottomConstraint.constant = 0 self.alpha = 1 UIView.animate(withDuration: 0.5, animations: { superview.layoutIfNeeded() @@ -134,21 +157,21 @@ extension CaptureSuggestionsView { }) }) } - - private func changeView(toState state: CaptureSuggestionsState) { + + fileprivate func changeView(toState state: CaptureSuggestionsState) { let delay: TimeInterval let nextState: CaptureSuggestionsState - + if state == .shown { delay = 0 nextState = .hidden changeSuggestionText() - suggestionContainer?.layoutIfNeeded() + suggestionContainer.layoutIfNeeded() } else { delay = repeatInterval nextState = .shown } - + updatePosition(withState: state) UIView.animate(withDuration: 0.5, delay: delay, options: [UIView.AnimationOptions.curveEaseInOut], animations: { @@ -158,28 +181,24 @@ extension CaptureSuggestionsView { self.changeView(toState: nextState) }) } - - private func changeSuggestionText() { - if let currentTitle = suggestionContainer?.titleLabel.text, - let currentIndex = suggestionTitle.firstIndex(of: currentTitle) { + + fileprivate func changeSuggestionText() { + if let currentText = suggestionText.text, let currentIndex = suggestionTexts.firstIndex(of: currentText) { let nextIndex: Int - if suggestionTitle.index(after: currentIndex) < suggestionTitle.endIndex { - nextIndex = suggestionTitle.index(after: currentIndex) + if suggestionTexts.index(after: currentIndex) < suggestionTexts.endIndex { + nextIndex = suggestionTexts.index(after: currentIndex) } else { nextIndex = 0 } - - suggestionContainer?.configureContent(with: suggestionIconImages[nextIndex], - title: suggestionTitle[nextIndex], - description: suggestionDescription[nextIndex]) + suggestionText.text = suggestionTexts[nextIndex] } } - - private func updatePosition(withState state: CaptureSuggestionsState) { + + fileprivate func updatePosition(withState state: CaptureSuggestionsState) { if state == .shown { self.itemSeparationConstraint.constant = 0 } else { - self.itemSeparationConstraint.constant = 2 * containerHeight + self.itemSeparationConstraint.constant = containerHeight } } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Analysis/CaptureSuggestionsViewContainer.swift b/Sources/GiniCaptureSDK/Core/Screens/Analysis/CaptureSuggestionsViewContainer.swift deleted file mode 100644 index 463e267..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Analysis/CaptureSuggestionsViewContainer.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// CaptureSuggestionsViewContainer.swift -// -// -// Created by David Vizaknai on 23.08.2022. -// - -import UIKit - -final class CaptureSuggestionsViewContainer: UIView { - @IBOutlet var imageView: UIImageView! - @IBOutlet var titleLabel: UILabel! - @IBOutlet var descriptionLabel: UILabel! - - init() { - super.init(frame: CGRect.zero) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - override func awakeFromNib() { - super.awakeFromNib() - configureView() - configureAccessibility() - } - - private func configureView() { - let configuration = GiniConfiguration.shared - - backgroundColor = GiniColor(light: UIColor.GiniCapture.light1, dark: UIColor.GiniCapture.dark3).uiColor() - layer.cornerRadius = 16 - - titleLabel.font = configuration.textStyleFonts[.calloutBold] - titleLabel.textColor = GiniColor(light: UIColor.GiniCapture.dark1, dark: UIColor.GiniCapture.light1).uiColor() - - descriptionLabel.font = configuration.textStyleFonts[.subheadline] - descriptionLabel.textColor = UIColor.GiniCapture.dark7 - } - - private func configureAccessibility() { - isAccessibilityElement = false - imageView.accessibilityTraits = .image - imageView.isAccessibilityElement = true - titleLabel.adjustsFontForContentSizeCategory = true - titleLabel.isAccessibilityElement = true - descriptionLabel.adjustsFontForContentSizeCategory = true - descriptionLabel.isAccessibilityElement = true - accessibilityElements = [imageView as Any, titleLabel as Any, descriptionLabel as Any] - } - - func configureContent(with image: UIImage?, title: String, description: String) { - imageView.image = image - imageView.accessibilityLabel = description - titleLabel.text = title - descriptionLabel.text = description - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Analysis/CustomLoadingIndicatorAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Analysis/CustomLoadingIndicatorAdapter.swift deleted file mode 100644 index 5c4e04e..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Analysis/CustomLoadingIndicatorAdapter.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// CustomLoadingIndicatorAdapter.swift -// -// -// Created by David Vizaknai on 12.09.2022. -// - -import Foundation -/** -* Adapter for injecting a custom loading indicator for the analysis viewcontroller. -*/ -public protocol CustomLoadingIndicatorAdapter: InjectedViewAdapter { - /** - * Called when the screen is loaded. You should start the loading indicator animation in this method. - */ - func startAnimation() - /** - * Called when the screen has disappeared. You should stop the loading indicator animation in this method. - */ - func stopAnimation() -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Analysis/PDFInformationView.swift b/Sources/GiniCaptureSDK/Core/Screens/Analysis/PDFInformationView.swift new file mode 100644 index 0000000..9096ddd --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Analysis/PDFInformationView.swift @@ -0,0 +1,130 @@ +// +// PDFInformationView.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 9/25/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import Foundation +import UIKit + +final class PDFInformationView: UIView { + + let titleLabel = UILabel() + let subtitleLabel = UILabel() + var shadowLayer: CALayer? + var viewBelow: UIView? + + init(title: String, + subtitle: String, + textColor: UIColor, + textFont: UIFont, + backgroundColor: UIColor, + superView: UIView?, + viewBelow: UIView? = nil) { + super.init(frame: .zero) + guard let superView = superView else { return } + + self.viewBelow = viewBelow + self.backgroundColor = backgroundColor + self.alpha = 0 + + titleLabel.text = title + titleLabel.textColor = textColor + titleLabel.textAlignment = .center + titleLabel.font = textFont.withSize(20.0) + titleLabel.minimumScaleFactor = 18.0 / 20.0 + titleLabel.adjustsFontSizeToFitWidth = true + + subtitleLabel.text = subtitle + subtitleLabel.textColor = textColor + subtitleLabel.textAlignment = .center + subtitleLabel.font = textFont.withSize(16.0) + + addSubview(titleLabel) + addSubview(subtitleLabel) + + addInnerShadow() + superView.addSubview(self) + + addConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("You should use init(title:subtitle:textColor:background) initializer") + } + + override func layoutSubviews() { + super.layoutSubviews() + arrangeShadow() + } + + fileprivate func addConstraints() { + guard let superview = superview else { return } + self.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + + Constraints.active(item: self, attr: .top, relatedBy: .equal, to: superview, attr: .top) + Constraints.active(item: self, attr: .leading, relatedBy: .equal, to: superview, attr: .leading) + Constraints.active(item: self, attr: .trailing, relatedBy: .equal, to: superview, attr: .trailing) + Constraints.active(item: self, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, constant: 95) + + if let viewBelow = viewBelow { + Constraints.active(item: self, attr: .bottom, relatedBy: .equal, to: viewBelow, attr: .top) + } + + Constraints.active(item: titleLabel, attr: .top, relatedBy: .equal, to: self, attr: .top, constant: 16) + Constraints.active(item: titleLabel, attr: .bottom, relatedBy: .equal, to: subtitleLabel, attr: .top, + constant: -16) + Constraints.active(item: titleLabel, attr: .leading, relatedBy: .equal, to: self, attr: .leading, constant: 16) + Constraints.active(item: titleLabel, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, + constant: -16, priority: 999) + + Constraints.active(item: subtitleLabel, attr: .leading, relatedBy: .equal, to: self, attr: .leading, + constant: 16) + Constraints.active(item: subtitleLabel, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, + constant: -16, priority: 999) + Constraints.active(item: subtitleLabel, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, + constant: -16, priority: 999) + + } + + fileprivate func addInnerShadow() { + if shadowLayer == nil { + let size = self.frame.size + self.clipsToBounds = true + let layer: CALayer = CALayer() + layer.backgroundColor = UIColor.lightGray.cgColor + layer.position = CGPoint(x: size.width / 2, y: -size.height / 2 + 0.5) + layer.bounds = CGRect(x: 0, y: 0, width: size.width, height: size.height) + layer.shadowColor = UIColor.darkGray.cgColor + layer.shadowOffset = CGSize(width: 0.5, height: 0.5) + layer.shadowOpacity = 0.8 + layer.shadowRadius = 5.0 + layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath + shadowLayer = layer + + self.layer.addSublayer(layer) + } + } + + fileprivate func arrangeShadow() { + let size = self.frame.size + shadowLayer?.position = CGPoint(x: size.width / 2, y: -size.height / 2 + 0.5) + shadowLayer?.bounds = CGRect(x: 0, y: 0, width: size.width, height: size.height) + } +} + +// MARK: Show and hide + +extension PDFInformationView { + func show(after seconds: Double = 0) { + DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: { + UIView.animate(withDuration: 0.5) { + self.alpha = 1.0 + } + }) + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift index 5606ab2..1bde9a9 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift @@ -201,11 +201,15 @@ final class Camera: NSObject, CameraProtocol { fileprivate extension Camera { var captureSettings: AVCapturePhotoSettings { - var captureSettings = AVCapturePhotoSettings(rawPixelFormatType: 0, - rawFileType: nil, - processedFormat: nil, - processedFileType: AVFileType.jpg) - + var captureSettings: AVCapturePhotoSettings + if #available(iOS 11.0, *) { + captureSettings = AVCapturePhotoSettings(rawPixelFormatType: 0, + rawFileType: nil, + processedFormat: nil, + processedFileType: AVFileType.jpg) + } else { + captureSettings = AVCapturePhotoSettings() + } guard let device = self.videoDeviceInput?.device else { return captureSettings } #if !targetEnvironment(simulator) diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController+Actions.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController+Actions.swift deleted file mode 100644 index c2ffb27..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController+Actions.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Camera2ViewController+Actions.swift -// -// -// Created by Krzysztof Kryniecki on 14/09/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -// MARK: - Toggle UI elements - -extension Camera2ViewController { - - /** - Show the capture button. Should be called when onboarding is dismissed. - */ - public func showCaptureButton() { - cameraPane.captureButton.alpha = 1 - } - - /** - Hide the capture button. Should be called when onboarding is presented. - */ - public func hideCaptureButton() { - cameraPane.captureButton.alpha = 0 - } - - public func setupCamera() { - cameraPreviewViewController.setupCamera() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController+Popups.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController+Popups.swift deleted file mode 100644 index 102756b..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController+Popups.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Camera2ViewController+Extension.swift -// -// -// Created by Krzysztof Kryniecki on 14/09/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -// MARK: - Document import - -extension Camera2ViewController { - - @objc func showImportFileSheet() { - let alertViewController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let alertViewControllerMessage: String = NSLocalizedStringPreferredFormat( - "ginicapture.camera.popupTitleImportPDForPhotos", - comment: "Info label") - if giniConfiguration.fileImportSupportedTypes == .pdf_and_images { - alertViewController.addAction(UIAlertAction(title: NSLocalizedStringPreferredFormat( - "ginicapture.camera.popupOptionPhotos", - comment: "Photos action"), - style: .default) { [unowned self] _ in - self.delegate?.camera(self, didSelect: .gallery) - }) - } - - alertViewController.view.tintColor = .GiniCapture.accent1 - - alertViewController.addAction(UIAlertAction(title: NSLocalizedStringPreferredFormat( - "ginicapture.camera.popupOptionFiles", - comment: "files action"), - style: .default) { [unowned self] _ in - self.delegate?.camera(self, didSelect: .explorer) - }) - alertViewController.addAction(UIAlertAction(title: NSLocalizedStringPreferredFormat( - "ginicapture.camera.popupCancel", - comment: "cancel action"), - style: .cancel, handler: nil)) - if alertViewControllerMessage.count > 0 { - alertViewController.message = alertViewControllerMessage - } else { - alertViewController.message = nil - } - alertViewController.popoverPresentationController?.sourceView = cameraPane.fileUploadButton - self.present(alertViewController, animated: true, completion: nil) - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController.swift deleted file mode 100644 index 42d1fdb..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/Camera2ViewController.swift +++ /dev/null @@ -1,580 +0,0 @@ -// -// Camera2ViewController.swift -// -// -// Created by Krzysztof Kryniecki on 06/09/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -// swiftlint:disable type_body_length -public final class Camera2ViewController: UIViewController, CameraScreen { - - /** - The object that acts as the delegate of the camera view controller. - */ - let giniConfiguration: GiniConfiguration - var detectedQRCodeDocument: GiniQRCodeDocument? - private var shouldShowQRCodeNext = false - lazy var cameraPreviewViewController: CameraPreviewViewController = { - let cameraPreviewViewController = CameraPreviewViewController() - cameraPreviewViewController.delegate = self - return cameraPreviewViewController - }() - - private lazy var qrCodeOverLay: QRCodeOverlay = { - let view = QRCodeOverlay() - view.isHidden = true - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private var resetTask: DispatchWorkItem? - private var hideTask: DispatchWorkItem? - private var validQRCodeProcessing: Bool = false - public weak var delegate: CameraViewControllerDelegate? - - private lazy var qrCodeScanningOnlyEnabled: Bool = { - return giniConfiguration.qrCodeScanningEnabled && giniConfiguration.onlyQRCodeScanningEnabled - }() - - @IBOutlet weak var cameraPane: CameraPane! - private let cameraButtonsViewModel: CameraButtonsViewModel - private var navigationBarBottomAdapter: CameraBottomNavigationBarAdapter? - private var bottomNavigationBar: UIView? - - @IBOutlet weak var iPadBottomPaneConstraint: NSLayoutConstraint! - @IBOutlet weak var bottomButtonsConstraints: NSLayoutConstraint! - @IBOutlet weak var bottomPaneConstraint: NSLayoutConstraint! - /** - Designated initializer for the `CameraViewController` which allows - to set the `GiniConfiguration for the camera screen`. - All the interactions with this screen are handled by `CameraViewControllerDelegate`. - - - parameter giniConfiguration: `GiniConfiguration` instance. - - - returns: A view controller instance allowing the user to take a picture or pick a document. - */ - public init( - giniConfiguration: GiniConfiguration, - viewModel: CameraButtonsViewModel - ) { - self.giniConfiguration = giniConfiguration - self.cameraButtonsViewModel = viewModel - if UIDevice.current.isIphone { - super.init(nibName: "CameraPhone", bundle: giniCaptureBundle()) - } else { - super.init(nibName: "CameraiPad", bundle: giniCaptureBundle()) - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - public override var preferredStatusBarStyle: UIStatusBarStyle { - return giniConfiguration.statusBarStyle - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - cameraPane.toggleCaptureButtonActivation(state: true) - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - validQRCodeProcessing = false - delegate?.cameraDidAppear(self) - } - - fileprivate func configureTitle() { - if UIDevice.current.isIphone { - self.title = NSLocalizedStringPreferredFormat( - "ginicapture.navigationbar.camera.title", - comment: "Info label") - } else { - self.title = NSLocalizedStringPreferredFormat( - "ginicapture.camera.infoLabel", - comment: "Info label") - } - } - - private func setupView() { - edgesForExtendedLayout = [] - view.backgroundColor = UIColor.GiniCapture.dark1 - cameraPreviewViewController.previewView.alpha = 0 - addChild(cameraPreviewViewController) - view.addSubview(cameraPreviewViewController.view) - cameraPreviewViewController.didMove(toParent: self) - view.sendSubviewToBack(cameraPreviewViewController.view) - view.addSubview(qrCodeOverLay) - configureConstraints() - configureTitle() - - if qrCodeScanningOnlyEnabled { - cameraPane.alpha = 0 - if giniConfiguration.bottomNavigationBarEnabled { - configureCustomTopNavigationBar(containsImage: false) - } else { - navigationItem.rightBarButtonItem = nil - } - } else { - configureCameraPaneButtons() - configureBottomNavigationBar() - } - } - - private func configureCustomTopNavigationBar(containsImage: Bool) { - navigationItem.leftBarButtonItem = nil - navigationItem.hidesBackButton = true - if !containsImage { - let cancelBarButton = GiniBarButton(ofType: .cancel) - cancelBarButton.addAction(cameraButtonsViewModel, #selector(cameraButtonsViewModel.cancelPressed)) - navigationItem.rightBarButtonItem = cancelBarButton.barButton - } else { - navigationItem.rightBarButtonItem = nil - } - } - - private func updateCustomNavigationBars(containsImage: Bool) { - guard let bottomNavigationBar = bottomNavigationBar else { - return - } - configureCustomTopNavigationBar(containsImage: containsImage) - if containsImage { - navigationBarBottomAdapter?.showButtons( - navigationBar: bottomNavigationBar, - navigationButtons: [.help, .back]) - } else { - navigationBarBottomAdapter?.showButtons( - navigationBar: bottomNavigationBar, - navigationButtons: [.help]) - } - } - - private func configureBottomNavigationBar() { - if giniConfiguration.bottomNavigationBarEnabled { - if let bottomBarAdapter = giniConfiguration.cameraNavigationBarBottomAdapter { - navigationBarBottomAdapter = bottomBarAdapter - } else { - navigationBarBottomAdapter = DefaultCameraBottomNavigationBarAdapter() - } - navigationBarBottomAdapter?.setHelpButtonClickedActionCallback { [weak self] in - self?.cameraButtonsViewModel.helpAction?() - } - navigationBarBottomAdapter?.setBackButtonClickedActionCallback { [weak self] in - self?.cameraButtonsViewModel.backButtonAction?() - } - - if let bar = - navigationBarBottomAdapter?.injectedView() { - bottomNavigationBar = bar - view.addSubview(bar) - layoutBottomNavigationBar(bar) - } - updateCustomNavigationBars( - containsImage: cameraButtonsViewModel.images.count > 0) - } - } - - private func layoutBottomNavigationBar(_ navigationBar: UIView) { - if UIDevice.current.isIpad { - view.removeConstraints([cameraPreviewBottomContraint, iPadBottomPaneConstraint]) - navigationBar.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(navigationBar) - NSLayoutConstraint.activate([ - navigationBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), - navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), - navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), - navigationBar.heightAnchor.constraint(equalToConstant: navigationBar.frame.height), - cameraPane.bottomAnchor.constraint(equalTo: navigationBar.topAnchor), - cameraPreviewViewController.view.bottomAnchor.constraint(equalTo: navigationBar.topAnchor) - ]) - } else { - view.removeConstraints([bottomPaneConstraint, bottomButtonsConstraints]) - navigationBar.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(navigationBar) - NSLayoutConstraint.activate([ - navigationBar.topAnchor.constraint(equalTo: cameraPane.bottomAnchor), - navigationBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), - navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), - navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), - navigationBar.heightAnchor.constraint(equalToConstant: navigationBar.frame.height), - cameraPane.leftButtonsStack.bottomAnchor.constraint(equalTo: cameraPane.bottomAnchor) - ]) - } - view.bringSubviewToFront(navigationBar) - view.layoutSubviews() - } - - private func configureCameraPaneButtons() { - cameraPane.setupAuthorization(isHidden: false) - configureLeftButtons() - cameraButtonsViewModel.captureAction = { [weak self] in - self?.cameraPane.toggleCaptureButtonActivation(state: false) - self?.cameraPreviewViewController.captureImage { [weak self] data, error in - guard let self = self else { return } - - var processedImageData = data - if let imageData = data, let image = UIImage(data: imageData)?.fixOrientation() { - let croppedImage = self.crop(image: image) - processedImageData = croppedImage.jpegData(compressionQuality: 1) - - #if targetEnvironment(simulator) - processedImageData = imageData - #endif - } - - if let image = self.cameraButtonsViewModel.didCapture(imageData: data, - processedImageData: processedImageData, - error: error, - orientation: - UIApplication.shared.statusBarOrientation, - giniConfiguration: self.giniConfiguration) { - - UIImpactFeedbackGenerator(style: .medium).impactOccurred() - self.didPick(image) - } - self.cameraPane.toggleCaptureButtonActivation(state: true) - } - } - - cameraPane.captureButton.addTarget( - cameraButtonsViewModel, - action: #selector(cameraButtonsViewModel.capturePressed), - for: .touchUpInside) - cameraButtonsViewModel.imageStackAction = { [weak self] in - if let strongSelf = self { - self?.delegate?.cameraDidTapReviewButton(strongSelf) - } - } - cameraButtonsViewModel.imagesUpdated = { [weak self] images in - if let lastImage = images.last { - self?.cameraPane.thumbnailView.updateStackStatus(to: .filled(count: images.count, lastImage: lastImage)) - } else { - self?.cameraPane.thumbnailView.updateStackStatus(to: ThumbnailView.State.empty) - } - if self?.giniConfiguration.bottomNavigationBarEnabled == true { - self?.updateCustomNavigationBars(containsImage: images.last != nil) - } - } - cameraButtonsViewModel.imagesUpdated?(cameraButtonsViewModel.images) - cameraPane.thumbnailView.thumbnailButton.addTarget( - cameraButtonsViewModel, - action: #selector(cameraButtonsViewModel.thumbnailPressed), - for: .touchUpInside) - } - // swiftlint:enable body_length - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - qrCodeOverLay.viewWillDisappear() - } - - private func configureUploadButton() { - if giniConfiguration.fileImportSupportedTypes != .none { - cameraPane.fileUploadButton.isHidden = false - cameraButtonsViewModel.importAction = { [weak self] in - self?.showImportFileSheet() - } - cameraPane.fileUploadButton.actionButton.addTarget( - cameraButtonsViewModel, - action: #selector(cameraButtonsViewModel.importPressed), - for: .touchUpInside) - } else { - cameraPane.fileUploadButton.isHidden = true - } - } - - private func configureFlashButton() { - cameraPane.toggleFlashButtonActivation( - state: cameraPreviewViewController.isFlashSupported) - cameraButtonsViewModel.isFlashOn = cameraPreviewViewController.isFlashOn - cameraPane.setupFlashButton(state: cameraButtonsViewModel.isFlashOn) - cameraButtonsViewModel.flashAction = { [weak self] isFlashOn in - self?.cameraPreviewViewController.isFlashOn = isFlashOn - self?.cameraPane.setupFlashButton(state: isFlashOn) - } - cameraPane.flashButton.actionButton.addTarget( - cameraButtonsViewModel, - action: #selector(cameraButtonsViewModel.toggleFlash), - for: .touchUpInside) - } - - private func configureLeftButtons() { - configureUploadButton() - configureFlashButton() - } - - private lazy var cameraPreviewBottomContraint: NSLayoutConstraint = - cameraPreviewViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) - - private func configureConstraints() { - if qrCodeScanningOnlyEnabled { - qrCodeOverLay.layoutViews(centeringBy: cameraPreviewViewController.qrCodeFrameView) - } else { - qrCodeOverLay.layoutViews(centeringBy: cameraPreviewViewController.cameraFrameView) - } - - NSLayoutConstraint.activate([ - qrCodeOverLay.centerYAnchor.constraint(equalTo: view.centerYAnchor), - qrCodeOverLay.leadingAnchor.constraint(equalTo: view.leadingAnchor), - qrCodeOverLay.topAnchor.constraint(equalTo: view.topAnchor), - qrCodeOverLay.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - cameraPreviewViewController.view.topAnchor.constraint(equalTo: view.topAnchor), - cameraPreviewViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - cameraPreviewViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - cameraPreviewBottomContraint - ] - ) - } - - fileprivate func didPick(_ document: GiniCaptureDocument) { - if let delegate = delegate { - delegate.camera(self, didCapture: document) - } else { - assertionFailure("The CameraViewControllerDelegate has not been assigned") - } - } - - override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: nil) - } - - /** - Replaces the captured images stack content with new images. - - - parameter images: New images to be shown in the stack. (Last image will be shown on top) - */ - public func replaceCapturedStackImages(with images: [UIImage]) { - if giniConfiguration.multipageEnabled { - cameraButtonsViewModel.images = images - } - } - - public func addValidationLoadingView() -> UIView { - let loadingIndicator = UIActivityIndicatorView(style: .whiteLarge) - let blurredView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - blurredView.alpha = 0 - blurredView.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin] - loadingIndicator.startAnimating() - blurredView.contentView.addSubview(loadingIndicator) - self.view.addSubview(blurredView) - blurredView.frame = self.view.bounds - loadingIndicator.center = blurredView.center - UIView.animate(withDuration: AnimationDuration.medium, animations: { - blurredView.alpha = 1 - }) - return blurredView - } - - private func showQRCodeFeedback(for document: GiniQRCodeDocument, isValid: Bool) { - guard !validQRCodeProcessing else { return } - guard detectedQRCodeDocument != document else { return } - - hideTask?.cancel() - resetTask?.cancel() - detectedQRCodeDocument = document - - hideTask = DispatchWorkItem(block: { - self.resetQRCodeScanning(isValid: isValid) - - if let QRDocument = self.detectedQRCodeDocument { - if isValid { - self.didPick(QRDocument) - } - } - }) - - if isValid { - showValidQRCodeFeedback() - } else { - showInvalidQRCodeFeedback() - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: hideTask!) - } - - private func showValidQRCodeFeedback() { - // Haptic feedback - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.success) - - validQRCodeProcessing = true - cameraPane.isUserInteractionEnabled = false - UIView.animate(withDuration: 0.3) { - self.qrCodeOverLay.isHidden = false - self.cameraPreviewViewController.changeFrameColor(to: .GiniCapture.success2) - } - - qrCodeOverLay.configureQrCodeOverlay(withCorrectQrCode: true) - } - - private func showInvalidQRCodeFeedback() { - // Haptic feedback - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.warning) - - qrCodeOverLay.isUserInteractionEnabled = false - UIView.animate(withDuration: 0.3) { - self.qrCodeOverLay.isHidden = false - self.cameraPreviewViewController.changeFrameColor(to: .GiniCapture.warning3) - } - - qrCodeOverLay.configureQrCodeOverlay(withCorrectQrCode: false) - } - - private func resetQRCodeScanning(isValid: Bool) { - resetTask = DispatchWorkItem(block: { - self.detectedQRCodeDocument = nil - }) - - if isValid { - cameraPreviewViewController.cameraFrameView.isHidden = true - qrCodeOverLay.showAnimation() - } else { - UIView.animate(withDuration: 0.3) { - self.cameraPreviewViewController.changeFrameColor(to: .GiniCapture.light1) - self.qrCodeOverLay.isHidden = true - self.cameraPane.isUserInteractionEnabled = true - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: resetTask!) - } - } -} - -// MARK: - Image Cropping - -extension Camera2ViewController { - // swiftlint:disable line_length - private func crop(image: UIImage) -> UIImage { - let standardImageAspectRatio: CGFloat = 0.75 // Standard aspect ratio of a 3/4 image - let screenAspectRatio = self.cameraPreviewViewController.view.frame.height / self.cameraPreviewViewController.view.frame.width - var scale: CGFloat - - if image.size.width > image.size.height { - // Landscape orientation - - // Calculate the scale based on the part of the image which is fully shown on the screen - if screenAspectRatio > standardImageAspectRatio { - // In this case the preview shows the full height of the camera preview - scale = image.size.height / self.cameraPreviewViewController.view.frame.height - } else { - // In this case the preview shows the full width of the camera preview - scale = image.size.width / self.cameraPreviewViewController.view.frame.width - } - } else { - // Portrait image - - // Calculate the scale based on the part of the image which is fully shown on the screen - if UIDevice.current.isIpad { - if screenAspectRatio < standardImageAspectRatio { - // In this case the preview shows the full height of the camera preview - scale = image.size.height / self.cameraPreviewViewController.view.frame.height - } else { - // In this case the preview shows the full width of the camera preview - scale = image.size.width / self.cameraPreviewViewController.view.frame.width - } - } else { - scale = image.size.height / self.cameraPreviewViewController.view.frame.height - } - } - - // Calculate the rectangle for the displayed image on the full size captured image - let widthDisplacement = (image.size.width - (self.cameraPreviewViewController.view.frame.width) * scale) / 2 - let heightDisplacement = (image.size.height - (self.cameraPreviewViewController.view.frame.height) * scale) / 2 - - // The frame of the A4 rect - let a4FrameRect = self.cameraPreviewViewController.cameraFrameView.frame.scaled(for: scale) - - // The origin of the cropping rect compared to the whole image - let cropRectX = widthDisplacement + a4FrameRect.origin.x - let cropRectY = heightDisplacement + a4FrameRect.origin.y - - // The A4 rect position and size on the whole image - let cropRect = CGRect(x: cropRectX, y: cropRectY, width: a4FrameRect.width, height: a4FrameRect.height) - - // Scaling up the rectangle 15% on each side - let scaledSize = CGSize(width: cropRect.width * 1.30, height: cropRect.height * 1.30) - - let scaledOriginX = cropRectX - cropRect.width * 0.15 - let scaledOriginY = cropRectY - cropRect.height * 0.15 - - var scaledRect = CGRect(x: scaledOriginX, y: scaledOriginY, width: scaledSize.width, height: scaledSize.height) - - if scaledRect.origin.x >= 0 && scaledRect.origin.y >= 0 { - // The area to be cropped is inside of the area of the image - return cut(image: image, to: scaledRect) - } else { - // The area to be cropped is outside of the area of the image - - // If the area is bigger than the image, reset the origin and subtract the extra width/height that is not present - if scaledOriginX < 0 { - scaledRect.size.width += scaledRect.origin.x - scaledRect.origin.x = 0 - } - - if scaledOriginY < 0 { - scaledRect.size.height += scaledRect.origin.y - scaledRect.origin.y = 0 - } - - return cut(image: image, to: scaledRect) - } - } - // swiftlint:enable line_length - - private func cut(image: UIImage, to rect: CGRect) -> UIImage { - guard let cgImage = image.cgImage else { return image } - guard let croppedImage = cgImage.cropping(to: rect) else { return image } - let finalImage = UIImage(cgImage: croppedImage, scale: 1, orientation: .up) - - return finalImage - } -} - -// MARK: - CameraPreviewViewControllerDelegate - -extension Camera2ViewController: CameraPreviewViewControllerDelegate { - - func cameraDidSetUp(_ viewController: CameraPreviewViewController, - camera: CameraProtocol) { - if !qrCodeScanningOnlyEnabled { - cameraPreviewViewController.cameraFrameView.isHidden = false - cameraPane.toggleCaptureButtonActivation(state: true) - } - - cameraPreviewViewController.updatePreviewViewOrientation() - UIView.animate(withDuration: 1.0) { - self.cameraPane.setupAuthorization(isHidden: false) - self.cameraPreviewViewController.previewView.alpha = 1 - } - } - - func cameraPreview( - _ viewController: CameraPreviewViewController, - didDetectInvalid qrCodeDocument: GiniQRCodeDocument) { - showQRCodeFeedback(for: qrCodeDocument, isValid: false) - } - - func cameraPreview( - _ viewController: CameraPreviewViewController, - didDetect qrCodeDocument: GiniQRCodeDocument) { - showQRCodeFeedback(for: qrCodeDocument, isValid: true) - } - - func notAuthorized() { - cameraPane.setupAuthorization(isHidden: true) - cameraPreviewViewController.cameraFrameView.isHidden = true - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/CameraButtonsViewModel.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/CameraButtonsViewModel.swift deleted file mode 100644 index 1bddc01..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera2/CameraButtonsViewModel.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// CameraButtonsViewModel.swift -// -// -// Created by Krzysztof Kryniecki on 14/09/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -public final class CameraButtonsViewModel { - // Pane buttons - var isFlashOn: Bool = false - var flashAction: (( Bool) -> Void)? - var importAction: (() -> Void)? - var captureAction: (() -> Void)? - var imageStackAction: (() -> Void)? - - // NavigationBar buttons - var cancelAction: (() -> Void)? - var helpAction: (() -> Void)? - var backButtonAction: (() -> Void)? - var imagesUpdated: (([UIImage]) -> Void)? - - var images: [UIImage] = [] { - didSet { - imagesUpdated?(images) - } - } - - public weak var trackingDelegate: CameraScreenTrackingDelegate? - public init( - trackingDelegate: CameraScreenTrackingDelegate? = nil - ) { - self.trackingDelegate = trackingDelegate - } - - @objc func toggleFlash() { - isFlashOn = !isFlashOn - flashAction?(isFlashOn) - } - - @objc func importPressed() { - importAction?() - } - - @objc func thumbnailPressed() { - imageStackAction?() - } - - @objc func cancelPressed() { - cancelAction?() - } - - @objc func capturePressed() { - trackingDelegate?.onCameraScreenEvent(event: Event(type: .takePicture)) - captureAction?() - } - - func didCapture( - imageData: Data?, - processedImageData: Data?, - error: CameraError?, - orientation: UIInterfaceOrientation, - giniConfiguration: GiniConfiguration - ) -> GiniImageDocument? { - guard let imageData = imageData, - let processedImageData = processedImageData, - error == nil else { - let errorMessage = error?.message ?? "Image data was nil" - let errorLog = ErrorLog( - description: "There was an error while capturing a picture: \(String(describing: errorMessage))", - error: error) - giniConfiguration.errorLogger.handleErrorLog(error: errorLog) - return nil - } - let imageDocument = GiniImageDocument( - data: imageData, - processedImageData: processedImageData, - imageSource: .camera, - deviceOrientation: orientation) - return imageDocument - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraButtonsViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraButtonsViewController.swift new file mode 100644 index 0000000..85fa32f --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraButtonsViewController.swift @@ -0,0 +1,319 @@ +// +// CameraButtonsViewController.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 2/18/19. +// + +import UIKit + +protocol CameraButtonsViewControllerDelegate: AnyObject { + func cameraButtons(_ viewController: CameraButtonsViewController, + didTapOn button: CameraButtonsViewController.Button) +} + +final class CameraButtonsViewController: UIViewController { + + weak var delegate: CameraButtonsViewControllerDelegate? + + var isFlashSupported: Bool = false { + didSet { + flashToggleButtonContainerView.isHidden = !isFlashSupported + } + } + + fileprivate let giniConfiguration: GiniConfiguration + fileprivate let currentDevice: UIDevice + fileprivate let captureButtonMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + fileprivate var cameraCaptureButtonImage: UIImage? { + return UIImageNamedPreferred(named: "cameraCaptureButton") + } + + enum Button: Equatable { + case fileImport, capture, imagesStack, flashToggle(Bool) + } + + lazy var captureButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(self.cameraCaptureButtonImage, for: .normal) + button.addTarget(self, action: #selector(captureImage), for: .touchUpInside) + button.accessibilityLabel = .localized(resource: CameraStrings.captureButton) + return button + }() + + lazy var flashToggleButtonContainerView: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var flashToggleButton: UIButton = { + let flashToggle = UIButton(type: .custom) + flashToggle.translatesAutoresizingMaskIntoConstraints = false + flashToggle.setImage(UIImageNamedPreferred(named: "flashOn"), for: .selected) + flashToggle.setImage(UIImageNamedPreferred(named: "flashOff"), for: .normal) + flashToggle.isSelected = giniConfiguration.flashOnByDefault + flashToggle.imageView?.contentMode = .scaleAspectFit + flashToggle.addTarget(self, action: #selector(tapOnFlashToggle), for: .touchUpInside) + + if currentDevice.isIpad { + flashToggle.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + } else { + flashToggle.imageEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + } + + return flashToggle + }() + + lazy var capturedImagesStackView: CapturedImagesStackView = { + let view = CapturedImagesStackView(giniConfiguration: giniConfiguration) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + view.didTapImageStackButton = { [weak self] in + guard let self = self else { return } + self.delegate?.cameraButtons(self, didTapOn: .imagesStack) + } + return view + }() + + lazy var fileImportButtonView: FileImportButtonView = { + let view = FileImportButtonView(giniConfiguration: giniConfiguration) + view.translatesAutoresizingMaskIntoConstraints = false + view.didTapButton = { [weak self] in + guard let self = self else { return } + self.delegate?.cameraButtons(self, didTapOn: .fileImport) + } + return view + }() + + lazy var leftStackView: UIStackView = { + let stackView = UIStackView(frame: .zero) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.axis = .horizontal + stackView.alignment = currentDevice.isIpad ? .top : .center + + return stackView + }() + + lazy var rightStackView: UIStackView = { + let stackView = UIStackView(frame: .zero) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.axis = .horizontal + stackView.alignment = currentDevice.isIpad ? .bottom : .center + + return stackView + }() + + var verticalAlignedStackView: UIStackView { + let verticalAlignedStackView = UIStackView() + verticalAlignedStackView.translatesAutoresizingMaskIntoConstraints = false + verticalAlignedStackView.axis = .vertical + verticalAlignedStackView.spacing = 32 + verticalAlignedStackView.alignment = .center + return verticalAlignedStackView + } + + init(giniConfiguration: GiniConfiguration = .shared, currentDevice: UIDevice = .current) { + self.giniConfiguration = giniConfiguration + self.currentDevice = currentDevice + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func loadView() { + super.loadView() + view.backgroundColor = UIColor.from(giniColor: giniConfiguration.cameraButtonsViewBackgroundColor) + view.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(captureButton) + view.addSubview(leftStackView) + view.addSubview(rightStackView) + flashToggleButtonContainerView.addSubview(flashToggleButton) + + if currentDevice.isIpad { + let topVerticalAlignedStackView = verticalAlignedStackView + + if giniConfiguration.multipageEnabled { + topVerticalAlignedStackView.addArrangedSubview(capturedImagesStackView) + } + + if giniConfiguration.flashToggleEnabled { + topVerticalAlignedStackView.addArrangedSubview(flashToggleButtonContainerView) + } + + if !topVerticalAlignedStackView.arrangedSubviews.isEmpty { + rightStackView.addArrangedSubview(topVerticalAlignedStackView) + } + } else { + if giniConfiguration.multipageEnabled { + rightStackView.addArrangedSubview(capturedImagesStackView) + } + + if giniConfiguration.flashToggleEnabled { + if giniConfiguration.multipageEnabled { + leftStackView.addArrangedSubview(flashToggleButtonContainerView) + } else { + rightStackView.addArrangedSubview(flashToggleButtonContainerView) + } + } + } + + flashToggleButtonContainerView.isHidden = !isFlashSupported + + addConstraints() + } + + func addFileImportButton() { + if currentDevice.isIpad { + let bottomVerticalAlignedStackView = verticalAlignedStackView + bottomVerticalAlignedStackView.addArrangedSubview(fileImportButtonView) + + leftStackView.addArrangedSubview(bottomVerticalAlignedStackView) + leftStackView.layoutIfNeeded() + } else { + leftStackView.insertArrangedSubview(fileImportButtonView, at: 0) + } + + addImportButtonConstraints() + } + + func toggleCaptureButtonActivation(state:Bool) { + captureButton.isUserInteractionEnabled = state + captureButton.isEnabled = state + } + +} + +// MARK: - Button actions + +fileprivate extension CameraButtonsViewController { + @objc func captureImage(_ sender: AnyObject) { + toggleCaptureButtonActivation(state: false) + delegate?.cameraButtons(self, didTapOn: .capture) + } + + @objc func tapOnFlashToggle(_ button: UIButton) { + UIImpactFeedbackGenerator().impactOccurred() + button.isSelected.toggle() + delegate?.cameraButtons(self, didTapOn: .flashToggle(button.isSelected)) + } +} + +// MARK: - Constraints + +fileprivate extension CameraButtonsViewController { + + func addConstraints() { + addCaptureButtonConstraints() + addStackViewConstraints() + + if giniConfiguration.flashToggleEnabled { + addFlashButtonConstraints() + } + } + + func addCaptureButtonConstraints() { + if currentDevice.isIpad { + captureButton.heightAnchor.constraint(equalTo: captureButton.widthAnchor).isActive = true + captureButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + captureButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, + constant: -captureButtonMargins.right).isActive = true + captureButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, + constant: captureButtonMargins.left).isActive = true + } else { + captureButton.heightAnchor.constraint(equalTo: captureButton.widthAnchor).isActive = true + captureButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + captureButton.topAnchor.constraint(equalTo: view.topAnchor, + constant: captureButtonMargins.left).isActive = true + captureButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, + constant: -captureButtonMargins.bottom).isActive = true + } + } + + func addStackViewConstraints() { + if currentDevice.isIpad { + rightStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + rightStackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + rightStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + rightStackView.bottomAnchor.constraint(equalTo: captureButton.topAnchor, constant: -30).isActive = true + + leftStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + leftStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + leftStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + leftStackView.topAnchor.constraint(equalTo: captureButton.bottomAnchor, constant: 30).isActive = true + } else { + rightStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + rightStackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + rightStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + rightStackView.leadingAnchor.constraint(equalTo: captureButton.trailingAnchor).isActive = true + + leftStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + leftStackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + leftStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + leftStackView.trailingAnchor.constraint(equalTo: captureButton.leadingAnchor).isActive = true + } + } + + func addImportButtonConstraints() { + if currentDevice.isIpad { + fileImportButtonView.heightAnchor + .constraint(equalToConstant: 70) + .isActive = true + fileImportButtonView.widthAnchor + .constraint(equalTo: leftStackView.widthAnchor, + constant: -(leftStackView.layoutMargins.left + leftStackView.layoutMargins.right)) + .isActive = true + } else { + let heightConstraint = fileImportButtonView + .heightAnchor.constraint(equalTo: leftStackView.heightAnchor) + heightConstraint.priority = UILayoutPriority(999) + heightConstraint.isActive = true + } + } + + func addFlashButtonConstraints() { + flashToggleButton.centerXAnchor.constraint(equalTo: flashToggleButtonContainerView.centerXAnchor) + .isActive = true + flashToggleButton.centerYAnchor.constraint(equalTo: flashToggleButtonContainerView.centerYAnchor) + .isActive = true + + if currentDevice.isIpad { + let heightConstraint = + flashToggleButtonContainerView.widthAnchor.constraint(equalTo: rightStackView.widthAnchor, + multiplier: 1/3) + heightConstraint.priority = UILayoutPriority(999) + heightConstraint.isActive = true + let widthConstraint = flashToggleButtonContainerView + .heightAnchor.constraint(equalTo: flashToggleButtonContainerView.widthAnchor, + multiplier: 17/11) + widthConstraint.priority = UILayoutPriority(999) + widthConstraint.isActive = true + + } else { + let heightConstraint = flashToggleButtonContainerView + .heightAnchor.constraint(equalTo: leftStackView.heightAnchor, + multiplier: 1) + heightConstraint.priority = UILayoutPriority(999) + heightConstraint.isActive = true + let widthConstraint = flashToggleButtonContainerView + .widthAnchor.constraint(equalTo: flashToggleButtonContainerView.heightAnchor, + multiplier: 11/17) + widthConstraint.priority = UILayoutPriority(999) + widthConstraint.isActive = true + } + + let heightConstraint = flashToggleButton + .heightAnchor.constraint(equalTo: flashToggleButtonContainerView.heightAnchor) + heightConstraint.priority = UILayoutPriority(999) + heightConstraint.isActive = true + + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraNotAuthorizedView.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraNotAuthorizedView.swift new file mode 100644 index 0000000..5d33a7b --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraNotAuthorizedView.swift @@ -0,0 +1,113 @@ +// +// CameraNotAuthorizedView.swift +// GiniCapture +// +// Created by Peter Pult on 06/07/16. +// Copyright © 2016 Gini GmbH. All rights reserved. +// + +import UIKit + +final class CameraNotAuthorizedView: UIView { + + // User interface + fileprivate var label = UILabel() + fileprivate var button = UIButton() + fileprivate var imageView = UIImageView() + fileprivate var contentView = UIView() + + // Images + fileprivate var noCameraImage: UIImage? { + return UIImageNamedPreferred(named: "cameraNotAuthorizedIcon") + } + + init(giniConfiguration: GiniConfiguration = GiniConfiguration.shared) { + super.init(frame: CGRect.zero) + + // Configure image view + imageView.image = noCameraImage + imageView.contentMode = .scaleAspectFit + + // Configure label + label.text = .localized(resource: CameraStrings.notAuthorizedMessage) + label.numberOfLines = 0 + label.textColor = giniConfiguration.cameraNotAuthorizedTextColor + label.textAlignment = .center + label.font = giniConfiguration.customFont.with(weight: .thin, size: 20, style: .title2) + + // Configure button + button.setTitle(.localized(resource: CameraStrings.notAuthorizedButton), for: .normal) + button.setTitleColor(giniConfiguration.cameraNotAuthorizedButtonTitleColor, for: .normal) + button.setTitleColor(giniConfiguration.cameraNotAuthorizedButtonTitleColor.withAlphaComponent(0.8), + for: .highlighted) + button.titleLabel?.font = giniConfiguration.customFont.with(weight: .regular, size: 20, style: .caption1) + + button.addTarget(self, action: #selector(openSettings), for: .touchUpInside) + + // Configure view hierachy + addSubview(contentView) + contentView.addSubview(imageView) + contentView.addSubview(label) + contentView.addSubview(button) + + // Add constraints + addConstraints() + } + + /** + Returns an object initialized from data in a given unarchiver. + + - warning: Not implemented. + */ + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @IBAction func openSettings(_ sender: AnyObject) { + UIApplication.shared.openAppSettings() + } + + // MARK: Constraints + fileprivate func addConstraints() { + let superview = self + + // Content view + contentView.translatesAutoresizingMaskIntoConstraints = false + Constraints.active(item: contentView, attr: .top, relatedBy: .greaterThanOrEqual, to: superview, attr: .top, + constant: 30) + Constraints.active(item: contentView, attr: .centerX, relatedBy: .equal, to: superview, attr: .centerX) + Constraints.active(item: contentView, attr: .centerY, relatedBy: .equal, to: superview, attr: .centerY, + constant: 5, priority: 999) + + // Image view + imageView.translatesAutoresizingMaskIntoConstraints = false + Constraints.active(item: imageView, attr: .top, relatedBy: .equal, to: contentView, attr: .top) + Constraints.active(item: imageView, attr: .width, relatedBy: .lessThanOrEqual, to: nil, attr: .width, + constant: 204) + Constraints.active(item: imageView, attr: .width, relatedBy: .greaterThanOrEqual, to: nil, attr: .width, + constant: 75) + Constraints.active(item: imageView, attr: .height, relatedBy: .lessThanOrEqual, to: nil, attr: .height, + constant: 75) + Constraints.active(item: imageView, attr: .height, relatedBy: .greaterThanOrEqual, to: nil, attr: .height, + constant: 50) + Constraints.active(item: imageView, attr: .centerX, relatedBy: .equal, to: contentView, attr: .centerX) + + // Text label + label.translatesAutoresizingMaskIntoConstraints = false + Constraints.active(item: label, attr: .top, relatedBy: .equal, to: imageView, attr: .bottom, constant: 35) + Constraints.active(item: label, attr: .trailing, relatedBy: .equal, to: contentView, attr: .trailing) + Constraints.active(item: label, attr: .leading, relatedBy: .equal, to: contentView, attr: .leading) + Constraints.active(item: label, attr: .width, relatedBy: .equal, to: nil, attr: .width, constant: 250) + Constraints.active(item: label, attr: .height, relatedBy: .greaterThanOrEqual, to: nil, attr: .height, + constant: 70) + + // Button + button.translatesAutoresizingMaskIntoConstraints = false + Constraints.active(item: button, attr: .top, relatedBy: .equal, to: label, attr: .bottom, constant: 10) + Constraints.active(item: button, attr: .bottom, relatedBy: .equal, to: contentView, attr: .bottom) + Constraints.active(item: button, attr: .width, relatedBy: .equal, to: label, attr: .width) + Constraints.active(item: button, attr: .height, relatedBy: .equal, to: nil, attr: .height, constant: 35) + Constraints.active(item: button, attr: .centerX, relatedBy: .equal, to: contentView, attr: .centerX) + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewView.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewView.swift new file mode 100644 index 0000000..d86db5f --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewView.swift @@ -0,0 +1,153 @@ +// +// CameraPreviewView.swift +// GiniCapture +// +// Created by Peter Pult / Nikola Sobadjiev on 14/06/16. +// Copyright © 2016 Gini GmbH. All rights reserved. +// + +import UIKit +import AVFoundation + +final class CameraPreviewView: UIView { + + let frameColor = UIColor.from(giniColor: GiniConfiguration.shared.cameraPreviewFrameColor) + let guideLineLength: CGFloat = 50.0 + let guideLineWidth: CGFloat = 2.0 + /// the size of the guides compared to the size of the whole view + /// 0.9 = 90% of the view + let guideLineSize: CGFloat = 0.9 + fileprivate(set) var guideColor = UIColor.white + fileprivate(set) var ratio: CGFloat = 21.0 / 31.0 // A4 by default + fileprivate(set) var guidesLayer: CAShapeLayer? + fileprivate(set) var frameLayer: CAShapeLayer? + + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + var session: AVCaptureSession { + get { + return (self.layer as? AVCaptureVideoPreviewLayer)!.session! + } + set(newSession) { + (self.layer as? AVCaptureVideoPreviewLayer)!.session = newSession + } + } + + override func layoutSubviews() { + super.layoutSubviews() + positionViews() + } + + func positionViews() { + positionGuides() + positionFrame() + } + +} + +extension CameraPreviewView { + + func drawGuides(withColor color: UIColor) { + guideColor = color + createGuides() + createGrayFrame() + } + + fileprivate func createGuides() { + let rectLayer = CAShapeLayer() + styleLayer(rectLayer) + layer.addSublayer(rectLayer) + guidesLayer = rectLayer + } + + fileprivate func createGrayFrame() { + let grayFrame = CAShapeLayer() + grayFrame.fillColor = frameColor.cgColor + grayFrame.lineWidth = 0 + layer.addSublayer(grayFrame) + frameLayer = grayFrame + } + + fileprivate func positionGuides() { + guard let guides = guidesLayer else { + return + } + // get a size that's a close to guideLineSize as possible, while still respecting the + // ratio of a standard A4 piece of paper + guides.frame = biggestA4SizeRect() + guides.position = center + guides.path = guidePath(size: guides.frame.size) + } + + fileprivate func positionFrame() { + guard let grayFrame = frameLayer else { + return + } + // The frame is as big as the guides + var innerRect = biggestA4SizeRect() + // However, in order for them to not overlap, the frame needs to get a little bigger + innerRect.origin.x = ((frame.width - innerRect.width) / 2.0) - (guideLineWidth / 2.0) + innerRect.origin.y = ((frame.height - innerRect.height) / 2.0) - (guideLineWidth / 2.0) + innerRect.size.width += guideLineWidth + innerRect.size.height += guideLineWidth + let cutOut = UIBezierPath(rect: innerRect) + let path = UIBezierPath(rect: bounds) + path.append(cutOut.reversing()) + + grayFrame.path = path.cgPath + grayFrame.frame = frame + grayFrame.position = center + } + + fileprivate func biggestA4SizeRect() -> CGRect { + let wholeFrame = bounds + let maxWidth = wholeFrame.width * guideLineSize + let maxHeight = wholeFrame.height * guideLineSize + + if (maxHeight > maxWidth || UIApplication.shared.statusBarOrientation.isPortrait) && + maxWidth > maxHeight * ratio { + return CGRect(x: 0, y: 0, width: maxHeight * ratio, height: maxHeight) + } else { + let height: CGFloat + if maxWidth > maxHeight * ratio { + // This only happens when the app is on landscape mode and + // it fills all the window (no SplitMode on iPads). + // In this case the a4 would be landscape (31.0 / 21.0) + height = maxWidth * ratio + } else { + height = maxWidth / ratio + } + return CGRect(x: 0, y: 0, width: maxWidth, height: height) + } + } + + fileprivate func guidePath(size: CGSize) -> CGPath { + let guidePath = UIBezierPath() + + guidePath.move(to: CGPoint(x: 0.0, y: guideLineLength)) + guidePath.addLine(to: CGPoint(x: 0.0, y: 0.0)) + guidePath.addLine(to: CGPoint(x: guideLineLength, y: 0.0)) + + guidePath.move(to: CGPoint(x: size.width - guideLineLength, y: 0.0)) + guidePath.addLine(to: CGPoint(x: size.width, y: 0.0)) + guidePath.addLine(to: CGPoint(x: size.width, y: guideLineLength)) + + guidePath.move(to: CGPoint(x: size.width, y: size.height - guideLineLength)) + guidePath.addLine(to: CGPoint(x: size.width, y: size.height)) + guidePath.addLine(to: CGPoint(x: size.width - guideLineLength, y: size.height)) + + guidePath.move(to: CGPoint(x: guideLineLength, y: size.height)) + guidePath.addLine(to: CGPoint(x: 0, y: size.height)) + guidePath.addLine(to: CGPoint(x: 0, y: size.height - guideLineLength)) + return guidePath.cgPath + } + + fileprivate func styleLayer(_ layer: CAShapeLayer) { + layer.strokeColor = guideColor.cgColor + layer.fillColor = UIColor.clear.cgColor + layer.lineWidth = guideLineWidth + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift index 1d6005c..281b0e5 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift @@ -40,28 +40,7 @@ final class CameraPreviewViewController: UIViewController { spinner.hidesWhenStopped = true return spinner }() - - lazy var cameraFrameView: UIImageView = { - let imageView = UIImageView() - imageView.image = UIImageNamedPreferred(named: "cameraFocus") - imageView.contentMode = .scaleAspectFit - imageView.isHidden = true - return imageView - }() - - lazy var qrCodeFrameView: UIImageView = { - let imageView = UIImageView() - imageView.image = UIImageNamedPreferred(named: "qrCodeFocus") - imageView.contentMode = .scaleAspectFit - imageView.isHidden = !qrCodeScanningOnlyEnabled - return imageView - }() - - private lazy var qrCodeScanningOnlyEnabled: Bool = { - return giniConfiguration.qrCodeScanningEnabled && giniConfiguration.onlyQRCodeScanningEnabled - }() - - private var notAuthorizedView: UIView? + fileprivate let giniConfiguration: GiniConfiguration fileprivate typealias FocusIndicator = UIImageView fileprivate var camera: CameraProtocol @@ -77,15 +56,13 @@ final class CameraPreviewViewController: UIViewController { fileprivate var cameraFocusSmall: UIImage? { return UIImageNamedPreferred(named: "cameraFocusSmall") } - - // A flag to determine the default image when testing on simulator. - private var isReturnAssistantTesting = true + + fileprivate var cameraFocusLarge: UIImage? { + return UIImageNamedPreferred(named: "cameraFocusLarge") + } + fileprivate var defaultImage: UIImage? { - if isReturnAssistantTesting { - return UIImageNamedPreferred(named: "CameraDefaultReturnAssistantDocument") - } else { - return UIImageNamedPreferred(named: "cameraDefaultDocumentImage") - } + return UIImageNamedPreferred(named: "cameraDefaultDocumentImage") } var isAuthorized = false @@ -93,12 +70,12 @@ final class CameraPreviewViewController: UIViewController { lazy var previewView: CameraPreviewView = { let previewView = CameraPreviewView() previewView.translatesAutoresizingMaskIntoConstraints = false - (previewView.layer as? AVCaptureVideoPreviewLayer)?.videoGravity = .resize + (previewView.layer as? AVCaptureVideoPreviewLayer)?.videoGravity = .resizeAspectFill let tapGesture = UITapGestureRecognizer(target: self, action: #selector(focusAndExposeTap)) previewView.addGestureRecognizer(tapGesture) return previewView }() - + init(giniConfiguration: GiniConfiguration = .shared, camera: CameraProtocol = Camera(giniConfiguration: .shared)) { self.giniConfiguration = giniConfiguration @@ -123,11 +100,11 @@ final class CameraPreviewViewController: UIViewController { selector: #selector(subjectAreaDidChange), name: NSNotification.Name.AVCaptureDeviceSubjectAreaDidChange, object: camera.videoDeviceInput?.device) - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.dark1, dark: UIColor.GiniCapture.dark1).uiColor() + + previewView.drawGuides(withColor: giniConfiguration.cameraPreviewCornerGuidesColor) + view.insertSubview(previewView, at: 0) - view.addSubview(qrCodeFrameView) - view.addSubview(cameraFrameView) - + Constraints.pin(view: previewView, toSuperView: view) addLoadingIndicator() } @@ -136,63 +113,6 @@ final class CameraPreviewViewController: UIViewController { camera.start() startLoadingIndicator() } - - override func viewDidLoad() { - super.viewDidLoad() - - setupConstraints() - } - - private lazy var cameraFrameViewHeightAnchorPortrait = - cameraFrameView.heightAnchor.constraint(equalTo: cameraFrameView.widthAnchor, - multiplier: Constants.a4AspectRatio) - - private lazy var cameraFrameViewHeightAnchorLandscape = - cameraFrameView.heightAnchor.constraint(equalTo: cameraFrameView.widthAnchor, - multiplier: 1 / Constants.a4AspectRatio) - - private func setupConstraints() { - cameraFrameView.translatesAutoresizingMaskIntoConstraints = false - qrCodeFrameView.translatesAutoresizingMaskIntoConstraints = false - - if UIDevice.current.isIpad { - NSLayoutConstraint.activate([ - cameraFrameView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: Constants.padding), - cameraFrameView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - cameraFrameView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, - constant: Constants.padding), - cameraFrameView.centerXAnchor.constraint(equalTo: view.centerXAnchor, - constant: -Constants.cameraPaneWidth/2), - cameraFrameViewHeightAnchorPortrait]) - } else { - // The height of the bottom controls - let bottomControlHeight = view.frame.height * 0.23 + - (giniConfiguration.bottomNavigationBarEnabled ? Constants.bottomNavigationBarHeight : 0) - - NSLayoutConstraint.activate([ - cameraFrameView.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.padding), - cameraFrameView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, - constant: Constants.padding), - cameraFrameView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - cameraFrameView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, - constant: -bottomControlHeight-Constants.padding), - cameraFrameView.widthAnchor.constraint(equalTo: cameraFrameView.heightAnchor, - multiplier: 1 / Constants.a4AspectRatio) - ]) - } - - NSLayoutConstraint.activate([ - previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - previewView.topAnchor.constraint(equalTo: view.topAnchor), - previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - - qrCodeFrameView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - qrCodeFrameView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - qrCodeFrameView.widthAnchor.constraint(equalToConstant: Constants.QRCodeScannerSize.width), - qrCodeFrameView.heightAnchor.constraint(equalToConstant: Constants.QRCodeScannerSize.height) - ]) - } public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -201,28 +121,11 @@ final class CameraPreviewViewController: UIViewController { public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - + coordinator.animate(alongsideTransition: { [weak self] _ in self?.updatePreviewViewOrientation() }) } - - private func updateFrameOrientation(with orientation: AVCaptureVideoOrientation) { - if UIDevice.current.isIpad { - let isLandscape = orientation == .landscapeRight || orientation == .landscapeLeft - cameraFrameViewHeightAnchorPortrait.isActive = !isLandscape - cameraFrameViewHeightAnchorLandscape.isActive = isLandscape - - if let image = cameraFrameView.image?.cgImage { - if isLandscape { - cameraFrameView.image = UIImage(cgImage: image, scale: 1.0, orientation: .left) - } else { - cameraFrameView.image = UIImage(cgImage: image, scale: 1.0, orientation: .up) - } - - } - } - } public override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() @@ -256,15 +159,18 @@ final class CameraPreviewViewController: UIViewController { } }) } - + + func showCameraOverlay() { + previewView.guidesLayer?.isHidden = false + previewView.frameLayer?.isHidden = false + } + + func hideCameraOverlay() { + previewView.guidesLayer?.isHidden = true + previewView.frameLayer?.isHidden = true + } + func setupCamera() { - if AVCaptureDevice.authorizationStatus(for: .video) != .authorized { - #if !targetEnvironment(simulator) - self.addNotAuthorizedView() - self.delegate?.notAuthorized() - #endif - } - camera.setup { error in if let error = error { switch error { @@ -284,10 +190,9 @@ final class CameraPreviewViewController: UIViewController { } } else { self.isAuthorized = true - self.notAuthorizedView?.isHidden = true self.delegate?.cameraDidSetUp(self, camera: self.camera) } - + self.stopLoadingIndicator() } @@ -322,16 +227,9 @@ final class CameraPreviewViewController: UIViewController { } else { orientation = .portrait } - if let cameraLayer = previewView.layer as? AVCaptureVideoPreviewLayer { cameraLayer.connection?.videoOrientation = orientation } - updateFrameOrientation(with: orientation) - } - - func changeFrameColor(to color: UIColor) { - cameraFrameView.image = cameraFrameView.image?.tintedImageWithColor(color) - qrCodeFrameView.image = qrCodeFrameView.image?.tintedImageWithColor(color) } } @@ -339,38 +237,35 @@ final class CameraPreviewViewController: UIViewController { extension CameraPreviewViewController { fileprivate func addNotAuthorizedView() { - let notAuthorizedView = CameraNotAuthorizedView() - self.notAuthorizedView = notAuthorizedView - super.view.addSubview(notAuthorizedView) - notAuthorizedView.translatesAutoresizingMaskIntoConstraints = false - - let withBottomPadding = giniConfiguration.bottomNavigationBarEnabled && !qrCodeScanningOnlyEnabled - let bottomPadding: CGFloat = withBottomPadding ? Constants.bottomNavigationBarHeight : 0 - - NSLayoutConstraint.activate([ - notAuthorizedView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - notAuthorizedView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -bottomPadding), - notAuthorizedView.topAnchor.constraint(equalTo: view.topAnchor), - notAuthorizedView.leadingAnchor.constraint(equalTo: view.leadingAnchor)]) + // Add not authorized view + let view = CameraNotAuthorizedView() + super.view.addSubview(view) + + view.translatesAutoresizingMaskIntoConstraints = false + + Constraints.active(item: view, attr: .width, relatedBy: .equal, to: super.view, attr: .width) + Constraints.active(item: view, attr: .height, relatedBy: .equal, to: super.view, attr: .height) + Constraints.active(item: view, attr: .centerX, relatedBy: .equal, to: super.view, attr: .centerX) + Constraints.active(item: view, attr: .centerY, relatedBy: .equal, to: super.view, attr: .centerY) + + // Hide camera UI + hideCameraOverlay() } /// Adds a default image to the canvas when no camera is available (DEBUG mode only) fileprivate func addDefaultImage() { - guard let defaultImage = defaultImage else { return } - defaultImageView = UIImageView(image: defaultImage) guard let defaultImageView = defaultImageView else { return } - defaultImageView.alpha = 0.5 - cameraFrameView.addSubview(defaultImageView) + + defaultImageView.contentMode = .scaleAspectFit + previewView.addSubview(defaultImageView) defaultImageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - defaultImageView.centerXAnchor.constraint(equalTo: cameraFrameView.centerXAnchor), - defaultImageView.centerYAnchor.constraint(equalTo: cameraFrameView.centerYAnchor), - defaultImageView.heightAnchor.constraint(equalTo: cameraFrameView.heightAnchor), - defaultImageView.widthAnchor.constraint(equalTo: cameraFrameView.widthAnchor) - ]) + Constraints.active(item: defaultImageView, attr: .width, relatedBy: .equal, to: previewView, attr: .width) + Constraints.active(item: defaultImageView, attr: .height, relatedBy: .equal, to: previewView, attr: .height) + Constraints.active(item: defaultImageView, attr: .centerX, relatedBy: .equal, to: previewView, attr: .centerX) + Constraints.active(item: defaultImageView, attr: .centerY, relatedBy: .equal, to: previewView, attr: .centerY) } } @@ -416,21 +311,18 @@ extension CameraPreviewViewController { } @objc fileprivate func subjectAreaDidChange(_ notification: Notification) { + guard let previewLayer = previewView.layer as? AVCaptureVideoPreviewLayer else { return } let devicePoint = CGPoint(x: 0.5, y: 0.5) + camera.focus(withMode: .continuousAutoFocus, exposeWithMode: .continuousAutoExposure, atDevicePoint: devicePoint, monitorSubjectAreaChange: false) + + let imageView = + createFocusIndicator(withImage: cameraFocusLarge, + atPoint: previewLayer.layerPointConverted(fromCaptureDevicePoint: devicePoint)) + showFocusIndicator(imageView) } } - -extension CameraPreviewViewController { - private enum Constants { - static let padding: CGFloat = 16 - static let a4AspectRatio: CGFloat = 1.414 - static let cameraPaneWidth: CGFloat = 124 - static let bottomNavigationBarHeight: CGFloat = 114 - static let QRCodeScannerSize = CGSize(width: 258, height: 258) - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraViewController+Constraints.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraViewController+Constraints.swift new file mode 100644 index 0000000..eba5098 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraViewController+Constraints.swift @@ -0,0 +1,72 @@ +// +// CameraViewController+Constraints.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 1/26/18. +// + +import UIKit + +extension CameraViewController { + func addConstraints() { + addPreviewViewConstraints() + addControlsViewConstraints() + } + + fileprivate func addPreviewViewConstraints() { + if UIDevice.current.isIpad { + Constraints.active(item: cameraPreviewViewController.view, attr: .top, relatedBy: .equal, + to: self.view, attr: .top) + Constraints.active(item: cameraPreviewViewController.view, attr: .bottom, relatedBy: .equal, + to: self.view, attr: .bottom) + Constraints.active(item: cameraPreviewViewController.view, attr: .leading, relatedBy: .equal, + to: self.view, attr: .leading) + Constraints.active(item: cameraPreviewViewController.view, attr: .trailing, relatedBy: .equal, + to: cameraButtonsViewController.view, attr: .leading) + } else { + // lower priority constraints - will make the preview "want" to get bigger + Constraints.active(item: cameraPreviewViewController.view, attr: .top, relatedBy: .equal, + to: self.view, attr: .top) + Constraints.active(item: cameraPreviewViewController.view, attr: .leading, relatedBy: .equal, + to: self.view, attr: .leading) + Constraints.active(item: cameraPreviewViewController.view, attr: .trailing, relatedBy: .equal, + to: self.view, attr: .trailing) + Constraints.active(item: cameraPreviewViewController.view, attr: .bottom, relatedBy: .equal, + to: cameraButtonsViewController.view, attr: .top) + } + } + + fileprivate func addControlsViewConstraints() { + if UIDevice.current.isIpad { + Constraints.active(item: cameraButtonsViewController.view, attr: .width, relatedBy: .equal, to: nil, + attr: .notAnAttribute, + constant: 102) + Constraints.active(item: cameraButtonsViewController.view, attr: .top, relatedBy: .equal, + to: self.view, + attr: .top) + Constraints.active(item: cameraButtonsViewController.view, attr: .trailing, relatedBy: .equal, + to: self.view, + attr: .trailing) + Constraints.active(item: cameraButtonsViewController.view, attr: .bottom, relatedBy: .equal, + to: self.view, + attr: .bottom) + Constraints.active(item: cameraButtonsViewController.view, attr: .leading, relatedBy: .equal, + to: cameraPreviewViewController.view, attr: .trailing, priority: 750) + } else { + Constraints.active(item: cameraButtonsViewController.view, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, + constant: 102) + Constraints.active(item: cameraButtonsViewController.view, attr: .top, relatedBy: .equal, + to: cameraPreviewViewController.view, + attr: .bottom) + Constraints.active(item: cameraButtonsViewController.view, attr: .bottom, relatedBy: .equal, + to: view.safeAreaLayoutGuide, + attr: .bottom) + Constraints.active(item: cameraButtonsViewController.view, attr: .trailing, relatedBy: .equal, + to: self.view, attr: .trailing) + Constraints.active(item: cameraButtonsViewController.view, attr: .leading, relatedBy: .equal, + to: self.view, attr: .leading) + } + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraViewController.swift new file mode 100644 index 0000000..d86da9d --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraViewController.swift @@ -0,0 +1,680 @@ +// +// CameraViewController.swift +// GiniCapture +// +// Created by Peter Pult on 08/06/16. +// Copyright © 2016 Gini GmbH. All rights reserved. +// + +import UIKit +import AVFoundation + +/** + The CameraViewControllerDelegate protocol defines methods that allow you to handle captured images and user + actions. + + - note: Component API only. + */ +@objc public protocol CameraViewControllerDelegate: AnyObject { + /** + Called when a user takes a picture, imports a PDF/QRCode or imports one or several images. + Once the method has been implemented, it is necessary to check if the number of + documents accumulated doesn't exceed the minimun (`GiniImageDocument.maxPagesCount`). + + - parameter viewController: `CameraViewController` where the documents were taken. + - parameter document: One or several documents either captured or imported in + the `CameraViewController`. They can contain an error produced in the validation process. + */ + @objc func camera(_ viewController: CameraViewController, + didCapture document: GiniCaptureDocument) + + /** + Called when a user selects a picker from the picker selector sheet. + + - parameter viewController: `CameraViewController` where the documents were taken. + - parameter documentPicker: `DocumentPickerType` selected in the sheet. + */ + @objc func camera(_ viewController: CameraViewController, didSelect documentPicker: DocumentPickerType) + + /** + Called when the `CameraViewController` appears. + + - parameter viewController: Camera view controller that appears. + */ + @objc func cameraDidAppear(_ viewController: CameraViewController) + + /** + Called when a user taps the `MultipageReviewButton` (the one with the thumbnail of the images(s) taken). + Once this method is called, the `MultipageReviewViewController` should be presented. + + - parameter viewController: Camera view controller where the button was tapped. + */ + @objc func cameraDidTapMultipageReviewButton(_ viewController: CameraViewController) + +} + +/** + The `CameraViewController` provides a custom camera screen which enables the user to take a + photo of a document to be analyzed. The user can focus the camera manually if the auto focus does not work. + + - note: Component API only. + */ +//swiftlint:disable file_length + +@objcMembers public final class CameraViewController: UIViewController { + + /** + The object that acts as the delegate of the camera view controller. + */ + public weak var delegate: CameraViewControllerDelegate? + public weak var trackingDelegate: CameraScreenTrackingDelegate? + + var opaqueView: UIView? + var fileImportToolTipView: ToolTipView? + var qrCodeToolTipView: ToolTipView? + let giniConfiguration: GiniConfiguration + var currentDevice: UIDevice + fileprivate var detectedQRCodeDocument: GiniQRCodeDocument? + fileprivate var currentQRCodePopup: QRCodeDetectedPopupView? + var shouldShowQRCodeNext = false + + lazy var cameraPreviewViewController: CameraPreviewViewController = { + let cameraPreviewViewController = CameraPreviewViewController() + cameraPreviewViewController.delegate = self + return cameraPreviewViewController + }() + + lazy var cameraButtonsViewController: CameraButtonsViewController = { + let cameraButtonsViewController = CameraButtonsViewController() + cameraButtonsViewController.delegate = self + return cameraButtonsViewController + }() + + /** + Designated initializer for the `CameraViewController` which allows + to set the `GiniConfiguration for the camera screen`. + All the interactions with this screen are handled by `CameraViewControllerDelegate`. + + - parameter giniConfiguration: `GiniConfiguration` instance. + + - returns: A view controller instance allowing the user to take a picture or pick a document. + */ + public init(giniConfiguration: GiniConfiguration) { + self.giniConfiguration = giniConfiguration + self.currentDevice = .current + super.init(nibName: nil, bundle: nil) + } + + /** + Returns an object initialized from data in a given unarchiver. + + - warning: Not implemented. + */ + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func didPick(_ document: GiniCaptureDocument) { + if let delegate = delegate { + delegate.camera(self, didCapture: document) + } else { + assertionFailure("The CameraViewControllerDelegate has not been assigned") + } + } + + + public override func loadView() { + super.loadView() + edgesForExtendedLayout = [] + view.backgroundColor = UIColor.from(giniColor: giniConfiguration.cameraContainerViewBackgroundColor) + + // `previewView` must be added at 0 because otherwise NotAuthorizedView button won't ever be touchable + addChild(cameraPreviewViewController) + view.addSubview(cameraPreviewViewController.view) + cameraPreviewViewController.didMove(toParent: self) + + addChild(cameraButtonsViewController) + view.insertSubview(cameraButtonsViewController.view, aboveSubview: cameraPreviewViewController.view) + cameraButtonsViewController.didMove(toParent: self) + + addConstraints() + } + + fileprivate func showTooltip() { + if giniConfiguration.fileImportSupportedTypes != .none { + cameraButtonsViewController.addFileImportButton() + + // If FileImportToolTip was shown and QRCodeToolTip not yet + if !OnboardingContainerViewController.willBeShown { + if ToolTipView.shouldShowFileImportToolTip { + showFileImportTip() + } else { + showQrCodeTip() + } + } + } + } + + public override func viewDidLoad() { + super.viewDidLoad() + showTooltip() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setStatusBarStyle(to: giniConfiguration.statusBarStyle) + if let tooltip = fileImportToolTipView, tooltip.isHidden == false { + } else { + cameraButtonsViewController.toggleCaptureButtonActivation(state: true) + } + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + delegate?.cameraDidAppear(self) + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + self.fileImportToolTipView?.arrangeViews() + self.qrCodeToolTipView?.arrangeViews() + self.opaqueView?.frame = cameraPreviewViewController.view.frame + } + + override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate(alongsideTransition: { [weak self] _ in + guard let self = self else { + return + } + + self.fileImportToolTipView?.arrangeViews() + self.qrCodeToolTipView?.arrangeViews() + + }) + } + + public func setupCamera() { + cameraPreviewViewController.setupCamera() + } +} + +// MARK: - Toggle UI elements + +extension CameraViewController { + + /** + Show the capture button. Should be called when onboarding is dismissed. + */ + public func showCaptureButton() { + cameraButtonsViewController.view.alpha = 1 + } + + /** + Hide the capture button. Should be called when onboarding is presented. + */ + public func hideCaptureButton() { + cameraButtonsViewController.view.alpha = 0 + } + + /** + Show the camera overlay. Should be called when onboarding is dismissed. + */ + public func showCameraOverlay() { + cameraPreviewViewController.showCameraOverlay() + } + + /** + Hide the camera overlay. Should be called when onboarding is presented. + */ + public func hideCameraOverlay() { + cameraPreviewViewController.hideCameraOverlay() + } + + /** + Disable captureButton and flashToggleButton. + */ + fileprivate func configureCameraButtonsForFileImportTip() { + cameraButtonsViewController.captureButton.isEnabled = false + cameraButtonsViewController.flashToggleButton.isEnabled = false + } + + /** + Show the fileImportTip. Should be called when onboarding is dismissed. + */ + public func showFileImportTip() { + self.configureCameraButtonsForFileImportTip() + createFileImportTip(giniConfiguration: giniConfiguration) + self.fileImportToolTipView?.show { + self.opaqueView?.alpha = 1 + } + ToolTipView.shouldShowFileImportToolTip = false + } + + /** + Hide the fileImportTip. Should be called when onboarding is presented. + */ + public func hideFileImportTip() { + self.fileImportToolTipView?.alpha = 0 + } + + /** + Disable all camera buttons except capture button. + */ + fileprivate func configureCameraButtonsForQRCodeTip() { + cameraButtonsViewController.captureButton.isEnabled = true + cameraButtonsViewController.flashToggleButton.isEnabled = true + cameraButtonsViewController.flashToggleButton.isSelected = giniConfiguration.flashOnByDefault + + cameraButtonsViewController.fileImportButtonView.importFileButton.isEnabled = false + cameraButtonsViewController.fileImportButtonView.importFileSubtitleLabel.isEnabled = false + cameraButtonsViewController.fileImportButtonView.isUserInteractionEnabled = false + } + + /** + Show the QR code Tip. Should be called when fileImportTip is dismissed. + */ + public func showQrCodeTip() { + if ToolTipView.shouldShowQRCodeToolTip && giniConfiguration.qrCodeScanningEnabled { + self.configureCameraButtonsForQRCodeTip() + createQRCodeTip(giniConfiguration: giniConfiguration) + self.qrCodeToolTipView?.show { + self.opaqueView?.alpha = 1 + } + ToolTipView.shouldShowQRCodeToolTip = false + self.shouldShowQRCodeNext = false + } + } + + /** + Hide the QR code Tip. Should be called when onboarding is presented. + */ + public func hideQrCodeTip() { + self.qrCodeToolTipView?.alpha = 0 + } + +} + +// MARK: - Image capture + +extension CameraViewController { + + /** + Used to animate the captured image, first shrinking it and then translating it to the captured images stack view. + + - parameter imageDocument: `GiniImageDocument` to be animated. + - parameter completion: Completion block. + + */ + public func animateToControlsView(imageDocument: GiniImageDocument, completion: (() -> Void)? = nil) { + guard let documentImage = imageDocument.previewImage else { return } + let previewImageView = previewCapturedImageView(with: documentImage) + view.addSubview(previewImageView) + + UIView.animate(withDuration: AnimationDuration.medium, animations: { + previewImageView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + }, completion: { _ in + UIView.animateKeyframes(withDuration: AnimationDuration.medium, delay: 0.6, animations: { + UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: { + let thumbnailSize = self.cameraButtonsViewController.capturedImagesStackView.thumbnailSize + let scaleRatioY = thumbnailSize.height / self.cameraPreviewViewController.view.frame.height + let scaleRatioX = thumbnailSize.width / self.cameraPreviewViewController.view.frame.width + + previewImageView.transform = CGAffineTransform(scaleX: scaleRatioX, y: scaleRatioY) + previewImageView.center = self.cameraButtonsViewController.capturedImagesStackView + .thumbnailFrameRelative(to: self.view) + .center + }) + if !self.cameraButtonsViewController.capturedImagesStackView.isHidden { + UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 1, animations: { + previewImageView.alpha = 0 + }) + } + }, completion: { _ in + previewImageView.removeFromSuperview() + self.cameraButtonsViewController.capturedImagesStackView.addImageToStack(image: documentImage) + completion?() + }) + }) + if let tooltip = fileImportToolTipView, tooltip.isHidden == false { + } else { + cameraButtonsViewController.toggleCaptureButtonActivation(state: true) + } + } + + /** + Replaces the captured images stack content with new images. + + - parameter images: New images to be shown in the stack. (Last image will be shown on top) + */ + public func replaceCapturedStackImages(with images: [UIImage]) { + cameraButtonsViewController.capturedImagesStackView.replaceStackImages(with: images) + cameraButtonsViewController.rightStackView.layoutIfNeeded() + } + + fileprivate func showPopup(forQRDetected qrDocument: GiniQRCodeDocument, didTapDone: @escaping () -> Void) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + let newQRCodePopup = QRCodeDetectedPopupView(parent: self.view, + refView: self.cameraPreviewViewController.view, + document: qrDocument, + giniConfiguration: self.giniConfiguration) + + let didDismiss: () -> Void = { [weak self] in + self?.detectedQRCodeDocument = nil + self?.currentQRCodePopup = nil + } + + newQRCodePopup.didTapDone = { [weak self] in + didTapDone() + self?.currentQRCodePopup?.hide(after: 0.0, completion: didDismiss) + } + + self.showCurrentPopup(self, newQRCodePopup: newQRCodePopup) { + didDismiss() + } + } + } + + fileprivate func showCurrentPopup(_ self: CameraViewController, newQRCodePopup: QRCodeDetectedPopupView, didDismiss: @escaping () -> Void) { + if self.currentQRCodePopup != nil { + self.currentQRCodePopup?.hide { [weak self] in + self?.currentQRCodePopup = newQRCodePopup + self?.currentQRCodePopup?.show(didDismiss: didDismiss) + } + } else { + self.currentQRCodePopup = newQRCodePopup + self.currentQRCodePopup?.show(didDismiss: didDismiss) + } + } + + fileprivate func showPopup(forUnsupportedQR qrDocument: GiniQRCodeDocument) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + let newQRCodePopup = QRCodeDetectedPopupView(parent: self.view, + refView: self.cameraPreviewViewController.view, + document: qrDocument, + giniConfiguration: self.giniConfiguration) + let didDismiss: () -> Void = { [weak self] in + self?.detectedQRCodeDocument = nil + self?.currentQRCodePopup = nil + } + + self.configurePopupViewForUnsupportedQR(newQRCodePopup, dismissCompletion: didDismiss) + self.showCurrentPopup(self, newQRCodePopup: newQRCodePopup) { + didDismiss() + } + } + } + + fileprivate func configurePopupViewForUnsupportedQR(_ newQRCodePopup: QRCodeDetectedPopupView, + dismissCompletion: @escaping () -> Void) { + newQRCodePopup.backgroundColor = UIColor.from(giniColor:giniConfiguration.unsupportedQrCodePopupBackgroundColor) + newQRCodePopup.qrText.textColor = UIColor.from(giniColor: giniConfiguration.unsupportedQrCodePopupTextColor) + newQRCodePopup.qrText.text = .localized(resource: CameraStrings.unsupportedQrCodeDetectedPopupMessage) + newQRCodePopup.proceedButton.setTitle("✕", for: .normal) + newQRCodePopup.proceedButton.setTitleColor(giniConfiguration.unsupportedQrCodePopupButtonColor, for: .normal) + newQRCodePopup.proceedButton.setTitleColor(giniConfiguration.unsupportedQrCodePopupButtonColor.withAlphaComponent(0.5), for: .highlighted) + newQRCodePopup.didTapDone = { [weak self] in + self?.currentQRCodePopup?.hide(after: 0.0, completion: dismissCompletion) + } + } + + private func cameraDidCapture(imageData: Data?, error: CameraError?) { + guard let imageData = imageData, + error == nil else { + let errorMessage = error?.message ?? "Image data was nil" + let errorLog = ErrorLog(description: "There was an error while capturing a picture: \(String(describing: errorMessage))", + error: error) + giniConfiguration.errorLogger.handleErrorLog(error: errorLog) + assertionFailure("There was an error while capturing a picture") + return + } + + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + + let imageDocument = GiniImageDocument(data: imageData, + imageSource: .camera, + deviceOrientation: UIApplication.shared.statusBarOrientation) + didPick(imageDocument) + } + + private func previewCapturedImageView(with image: UIImage) -> UIImageView { + let imageFrame = cameraPreviewViewController.view.frame + let imageView = UIImageView(frame: imageFrame) + imageView.center = cameraPreviewViewController.view.center + imageView.image = image + imageView.layer.shadowColor = UIColor.black.cgColor + imageView.layer.shadowOffset = CGSize(width: -2, height: 2) + imageView.layer.shadowRadius = 4 + imageView.layer.shadowOpacity = 0.3 + imageView.layer.shadowPath = UIBezierPath(rect: imageView.bounds).cgPath + + return imageView + } +} + +// MARK: - CameraPreviewViewControllerDelegate + +extension CameraViewController: CameraPreviewViewControllerDelegate { + func notAuthorized() { + cameraButtonsViewController.captureButton.isEnabled = false + } + + + func cameraDidSetUp(_ viewController: CameraPreviewViewController, camera: CameraProtocol) { + if let tooltip = fileImportToolTipView, tooltip.isHidden == false { + } else { + cameraButtonsViewController.toggleCaptureButtonActivation(state: true) + } + cameraButtonsViewController.isFlashSupported = camera.isFlashSupported + cameraButtonsViewController.view.setNeedsLayout() + cameraButtonsViewController.view.layoutIfNeeded() + cameraPreviewViewController.updatePreviewViewOrientation() + } + + func cameraPreview(_ viewController: CameraPreviewViewController, didDetect qrCodeDocument: GiniQRCodeDocument) { + dismissQRCodeTooltip() + if detectedQRCodeDocument != qrCodeDocument { + detectedQRCodeDocument = qrCodeDocument + showPopup(forQRDetected: qrCodeDocument) { [weak self] in + guard let self = self else { return } + self.didPick(qrCodeDocument) + } + } + } + + fileprivate func dismissQRCodeTooltip() { + if let tooltip = qrCodeToolTipView, !tooltip.isHidden { + qrCodeToolTipView?.dismiss() + } + } + + func cameraPreview(_ viewController: CameraPreviewViewController, didDetectInvalid qrCodeDocument: GiniQRCodeDocument) { + dismissQRCodeTooltip() + if detectedQRCodeDocument != qrCodeDocument { + detectedQRCodeDocument = qrCodeDocument + showPopup(forUnsupportedQR: qrCodeDocument) + } + } +} + +// MARK: - CameraButtonsViewControllerDelegate + +extension CameraViewController: CameraButtonsViewControllerDelegate { + func cameraButtons(_ viewController: CameraButtonsViewController, + didTapOn button: CameraButtonsViewController.Button) { + switch button { + case let .flashToggle(isOn): + cameraPreviewViewController.isFlashOn = isOn + case .fileImport: + if let tooltip = fileImportToolTipView, tooltip.isHidden == false { + showImportFileSheet() + } else { + if let fileImportToolTipView = self.fileImportToolTipView, ToolTipView.shouldShowFileImportToolTip { + shouldShowQRCodeNext = true + fileImportToolTipView.dismiss(withCompletion: nil) + self.fileImportToolTipView = nil + } else { + showImportFileSheet() + } + } + case .capture: + if let qrToolTip = qrCodeToolTipView, !qrToolTip.isHidden { + qrCodeToolTipView?.dismiss(withCompletion: nil) + qrCodeToolTipView = nil + } + trackingDelegate?.onCameraScreenEvent(event: Event(type: .takePicture)) + cameraPreviewViewController.captureImage { [weak self] data, error in + guard let self = self else { return } + self.cameraDidCapture(imageData: data, error: error) + viewController.toggleCaptureButtonActivation(state: true) + } + + case .imagesStack: + delegate?.cameraDidTapMultipageReviewButton(self) + } + } +} + +// MARK: - Document import + +extension CameraViewController { + func addValidationLoadingView() -> UIView { + let loadingIndicator = UIActivityIndicatorView(style: .whiteLarge) + let blurredView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + blurredView.alpha = 0 + blurredView.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin] + loadingIndicator.startAnimating() + blurredView.contentView.addSubview(loadingIndicator) + self.view.addSubview(blurredView) + blurredView.frame = self.view.bounds + loadingIndicator.center = blurredView.center + UIView.animate(withDuration: AnimationDuration.medium, animations: { + blurredView.alpha = 1 + }) + + return blurredView + } + + @objc fileprivate func showImportFileSheet() { + if let tooltip = fileImportToolTipView, !tooltip.isHidden { fileImportToolTipView?.dismiss(withCompletion: nil) + } + + let alertViewController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + var alertViewControllerMessage: String = .localized(resource: CameraStrings.popupTitleImportPDF) + + if giniConfiguration.fileImportSupportedTypes == .pdf_and_images { + alertViewController.addAction(UIAlertAction(title: .localized(resource: CameraStrings.popupOptionPhotos), + style: .default) { [unowned self] _ in + self.delegate?.camera(self, didSelect: .gallery) + }) + alertViewControllerMessage = .localized(resource: CameraStrings.popupTitleImportPDForPhotos) + } + + alertViewController.addAction(UIAlertAction(title: .localized(resource: CameraStrings.popupOptionFiles), + style: .default) { [unowned self] _ in + self.delegate?.camera(self, didSelect: .explorer) + }) + + alertViewController.addAction(UIAlertAction(title: .localized(resource: CameraStrings.popupCancel), + style: .cancel, handler: nil)) + + alertViewController.message = alertViewControllerMessage + alertViewController.popoverPresentationController?.sourceView = cameraButtonsViewController.fileImportButtonView + + self.present(alertViewController, animated: true, completion: nil) + } + + fileprivate func createFileImportTip(giniConfiguration: GiniConfiguration) { + opaqueView = OpaqueViewFactory.create(with: giniConfiguration.toolTipOpaqueBackgroundStyle) + opaqueView?.alpha = 0 + self.view.addSubview(opaqueView!) + + fileImportToolTipView = ToolTipView(text: .localized(resource: CameraStrings.fileImportTipLabel), + giniConfiguration: giniConfiguration, + referenceView: cameraButtonsViewController + .fileImportButtonView.importFileButton.imageView ?? cameraButtonsViewController + .fileImportButtonView.importFileButton, + superView: self.view, + position: UIDevice.current.isIpad ? .left : .above, + distanceToRefView: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)) + + fileImportToolTipView?.willDismiss = { [weak self] in + guard let self = self else { return } + self.opaqueView?.removeFromSuperview() + self.fileImportToolTipView = nil + if !ToolTipView.shouldShowFileImportToolTip && ToolTipView.shouldShowQRCodeToolTip && self.shouldShowQRCodeNext { + self.configureCameraWhenTooltipDismissed() + self.showQrCodeTip() + } else { + self.configureCameraWhenTooltipDismissed() + } + } + fileImportToolTipView?.willDismissOnCloseButtonTap = { [weak self] in + guard let self = self else { return } + self.opaqueView?.removeFromSuperview() + self.fileImportToolTipView = nil + if !ToolTipView.shouldShowFileImportToolTip && ToolTipView.shouldShowQRCodeToolTip { + self.configureCameraWhenTooltipDismissed() + self.showQrCodeTip() + } else { + self.configureCameraWhenTooltipDismissed() + } + } + } + + fileprivate func configureCameraWhenTooltipDismissed() { + let isFlashOn = giniConfiguration.flashOnByDefault + cameraButtonsViewController.captureButton.isEnabled = true + cameraButtonsViewController.captureButton.isUserInteractionEnabled = true + cameraButtonsViewController.flashToggleButton.isEnabled = true + cameraButtonsViewController.flashToggleButton.isSelected = isFlashOn + cameraButtonsViewController.fileImportButtonView.importFileButton.isEnabled = true + cameraButtonsViewController.fileImportButtonView.importFileSubtitleLabel.isEnabled = true + cameraButtonsViewController.fileImportButtonView.isUserInteractionEnabled = true + } + + fileprivate func createQRCodeTip(giniConfiguration: GiniConfiguration) { + + qrCodeToolTipView = ToolTipView(text: .localized(resource: CameraStrings.qrCodeTipLabel), + giniConfiguration: giniConfiguration, + referenceView: cameraButtonsViewController + .captureButton, + superView: self.view, + position: UIDevice.current.isIpad ? .left : .above, + distanceToRefView: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)) + + qrCodeToolTipView?.willDismiss = { [weak self] in + guard let self = self else { return } + self.configureCameraWhenTooltipDismissed() + } + + qrCodeToolTipView?.willDismissOnCloseButtonTap = { [weak self] in + guard let self = self else { return } + self.configureCameraWhenTooltipDismissed() + } + + } + /** + Handle tooltip dismiss on tap outside. + */ + override public func touchesBegan(_ touches: Set, with event: UIEvent?) { + let touch = touches.first + if let fileImportTooltip = self.fileImportToolTipView, touch?.view != fileImportTooltip && !fileImportTooltip.isHidden { + fileImportToolTipView?.dismiss { + if !ToolTipView.shouldShowFileImportToolTip && ToolTipView.shouldShowQRCodeToolTip { + self.showQrCodeTip() + self.fileImportToolTipView = nil + } + } + } else if let qrTooltip = self.qrCodeToolTipView, touch?.view != qrTooltip && !qrTooltip.isHidden { + qrCodeToolTipView?.dismiss() + qrCodeToolTipView = nil + } + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CapturedImagesStackView.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CapturedImagesStackView.swift new file mode 100644 index 0000000..e92ac55 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CapturedImagesStackView.swift @@ -0,0 +1,196 @@ +// +// CapturedImagesStackView.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 4/25/18. +// + +import UIKit + +final class CapturedImagesStackView: UIView { + + enum State { + case filled(count: Int, lastImage: UIImage), empty + } + + let thumbnailSize: CGSize = { + if UIDevice.current.isIpad { + return CGSize(width: 40, height: 60) + } else { + return CGSize(width: 30, height: 45) + } + }() + var didTapImageStackButton: (() -> Void)? + fileprivate let giniConfiguration: GiniConfiguration + fileprivate let stackCountCircleSize = CGSize(width: 25, height: 25) + fileprivate var imagesCount: Int = 0 + + lazy var thumbnailButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.imageView?.contentMode = .scaleAspectFill + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowRadius = 1 + button.layer.shadowOpacity = 0.5 + button.layer.shadowOffset = CGSize(width: -2, height: 2) + button.addTarget(self, action: #selector(thumbnailButtonAction), for: .touchUpInside) + button.layer.shadowPath = UIBezierPath(rect: button.bounds).cgPath + + return button + }() + + lazy var thumbnailStackBackgroundView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .lightGray + view.isHidden = true + return view + }() + + lazy var stackIndicatorLabel: UILabel = { + var label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = giniConfiguration.imagesStackIndicatorLabelTextcolor + return label + }() + + fileprivate lazy var stackIndicatorCircleView: UIView = { + var view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.frame.size = self.stackCountCircleSize + view.backgroundColor = .white + view.layer.cornerRadius = self.stackCountCircleSize.width / 2 + view.layer.shadowRadius = 1 + view.layer.shadowOpacity = 0.3 + view.layer.shadowOffset = CGSize(width: -1, height: 1) + view.layer.shadowPath = UIBezierPath(rect: view.bounds).cgPath + return view + }() + + fileprivate lazy var capturedImagesStackSubtitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = .localized(resource: CameraStrings.capturedImagesStackSubtitleLabel) + label.textAlignment = .center + label.textColor = .white + label.font = giniConfiguration.customFont.with(weight: .regular, size: 12, style: .footnote) + label.numberOfLines = 0 + label.minimumScaleFactor = 10 / label.font.pointSize + label.adjustsFontSizeToFitWidth = true + + return label + }() + + init(giniConfiguration: GiniConfiguration = .shared) { + self.giniConfiguration = giniConfiguration + super.init(frame: .zero) + addSubview(thumbnailStackBackgroundView) + addSubview(thumbnailButton) + addSubview(stackIndicatorCircleView) + addSubview(capturedImagesStackSubtitleLabel) + stackIndicatorCircleView.addSubview(stackIndicatorLabel) + addConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("Use init(frame:) initializer instead") + } + +} + +// MARK: - Public methods + +extension CapturedImagesStackView { + /** + Return the thumbnail frame relative to a given view coordinate space. + Used to make a transition from the thumbnail to a new screen. + */ + func thumbnailFrameRelative(to view: UIView) -> CGRect { + return convert(thumbnailButton.frame, to: view) + } + + func replaceStackImages(with images: [UIImage]) { + if let lastImage = images.last { + updateStackStatus(to: .filled(count: images.count, lastImage: lastImage)) + } else { + updateStackStatus(to: .empty) + } + } + + func addImageToStack(image: UIImage) { + updateStackStatus(to: .filled(count: imagesCount + 1, lastImage: image)) + } + + private func updateStackStatus(to status: State) { + switch status { + case .filled(let count, let lastImage): + imagesCount = count + thumbnailStackBackgroundView.isHidden = count < 2 + thumbnailButton.setImage(lastImage, for: .normal) + isHidden = false + case .empty: + imagesCount = 0 + thumbnailStackBackgroundView.isHidden = true + thumbnailButton.setImage(nil, for: .normal) + isHidden = true + } + + stackIndicatorLabel.text = "\(imagesCount)" + } +} + +// MARK: - Private methods + +extension CapturedImagesStackView { + @objc fileprivate func thumbnailButtonAction() { + didTapImageStackButton?() + } + + fileprivate func addConstraints() { + // multipageReviewButton + Constraints.active(item: thumbnailButton, attr: .centerX, relatedBy: .equal, + to: self, attr: .centerX) + Constraints.active(item: thumbnailButton, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: thumbnailSize.height, priority: 999) + Constraints.active(item: thumbnailButton, attr: .width, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: thumbnailSize.width, priority: 999) + + // multipageReviewBackgroundView + Constraints.active(item: thumbnailStackBackgroundView, attr: .centerY, relatedBy: .equal, + to: thumbnailButton, attr: .centerY, constant: 3) + Constraints.active(item: thumbnailStackBackgroundView, attr: .centerX, relatedBy: .equal, + to: thumbnailButton, attr: .centerX, constant: -3) + Constraints.active(item: thumbnailStackBackgroundView, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: thumbnailSize.height, priority: 999) + Constraints.active(item: thumbnailStackBackgroundView, attr: .width, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: thumbnailSize.width, priority: 999) + + // stackIndicatorCircleView + Constraints.active(item: stackIndicatorCircleView, attr: .trailing, relatedBy: .equal, + to: thumbnailButton, attr: .trailing, constant: stackCountCircleSize.height / 2) + Constraints.active(item: stackIndicatorCircleView, attr: .top, relatedBy: .equal, + to: thumbnailButton, attr: .top, constant: -stackCountCircleSize.height / 2) + Constraints.active(item: stackIndicatorCircleView, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: stackCountCircleSize.height) + Constraints.active(item: stackIndicatorCircleView, attr: .width, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: stackCountCircleSize.width) + Constraints.active(item: stackIndicatorCircleView, attr: .top, relatedBy: .greaterThanOrEqual, + to: self, attr: .top, constant: 10) + // stackIndicatorLabel + Constraints.active(item: stackIndicatorLabel, attr: .centerX, relatedBy: .equal, + to: stackIndicatorCircleView, attr: .centerX) + Constraints.active(item: stackIndicatorLabel, attr: .centerY, relatedBy: .equal, + to: stackIndicatorCircleView, attr: .centerY) + + // capturedImagesStackSubtitleLabel + Constraints.active(item: capturedImagesStackSubtitleLabel, attr: .bottom, relatedBy: .equal, + to: self, attr: .bottom) + Constraints.active(item: capturedImagesStackSubtitleLabel, attr: .top, relatedBy: .equal, + to: thumbnailStackBackgroundView, attr: .bottom, constant: 4, priority: 999) + Constraints.active(item: capturedImagesStackSubtitleLabel, attr: .leading, relatedBy: .equal, + to: self, attr: .leading) + Constraints.active(item: capturedImagesStackSubtitleLabel, attr: .trailing, relatedBy: .equal, + to: self, attr: .trailing) + + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraBottomNavigationBarAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraBottomNavigationBarAdapter.swift deleted file mode 100644 index 2ef8ba2..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraBottomNavigationBarAdapter.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// CameraBottomNavigationBarAdapter.swift -// -// -// Created by Krzysztof Kryniecki on 26/09/2022. -// - -import UIKit - -/** -Protocol for injecting a custom bottom navigation bar on the camera screen. - -- note: Bottom navigation only. -*/ -public protocol CameraBottomNavigationBarAdapter: InjectedViewAdapter { - /** - * Called when the displayed buttons have to change. Show only the buttons that are in the list. - * - * - Parameter navigationBar: The navigation bar that holds buttons - * - Parameter navigationButtons: The list of buttons that have to be shown - */ - func showButtons( - navigationBar: UIView, - navigationButtons: [CameraNavigationBarBottomButton]) - /** - * Set the callback for the help button action. - * - * - Parameter callback: An action callback, which should be retained and called in help button action method - */ - func setHelpButtonClickedActionCallback(_ callback: @escaping () -> Void) - /** - * Set the callback for the back button action. - * - * - Parameter callback: An action callback, which should be retained and called in back button action method - */ - func setBackButtonClickedActionCallback(_ callback: @escaping () -> Void) -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraScreen.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraScreen.swift deleted file mode 100644 index 1c40707..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraScreen.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CameraScreen.swift -// -// -// Created by Krzysztof Kryniecki on 12/09/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -@objc public protocol CameraScreen: CameraTips where Self: UIViewController { - weak var delegate: CameraViewControllerDelegate? {get set} - func setupCamera() - func addValidationLoadingView() -> UIView - func replaceCapturedStackImages(with images: [UIImage]) -} - -@objc public protocol CameraTips { - func hideCaptureButton() - func showCaptureButton() -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraViewControllerDelegate.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraViewControllerDelegate.swift deleted file mode 100644 index 69ee0a7..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Protocols/CameraViewControllerDelegate.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// CameraViewControllerDelegate.swift -// -// -// Created by Krzysztof Kryniecki on 12/09/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -/** - The CameraViewControllerDelegate protocol defines methods that allow you to handle captured images and user - actions. - - - note: Component API only. - */ -@objc public protocol CameraViewControllerDelegate: AnyObject { - /** - Called when a user takes a picture, imports a PDF/QRCode or imports one or several images. - Once the method has been implemented, it is necessary to check if the number of - documents accumulated doesn't exceed the minimun (`GiniImageDocument.maxPagesCount`). - - - parameter viewController: `CameraScreenViewController` where the documents were taken. - - parameter document: One or several documents either captured or imported in - the `CameraViewController`. They can contain an error produced in the validation process. - */ - @objc func camera(_ viewController: CameraScreen, - didCapture document: GiniCaptureDocument) - - /** - Called when a user selects a picker from the picker selector sheet. - - - parameter viewController: `CameraViewController` where the documents were taken. - - parameter documentPicker: `DocumentPickerType` selected in the sheet. - */ - @objc func camera(_ viewController: CameraScreen, didSelect documentPicker: DocumentPickerType) - - /** - Called when the `CameraViewController` appears. - - - parameter viewController: Camera view controller that appears. - */ - @objc func cameraDidAppear(_ viewController: CameraScreen) - - /** - Called when a user taps the `MultipageReviewButton` (the one with the thumbnail of the images(s) taken). - Once this method is called, the `MultipageReviewViewController` should be presented. - - - parameter viewController: Camera view controller where the button was tapped. - */ - @objc func cameraDidTapReviewButton(_ viewController: CameraScreen) - -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/QRCodeDetectedPopupView.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/QRCodeDetectedPopupView.swift new file mode 100644 index 0000000..a8ee26b --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/QRCodeDetectedPopupView.swift @@ -0,0 +1,172 @@ +// +// QRCodeDetectedPopupView.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 12/6/17. +// + +import UIKit + +final class QRCodeDetectedPopupView: UIView { + + let maxWidth: CGFloat = 375.0 + let margin: (left: CGFloat, right: CGFloat, top: CGFloat, bottom: CGFloat) = (10, 10, 10, 10) + let padding: (left: CGFloat, right: CGFloat, top: CGFloat, bottom: CGFloat) = (20, 20, 10, 10) + let imageSize: CGSize = CGSize(width: 35, height: 35) + let hiddingDelay: TimeInterval = 10.0 + var bottomConstraint: NSLayoutConstraint? + var didTapDone: (() -> Void) = {} + + lazy var qrImage: UIImageView = { + let imageView = UIImageView(image: UIImageNamedPreferred(named: "toolTipCloseButton")) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + lazy var qrText: UILabel = { + let message: String = .localized(resource: CameraStrings.qrCodeDetectedPopupMessage) + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = message + label.numberOfLines = 2 + label.minimumScaleFactor = 10/14 + label.adjustsFontSizeToFitWidth = true + return label + }() + + lazy var proceedButton: UIButton = { + let title: String = .localized(resource: CameraStrings.qrCodeDetectedPopupButton) + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(title, for: .normal) + button.addTarget(self, action: #selector(self.didTapDoneAction), for: .touchUpInside) + return button + }() + + init(parent: UIView, refView: UIView, document: GiniQRCodeDocument, giniConfiguration: GiniConfiguration) { + super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + addShadow() + + qrImage.image = document.previewImage + setupViews(with: giniConfiguration) + + parent.insertSubview(self, aboveSubview: refView) + addSubview(qrImage) + addSubview(qrText) + addSubview(proceedButton) + addConstraints(onSuperView: parent, refView: refView) + + layoutIfNeeded() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + fileprivate func setupViews(with giniConfiguration: GiniConfiguration) { + backgroundColor = UIColor.from(giniColor: giniConfiguration.qrCodePopupBackgroundColor) + qrText.font = giniConfiguration.customFont.with(weight: .regular, size: 14, style: .body) + qrText.textColor = UIColor.from(giniColor: giniConfiguration.qrCodePopupTextColor) + proceedButton.titleLabel?.font = giniConfiguration.customFont.with(weight: .bold, size: 14, style: .caption1) + proceedButton.setTitleColor(giniConfiguration.qrCodePopupButtonColor, for: .normal) + proceedButton.setTitleColor(giniConfiguration.qrCodePopupButtonColor.withAlphaComponent(0.5), + for: .highlighted) + } + + fileprivate func addConstraints(onSuperView superView: UIView, refView: UIView) { + Constraints.active(item: self, attr: .width, relatedBy: .lessThanOrEqual, to: nil, attr: .notAnAttribute, + constant: maxWidth) + Constraints.active(item: self, attr: .leading, relatedBy: .greaterThanOrEqual, to: superView, attr: .leading, + constant: margin.left) + Constraints.active(item: self, attr: .trailing, relatedBy: .lessThanOrEqual, to: superView, attr: .trailing, + constant: -margin.right) + Constraints.active(item: self, attr: .centerX, relatedBy: .equal, to: refView, attr: .centerX) + bottomConstraint = NSLayoutConstraint(item: self, + attribute: .bottom, + relatedBy: .equal, + toItem: refView, + attribute: .bottom, + multiplier: 1.0, + constant: imageSize.height + + padding.top + + padding.bottom + + margin.bottom) + Constraints.active(constraint: bottomConstraint!) + + Constraints.active(item: qrImage, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: imageSize.width) + Constraints.active(item: qrImage, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: imageSize.height) + Constraints.active(item: qrImage, attr: .leading, relatedBy: .equal, to: self, attr: .leading, + constant: padding.left) + Constraints.active(item: qrImage, attr: .top, relatedBy: .equal, to: self, attr: .top, constant: padding.top) + Constraints.active(item: qrImage, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, + constant: -padding.bottom) + Constraints.active(item: qrImage, attr: .trailing, relatedBy: .equal, to: qrText, attr: .leading, + constant: -padding.right / 2, priority: 999) + + Constraints.active(item: qrText, attr: .centerY, relatedBy: .equal, to: qrImage, attr: .centerY) + Constraints.active(item: qrText, attr: .trailing, relatedBy: .lessThanOrEqual, to: proceedButton, + attr: .leading, constant: -padding.right) + Constraints.active(item: proceedButton, attr: .centerY, relatedBy: .equal, to: qrImage, attr: .centerY) + Constraints.active(item: proceedButton, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, + constant: -padding.right) + if let minButtonWidth = proceedButton.titleLabel?.intrinsicContentSize.width { + Constraints.active(item: proceedButton, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: minButtonWidth) + } + + } + + fileprivate func addShadow() { + self.layer.shadowOffset = CGSize(width: 0, height: 2) + self.layer.shadowRadius = 0.8 + self.layer.shadowOpacity = 0.2 + self.layer.shadowColor = UIColor.black.cgColor + self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath + } + + @objc fileprivate func didTapDoneAction() { + didTapDone() + } +} + +// MARK: - Animations + +extension QRCodeDetectedPopupView { + func show(after seconds: TimeInterval = 0, didDismiss: (() -> Void)? = nil) { + DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: { [weak self] in + guard let self = self, let superview = self.superview else { return } + self.bottomConstraint?.constant = -self.margin.bottom + UIView.animate(withDuration: AnimationDuration.medium, + delay: 0, + options: [.curveEaseInOut], + animations: { + superview.layoutIfNeeded() + }, completion: { [weak self] _ in + guard let self = self else { return } + self.hide(after: self.hiddingDelay, completion: didDismiss) + }) + }) + } + + func hide(after seconds: TimeInterval = 0, completion: (() -> Void)? = nil) { + DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: { [weak self] in + guard let self = self, let superview = self.superview else { return } + self.bottomConstraint?.constant = self.imageSize.height + + self.padding.top + + self.padding.bottom + + self.margin.bottom + UIView.animate(withDuration: AnimationDuration.medium, + delay: 0, + options: [.curveEaseInOut], + animations: { + superview.layoutIfNeeded() + }, completion: { [weak self] _ in + self?.removeFromSuperview() + completion?() + }) + }) + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraBottomNavigationBar.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraBottomNavigationBar.swift deleted file mode 100644 index c902ee9..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraBottomNavigationBar.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// CameraBottomNavigationBar.swift -// -// -// Created by Krzysztof Kryniecki on 26/09/2022. -// - -import UIKit - -class CameraBottomNavigationBar: UIView { - - @IBOutlet weak var rightButton: UIButton! - @IBOutlet weak var leftButton: UIButton! - - override func awakeFromNib() { - super.awakeFromNib() - setupView() - } - - func setupView() { - let configuration = GiniConfiguration.shared - let textColor = UIColor.GiniCapture.accent1 - let font = configuration.textStyleFonts[.body] ?? UIFont.systemFont(ofSize: 20) - let rightButtonTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.camera.help", - comment: "Camera Help Button") - let rightAttributedString = NSAttributedString(string: rightButtonTitle, - attributes: [NSAttributedString.Key.font: font, - NSAttributedString.Key.foregroundColor: textColor]) - rightButton.setAttributedTitle(rightAttributedString, for: .normal) - rightButton.tintColor = GiniColor(light: UIColor.GiniCapture.dark1, dark: UIColor.GiniCapture.light1).uiColor() - - let leftButtonTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.analysis.backToReview", - comment: "Review screen title") - let leftAttributedString = NSAttributedString(string: leftButtonTitle, - attributes: [NSAttributedString.Key.font: font, - NSAttributedString.Key.foregroundColor: textColor]) - leftButton.setAttributedTitle(leftAttributedString, for: .normal) - - if #available(iOS 15.0, *) { - leftButton.configuration?.imagePadding = 4 - } else { - leftButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 0) - } - - leftButton.setImage( - UIImageNamedPreferred(named: "arrowBack")?.tintedImageWithColor(textColor) ?? UIImage(), - for: .normal) - backgroundColor = GiniColor(light: UIColor.GiniCapture.light1, dark: UIColor.GiniCapture.dark1).uiColor() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraNotAuthorizedView.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraNotAuthorizedView.swift deleted file mode 100644 index 297261b..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraNotAuthorizedView.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// CameraNotAuthorizedView.swift -// GiniCapture -// -// Created by Peter Pult on 06/07/16. -// Copyright © 2016 Gini GmbH. All rights reserved. -// - -import UIKit -import AVFoundation - -final class CameraNotAuthorizedView: UIView { - // User interface - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.text = NSLocalizedStringPreferredFormat("ginicapture.camera.notAuthorized.title", - comment: "Not authorized title") - label.numberOfLines = 0 - label.textColor = GiniColor(light: UIColor.GiniCapture.dark1, dark: UIColor.GiniCapture.light1).uiColor() - label.textAlignment = .center - label.font = configuration.textStyleFonts[.title2] - return label - }() - - private lazy var descriptionLabel: UILabel = { - let label = UILabel() - label.text = NSLocalizedStringPreferredFormat("ginicapture.camera.notAuthorized.description", - comment: "Not authorized description") - label.numberOfLines = 0 - label.textColor = UIColor.GiniCapture.dark7 - label.textAlignment = .center - label.font = configuration.textStyleFonts[.headline] - return label - }() - - private lazy var imageView: UIImageView = { - let imageView = UIImageView() - imageView.image = UIImageNamedPreferred(named: "cameraNotAuthorizedIcon") - imageView.contentMode = .scaleAspectFit - return imageView - }() - - private lazy var button: UIButton = { - let button = UIButton() - button.setTitle(NSLocalizedStringPreferredFormat("ginicapture.camera.notAuthorizedButton.noStatus", - comment: "Grant permission"), for: .normal) - button.setTitleColor(GiniColor(light: UIColor.GiniCapture.accent1, - dark: UIColor.GiniCapture.accent1).uiColor(), for: .normal) - button.setTitleColor(GiniColor(light: UIColor.GiniCapture.accent1, - dark: UIColor.GiniCapture.accent1).uiColor().withAlphaComponent(0.8), - for: .highlighted) - button.titleLabel?.font = configuration.textStyleFonts[.subheadline] - button.addTarget(self, action: #selector(openSettings), for: .touchUpInside) - return button - }() - - private var contentView = UIView() - - private let configuration = GiniConfiguration.shared - - init() { - super.init(frame: CGRect.zero) - - backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - - // Configure view hierachy - addSubview(contentView) - contentView.addSubview(imageView) - contentView.addSubview(titleLabel) - contentView.addSubview(descriptionLabel) - addSubview(button) - - // Add constraints - addConstraints() - } - - /** - Returns an object initialized from data in a given unarchiver. - - - warning: Not implemented. - */ - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc - private func openSettings() { - UIApplication.shared.openAppSettings() - } - - // MARK: Constraints - fileprivate func addConstraints() { - contentView.translatesAutoresizingMaskIntoConstraints = false - imageView.translatesAutoresizingMaskIntoConstraints = false - titleLabel.translatesAutoresizingMaskIntoConstraints = false - descriptionLabel.translatesAutoresizingMaskIntoConstraints = false - button.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - contentView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: Constants.padding * 2), - contentView.bottomAnchor.constraint(equalTo: centerYAnchor), - contentView.centerXAnchor.constraint(equalTo: centerXAnchor), - contentView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding), - // Image view - imageView.topAnchor.constraint(equalTo: contentView.topAnchor), - imageView.widthAnchor.constraint(equalToConstant: Constants.imageSize.width), - imageView.heightAnchor.constraint(equalToConstant: Constants.imageSize.height), - imageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - // titleLabel - titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: Constants.padding * 2), - titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - titleLabel.widthAnchor.constraint(equalTo: contentView.widthAnchor, - multiplier: Constants.widthCoefficient), - // descriptionLabel - descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: Constants.padding), - descriptionLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - descriptionLabel.widthAnchor.constraint(equalTo: contentView.widthAnchor, - multiplier: Constants.widthCoefficient), - descriptionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - // button - button.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, - constant: -Constants.padding * 2), - button.widthAnchor.constraint(greaterThanOrEqualTo: descriptionLabel.widthAnchor), - button.heightAnchor.constraint(equalToConstant: Constants.padding * 4), - button.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: Constants.padding), - button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)]) - } -} - -extension CameraNotAuthorizedView { - private enum Constants { - static let padding: CGFloat = 16 - static let widthCoefficient: CGFloat = 0.7 - static let imageSize: CGSize = CGSize(width: 50, height: 50) - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraPane.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraPane.swift deleted file mode 100644 index f9ff88f..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraPane.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// CameraPane.swift -// -// -// Created by Krzysztof Kryniecki on 14/09/2022. -// - -import UIKit - -final class CameraPane: UIView { - @IBOutlet weak var cameraTitleLabel: UILabel! - @IBOutlet weak var captureButton: UIButton! - @IBOutlet weak var fileUploadButton: BottomLabelButton! - @IBOutlet weak var flashButton: BottomLabelButton! - @IBOutlet weak var thumbnailView: ThumbnailView! - @IBOutlet weak var leftButtonsStack: UIView! - @IBOutlet weak var thumbnailConstraint: NSLayoutConstraint! - @IBOutlet weak var leftStackViewMargin: NSLayoutConstraint! - - private var shouldShowFlashButton: Bool = false - override func awakeFromNib() { - super.awakeFromNib() - setupView() - } - - func setupView() { - let giniConfiguration = GiniConfiguration.shared - backgroundColor = GiniColor( - light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.dark1).uiColor().withAlphaComponent(0.4) - captureButton.setTitle("", for: .normal) - thumbnailView.isHidden = true - fileUploadButton.setupButton(with: UIImageNamedPreferred(named: "folder") ?? UIImage(), - name: NSLocalizedStringPreferredFormat("ginicapture.camera.fileImportButtonLabel", - comment: "Upload file button title")) - flashButton.setupButton(with: UIImageNamedPreferred(named: "flashOff") ?? UIImage(), - name: NSLocalizedStringPreferredFormat("ginicapture.camera.flashButtonLabel", - comment: "Flash button title")) - - flashButton.actionLabel.font = giniConfiguration.textStyleFonts[.caption1] - fileUploadButton.actionLabel.font = giniConfiguration.textStyleFonts[.caption1] - - flashButton.configure(with: giniConfiguration.cameraControlButtonConfiguration) - fileUploadButton.configure(with: giniConfiguration.cameraControlButtonConfiguration) - - if cameraTitleLabel != nil { - configureTitle(giniConfiguration: giniConfiguration) - } - captureButton.accessibilityLabel = "" - captureButton.accessibilityValue = NSLocalizedStringPreferredFormat( - "ginicapture.camera.capturebutton", - comment: "Capture") - } - - private func configureTitle(giniConfiguration: GiniConfiguration) { - cameraTitleLabel.text = NSLocalizedStringPreferredFormat( - "ginicapture.camera.infoLabel", - comment: "Info label") - cameraTitleLabel.adjustsFontForContentSizeCategory = true - cameraTitleLabel.adjustsFontSizeToFitWidth = true - cameraTitleLabel.numberOfLines = 1 - cameraTitleLabel.minimumScaleFactor = 5/UIFont.labelFontSize - cameraTitleLabel.font = giniConfiguration.textStyleFonts[.footnote] - cameraTitleLabel.textColor = GiniColor( - light: UIColor.GiniCapture.light1, - dark: UIColor.GiniCapture.light1).uiColor() - } - - func setupFlashButton(state: Bool) { - if state { - flashButton.setupButton(with: UIImageNamedPreferred(named: "flashOn") ?? UIImage(), - name: NSLocalizedStringPreferredFormat("ginicapture.camera.flashButtonLabel.On", - comment: "Flash button on voice-over title")) - flashButton.accessibilityValue = NSLocalizedStringPreferredFormat( - "ginicapture.camera.flashButtonLabel.On.Voice.Over", - comment: "Flash button voice over") - } else { - flashButton.setupButton(with: UIImageNamedPreferred(named: "flashOff") ?? UIImage(), - name: NSLocalizedStringPreferredFormat("ginicapture.camera.flashButtonLabel.Off", - comment: "Flash button title")) - flashButton.accessibilityValue = NSLocalizedStringPreferredFormat( - "ginicapture.camera.flashButtonLabel.Off.Voice.Over", - comment: "Flash button off voice over") - } - } - - func toggleFlashButtonActivation(state: Bool) { - shouldShowFlashButton = state - flashButton.isHidden = !state - } - - func toggleCaptureButtonActivation(state: Bool) { - captureButton.isUserInteractionEnabled = state - captureButton.isEnabled = state - } - - func setupAuthorization(isHidden: Bool) { - let giniConfiguration = GiniConfiguration.shared - self.isHidden = isHidden - - captureButton.isHidden = isHidden - if shouldShowFlashButton { - flashButton.isHidden = isHidden - } - if cameraTitleLabel != nil { - cameraTitleLabel.isHidden = isHidden - } - if giniConfiguration.fileImportSupportedTypes != .none { - fileUploadButton.isHidden = isHidden - } - if thumbnailView.thumbnailImageView.image != nil { - thumbnailView.isHidden = isHidden - } - } - -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraPreviewView.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraPreviewView.swift deleted file mode 100644 index 116e647..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/CameraPreviewView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CameraPreviewView.swift -// GiniCapture -// -// Created by Peter Pult / Nikola Sobadjiev on 14/06/16. -// Copyright © 2016 Gini GmbH. All rights reserved. -// - -import UIKit -import AVFoundation - -final class CameraPreviewView: UIView { - override class var layerClass: AnyClass { - return AVCaptureVideoPreviewLayer.self - } - - var session: AVCaptureSession { - get { - return (self.layer as? AVCaptureVideoPreviewLayer)!.session! - } - set(newSession) { - if let captureLayer = layer as? AVCaptureVideoPreviewLayer { - captureLayer.videoGravity = .resizeAspectFill - } - (self.layer as? AVCaptureVideoPreviewLayer)!.session = newSession - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/DefaultCameraBottomNavigationBarAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/DefaultCameraBottomNavigationBarAdapter.swift deleted file mode 100644 index 1f755bc..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/DefaultCameraBottomNavigationBarAdapter.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// CameraBottomNavigationBarAdapter.swift -// -// -// Created by Krzysztof Kryniecki on 26/09/2022. -// - -import UIKit - -public enum CameraNavigationBarBottomButton { - case back - case help -} - -class DefaultCameraBottomNavigationBarAdapter: CameraBottomNavigationBarAdapter { - - private var helpButtonCallback: (() -> Void)? - private var backButtonCallback: (() -> Void)? - - func showButtons(navigationBar: UIView, navigationButtons: [CameraNavigationBarBottomButton]) { - if let navigationView = navigationBar as? CameraBottomNavigationBar { - if navigationButtons.contains(.help) { - navigationView.rightButton.isHidden = false - } else { - navigationView.rightButton.isHidden = true - } - if navigationButtons.contains(.back) { - navigationView.leftButton.isHidden = false - } else { - navigationView.leftButton.isHidden = true - } - } - } - - // Add the callback whenever the help button is clicked - func setHelpButtonClickedActionCallback(_ callback: @escaping () -> Void) { - helpButtonCallback = callback - } - - // Add the callback whenever the back button is clicked - func setBackButtonClickedActionCallback(_ callback: @escaping () -> Void) { - backButtonCallback = callback - } - - func injectedView() -> UIView { - if let navigationBarView = - CameraBottomNavigationBar().loadNib() as? - CameraBottomNavigationBar { - navigationBarView.rightButton.addTarget( - self, - action: #selector(helpButtonClicked), - for: .touchUpInside) - navigationBarView.leftButton.addTarget( - self, - action: #selector(backButtonClicked), - for: .touchUpInside) - return navigationBarView - } - return UIView() - } - - @objc func helpButtonClicked() { - helpButtonCallback?() - } - - @objc func backButtonClicked() { - backButtonCallback?() - } - - func onDeinit() { - helpButtonCallback = nil - backButtonCallback = nil - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/QRCodeOverlay.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/QRCodeOverlay.swift deleted file mode 100644 index accc710..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/QRCodeOverlay.swift +++ /dev/null @@ -1,272 +0,0 @@ -// -// QRCodeOverlay.swift -// -// -// Created by David Vizaknai on 01.11.2022. -// - -import UIKit - -final class CorrectQRCodeTextContainer: UIView { - private let configuration = GiniConfiguration.shared - - lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = configuration.textStyleFonts[.caption2] - label.textAlignment = .center - label.textColor = .GiniCapture.light1 - label.text = NSLocalizedStringPreferredFormat("ginicapture.QRscanning.correct", - comment: "QR Detected") - return label - }() - - init() { - super.init(frame: .zero) - backgroundColor = .GiniCapture.success2 - addSubview(titleLabel) - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: Constants.spacing / 2), - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.spacing) - ]) - } -} - -final class IncorrectQRCodeTextContainer: UIView { - private let configuration = GiniConfiguration.shared - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.font = configuration.textStyleFonts[.footnoteBold] - label.textColor = .GiniCapture.dark1 - label.text = NSLocalizedStringPreferredFormat("ginicapture.QRscanning.incorrect.title", - comment: "Unknown QR") - return label - }() - - private lazy var descriptionLabel: UILabel = { - let label = UILabel() - label.font = configuration.textStyleFonts[.footnote] - label.textColor = .GiniCapture.dark1 - label.numberOfLines = 0 - label.text = NSLocalizedStringPreferredFormat("ginicapture.QRscanning.incorrect.description", - comment: "No content") - return label - }() - - private lazy var textStackView: UIStackView = { - let textStackView = UIStackView() - textStackView.axis = .vertical - textStackView.distribution = .fillProportionally - textStackView.spacing = Constants.spacing - textStackView.translatesAutoresizingMaskIntoConstraints = false - return textStackView - }() - - init() { - super.init(frame: .zero) - - backgroundColor = .GiniCapture.warning3 - addSubview(textStackView) - textStackView.addArrangedSubview(titleLabel) - textStackView.addArrangedSubview(descriptionLabel) - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - textStackView.centerXAnchor.constraint(equalTo: centerXAnchor), - textStackView.centerYAnchor.constraint(equalTo: centerYAnchor), - textStackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.spacing * 2), - textStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.spacing * 2) - ]) - } -} - -final class QRCodeOverlay: UIView { - private let configuration = GiniConfiguration.shared - - private lazy var correctQRFeedback: CorrectQRCodeTextContainer = { - let view = CorrectQRCodeTextContainer() - view.layer.cornerRadius = Constants.spacing - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - return view - }() - - private lazy var incorrectQRFeedback: IncorrectQRCodeTextContainer = { - let view = IncorrectQRCodeTextContainer() - view.layer.cornerRadius = Constants.spacing - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - return view - }() - - private lazy var checkMarkImageView: UIImageView = { - let imageView = UIImageView() - imageView.image = UIImageNamedPreferred(named: "greenCheckMark") - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.isHidden = true - return imageView - }() - - private lazy var loadingIndicatorView: UIActivityIndicatorView = { - let indicatorView = UIActivityIndicatorView() - indicatorView.hidesWhenStopped = true - indicatorView.style = .whiteLarge - return indicatorView - }() - - private lazy var loadingIndicatorText: UILabel = { - var loadingIndicatorText = UILabel() - loadingIndicatorText.font = configuration.textStyleFonts[.bodyBold] - loadingIndicatorText.textAlignment = .center - loadingIndicatorText.adjustsFontForContentSizeCategory = true - loadingIndicatorText.textColor = .GiniCapture.light1 - loadingIndicatorText.isAccessibilityElement = true - loadingIndicatorText.text = NSLocalizedStringPreferredFormat("ginicapture.QRscanning.loading", - comment: "Retrievenig invoice") - return loadingIndicatorText - }() - - private lazy var loadingContainer: UIStackView = { - let textStackView = UIStackView() - textStackView.axis = .vertical - textStackView.distribution = .fillProportionally - textStackView.spacing = Constants.spacing * 2 - textStackView.translatesAutoresizingMaskIntoConstraints = false - textStackView.isHidden = true - return textStackView - }() - - init() { - super.init(frame: .zero) - addSubview(correctQRFeedback) - addSubview(checkMarkImageView) - addSubview(incorrectQRFeedback) - - addLoadingView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func addLoadingView() { - let loadingIndicator: UIView - - if let customLoadingIndicator = configuration.customLoadingIndicator?.injectedView() { - loadingIndicator = customLoadingIndicator - } else { - loadingIndicator = loadingIndicatorView - } - - addSubview(loadingContainer) - loadingContainer.addArrangedSubview(loadingIndicator) - loadingContainer.addArrangedSubview(loadingIndicatorText) - } - - func layoutViews(centeringBy cameraFrame: UIView) { - layoutCorrectQRCode(centeringBy: cameraFrame) - layoutIncorrectQRCode(centeringBy: cameraFrame) - layoutLoadingIndicator(centeringBy: cameraFrame) - } - - private func layoutCorrectQRCode(centeringBy cameraFrame: UIView) { - NSLayoutConstraint.activate([ - correctQRFeedback.centerXAnchor.constraint(equalTo: cameraFrame.centerXAnchor), - correctQRFeedback.centerYAnchor.constraint(equalTo: cameraFrame.topAnchor), - correctQRFeedback.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, - constant: Constants.spacing * 2), - - checkMarkImageView.centerXAnchor.constraint(equalTo: cameraFrame.centerXAnchor), - checkMarkImageView.centerYAnchor.constraint(equalTo: cameraFrame.centerYAnchor), - checkMarkImageView.heightAnchor.constraint(equalToConstant: 56), - checkMarkImageView.widthAnchor.constraint(equalToConstant: 56) - ]) - } - - private func layoutIncorrectQRCode(centeringBy cameraFrame: UIView) { - NSLayoutConstraint.activate([ - incorrectQRFeedback.topAnchor.constraint(equalTo: cameraFrame.topAnchor, constant: Constants.spacing), - incorrectQRFeedback.leadingAnchor.constraint(equalTo: cameraFrame.leadingAnchor, - constant: Constants.spacing), - incorrectQRFeedback.trailingAnchor.constraint(equalTo: cameraFrame.trailingAnchor, - constant: -Constants.spacing) - ]) - } - - private func layoutLoadingIndicator(centeringBy cameraFrame: UIView) { - NSLayoutConstraint.activate([ - loadingContainer.centerXAnchor.constraint(equalTo: cameraFrame.centerXAnchor), - loadingContainer.centerYAnchor.constraint(equalTo: cameraFrame.centerYAnchor), - loadingContainer.leadingAnchor.constraint(equalTo: cameraFrame.leadingAnchor), - loadingContainer.topAnchor.constraint(greaterThanOrEqualTo: cameraFrame.topAnchor) - ]) - } - - func configureQrCodeOverlay(withCorrectQrCode isQrCodeCorrect: Bool) { - if isQrCodeCorrect { - backgroundColor = .GiniCapture.dark3.withAlphaComponent(0.8) - correctQRFeedback.isHidden = false - checkMarkImageView.isHidden = false - incorrectQRFeedback.isHidden = true - } else { - backgroundColor = .clear - correctQRFeedback.isHidden = true - checkMarkImageView.isHidden = true - incorrectQRFeedback.isHidden = false - } - } - - func viewWillDisappear() { - hideAnimation() - } - - // MARK: Toggle animation - /** - Displays a loading activity indicator. Should be called when invoice retrieving is started. - */ - public func showAnimation() { - checkMarkImageView.isHidden = true - loadingContainer.isHidden = false - - if let loadingIndicator = configuration.customLoadingIndicator { - loadingIndicator.startAnimation() - } else { - loadingIndicatorView.startAnimating() - } - } - - /** - Hides the loading activity indicator. Should be called when invoice retrieving is finished. - */ - public func hideAnimation() { - checkMarkImageView.isHidden = true - loadingContainer.isHidden = true - - if let loadingIndicator = configuration.customLoadingIndicator { - loadingIndicator.stopAnimation() - } else { - loadingIndicatorView.stopAnimating() - } - } -} - -private enum Constants { - static let spacing: CGFloat = 8 -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/ThumbnailView.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/ThumbnailView.swift deleted file mode 100644 index 51bba85..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/ThumbnailView.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// ThumbnailView.swift -// -// -// Created by Krzysztof Kryniecki on 07/09/2022. -// - -import UIKit - -final class ThumbnailView: UIView { - - enum State { - case filled(count: Int, lastImage: UIImage), empty - } - - var didTapImageStackButton: (() -> Void)? - let thumbnailSize = CGSize(width: 44, height: 52) - private var giniConfiguration: GiniConfiguration = .shared - private let stackCountCircleSize = CGSize(width: 20, height: 20) - private var imagesCount: Int = 0 - - lazy var thumbnailButton: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(thumbnailButtonAction), for: .touchUpInside) - return button - }() - - lazy var thumbnailImageView: UIImageView = { - let imageView = UIImageView() - imageView.backgroundColor = UIColor.white - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - lazy var stackIndicatorLabel: UILabel = { - var label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = true - label.textAlignment = .center - label.font = giniConfiguration.textStyleFonts[.caption1] - label.textColor = GiniColor(light: UIColor.GiniCapture.light1, dark: UIColor.GiniCapture.light1).uiColor() - label.adjustsFontSizeToFitWidth = true - label.text = "" - return label - }() - - fileprivate lazy var stackIndicatorCircleView: UIView = { - var view = UIView(frame: .zero) - view.translatesAutoresizingMaskIntoConstraints = false - view.frame.size = self.stackCountCircleSize - view.layer.cornerRadius = stackCountCircleSize.height * 0.5 - view.layer.borderWidth = 1 - view.layer.borderColor = UIColor.GiniCapture.dark5.cgColor - view.backgroundColor = GiniColor( - light: UIColor.GiniCapture.accent1, - dark: UIColor.GiniCapture.accent1).uiColor() - return view - }() - - required init?(coder aDecoder: NSCoder) { - super.init(frame: .zero) - } - - override func awakeFromNib() { - super.awakeFromNib() - setupView() - isAccessibilityElement = true - accessibilityTraits = .button - } - - private func setupView() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(thumbnailImageView) - addSubview(stackIndicatorCircleView) - addSubview(thumbnailButton) - stackIndicatorCircleView.addSubview(stackIndicatorLabel) - addConstraints() - } - - func configureView(giniConfiguration: GiniConfiguration) { - self.giniConfiguration = giniConfiguration - } - - func addImageToStack(image: UIImage) { - updateStackStatus(to: .filled(count: imagesCount + 1, lastImage: image)) - } - - func updateStackStatus(to status: State) { - switch status { - case .filled(let count, let lastImage): - imagesCount = count - thumbnailImageView.image = lastImage - isHidden = false - accessibilityValue = NSLocalizedStringPreferredFormat( - "ginicapture.camera.thumbnail.Voice.Over", - comment: "Thumbnail button") + " \(count)" - case .empty: - imagesCount = 0 - thumbnailImageView.image = nil - isHidden = true - accessibilityValue = NSLocalizedStringPreferredFormat( - "ginicapture.camera.thumbnail.Voice.Over", - comment: "Thumbnail button") + " 0" - } - - stackIndicatorLabel.text = "\(imagesCount)" - } - - @objc fileprivate func thumbnailButtonAction() { - didTapImageStackButton?() - } - - private func addConstraints() { - NSLayoutConstraint.activate([ - thumbnailButton.topAnchor.constraint(equalTo: topAnchor), - thumbnailButton.trailingAnchor.constraint(equalTo: trailingAnchor), - thumbnailButton.leadingAnchor.constraint(equalTo: leadingAnchor), - thumbnailButton.bottomAnchor.constraint(equalTo: bottomAnchor), - thumbnailImageView.topAnchor.constraint(equalTo: topAnchor, constant: 5), - thumbnailImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -9), - thumbnailImageView.leadingAnchor.constraint(equalTo: leadingAnchor), - thumbnailImageView.bottomAnchor.constraint(equalTo: bottomAnchor), - - stackIndicatorCircleView.topAnchor.constraint(equalTo: topAnchor), - stackIndicatorCircleView.trailingAnchor.constraint(equalTo: trailingAnchor), - stackIndicatorCircleView.heightAnchor.constraint(equalToConstant: stackCountCircleSize.height), - stackIndicatorCircleView.widthAnchor.constraint(equalToConstant: stackCountCircleSize.width), - stackIndicatorLabel.leadingAnchor.constraint(equalTo: stackIndicatorCircleView.leadingAnchor), - stackIndicatorLabel.trailingAnchor.constraint(equalTo: stackIndicatorCircleView.trailingAnchor), - stackIndicatorLabel.topAnchor.constraint(equalTo: stackIndicatorCircleView.topAnchor), - stackIndicatorLabel.bottomAnchor.constraint(equalTo: stackIndicatorCircleView.bottomAnchor), - heightAnchor.constraint(equalToConstant: thumbnailSize.height), - widthAnchor.constraint(equalToConstant: thumbnailSize.width) - ]) - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/DocumentPickerCoordinator.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/DocumentPickerCoordinator.swift index 3ebaa05..f71d067 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/DocumentPickerCoordinator.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/DocumentPickerCoordinator.swift @@ -227,25 +227,24 @@ public final class DocumentPickerCoordinator: NSObject { // MARK: - Fileprivate methods fileprivate extension DocumentPickerCoordinator { - func createDocument(fromData dataDictionary: (Data?, String?)) -> GiniCaptureDocument? { - guard let data = dataDictionary.0 else { return nil } + func createDocument(fromData data: Data) -> GiniCaptureDocument? { let documentBuilder = GiniCaptureDocumentBuilder(documentSource: .external) documentBuilder.importMethod = .picker - return documentBuilder.build(with: data, fileName: dataDictionary.1) + return documentBuilder.build(with: data) } - func data(fromUrl url: URL) -> (Data?, String?) { + func data(fromUrl url: URL) -> Data? { do { _ = url.startAccessingSecurityScopedResource() let data = try Data(contentsOf: url) url.stopAccessingSecurityScopedResource() - return (data, url.lastPathComponent) + return data } catch { url.stopAccessingSecurityScopedResource() } - return (nil, nil) + return nil } func saveCurrentNavBarAppearance() { @@ -326,7 +325,7 @@ extension DocumentPickerCoordinator: UIDocumentPickerDelegate { return } - if #available(iOS 12.0, *), giniConfiguration.documentPickerNavigationBarTintColor != nil { + if #available(iOS 11.0, *), giniConfiguration.documentPickerNavigationBarTintColor != nil { restoreSavedNavBarAppearance() } @@ -338,7 +337,7 @@ extension DocumentPickerCoordinator: UIDocumentPickerDelegate { } public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { - if #available(iOS 12.0, *), giniConfiguration.documentPickerNavigationBarTintColor != nil { + if #available(iOS 11.0, *), giniConfiguration.documentPickerNavigationBarTintColor != nil { restoreSavedNavBarAppearance() } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsFooterView.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsFooterView.swift index 860d4af..8f0312c 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsFooterView.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsFooterView.swift @@ -13,17 +13,16 @@ final class AlbumsFooterView: UIView { label.translatesAutoresizingMaskIntoConstraints = false label.numberOfLines = 0 let configuration = GiniConfiguration.shared - let titleString = NSLocalizedStringPreferredFormat("ginicapture.albums.footer", - comment: "Albums footer message") - - label.isAccessibilityElement = true - label.text = titleString - label.accessibilityValue = titleString - label.font = configuration.textStyleFonts[.footnote] - label.textColor = GiniColor(light: .GiniCapture.dark1, dark: .GiniCapture.light1).uiColor() + label.text = NSLocalizedStringPreferredFormat("ginicapture.albums.footer", + comment: "Albums footer message") + label.font = configuration.customFont.with(weight: .regular, size: 14, style: .footnote) + if #available(iOS 13.0, *) { + label.textColor = .label + } else { + label.textColor = .black + } label.textAlignment = .center label.lineBreakMode = .byWordWrapping - label.adjustsFontForContentSizeCategory = true return label }() @@ -38,8 +37,7 @@ final class AlbumsFooterView: UIView { fileprivate func setupConstraints() { // Hack to fix AutoLayout bug related to UIView-Encapsulated-Layout-Width - let leadingContraint = contentLabel.leadingAnchor.constraint(equalTo: leadingAnchor, - constant: Constants.padding) + let leadingContraint = contentLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) leadingContraint.priority = .defaultHigh // Hack to fix AutoLayout bug related to UIView-Encapsulated-Layout-Height @@ -49,7 +47,7 @@ final class AlbumsFooterView: UIView { NSLayoutConstraint.activate([ leadingContraint, topConstraint, - contentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding), + contentLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), contentLabel.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), ]) } @@ -59,9 +57,3 @@ final class AlbumsFooterView: UIView { setupConstraints() } } - -extension AlbumsFooterView { - private enum Constants { - static let padding: CGFloat = 16 - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsHeaderView.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsHeaderView.swift index 34df61a..a466700 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsHeaderView.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsHeaderView.swift @@ -19,13 +19,10 @@ final class AlbumsHeaderView: UITableViewHeaderFooterView { let configuration = GiniConfiguration.shared let buttonTitle = NSLocalizedStringPreferredFormat("ginicapture.albums.selectMorePhotosButton", comment: "Title for select more photos button") - selectPhotosButton.titleLabel?.font = configuration.textStyleFonts[.footnote] + selectPhotosButton.titleLabel?.font = configuration.customFont.with(weight: .regular, size: 16, style: .footnote) selectPhotosButton.setTitle(buttonTitle, for: .normal) - selectPhotosButton.titleLabel?.isAccessibilityElement = true - selectPhotosButton.titleLabel?.accessibilityValue = buttonTitle - selectPhotosButton.setTitleColor(.GiniCapture.accent1, for: .normal) + selectPhotosButton.setTitleColor(UIColor.from(giniColor: configuration.albumsScreenSelectMorePhotosTextColor), for: .normal) selectPhotosButton.sizeToFit() - selectPhotosButton.titleLabel?.adjustsFontForContentSizeCategory = true } override init(reuseIdentifier: String?) { diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerTableViewCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerTableViewCell.swift index fc9da4f..d2af2c1 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerTableViewCell.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerTableViewCell.swift @@ -10,6 +10,8 @@ import UIKit final class AlbumsPickerTableViewCell: UITableViewCell { static let identifier = "AlbumsPickerTableViewCellIdentifier" + static let height: CGFloat = 90.0 + let padding = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) lazy var albumThumbnailView: UIImageView = { let imageView = UIImageView(frame: .zero) @@ -21,60 +23,65 @@ final class AlbumsPickerTableViewCell: UITableViewCell { imageView.layer.shadowOpacity = 0.5 imageView.layer.shadowOffset = CGSize(width: -2, height: 2) imageView.layer.shadowPath = UIBezierPath(rect: imageView.bounds).cgPath - imageView.layer.cornerRadius = 8 return imageView }() lazy var albumTitleLabel: UILabel = { - let albumTitleLabel = UILabel(frame: .zero) - albumTitleLabel.translatesAutoresizingMaskIntoConstraints = false - albumTitleLabel.textColor = GiniColor(light: .GiniCapture.dark1, dark: .GiniCapture.light1).uiColor() - albumTitleLabel.isAccessibilityElement = true - albumTitleLabel.adjustsFontForContentSizeCategory = true - return albumTitleLabel + let albumTitle = UILabel(frame: .zero) + albumTitle.translatesAutoresizingMaskIntoConstraints = false + + return albumTitle }() lazy var albumSubTitleLabel: UILabel = { - let albumSubTitleLabel = UILabel(frame: .zero) - albumSubTitleLabel.translatesAutoresizingMaskIntoConstraints = false - albumSubTitleLabel.isAccessibilityElement = true - albumSubTitleLabel.adjustsFontForContentSizeCategory = true - albumSubTitleLabel.textColor = GiniColor(light: .GiniCapture.dark6, dark: .GiniCapture.light6).uiColor() - return albumSubTitleLabel + let albumSubTitle = UILabel(frame: .zero) + albumSubTitle.translatesAutoresizingMaskIntoConstraints = false + + if #available(iOS 13.0, *) { + albumSubTitle.textColor = .secondaryLabel + } else { + albumSubTitle.textColor = .lightGray + } + + return albumSubTitle }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) accessoryType = .disclosureIndicator - contentView.addSubview(albumThumbnailView) - contentView.addSubview(albumTitleLabel) - contentView.addSubview(albumSubTitleLabel) - - backgroundColor = .clear + addSubview(albumThumbnailView) + addSubview(albumTitleLabel) + addSubview(albumSubTitleLabel) addConstraints() } private func addConstraints() { - NSLayoutConstraint.activate([ - albumThumbnailView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.paddingBig), - albumThumbnailView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: Constants.padding), - albumThumbnailView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Constants.padding), - albumThumbnailView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - albumThumbnailView.heightAnchor.constraint(equalToConstant: Constants.imageSize.height), - albumThumbnailView.widthAnchor.constraint(equalTo: albumThumbnailView.heightAnchor), - - albumTitleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: Constants.padding), - albumTitleLabel.leadingAnchor.constraint(equalTo: albumThumbnailView.trailingAnchor, constant: Constants.paddingBig), - albumTitleLabel.bottomAnchor.constraint(equalTo: contentView.centerYAnchor, constant: -Constants.paddingHalf), - albumTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.paddingHalf), - - albumSubTitleLabel.topAnchor.constraint(equalTo: contentView.centerYAnchor, constant: Constants.paddingHalf), - albumSubTitleLabel.leadingAnchor.constraint(equalTo: albumTitleLabel.leadingAnchor), - albumSubTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.paddingBig), - albumSubTitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Constants.padding) - - ]) + // albumThumbnailView + Constraints.active(item: albumThumbnailView, attr: .leading, relatedBy: .equal, to: self, attr: .leading, + constant: padding.left) + Constraints.active(item: albumThumbnailView, attr: .top, relatedBy: .equal, to: self, attr: .top, constant: + padding.top) + Constraints.active(item: albumThumbnailView, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, + constant: -padding.bottom) + Constraints.active(item: albumThumbnailView, attr: .trailing, relatedBy: .equal, to: albumTitleLabel, + attr: .leading, constant: -20) + Constraints.active(item: albumThumbnailView, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 70) + Constraints.active(item: albumThumbnailView, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 70) + + // albumTitleLabel + Constraints.active(item: albumTitleLabel, attr: .centerY, relatedBy: .equal, to: self, attr: .centerY, + constant: -padding.top) + Constraints.active(item: albumTitleLabel, attr: .bottom, relatedBy: .equal, to: albumSubTitleLabel, + attr: .top, constant: -5) + + // albumSubTitleLabel + Constraints.active(item: albumSubTitleLabel, attr: .bottom, relatedBy: .lessThanOrEqual, to: self, + attr: .bottom, constant: 0) + Constraints.active(item: albumSubTitleLabel, attr: .leading, relatedBy: .equal, to: albumTitleLabel, + attr: .leading) } required init?(coder aDecoder: NSCoder) { @@ -84,12 +91,8 @@ final class AlbumsPickerTableViewCell: UITableViewCell { func setUp(with album: Album, giniConfiguration: GiniConfiguration, galleryManager: GalleryManagerProtocol) { albumTitleLabel.text = album.title albumSubTitleLabel.text = "\(album.count)" - - albumTitleLabel.accessibilityValue = album.title - albumSubTitleLabel.accessibilityValue = "\(album.count)" - - albumTitleLabel.font = giniConfiguration.textStyleFonts[.headline] - albumSubTitleLabel.font = giniConfiguration.textStyleFonts[.subheadline] + albumTitleLabel.font = giniConfiguration.customFont.with(weight: .regular, size: 16, style: .headline) + albumSubTitleLabel.font = giniConfiguration.customFont.with(weight: .regular, size: 12, style: .subheadline) let asset = album.assets[album.assets.count - 1] galleryManager.fetchImage(from: asset, @@ -99,12 +102,3 @@ final class AlbumsPickerTableViewCell: UITableViewCell { } } } - -extension AlbumsPickerTableViewCell { - private enum Constants { - static let imageSize: CGSize = CGSize(width: 70, height: 70) - static let paddingHalf: CGFloat = 4 - static let padding: CGFloat = 8 - static let paddingBig: CGFloat = 16 - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerViewController.swift index 691efc6..cfabec1 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/AlbumsPickerViewController.swift @@ -22,14 +22,6 @@ final class AlbumsPickerViewController: UIViewController, PHPhotoLibraryChangeOb fileprivate let footerHeight: CGFloat = 50.0 fileprivate let headerIdentifier = "AlbumsHeaderView" - var tableViewContentHeight: CGFloat { - albumsTableView.layoutIfNeeded() - - var height = albumsTableView.contentSize.height - height += Constants.padding * 2 // adding the content inset - return height - } - // MARK: - Views lazy var albumsTableView: UITableView = { @@ -37,12 +29,14 @@ final class AlbumsPickerViewController: UIViewController, PHPhotoLibraryChangeOb tableView.translatesAutoresizingMaskIntoConstraints = false tableView.dataSource = self tableView.delegate = self - tableView.backgroundColor = GiniColor(light: .GiniCapture.light1, dark: .GiniCapture.dark1).uiColor() + + if #available(iOS 13.0, *) { + tableView.backgroundColor = Colors.Gini.dynamicPearl + } else { + tableView.backgroundColor = Colors.Gini.pearl + } tableView.register(AlbumsPickerTableViewCell.self, forCellReuseIdentifier: AlbumsPickerTableViewCell.identifier) - tableView.layer.cornerRadius = Constants.cornerRadius - tableView.contentInset = UIEdgeInsets(top: Constants.padding, left: 0, bottom: Constants.padding, right: 0) - tableView.separatorColor = GiniColor(light: .GiniCapture.light4, dark: .GiniCapture.dark6).uiColor() return tableView }() @@ -63,28 +57,16 @@ final class AlbumsPickerViewController: UIViewController, PHPhotoLibraryChangeOb override func loadView() { super.loadView() - title = NSLocalizedStringPreferredFormat("ginicapture.albums.title", comment: "Albums") - view.backgroundColor = GiniColor(light: .GiniCapture.light2, dark: .GiniCapture.dark2).uiColor() + title = .localized(resource: GalleryStrings.albumsTitle) setupTableView() } - private lazy var tableViewHeightAnchor = albumsTableView.heightAnchor.constraint(equalToConstant: tableViewContentHeight) func setupTableView() { if #available(iOS 15.0, *) { albumsTableView.sectionHeaderTopPadding = 0 } view.addSubview(albumsTableView) - - tableViewHeightAnchor.priority = .defaultHigh - - NSLayoutConstraint.activate([ - albumsTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.padding * 2), - albumsTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.padding * 2), - albumsTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.padding * 2), - albumsTableView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -Constants.padding * 2), - tableViewHeightAnchor - ]) + Constraints.pin(view: albumsTableView, toSuperView: view) } func reloadAlbums() { @@ -92,6 +74,9 @@ final class AlbumsPickerViewController: UIViewController, PHPhotoLibraryChangeOb } func showLimitedLibraryPicker() { + if #available(iOS 14.0, *) { + library.presentLimitedLibraryPicker(from: self) + } if #available(iOS 15.0, *) { library.presentLimitedLibraryPicker(from: self) { _ in DispatchQueue.main.async { @@ -99,11 +84,6 @@ final class AlbumsPickerViewController: UIViewController, PHPhotoLibraryChangeOb self.reloadAlbums() } } - return - } - - if #available(iOS 14.0, *) { - library.presentLimitedLibraryPicker(from: self) } } @@ -116,20 +96,15 @@ final class AlbumsPickerViewController: UIViewController, PHPhotoLibraryChangeOb albumsTableView.tableFooterView = footerView } } - - edgesForExtendedLayout = [] } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - if #available(iOS 14.0, *) { if galleryManager.isGalleryAccessLimited { self.updateLayoutForFooter() } } - tableViewHeightAnchor.constant = tableViewContentHeight - view.layoutIfNeeded() } fileprivate func updateLayoutForFooter(){ @@ -215,11 +190,8 @@ extension AlbumsPickerViewController: UITableViewDelegate { delegate?.albumsPicker(self, didSelectAlbum: galleryManager.albums[indexPath.row]) tableView.deselectRow(at: indexPath, animated: true) } -} -extension AlbumsPickerViewController { - private enum Constants { - static let padding: CGFloat = 8 - static let cornerRadius: CGFloat = 16 + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return AlbumsPickerTableViewCell.height } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/DefaultImagePickerBottomNavigationBarAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/DefaultImagePickerBottomNavigationBarAdapter.swift deleted file mode 100644 index 0baa960..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/DefaultImagePickerBottomNavigationBarAdapter.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// DefaultImagePickerBottomNavigationBarAdapter.swift -// -// -// Created by David Vizaknai on 13.01.2023. -// - -import UIKit - -final class DefaultImagePickerBottomNavigationBarAdapter: ImagePickerBottomNavigationBarAdapter { - private var backButtonCallback: (() -> Void)? - - // Add the callback whenever the back button is clicked - func setBackButtonClickedActionCallback(_ callback: @escaping () -> Void) { - backButtonCallback = callback - } - - func injectedView() -> UIView { - if let navigationBarView = BackButtonBottomNavigationBar().loadNib() as? BackButtonBottomNavigationBar { - navigationBarView.backButton.addTarget( - self, - action: #selector(backButtonClicked), - for: .touchUpInside) - return navigationBarView - } else { - return UIView() - } - } - - @objc func backButtonClicked() { - backButtonCallback?() - } - - func onDeinit() { - backButtonCallback = nil - } -} - diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/GalleryCoordinator.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/GalleryCoordinator.swift index d3e705a..e274617 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/GalleryCoordinator.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/GalleryCoordinator.swift @@ -22,8 +22,9 @@ final class GalleryCoordinator: NSObject, Coordinator { let galleryManager: GalleryManagerProtocol fileprivate(set) var selectedImageDocuments: [(assetId: String, imageDocument: GiniImageDocument)] = [] { didSet { - let button = selectedImageDocuments.isEmpty ? cancelButton : openImagesButton - currentImagePickerViewController?.navigationItem.setRightBarButton(button, animated: true) + currentImagePickerViewController? + .navigationItem + .setRightBarButton(selectedImageDocuments.isEmpty ? cancelButton : openImagesButton, animated: true) } } @@ -45,9 +46,7 @@ final class GalleryCoordinator: NSObject, Coordinator { lazy fileprivate(set) var galleryNavigator: UINavigationController = { let navController = UINavigationController(rootViewController: self.albumsController) - if giniConfiguration.customNavigationController == nil { - navController.applyStyle(withConfiguration: self.giniConfiguration) - } + navController.applyStyle(withConfiguration: self.giniConfiguration) navController.delegate = self return navController }() @@ -55,28 +54,35 @@ final class GalleryCoordinator: NSObject, Coordinator { lazy fileprivate(set) var albumsController: AlbumsPickerViewController = { let albumsPickerVC = AlbumsPickerViewController(galleryManager: self.galleryManager) albumsPickerVC.delegate = self - if giniConfiguration.bottomNavigationBarEnabled { - albumsPickerVC.navigationItem.rightBarButtonItem = self.cancelButton - } else { - albumsPickerVC.navigationItem.leftBarButtonItem = self.cancelButton - } + albumsPickerVC.navigationItem.rightBarButtonItem = self.cancelButton return albumsPickerVC }() fileprivate(set) var currentImagePickerViewController: ImagePickerViewController? // MARK: - Navigation bar buttons - - lazy var cancelButton: UIBarButtonItem = { - let cancelButton = GiniBarButton(ofType: .cancel) - cancelButton.addAction(self, #selector(cancelAction)) - return cancelButton.barButton - }() - + + lazy var cancelButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, + target: self, + action: #selector(cancelAction)) + lazy var openImagesButton: UIBarButtonItem = { - let openButton = GiniBarButton(ofType: .done) - openButton.addAction(self, #selector(openImages)) - return openButton.barButton + let button = UIButton(type: UIButton.ButtonType.custom) + button.addTarget(self, action: #selector(openImages), for: .touchUpInside) + button.frame.size = CGSize(width: 50, height: 20) + button.titleLabel?.textColor = giniConfiguration.navigationBarItemTintColor + + let currentFont = button.titleLabel?.font + let fontSize = currentFont?.pointSize ?? 18 + let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize)] + let openLocalizedString: String = .localized(resource: GalleryStrings.imagePickerOpenButton) + let attributedString = NSMutableAttributedString(string: openLocalizedString, + attributes: attributes) + button.setAttributedTitle(attributedString, for: .normal) + button.titleLabel?.adjustsFontSizeToFitWidth = true + button.titleLabel?.minimumScaleFactor = 14/fontSize + + return UIBarButtonItem(customView: button) }() // MARK: - Initializer @@ -119,22 +125,18 @@ final class GalleryCoordinator: NSObject, Coordinator { } // MARK: - Bar button actions - @objc func cancelAction() { + + @objc fileprivate func cancelAction() { selectedImageDocuments = [] delegate?.gallery(self, didCancel: ()) } - @objc func openImages() { + @objc fileprivate func openImages() { DispatchQueue.main.async { let imageDocuments: [GiniImageDocument] = self.selectedImageDocuments.map { $0.imageDocument } self.delegate?.gallery(self, didSelectImageDocuments: imageDocuments) } } - - @objc - private func backAction() { - galleryNavigator.popViewController(animated: true) - } // MARK: - Image picker generation. @@ -144,14 +146,6 @@ final class GalleryCoordinator: NSObject, Coordinator { giniConfiguration: giniConfiguration) imagePickerViewController.delegate = self imagePickerViewController.navigationItem.rightBarButtonItem = cancelButton - imagePickerViewController.navigationItem.setHidesBackButton(true, animated: false) - if !giniConfiguration.bottomNavigationBarEnabled { - let buttonTitle = NSLocalizedStringPreferredFormat("ginicapture.images.backToAlbums", comment: "Albums") - let backButton = GiniBarButton(ofType: .back(title: buttonTitle)) - backButton.addAction(self, #selector(backAction)) - imagePickerViewController.navigationItem.leftBarButtonItem = backButton.barButton - } - return imagePickerViewController } @@ -231,8 +225,8 @@ extension GalleryCoordinator: UINavigationControllerDelegate { to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if let imagePicker = fromVC as? ImagePickerViewController { galleryManager.stopCachingImages(for: imagePicker.currentAlbum) - currentImagePickerViewController = nil selectedImageDocuments.removeAll() + currentImagePickerViewController = nil } return nil } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerBottomNavigationBarAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerBottomNavigationBarAdapter.swift deleted file mode 100644 index 667fd37..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerBottomNavigationBarAdapter.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ImagePickerBottomNavigationBarAdapter.swift.swift -// -// -// Created by David Vizaknai on 13.01.2023. -// - -import UIKit - -/** -Protocol for injecting a custom bottom navigation bar on the image picker screen. - -- note: Bottom navigation only. -*/ -public protocol ImagePickerBottomNavigationBarAdapter: InjectedViewAdapter { - - /** - * Set the callback for the back button action. - * - * - Parameter callback: An action callback, which should be retained and called in back button action method - */ - func setBackButtonClickedActionCallback(_ callback: @escaping () -> Void) -} - diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerCollectionViewCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerCollectionViewCell.swift index 41393d3..1d66989 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerCollectionViewCell.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerCollectionViewCell.swift @@ -30,18 +30,16 @@ final class ImagePickerCollectionViewCell: UICollectionViewCell { fileprivate lazy var selectedForegroundView: UIView = { var view = UIView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false - view.layer.borderColor = UIColor.GiniCapture.accent1.cgColor - view.layer.borderWidth = Constants.borderWidth - view.backgroundColor = .GiniCapture.accent1.withAlphaComponent(0.5) + view.backgroundColor = UIColor.white.withAlphaComponent(0.5) view.alpha = 0 return view }() lazy var checkImage: UIImageView = { - let image = UIImageNamedPreferred(named: "checkMarkBlue") + let image = UIImageNamedPreferred(named: "supportedFormatsIcon") var imageView = UIImageView(image: image) imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.isHidden = true + imageView.alpha = 0 return imageView }() @@ -50,10 +48,10 @@ final class ImagePickerCollectionViewCell: UICollectionViewCell { circleView.translatesAutoresizingMaskIntoConstraints = false circleView.layer.borderWidth = 1 circleView.layer.cornerRadius = self.selectedCircleSize.width / 2 - circleView.layer.borderColor = UIColor.GiniCapture.light1.cgColor + circleView.layer.borderColor = UIColor.white.cgColor return circleView }() - + var isProgramaticallySelected: Bool = false { didSet { selectedForegroundView.alpha = isProgramaticallySelected ? 1 : 0 @@ -73,49 +71,33 @@ final class ImagePickerCollectionViewCell: UICollectionViewCell { addSubview(selectedForegroundView) addSubview(checkCircleBackground) addSubview(activityIndicator) - addSubview(checkImage) - - setupConstraints() + checkCircleBackground.addSubview(checkImage) + + Constraints.center(view: activityIndicator, with: self) + Constraints.pin(view: galleryImage, toSuperView: self) + Constraints.pin(view: selectedForegroundView, toSuperView: self) + Constraints.active(item: checkCircleBackground, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, + constant: -5) + Constraints.active(item: checkCircleBackground, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, + constant: -5) + Constraints.active(item: checkCircleBackground, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: selectedCircleSize.width) + Constraints.active(item: checkCircleBackground, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: selectedCircleSize.height) + Constraints.center(view: checkImage, with: checkCircleBackground) + } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), - activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), - - galleryImage.topAnchor.constraint(equalTo: topAnchor), - galleryImage.leadingAnchor.constraint(equalTo: leadingAnchor), - galleryImage.centerXAnchor.constraint(equalTo: centerXAnchor), - galleryImage.centerYAnchor.constraint(equalTo: centerYAnchor), - - selectedForegroundView.topAnchor.constraint(equalTo: topAnchor), - selectedForegroundView.leadingAnchor.constraint(equalTo: leadingAnchor), - selectedForegroundView.centerXAnchor.constraint(equalTo: centerXAnchor), - selectedForegroundView.centerYAnchor.constraint(equalTo: centerYAnchor), - - checkCircleBackground.topAnchor.constraint(equalTo: topAnchor, constant: Constants.circlePadding), - checkCircleBackground.trailingAnchor.constraint(equalTo: trailingAnchor, - constant: -Constants.circlePadding), - checkCircleBackground.widthAnchor.constraint(equalToConstant: Constants.selectedCircleSize.width), - checkCircleBackground.heightAnchor.constraint(equalToConstant: Constants.selectedCircleSize.height), - - checkImage.topAnchor.constraint(equalTo: checkCircleBackground.topAnchor), - checkImage.leadingAnchor.constraint(equalTo: checkCircleBackground.leadingAnchor), - checkImage.centerXAnchor.constraint(equalTo: checkCircleBackground.centerXAnchor), - checkImage.centerYAnchor.constraint(equalTo: checkCircleBackground.centerYAnchor), - ]) - } func fill(withAsset asset: Asset, multipleSelectionEnabled: Bool, galleryManager: GalleryManagerProtocol, isDownloading: Bool, isSelected: Bool) { - checkCircleBackground.isHidden = !(multipleSelectionEnabled && !isDownloading) + checkCircleBackground.alpha = multipleSelectionEnabled && !isDownloading ? 1 : 0 activityIndicator.alpha = isDownloading ? 1 : 0 isProgramaticallySelected = isSelected selectedForegroundView.alpha = isSelected || isDownloading ? 1 : 0 @@ -145,15 +127,15 @@ final class ImagePickerCollectionViewCell: UICollectionViewCell { } func changeCheckCircle(to selected: Bool, giniConfiguration: GiniConfiguration = .shared) { - checkCircleBackground.isHidden = selected - checkImage.isHidden = !selected - } -} - -extension ImagePickerCollectionViewCell { - enum Constants { - static let borderWidth: CGFloat = 2 - static let selectedCircleSize = CGSize(width: 25, height: 25) - static let circlePadding: CGFloat = 5 + if selected { + checkCircleBackground.layer.borderColor = + giniConfiguration.galleryPickerItemSelectedBackgroundCheckColor.cgColor + checkCircleBackground.backgroundColor = giniConfiguration.galleryPickerItemSelectedBackgroundCheckColor + checkImage.alpha = 1 + } else { + checkCircleBackground.layer.borderColor = UIColor.white.cgColor + checkCircleBackground.backgroundColor = .clear + checkImage.alpha = 0 + } } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift index d373f9b..1c94a5f 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift @@ -26,8 +26,6 @@ final class ImagePickerViewController: UIViewController { fileprivate let galleryManager: GalleryManagerProtocol fileprivate let giniConfiguration: GiniConfiguration private var isInitialized: Bool = false - private var isLayoutDone: Bool = false - private var navigationBarBottomAdapter: ImagePickerBottomNavigationBarAdapter? // MARK: - Views @@ -46,12 +44,6 @@ final class ImagePickerViewController: UIViewController { forCellWithReuseIdentifier: ImagePickerCollectionViewCell.identifier) return collectionView }() - - private lazy var contentView: UIView = { - let contentView = UIView() - contentView.translatesAutoresizingMaskIntoConstraints = false - return contentView - }() // MARK: - Initializers @@ -70,47 +62,23 @@ final class ImagePickerViewController: UIViewController { // MARK: - UIViewController - override func viewDidLoad() { - super.viewDidLoad() - - setupView() - configureBottomNavigationBar() - setupConstraints() + override func loadView() { + super.loadView() + + title = currentAlbum.title + + view.backgroundColor = UIColor.from(giniColor: giniConfiguration.galleryScreenBackgroundColor) + + view.addSubview(collectionView) + + Constraints.pin(view: collectionView, toSuperView: view) } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - scrollToBottomOnStartup() - isLayoutDone = true - } - - private func setupView() { - title = currentAlbum.title - - view.backgroundColor = GiniColor(light: .GiniCapture.light2, dark: .GiniCapture.dark2).uiColor() - view.addSubview(contentView) - contentView.addSubview(collectionView) - } - - private func setupConstraints() { - let contentViewBottomConstraint = contentView.bottomAnchor.constraint( - greaterThanOrEqualTo: view.bottomAnchor) - contentViewBottomConstraint.priority = .defaultLow - - NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), - collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - - contentView.topAnchor.constraint(equalTo: view.topAnchor), - contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - contentViewBottomConstraint - ]) } - + // MARK: - Others func addToDownloadingItems(index: IndexPath, needsReloading: Bool = true) { @@ -150,7 +118,6 @@ final class ImagePickerViewController: UIViewController { } fileprivate func scrollToBottomOnStartup() { - guard isLayoutDone else { return } // This tweak is needed to fix an issue with the UICollectionView. UICollectionView doesn't // scroll to the bottom on `viewWillAppear`, which is right after `viewDidLayoutSubviews`. // Since this method can be called several times during the lifecycle, there should be @@ -163,42 +130,6 @@ final class ImagePickerViewController: UIViewController { animated: false) } } - - private func configureBottomNavigationBar() { - if giniConfiguration.bottomNavigationBarEnabled { - if let bottomBar = giniConfiguration.imagePickerNavigationBarBottomAdapter { - navigationBarBottomAdapter = bottomBar - } else { - navigationBarBottomAdapter = DefaultImagePickerBottomNavigationBarAdapter() - } - - navigationBarBottomAdapter?.setBackButtonClickedActionCallback { [weak self] in - self?.navigationController?.popViewController(animated: true) - } - - if let navigationBar = - navigationBarBottomAdapter?.injectedView() { - navigationBar.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(navigationBar) - - layoutBottomNavigationBar(navigationBar) - } - } - } - - private func layoutBottomNavigationBar(_ navigationBar: UIView) { - navigationBar.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(navigationBar) - NSLayoutConstraint.activate([ - contentView.bottomAnchor.constraint(equalTo: navigationBar.topAnchor), - navigationBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), - navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), - navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), - navigationBar.heightAnchor.constraint(equalToConstant: 114) - ]) - view.bringSubviewToFront(navigationBar) - view.layoutSubviews() - } } // MARK: UICollectionViewDataSource diff --git a/Sources/GiniCaptureSDK/Core/Screens/Error/ErrorScreenViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Error/ErrorScreenViewController.swift deleted file mode 100644 index cb78b8c..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Error/ErrorScreenViewController.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// ErrorScreenViewController.swift -// -// -// Created by Krzysztof Kryniecki on 21/11/2022. -// - -import UIKit - -class ErrorScreenViewController: UIViewController { - private var giniConfiguration: GiniConfiguration - lazy var errorHeader: IconHeader = { - if let header = IconHeader().loadNib() as? IconHeader { - header.headerLabel.adjustsFontForContentSizeCategory = true - header.headerLabel.adjustsFontSizeToFitWidth = true - header.translatesAutoresizingMaskIntoConstraints = false - return header - } - fatalError("Error header not found") - }() - - lazy var buttonsView: ButtonsView = { - let view = ButtonsView( - firstTitle: NSLocalizedStringPreferredFormat( - "ginicapture.error.enterManually", - comment: "Enter manually button title"), - secondTitle: NSLocalizedStringPreferredFormat( - "ginicapture.error.backToCamera", - comment: "Back to camera button title")) - view.translatesAutoresizingMaskIntoConstraints = false - view.enterButton.isHidden = viewModel.isEnterManuallyHidden() - view.retakeButton.isHidden = viewModel.isRetakePressedHidden() - return view - }() - - lazy var errorContent: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - label.isAccessibilityElement = true - return label - }() - - private lazy var scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.translatesAutoresizingMaskIntoConstraints = false - return scrollView - }() - - let viewModel: BottomButtonsViewModel - private let errorType: ErrorType - private let documentType: GiniCaptureDocumentType - private var buttonsHeightConstraint: NSLayoutConstraint? - private var numberOfButtons: Int { - return [ - viewModel.isEnterManuallyHidden(), - viewModel.isRetakePressedHidden() - ].filter({ - !$0 - }).count - } - - /** - Designated initializer for the `ErrorScreenViewController` which shows generic error screen - - - parameter giniConfiguration: `GiniConfiguration` instance. - - parameter type: `ErrorType` type of generic error. - - parameter viewModel: `BottomButtonsViewModel` provide actions for buttons . - - - returns: A view controller instance allowing the user to take a picture or pick a document. - */ - public init( - giniConfiguration: GiniConfiguration, - type: ErrorType, - documentType: GiniCaptureDocumentType, - viewModel: BottomButtonsViewModel - ) { - self.giniConfiguration = giniConfiguration - self.viewModel = viewModel - self.errorType = type - self.documentType = documentType - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - func setupView() { - title = NSLocalizedStringPreferredFormat( - "ginicapture.error.title", - comment: "Error screen title") - configureErrorHeader() - configureErrorContent() - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - view.addSubview(errorHeader) - view.addSubview(scrollView) - scrollView.addSubview(errorContent) - buttonsView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(buttonsView) - configureButtons() - configureCustomTopNavigationBar() - configureConstraints() - } - - private func configureErrorHeader() { - errorHeader.iconImageView.accessibilityLabel = NSLocalizedStringPreferredFormat( - "ginicapture.error.title", - comment: "Error screen title") - errorHeader.headerLabel.text = errorType.title() - errorHeader.headerLabel.font = giniConfiguration.textStyleFonts[.subheadline] - errorHeader.headerLabel.textColor = GiniColor( - light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1 - ).uiColor() - errorHeader.backgroundColor = GiniColor( - light: UIColor.GiniCapture.error4, - dark: UIColor.GiniCapture.error1 - ).uiColor() - errorHeader.iconImageView.image = UIImageNamedPreferred(named: errorType.iconName()) - } - - private func configureErrorContent() { - errorContent.text = errorType.content() - errorContent.font = giniConfiguration.textStyleFonts[.body] - errorContent.textColor = GiniColor(light: UIColor.GiniCapture.dark6, dark: UIColor.GiniCapture.dark7).uiColor() - } - - private func configureButtons() { - buttonsView.enterButton.addTarget( - viewModel, - action: #selector(viewModel.didPressEnterManually), - for: .touchUpInside) - buttonsView.retakeButton.addTarget( - viewModel, - action: #selector(viewModel.didPressRetake), - for: .touchUpInside) - } - - private func configureCustomTopNavigationBar() { - let cancelButton = GiniBarButton(ofType: .cancel) - cancelButton.addAction(viewModel, #selector(viewModel.didPressCancell)) - - if giniConfiguration.bottomNavigationBarEnabled { - navigationItem.rightBarButtonItem = cancelButton.barButton - - navigationItem.setHidesBackButton(true, animated: true) - } else { - navigationItem.leftBarButtonItem = cancelButton.barButton - } - } - - private func getButtonsMinHeight(numberOfButtons: Int) -> CGFloat { - if numberOfButtons == 1 { - return Constants.singleButtonHeight.rawValue - } else { - return Constants.twoButtonsHeight.rawValue - } - } - - private func configureConstraints() { - errorHeader.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical) - errorHeader.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - - errorContent.setContentHuggingPriority(.required, for: .vertical) - errorContent.setContentCompressionResistancePriority(.required, for: .vertical) - - let buttonsConstraint = buttonsView.heightAnchor.constraint( - greaterThanOrEqualToConstant: getButtonsMinHeight(numberOfButtons: numberOfButtons) - ) - - buttonsHeightConstraint = buttonsConstraint - NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: errorHeader.bottomAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: buttonsView.topAnchor), - - errorHeader.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - errorHeader.leadingAnchor.constraint(equalTo: view.leadingAnchor), - errorHeader.trailingAnchor.constraint(equalTo: view.trailingAnchor), - errorHeader.heightAnchor.constraint( - greaterThanOrEqualToConstant: Constants.errorHeaderMinHeight.rawValue), - errorHeader.heightAnchor.constraint( - lessThanOrEqualToConstant: Constants.errorHeaderMaxHeight.rawValue), - errorContent.topAnchor.constraint(equalTo: scrollView.topAnchor, - constant: Constants.errorContentBottomMargin.rawValue), - buttonsConstraint, - buttonsView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -GiniMargins.margin) - ]) - configureHorizontalConstraints() - view.layoutSubviews() - } - - private func configureHorizontalConstraints() { - if UIDevice.current.isIpad { - NSLayoutConstraint.activate([ - errorContent.centerXAnchor.constraint(equalTo: view.centerXAnchor), - errorContent.widthAnchor.constraint(equalTo: view.widthAnchor, - multiplier: Constants.iPadWidthMultiplier.rawValue), - buttonsView.leadingAnchor.constraint(equalTo: view.leadingAnchor, - constant: GiniMargins.margin), - buttonsView.trailingAnchor.constraint(equalTo: view.trailingAnchor, - constant: -GiniMargins.margin) - ]) - } else { - NSLayoutConstraint.activate([ - errorContent.leadingAnchor.constraint(equalTo: view.leadingAnchor, - constant: Constants.textContentMargin.rawValue), - errorContent.trailingAnchor.constraint(equalTo: view.trailingAnchor, - constant: -Constants.textContentMargin.rawValue), - errorContent.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.bottomAnchor), - buttonsView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, - constant: GiniMargins.margin), - buttonsView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, - constant: -GiniMargins.margin) - ]) - } - } - - private enum Constants: CGFloat { - case singleButtonHeight = 50 - case twoButtonsHeight = 112 - case textContentMargin = 24 - case iPadButtonsWidth = 280 - case errorHeaderMinHeight = 64 - case errorHeaderMaxHeight = 180 - case errorContentBottomMargin = 13 - case iPadWidthMultiplier = 0.7 - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Error/ErrorType.swift b/Sources/GiniCaptureSDK/Core/Screens/Error/ErrorType.swift deleted file mode 100644 index 22ad99c..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Error/ErrorType.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// ErrorType.swift -// -// -// Created by Krzysztof Kryniecki on 24/11/2022. -// - -import Foundation -import GiniBankAPILibrary - -/** - Enum representing different types of errors that can occur. - - - connection: Error related to establishing a connection. - - request: Error related to the request being made. - - serverError: Error returned by the server. - - authentication: Error related to authentication. - - unexpected: Unexpected error that is not covered by the other cases. - - importError: Error related to importing documents. - */ - -@objc public enum ErrorType: Int { - case connection - case request - case serverError - case authentication - case unexpected - case importError - - /** - Initializes a new instance of the `ErrorType` enum based on the given `GiniError`. - - - Parameters: - - error: The `GiniError` to base the `ErrorType` on. - */ - public init(error: GiniError) { - switch error { - case .unauthorized(_, _): - self = .authentication - case .noResponse: - self = .connection - case .notAcceptable(let response, _), .tooManyRequests(let response, _), - .parseError(_, let response, _), .badRequest(let response, _), .notFound(let response, _): - if let status = response?.statusCode { - switch status { - case 400, 402 ... 499: - self = .request - case 401: - self = .authentication - case let code where code >= 500: - self = .serverError - default: - self = .unexpected - } - } else { - self = .serverError - } - default: - self = .unexpected - } - } - - func iconName() -> String { - switch self { - case .connection: - return "errorGlobe" - case .request: - return "errorUpload" - case .authentication: - return "errorAuth" - case .serverError: - return "errorCloud" - case .unexpected: - return "alertTriangle" - case .importError: - return "alertTriangle" - } - } - - func content() -> String { - switch self { - case .connection: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.connection.content", - comment: "Connection error") - case .request: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.request.content", - comment: "Request error") - case .authentication: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.authentication.content", - comment: "Authentication error") - case .serverError: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.serverError.content", - comment: "Server error") - case .unexpected: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.unexpected.content", - comment: "Unexpected error") - case .importError: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.importError.content", - comment: "Import error") - } - } - - func title() -> String { - switch self { - case .connection: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.connection.title", - comment: "Connection error") - case .authentication: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.authentication.title", - comment: "Authentication error") - case .serverError: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.serverError.title", - comment: "Server error") - case .unexpected: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.unexpected.title", - comment: "Unexpected error") - case .request: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.request.title", - comment: "Upload error") - case .importError: - return NSLocalizedStringPreferredFormat( - "ginicapture.error.importError.title", - comment: "Upload error") - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpFormatsDataSource.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpFormatsDataSource.swift deleted file mode 100644 index a9f6187..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpFormatsDataSource.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// HelpFormatsDataSource.swift -// -// -// Created by Krzysztof Kryniecki on 03/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -typealias HelpFormatsCollectionSection = (title: String, - formats: [String], - formatsImage: UIImage?) - -class HelpFormatsDataSource: HelpRoundedCornersDataSource { - - lazy var itemSections: [HelpFormatsCollectionSection] = { - var sections: [HelpFormatsCollectionSection] = [ - (NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.1.title", - comment: "supported format for section 1 title"), - [ - NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.1.item.1", - comment: "supported format for section 1 item 1")], - UIImageNamedPreferred(named: "supportedFormatsIcon")), - (NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.2.title", - comment: "supported format for section 2 title"), - [ - NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.2.item.1", - comment: "supported format for section 2 item 1")], - UIImageNamedPreferred(named: "nonSupportedFormatsIcon")) - ] - - if giniConfiguration.fileImportSupportedTypes != .none { - if giniConfiguration.fileImportSupportedTypes == .pdf_and_images { - sections[0].formats.append( - NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.1.item.2", - comment: "supported format for section 1 itemm 2")) - } - sections[0].formats.append( - NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.1.item.3", - comment: "supported format for section 1 item 3")) - } - - if giniConfiguration.qrCodeScanningEnabled { - sections[0].formats.append( - NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.1.item.4", - comment: "supported format for section 1 item 4")) - } - sections[0].formats.append( - NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.section.1.item.5", - comment: "supported format for section 1 item 5")) - return sections - }() - - override var items: [HelpFormatsCollectionSection] { - get { - return itemSections - } - set { - itemSections = newValue - } - } - - private func configureCellAccessibility( - cell: HelpFormatCell, - title: String) { - cell.iconImageView?.accessibilityTraits = .image - cell.iconImageView.accessibilityLabel = title - } - - override func configureCell(cell: HelpFormatCell, indexPath: IndexPath) { - let section = items[indexPath.section] - let item = section.formats[indexPath.row] - cell.descriptionLabel.text = item - cell.descriptionLabel.font = giniConfiguration.textStyleFonts[.body] - cell.descriptionLabel.textColor = GiniColor( - light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - cell.descriptionLabel.adjustsFontForContentSizeCategory = true - cell.iconImageView.image = section.formatsImage - cell.iconImageView.backgroundColor = UIColor.clear - cell.backgroundColor = GiniColor(light: UIColor.GiniCapture.light1, dark: UIColor.GiniCapture.dark3).uiColor() - cell.separatorView.backgroundColor = GiniColor( - light: UIColor.GiniCapture.light3, - dark: UIColor.GiniCapture.dark4).uiColor() - configureCellAccessibility(cell: cell, title: section.title.uppercased()) - if indexPath.row == items[indexPath.section].formats.count - 1 { - cell.separatorView.isHidden = true - } else { - cell.separatorView.isHidden = false - } - } - - private func configureHeader( - header: HelpFormatSectionHeader, - section: Int) { - header.titleLabel.font = giniConfiguration.textStyleFonts[.caption1] - header.titleLabel.adjustsFontForContentSizeCategory = true - header.titleLabel.numberOfLines = 0 - header.titleLabel.textColor = GiniColor(light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - header.titleLabel.text = items[section].title.uppercased() - header.backgroundView?.backgroundColor = UIColor.clear - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if let header = tableView.dequeueReusableHeaderFooterView( - withIdentifier: HelpFormatSectionHeader.reuseIdentifier - ) as? HelpFormatSectionHeader { - configureHeader(header: header, section: section) - return header - } - fatalError("Section header is missing") - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if let cell = tableView.dequeueReusableCell( - withIdentifier: HelpFormatCell.reuseIdentifier, - for: indexPath) as? HelpFormatCell { - configureCell(cell: cell, indexPath: indexPath) - return cell - } - fatalError() - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return items.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return items[section].formats.count - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return UITableView.automaticDimension - } - - override func tableView( - _ tableView: UITableView, - willDisplay cell: UITableViewCell, - forRowAt indexPath: IndexPath) { - if items[indexPath.section].formats.count == 1 { - cell.round( - corners: [.bottomLeft, .bottomRight, .topLeft, .topRight], withRadius: RoundedCorners.cornerRadius) - } else { - if indexPath.row == 0 { - cell.round(corners: [.topLeft, .topRight], withRadius: RoundedCorners.cornerRadius) - } else { - if indexPath.row == items[indexPath.section].formats.count - 1 { - cell.round(corners: [.bottomLeft, .bottomRight], withRadius: RoundedCorners.cornerRadius) - } else { - cell.reset() - } - } - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpMenu/HelpMenuDataSource.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpMenu/HelpMenuDataSource.swift deleted file mode 100644 index fe2cc06..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpMenu/HelpMenuDataSource.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// HelpMenuDataSource.swift -// -// -// Created by Krzysztof Kryniecki on 28/07/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -protocol HelpMenuDataSourceDelegate: UIViewController { - func didSelectHelpItem(didSelect item: HelpMenuItem) -} - -final class HelpMenuDataSource: HelpRoundedCornersDataSource { - - private lazy var defaultItems: [HelpMenuItem] = { - var defaultItems: [HelpMenuItem] = [ .noResultsTips] - - if giniConfiguration.shouldShowSupportedFormatsScreen { - defaultItems.append(.supportedFormats) - } - if giniConfiguration.openWithEnabled { - defaultItems.append(.openWithTutorial) - } - return defaultItems - }() - - weak var delegate: HelpMenuDataSourceDelegate? - - required init( - configuration: GiniConfiguration - ) { - super.init(configuration: configuration) - self.items.append(contentsOf: defaultItems) - self.items.append(contentsOf: configuration.customMenuItems) - } - - override func configureCell(cell: HelpMenuCell, indexPath: IndexPath) { - cell.backgroundColor = GiniColor(light: UIColor.GiniCapture.light1, dark: UIColor.GiniCapture.dark3).uiColor() - cell.titleLabel.text = items[indexPath.row].title - cell.titleLabel.textColor = GiniColor( - light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - cell.titleLabel.numberOfLines = 0 - cell.titleLabel.font = giniConfiguration.textStyleFonts[.body] - cell.titleLabel.adjustsFontForContentSizeCategory = true - cell.accessoryType = .disclosureIndicator - cell.selectionStyle = .none - cell.separatorView.backgroundColor = GiniColor( - light: UIColor.GiniCapture.light3, - dark: UIColor.GiniCapture.dark4 - ).uiColor() - if indexPath.row == self.items.count - 1 { - cell.separatorView.isHidden = true - } else { - cell.separatorView.isHidden = false - } - } - - // MARK: - UITableViewDelegate - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let item = items[indexPath.row] - self.delegate?.didSelectHelpItem(didSelect: item) - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 0 - } - -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpMenu/HelpMenuItem.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpMenu/HelpMenuItem.swift deleted file mode 100644 index 3fa4b56..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpMenu/HelpMenuItem.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// HelpMenuItem.swift -// -// -// Created by Krzysztof Kryniecki on 02/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -/** -* HelpMenuItem is an enum that defines different options that can be displayed on the help screen. -* -* - noResultsTips: displays tips for when no results are returned. -* - openWithTutorial: displays a tutorial on how to use the `open with`functionality -* - supportedFormats: displays a list of supported formats. -* - custom(String, UIViewController): allows for the creation of a custom option, with a title and a corresponding UIViewController. -*/ - -public enum HelpMenuItem { - case noResultsTips - case openWithTutorial - case supportedFormats - case custom(String, UIViewController) - - // The title of the HelpMenuItems - var title: String { - switch self { - case .noResultsTips: - return NSLocalizedStringPreferredFormat("ginicapture.help.menu.tips", comment: "Tips Menu Item") - case .openWithTutorial: - return NSLocalizedStringPreferredFormat("ginicapture.help.menu.import", comment: "Import Menu Item") - case .supportedFormats: - return NSLocalizedStringPreferredFormat("ginicapture.help.menu.formats", comment: "Format Menu Item") - case .custom(let title, _): - return title - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpRoundedCornersDataSource.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpRoundedCornersDataSource.swift deleted file mode 100644 index bb3bdc9..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpRoundedCornersDataSource.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// HelpBaseDataSource.swift -// -// -// Created by Krzysztof Kryniecki on 02/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -class HelpRoundedCornersDataSource: NSObject, HelpDataSource where Cell: HelpCell { - var items: [Item] = [] - let giniConfiguration: GiniConfiguration - - required init( - configuration: GiniConfiguration - ) { - giniConfiguration = configuration - } - - func configureCell(cell: Cell, indexPath: IndexPath) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - HelpMenuDataSourceDelegate - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return items.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier) as? Cell { - self.configureCell(cell: cell, indexPath: indexPath) - return cell - } - fatalError("undefined cell") - } - - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - return nil - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if items.count == 1 { - cell.round( - corners: [.bottomLeft, .bottomRight, .topLeft, .topRight], - withRadius: RoundedCorners.cornerRadius) - } else { - if indexPath.row == 0 { - cell.round(corners: [.topLeft, .topRight], withRadius: RoundedCorners.cornerRadius) - } else { - if indexPath.row == items.count - 1 { - cell.round(corners: [.bottomLeft, .bottomRight], withRadius: RoundedCorners.cornerRadius) - } else { - cell.reset() - } - } - } - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - fatalError("tableView(tableView: didSelectRowAt:) has not been implemented") - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpTipsDataSource.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpTipsDataSource.swift deleted file mode 100644 index 70e45db..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/DataSources/HelpTipsDataSource.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// HelpTipsDataSource.swift -// -// -// Created by Krzysztof Kryniecki on 01/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -struct HelpTipsItem { - let header: String - let details: String - let iconName: String -} - -final class HelpTipsDataSource: HelpRoundedCornersDataSource { - // swiftlint:disable function_body_length - required init(configuration: GiniConfiguration) { - super.init(configuration: configuration) - items.append(contentsOf: [ - HelpTipsItem( - header: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.1", - comment: "Analysis suggestion 1 header"), - details: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.1.details", - comment: "Analysis suggestion 1 details"), - iconName: "captureSuggestion1"), - HelpTipsItem( - header: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.2", - comment: "Analysis suggestion 2 header"), - details: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.2.details", - comment: "Analysis suggestion 2 details"), - iconName: "captureSuggestion2"), - HelpTipsItem( - header: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.3", - comment: "Analysis suggestion 3 header"), - details: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.3.details", - comment: "Analysis suggestion 3 details"), - iconName: "captureSuggestion3"), - HelpTipsItem( - header: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.4", - comment: "Analysis suggestion 4 header"), - details: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.4.details", - comment: "Analysis suggestion 4 details"), - iconName: "captureSuggestion4") - ]) - - if giniConfiguration.multipageEnabled { - items.append( - HelpTipsItem( - header: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.5", - comment: "Analysis suggestion 5 header"), - details: NSLocalizedStringPreferredFormat( - "ginicapture.analysis.suggestion.5.details", - comment: "Analysis suggestion 5 details"), - iconName: "captureSuggestion5")) - } - } - - private func configureCellAccessibility( - cell: HelpTipCell, - item: HelpTipsItem) { - cell.iconImageView?.accessibilityTraits = .image - cell.iconImageView?.accessibilityLabel = item.header - } - - private func configureHeader( - header: HelpFormatSectionHeader, - section: Int) { - header.titleLabel.font = giniConfiguration.textStyleFonts[.caption1] - header.titleLabel.adjustsFontForContentSizeCategory = true - header.titleLabel.numberOfLines = 0 - header.titleLabel.textColor = GiniColor( - light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - header.titleLabel.text = NSLocalizedStringPreferredFormat( - "ginicapture.analysis.section.header", - comment: "Analysis section header").uppercased() - header.backgroundView?.backgroundColor = UIColor.clear - } - - var showHeader = false - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if showHeader, let header = tableView.dequeueReusableHeaderFooterView( - withIdentifier: HelpFormatSectionHeader.reuseIdentifier - ) as? HelpFormatSectionHeader { - configureHeader(header: header, section: section) - return header - } - return nil - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if showHeader { - return UITableView.automaticDimension - } - return 0 - - } - - override func configureCell(cell: HelpTipCell, indexPath: IndexPath) { - let item = items[indexPath.row] - cell.headerLabel.text = item.header - cell.headerLabel.font = giniConfiguration.textStyleFonts[.calloutBold] - cell.headerLabel.adjustsFontForContentSizeCategory = true - cell.headerLabel.textColor = GiniColor( - light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - cell.backgroundColor = GiniColor( - light: UIColor.GiniCapture.light1, - dark: UIColor.GiniCapture.dark3).uiColor() - cell.descriptionLabel.text = item.details - cell.descriptionLabel.font = giniConfiguration.textStyleFonts[.subheadline] - cell.descriptionLabel.adjustsFontForContentSizeCategory = true - cell.descriptionLabel.textColor = GiniColor( - light: UIColor.GiniCapture.dark7, - dark: UIColor.GiniCapture.dark7).uiColor() - cell.iconImageView.image = UIImageNamedPreferred(named: item.iconName) - cell.separatorView.backgroundColor = GiniColor( - light: UIColor.GiniCapture.light3, - dark: UIColor.GiniCapture.dark4).uiColor() - cell.selectionStyle = .none - configureCellAccessibility(cell: cell, item: item) - if indexPath.row == items.count - 1 { - cell.separatorView.alpha = 0 - } else { - cell.separatorView.alpha = 1 - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/HelpMenuViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/HelpMenuViewController.swift new file mode 100644 index 0000000..44b6764 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/HelpMenuViewController.swift @@ -0,0 +1,166 @@ +// +// HelpMenuViewController.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 10/18/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import UIKit + +/** + The `HelpMenuViewControllerDelegate` protocol defines methods that allow you to handle table item selection actions. + + - note: Component API only. + */ + +public protocol HelpMenuViewControllerDelegate: AnyObject { + func help(_ menuViewController: HelpMenuViewController, didSelect item: HelpMenuViewController.Item) +} + +/** + The `HelpMenuViewController` provides explanations on how to take better pictures, how to + use the _Open with_ feature and which formats are supported by the Gini Capture SDK. + */ + +final public class HelpMenuViewController: UITableViewController { + + public weak var delegate: HelpMenuViewControllerDelegate? + let giniConfiguration: GiniConfiguration + let tableRowHeight: CGFloat = 64 + var helpMenuCellIdentifier = "helpMenuCellIdentifier" + + public enum Item { + case noResultsTips + case openWithTutorial + case supportedFormats + case custom(String, UIViewController) + + var title: String { + switch self { + case .noResultsTips: + return .localized(resource: HelpStrings.menuFirstItemText) + case .openWithTutorial: + return .localized(resource: HelpStrings.menuSecondItemText) + case .supportedFormats: + return .localized(resource: HelpStrings.menuThirdItemText) + case .custom(let title, _): + return title + } + } + + var viewController: UIViewController { + let viewController: UIViewController + switch self { + case .noResultsTips: + let title: String = .localized(resource: ImageAnalysisNoResultsStrings.titleText) + let topViewText: String = .localized(resource: ImageAnalysisNoResultsStrings.warningHelpMenuText) + viewController = ImageAnalysisNoResultsViewController(title: title, + subHeaderText: nil, + topViewText: topViewText, + topViewIcon: nil) + case .openWithTutorial: + viewController = OpenWithTutorialViewController() + case .supportedFormats: + viewController = SupportedFormatsViewController() + case .custom(_, let customViewController): + viewController = customViewController + } + return viewController + + } + } + lazy var menuItems: [Item] = [] + + lazy var defaultItems: [Item] = { + var defaultItems: [Item] = [ .noResultsTips] + + if giniConfiguration.shouldShowSupportedFormatsScreen { + defaultItems.append(.supportedFormats) + } + + if giniConfiguration.openWithEnabled { + defaultItems.append(.openWithTutorial) + } + + return defaultItems + }() + + public init(giniConfiguration: GiniConfiguration) { + self.giniConfiguration = giniConfiguration + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(giniConfiguration:) has not been implemented") + } + + func configureMenuItems() { + menuItems.append(contentsOf: defaultItems) + menuItems.append(contentsOf: giniConfiguration.customMenuItems) + } + + fileprivate func configureTableView() { + tableView.tableFooterView = UIView() + tableView.register(UITableViewCell.self, forCellReuseIdentifier: helpMenuCellIdentifier) + tableView.rowHeight = tableRowHeight + + tableView.backgroundColor = UIColor.from(giniColor: giniConfiguration.helpScreenBackgroundColor) + + // In iOS it is .automatic by default, having an initial animation when the view is loaded. + tableView.contentInsetAdjustmentBehavior = .never + } + + fileprivate func configureMainView() { + title = .localized(resource: HelpStrings.menuTitle) + } + + fileprivate func configureLayout() { + configureMainView() + configureTableView() + configureMenuItems() + edgesForExtendedLayout = [] + } + + override public func viewDidLoad() { + super.viewDidLoad() + configureLayout() + } + + @objc func back() { + navigationController?.popViewController(animated: true) + } + +} + +// MARK: - UITableViewDataSource + +extension HelpMenuViewController { + override public func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return menuItems.count + } + + override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: helpMenuCellIdentifier, for: indexPath) + cell.backgroundColor = UIColor.from(giniColor: giniConfiguration.helpScreenCellsBackgroundColor) + cell.textLabel?.text = menuItems[indexPath.row].title + cell.textLabel?.font = giniConfiguration.customFont.with(weight: .regular, size: 14, style: .body) + cell.accessoryType = .disclosureIndicator + + return cell + } + + override public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let item = menuItems[indexPath.row] + + guard delegate == nil else { + delegate?.help(self, didSelect: item) + return + } + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionCell.swift index 7d6688b..848eb4c 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionCell.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionCell.swift @@ -9,14 +9,14 @@ import UIKit final class CaptureSuggestionsCollectionCell: UICollectionViewCell { - + var suggestionImage: UIImageView = { let suggestionImage = UIImageView() suggestionImage.translatesAutoresizingMaskIntoConstraints = false suggestionImage.contentMode = .scaleAspectFit return suggestionImage }() - + var suggestionText: UILabel = { let suggestionText = UILabel() suggestionText.translatesAutoresizingMaskIntoConstraints = false @@ -25,18 +25,18 @@ final class CaptureSuggestionsCollectionCell: UICollectionViewCell { suggestionText.minimumScaleFactor = 10 / 14 return suggestionText }() - + override init(frame: CGRect) { super.init(frame: frame) addSubview(suggestionImage) addSubview(suggestionText) addConstraints() } - + required init?(coder aDecoder: NSCoder) { fatalError("init(frame:) should be used instead") } - + private func addConstraints() { Constraints.active(item: suggestionImage, attr: .top, relatedBy: .equal, to: self, attr: .top, priority: 999) Constraints.active(item: suggestionImage, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, @@ -50,10 +50,11 @@ final class CaptureSuggestionsCollectionCell: UICollectionViewCell { Constraints.active(item: suggestionImage, attr: .height, relatedBy: .lessThanOrEqual, to: nil, attr: .notAnAttribute, constant: 75) Constraints.active(item: suggestionImage, attr: .centerY, relatedBy: .equal, to: self, attr: .centerY) - + Constraints.active(item: suggestionText, attr: .top, relatedBy: .equal, to: self, attr: .top) Constraints.active(item: suggestionText, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom) Constraints.active(item: suggestionText, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, constant: -20, priority: 999) } } + diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionHeader.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionHeader.swift index cef2c5a..9e7939e 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionHeader.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionHeader.swift @@ -9,10 +9,10 @@ import UIKit final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { - + static let topContainerHeight: CGFloat = 100 static let subHeaderHeight: CGFloat = 60 - + private let topViewIconWidth: CGFloat = 25 private var leadingTopViewTextConstraint: NSLayoutConstraint? private var bottomTopViewContainerConstraint: NSLayoutConstraint? @@ -25,7 +25,7 @@ final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { } } } - + var shouldShowSubHeader: Bool = true { didSet { if !shouldShowSubHeader { @@ -35,43 +35,43 @@ final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { } } } - + // Views lazy var topViewContainer: UIView = { let container = UIView() container.translatesAutoresizingMaskIntoConstraints = false return container }() - + lazy var topViewContainerBottomLine: UIView = { let line = UIView() line.translatesAutoresizingMaskIntoConstraints = false line.backgroundColor = .lightGray return line }() - + lazy var topViewIcon: UIImageView = { let icon = UIImageView() icon.translatesAutoresizingMaskIntoConstraints = false icon.contentMode = .scaleAspectFit - icon.tintColor = UIColor.GiniCapture.warning1 + icon.tintColor = GiniConfiguration.shared.noResultsWarningContainerIconColor return icon }() - + lazy var topViewText: UILabel = { let text = UILabel() text.translatesAutoresizingMaskIntoConstraints = false text.numberOfLines = 0 return text }() - + lazy var subHeaderTitle: UILabel = { let subHeaderTitle = UILabel() subHeaderTitle.translatesAutoresizingMaskIntoConstraints = false subHeaderTitle.numberOfLines = 0 return subHeaderTitle }() - + override init(frame: CGRect) { super.init(frame: frame) topViewContainer.addSubview(topViewContainerBottomLine) @@ -81,11 +81,11 @@ final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { addSubview(subHeaderTitle) addConstraints() } - + required init?(coder aDecoder: NSCoder) { fatalError("init(frame:) should be used instead") } - + private func addConstraints() { // Top view container bottom line Constraints.active(item: topViewContainerBottomLine, attr: .height, relatedBy: .equal, to: nil, @@ -94,7 +94,7 @@ final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { attr: .width) Constraints.active(item: topViewContainerBottomLine, attr: .top, relatedBy: .equal, to: topViewContainer, attr: .bottom) - + // Top Container Constraints.active(item: topViewContainer, attr: .top, relatedBy: .equal, to: self, attr: .top) Constraints.active(item: topViewContainer, attr: .leading, relatedBy: .equal, to: self, attr: .leading) @@ -116,7 +116,7 @@ final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { constant: -16) Constraints.active(item: topViewIcon, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, constant: topViewIconWidth) - + // Top text Constraints.active(item: topViewText, attr: .top, relatedBy: .equal, to: topViewContainer, attr: .top, constant: 16) @@ -127,7 +127,7 @@ final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { leadingTopViewTextConstraint = NSLayoutConstraint(item: topViewText, attribute: .leading, relatedBy: .equal, toItem: topViewContainer, attribute: .leading, multiplier: 1, constant: 16) - + // Sub header title Constraints.active(item: subHeaderTitle, attr: .top, relatedBy: .equal, to: topViewContainerBottomLine, attr: .bottom, constant: 20) @@ -136,5 +136,7 @@ final class CaptureSuggestionsCollectionHeader: UICollectionReusableView { Constraints.active(item: subHeaderTitle, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, constant: -20) Constraints.active(item: subHeaderTitle, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom) + } } + diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionView.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionView.swift index 34db773..d3334d1 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionView.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/CaptureSuggestionsCollectionView.swift @@ -9,16 +9,16 @@ import UIKit final class CaptureSuggestionsCollectionView: UICollectionView { - + static let captureSuggestionsCellIdentifier = "captureSuggestionsCellIdentifier" static let captureSuggestionsHeaderIdentifier = "captureSuggestionsHeaderIdentifier" - + private let cellHeight: (max: CGFloat, min: CGFloat) = (180.0, 80.0) private let rowsInLandscape: CGFloat = 2.0 private var captureSuggestionsCollectionLayout: UICollectionViewFlowLayout { return (collectionViewLayout as? UICollectionViewFlowLayout)! } - + var sectionInset: UIEdgeInsets { if UIDevice.current.isIpad { return UIEdgeInsets(top: 0, left: 20, bottom: 20, right: 20) @@ -26,7 +26,7 @@ final class CaptureSuggestionsCollectionView: UICollectionView { return UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) } } - + init() { super.init(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) self.register(CaptureSuggestionsCollectionCell.self, @@ -34,25 +34,25 @@ final class CaptureSuggestionsCollectionView: UICollectionView { self.register(CaptureSuggestionsCollectionHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: CaptureSuggestionsCollectionView.captureSuggestionsHeaderIdentifier) - + self.showsVerticalScrollIndicator = false - + if #available(iOS 13.0, *) { backgroundColor = .systemBackground } else { backgroundColor = .white } - + captureSuggestionsCollectionLayout.minimumLineSpacing = 20 captureSuggestionsCollectionLayout.minimumInteritemSpacing = 0 captureSuggestionsCollectionLayout.sectionInset = sectionInset - + } - + required init?(coder aDecoder: NSCoder) { fatalError("init() should be used instead") } - + func cellSize(ofSection section: Int = 0) -> CGSize { let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height let itemCount = CGFloat(self.numberOfItems(inSection: section)) @@ -65,21 +65,21 @@ final class CaptureSuggestionsCollectionView: UICollectionView { var width: CGFloat = (UIScreen.main.bounds.width - captureSuggestionsCollectionLayout.sectionInset.left - captureSuggestionsCollectionLayout.sectionInset.right) - + if isLandscape && UIDevice.current.isIpad { height *= rowsInLandscape width /= rowsInLandscape } - + if height < cellHeight.min { height = cellHeight.min } else if height > cellHeight.max { height = cellHeight.max } - + return CGSize(width: width, height: height) } - + func headerSize(withSubHeader: Bool) -> CGSize { var height = CaptureSuggestionsCollectionHeader.topContainerHeight if withSubHeader { @@ -87,5 +87,6 @@ final class CaptureSuggestionsCollectionView: UICollectionView { } return CGSize(width: UIScreen.main.bounds.width, height: height) } - + } + diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/ImageAnalysisNoResultsViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/ImageAnalysisNoResultsViewController.swift similarity index 78% rename from Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/ImageAnalysisNoResultsViewController.swift rename to Sources/GiniCaptureSDK/Core/Screens/Help/No results/ImageAnalysisNoResultsViewController.swift index 2f7eae4..cb006d6 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/ImageAnalysisNoResultsViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/No results/ImageAnalysisNoResultsViewController.swift @@ -9,84 +9,72 @@ import Foundation import UIKit -// This is unused and should be deleted! /** The `ImageAnalysisNoResultsViewController` provides a custom no results screen which shows some capture suggestions when there is no results when analysing an image. */ -final class ImageAnalysisNoResultsViewController: UIViewController { - +public final class ImageAnalysisNoResultsViewController: UIViewController { + lazy var suggestionsCollectionView: CaptureSuggestionsCollectionView = { let collection = CaptureSuggestionsCollectionView() collection.translatesAutoresizingMaskIntoConstraints = false return collection }() - + lazy var bottomButton: UIButton = { let bottomButton = UIButton() bottomButton.translatesAutoresizingMaskIntoConstraints = false bottomButton.setTitle(self.bottomButtonText, for: .normal) bottomButton.titleLabel?.font = giniConfiguration.customFont.with(weight: .bold, size: 14, style: .caption1) - let bottomButtonTextColor = GiniColor(light: .GiniCapture.accent1, dark: .GiniCapture.accent1).uiColor() + let bottomButtonTextColor = UIColor.from(giniColor: giniConfiguration.noResultsBottomButtonTextColor) bottomButton.setTitleColor(bottomButtonTextColor, for: .normal) bottomButton.setTitleColor(bottomButtonTextColor.withAlphaComponent(0.5), for: .highlighted) bottomButton.setImage(self.bottomButtonIconImage, for: .normal) - if let highlightedImage = self.bottomButtonIconImage?.tintedImageWithColor( - bottomButtonTextColor.withAlphaComponent(0.5)) { + if let highlightedImage = self.bottomButtonIconImage?.tintedImageWithColor(bottomButtonTextColor.withAlphaComponent(0.5)){ bottomButton.setImage(highlightedImage, for: .highlighted) } bottomButton.addTarget(self, action: #selector(didTapBottomButtonAction), for: .touchUpInside) bottomButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20) - bottomButton.backgroundColor = bottomButtonTextColor + bottomButton.backgroundColor = giniConfiguration.noResultsBottomButtonColor + bottomButton.layer.cornerRadius = giniConfiguration.noResultsBottomButtonCornerRadius return bottomButton }() - + lazy var captureSuggestions: [(image: UIImage?, text: String)] = { var suggestions: [(image: UIImage?, text: String)] = [ (UIImageNamedPreferred(named: "captureSuggestion1"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.1", - comment: "First suggestion title for analysis screen")), + .localized(resource: AnalysisStrings.suggestion1Text)), (UIImageNamedPreferred(named: "captureSuggestion2"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.2", - comment: "Second suggestion title for analysis screen")), + .localized(resource: AnalysisStrings.suggestion2Text)), (UIImageNamedPreferred(named: "captureSuggestion3"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.3", - comment: "Third suggestion title for analysis screen")), + .localized(resource: AnalysisStrings.suggestion3Text)), (UIImageNamedPreferred(named: "captureSuggestion4"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.4", - comment: "Fourth suggestion title for analysis screen")) + .localized(resource: AnalysisStrings.suggestion4Text)) ] - + if giniConfiguration.multipageEnabled { suggestions.append((UIImageNamedPreferred(named: "captureSuggestion5"), - NSLocalizedStringPreferredFormat("ginicapture.analysis.suggestion.5", - comment: "Fifth suggestion title for analysis screen"))) + .localized(resource: AnalysisStrings.suggestion5Text))) } return suggestions }() - + fileprivate var subHeaderTitle: String? fileprivate var topViewText: String? fileprivate var topViewIcon: UIImage? fileprivate var bottomButtonText: String? fileprivate var bottomButtonIconImage: UIImage? fileprivate var giniConfiguration: GiniConfiguration - - var didTapBottomButton: (() -> Void) = { } - - convenience init(title: String? = nil, - subHeaderText: String? = NSLocalizedStringPreferredFormat( - "ginicapture.noresults.collection.header", - comment: "no results suggestions collection header title"), - topViewText: String = NSLocalizedStringPreferredFormat( - "ginicapture.noresults.warning", - comment: "Warning text that indicates that there " + + + public var didTapBottomButton: (() -> Void) = { } + + public convenience init(title: String? = nil, + subHeaderText: String? = NSLocalizedStringPreferredFormat("ginicapture.noresults.collection.header", comment: "no results suggestions collection header title"), + topViewText: String = NSLocalizedStringPreferredFormat("ginicapture.noresults.warning", comment: "Warning text that indicates that there " + "was any result for this photo analysis"), topViewIcon: UIImage? = UIImageNamedPreferred(named: "warningNoResults"), - bottomButtonText: String? = NSLocalizedStringPreferredFormat( - "ginicapture.noresults.gotocamera", - comment: "bottom button title (go to camera button)"), + bottomButtonText: String? = NSLocalizedStringPreferredFormat("ginicapture.noresults.gotocamera", comment: "bottom button title (go to camera button)"), bottomButtonIcon: UIImage? = UIImageNamedPreferred(named: "cameraIcon")) { self.init(title: title, subHeaderText: subHeaderText, @@ -96,7 +84,7 @@ final class ImageAnalysisNoResultsViewController: UIViewController { bottomButtonIcon: bottomButtonIcon, giniConfiguration: .shared) } - + init(title: String? = nil, subHeaderText: String?, topViewText: String, @@ -110,53 +98,53 @@ final class ImageAnalysisNoResultsViewController: UIViewController { self.subHeaderTitle = subHeaderText self.topViewText = topViewText if let topViewIcon = topViewIcon { - self.topViewIcon = topViewIcon.tintedImageWithColor(UIColor.GiniCapture.warning1) + self.topViewIcon = topViewIcon.tintedImageWithColor(giniConfiguration.noResultsWarningContainerIconColor) } self.bottomButtonText = bottomButtonText self.bottomButtonIconImage = bottomButtonIcon } - - required init?(coder aDecoder: NSCoder) { + + required public init?(coder aDecoder: NSCoder) { fatalError("init(title:subHeaderText:topViewText:topViewIcon:bottomButtonText:bottomButtonIcon:)" + "has not been implemented") } - - override func loadView() { + + public override func loadView() { super.loadView() edgesForExtendedLayout = [] - + if #available(iOS 13.0, *) { view.backgroundColor = .systemBackground } else { view.backgroundColor = .white } - + view.addSubview(suggestionsCollectionView) - + if bottomButtonText != nil { view.addSubview(bottomButton) } addConstraints() - + suggestionsCollectionView.dataSource = self suggestionsCollectionView.delegate = self } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + + public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { coordinator.animate(alongsideTransition: { _ in self.suggestionsCollectionView.collectionViewLayout.invalidateLayout() }, completion: nil) } - + fileprivate func addConstraints() { - + // Collection View Constraints.active(item: suggestionsCollectionView, attr: .top, relatedBy: .equal, to: self.view, attr: .top) Constraints.active(item: self.view, attr: .leading, relatedBy: .equal, to: suggestionsCollectionView, attr: .leading) Constraints.active(item: self.view, attr: .trailing, relatedBy: .equal, to: suggestionsCollectionView, attr: .trailing) - + // Button if bottomButtonText != nil { Constraints.active(item: view.safeAreaLayoutGuide, attr: .bottom, relatedBy: .equal, to: bottomButton, @@ -176,9 +164,9 @@ final class ImageAnalysisNoResultsViewController: UIViewController { Constraints.active(item: self.view, attr: .bottom, relatedBy: .equal, to: suggestionsCollectionView, attr: .bottom, constant: 0, priority: 999) } - + } - + // MARK: Button action @objc func didTapBottomButtonAction() { didTapBottomButton() @@ -188,11 +176,11 @@ final class ImageAnalysisNoResultsViewController: UIViewController { // MARK: UICollectionViewDataSource extension ImageAnalysisNoResultsViewController: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return captureSuggestions.count } - - func collectionView(_ collectionView: UICollectionView, + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let identifier = CaptureSuggestionsCollectionView.captureSuggestionsCellIdentifier let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: identifier, @@ -202,8 +190,8 @@ extension ImageAnalysisNoResultsViewController: UICollectionViewDataSource { cell.suggestionImage.image = self.captureSuggestions[indexPath.row].image return cell } - - func numberOfSections(in collectionView: UICollectionView) -> Int { + + public func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } } @@ -211,19 +199,19 @@ extension ImageAnalysisNoResultsViewController: UICollectionViewDataSource { // MARK: UICollectionViewDelegateFlowLayout extension ImageAnalysisNoResultsViewController: UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return suggestionsCollectionView.cellSize() } - - func collectionView(_ collectionView: UICollectionView, + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return suggestionsCollectionView.headerSize(withSubHeader: subHeaderTitle != nil) } - - func collectionView(_ collectionView: UICollectionView, + + public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { let identifier = CaptureSuggestionsCollectionView.captureSuggestionsHeaderIdentifier diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionCell.swift new file mode 100644 index 0000000..b04ff1e --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionCell.swift @@ -0,0 +1,225 @@ +// +// OpenWithTutorialCollectionCell.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 10/23/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import UIKit + +final class OpenWithTutorialCollectionCell: UICollectionViewCell { + + let padding:(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) = (40, 20, 40, 20) + let stepIndicatorCircleSize: CGSize = CGSize(width: 30, height: 30) + let imageHeight: CGFloat = UIDevice.current.isIpad ? 250 : 190 + + let indicatorToTitleDistance: CGFloat = 30 + let titleToSubtitleDistance: CGFloat = 20 + let subtitleToImageDistance: CGFloat = 40 + + static let maxTitleFontSize: CGFloat = UIDevice.current.isIpad ? 18 : 14 + static let maxSubtitleFontSize: CGFloat = UIDevice.current.isIpad ? 16 : 14 + + lazy var stepIndicator: UILabel = { + var label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = GiniConfiguration.shared.stepIndicatorColor + return label + }() + + lazy var stepIndicatorCircle: UIView = { + var view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.frame.size = self.stepIndicatorCircleSize + + updateStepIndicatorCircleColor(stepIndicatorCircle: view) + + view.layer.borderWidth = 1 + view.layer.cornerRadius = self.stepIndicatorCircleSize.width / 2 + return view + }() + + private func updateStepIndicatorCircleColor(stepIndicatorCircle: UIView) { + + stepIndicatorCircle.layer.borderColor = UIColor.from(giniColor: GiniConfiguration.shared.indicatorCircleColor).cgColor + } + + lazy var stepTitle: UILabel = { + var label = UILabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + lazy var stepSubTitle: UILabel = { + var label = UILabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + + if #available(iOS 13.0, *) { + label.textColor = .secondaryLabel + } else { + label.textColor = .lightGray + } + + return label + }() + + lazy var stepImage: UIImageView = { + var imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.layer.shadowOffset = CGSize(width: 0, height: 4) + + updateStepImageShadow(stepImage: imageView) + + imageView.layer.shadowOpacity = 0.1 + imageView.layer.shadowRadius = 14 + imageView.layer.shadowPath = UIBezierPath(rect: imageView.bounds).cgPath + return imageView + }() + + private func updateStepImageShadow(stepImage: UIImageView) { + + if #available(iOS 13.0, *) { + stepImage.layer.shadowColor = Colors.Gini.shadowColor.cgColor + } else { + stepImage.layer.shadowColor = UIColor.black.cgColor + } + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + + super.traitCollectionDidChange(previousTraitCollection) + + if #available(iOS 13.0, *) { + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + + updateStepIndicatorCircleColor(stepIndicatorCircle: stepIndicatorCircle) + updateStepImageShadow(stepImage: stepImage) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + if #available(iOS 13.0, *) { + backgroundColor = .systemBackground + } else { + backgroundColor = .white + } + + addSubview(stepIndicator) + addSubview(stepIndicatorCircle) + addSubview(stepTitle) + addSubview(stepSubTitle) + addSubview(stepImage) + + addConstrains() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(frame:) should be used instead") + } + + private func addConstrains() { + + // stepIndicator + Constraints.active(item: stepIndicator, attr: .centerX, relatedBy: .equal, to: stepIndicatorCircle, + attr: .centerX) + Constraints.active(item: stepIndicator, attr: .centerY, relatedBy: .equal, to: stepIndicatorCircle, + attr: .centerY) + + // stepIndicatorCircle + Constraints.active(item: stepIndicatorCircle, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: stepIndicatorCircleSize.height) + Constraints.active(item: stepIndicatorCircle, attr: .width, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: stepIndicatorCircleSize.width) + Constraints.active(item: stepIndicatorCircle, attr: .top, relatedBy: .equal, to: self, + attr: .top, constant: padding.top) + Constraints.active(item: stepIndicatorCircle, attr: .leading, relatedBy: .equal, to: self, + attr: .leading, constant: padding.left) + + // stepTitle + Constraints.active(item: stepTitle, attr: .top, relatedBy: .equal, to: stepIndicatorCircle, + attr: .bottom, constant: indicatorToTitleDistance) + Constraints.active(item: stepTitle, attr: .leading, relatedBy: .equal, to: self, + attr: .leading, constant: padding.left) + Constraints.active(item: stepTitle, attr: .trailing, relatedBy: .equal, to: self, + attr: .trailing, constant: -padding.right) + + // stepSubTitle + Constraints.active(item: stepSubTitle, attr: .top, relatedBy: .equal, to: stepTitle, + attr: .bottom, constant: titleToSubtitleDistance) + Constraints.active(item: stepSubTitle, attr: .leading, relatedBy: .equal, to: self, + attr: .leading, constant: padding.left) + Constraints.active(item: stepSubTitle, attr: .trailing, relatedBy: .equal, to: self, + attr: .trailing, constant: -padding.right) + stepSubTitle.setContentCompressionResistancePriority(.required, for: .vertical) + + // stepImage + Constraints.active(item: stepImage, attr: .top, relatedBy: .equal, to: stepSubTitle, + attr: .bottom, constant: subtitleToImageDistance) + Constraints.active(item: stepImage, attr: .leading, relatedBy: .equal, to: self, + attr: .leading, constant: padding.left, priority: 999) + Constraints.active(item: stepImage, attr: .trailing, relatedBy: .equal, to: self, + attr: .trailing, constant: -padding.right, priority: 999) + Constraints.active(item: stepImage, attr: .centerX, relatedBy: .equal, to: self, + attr: .centerX) + Constraints.active(item: stepImage, attr: .bottom, relatedBy: .lessThanOrEqual, + to: self, attr: .bottom, constant: -padding.bottom, priority: 999) + Constraints.active(item: stepImage, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: imageHeight) + + } + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) + -> UICollectionViewLayoutAttributes { + guard let collectionView = superview as? UICollectionView else { return layoutAttributes } + let isHorizontalLayout = collectionView.frame.width > collectionView.frame.height && UIDevice.current.isIpad + var maxTextWidth: CGFloat = UIScreen.main.bounds.width - padding.left - padding.right + + if isHorizontalLayout { + maxTextWidth = (UIScreen.main.bounds.width / CGFloat(collectionView.numberOfItems(inSection: 0))) - + padding.left - + padding.right + } + + let itemSeparations: CGFloat = padding.top + + padding.bottom + + indicatorToTitleDistance + + titleToSubtitleDistance + + subtitleToImageDistance + let itemsHeight = stepIndicatorCircleSize.height + + imageHeight + + stepTitle.textHeight(forWidth: maxTextWidth) + + stepSubTitle.textHeight(forWidth: maxTextWidth) + + var height: CGFloat = ceil(itemsHeight + itemSeparations) + + if isHorizontalLayout && height < collectionView.frame.height { + height = collectionView.frame.height + } + + layoutAttributes.frame.size.height = height + + return layoutAttributes + } + + public func fillWith(item: OpenWithTutorialStep, at position: Int, giniConfiguration: GiniConfiguration) { + stepIndicator.text = String(describing: position + 1) + stepTitle.text = item.title + stepTitle.font = giniConfiguration.customFont.with(weight: .regular, + size: OpenWithTutorialCollectionCell.maxTitleFontSize, + style: .headline) + stepSubTitle.text = item.subtitle + stepSubTitle.font = giniConfiguration.customFont.with(weight: .regular, + size: OpenWithTutorialCollectionCell.maxSubtitleFontSize, + style: .headline) + stepImage.image = item.image + } +} + diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionFlowLayout.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionFlowLayout.swift new file mode 100644 index 0000000..d5e44ea --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionFlowLayout.swift @@ -0,0 +1,51 @@ +// +// OpenWithTutorialCollectionFlowLayout.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 10/24/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import UIKit + +final class OpenWithTutorialCollectionFlowLayout: UICollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + if let attrs = super.layoutAttributesForElements(in: rect) { + var baseline: CGFloat = -2 + var sameLineElements = [UICollectionViewLayoutAttributes]() + for element in attrs where element.representedElementCategory == .cell { + let frame = element.frame + let centerY = frame.midY + if abs(centerY - baseline) > 1 { + baseline = centerY + OpenWithTutorialCollectionFlowLayout + .alignToTopForSameLineElements(sameLineElements: sameLineElements) + sameLineElements.removeAll() + } + sameLineElements.append(element) + } + OpenWithTutorialCollectionFlowLayout.alignToTopForSameLineElements(sameLineElements: sameLineElements) + return attrs + } + return nil + } + + private class func alignToTopForSameLineElements(sameLineElements: [UICollectionViewLayoutAttributes]) { + if sameLineElements.count < 1 { + return + } + + let sorted = sameLineElements.sorted { + let height1 = $0.frame.size.height + let height2 = $1.frame.size.height + let delta = height1 - height2 + return delta <= 0 + } + + if let tallest = sorted.last { + for obj in sameLineElements { + obj.frame = obj.frame.offsetBy(dx: 0, dy: tallest.frame.origin.y - obj.frame.origin.y) + } + } + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionHeader.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionHeader.swift new file mode 100644 index 0000000..59b3787 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialCollectionHeader.swift @@ -0,0 +1,81 @@ +// +// OpenWithTutorialCollectionHeader.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 10/24/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import UIKit + +final class OpenWithTutorialCollectionHeader: UICollectionReusableView { + + let padding:(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) = (20, 20, 20, 20) + + lazy var headerContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + if #available(iOS 13.0, *) { + view.backgroundColor = .systemBackground + } else { + view.backgroundColor = .white + } + + return view + }() + + static let maxHeaderFontSize: CGFloat = UIDevice.current.isIpad ? 16 : 14 + + lazy var headerTitle: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 12 / OpenWithTutorialCollectionHeader.maxHeaderFontSize + return label + }() + + lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = nil + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + headerContainer.addSubview(headerTitle) + addSubview(headerContainer) + addSubview(bottomLine) + + addConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(frame:) should be used instead") + } + + private func addConstraints() { + Constraints.active(item: headerContainer, attr: .top, relatedBy: .equal, to: self, attr: .top) + Constraints.active(item: headerContainer, attr: .leading, relatedBy: .equal, to: self, attr: .leading) + Constraints.active(item: headerContainer, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing) + + Constraints.active(item: headerTitle, attr: .top, relatedBy: .equal, to: headerContainer, attr: .top, + constant: padding.top) + Constraints.active(item: headerTitle, attr: .leading, relatedBy: .equal, to: headerContainer, attr: .leading, + constant: padding.left) + Constraints.active(item: headerTitle, attr: .trailing, relatedBy: .equal, to: headerContainer, attr: .trailing, + constant: -padding.right) + Constraints.active(item: headerTitle, attr: .bottom, relatedBy: .equal, to: headerContainer, attr: .bottom, + constant: -padding.bottom) + + Constraints.active(item: bottomLine, attr: .top, relatedBy: .equal, to: headerContainer, attr: .bottom) + Constraints.active(item: bottomLine, attr: .leading, relatedBy: .equal, to: self, attr: .leading) + Constraints.active(item: bottomLine, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing) + Constraints.active(item: bottomLine, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom) + Constraints.active(item: bottomLine, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 1) + } +} + diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialViewController.swift new file mode 100644 index 0000000..ef4b770 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/Open with tutorial/OpenWithTutorialViewController.swift @@ -0,0 +1,153 @@ +// +// OpenWithTutorialViewController.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 10/20/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import UIKit + +typealias OpenWithTutorialStep = (title: String, subtitle: String, image: UIImage?) + +final class OpenWithTutorialViewController: UICollectionViewController { + let openWithTutorialCollectionCellIdentifier = "openWithTutorialCollectionCellIdentifier" + let openWithTutorialCollectionHeaderIdentifier = "openWithTutorialCollectionHeaderIdentifier" + + let giniConfiguration: GiniConfiguration + var appName: String { + return giniConfiguration.openWithAppNameForTexts + } + + lazy var items: [OpenWithTutorialStep] = { + var items: [OpenWithTutorialStep] = [ + (.localized(resource: HelpStrings.openWithTutorialStep1Title), + .localized(resource: HelpStrings.openWithTutorialStep1Subtitle), + UIImageNamedPreferred(named: .localized(resource: ImageAssetsStrings.openWithTutorialStep1))), + (.localized(resource: HelpStrings.openWithTutorialStep2Title), + .localized(resource: HelpStrings.openWithTutorialStep2Subtitle, args: + appName, + appName), + UIImageNamedPreferred(named: .localized(resource: ImageAssetsStrings.openWithTutorialStep2))) + ] + + if self.giniConfiguration.shouldShowDragAndDropTutorial { + items.append((.localized(resource: HelpStrings.openWithTutorialStep3Title), + .localized(resource: HelpStrings.openWithTutorialStep3Subtitle, args: + appName, + appName, + appName), + UIImageNamedPreferred(named: .localized(resource: ImageAssetsStrings.openWithTutorialStep3)))) + } + + return items + }() + + lazy var headerTitle: String = { + return .localized(resource: HelpStrings.openWithTutorialCollectionHeader, args: appName) + }() + + fileprivate var stepsCollectionLayout: OpenWithTutorialCollectionFlowLayout { + return (self.collectionView?.collectionViewLayout as? OpenWithTutorialCollectionFlowLayout)! + } + + init(giniConfiguration: GiniConfiguration = .shared) { + self.giniConfiguration = giniConfiguration + super.init(collectionViewLayout: OpenWithTutorialCollectionFlowLayout()) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init() should be called instead") + } + + override func viewDidLoad() { + super.viewDidLoad() + self.title = .localized(resource: HelpStrings.openWithTutorialTitle) + + if #available(iOS 13.0, *) { + view.backgroundColor = Colors.Gini.dynamicPearl + } else { + view.backgroundColor = Colors.Gini.pearl + } + + self.collectionView!.backgroundColor = nil + self.collectionView.contentInsetAdjustmentBehavior = .never + + self.collectionView!.register(OpenWithTutorialCollectionCell.self, + forCellWithReuseIdentifier: openWithTutorialCollectionCellIdentifier) + self.collectionView!.register(OpenWithTutorialCollectionHeader.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: openWithTutorialCollectionHeaderIdentifier) + + stepsCollectionLayout.minimumLineSpacing = 1 + stepsCollectionLayout.minimumInteritemSpacing = 1 + stepsCollectionLayout.estimatedItemSize = estimatedCellSize(widthParentSize: view.frame.size) + self.edgesForExtendedLayout = [] + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate(alongsideTransition: { [unowned self] _ in + self.stepsCollectionLayout.estimatedItemSize = self.estimatedCellSize(widthParentSize: size) + self.collectionView?.collectionViewLayout.invalidateLayout() + }) + } + + private func estimatedCellSize(widthParentSize size: CGSize) -> CGSize { + if size.width > size.height && UIDevice.current.isIpad { + let width: CGFloat = round(UIScreen.main.bounds.width / CGFloat(self.items.count) - + CGFloat(self.stepsCollectionLayout.minimumInteritemSpacing * CGFloat(self.items.count - 1))) + return CGSize(width: width, height: size.height) + } else { + return CGSize(width: UIScreen.main.bounds.width, height: 100) + } + } + + // MARK: UICollectionViewDataSource + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + override func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: openWithTutorialCollectionCellIdentifier, + for: indexPath) as? OpenWithTutorialCollectionCell)! + cell.fillWith(item: items[indexPath.row], at: indexPath.row, giniConfiguration: giniConfiguration) + + return cell + } + + override func collectionView(_ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath) -> UICollectionReusableView { + let header = (collectionView + .dequeueReusableSupplementaryView(ofKind: kind, + withReuseIdentifier: openWithTutorialCollectionHeaderIdentifier, + for: indexPath) as? OpenWithTutorialCollectionHeader)! + header.headerTitle.font = giniConfiguration + .customFont.with(weight: .regular, + size: OpenWithTutorialCollectionHeader.maxHeaderFontSize, + style: .body) + header.headerTitle.text = headerTitle + return header + } + +} + +// MARK: UICollectionViewDelegateFlowLayout + +extension OpenWithTutorialViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + referenceSizeForHeaderInSection section: Int) -> CGSize { + let height: CGFloat = collectionView.frame.width > collectionView.frame.height ? 0 : 130 + + return CGSize(width: UIScreen.main.bounds.width, height: height) + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpBottomAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpBottomAdapter.swift deleted file mode 100644 index b820049..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpBottomAdapter.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// HelpBottomNavigationBarAdapter.swift -// -// -// Created by Krzysztof Kryniecki on 04/10/2022. -// - -import UIKit - -/** -Protocol for injecting a custom bottom navigation bar on the help screens. - -- note: Bottom navigation only. -*/ -public protocol HelpBottomNavigationBarAdapter: InjectedViewAdapter { - - /** - * Set the callback for the back button action. - * - * - Parameter callback: An action callback, which should be retained and called in back button action method - */ - func setBackButtonClickedActionCallback(_ callback: @escaping () -> Void) -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpBottomBarEnabledViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpBottomBarEnabledViewController.swift deleted file mode 100644 index bfe6b04..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpBottomBarEnabledViewController.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// HelpBottomBarEnabledViewController.swift -// -// -// Created by Krzysztof Kryniecki on 05/10/2022. -// - -import UIKit - -protocol HelpBottomBarEnabledViewController: UIViewController { - var bottomNavigationBar: UIView? {get set} - var navigationBarBottomAdapter: HelpBottomNavigationBarAdapter? {get set} - - func configureBottomNavigationBar( - configuration: GiniConfiguration, - under underView: UIView) -} - -extension HelpBottomBarEnabledViewController { - - func configureCustomTopNavigationBar() { - navigationItem.leftBarButtonItem = nil - navigationItem.setHidesBackButton(true, animated: true) - } - - private func configureBottomNavigationBarConstraints( - bottomNavigationBar: UIView, - superView: UIView, - under view: UIView - ) { - bottomNavigationBar.translatesAutoresizingMaskIntoConstraints = false - superView.addSubview(bottomNavigationBar) - superView.bringSubviewToFront(bottomNavigationBar) - NSLayoutConstraint.activate([ - bottomNavigationBar.bottomAnchor.constraint(equalTo: superView.bottomAnchor), - bottomNavigationBar.leadingAnchor.constraint(equalTo: superView.leadingAnchor), - bottomNavigationBar.trailingAnchor.constraint(equalTo: superView.trailingAnchor), - bottomNavigationBar.heightAnchor.constraint(equalToConstant: bottomNavigationBar.frame.height), - view.bottomAnchor.constraint(equalTo: bottomNavigationBar.topAnchor) - ]) - superView.layoutSubviews() - } - - func configureBottomNavigationBar( - configuration: GiniConfiguration, - under underView: UIView) { - if configuration.bottomNavigationBarEnabled { - configureCustomTopNavigationBar() - if let bottomBarAdapter = configuration.helpNavigationBarBottomAdapter { - navigationBarBottomAdapter = bottomBarAdapter - } else { - navigationBarBottomAdapter = DefaultHelpBottomNavigationBarAdapter() - } - - navigationBarBottomAdapter?.setBackButtonClickedActionCallback { [weak self] in - self?.navigationController?.popViewController(animated: true) - } - if let adapter = navigationBarBottomAdapter { - let injectedView = adapter.injectedView() - configureBottomNavigationBarConstraints( - bottomNavigationBar: injectedView, - superView: view, - under: underView) - bottomNavigationBar = injectedView - } - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpCellProtocol.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpCellProtocol.swift deleted file mode 100644 index 032c766..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpCellProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// HelpCellProtocol.swift -// -// -// Created by Krzysztof Kryniecki on 02/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -protocol HelpCell: UITableViewCell { - static var reuseIdentifier: String { get } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpDataSource.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpDataSource.swift deleted file mode 100644 index 2d98d5a..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Protocols/HelpDataSource.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// HelpDataSource.swift -// -// -// Created by Krzysztof Kryniecki on 23/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -protocol HelpDataSource: UITableViewDelegate, UITableViewDataSource { - init( - configuration: GiniConfiguration - ) -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Supported formats/SupportedFormatsTableViewCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Supported formats/SupportedFormatsTableViewCell.swift new file mode 100644 index 0000000..cff9f90 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/Supported formats/SupportedFormatsTableViewCell.swift @@ -0,0 +1,60 @@ +// +// SupportedFormatsTableViewCell.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 10/23/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import UIKit + +final class SupportedFormatsTableViewCell: UITableViewCell { + + let imageViewSize = CGSize(width: 12, height: 12) + let imageBackgroundSize = CGSize(width: 22, height: 22) + + lazy var imageBackgroundView: UIView = { + let view = UIView(frame: CGRect(origin: .zero, size: self.imageBackgroundSize)) + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = view.frame.width / 2 + return view + }() + + func addSubViewsAndlayout() { + + if let imageView = imageView { + imageView.tintColor = .white + imageView.frame = CGRect(origin: CGPoint(x: imageView.frame.origin.x, + y: (self.frame.height - imageViewSize.height) / 2), + size: imageViewSize) + contentView.insertSubview(imageBackgroundView, belowSubview: imageView) + addConstraints() + } + + if let textLabel = textLabel { + textLabel.numberOfLines = 0 + let textOrigin = CGPoint(x: textLabel.frame.origin.x + imageBackgroundSize.width - imageViewSize.width, + y: textLabel.frame.origin.y) + textLabel.frame.origin = textOrigin + } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + addSubViewsAndlayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func addConstraints() { + Constraints.active(item: imageBackgroundView, attr: .centerX, relatedBy: .equal, to: imageView!, attr: .centerX) + Constraints.active(item: imageBackgroundView, attr: .centerY, relatedBy: .equal, to: imageView!, attr: .centerY) + + Constraints.active(item: imageBackgroundView, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 22) + Constraints.active(item: imageBackgroundView, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 22) + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Supported formats/SupportedFormatsViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Supported formats/SupportedFormatsViewController.swift new file mode 100644 index 0000000..c780697 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Help/Supported formats/SupportedFormatsViewController.swift @@ -0,0 +1,114 @@ +// +// SupportedTypesViewController.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 10/19/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import UIKit + +typealias SupportedFormatCollectionSection = (title: String, + items: [String], + itemsImage: UIImage?, + itemsImageBackgroundColor: UIColor) + +final class SupportedFormatsViewController: UITableViewController { + + let supportedFormatsCellIdentifier = "SupportedFormatsCellIdentifier" + let rowHeight: CGFloat = 70 + let sectionHeight: CGFloat = 70 + fileprivate let giniConfiguration: GiniConfiguration + lazy var sections: [SupportedFormatCollectionSection] = { + var sections: [SupportedFormatCollectionSection] = [ + (.localized(resource: HelpStrings.supportedFormatsSection1Title), + [.localized(resource: HelpStrings.supportedFormatsSection1Item1Text)], + UIImageNamedPreferred(named: "supportedFormatsIcon"), + GiniConfiguration.shared.supportedFormatsIconColor), + (.localized(resource: HelpStrings.supportedFormatsSection2Title), + [.localized(resource: HelpStrings.supportedFormatsSection2Item1Text), + .localized(resource: HelpStrings.supportedFormatsSection2Item2Text)], + UIImageNamedPreferred(named: "nonSupportedFormatsIcon"), + GiniConfiguration.shared.nonSupportedFormatsIconColor) + ] + + if GiniConfiguration.shared.fileImportSupportedTypes != .none { + if GiniConfiguration.shared.fileImportSupportedTypes == .pdf_and_images { + sections[0].items.append(.localized(resource: HelpStrings.supportedFormatsSection1Item2Text)) + } + sections[0].items.append(.localized(resource: HelpStrings.supportedFormatsSection1Item3Text)) + } + if GiniConfiguration.shared.qrCodeScanningEnabled { + sections[0].items.append(.localized(resource: HelpStrings.supportedFormatsSection1Item4Text)) + } + return sections + }() + + init(giniConfiguration: GiniConfiguration = .shared) { + self.giniConfiguration = giniConfiguration + super.init(style: .plain) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func configureTableView() { + tableView.register(SupportedFormatsTableViewCell.self, forCellReuseIdentifier: supportedFormatsCellIdentifier) + tableView.rowHeight = rowHeight + tableView.tableFooterView = UIView() + tableView.sectionHeaderHeight = sectionHeight + tableView.allowsSelection = false + + if #available(iOS 13.0, *) { + tableView.backgroundColor = Colors.Gini.dynamicPearl + } else { + tableView.backgroundColor = Colors.Gini.pearl + } + + tableView.alwaysBounceVertical = false + tableView.contentInsetAdjustmentBehavior = .never + } + + override func viewDidLoad() { + super.viewDidLoad() + title = .localized(resource: HelpStrings.supportedFormatsTitle) + configureTableView() + edgesForExtendedLayout = [] + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].items.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let section = sections[indexPath.section] + let item = section.items[indexPath.row] + + let cell = (tableView.dequeueReusableCell(withIdentifier: supportedFormatsCellIdentifier, + for: indexPath) as? SupportedFormatsTableViewCell)! + cell.textLabel?.text = item + cell.textLabel?.font = giniConfiguration.customFont.with(weight: .regular, size: 14, style: .body) + cell.imageView?.image = section.itemsImage + cell.imageBackgroundView.backgroundColor = section.itemsImageBackgroundColor + + return cell + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sections[section].title + } + + override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + if let header = view as? UITableViewHeaderFooterView { + header.backgroundView?.backgroundColor = nil + } + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpFormatsViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpFormatsViewController.swift deleted file mode 100644 index 1563bb1..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpFormatsViewController.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// HelpFormatsViewController.swift -// -// -// Created by Krzysztof Kryniecki on 03/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -final class HelpFormatsViewController: UIViewController, HelpBottomBarEnabledViewController { - - var bottomNavigationBar: UIView? - var navigationBarBottomAdapter: HelpBottomNavigationBarAdapter? - - lazy var tableView: UITableView = { - var tableView: UITableView - if #available(iOS 13.0, *) { - tableView = UITableView(frame: .zero, style: .insetGrouped) - } else { - tableView = UITableView(frame: .zero, style: .grouped) - } - tableView.translatesAutoresizingMaskIntoConstraints = false - return tableView - }() - private (set) var dataSource: HelpFormatsDataSource - private var giniConfiguration: GiniConfiguration - private let tableRowHeight: CGFloat = 44 - private let sectionHeight: CGFloat = 70 - - init(giniConfiguration: GiniConfiguration) { - self.giniConfiguration = giniConfiguration - self.dataSource = HelpFormatsDataSource(configuration: giniConfiguration) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: GiniMargins.margin, right: 0) - tableView.reloadData() - } - - private func setupView() { - configureMainView() - configureTableView() - configureConstraints() - edgesForExtendedLayout = [] - } - - private func configureMainView() { - title = NSLocalizedStringPreferredFormat( - "ginicapture.help.supportedFormats.title", - comment: "Supported formats screen title") - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - view.addSubview(tableView) - configureBottomNavigationBar( - configuration: giniConfiguration, - under: tableView) - } - - private func configureTableView() { - tableView.register( - UINib( - nibName: "HelpFormatCell", - bundle: giniCaptureBundle()), - forCellReuseIdentifier: HelpFormatCell.reuseIdentifier) - tableView.register( - UINib( - nibName: "HelpFormatSectionHeader", - bundle: giniCaptureBundle()), - forHeaderFooterViewReuseIdentifier: HelpFormatSectionHeader.reuseIdentifier) - tableView.delegate = self.dataSource - tableView.dataSource = self.dataSource - tableView.estimatedRowHeight = tableRowHeight - tableView.rowHeight = UITableView.automaticDimension - tableView.tableFooterView = UIView() - tableView.tableHeaderView = UIView() - tableView.sectionHeaderHeight = sectionHeight - tableView.allowsSelection = false - tableView.backgroundColor = UIColor.clear - tableView.alwaysBounceVertical = false - tableView.contentInsetAdjustmentBehavior = .never - tableView.separatorStyle = .none - if #available(iOS 14.0, *) { - var bgConfig = UIBackgroundConfiguration.listPlainCell() - bgConfig.backgroundColor = UIColor.clear - UITableViewHeaderFooterView.appearance().backgroundConfiguration = bgConfig - } - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - tableView.reloadData() - } - - private func configureConstraints() { - if giniConfiguration.bottomNavigationBarEnabled == false { - NSLayoutConstraint.activate([tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]) - } - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: GiniMargins.margin) - ]) - if UIDevice.current.isIpad { - NSLayoutConstraint.activate([ - tableView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: GiniMargins.iPadAspectScale), - tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor) - ]) - } else { - NSLayoutConstraint.activate([ - tableView.leadingAnchor.constraint( - equalTo: view.leadingAnchor, - constant: GiniMargins.margin), - tableView.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -GiniMargins.margin) - ]) - } - view.layoutSubviews() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpImportViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpImportViewController.swift deleted file mode 100644 index 963e597..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpImportViewController.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// HelpImportViewController.swift -// -// -// Created by Krzysztof Kryniecki on 03/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -final class HelpImportViewController: UIViewController, HelpBottomBarEnabledViewController { - - var bottomNavigationBar: UIView? - var navigationBarBottomAdapter: HelpBottomNavigationBarAdapter? - - private enum HelpImportCellType { - case selectInvoice - case importToApp - case dragAndDrop - } - - private lazy var tableView: UITableView = { - let tableView = UITableView() - tableView.translatesAutoresizingMaskIntoConstraints = false - return tableView - }() - private var dataSource: [HelpImportCellType] = [.selectInvoice, .importToApp, .dragAndDrop] - private var giniConfiguration: GiniConfiguration - - init(giniConfiguration: GiniConfiguration) { - self.giniConfiguration = giniConfiguration - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(giniConfiguration:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: GiniMargins.margin * 2, right: 0) - } - - private func setupView() { - if UIDevice.current.isIphone { - dataSource = [.selectInvoice, .importToApp] - } else { - dataSource = [.selectInvoice, .importToApp, .dragAndDrop] - } - configureMainView() - configureTableView() - configureConstraints() - } - - func configureMainView() { - self.title = NSLocalizedStringPreferredFormat( - "ginicapture.help.import.title", - comment: "Help Import screen title") - view.addSubview(tableView) - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - edgesForExtendedLayout = [] - configureBottomNavigationBar( - configuration: giniConfiguration, - under: tableView) - } - - private func configureTableView() { - tableView.dataSource = self - tableView.delegate = self - tableView.separatorStyle = .none - tableView.allowsSelection = false - tableView.backgroundColor = UIColor.clear - tableView.tableFooterView = UIView() - tableView.estimatedRowHeight = 300 - tableView.register( - UINib( - nibName: "HelpImportCell", - bundle: giniCaptureBundle()), - forCellReuseIdentifier: HelpImportCell.reuseIdentifier) - tableView.contentInsetAdjustmentBehavior = .never - } - - private func configureConstraints() { - if !giniConfiguration.bottomNavigationBarEnabled { - NSLayoutConstraint.activate([ - tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)]) - } - view.addConstraints([ - tableView.topAnchor.constraint(equalTo: view.topAnchor) - ]) - if UIDevice.current.isIpad { - view.addConstraints([ - tableView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: GiniMargins.iPadAspectScale), - tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor) - ]) - } else { - view.addConstraints([ - tableView.leadingAnchor.constraint( - equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint( - equalTo: view.trailingAnchor) - ]) - } - view.layoutSubviews() - } -} - -extension HelpImportViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } -} - -extension HelpImportViewController: UITableViewDataSource { - - private func configureCellAccessibility( - cell: HelpImportCell, - item: String) { - cell.importImageView?.accessibilityTraits = .image - cell.importImageView.accessibilityLabel = item - } - - private func configureAssetsForCell( - cell: HelpImportCell, - itemType: HelpImportCellType, - rowNumber: Int) { - let headerTitle: String - let accessibilityDescription: String - - switch itemType { - case .selectInvoice: - headerTitle = "\(rowNumber). " + NSLocalizedStringPreferredFormat( - "ginicapture.help.import.selectInvoice.title", - comment: "Select an invoice header") - cell.headerLabel.text = headerTitle - cell.descriptionLabel.text = NSLocalizedStringPreferredFormat( - "ginicapture.help.import.selectInvoice.desc", - comment: "Select an invoice description") - cell.importImageView.image = UIImageNamedPreferred(named: "helpImport1") - - accessibilityDescription = NSLocalizedStringPreferredFormat( - "ginicapture.help.import.selectInvoice.accessibility", - comment: "Select an invoice accessibility description") - case .importToApp: - headerTitle = "\(rowNumber). " + NSLocalizedStringPreferredFormat( - "ginicapture.help.import.importtoapp.title", comment: "Import to app header") - cell.headerLabel.text = headerTitle - cell.descriptionLabel.text = NSLocalizedStringPreferredFormat( - "ginicapture.help.import.importtoapp.desc", - comment: "Import to app description") - cell.importImageView.image = UIImageNamedPreferred(named: "helpImport2") - - accessibilityDescription = NSLocalizedStringPreferredFormat( - "ginicapture.help.import.importtoapp.accessibility", - comment: "Import to app accessibility description") - case .dragAndDrop: - headerTitle = "\(rowNumber). " + NSLocalizedStringPreferredFormat( - "ginicapture.help.import.draganddrop.title", comment: "Drag and Drop header") - cell.headerLabel.text = headerTitle - cell.descriptionLabel.text = NSLocalizedStringPreferredFormat( - "ginicapture.help.import.draganddrop.desc", - comment: "Drag and Drop description") - cell.importImageView.image = UIImageNamedPreferred(named: "helpImport3") - - accessibilityDescription = NSLocalizedStringPreferredFormat( - "ginicapture.help.import.draganddrop.accessibility", - comment: "Drag and Drop accessibility description") - } - configureCellAccessibility(cell: cell, item: accessibilityDescription) - } - - private func configureCell(cell: HelpImportCell, indexPath: IndexPath) { - let itemType = dataSource[indexPath.row] - let rowNumber = indexPath.row + 1 - configureAssetsForCell(cell: cell, itemType: itemType, rowNumber: rowNumber) - cell.backgroundColor = UIColor.clear - cell.headerLabel.textColor = GiniColor(light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - cell.headerLabel.backgroundColor = UIColor.clear - cell.headerLabel.adjustsFontForContentSizeCategory = true - cell.headerLabel.font = giniConfiguration.textStyleFonts[.bodyBold] - cell.descriptionLabel.backgroundColor = UIColor.clear - cell.descriptionLabel.textColor = GiniColor(light: UIColor.GiniCapture.dark6, - dark: UIColor.GiniCapture.dark7).uiColor() - cell.descriptionLabel.font = giniConfiguration.textStyleFonts[.body] - cell.descriptionLabel.adjustsFontForContentSizeCategory = true - cell.contentView.backgroundColor = UIColor.clear - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if let cell = tableView.dequeueReusableCell(withIdentifier: HelpImportCell.reuseIdentifier) as? HelpImportCell { - configureCell(cell: cell, indexPath: indexPath) - return cell - } - fatalError("undefined cell") - } - - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return dataSource.count - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpMenuViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpMenuViewController.swift deleted file mode 100644 index 71432fb..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpMenuViewController.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// HelpMenuViewController.swift -// GiniCapture -// -// Created by Enrique del Pozo Gómez on 10/18/17. -// Copyright © 2017 Gini GmbH. All rights reserved. -// - -import UIKit - -/** - The `HelpMenuViewControllerDelegate` protocol defines methods that allow you to handle table item selection actions. - */ - -protocol HelpMenuViewControllerDelegate: AnyObject { - func help(_ menuViewController: HelpMenuViewController, didSelect item: HelpMenuItem) -} - -/** - The `HelpMenuViewController` provides explanations on how to take better pictures, how to - use the _Open with_ feature and which formats are supported by the Gini Capture SDK. - */ - -final class HelpMenuViewController: UIViewController, HelpBottomBarEnabledViewController { - - weak var delegate: HelpMenuViewControllerDelegate? - private (set) var dataSource: HelpMenuDataSource - private let giniConfiguration: GiniConfiguration - private let tableRowHeight: CGFloat = 44 - var navigationBarBottomAdapter: HelpBottomNavigationBarAdapter? - var bottomNavigationBar: UIView? - private var bottomConstraint: NSLayoutConstraint? - - lazy var tableView: UITableView = { - let tableView = UITableView() - tableView.translatesAutoresizingMaskIntoConstraints = false - return tableView - }() - - init(giniConfiguration: GiniConfiguration) { - self.giniConfiguration = giniConfiguration - self.dataSource = HelpMenuDataSource(configuration: giniConfiguration) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(giniConfiguration:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - self.dataSource.delegate = self - setupView() - } - - private func setupView() { - configureMainView() - configureTableView() - configureConstraints() - edgesForExtendedLayout = [] - } - - private func configureTableView() { - tableView.dataSource = self.dataSource - tableView.delegate = self.dataSource - tableView.backgroundColor = UIColor.clear - tableView.showsVerticalScrollIndicator = false - tableView.tableHeaderView = UIView() - tableView.tableFooterView = UIView() - tableView.register( - UINib( - nibName: "HelpMenuCell", - bundle: giniCaptureBundle()), - forCellReuseIdentifier: HelpMenuCell.reuseIdentifier) - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = tableRowHeight - tableView.contentInsetAdjustmentBehavior = .never - tableView.separatorStyle = .none - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - tableView.reloadData() - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - } - - private func configureMainView() { - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - view.addSubview(tableView) - title = NSLocalizedStringPreferredFormat("ginicapture.help.menu.title", comment: "Help Import screen title") - view.layoutSubviews() - configureBottomNavigationBar( - configuration: giniConfiguration, - under: tableView) - } - - private func configureConstraints() { - if giniConfiguration.bottomNavigationBarEnabled == false { - NSLayoutConstraint.activate([tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]) - } - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: GiniMargins.margin) - ]) - if UIDevice.current.isIpad { - NSLayoutConstraint.activate([ - tableView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: GiniMargins.iPadAspectScale), - tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor) - ]) - } else { - NSLayoutConstraint.activate([ - tableView.leadingAnchor.constraint( - equalTo: view.leadingAnchor, - constant: GiniMargins.margin), - tableView.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -GiniMargins.margin) - ]) - } - view.layoutSubviews() - } - - @objc func back() { - navigationController?.popViewController(animated: true) - } -} - -// MARK: - HelpMenuDataSourceDelegate - -extension HelpMenuViewController: HelpMenuDataSourceDelegate { - func didSelectHelpItem(didSelect item: HelpMenuItem) { - delegate?.help(self, didSelect: item) - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpTipsViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpTipsViewController.swift deleted file mode 100644 index 30afaba..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/ViewControllers/HelpTipsViewController.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// HelpTipsViewController.swift -// GiniCapture -// -// Created by Enrique del Pozo Gómez on 10/6/17. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -/** - The `HelpTipsViewController` provides a custom no results screen which shows some capture - suggestions when there is no results when analysing an image. - */ - -final class HelpTipsViewController: UIViewController, HelpBottomBarEnabledViewController { - var bottomNavigationBar: UIView? - var navigationBarBottomAdapter: HelpBottomNavigationBarAdapter? - - private lazy var tableView: UITableView = { - var tableView: UITableView - if #available(iOS 13.0, *) { - tableView = UITableView(frame: .zero, style: .insetGrouped) - } else { - tableView = UITableView(frame: .zero, style: .grouped) - } - tableView.translatesAutoresizingMaskIntoConstraints = false - return tableView - }() - private (set) var dataSource: HelpTipsDataSource - private var giniConfiguration: GiniConfiguration - private let tableRowHeight: CGFloat = 76 - - init(giniConfiguration: GiniConfiguration) { - self.giniConfiguration = giniConfiguration - self.dataSource = HelpTipsDataSource(configuration: giniConfiguration) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(title:subHeaderText:topViewText:topViewIcon:bottomButtonText:bottomButtonIcon:)" + - "has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: GiniMargins.margin, right: 0) - tableView.reloadData() - } - - private func setupView() { - configureMainView() - configureTableView() - configureConstraints() - } - - func configureMainView() { - view.addSubview(tableView) - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - edgesForExtendedLayout = [] - tableView.bounces = false - configureBottomNavigationBar( - configuration: giniConfiguration, - under: tableView) - } - - private func configureTableView() { - tableView.dataSource = self.dataSource - tableView.delegate = self.dataSource - tableView.backgroundColor = UIColor.clear - tableView.tableFooterView = UIView() - tableView.register( - UINib( - nibName: "HelpTipCell", - bundle: giniCaptureBundle()), - forCellReuseIdentifier: HelpTipCell.reuseIdentifier) - tableView.register( - UINib( - nibName: "HelpFormatSectionHeader", - bundle: giniCaptureBundle()), - forHeaderFooterViewReuseIdentifier: HelpFormatSectionHeader.reuseIdentifier) - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = tableRowHeight - tableView.contentInsetAdjustmentBehavior = .never - tableView.separatorStyle = .none - tableView.allowsSelection = false - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - tableView.reloadData() - } - - private func configureConstraints() { - if giniConfiguration.bottomNavigationBarEnabled == false { - NSLayoutConstraint.activate([tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]) - } - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: GiniMargins.margin) - ]) - if UIDevice.current.isIpad { - NSLayoutConstraint.activate([ - tableView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: GiniMargins.iPadAspectScale), - tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor) - ]) - } else { - NSLayoutConstraint.activate([ - tableView.leadingAnchor.constraint( - equalTo: view.leadingAnchor, - constant: GiniMargins.margin), - tableView.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -GiniMargins.margin) - ]) - } - view.layoutSubviews() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/DefaultHelpBottomNavigationBarAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Views/DefaultHelpBottomNavigationBarAdapter.swift deleted file mode 100644 index 736581a..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/DefaultHelpBottomNavigationBarAdapter.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// DefaultHelpBottomNavigationBarAdapter.swift -// -// -// Created by Krzysztof Kryniecki on 04/10/2022. -// - -import UIKit - -final class DefaultHelpBottomNavigationBarAdapter: HelpBottomNavigationBarAdapter { - private var backButtonCallback: (() -> Void)? - - // Add the callback whenever the back button is clicked - func setBackButtonClickedActionCallback(_ callback: @escaping () -> Void) { - backButtonCallback = callback - } - - func injectedView() -> UIView { - if let navigationBarView = BackButtonBottomNavigationBar().loadNib() as? BackButtonBottomNavigationBar { - navigationBarView.backButton.addTarget( - self, - action: #selector(backButtonClicked), - for: .touchUpInside) - return navigationBarView - } else { - return UIView() - } - } - - @objc func backButtonClicked() { - backButtonCallback?() - } - - func onDeinit() { - backButtonCallback = nil - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpFormatCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpFormatCell.swift deleted file mode 100644 index 1ed9c6a..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpFormatCell.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// HelpFormatCell.swift -// -// -// Created by Krzysztof Kryniecki on 03/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -final class HelpFormatCell: UITableViewCell, HelpCell { - @IBOutlet weak var iconImageView: UIImageView! - @IBOutlet weak var descriptionLabel: UILabel! - - @IBOutlet weak var separatorView: UIView! - static var reuseIdentifier: String = "kHelpFormatCell" - - override func awakeFromNib() { - super.awakeFromNib() - self.isAccessibilityElement = false - self.iconImageView.isAccessibilityElement = true - self.descriptionLabel.isAccessibilityElement = true - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpFormatSectionHeader.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpFormatSectionHeader.swift deleted file mode 100644 index 5b54826..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpFormatSectionHeader.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// HelpFormatSectionHeader.swift -// -// -// Created by Krzysztof Kryniecki on 12/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -final class HelpFormatSectionHeader: UITableViewHeaderFooterView { - @IBOutlet weak var titleLabel: UILabel! - static var reuseIdentifier: String = "kHelpFormatSectionHeader" -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpImportCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpImportCell.swift deleted file mode 100644 index be5f85b..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpImportCell.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// HelpImportCell.swift -// -// -// Created by Krzysztof Kryniecki on 03/08/2022. -// - -import UIKit - -final class HelpImportCell: UITableViewCell { - static var reuseIdentifier: String = "kHelpImportCell" - - @IBOutlet weak var headerLabel: UILabel! - @IBOutlet weak var descriptionLabel: UILabel! - @IBOutlet weak var importImageView: UIImageView! - - override func awakeFromNib() { - super.awakeFromNib() - self.isAccessibilityElement = false - self.accessibilityElements = [headerLabel as Any, descriptionLabel as Any, importImageView as Any] - self.headerLabel.isAccessibilityElement = true - self.descriptionLabel.isAccessibilityElement = true - self.importImageView.isAccessibilityElement = true - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpMenuCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpMenuCell.swift deleted file mode 100644 index 0143446..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpMenuCell.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// HelpMenuCell.swift -// -// -// Created by Krzysztof Kryniecki on 02/08/2022. -// - -import UIKit - -final class HelpMenuCell: UITableViewCell, HelpCell { - static var reuseIdentifier: String = "kHelpMenuCell" - weak var separatorView: UIView! - @IBOutlet weak var titleLabel: UILabel! - - override func awakeFromNib() { - super.awakeFromNib() - setupView() - } - - func setupView() { - let separator = UIView() - separator.translatesAutoresizingMaskIntoConstraints = false - self.addSubview(separator) - separatorView = separator - configureConstraints() - } - - private func configureConstraints() { - if let separatorView = separatorView { - NSLayoutConstraint.activate([ - separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), - separatorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), - separatorView.heightAnchor.constraint(equalToConstant: 1), - separatorView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -1) - ]) - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpTipCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpTipCell.swift deleted file mode 100644 index 8657706..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Help/Views/HelpTipCell.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// HelpTipCell.swift -// -// -// Created by Krzysztof Kryniecki on 01/08/2022. -// - -import UIKit - -final class HelpTipCell: UITableViewCell, HelpCell { - @IBOutlet weak var separatorView: UIView! - @IBOutlet weak var iconImageView: UIImageView! - @IBOutlet weak var headerLabel: UILabel! - @IBOutlet weak var descriptionLabel: UILabel! - - static var reuseIdentifier: String = "kHelpTipCell" - - override func awakeFromNib() { - super.awakeFromNib() - self.isAccessibilityElement = false - self.iconImageView.isAccessibilityElement = true - self.headerLabel.isAccessibilityElement = true - self.descriptionLabel.isAccessibilityElement = true - self.iconImageView.adjustsImageSizeForAccessibilityContentSizeCategory = true - } - -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewCollectionsCellPresenter.swift b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewCollectionsCellPresenter.swift new file mode 100644 index 0000000..6c0f128 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewCollectionsCellPresenter.swift @@ -0,0 +1,205 @@ +// +// MultipageReviewCollectionCellPresenter.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 12/28/18. +// + +import UIKit + +protocol MultipageReviewCollectionCellPresenterDelegate: AnyObject { + func multipage(_ reviewCollectionCellPresenter: MultipageReviewCollectionCellPresenter, + didUpdate cell: MultipageReviewCollectionCellPresenter.MultipageCollectionCellType, + at indexPath: IndexPath) +} + +final class MultipageReviewCollectionCellPresenter { + + weak var delegate: MultipageReviewCollectionCellPresenterDelegate? + var thumbnails: [String: [ThumbnailType: UIImage]] = [:] + fileprivate let giniConfiguration: GiniConfiguration + fileprivate let thumbnailsQueue = DispatchQueue(label: "Thumbnails queue") + + enum MultipageCollectionCellType { + case main(MultipageReviewMainCollectionCell, (NoticeActionType) -> Void) + case pages(MultipageReviewPagesCollectionCell) + } + + enum ThumbnailType { + case big, small + + var scale: CGFloat { + switch self { + case .big: + return 1.0 + case .small: + return 1/4 + } + } + } + + init(giniConfiguration: GiniConfiguration = .shared) { + self.giniConfiguration = giniConfiguration + } + + func setUp(_ cell: MultipageCollectionCellType, + with page: GiniCapturePage, + isSelected: Bool, + at indexPath: IndexPath) -> UICollectionViewCell { + + let collectionCell: UICollectionViewCell + switch cell { + case .main(let mainCell, let errorAction): + setUp(cell: mainCell, + with: page, + fetchThumbnailTrigger: fetchThumbnailImage(for: page, of: .big, in: cell, at: indexPath), + didTapErrorNotice: errorAction) + collectionCell = mainCell + case .pages(let pageCell): + setUp(cell: pageCell, + with: page, + fetchThumbnailTrigger: fetchThumbnailImage(for: page, of: .small, in: cell, at: indexPath), + pageIndex: indexPath.row, + selected: isSelected) + collectionCell = pageCell + } + + return collectionCell + } + + func rotateThumbnails(for page: GiniCapturePage) { + if let smallRotatedImage = thumbnails[page.document.id]?[.small]?.rotated90Degrees() { + thumbnails[page.document.id]![.small] = smallRotatedImage + } + + if let bigRotatedImage = thumbnails[page.document.id]?[.big]?.rotated90Degrees() { + thumbnails[page.document.id]![.big] = bigRotatedImage + } + } +} + +// MARK: - Cells setup + +fileprivate extension MultipageReviewCollectionCellPresenter { + + // MARK: - MultipageReviewPagesCollectionCell + + func setUp(cell: MultipageReviewPagesCollectionCell, + with page: GiniCapturePage, + fetchThumbnailTrigger: @autoclosure () -> Void, + pageIndex: Int, + selected: Bool) { + // Thumbnail set + if let thumbnail = self.thumbnails[page.document.id, default: [:]][.small] { + cell.documentImage.contentMode = thumbnail.size.width > thumbnail.size.height ? + .scaleAspectFit : + .scaleAspectFill + cell.documentImage.image = thumbnail + } else { + cell.documentImage.image = nil + fetchThumbnailTrigger() + } + + cell.pageIndicatorLabel.text = "\(pageIndex + 1)" + cell.giniConfiguration = giniConfiguration + // Every time the cell is dequeued, its `isSelected` state is set to default, false. + cell.isSelected = selected + + if page.isUploaded { + cell.stateView.update(to: .succeeded) + } else if page.error != nil { + cell.stateView.update(to: .failed) + } else { + cell.stateView.update(to: .loading) + } + } + + // MARK: - MultipageReviewMainCollectionCell + + func setUp(cell: MultipageReviewMainCollectionCell, + with page: GiniCapturePage, + fetchThumbnailTrigger: @autoclosure () -> Void, + didTapErrorNotice action: @escaping (NoticeActionType) -> Void) { + // Thumbnail set + if let thumbnail = self.thumbnails[page.document.id, default: [:]][.big] { + cell.documentImage.image = thumbnail + } else { + cell.documentImage.image = nil + fetchThumbnailTrigger() + } + + if let error = page.error { + setUpErrorView(in: cell, with: error, didTapErrorNoticeAction: action) + cell.errorView.show(false) + } else { + cell.errorView.hide(false, completion: nil) + } + } + + func setUpErrorView(in cell: MultipageReviewMainCollectionCell, + with error: Error, + didTapErrorNoticeAction: @escaping (NoticeActionType) -> Void) { + let buttonTitle: String + let action: NoticeActionType + + switch error { + case is AnalysisError: + buttonTitle = .localized(resource: MultipageReviewStrings.retryActionButton) + action = .retry + default: + buttonTitle = .localized(resource: MultipageReviewStrings.retakeActionButton) + action = .retake + } + + let message: String + + switch error { + case let error as GiniCaptureError: + message = error.message + case let error as CustomDocumentValidationError: + message = error.message + default: + message = DocumentValidationError.unknown.message + } + + cell.errorView.textLabel.text = message + cell.errorView.actionButton.setTitle(buttonTitle, for: .normal) + cell.errorView.userAction = NoticeAction(title: buttonTitle) { + didTapErrorNoticeAction(action) + } + cell.errorView.layoutIfNeeded() + } +} + +// MARK: - Thumbnails + +fileprivate extension MultipageReviewCollectionCellPresenter { + func fetchThumbnailImage(for page: GiniCapturePage, + of type: ThumbnailType, + in cell: MultipageCollectionCellType, + at indexPath: IndexPath) { + thumbnailsQueue.async { [weak self] in + guard let self = self else { return } + let thumbnail = UIImage.downsample(from: page.document.data, + to: self.targetThumbnailSize(from: page.document.data), + scale: type.scale) + self.thumbnails[page.document.id, default: [:]][type] = thumbnail + + DispatchQueue.main.async { + self.delegate?.multipage(self, didUpdate: cell, at: indexPath) + } + } + } + + func targetThumbnailSize(from imageData: Data, screen: UIScreen = .main) -> CGSize { + let imageSize = UIImage(data: imageData)?.size ?? .zero + + if imageSize.width > (screen.bounds.size.width * 2) { + let maxWidth = screen.bounds.size.width * 2 + return CGSize(width: maxWidth, height: imageSize.height * maxWidth / imageSize.width) + } else { + return imageSize + } + + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewMainCollectionCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewMainCollectionCell.swift new file mode 100644 index 0000000..4c6bf1d --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewMainCollectionCell.swift @@ -0,0 +1,89 @@ +// +// MultipageReviewMainCollectionCell.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 1/30/18. +// + +import UIKit + +final class MultipageReviewMainCollectionCell: UICollectionViewCell { + + static let identifier = "MultipageReviewMainCollectionCellIdentifier" + + lazy var documentImage: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + + imageView.accessibilityLabel = NSLocalizedStringPreferredFormat("ginicapture.review.documentImageTitle", comment: "Document") + + return imageView + }() + + lazy var zoomableScrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.delegate = self + scrollView.minimumZoomScale = 1 + scrollView.maximumZoomScale = 5 + + return scrollView + }() + + lazy var errorView: NoticeView = { + let noticeView = NoticeView(text: "", + type: .error, + noticeAction: NoticeAction(title: "", action: {})) + noticeView.translatesAutoresizingMaskIntoConstraints = false + + noticeView.hide(false, completion: nil) + return noticeView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + zoomableScrollView.addSubview(documentImage) + addSubview(zoomableScrollView) + addSubview(errorView) + + addConstraints() + addDoubleTapGesture() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(frame:) has not been implemented") + } + + private func addConstraints() { + Constraints.pin(view: documentImage, toSuperView: zoomableScrollView) + Constraints.pin(view: zoomableScrollView, toSuperView: self) + Constraints.pin(view: errorView, toSuperView: self, positions: [.top, .left, .right]) + Constraints.center(view: documentImage, with: zoomableScrollView) + } + + private func addDoubleTapGesture() { + let tapGesture = UITapGestureRecognizer() + tapGesture.numberOfTapsRequired = 2 + tapGesture.addTarget(self, action: #selector(doubleTapToZoom)) + zoomableScrollView.addGestureRecognizer(tapGesture) + } + + @objc private func doubleTapToZoom() { + if zoomableScrollView.zoomScale == 1.0 { + zoomableScrollView.setZoomScale(2.0, animated: true) + } else { + zoomableScrollView.setZoomScale(1.0, animated: true) + } + } + +} + +// MARK: - UIScrollViewDelegate + +extension MultipageReviewMainCollectionCell: UIScrollViewDelegate { + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return documentImage + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewPagesCollectionCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewPagesCollectionCell.swift new file mode 100644 index 0000000..468052b --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewPagesCollectionCell.swift @@ -0,0 +1,253 @@ +// +// MultipageReviewPagesCollectionCell.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 2/1/18. +// + +import UIKit + +final class MultipageReviewPagesCollectionCell: UICollectionViewCell { + + static let identifier = "MultipageReviewPagesCollectionCellIdentifier" + static let shadowHeight: CGFloat = 2 + static let shadowRadius: CGFloat = 1 + let pageIndicatorCircleSize = CGSize(width: 25, height: 25) + let stateViewSize = CGSize(width: 40, height: 40) + + private var _giniConfiguration: GiniConfiguration? + + var giniConfiguration: GiniConfiguration { + set { + _giniConfiguration = newValue + applyConfiguration(giniConfiguration: giniConfiguration) + } + get { + return _giniConfiguration ?? GiniConfiguration.shared + } + } + + private func applyConfiguration(giniConfiguration: GiniConfiguration) { + draggableIcon.tintColor = giniConfiguration.multipageDraggableIconColor + bottomContainer.backgroundColor = UIColor.from(giniColor: giniConfiguration.multipagePageBackgroundColor) + pageIndicatorLabel.textColor = giniConfiguration.multipagePageIndicatorColor + pageSelectedLine.backgroundColor = giniConfiguration.multipagePageSelectedIndicatorColor + pageIndicatorCircle.layer.borderColor = UIColor.from(giniColor: giniConfiguration.indicatorCircleColor).cgColor + } + + private lazy var roundMask: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = 5.0 + view.layer.masksToBounds = true + return view + }() + + lazy var documentImage: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = Colors.Gini.veryLightGray + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + return imageView + }() + + lazy var stateView: PageStateView = { + let view = PageStateView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = self.stateViewSize.width / 2 + + return view + }() + + private lazy var traslucentBackground: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.black.withAlphaComponent(0.3) + return view + }() + + lazy var draggableIcon: UIImageView = { + let image = UIImage(named: "draggablePageIcon", in: giniCaptureBundle(), compatibleWith: nil) + let imageView = UIImageView(image: image?.withRenderingMode(.alwaysTemplate)) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.tintColor = giniConfiguration.multipageDraggableIconColor + return imageView + }() + + lazy var bottomContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + + view.backgroundColor = UIColor.from(giniColor: giniConfiguration.multipagePageBackgroundColor) + + return view + }() + + lazy var pageIndicatorLabel: UILabel = { + var label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = giniConfiguration.multipagePageIndicatorColor + return label + }() + + private lazy var pageIndicatorCircle: UIView = { + var view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.frame.size = self.pageIndicatorCircleSize + view.layer.borderWidth = 1 + view.layer.cornerRadius = self.pageIndicatorCircleSize.width / 2 + updatePageIndicatorCircleColor(pageIndicatorCircle: view) + return view + }() + + lazy var pageSelectedLine: UIView = { + var view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = giniConfiguration.multipagePageSelectedIndicatorColor + view.alpha = 0 + return view + }() + + override var isSelected: Bool { + didSet { + self.pageSelectedLine.alpha = isSelected ? 1 : 0 + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(roundMask) + roundMask.addSubview(bottomContainer) + roundMask.addSubview(pageSelectedLine) + roundMask.addSubview(documentImage) + roundMask.addSubview(traslucentBackground) + roundMask.addSubview(stateView) + bottomContainer.addSubview(pageIndicatorLabel) + bottomContainer.addSubview(pageIndicatorCircle) + bottomContainer.addSubview(draggableIcon) + + addShadow() + addConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(frame:) has not been implemented") + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + + super.traitCollectionDidChange(previousTraitCollection) + + if #available(iOS 13.0, *) { + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + updatePageIndicatorCircleColor(pageIndicatorCircle: pageIndicatorCircle) + } + } + } + + private func updatePageIndicatorCircleColor(pageIndicatorCircle: UIView) { + + pageIndicatorCircle.layer.borderColor = UIColor.from(giniColor: giniConfiguration.indicatorCircleColor).cgColor + } + + //swiftlint:disable function_body_length + fileprivate func addConstraints() { + // roundMask + Constraints.active(item: roundMask, attr: .top, relatedBy: .equal, to: self, attr: .top) + Constraints.active(item: roundMask, attr: .leading, relatedBy: .equal, to: self, attr: .leading) + Constraints.active(item: roundMask, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing) + Constraints.active(item: roundMask, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, + constant: -(MultipageReviewPagesCollectionCell.shadowHeight + + MultipageReviewPagesCollectionCell.shadowRadius), priority: 999) + + // pageIndicator + Constraints.active(item: pageIndicatorLabel, attr: .centerX, relatedBy: .equal, to: pageIndicatorCircle, + attr: .centerX) + Constraints.active(item: pageIndicatorLabel, attr: .centerY, relatedBy: .equal, to: pageIndicatorCircle, + attr: .centerY) + + // stateView + Constraints.active(item: stateView, attr: .centerX, relatedBy: .equal, to: documentImage, attr: .centerX) + Constraints.active(item: stateView, attr: .centerY, relatedBy: .equal, to: documentImage, attr: .centerY) + Constraints.active(item: stateView, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: stateViewSize.height) + Constraints.active(item: stateView, attr: .width, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: stateViewSize.width) + + // pageIndicatorCircle + Constraints.active(item: pageIndicatorCircle, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: pageIndicatorCircleSize.height) + Constraints.active(item: pageIndicatorCircle, attr: .width, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: pageIndicatorCircleSize.width) + Constraints.active(item: pageIndicatorCircle, attr: .top, relatedBy: .equal, to: bottomContainer, + attr: .top, constant: 10, priority: 999) + Constraints.active(item: pageIndicatorCircle, attr: .bottom, relatedBy: .equal, to: bottomContainer, + attr: .bottom, constant: -10, priority: 999) + Constraints.active(item: pageIndicatorCircle, attr: .centerX, relatedBy: .equal, to: bottomContainer, + attr: .centerX) + Constraints.active(item: pageIndicatorCircle, attr: .centerY, relatedBy: .equal, to: bottomContainer, + attr: .centerY) + + // pageSelectedLine + Constraints.active(item: pageSelectedLine, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 4) + Constraints.active(item: pageSelectedLine, attr: .leading, relatedBy: .equal, to: roundMask, attr: .leading) + Constraints.active(item: pageSelectedLine, attr: .trailing, relatedBy: .equal, to: roundMask, attr: .trailing) + Constraints.active(item: pageSelectedLine, attr: .bottom, relatedBy: .equal, to: roundMask, attr: .bottom) + + // draggableIcon + Constraints.active(item: draggableIcon, attr: .top, relatedBy: .equal, to: bottomContainer, attr: .top, + constant: 12, priority: 999) + Constraints.active(item: draggableIcon, attr: .bottom, relatedBy: .equal, to: bottomContainer, attr: .bottom, + constant: -12) + Constraints.active(item: draggableIcon, attr: .leading, relatedBy: .greaterThanOrEqual, to: pageIndicatorCircle, + attr: .trailing, constant: 6, priority: 999) + Constraints.active(item: draggableIcon, attr: .trailing, relatedBy: .lessThanOrEqual, to: bottomContainer, + attr: .trailing, constant: -6) + + // documentImage + Constraints.active(item: documentImage, attr: .top, relatedBy: .equal, to: roundMask, attr: .top) + Constraints.active(item: documentImage, attr: .leading, relatedBy: .equal, to: roundMask, attr: .leading) + Constraints.active(item: documentImage, attr: .trailing, relatedBy: .equal, to: roundMask, attr: .trailing) + Constraints.active(item: documentImage, attr: .bottom, relatedBy: .equal, to: bottomContainer, attr: .top) + + // traslucentBackground + Constraints.active(item: traslucentBackground, attr: .top, relatedBy: .equal, to: documentImage, attr: .top) + Constraints.active(item: traslucentBackground, attr: .leading, relatedBy: .equal, to: documentImage, + attr: .leading) + Constraints.active(item: traslucentBackground, attr: .trailing, relatedBy: .equal, to: documentImage, + attr: .trailing) + Constraints.active(item: traslucentBackground, attr: .bottom, relatedBy: .equal, to: documentImage, + attr: .bottom) + + // bottomContainer + Constraints.active(item: bottomContainer, attr: .bottom, relatedBy: .equal, to: roundMask, attr: .bottom, + priority: 999) + Constraints.active(item: bottomContainer, attr: .leading, relatedBy: .equal, to: roundMask, attr: .leading) + Constraints.active(item: bottomContainer, attr: .trailing, relatedBy: .equal, to: roundMask, attr: .trailing) + Constraints.active(item: bottomContainer, attr: .height, relatedBy: .equal, to: nil, attr: .notAnAttribute, + constant: 46) + } + + fileprivate func addShadow() { + layer.shadowColor = UIColor.black.cgColor + layer.shadowRadius = MultipageReviewPagesCollectionCell.shadowRadius + layer.shadowOpacity = 0.3 + layer.shadowOffset = CGSize(width: 0, + height: MultipageReviewPagesCollectionCell.shadowHeight) + layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath + } + + class func size(in collection: UICollectionView) -> CGSize { + let collectionInset = UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0) + let height = collection.frame.height - + collectionInset.top - + collectionInset.bottom + let aspectRatio: CGFloat = 11 / 20 + let width = height * aspectRatio + + return CGSize(width: width, height: height) + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewPagesCollectionFooter.swift b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewPagesCollectionFooter.swift new file mode 100644 index 0000000..0e50890 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewPagesCollectionFooter.swift @@ -0,0 +1,173 @@ +// +// MultipageReviewPagesCollectionFooter.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 4/26/18. +// + +import UIKit + +final class MultipageReviewPagesCollectionFooter: UICollectionReusableView { + + static let identifier = "MultipageReviewPagesCollectionFooterIdentifier" + var didTapAddButton: (() -> Void)? + static let defaultPadding: CGFloat = 10 + + var trailingConstraint: NSLayoutConstraint? + fileprivate var roundMaskHeightConstraint: NSLayoutConstraint? + fileprivate var roundMaskWidthConstraint: NSLayoutConstraint? + + fileprivate lazy var roundMask: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .gray + view.layer.cornerRadius = 5.0 + view.layer.masksToBounds = true + return view + }() + + fileprivate lazy var addButton: UIButton = { + let button = UIButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(UIImageNamedPreferred(named: "addCircleIcon"), for: .normal) + button.tintColor = .white + button.addTarget(self, action: #selector(addImageButtonAction), for: .touchUpInside) + return button + }() + + lazy var addLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = .localized(resource: MultipageReviewStrings.addButtonLabel) + label.textAlignment = .center + label.numberOfLines = 0 + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(roundMask) + roundMask.addSubview(addButton) + roundMask.addSubview(addLabel) + + addInnerShadow() + addConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + class func size(in collectionView: UICollectionView) -> CGSize { + let padding = self.padding(in: collectionView) + let height = MultipageReviewPagesCollectionFooter.contentSize(in: collectionView).height + let width = MultipageReviewPagesCollectionFooter.contentSize(in: collectionView).width + + padding.left + + padding.right + + return CGSize(width: width, height: height) + } + + class func padding(in collectionView: UICollectionView? = nil) -> UIEdgeInsets { + var rightPadding: CGFloat = defaultPadding + if let collectionView = collectionView { + rightPadding = ((collectionView.frame.width - + MultipageReviewPagesCollectionCell.size(in: collectionView).width) / 2) + } + + return UIEdgeInsets(top: defaultPadding, left: defaultPadding, bottom: defaultPadding, right: rightPadding) + } + + func updateMaskConstraints(with collectionView: UICollectionView) { + roundMaskHeightConstraint?.constant = MultipageReviewPagesCollectionFooter + .contentSize(in: collectionView).height + roundMaskWidthConstraint?.constant = MultipageReviewPagesCollectionFooter + .contentSize(in: collectionView).width + } +} + +// MARK: - Private methods + +extension MultipageReviewPagesCollectionFooter { + + @objc fileprivate func addImageButtonAction() { + UIImpactFeedbackGenerator().impactOccurred() + didTapAddButton?() + } + + fileprivate func addInnerShadow() { + let innerShadow = CALayer() + innerShadow.frame = bounds + + let path = UIBezierPath(rect: innerShadow.bounds.insetBy(dx: -3, dy: -3)) + let cutout = UIBezierPath(rect: innerShadow.bounds).reversing() + path.append(cutout) + innerShadow.shadowPath = path.cgPath + innerShadow.masksToBounds = true + + innerShadow.shadowColor = UIColor.black.cgColor + innerShadow.shadowOffset = CGSize.zero + innerShadow.shadowOpacity = 0.5 + innerShadow.shadowRadius = 3 + + roundMask.layer.addSublayer(innerShadow) + } + + fileprivate class func contentSize(in collectionView: UICollectionView) -> CGSize { + return MultipageReviewPagesCollectionCell.size(in: collectionView) + } + + fileprivate func addConstraints() { + // roundMask + roundMaskHeightConstraint = NSLayoutConstraint(item: roundMask, + attribute: .height, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1.0, + constant: 0) + roundMaskWidthConstraint = NSLayoutConstraint(item: roundMask, + attribute: .width, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1.0, + constant: 0) + Constraints.active(constraint: roundMaskHeightConstraint!) + Constraints.active(constraint: roundMaskWidthConstraint!) + Constraints.active(item: roundMask, attr: .centerY, relatedBy: .equal, to: self, attr: .centerY) + + // addButton + Constraints.active(item: addButton, attr: .centerX, relatedBy: .equal, to: roundMask, attr: .centerX) + Constraints.active(item: addButton, attr: .centerY, relatedBy: .equal, to: roundMask, + attr: .centerY, constant: -20) + Constraints.active(item: addButton, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: 60, priority: 999) + Constraints.active(item: addButton, attr: .width, relatedBy: .equal, to: addButton, attr: .height) + Constraints.active(item: addButton, attr: .top, relatedBy: .greaterThanOrEqual, to: roundMask, + attr: .top, constant: MultipageReviewPagesCollectionFooter.padding().top, priority: 999) + + // addLabel + Constraints.active(item: addLabel, attr: .top, relatedBy: .equal, to: addButton, attr: .bottom, + constant: MultipageReviewPagesCollectionFooter.padding().top) + Constraints.active(item: addLabel, attr: .bottom, relatedBy: .lessThanOrEqual, to: roundMask, attr: .bottom, + constant: -MultipageReviewPagesCollectionFooter.padding().bottom) + Constraints.active(item: addLabel, attr: .leading, relatedBy: .equal, to: roundMask, attr: .leading, + constant: MultipageReviewPagesCollectionFooter.padding().left) + Constraints.active(item: addLabel, attr: .trailing, relatedBy: .equal, to: roundMask, attr: .trailing, + constant: -MultipageReviewPagesCollectionFooter.padding().right, priority: 999) + + // Since it is not possible to add an inset to the footer, but only to the section + // (header and footer are not part of the section), we add a right inset dynamically through a + // constraint in our round container view (roundMask). + trailingConstraint = NSLayoutConstraint(item: roundMask, + attribute: .trailing, + relatedBy: .equal, + toItem: self, + attribute: .trailing, + multiplier: 1.0, + constant: -MultipageReviewPagesCollectionFooter.padding().right) + Constraints.active(constraint: trailingConstraint!) + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewViewController.swift new file mode 100644 index 0000000..d620de2 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/MultipageReviewViewController.swift @@ -0,0 +1,734 @@ +// +// MultipageReviewViewController.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 1/26/18. +// + +import UIKit + +/** + The MultipageReviewViewControllerDelegate protocol defines methods that allow you to handle user actions in the + MultipageReviewViewControllerDelegate + (rotate, reorder, tap add, delete...) + + - note: Component API only. + */ +public protocol MultipageReviewViewControllerDelegate: AnyObject { + /** + Called when a user reorder the pages collection + + - parameter viewController: `MultipageReviewViewController` where the pages are reviewed. + - parameter pages: Reordered pages collection + */ + func multipageReview(_ viewController: MultipageReviewViewController, + didReorder pages: [GiniCapturePage]) + /** + Called when a user rotates one of the pages. + + - parameter viewController: `MultipageReviewViewController` where the pages are reviewed. + - parameter page: `GiniCapturePage` rotated. + */ + func multipageReview(_ viewController: MultipageReviewViewController, + didRotate page: GiniCapturePage) + + /** + Called when a user deletes one of the pages. + + - parameter viewController: `MultipageReviewViewController` where the pages are reviewed. + - parameter page: Page deleted. + */ + func multipageReview(_ viewController: MultipageReviewViewController, + didDelete page: GiniCapturePage) + + /** + Called when a user taps on the error action when the errored page + + - parameter viewController: `MultipageReviewViewController` where the pages are reviewed. + - parameter errorAction: `NoticeActionType` selected. + - parameter page: Page where the error action has been triggered + */ + func multipageReview(_ viewController: MultipageReviewViewController, + didTapRetryUploadFor page: GiniCapturePage) + + /** + Called when a user taps on the add page button + + - parameter viewController: `MultipageReviewViewController` where the pages are reviewed. + */ + func multipageReviewDidTapAddImage(_ viewController: MultipageReviewViewController) +} + +//swiftlint:disable file_length +public final class MultipageReviewViewController: UIViewController { + + /** + The object that acts as the delegate of the multipage review view controller. + */ + public weak var delegate: MultipageReviewViewControllerDelegate? + + var pages: [GiniCapturePage] + fileprivate var currentSelectedItemPosition: Int = 0 + fileprivate let giniConfiguration: GiniConfiguration + fileprivate lazy var presenter: MultipageReviewCollectionCellPresenter = { + let presenter = MultipageReviewCollectionCellPresenter() + presenter.delegate = self + return presenter + }() + + // MARK: - UI initialization + + lazy var mainCollection: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + + var collection = UICollectionView(frame: .zero, collectionViewLayout: layout) + collection.translatesAutoresizingMaskIntoConstraints = false + + if #available(iOS 13.0, *) { + collection.backgroundColor = Colors.Gini.dynamicVeryLightGray + } else { + collection.backgroundColor = Colors.Gini.veryLightGray + } + + collection.dataSource = self + collection.delegate = self + collection.isPagingEnabled = true + collection.showsHorizontalScrollIndicator = false + collection.register(MultipageReviewMainCollectionCell.self, + forCellWithReuseIdentifier: MultipageReviewMainCollectionCell.identifier) + return collection + }() + + var pagesCollectionInsets: UIEdgeInsets { + let sideInset: CGFloat = (pagesCollection.frame.width - + MultipageReviewPagesCollectionCell.size(in: pagesCollection).width) / 2 + return UIEdgeInsets(top: 16, left: sideInset, bottom: 16, right: 0) + } + + lazy var pagesCollectionContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.from(giniColor: giniConfiguration.multipagePagesContainerAndToolBarColor) + return view + }() + + lazy var pagesCollectionTopBorder: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + + if #available(iOS 13.0, *) { + view.backgroundColor = .systemGray2 + } else { + view.backgroundColor = .lightGray + } + + return view + }() + + lazy var pagesCollectionBottomTipLabel: UITextView = { + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.text = .localized(resource: MultipageReviewStrings.dragAndDropTipMessage) + textView.font = textView.font?.withSize(11) + textView.isScrollEnabled = false + textView.isUserInteractionEnabled = false + textView.backgroundColor = .clear + return textView + }() + + lazy var pagesCollection: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumInteritemSpacing = 10 + var collection = UICollectionView(frame: .zero, collectionViewLayout: layout) + collection.translatesAutoresizingMaskIntoConstraints = false + collection.dataSource = self + collection.delegate = self + collection.backgroundColor = UIColor.clear + collection.showsHorizontalScrollIndicator = false + + collection.register(MultipageReviewPagesCollectionCell.self, + forCellWithReuseIdentifier: MultipageReviewPagesCollectionCell.identifier) + collection.register(MultipageReviewPagesCollectionFooter.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: MultipageReviewPagesCollectionFooter.identifier) + return collection + }() + + lazy var toolBar: UIToolbar = { + let toolBar = UIToolbar(frame: .zero) + toolBar.translatesAutoresizingMaskIntoConstraints = false + toolBar.barTintColor = UIColor.from(giniColor: giniConfiguration.multipagePagesContainerAndToolBarColor) + toolBar.isTranslucent = false + toolBar.alpha = 0 + + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, + target: self, + action: nil) + var items = [flexibleSpace, + self.rotateButton, + flexibleSpace, + flexibleSpace, + self.deleteButton, + flexibleSpace] + + toolBar.setItems(items, animated: false) + + return toolBar + }() + + var reorderContainerTooltipView: ToolTipView? + fileprivate var opaqueView: UIView? + + lazy var rotateButton: UIBarButtonItem = { + let button = barButtonItem(withImage: UIImageNamedPreferred(named: "rotateImageIcon"), + insets: UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), + action: #selector(rotateImageButtonAction)) + + button.accessibilityLabel = NSLocalizedStringPreferredFormat("ginicapture.review.rotateButton", comment: "Rotate button") + return button + }() + + lazy var deleteButton: UIBarButtonItem = { + return barButtonItem(withImage: UIImageNamedPreferred(named: "trashIcon"), + insets: UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), + action: #selector(deleteImageButtonAction)) + }() + + fileprivate lazy var pagesCollectionTipLabelHeightConstraint: NSLayoutConstraint = { + let constraint = NSLayoutConstraint(item: self.pagesCollectionBottomTipLabel, + attribute: .height, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1.0, + constant: 0) + return constraint + }() + + fileprivate lazy var longPressGesture = UILongPressGestureRecognizer(target: self, + action: #selector(self.handleLongGesture)) + + // MARK: - Init + + public init(pages: [GiniCapturePage], giniConfiguration: GiniConfiguration) { + self.pages = pages + self.giniConfiguration = giniConfiguration + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(imageDocuments:) has not been implemented") + } +} + +// MARK: - UIViewController + +extension MultipageReviewViewController { + override public func loadView() { + super.loadView() + view.addSubview(mainCollection) + view.addSubview(toolBar) + view.insertSubview(pagesCollectionContainer, belowSubview: toolBar) + pagesCollectionContainer.addSubview(pagesCollection) + pagesCollectionContainer.addSubview(pagesCollectionTopBorder) + pagesCollectionContainer.addSubview(pagesCollectionBottomTipLabel) + + addConstraints() + } + + override public func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = mainCollection.backgroundColor + mainCollection.contentInsetAdjustmentBehavior = .never + edgesForExtendedLayout = [] + + longPressGesture.delaysTouchesBegan = true + pagesCollection.addGestureRecognizer(longPressGesture) + + if ToolTipView.shouldShowReorderPagesButtonToolTip { + createReorderPagesTip() + } + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + selectItem(at: currentSelectedItemPosition) + changeReorderTipVisibility(to: pages.count < 2) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + showToolbar() + + reorderContainerTooltipView?.show { + self.opaqueView?.alpha = 1 + self.deleteButton.isEnabled = false + self.rotateButton.isEnabled = false + self.navigationItem.rightBarButtonItem?.isEnabled = false + ToolTipView.shouldShowReorderPagesButtonToolTip = false + } + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + reorderContainerTooltipView?.arrangeViews() + opaqueView?.frame = self.mainCollection.frame + } + + public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate(alongsideTransition: { [weak self] _ in + guard let self = self else { + return + } + + self.reorderContainerTooltipView?.arrangeViews() + + }) + } + + /** + Updates the collections with the given pages. + + - parameter pages: Pages to be used in the collections. + */ + + public func updateCollections(with pages: [GiniCapturePage], animated: Bool = false) { + let diff = self.pages.diffIndexes(with: pages) + + let updatedIndexPaths = diff.updated.map { IndexPath(row: $0, section: 0) } + let removedIndexPaths = diff.removed.map { IndexPath(row: $0, section: 0) } + let insertedIndexPaths = diff.inserted.map { IndexPath(row: $0, section: 0) } + + reload(pagesCollection, + pages: pages, + indexPaths: (updatedIndexPaths, removedIndexPaths, insertedIndexPaths), + animated: true) { [weak self] _ in + guard let self = self else { return } + self.selectItem(at: self.currentSelectedItemPosition) + } + + mainCollection.reloadData() + } + + private func reload(_ collection: UICollectionView, + pages: [GiniCapturePage], + indexPaths: (updated: [IndexPath], removed: [IndexPath], inserted: [IndexPath]), + animated: Bool, completion: @escaping (Bool) -> Void) { + // When the collection has not been loaded before, the data should be reloaded + guard collection.numberOfItems(inSection: 0) > 0 else { + self.pages = pages + collection.reloadData() + return + } + + collection.performBatchUpdates(animated: animated, updates: {[weak self] in + self?.pages = pages + collection.reloadItems(at: indexPaths.updated) + collection.deleteItems(at: indexPaths.removed) + collection.insertItems(at: indexPaths.inserted) + }, completion: completion) + } + + func selectItem(at position: Int, in section: Int = 0, animated: Bool = true) { + guard self.pages.count > 0 else { + return + } + + var indexPathRow = position + if position < 0 || position >= self.pages.count { + indexPathRow = 0 + } + + let indexPath = IndexPath(row: indexPathRow, section: section) + pagesCollection.selectItem(at: indexPath, + animated: animated, + scrollPosition: .centeredHorizontally) + + collectionView(pagesCollection, didSelectItemAt: indexPath) + } + +} + +// MARK: - Private methods + +extension MultipageReviewViewController { + fileprivate func barButtonItem(withImage image: UIImage?, + insets: UIEdgeInsets, + action: Selector) -> UIBarButtonItem { + let button = UIButton(type: .custom) + button.setImage(image, for: .normal) + button.addTarget(self, action: action, for: .touchUpInside) + button.imageEdgeInsets = insets + button.layer.cornerRadius = 5 + button.tintColor = giniConfiguration.multipageToolbarItemsColor + + // This is needed since on iOS 9 and below, + // the buttons are not resized automatically when using autolayout + if let image = image { + button.frame = CGRect(origin: .zero, size: image.size) + } + + return UIBarButtonItem(customView: button) + } + + fileprivate func changeTitle(withPage page: Int) { + title = .localized(resource: MultipageReviewStrings.titleMessage, args: page, pages.count) + } + + fileprivate func changeReorderTipVisibility(to hidden: Bool) { + pagesCollectionBottomTipLabel.isHidden = hidden + pagesCollectionTipLabelHeightConstraint.constant = hidden ? 0 : 30 + } + + @objc fileprivate func handleLongGesture(gesture: UILongPressGestureRecognizer) { + switch gesture.state { + case .began: + guard let selectedIndexPath = self.pagesCollection + .indexPathForItem(at: gesture.location(in: self.pagesCollection)) else { + break + } + pagesCollection.beginInteractiveMovementForItem(at: selectedIndexPath) + case .changed: + if let collectionView = gesture.view as? UICollectionView, collectionView == pagesCollection { + let gesturePosition = gesture.location(in: collectionView) + let maxY = (collectionView.frame.height / 2) + pagesCollectionInsets.top + let minY = (collectionView.frame.height / 2) - pagesCollectionInsets.top + let y = gesturePosition.y > minY ? min(maxY, gesturePosition.y) : minY + pagesCollection.updateInteractiveMovementTargetPosition(CGPoint(x: gesturePosition.x, y: y)) + } + case .ended: + pagesCollection.endInteractiveMovement() + default: + pagesCollection.cancelInteractiveMovement() + } + } + + fileprivate func createReorderPagesTip() { + let opaqueView = OpaqueViewFactory.create(with: giniConfiguration.multipageToolTipOpaqueBackgroundStyle) + opaqueView.alpha = 0 + self.opaqueView = opaqueView + self.view.addSubview(opaqueView) + + reorderContainerTooltipView = ToolTipView(text: .localized(resource: MultipageReviewStrings.reorderContainerTooltipMessage), + giniConfiguration: giniConfiguration, + referenceView: pagesCollectionContainer, + superView: view, + position: .above, + distanceToRefView: .zero) + + reorderContainerTooltipView?.willDismiss = { [weak self] in + guard let self = self else { return } + self.closeTooltip() + } + + reorderContainerTooltipView?.willDismissOnCloseButtonTap = { [weak self] in + guard let self = self else { return } + self.closeTooltip() + } + } + + fileprivate func closeTooltip(){ + self.opaqueView?.removeFromSuperview() + self.deleteButton.isEnabled = true + self.rotateButton.isEnabled = true + self.navigationItem.rightBarButtonItem?.isEnabled = true + self.reorderContainerTooltipView = nil + } + + fileprivate func showToolbar() { + UIView.animate(withDuration: AnimationDuration.fast, animations: { + self.toolBar.alpha = 1 + }, completion: { _ in + self.pagesCollectionContainer.alpha = 1 + }) + } + + fileprivate func pagesCollectionMaxHeight(in device: UIDevice = UIDevice.current) -> CGFloat { + return device.isIpad ? 300 : 224 + } + + fileprivate func addConstraints() { + // mainCollection + Constraints.active(item: mainCollection, attr: .bottom, relatedBy: .equal, to: pagesCollectionContainer, + attr: .top) + Constraints.active(item: mainCollection, attr: .top, relatedBy: .equal, to: view.safeAreaLayoutGuide, + attr: .top) + Constraints.active(item: mainCollection, attr: .trailing, relatedBy: .equal, to: view, attr: .trailing) + Constraints.active(item: mainCollection, attr: .leading, relatedBy: .equal, to: view, attr: .leading) + + // toolBar + Constraints.active(item: toolBar, attr: .bottom, relatedBy: .equal, to: view.safeAreaLayoutGuide, + attr: .bottom) + Constraints.active(item: toolBar, attr: .trailing, relatedBy: .equal, to: view, attr: .trailing) + Constraints.active(item: toolBar, attr: .leading, relatedBy: .equal, to: view, attr: .leading) + + // pagesCollectionContainer + Constraints.active(item: pagesCollectionContainer, attr: .bottom, relatedBy: .equal, to: toolBar, attr: .top) + Constraints.active(item: pagesCollectionContainer, attr: .trailing, relatedBy: .equal, to: view, + attr: .trailing) + Constraints.active(item: pagesCollectionContainer, attr: .leading, relatedBy: .equal, to: view, attr: .leading) + + // pagesCollectionTopBorder + Constraints.active(item: pagesCollectionTopBorder, attr: .top, relatedBy: .equal, + to: pagesCollectionContainer, attr: .top) + Constraints.active(item: pagesCollectionTopBorder, attr: .leading, relatedBy: .equal, + to: pagesCollectionContainer, attr: .leading) + Constraints.active(item: pagesCollectionTopBorder, attr: .trailing, relatedBy: .equal, + to: pagesCollectionContainer, attr: .trailing) + Constraints.active(item: pagesCollectionTopBorder, attr: .height, relatedBy: .equal, to: nil, + attr: .notAnAttribute, constant: 0.5) + + // pagesCollection + Constraints.active(item: pagesCollection, attr: .bottom, relatedBy: .equal, to: pagesCollectionBottomTipLabel, + attr: .top) + Constraints.active(item: pagesCollection, attr: .top, relatedBy: .equal, to: pagesCollectionContainer, + attr: .top) + Constraints.active(item: pagesCollection, attr: .trailing, relatedBy: .equal, to: view, attr: .trailing) + Constraints.active(item: pagesCollection, attr: .leading, relatedBy: .equal, to: view, attr: .leading) + Constraints.active(item: pagesCollection, attr: .height, relatedBy: .equal, to: view, attr: .height, + multiplier: 2 / 5, priority: 999) + Constraints.active(item: pagesCollection, attr: .height, relatedBy: .lessThanOrEqual, to: nil, + attr: .notAnAttribute, constant: pagesCollectionMaxHeight()) + + // pagesCollectionBottomTipLabel + Constraints.active(item: pagesCollectionBottomTipLabel, attr: .bottom, relatedBy: .equal, + to: pagesCollectionContainer, attr: .bottom) + Constraints.active(constraint: pagesCollectionTipLabelHeightConstraint) + Constraints.active(item: pagesCollectionBottomTipLabel, attr: .centerX, relatedBy: .equal, + to: pagesCollectionContainer, attr: .centerX) + } +} + +// MARK: - Toolbar actions + +extension MultipageReviewViewController { + + fileprivate func deleteItem(at indexPath: IndexPath) { + let pageToDelete = pages[indexPath.row] + pages.remove(at: indexPath.row) + mainCollection.deleteItems(at: [indexPath]) + delegate?.multipageReview(self, didDelete: pageToDelete) + deleteButton.isEnabled = false + + pagesCollection.performBatchUpdates({ + self.pagesCollection.deleteItems(at: [indexPath]) + }, completion: { _ in + if self.pages.count > 0 { + if indexPath.row != self.pages.count { + self.reloadPagesStarting(from: indexPath) + } + + self.selectItem(at: min(indexPath.row, self.pages.count - 1)) + } + self.deleteButton.isEnabled = true + }) + } + + private func reloadPagesStarting(from indexPath: IndexPath) { + var indexes = IndexPath.indexesBetween(indexPath, + and: IndexPath(row: pages.count, + section: 0)) + indexes.append(indexPath) + pagesCollection.reloadItems(at: indexes) + } + + @objc fileprivate func rotateImageButtonAction() { + if let currentIndexPath = visibleCell(in: self.mainCollection) { + presenter.rotateThumbnails(for: pages[currentIndexPath.row]) + + mainCollection.reloadItems(at: [currentIndexPath]) + pagesCollection.reloadItems(at: [currentIndexPath]) + + selectItem(at: currentIndexPath.row) + delegate?.multipageReview(self, didRotate: pages[currentIndexPath.row]) + } + } + + @objc fileprivate func deleteImageButtonAction() { + if let currentIndexPath = visibleCell(in: self.mainCollection) { + deleteItem(at: currentIndexPath) + } + } + +} + +// MARK: UICollectionViewDataSource + +extension MultipageReviewViewController: UICollectionViewDataSource { + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return pages.count + } + + public func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let page = pages[indexPath.row] + let isSelected = self.currentSelectedItemPosition == indexPath.row + let collectionCell: MultipageReviewCollectionCellPresenter.MultipageCollectionCellType + + if collectionView == mainCollection { + let cell = mainCollection + .dequeueReusableCell(withReuseIdentifier: MultipageReviewMainCollectionCell.identifier, + for: indexPath) as? MultipageReviewMainCollectionCell + collectionCell = .main(cell!, didFailUpload(page: page, indexPath: indexPath)) + } else { + let cell = pagesCollection + .dequeueReusableCell(withReuseIdentifier: MultipageReviewPagesCollectionCell.identifier, + for: indexPath) as? MultipageReviewPagesCollectionCell + collectionCell = .pages(cell!) + } + + return presenter.setUp(collectionCell, with: page, isSelected: isSelected, at: indexPath) + } + + public func collectionView(_ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath) -> UICollectionReusableView { + let footer = collectionView + .dequeueReusableSupplementaryView(ofKind: kind, + withReuseIdentifier: MultipageReviewPagesCollectionFooter.identifier, + for: indexPath) as? MultipageReviewPagesCollectionFooter + footer?.updateMaskConstraints(with: collectionView) + footer?.trailingConstraint?.constant = -MultipageReviewPagesCollectionFooter.padding(in: collectionView).right + footer?.addLabel.font = giniConfiguration.customFont.with(weight: .bold, size: 12, style: .footnote) + footer?.didTapAddButton = { [weak self] in + guard let self = self else { return } + self.delegate?.multipageReviewDidTapAddImage(self) + } + + return footer! + } + + public func collectionView(_ collectionView: UICollectionView, + moveItemAt sourceIndexPath: IndexPath, + to destinationIndexPath: IndexPath) { + if collectionView == pagesCollection { + var indexes = IndexPath.indexesBetween(sourceIndexPath, and: destinationIndexPath) + indexes.append(sourceIndexPath) + + // On iOS < 11 the destinationIndexPath is not reloaded automatically. + if ProcessInfo().operatingSystemVersion.majorVersion < 11 { + indexes.append(destinationIndexPath) + } + + let elementMoved = pages.remove(at: sourceIndexPath.row) + pages.insert(elementMoved, at: destinationIndexPath.row) + + if sourceIndexPath.row != currentSelectedItemPosition { + mainCollection.reloadData() + } + + // This is needed because this method is called before the dragging animation finishes. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { [weak self] in + guard let self = self else { return } + self.pagesCollection.reloadData() + self.selectItem(at: destinationIndexPath.row) + self.delegate?.multipageReview(self, didReorder: self.pages) + }) + } + } + + private func didFailUpload(page: GiniCapturePage, indexPath: IndexPath) -> ((NoticeActionType) -> Void) { + return {[weak self] action in + guard let self = self else { return } + switch action { + case .retry: + self.delegate?.multipageReview(self, didTapRetryUploadFor: page) + case .retake: + self.deleteItem(at: indexPath) + self.delegate?.multipageReviewDidTapAddImage(self) + } + } + } + +} + +// MARK: - MultipageReviewCollectionsAdapterDelegate + +extension MultipageReviewViewController: MultipageReviewCollectionCellPresenterDelegate { + func multipage(_ reviewCollectionCellPresenter: MultipageReviewCollectionCellPresenter, + didUpdate cell: MultipageReviewCollectionCellPresenter.MultipageCollectionCellType, + at indexPath: IndexPath) { + switch cell { + case .main: + mainCollection.reloadItems(at: [indexPath]) + case .pages: + pagesCollection.reloadItems(at: [indexPath]) + } + } + + func multipage(_ reviewCollectionCellPresenter: MultipageReviewCollectionCellPresenter, + didUpdateElementIn collectionView: UICollectionView, + at indexPath: IndexPath) { + collectionView.reloadItems(at: [indexPath]) + } +} + +// MARK: UICollectionViewDelegateFlowLayout + +extension MultipageReviewViewController: UICollectionViewDelegateFlowLayout { + + public func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + return collectionView == mainCollection ? + collectionView.frame.size : + MultipageReviewPagesCollectionCell.size(in: collectionView) + } + + public func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + referenceSizeForFooterInSection section: Int) -> CGSize { + guard collectionView == pagesCollection else { + return .zero + } + + return MultipageReviewPagesCollectionFooter.size(in: collectionView) + } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if collectionView == pagesCollection { + if pages.count > 1 { + mainCollection.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) + pagesCollection.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) + } + changeTitle(withPage: indexPath.row + 1) + currentSelectedItemPosition = indexPath.row + } + } + + public func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + insetForSectionAt section: Int) -> UIEdgeInsets { + return collectionView == mainCollection ? .zero : pagesCollectionInsets + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + if scrollView == mainCollection { + if let indexPath = visibleCell(in: mainCollection) { + selectItem(at: indexPath.row) + } + } + } + + func visibleCell(in collectionView: UICollectionView) -> IndexPath? { + collectionView.layoutIfNeeded() // It is needed due to a bug in UIKit. + return collectionView.indexPathsForVisibleItems.first + } + + private func frame(for imageView: UIImageView, from coordinateSpace: UICoordinateSpace) -> CGRect { + guard let image = imageView.image else { return .zero } + let origin = view.convert(imageView.frame.origin, to: coordinateSpace) + let imageWidth = imageView.frame.size.height * image.size.width / image.size.height + let imageOriginX = (imageView.frame.size.width - imageWidth) / 2 + + return CGRect(origin: CGPoint(x: imageOriginX, y: origin.y), + size: CGSize(width: imageWidth, height: imageView.frame.size.height)) + } + +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/PageStateView.swift b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/PageStateView.swift new file mode 100644 index 0000000..c179ede --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Multipage Review/PageStateView.swift @@ -0,0 +1,71 @@ +// +// PageStateView.swift +// GiniCapture +// +// Created by Enrique del Pozo Gómez on 4/13/18. +// + +import UIKit + +final class PageStateView: UIView { + + enum State { + case succeeded, failed, loading + } + + lazy private(set) var icon: UIImageView = { + let icon = UIImageView() + icon.translatesAutoresizingMaskIntoConstraints = false + icon.contentMode = .scaleAspectFit + + return icon + }() + + lazy private(set) var loadingIndicator: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(style: .white) + indicator.translatesAutoresizingMaskIntoConstraints = false + indicator.hidesWhenStopped = true + return indicator + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(icon) + addSubview(loadingIndicator) + addConstraints() + } + + func addConstraints() { + // loadingIndicator + Constraints.active(item: loadingIndicator, attr: .centerX, relatedBy: .equal, to: self, attr: .centerX) + Constraints.active(item: loadingIndicator, attr: .centerY, relatedBy: .equal, to: self, attr: .centerY) + + // icon + Constraints.active(item: icon, attr: .top, relatedBy: .equal, to: self, attr: .top, constant: 10) + Constraints.active(item: icon, attr: .leading, relatedBy: .equal, to: self, attr: .leading, constant: 10) + Constraints.active(item: icon, attr: .trailing, relatedBy: .equal, to: self, attr: .trailing, constant: -10) + Constraints.active(item: icon, attr: .bottom, relatedBy: .equal, to: self, attr: .bottom, constant: -10) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(to status: State) { + switch status { + case .succeeded: + backgroundColor = GiniConfiguration.shared.multipagePageSuccessfullUploadIconBackgroundColor + icon.image = UIImageNamedPreferred(named: "successfullUploadIcon") + loadingIndicator.stopAnimating() + + case .failed: + backgroundColor = GiniConfiguration.shared.multipagePageFailureUploadIconBackgroundColor + icon.image = UIImageNamedPreferred(named: "failureUploadIcon") + loadingIndicator.stopAnimating() + case .loading: + backgroundColor = .clear + icon.image = nil + loadingIndicator.startAnimating() + } + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/NoResult/BottomButtonsViewModel.swift b/Sources/GiniCaptureSDK/Core/Screens/NoResult/BottomButtonsViewModel.swift deleted file mode 100644 index 521adb2..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/NoResult/BottomButtonsViewModel.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// BottomButtonsViewModel.swift -// GiniCapture -// -// Created by Krzysztof Kryniecki on 23/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation - -final class BottomButtonsViewModel { - let retakePressed: (() -> Void)? - let enterManuallyPressed: (() -> Void)? - let cancelPressed: (() -> Void) - - init( - retakeBlock: (() -> Void)? = nil, - manuallyPressed: (() -> Void)? = nil, - cancelPressed: @escaping(() -> Void)) { - self.retakePressed = retakeBlock - self.enterManuallyPressed = manuallyPressed - self.cancelPressed = cancelPressed - } - - @objc func didPressRetake() { - errorOccurred = false - retakePressed?() - } - - @objc func didPressEnterManually() { - errorOccurred = false - enterManuallyPressed?() - } - - @objc func didPressCancell() { - errorOccurred = false - cancelPressed() - } - - func isEnterManuallyHidden() -> Bool { - return enterManuallyPressed == nil - } - - func isRetakePressedHidden() -> Bool { - return retakePressed == nil - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/NoResult/NoResultScreenViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/NoResult/NoResultScreenViewController.swift deleted file mode 100644 index c95faa4..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/NoResult/NoResultScreenViewController.swift +++ /dev/null @@ -1,309 +0,0 @@ -// -// NoResultScreenViewController.swift -// GiniCapture -// -// Created by Krzysztof Kryniecki on 22/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import UIKit - -final class NoResultScreenViewController: UIViewController { - enum NoResultType { - case image - case pdf - case custom(String) - - var description: String { - switch self { - case .pdf: - return NSLocalizedStringPreferredFormat( - "ginicapture.noresult.header", - comment: "no results header") - case .image: - return NSLocalizedStringPreferredFormat( - "ginicapture.noresult.header", - comment: "no results header") - case .custom(let text): - return text - } - } - } - - lazy var tableView: UITableView = { - var tableView: UITableView - if #available(iOS 13.0, *) { - tableView = UITableView(frame: .zero, style: .insetGrouped) - } else { - tableView = UITableView(frame: .zero, style: .grouped) - } - - tableView.translatesAutoresizingMaskIntoConstraints = false - return tableView - }() - - lazy var buttonsView: ButtonsView = { - let view = ButtonsView( - firstTitle: NSLocalizedStringPreferredFormat( - "ginicapture.noresult.enterManually", - comment: "Enter manually button title"), - secondTitle: NSLocalizedStringPreferredFormat( - "ginicapture.noresult.retakeImages", - comment: "Retake images button title")) - view.translatesAutoresizingMaskIntoConstraints = false - - view.enterButton.isHidden = viewModel.isEnterManuallyHidden() - view.retakeButton.isHidden = viewModel.isRetakePressedHidden() - - return view - }() - - lazy var header: IconHeader = { - if let header = IconHeader().loadNib() as? IconHeader { - header.headerLabel.adjustsFontForContentSizeCategory = true - header.headerLabel.adjustsFontSizeToFitWidth = true - header.translatesAutoresizingMaskIntoConstraints = false - return header - } - fatalError("No result header not found") - }() - private (set) var dataSource: HelpDataSource - private var giniConfiguration: GiniConfiguration - private let type: NoResultType - private let viewModel: BottomButtonsViewModel - private var buttonsHeightConstraint: NSLayoutConstraint? - private var numberOfButtons: Int { - return [ - viewModel.isEnterManuallyHidden(), - viewModel.isRetakePressedHidden() - ].filter({ - !$0 - }).count - } - - public init( - giniConfiguration: GiniConfiguration, - type: NoResultType, - viewModel: BottomButtonsViewModel - ) { - self.giniConfiguration = giniConfiguration - self.type = type - switch type { - case .image: - let tipsDS = HelpTipsDataSource(configuration: giniConfiguration) - tipsDS.showHeader = true - self.dataSource = tipsDS - case .pdf: - self.dataSource = HelpFormatsDataSource(configuration: giniConfiguration) - case .custom(_): - self.dataSource = HelpFormatsDataSource(configuration: giniConfiguration) - } - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - self.setupView() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - if numberOfButtons > 0 { - tableView.contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: buttonsView.bounds.size.height + CGFloat(numberOfButtons) * GiniMargins.margin, - right: 0) - } else { - tableView.contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: GiniMargins.margin, - right: 0) - } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - tableView.contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: buttonsView.bounds.size.height + GiniMargins.margin, - right: 0) - } - - private func setupView() { - configureMainView() - configureTableView() - configureConstraints() - configureButtons() - configureCustomTopNavigationBar() - edgesForExtendedLayout = [] - } - - private func configureMainView() { - title = NSLocalizedStringPreferredFormat( - "ginicapture.noresult.title", - comment: "No result screen title") - header.iconImageView.accessibilityLabel = NSLocalizedStringPreferredFormat( - "ginicapture.noresult.title", - comment: "No result screen title") - header.headerLabel.text = type.description - header.headerLabel.font = giniConfiguration.textStyleFonts[.subheadline] - header.headerLabel.textColor = GiniColor( - light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1 - ).uiColor() - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - view.addSubview(header) - view.addSubview(tableView) - view.addSubview(buttonsView) - header.backgroundColor = GiniColor( - light: UIColor.GiniCapture.error4, - dark: UIColor.GiniCapture.error1 - ).uiColor() - } - - private func configureCustomTopNavigationBar() { - let cancelButton = GiniBarButton(ofType: .cancel) - cancelButton.addAction(viewModel, #selector(viewModel.didPressCancell)) - - if giniConfiguration.bottomNavigationBarEnabled { - navigationItem.rightBarButtonItem = cancelButton.barButton - navigationItem.setHidesBackButton(true, animated: true) - } else { - navigationItem.leftBarButtonItem = cancelButton.barButton - } - } - - private func getButtonsMinHeight(numberOfButtons: Int) -> CGFloat { - if numberOfButtons == 1 { - return Constants.singleButtonHeight.rawValue - } else { - return Constants.twoButtonsHeight.rawValue - } - } - - private func configureTableView() { - registerCells() - tableView.delegate = self.dataSource - tableView.dataSource = self.dataSource - tableView.estimatedRowHeight = Constants.tableRowHeight.rawValue - tableView.rowHeight = UITableView.automaticDimension - tableView.tableFooterView = UIView() - tableView.tableHeaderView = UIView() - tableView.sectionHeaderHeight = Constants.sectionHeight.rawValue - tableView.allowsSelection = false - tableView.backgroundColor = UIColor.clear - tableView.alwaysBounceVertical = false - tableView.showsVerticalScrollIndicator = false - tableView.separatorStyle = .none - - if #available(iOS 14.0, *) { - var bgConfig = UIBackgroundConfiguration.listPlainCell() - bgConfig.backgroundColor = UIColor.clear - UITableViewHeaderFooterView.appearance().backgroundConfiguration = bgConfig - } - } - - private func registerCells() { - switch type { - case .pdf: - tableView.register( - UINib( - nibName: "HelpFormatCell", - bundle: giniCaptureBundle()), - forCellReuseIdentifier: HelpFormatCell.reuseIdentifier) - case .image, .custom(_): - tableView.register( - UINib( - nibName: "HelpTipCell", - bundle: giniCaptureBundle()), - forCellReuseIdentifier: HelpTipCell.reuseIdentifier) - } - tableView.register( - UINib( - nibName: "HelpFormatSectionHeader", - bundle: giniCaptureBundle()), - forHeaderFooterViewReuseIdentifier: HelpFormatSectionHeader.reuseIdentifier) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - tableView.reloadData() - view.layoutSubviews() - } - - private func configureButtons() { - buttonsView.enterButton.addTarget( - viewModel, - action: #selector(viewModel.didPressEnterManually), - for: .touchUpInside) - buttonsView.retakeButton.addTarget( - viewModel, - action: #selector(viewModel.didPressRetake), - for: .touchUpInside) - } - - private func configureConstraints() { - header.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical) - header.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - tableView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) - let buttonsConstraint = buttonsView.heightAnchor.constraint( - greaterThanOrEqualToConstant: getButtonsMinHeight(numberOfButtons: numberOfButtons) - ) - buttonsHeightConstraint = buttonsConstraint - NSLayoutConstraint.activate([ - tableView.heightAnchor.constraint(greaterThanOrEqualToConstant: view.bounds.size.height * 0.6), - header.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0), - header.leadingAnchor.constraint(equalTo: view.leadingAnchor), - header.trailingAnchor.constraint(equalTo: view.trailingAnchor), - header.heightAnchor.constraint(greaterThanOrEqualToConstant: 62), - tableView.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 13), - tableView.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: 16), - buttonsView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -GiniMargins.margin), - buttonsConstraint - ]) - configureHorizontalConstraints() - view.layoutSubviews() - } - - private func configureHorizontalConstraints() { - if UIDevice.current.isIpad { - NSLayoutConstraint.activate([ - tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - tableView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.7), - buttonsView.leadingAnchor.constraint( - equalTo: view.leadingAnchor, - constant: GiniMargins.margin), - buttonsView.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -GiniMargins.margin) - ]) - } else { - NSLayoutConstraint.activate([ - tableView.leadingAnchor.constraint( - equalTo: view.leadingAnchor, - constant: GiniMargins.margin), - tableView.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -GiniMargins.margin), - buttonsView.leadingAnchor.constraint(equalTo: tableView.leadingAnchor), - buttonsView.trailingAnchor.constraint(equalTo: tableView.trailingAnchor) - ]) - } - } - - private enum Constants: CGFloat { - case singleButtonHeight = 50 - case twoButtonsHeight = 112 - case tableRowHeight = 44 - case sectionHeight = 70 - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/NoResult/Views/IconHeader.swift b/Sources/GiniCaptureSDK/Core/Screens/NoResult/Views/IconHeader.swift deleted file mode 100644 index d79fab5..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/NoResult/Views/IconHeader.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NoResultHeader.swift -// GiniCapture -// -// Created by Krzysztof Kryniecki on 22/08/2022. -// Copyright © 2022 Gini GmbH. All rights reserved. -// - -import Foundation -import UIKit - -class IconHeader: UIView { - @IBOutlet weak var iconImageView: UIImageView! - @IBOutlet weak var headerLabel: UILabel! - - override func awakeFromNib() { - super.awakeFromNib() - isAccessibilityElement = false - accessibilityElements = [iconImageView as Any, headerLabel as Any] - headerLabel.adjustsFontForContentSizeCategory = true - headerLabel.isAccessibilityElement = true - iconImageView.isAccessibilityElement = true - iconImageView.accessibilityTraits = .image - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/DefaultOnboardingNavigationBarBottomAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/DefaultOnboardingNavigationBarBottomAdapter.swift deleted file mode 100644 index 5e112c4..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/DefaultOnboardingNavigationBarBottomAdapter.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// OnboardingNavigationBarBottomAdapter.swift -// -// -// Created by Nadya Karaban on 08.08.22. -// - -import Foundation -import UIKit - -class DefaultOnboardingNavigationBarBottomAdapter: OnboardingNavigationBarBottomAdapter { - private var nextButtonCallback: (() -> Void)? - private var skipButtonCallback: (() -> Void)? - private var getStartedButtonCallback: (() -> Void)? - - // Add the callback whenever the next button is clicked - @objc func setNextButtonClickedActionCallback(_ callback: @escaping () -> Void) { - nextButtonCallback = callback - } - - // Add the callback whenever the skip button is clicked - @objc func setSkipButtonClickedActionCallback(_ callback: @escaping () -> Void) { - skipButtonCallback = callback - } - - // Add the callback whenever the get started button is clicked - @objc func setGetStartedButtonClickedActionCallback(_ callback: @escaping () -> Void) { - getStartedButtonCallback = callback - } - - func showButtons( - navigationButtons: [OnboardingNavigationBarBottomButton], - navigationBar: UIView) { - if let bar = navigationBar as? OnboardingBottomNavigationBar { - bar.getStarted.isHidden = true - bar.nextButton.isHidden = true - bar.skipButton.isHidden = true - for button in navigationButtons { - switch button { - case .getStarted: - bar.getStarted.isHidden = false - case .next: - bar.nextButton.isHidden = false - case .skip: - bar.skipButton.isHidden = false - } - } - } - } - - @objc func nextButtonClicked() { - nextButtonCallback?() - } - - @objc func skipButtonClicked() { - skipButtonCallback?() - } - - @objc func getStartedButtonClicked() { - getStartedButtonCallback?() - } - - func injectedView() -> UIView { - if let navigationBarView = - OnboardingBottomNavigationBar().loadNib() as? - OnboardingBottomNavigationBar { - navigationBarView.nextButton.addTarget(self, action: #selector(nextButtonClicked), for: .touchUpInside) - navigationBarView.skipButton.addTarget(self, action: #selector(skipButtonClicked), for: .touchUpInside) - navigationBarView.getStarted.addTarget(self, action: #selector(getStartedButtonClicked), - for: .touchUpInside) - - return navigationBarView - } else { - return UIView() - } - } - - func onDeinit() { - nextButtonCallback = nil - skipButtonCallback = nil - getStartedButtonCallback = nil - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingContainerViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingContainerViewController.swift new file mode 100644 index 0000000..bcc4d39 --- /dev/null +++ b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingContainerViewController.swift @@ -0,0 +1,221 @@ +// +// OnboardingContainerViewController.swift +// GiniCapture +// +// Created by Peter Pult on 24/06/16. +// Copyright © 2016 Gini GmbH. All rights reserved. +// + +import UIKit + +/** + Block that will be executed when the onboarding was dismissed. + */ +typealias OnboardingContainerCompletionBlock = () -> Void + +/** + Container class for `OnboardingViewController`. + + - note: Should be embedded in a navigation controller. + */ +final class OnboardingContainerViewController: UIViewController, ContainerViewController { + + let giniConfiguration: GiniConfiguration + public weak var trackingDelegate: OnboardingScreenTrackingDelegate? + fileprivate let backgroundAlpha: CGFloat = 0.25 + fileprivate var completionBlock: OnboardingContainerCompletionBlock? + + lazy var containerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var contentController: UIViewController = { + var pages = self.giniConfiguration.onboardingPages + pages.append(UIView()) // Add an empty last page to transition nicely back to camera + return OnboardingViewController(pages: pages, + scrollViewDelegate: self) + }() + + fileprivate lazy var pageControlContainerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + fileprivate lazy var pageControl: UIPageControl = { + let pageControl = UIPageControl() + pageControl.translatesAutoresizingMaskIntoConstraints = false + pageControl.currentPage = 0 + pageControl.numberOfPages = self.giniConfiguration.onboardingPages.count + pageControl.currentPageIndicatorTintColor = UIColor.from(giniColor: giniConfiguration.onboardingCurrentPageIndicatorColor).withAlphaComponent(giniConfiguration.onboardingCurrentPageIndicatorAlpha) + pageControl.pageIndicatorTintColor = UIColor.from(giniColor: giniConfiguration.onboardingPageIndicatorColor) + pageControl.isUserInteractionEnabled = false + pageControl.isAccessibilityElement = false + return pageControl + }() + + fileprivate lazy var continueButton: UIBarButtonItem = { + let continueButtonResources = + GiniPreferredButtonResource(image: "navigationOnboardingContinue", + title: "ginicapture.navigationbar.onboarding.continue", + comment: "Button title in the navigation bar for the " + + "continue button on the onboarding screen", + configEntry: self.giniConfiguration.navigationBarOnboardingTitleContinueButton) + return GiniBarButtonItem( + image: continueButtonResources.preferredImage, + title: continueButtonResources.preferredText, + style: .plain, + target: self, + action: #selector(self.nextPage) + ) + }() + + init(giniConfiguration: GiniConfiguration = GiniConfiguration.shared, + trackingDelegate: OnboardingScreenTrackingDelegate? = nil, + withCompletion completion: @escaping OnboardingContainerCompletionBlock) { + self.giniConfiguration = giniConfiguration + self.trackingDelegate = trackingDelegate + self.completionBlock = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + super.loadView() + + view.addSubview(containerView) + view.addSubview(pageControlContainerView) + pageControlContainerView.addSubview(pageControl) + navigationItem.setRightBarButton(continueButton, animated: false) + + addConstraints() + } + + override func viewDidLoad() { + super.viewDidLoad() + title = .localized(resource: NavigationBarStrings.onboardingTitle) + view.backgroundColor = UIColor.from(giniColor: giniConfiguration.onboardingScreenBackgroundColor) + + trackingDelegate?.onOnboardingScreenEvent(event: Event(type: .start)) + + // Add content to container view + displayContent(contentController) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + trackingDelegate?.onOnboardingScreenEvent(event: Event(type: .finish)) + } + + public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + // Current onboarding page needs to be centered during transition (after ScrollView changes its frame) + coordinator.animate(alongsideTransition: { [weak self] _ in + guard let self = self else { + return + } + (self.contentController as? OnboardingViewController)?.centerTo(page: self.pageControl.currentPage) + }) + } + + // MARK: Actions + + @IBAction func close() { + dismiss(animated: false, completion: completionBlock) + } + + @IBAction func nextPage() { + (contentController as? OnboardingViewController)?.scrollToNextPage(true) + } + + // MARK: Constraints + + fileprivate func addConstraints() { + let superview = self.view + + // Container view + Constraints.active(item: containerView, attr: .top, relatedBy: .equal, to: superview, attr: .top) + Constraints.active(item: containerView, attr: .bottom, relatedBy: .greaterThanOrEqual, to: superview, + attr: .bottom, priority: 750) + Constraints.active(item: containerView, attr: .width, relatedBy: .equal, to: superview, attr: .width, + priority: 750) + Constraints.active(item: containerView, attr: .width, relatedBy: .lessThanOrEqual, to: superview, + attr: .width, priority: 999) + Constraints.active(item: containerView, attr: .centerX, relatedBy: .equal, to: superview, attr: .centerX) + + // Page control container view + Constraints.active(item: pageControlContainerView, attr: .top, relatedBy: .equal, to: containerView, + attr: .bottom, priority: 750) + Constraints.active(item: pageControlContainerView, attr: .trailing, relatedBy: .equal, to: superview, + attr: .trailing) + Constraints.active(item: pageControlContainerView, attr: .bottom, relatedBy: .equal, to: view.safeAreaLayoutGuide, + attr: .bottom) + Constraints.active(item: pageControlContainerView, attr: .leading, relatedBy: .equal, to: superview, + attr: .leading) + Constraints.active(item: pageControlContainerView, attr: .height, relatedBy: .greaterThanOrEqual, + to: pageControl, attr: .height, multiplier: 1.1) + + // Page control + Constraints.active(item: pageControl, attr: .height, relatedBy: .equal, to: nil, attr: .height, constant: 55) + Constraints.active(item: pageControl, attr: .centerX, relatedBy: .equal, to: pageControlContainerView, + attr: .centerX) + Constraints.active(item: pageControl, attr: .centerY, relatedBy: .equal, to: pageControlContainerView, + attr: .centerY) + + } +} + +// MARK: UIScrollViewDelegate + +extension OnboardingContainerViewController: UIScrollViewDelegate { + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + setPageControl(scrollView) + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + setPageControl(scrollView) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + // Update fixed elements position + let pageWidth = scrollView.frame.size.width + let contentOffsetX = scrollView.contentOffset.x + var frameOffsetX = contentOffsetX - pageWidth * CGFloat(pageControl.numberOfPages - 1) + let fixedFrame = view.frame + frameOffsetX = max(-frameOffsetX, -fixedFrame.width) + frameOffsetX = min(frameOffsetX, 0) + navigationController?.navigationBar.frame.origin.x = frameOffsetX + pageControlContainerView.frame.origin.x = frameOffsetX + } + + fileprivate func setPageControl(_ scrollView: UIScrollView) { + let pageWidth = scrollView.frame.size.width + let contentOffsetX = scrollView.contentOffset.x + let currentPage = contentOffsetX / pageWidth + pageControl.currentPage = Int(currentPage) + + // Dismiss onboarding on reach of end + if contentOffsetX + pageWidth >= scrollView.contentSize.width { + close() + } + } + +} + +// MARK: User defaults flags + +extension OnboardingContainerViewController { + static var willBeShown: Bool { + return (GiniConfiguration.shared.onboardingShowAtFirstLaunch && + !UserDefaults.standard.bool(forKey: "ginicapture.defaults.onboardingShowed")) || + GiniConfiguration.shared.onboardingShowAtLaunch + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingDataSource.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingDataSource.swift deleted file mode 100644 index 020b506..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingDataSource.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// OnboardingPagesDataSource.swift -// -// -// Created by Nadya Karaban on 14.09.22. -// - -import Foundation -import UIKit - -protocol BaseCollectionViewDataSource: UICollectionViewDelegate, - UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - init( - configuration: GiniConfiguration - ) -} - -protocol OnboardingScreen: AnyObject { - func didScroll(page: Int) -} - -class OnboardingDataSource: NSObject, BaseCollectionViewDataSource { - private enum OnboadingPageType: Int { - case alignCorners = 0 - case lighting = 1 - case multipage = 2 - case qrcode = 3 - } - private var adapters: [OnboadingPageType: OnboardingIllustrationAdapter?] = [:] - private let giniConfiguration: GiniConfiguration - weak var delegate: OnboardingScreen? - var currentPage = 0 - - lazy var itemSections: [OnboardingPage] = { - if let customPages = giniConfiguration.customOnboardingPages { - return customPages - } else { - var sections: [OnboardingPage] = - [ - OnboardingPage(imageName: "onboardingFlatPaper", title: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.flatPaper.title", - comment: "onboarding flat paper title"), description: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.flatPaper.description", - comment: "onboarding flat paper description")), - OnboardingPage(imageName: "onboardingGoodLighting", title: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.goodLighting.title", - comment: "onboarding good lighting title"), description: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.goodLighting.description", - comment: "onboarding good lighting description")) - ] - if giniConfiguration.multipageEnabled { - sections.append( - OnboardingPage(imageName: "onboardingMultiPages", title: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.multiPages.title", - comment: "onboarding multi pages title"), - description: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.multiPages.description", - comment: "onboarding multi pages description"))) - } else { - adapters[.multipage] = nil - } - if giniConfiguration.qrCodeScanningEnabled { - sections.append( - OnboardingPage(imageName: "onboardingQRCode", title: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.qrCode.title", - comment: "onboarding qrcode title"), description: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.qrCode.description", - comment: "onboarding qrcode description"))) - } else { - adapters[.qrcode] = nil - } - return sections - } - }() - - required init(configuration: GiniConfiguration) { - giniConfiguration = configuration - - adapters = [ - .alignCorners: giniConfiguration.onboardingAlignCornersIllustrationAdapter, - .lighting: giniConfiguration.onboardingLightingIllustrationAdapter, - .multipage: giniConfiguration.onboardingMultiPageIllustrationAdapter, - .qrcode: giniConfiguration.onboardingQRCodeIllustrationAdapter - ] - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - itemSections.count - } - - private func configureCell(cell: OnboardingPageCell, indexPath: IndexPath) { - let item = itemSections[indexPath.row] - let image = UIImageNamedPreferred(named: item.imageName) - guard let onboardingPageType = OnboadingPageType.init(rawValue: indexPath.row) else { - return - } - if let adapter = adapters[onboardingPageType], adapter != nil { - cell.iconView.illustrationAdapter = adapter - } else { - cell.iconView.illustrationAdapter = ImageOnboardingIllustrationAdapter() - cell.iconView.icon = image - } - cell.iconView.setupView() - - cell.descriptionLabel.text = item.description - cell.descriptionLabel.accessibilityValue = item.description - cell.titleLabel.text = item.title - cell.titleLabel.accessibilityValue = item.title - } - - func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - if let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: OnboardingPageCell.reuseIdentifier, - for: indexPath) as? OnboardingPageCell { - configureCell(cell: cell, indexPath: indexPath) - return cell - } - fatalError("OnboardingPageCell wasn't initialized") - } - - func collectionView( - _ collectionView: UICollectionView, - willDisplay cell: UICollectionViewCell, - forItemAt indexPath: IndexPath) { - if let onboardingPageType = OnboadingPageType.init(rawValue: indexPath.row), - let adapter = adapters[onboardingPageType] { - adapter?.pageDidAppear() - } - } - - func collectionView( - _ collectionView: UICollectionView, - didEndDisplaying cell: UICollectionViewCell, - forItemAt indexPath: IndexPath) { - if let onboardingPageType = OnboadingPageType.init(rawValue: indexPath.row), - let adapter = adapters[onboardingPageType] { - adapter?.pageDidDisappear() - } - } - - func collectionView( - _ collectionView: UICollectionView, - targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint - ) -> CGPoint { - let index = IndexPath(row: currentPage, section: 0) - let attr = collectionView.layoutAttributesForItem(at: index) - return attr?.frame.origin ?? CGPoint.zero - } - // MARK: - Display the page number in page controll of collection view Cell - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - let page = Int(scrollView.contentOffset.x) / Int(scrollView.frame.width) - currentPage = page - delegate?.didScroll(page: page) - } - - // MARK: - UICollectionViewDelegateFlowLayout - public func collectionView(_ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath) -> CGSize { - return collectionView.bounds.size - } - -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingPage.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingPage.swift index dc7ee39..430e973 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingPage.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingPage.swift @@ -1,32 +1,127 @@ // // OnboardingPage.swift -// +// GiniCapture // -// Created by Nadya Karaban on 15.09.22. +// Created by Peter Pult on 24/06/16. +// Copyright © 2016 Gini GmbH. All rights reserved. // -import Foundation +import UIKit /** - `OnboardingPage` represents the onboarding page with all it's properties. + Custom view to easily create onboarding pages which can then be used in `OnboardingViewController`. + Simply pass an image and a name. Both will be beautifully aligned and displayed to the user. + + - note: The text length should not exceed 50 characters, depending on the font used, + and should preferably stretch out over three lines. */ +@objcMembers public final class OnboardingPage: UIView { + + fileprivate var contentView = UIView() + fileprivate var imageView = UIImageView() + fileprivate var textLabel = UILabel() + fileprivate var needsToRotateImageInLandscape: Bool = false + + /** + Designated initializer for the `OnboardingPage` class which allows to create a custom onboarding page + just by passing an image and a text. The text will be displayed underneath the image. + + - parameter image: The image to be displayed. + - parameter text: The text to be displayed underneath the image. + + - returns: A simple custom view to be displayed in the onboarding. + */ + public init(image: UIImage, text: String, rotateImageInLandscape: Bool = false) { + super.init(frame: CGRect.zero) + + // Set image and text + imageView.image = image + textLabel.text = text + needsToRotateImageInLandscape = rotateImageInLandscape -public struct OnboardingPage { - let imageName: String - let title: String - let description: String - + // Configure label + textLabel.numberOfLines = 0 + textLabel.textColor = UIColor.from(giniColor: GiniConfiguration.shared.onboardingTextColor) + textLabel.textAlignment = .center + textLabel.font = GiniConfiguration.shared.customFont.with(weight: .thin, size: 28, style: .title1) + + // Configure view hierachy + addSubview(contentView) + contentView.addSubview(imageView) + contentView.addSubview(textLabel) + + // Add constraints + addConstraints() + } + /** - * Creates an `OnboardingPage` instance. - * - * - Parameter imageName: A name of the image associated with the onboarding page. - * - Parameter title: A title of the onboarding page - * - Parameter description: A short description of the onboarding page + Convenience initializer for the `OnboardingPage` class which allows to create a custom onboarding + page simply by passing an image name and a text. The text will be displayed underneath the image. + + - parameter imageName: The name of the image to be displayed. + - parameter text: The text to be displayed underneath the image. + + - returns: A simple custom view to be displayed in the onboarding or `nil` when no image + with the given name could be found. */ + public convenience init?(imageNamed imageName: String, text: String, rotateImageInLandscape: Bool = false) { + guard let image = UIImageNamedPreferred(named: imageName) else { return nil } + self.init(image: image, text: text, rotateImageInLandscape: rotateImageInLandscape) + } + + /** + Returns an object initialized from data in a given unarchiver. + + - warning: Not implemented. + */ + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func layoutSubviews() { + super.layoutSubviews() + + if needsToRotateImageInLandscape { + let rotationAngle: CGFloat = frame.width > frame.height ? -.pi / 2 : 0.0 + imageView.transform = CGAffineTransform(rotationAngle: rotationAngle) + } + } + + // MARK: Constraints + fileprivate func addConstraints() { + let superview = self + + // Content view + contentView.translatesAutoresizingMaskIntoConstraints = false + Constraints.active(item: contentView, attr: .top, relatedBy: .greaterThanOrEqual, to: superview, attr: .top, + constant: 30) + Constraints.active(item: contentView, attr: .centerX, relatedBy: .equal, to: superview, attr: .centerX) + Constraints.active(item: contentView, attr: .centerY, relatedBy: .equal, to: superview, attr: .centerY, + constant: 5, priority: 999) + + // Image view + imageView.translatesAutoresizingMaskIntoConstraints = false + Constraints.active(item: imageView, attr: .top, relatedBy: .equal, to: contentView, attr: .top) + Constraints.active(item: imageView, attr: .width, relatedBy: .lessThanOrEqual, to: nil, attr: .width, + constant: 612) + Constraints.active(item: imageView, attr: .width, relatedBy: .greaterThanOrEqual, to: nil, attr: .width, + constant: 75) + Constraints.active(item: imageView, attr: .height, relatedBy: .lessThanOrEqual, to: nil, attr: .height, + constant: 360) + Constraints.active(item: imageView, attr: .height, relatedBy: .greaterThanOrEqual, to: nil, attr: .height, + constant: 100) + Constraints.active(item: imageView, attr: .centerX, relatedBy: .equal, to: contentView, attr: .centerX) - public init(imageName: String, title: String, description: String) { - self.imageName = imageName - self.title = title - self.description = description + // Text label + textLabel.translatesAutoresizingMaskIntoConstraints = false + Constraints.active(item: textLabel, attr: .top, relatedBy: .equal, to: imageView, attr: .bottom, constant: 35) + Constraints.active(item: textLabel, attr: .trailing, relatedBy: .equal, to: contentView, attr: .trailing) + Constraints.active(item: textLabel, attr: .bottom, relatedBy: .equal, to: contentView, attr: .bottom) + Constraints.active(item: textLabel, attr: .leading, relatedBy: .equal, to: contentView, attr: .leading) + Constraints.active(item: textLabel, attr: .width, relatedBy: .equal, to: superview, attr: .width, + multiplier: 2/3) + Constraints.active(item: textLabel, attr: .height, relatedBy: .greaterThanOrEqual, to: nil, attr: .height, + constant: 70) } + } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingViewController.swift index c71f457..0db8ae2 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/OnboardingViewController.swift @@ -1,212 +1,181 @@ // // OnboardingViewController.swift -// GiniCaptureSDK +// GiniCapture // -// Created by Nadya Karaban on 07.06.22. +// Created by Peter Pult on 24/06/16. +// Copyright © 2016 Gini GmbH. All rights reserved. // -import Foundation import UIKit -class OnboardingViewController: UIViewController { - @IBOutlet weak var pagesCollection: UICollectionView! - @IBOutlet weak var pageControl: UIPageControl! - @IBOutlet weak var nextButton: MultilineTitleButton! - private (set) var dataSource: OnboardingDataSource - private let configuration = GiniConfiguration.shared - private var navigationBarBottomAdapter: OnboardingNavigationBarBottomAdapter? - private var bottomNavigationBar: UIView? - - lazy var skipButton = UIBarButtonItem(title: NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.skip", - comment: "Skip button"), - style: .plain, - target: self, - action: #selector(close)) - init() { - dataSource = OnboardingDataSource(configuration: configuration) - super.init(nibName: "OnboardingViewController", bundle: giniCaptureBundle()) - } - - required init?(coder: NSCoder) { +/** + The `OnboardingViewController` provides a custom onboarding screen which presents some + introductory screens to the user on how to get the camera in a perfect position etc. + By default, three screens are pre-configured. + + To allow displaying the onboarding as a transparent modal view, set the `modalPresentationStyle` + of the container class to `.OverCurrentContext`. Add a blank page at the end to make it possible + to "swipe away" the onboarding. To achieve this, the container class needs to implement `UIScrollViewDelegate` + and dismiss the view when the last (empty) page is reached. With the `UIScrollViewDelegate` callbacks + it is also possible to add a custom page control and update the current page accordingly. + + Use the `OnboardingPage` class to quickly create custom onboarding pages in a nice consistent design. + See below how easy it is to present an custom onboarding view controller. + + let pages = [ + OnboardingPage(image: myOnboardingImage1, text: "My Onboarding Page 1"), + OnboardingPage(image: myOnboardingImage2, text: "My Onboarding Page 2"), + OnboardingPage(image: myOnboardingImage3, text: "My Onboarding Page 3") + OnboardingPage(image: myOnboardingImage4, text: "My Onboarding Page 4") + ] + let onboardingController = OnboardingViewController(pages: pages, scrollViewDelegate: self) + presentViewController(onboardingController, animated: true, completion: nil) + + - note: Component API only. + */ +@objcMembers public final class OnboardingViewController: UIViewController { + + weak var scrollViewDelegate: UIScrollViewDelegate? + + /** + Array of views displayed as pages inside the scroll view. + */ + public var pages = [UIView]() + + /** + Scroll view used to display different onboarding pages. + */ + public lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.delegate = self.scrollViewDelegate + scrollView.showsVerticalScrollIndicator = false + scrollView.showsHorizontalScrollIndicator = false + scrollView.isPagingEnabled = true + return scrollView + }() + + private lazy var contentView: UIView = { + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + return contentView + }() + + /** + Designated intitializer for the `OnboardingViewController` which allows to pass a custom set of + views which will be displayed in horizontal scroll view. + + - parameter pages: An array of views to be displayed in the scroll view. + - parameter scrollViewDelegate: The receiver for the scroll view delegate callbacks. + + - returns: A view controller instance intended to allow the user to get a brief overview over + the functionality provided by the Gini Capture SDK. + */ + public init(pages: [UIView], scrollViewDelegate: UIScrollViewDelegate?) { + self.scrollViewDelegate = scrollViewDelegate + self.pages = pages + super.init(nibName: nil, bundle: nil) + } + + /** + Convenience initializer for the `OnboardingViewController` which will set a predefined set + of views as the onboarding pages. + + - parameter scrollViewDelegate: The receiver for the scroll view delegate callbacks. + + - returns: A view controller instance intended to allow the user to get a brief overview over + the functionality provided by the Gini Capture SDK. + */ + public convenience init(scrollViewDelegate: UIScrollViewDelegate?) { + self.init(pages: GiniConfiguration.shared.onboardingPages, scrollViewDelegate: scrollViewDelegate) + } + + /** + Returns an object initialized from data in a given unarchiver. + + - warning: Not implemented. + */ + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func configureCollectionView() { - pagesCollection.register( - UINib(nibName: "OnboardingPageCell", bundle: giniCaptureBundle()), - forCellWithReuseIdentifier: OnboardingPageCell.reuseIdentifier) - pagesCollection.isPagingEnabled = true - pagesCollection.showsHorizontalScrollIndicator = false - pagesCollection.dataSource = dataSource - pagesCollection.delegate = dataSource - dataSource.delegate = self - } - - private func configurePageControl() { - pageControl.pageIndicatorTintColor = GiniColor(light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1 - ).uiColor().withAlphaComponent(0.3) - pageControl.currentPageIndicatorTintColor = GiniColor(light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1 - ).uiColor() - pageControl.addTarget(self, action: #selector(self.pageControlSelectionAction(_:)), for: .valueChanged) - pageControl.numberOfPages = dataSource.itemSections.count - pageControl.isAccessibilityElement = true - } - - private func setupView() { - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - configureCollectionView() - if configuration.bottomNavigationBarEnabled { - configureBottomNavigation() - } else { - configureBasicNavigation() - } - configurePageControl() - } - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - private func layoutBottomNavigationBar(_ navigationBar: UIView) { - navigationBar.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - navigationBar.topAnchor.constraint(equalTo: pageControl.bottomAnchor, constant: 46), - navigationBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), - navigationBar.heightAnchor.constraint(equalToConstant: navigationBar.frame.height), - navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), - navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - } - - private func configureBasicNavigation() { - nextButton.titleLabel?.font = configuration.textStyleFonts[.bodyBold] - nextButton.accessibilityValue = NSLocalizedStringPreferredFormat("ginicapture.onboarding.next", - comment: "Next button") - nextButton.configure(with: GiniConfiguration.shared.primaryButtonConfiguration) - nextButton.addTarget(self, action: #selector(nextPage), for: .touchUpInside) - navigationItem.rightBarButtonItem = skipButton - } - - private func hideTopNavigation() { - navigationController?.setNavigationBarHidden(true, animated: false) - } - - private func configureBottomNavigation() { - hideTopNavigation() - removeButtons() - if let customBottomNavigationBar = configuration.onboardingNavigationBarBottomAdapter { - navigationBarBottomAdapter = customBottomNavigationBar - } else { - navigationBarBottomAdapter = DefaultOnboardingNavigationBarBottomAdapter() - } - navigationBarBottomAdapter?.setNextButtonClickedActionCallback { [weak self] in - self?.nextPage() - } - navigationBarBottomAdapter?.setSkipButtonClickedActionCallback { [weak self] in - self?.close() - } - navigationBarBottomAdapter?.setGetStartedButtonClickedActionCallback { [weak self] in - self?.close() - } - if let navigationBar = navigationBarBottomAdapter?.injectedView() { - bottomNavigationBar = navigationBar - view.addSubview(navigationBar) - layoutBottomNavigationBar(navigationBar) - navigationBarBottomAdapter?.showButtons(navigationButtons: [.skip, .next], navigationBar: navigationBar) - - nextButton.setTitle(NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.next", - comment: "Next button"), for: .normal) - - nextButton.addTarget(self, action: #selector(nextPage), for: .touchUpInside) - navigationItem.rightBarButtonItem = skipButton - } - } - - private func removeButtons() { - nextButton.removeFromSuperview() - } - - @objc private func close() { - dismiss(animated: true) - } - - @objc private func pageControlSelectionAction(_ sender: UIPageControl) { - let index = IndexPath(item: sender.currentPage, section: 0) - pagesCollection.scrollToItem(at: index, at: .centeredHorizontally, animated: true) - } - - @objc private func nextPage() { - if dataSource.currentPage < dataSource.itemSections.count - 1 { - let index = IndexPath(item: dataSource.currentPage + 1, section: 0) - pagesCollection.scrollToItem(at: index, at: .centeredHorizontally, animated: true) - } else { - close() + + public override func loadView() { + super.loadView() + view.addSubview(scrollView) + scrollView.addSubview(contentView) + for page in self.pages { + contentView.addSubview(page) } + + addConstraints() } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - pagesCollection.collectionViewLayout.invalidateLayout() - } - - override func viewWillLayoutSubviews() { + + public override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() - pagesCollection.collectionViewLayout.invalidateLayout() - } - - deinit { - navigationBarBottomAdapter?.onDeinit() + scrollView.contentInsetAdjustmentBehavior = .never + } + + /** + Scrolls the scroll view to the next page. + + - parameter animated: Defines whether scrolling should be animated. + */ + public func scrollToNextPage(_ animated: Bool) { + // Make sure there is no overflow and scrolling only happens from page to page + if let nextOffset = nextPageOffset(), nextOffset.x < scrollView.contentSize.width { + scrollView.setContentOffset(nextOffset, animated: animated) + } } -} + + public func nextPageOffset() -> CGPoint? { + let pageSize = contentView.frame.size.width / CGFloat(pages.count) -extension OnboardingViewController: OnboardingScreen { - func didScroll(page: Int) { - switch page { - case dataSource.itemSections.count - 1: - if configuration.bottomNavigationBarEnabled, - let bottomNavigationBar = bottomNavigationBar { - navigationBarBottomAdapter?.showButtons( - navigationButtons: [.getStarted], - navigationBar: bottomNavigationBar) - } else { - navigationItem.rightBarButtonItem = nil - if nextButton != nil { - nextButton.setTitle(NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.getstarted", - comment: "Get Started button"), for: .normal) - nextButton.accessibilityValue = NSLocalizedStringPreferredFormat( - "ginicapture.onboarding.getstarted", - comment: "Get Started button") - } + for index in 1.. Bool { - return true } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingBottomNavigationBar.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingBottomNavigationBar.swift deleted file mode 100644 index d346187..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingBottomNavigationBar.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// OnboardingBottomNavigationBar.swift -// -// -// Created by Nadya Karaban on 23.05.22. -// - -import Foundation -import UIKit - -final class OnboardingBottomNavigationBar: UIView { - - @IBOutlet weak var nextButton: UIButton! - @IBOutlet weak var skipButton: UIButton! - @IBOutlet weak var getStarted: UIButton! - - override func awakeFromNib() { - super.awakeFromNib() - setupView() - } - - func setupView() { - backgroundColor = GiniColor(light: .GiniCapture.light1, dark: .GiniCapture.dark1).uiColor() - setupButtons() - } - - private func setupButtons() { - let configuration = GiniConfiguration.shared - - nextButton.titleLabel?.font = configuration.textStyleFonts[.bodyBold] - nextButton.configure(with: configuration.primaryButtonConfiguration) - nextButton.isAccessibilityElement = true - - skipButton.titleLabel?.font = configuration.textStyleFonts[.bodyBold] - skipButton.configure(with: configuration.transparentButtonConfiguration) - skipButton.isAccessibilityElement = true - - getStarted.titleLabel?.font = configuration.textStyleFonts[.bodyBold] - getStarted.configure(with: configuration.primaryButtonConfiguration) - getStarted.isAccessibilityElement = true - - skipButton.setTitle(NSLocalizedStringPreferredFormat("ginicapture.onboarding.skip", - comment: "Skip button"), for: .normal) - skipButton.accessibilityValue = NSLocalizedStringPreferredFormat("ginicapture.onboarding.skip", - comment: "Skip button") - - nextButton.setTitle(NSLocalizedStringPreferredFormat("ginicapture.onboarding.next", - comment: "Next button"), for: .normal) - nextButton.accessibilityValue = NSLocalizedStringPreferredFormat("ginicapture.onboarding.next", - comment: "Next button") - - getStarted.setTitle(NSLocalizedStringPreferredFormat("ginicapture.onboarding.getstarted", - comment: "Get Started button"), for: .normal) - getStarted.accessibilityValue = NSLocalizedStringPreferredFormat("ginicapture.onboarding.getstarted", - comment: "Get Started button") - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingImageView.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingImageView.swift deleted file mode 100644 index fb18770..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingImageView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// OnboardingImageView.swift -// GiniCapture -// Created by Nadya Karaban on 08.06.22. -// - -import UIKit - -/** -The ImageOnboardingIllustrationAdapter class implements the OnboardingIllustrationAdapter protocol to provide an image-based illustration for an onboarding view. -*/ - -public class ImageOnboardingIllustrationAdapter: OnboardingIllustrationAdapter { - /** - Called when the onboarding page appears. - */ - public func pageDidAppear() { - } - - /** - Called when the onboarding page disappears. - */ - public func pageDidDisappear() { - } - - /** - Returns a UIImageView instance to be used as the illustration for the onboarding view. - - - Returns: A UIImageView instance. - */ - public func injectedView() -> UIView { - let imageView = UIImageView() - imageView.backgroundColor = UIColor.clear - imageView.contentMode = .scaleAspectFit - return imageView - } - - /** - Initializes and returns a newly allocated ImageOnboardingIllustrationAdapter object. - */ - public init() {} - - /** - Called when the ImageOnboardingIllustrationAdapter object is deallocated. - */ - public func onDeinit() {} -} - -/** -The OnboardingImageView class represents a custom UIView used for displaying onboarding illustrations. -*/ - -public class OnboardingImageView: UIView { - /** The object responsible for providing the illustration to be displayed in the view. */ - public var illustrationAdapter: OnboardingIllustrationAdapter? - - /** The icon to be displayed in the view. Setting this property automatically calls the `setupView()` method to update the view. */ - public var icon: UIImage? { - didSet { - setupView() - } - } - private let injectedViewTag = 1010 - -// MARK: - Initializers - - /** - Initializes and returns a newly allocated OnboardingImageView object with the specified frame rectangle. - - - Parameter frame: The frame rectangle for the view. - */ - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - /** - Sets up the view by adding the injected view to the view hierarchy if it is not already present. - If an injected view is present, its image is updated to reflect the current `icon` value. - */ - public func setupView() { - // add injected view if it wasn't already there - if viewWithTag(injectedViewTag) == nil, let containerView = illustrationAdapter?.injectedView() { - if let imageView = containerView as? UIImageView { - imageView.image = icon - } - containerView.tag = injectedViewTag - containerView.fixInView(self) - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingPageCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingPageCell.swift deleted file mode 100644 index 67f8a3e..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/Views/OnboardingPageCell.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// OnboardingPageCell.swift -// GiniCapture -// Created by Nadya Karaban on 08.06.22. -// - -import Foundation -import UIKit - -class OnboardingPageCell: UICollectionViewCell { - - @IBOutlet weak var iconView: OnboardingImageView! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var descriptionLabel: UILabel! - - @IBOutlet weak var iconBottomConstraint: NSLayoutConstraint! - @IBOutlet weak var topConstraint: NSLayoutConstraint! - - override func awakeFromNib() { - super.awakeFromNib() - setupView() - } - - private func setupView() { - titleLabel.textColor = GiniColor(light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - titleLabel.font = GiniConfiguration.shared.textStyleFonts[.title2Bold] - titleLabel.isAccessibilityElement = true - - descriptionLabel.textColor = GiniColor(light: UIColor.GiniCapture.dark6, - dark: UIColor.GiniCapture.dark7).uiColor() - descriptionLabel.font = GiniConfiguration.shared.textStyleFonts[.title2Bold] - descriptionLabel.isAccessibilityElement = true - - } - - private func calculateIconMargin() -> CGFloat { - let largestHeightDiff: CGFloat = 265 // 932 PRO MAX - 667 SE - let scaleFactor = (UIScreen.main.bounds.size.height - 667) / largestHeightDiff - let diff = Constants.maxIconPadding - Constants.iconPadding - - return Constants.iconPadding + diff * min(scaleFactor, 1) - } - - override func layoutSubviews() { - if UIDevice.current.isIpad { - if UIApplication.shared.statusBarOrientation.isLandscape { - topConstraint.constant = Constants.compactTopPadding - iconBottomConstraint.constant = calculateIconMargin() - } else { - topConstraint.constant = Constants.regularTopPadding - iconBottomConstraint.constant = Constants.maxIconPadding - } - } else { - topConstraint.constant = Constants.compactTopPadding - iconBottomConstraint.constant = calculateIconMargin() - } - super.layoutSubviews() - } - - func configureCell() { - - } - - override func prepareForReuse() { - super.prepareForReuse() - iconView.illustrationAdapter = nil - iconView.icon = nil - iconView.subviews.forEach({ $0.removeFromSuperview() }) - titleLabel.text = "" - descriptionLabel.text = "" - } -} - -private extension OnboardingPageCell { - enum Constants { - static let compactTopPadding: CGFloat = 85 - static let regularTopPadding: CGFloat = 150 - static let iconPadding: CGFloat = 30 - static let maxIconPadding: CGFloat = 58 - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/InjectedViewAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/InjectedViewAdapter.swift deleted file mode 100644 index bdb976c..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/InjectedViewAdapter.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// InjectedViewAdapter.swift -// -// -// Created by Nadya Karaban on 20.05.22. -// - -import Foundation -import UIKit - -/** - * Adapter for injectable views. It allows clients to inject their own views into our layouts. - */ -public protocol InjectedViewAdapter { -/** - * Called when the custom view is required. It will be injected into the SDK's layout. - */ - func injectedView() -> UIView -/** - * Called when the view is destroyed/deinitialized. - */ - func onDeinit() -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/OnboardingIllustrationAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/OnboardingIllustrationAdapter.swift deleted file mode 100644 index 6ab7809..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/OnboardingIllustrationAdapter.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// OnboardingIllustrationAdapter.swift -// -// -// Created by Nadya Karaban on 08.08.22. -// - -import Foundation -/** -* Adapter for injecting a custom onboarding illustration with a custom animation for the onboarding page. -*/ -public protocol OnboardingIllustrationAdapter: InjectedViewAdapter { -/** - * Called when the page appears on screen. If you use animations, then you can start the animation here. - */ - func pageDidAppear() -/** - * Called when the page disappears. If you use animations, then you can stop the animation here. - */ - func pageDidDisappear() -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/OnboardingNavigationBarBottomAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/OnboardingNavigationBarBottomAdapter.swift deleted file mode 100644 index eccab95..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Onboarding/protocols/OnboardingNavigationBarBottomAdapter.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// OnboardingNavigationBarBottomAdapter.swift -// -// -// Created by Krzysztof Kryniecki on 27/10/2022. -// - -import Foundation -import UIKit - -public enum OnboardingNavigationBarBottomButton: Int { - case skip - case next - case getStarted -} - -/** -Protocol for injecting a custom bottom navigation bar on the onboarding screen. - -- note: Bottom navigation only. -*/ -public protocol OnboardingNavigationBarBottomAdapter: InjectedViewAdapter { - - /** - * Called when the displayed buttons have to change. Show only the buttons that are in the list. - * - * - Parameter navigationButtons: The list of buttons that have to be shown - * - Parameter navigationBar: The navigationbar that holds the buttons - */ - func showButtons( - navigationButtons: [OnboardingNavigationBarBottomButton], - navigationBar: UIView) - /** - * Set the callback for the next button action. - * - * - Parameter callback: An action callback, which should be retained and called in next button action method - */ - func setNextButtonClickedActionCallback(_ callback: @escaping () -> Void) - /** - * Set the callback for the skip button action. - * - * - Parameter callback: An action callback, which should be retained and called in skip button action method - */ - func setSkipButtonClickedActionCallback(_ callback: @escaping () -> Void) - /** - * Set the callback for the get started button action. - * - * - Parameter callback: An action callback, which should be retained and called in get started button action method - */ - func setGetStartedButtonClickedActionCallback(_ callback: @escaping () -> Void) -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/DefaultReviewBottomNavigationBarAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/DefaultReviewBottomNavigationBarAdapter.swift deleted file mode 100644 index 73250bc..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/DefaultReviewBottomNavigationBarAdapter.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// DefaultReviewBottomNavigationBarAdapter.swift -// -// -// Created by David Vizaknai on 21.10.2022. -// - -import UIKit - -class DefaultReviewBottomNavigationBarAdapter: ReviewScreenBottomNavigationBarAdapter { - private var mainButtonCallback: (() -> Void)? - private var secondaryButtonCallback: (() -> Void)? - var view: ReviewBottomNavigationBar? - - func setMainButtonClickedActionCallback(_ callback: @escaping () -> Void) { - mainButtonCallback = callback - } - - func setSecondaryButtonClickedActionCallback(_ callback: @escaping () -> Void) { - secondaryButtonCallback = callback - } - - func injectedView() -> UIView { - if let navigationBarView = ReviewBottomNavigationBar().loadNib() as? ReviewBottomNavigationBar { - self.view = navigationBarView - self.view?.delegate = self - return navigationBarView - } else { - return UIView() - } - } - - func set(loadingState isLoading: Bool) { - view?.set(loadingState: isLoading) - } - - @objc func mainButtonClicked() { - mainButtonCallback?() - } - - @objc func secondaryButtonClicked() { - secondaryButtonCallback?() - } - - func onDeinit() { - mainButtonCallback = nil - secondaryButtonCallback = nil - } -} - -extension DefaultReviewBottomNavigationBarAdapter: ReviewBottomNavigationBarDelegate { - func didTapMainButton(on navigationBar: ReviewBottomNavigationBar) { - mainButtonClicked() - } - - func didTapSecondaryButton(on navigationBar: ReviewBottomNavigationBar) { - secondaryButtonClicked() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/OnButtonLoadingIndicatorAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/OnButtonLoadingIndicatorAdapter.swift deleted file mode 100644 index c5ed08f..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/OnButtonLoadingIndicatorAdapter.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// OnButtonLoadingIndicatorAdapter.swift -// -// -// Created by David Vizaknai on 19.10.2022. -// - -import Foundation -import UIKit -/** -* Adapter for injecting a custom loading indicator on top of buttons. -*/ -public protocol OnButtonLoadingIndicatorAdapter: InjectedViewAdapter { - /** - * Called when the ther is loading in the background. You should start the loading indicator animation in this method. - */ - func startAnimation() - /** - * Called when the loading is finished. You should stop the loading indicator animation in this method. - */ - func stopAnimation() -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewBottomNavigationBar.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewBottomNavigationBar.swift deleted file mode 100644 index 9a8b991..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewBottomNavigationBar.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// ReviewBottomNavigationBar.swift -// -// -// Created by David Vizaknai on 24.10.2022. -// - -import UIKit - -protocol ReviewBottomNavigationBarDelegate: AnyObject { - func didTapMainButton(on navigationBar: ReviewBottomNavigationBar) - func didTapSecondaryButton(on navigationBar: ReviewBottomNavigationBar) -} - -final class ReviewBottomNavigationBar: UIView { - private let configuration = GiniConfiguration.shared - weak var delegate: ReviewBottomNavigationBarDelegate? - - @IBOutlet weak var mainButton: MultilineTitleButton! - @IBOutlet weak var secondaryButton: BottomLabelButton! - - override func awakeFromNib() { - super.awakeFromNib() - setupView() - } - - func setupView() { - let configuration = GiniConfiguration.shared - backgroundColor = GiniColor(light: UIColor.GiniCapture.light1, dark: UIColor.GiniCapture.dark1).uiColor() - - mainButton.configure(with: configuration.primaryButtonConfiguration) - mainButton.titleLabel?.font = configuration.textStyleFonts[.bodyBold] - mainButton.setTitle(NSLocalizedStringPreferredFormat("ginicapture.multipagereview.mainButtonTitle", - comment: "Process button title"), for: .normal) - mainButton.addTarget(self, action: #selector(mainButtonClicked), for: .touchUpInside) - - secondaryButton.translatesAutoresizingMaskIntoConstraints = false - secondaryButton.setupButton(with: UIImageNamedPreferred(named: "plus_icon") ?? UIImage(), - name: NSLocalizedStringPreferredFormat( - "ginicapture.multipagereview.secondaryButtonTitle", - comment: "Add pages button title")) - secondaryButton.isHidden = !configuration.multipageEnabled - - secondaryButton.actionLabel.font = configuration.textStyleFonts[.bodyBold] - secondaryButton.configure(with: configuration.addPageButtonConfiguration) - secondaryButton.didTapButton = { [weak self] in - self?.secondaryButtonClicked() - } - addLoadingView() - } - - private var loadingIndicatorView: UIActivityIndicatorView = { - let indicatorView = UIActivityIndicatorView() - indicatorView.hidesWhenStopped = true - indicatorView.style = .whiteLarge - indicatorView.color = GiniColor(light: UIColor.GiniCapture.dark3, dark: UIColor.GiniCapture.light3).uiColor() - return indicatorView - }() - - private func addLoadingView() { - let loadingIndicator: UIView - - if let customLoadingIndicator = configuration.onButtonLoadingIndicator?.injectedView() { - loadingIndicator = customLoadingIndicator - } else { - loadingIndicator = loadingIndicatorView - } - - loadingIndicator.translatesAutoresizingMaskIntoConstraints = false - addSubview(loadingIndicator) - bringSubviewToFront(loadingIndicator) - - NSLayoutConstraint.activate([ - loadingIndicator.centerXAnchor.constraint(equalTo: mainButton.centerXAnchor), - loadingIndicator.centerYAnchor.constraint(equalTo: mainButton.centerYAnchor), - loadingIndicator.widthAnchor.constraint(equalToConstant: 45), - loadingIndicator.heightAnchor.constraint(equalToConstant: 45) - ]) - } - - @objc - private func mainButtonClicked() { - delegate?.didTapMainButton(on: self) - } - - @objc - private func secondaryButtonClicked() { - delegate?.didTapSecondaryButton(on: self) - } - - func set(loadingState isLoading: Bool) { - if self.configuration.multipageEnabled { - if !isLoading { - self.mainButton.alpha = 1 - self.mainButton.isEnabled = true - self.hideAnimation() - return - } - - self.mainButton.alpha = 0.3 - self.mainButton.isEnabled = false - self.showAnimation() - } - } - - private func showAnimation() { - if let loadingIndicator = configuration.onButtonLoadingIndicator { - loadingIndicator.startAnimation() - } else { - loadingIndicatorView.startAnimating() - } - } - - private func hideAnimation() { - if let loadingIndicator = configuration.onButtonLoadingIndicator { - loadingIndicator.stopAnimation() - } else { - loadingIndicatorView.stopAnimating() - } - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewCollectionCell.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewCollectionCell.swift deleted file mode 100644 index 8e96849..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewCollectionCell.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// ReviewCollectionCell.swift -// GiniCapture -// -// Created by Vizaknai David on 28.09.2022. -// - -import UIKit - -protocol ReviewCollectionViewDelegate: AnyObject { - func didTapDelete(on cell: ReviewCollectionCell) -} - -final class ReviewCollectionCell: UICollectionViewCell { - weak var delegate: ReviewCollectionViewDelegate? - - lazy var documentImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - imageView.clipsToBounds = true - imageView.accessibilityLabel = NSLocalizedStringPreferredFormat("ginicapture.review.documentImageTitle", - comment: "Document") - imageView.backgroundColor = GiniColor(light: UIColor.GiniCapture.light1, - dark: UIColor.GiniCapture.dark1).uiColor() - return imageView - }() - - private lazy var deleteButton: UIButton = { - let deleteIcon = UIImageNamedPreferred(named: "delete_icon") - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(deleteIcon, for: .normal) - button.imageView?.contentMode = .scaleAspectFit - button.addTarget(self, action: #selector(didTapDelete), for: .touchUpInside) - button.isHidden = true - button.isAccessibilityElement = true - button.accessibilityLabel = NSLocalizedStringPreferredFormat("ginicapture.review.delete", comment: "Delete") - return button - }() - - private func setActiveStatus(_ isActive: Bool) { - documentImageView.layer.borderColor = isActive ? UIColor.GiniCapture.accent1.cgColor : UIColor.clear.cgColor - documentImageView.layer.borderWidth = isActive ? 2 : 0 - deleteButton.isHidden = !isActive - } - - var isActive: Bool = false { - didSet { - setActiveStatus(isActive) - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - addSubview(documentImageView) - addSubview(deleteButton) - bringSubviewToFront(deleteButton) - - addConstraints() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(frame:) has not been implemented") - } - - private func addConstraints() { - - NSLayoutConstraint.activate([ - documentImageView.leadingAnchor.constraint(equalTo: leadingAnchor), - documentImageView.trailingAnchor.constraint(equalTo: trailingAnchor), - documentImageView.topAnchor.constraint(equalTo: topAnchor), - documentImageView.bottomAnchor.constraint(equalTo: bottomAnchor), - - deleteButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), - deleteButton.topAnchor.constraint(equalTo: topAnchor, constant: 16), - deleteButton.heightAnchor.constraint(equalToConstant: 44), - deleteButton.widthAnchor.constraint(equalToConstant: 44) - ]) - } - - @objc - private func didTapDelete() { - delegate?.didTapDelete(on: self) - } - - override func prepareForReuse() { - super.prepareForReuse() - isActive = false - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewCollectionsCellPresenter.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewCollectionsCellPresenter.swift deleted file mode 100644 index e918c04..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewCollectionsCellPresenter.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// ReviewCollectionCellPresenter.swift -// GiniCapture -// -// Created by Enrique del Pozo Gómez on 12/28/18. -// - -import UIKit - -protocol ReviewCollectionCellPresenterDelegate: AnyObject { - func multipage(_ reviewCollectionCellPresenter: ReviewCollectionCellPresenter, - didUpdateCellAt indexPath: IndexPath) -} - -final class ReviewCollectionCellPresenter { - weak var delegate: ReviewCollectionCellPresenterDelegate? - var thumbnails: [String: [ThumbnailType: UIImage]] = [:] - private let giniConfiguration: GiniConfiguration - private let thumbnailsQueue = DispatchQueue(label: "Thumbnails queue") - - enum ThumbnailType { - case big, small - - var scale: CGFloat { - switch self { - case .big: - return 1.0 - case .small: - return 1/4 - } - } - } - - init(giniConfiguration: GiniConfiguration = .shared) { - self.giniConfiguration = giniConfiguration - } - - func setUp(_ cell: ReviewCollectionCell, - with page: GiniCapturePage, - at indexPath: IndexPath) -> UICollectionViewCell { - - if let thumbnail = self.thumbnails[page.document.id, default: [:]][.big] { - cell.documentImageView.image = thumbnail - } else { - cell.documentImageView.image = nil - fetchThumbnailImage(for: page, of: .big, in: cell, at: indexPath) - } - - return cell - } - - // MARK: - Thumbnails - - private func fetchThumbnailImage(for page: GiniCapturePage, - of type: ThumbnailType, - in cell: ReviewCollectionCell, - at indexPath: IndexPath) { - thumbnailsQueue.async { [weak self] in - guard let self = self else { return } - let thumbnail = UIImage.downsample(from: page.document.data, - to: self.targetThumbnailSize(from: page.document.data), - scale: type.scale) - self.thumbnails[page.document.id, default: [:]][type] = thumbnail - - DispatchQueue.main.async { - self.delegate?.multipage(self, didUpdateCellAt: indexPath) - } - } - } - - private func targetThumbnailSize(from imageData: Data, screen: UIScreen = .main) -> CGSize { - let imageSize = UIImage(data: imageData)?.size ?? .zero - - if imageSize.width > (screen.bounds.size.width * 2) { - let maxWidth = screen.bounds.size.width * 2 - return CGSize(width: maxWidth, height: imageSize.height * maxWidth / imageSize.width) - } else { - return imageSize - } - - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewScreenBottomNavigationBarAdapter.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewScreenBottomNavigationBarAdapter.swift deleted file mode 100644 index 34a9573..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewScreenBottomNavigationBarAdapter.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ReviewScreenBottomNavigationBarAdapter.swift -// -// -// Created by David Vizaknai on 21.10.2022. -// - -import UIKit - -/** -Protocol for injecting a custom bottom navigation bar on the Review screen. - -- note: Bottom navigation only. -*/ -public protocol ReviewScreenBottomNavigationBarAdapter: InjectedViewAdapter { - /** - * Set the callback for the 'Process documents' button action. - * - * - Parameter callback: An action callback, which should be retained and called in 'Process documents' button action method - */ - func setMainButtonClickedActionCallback(_ callback: @escaping () -> Void) - /** - * Set the callback for the Add Pages button action. - * - * - Parameter callback: An action callback, which should be retained and called in back button action method - */ - func setSecondaryButtonClickedActionCallback(_ callback: @escaping () -> Void) - - /** - * Set the loading indicator state on the 'Process documents' button when multipage upload is happening. - * - * - Parameter isLoading: A boolean to set the loading state of the button - */ - func set(loadingState isLoading: Bool) -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewViewController.swift index 3a86db3..41c89ed 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewViewController.swift @@ -2,712 +2,302 @@ // ReviewViewController.swift // GiniCapture // -// Created by Vizaknai David on 28.09.2022 +// Created by Peter Pult on 20/06/16. +// Copyright © 2016 Gini GmbH. All rights reserved. // import UIKit /** - The ReviewViewControllerDelegate protocol defines methods that allow you to handle user actions in the - ReviewViewControllerDelegate - (tap add, delete...) + The ReviewViewControllerDelegate protocol defines methods that allow you to manage when a user rotates an image. - note: Component API only. - - TODO: - REMOVE Componen API */ -public protocol ReviewViewControllerDelegate: AnyObject { - /** - Called when a user deletes one of the pages. - - - parameter viewController: `ReviewViewController` where the pages are reviewed. - - parameter page: Page deleted. - */ - func review(_ viewController: ReviewViewController, didDelete page: GiniCapturePage) - - /** - Called when a user taps on the error action when the errored page - - - parameter viewController: `ReviewViewController` where the pages are reviewed. - - parameter errorAction: `NoticeActionType` selected. - - parameter page: Page where the error action has been triggered - */ - func review(_ viewController: ReviewViewController, didTapRetryUploadFor page: GiniCapturePage) - - /** - Called when a user taps on the add page button - - - parameter viewController: `ReviewViewController` where the pages are reviewed. - */ - func reviewDidTapAddImage(_ viewController: ReviewViewController) - /** - Called when a user taps on the process documents button - - - parameter viewController: `ReviewViewController` where the pages are reviewed. - */ - func reviewDidTapProcess(_ viewController: ReviewViewController) - - /** - Called when a user taps on a page - - - parameter viewController: `ReviewViewController` where the pages are reviewed. - - parameter page: Page that the user selected - */ - func review(_ viewController: ReviewViewController, didSelectPage page: GiniCapturePage) +@objc public protocol ReviewViewControllerDelegate: AnyObject { + func review(_ viewController: ReviewViewController, didReview document: GiniCaptureDocument) } /** - The `ReviewViewController` provides a custom review screen. The user has the option to check - for blurriness and document orientation. If the result is not satisfying, the user can return to the camera screen. - The photo should be uploaded to Gini’s backend immediately after having been taken as it is safe to assume that - in most cases the photo is good enough to be processed further. - - - note: Component API only. - */ - -public final class ReviewViewController: UIViewController { - + The `ReviewViewController` provides a custom review screen. The user has the option to check + for blurriness and document orientation. If the result is not satisfying, the user can either + return to the camera screen or rotate the photo by steps of 90 degrees. The photo should be + uploaded to Gini’s backend immediately after having been taken as it is safe to assume that + in most cases the photo is good enough to be processed further. + + - note: Component API only. + */ +@objcMembers public final class ReviewViewController: UIViewController { + /** The object that acts as the delegate of the review view controller. */ public weak var delegate: ReviewViewControllerDelegate? - - var pages: [GiniCapturePage] - var resetToEnd = false - fileprivate let giniConfiguration: GiniConfiguration - fileprivate lazy var presenter: ReviewCollectionCellPresenter = { - let presenter = ReviewCollectionCellPresenter() - presenter.delegate = self - return presenter - }() - - private var navigationBarBottomAdapter: ReviewScreenBottomNavigationBarAdapter? - - // MARK: - UI initialization - - private lazy var scrollView: UIScrollView = { + + // User interface + fileprivate lazy var scrollView: UIScrollView = { let scrollView = UIScrollView() - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false scrollView.translatesAutoresizingMaskIntoConstraints = false - return scrollView - }() - - private lazy var contentView: UIView = { - let contentView = UIView() - contentView.translatesAutoresizingMaskIntoConstraints = false - return contentView - }() - - lazy var collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.minimumLineSpacing = 8 - layout.minimumInteritemSpacing = 1 - - var collection = UICollectionView(frame: .zero, collectionViewLayout: layout) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, - dark: UIColor.GiniCapture.dark2).uiColor() - collection.dataSource = self - collection.delegate = self - collection.showsHorizontalScrollIndicator = false - collection.register(ReviewCollectionCell.self, - forCellWithReuseIdentifier: ReviewCollectionCell.reuseIdentifier) - - collection.isScrollEnabled = false - collection.contentInsetAdjustmentBehavior = .never - let swipeLeftRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipeHandler(sender:))) - swipeLeftRecognizer.direction = .left - collection.addGestureRecognizer(swipeLeftRecognizer) + let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) + doubleTapGesture.numberOfTapsRequired = 2 + scrollView.addGestureRecognizer(doubleTapGesture) - let swipeRightRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipeHandler(sender:))) - swipeRightRecognizer.direction = .right - collection.addGestureRecognizer(swipeRightRecognizer) - return collection - }() - - private lazy var tipLabel: UILabel = { - var tipLabel = UILabel() - tipLabel.font = giniConfiguration.textStyleFonts[.caption2] - tipLabel.textAlignment = .center - tipLabel.adjustsFontForContentSizeCategory = true - tipLabel.translatesAutoresizingMaskIntoConstraints = false - tipLabel.textColor = GiniColor(light: .GiniCapture.dark1, dark: .GiniCapture.light1).uiColor() - tipLabel.isAccessibilityElement = true - tipLabel.numberOfLines = 0 - tipLabel.text = NSLocalizedStringPreferredFormat("ginicapture.multipagereview.description", - comment: "Tip on review screen") - - return tipLabel - }() - - private lazy var pageControl: UIPageControl = { - let pageControl = UIPageControl() - pageControl.numberOfPages = pages.count - pageControl.currentPage = 0 - pageControl.pageIndicatorTintColor = GiniColor(light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1) - .uiColor().withAlphaComponent(0.3) - pageControl.currentPageIndicatorTintColor = GiniColor(light: UIColor.GiniCapture.dark1, - dark: UIColor.GiniCapture.light1).uiColor() - pageControl.translatesAutoresizingMaskIntoConstraints = false - pageControl.isAccessibilityElement = true - pageControl.addTarget(self, action: #selector(pageControlTapHandler(sender:)), for: .touchUpInside) - - return pageControl + return scrollView }() - - private lazy var processButton: MultilineTitleButton = { - let button = MultilineTitleButton() - button.configure(with: giniConfiguration.primaryButtonConfiguration) - button.titleLabel?.font = giniConfiguration.textStyleFonts[.bodyBold] - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(NSLocalizedStringPreferredFormat("ginicapture.multipagereview.mainButtonTitle", - comment: "Process button title"), for: .normal) - button.addTarget(self, action: #selector(didTapProcessDocument), for: .touchUpInside) - button.isAccessibilityElement = true - button.accessibilityLabel = NSLocalizedStringPreferredFormat("ginicapture.multipagereview.mainButtonTitle", - comment: "Process button title") - return button + + fileprivate var imageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.accessibilityLabel = .localized(resource: ReviewStrings.documentImageTitle) + return imageView }() - - private lazy var addPagesButton: BottomLabelButton = { - let addPagesButton = BottomLabelButton() - addPagesButton.translatesAutoresizingMaskIntoConstraints = false - addPagesButton.setupButton(with: UIImageNamedPreferred(named: "plus_icon") ?? UIImage(), - name: NSLocalizedStringPreferredFormat( - "ginicapture.multipagereview.secondaryButtonTitle", - comment: "Add pages button title")) - addPagesButton.isHidden = !giniConfiguration.multipageEnabled - addPagesButton.actionLabel.font = giniConfiguration.textStyleFonts[.bodyBold] - addPagesButton.configure(with: giniConfiguration.addPageButtonConfiguration) - addPagesButton.didTapButton = { [weak self] in - guard let self = self else { return } - self.setCellStatus(for: self.currentPage, isActive: false) - self.delegate?.reviewDidTapAddImage(self) - } - addPagesButton.isAccessibilityElement = true - addPagesButton.accessibilityLabel = NSLocalizedStringPreferredFormat( - "ginicapture.multipagereview.secondaryButtonTitle", - comment: "Add pages button title") - return addPagesButton + + fileprivate var topView: UIView = { + let topView = NoticeView(text: .localized(resource: ReviewStrings.topText)) + topView.translatesAutoresizingMaskIntoConstraints = false + return topView }() - - private lazy var buttonContainer: UIView = { + + fileprivate var bottomView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = GiniConfiguration.shared + .reviewBottomViewBackgroundColor + .withAlphaComponent(0.8) return view }() + + lazy var rotateButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(rotate), for: .touchUpInside) + button.accessibilityLabel = .localized(resource: ReviewStrings.rotateButton) - private var loadingIndicatorView: UIActivityIndicatorView = { - let indicatorView = UIActivityIndicatorView() - indicatorView.hidesWhenStopped = true - indicatorView.style = .whiteLarge - indicatorView.color = GiniColor(light: UIColor.GiniCapture.dark3, dark: UIColor.GiniCapture.light3).uiColor() - return indicatorView + return button }() - - private lazy var cellSize: CGSize = { - return calculatedCellSize() + + fileprivate lazy var bottomLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = .localized(resource: ReviewStrings.bottomText) + label.numberOfLines = 0 + label.textColor = giniConfiguration.reviewTextBottomColor + label.textAlignment = .right + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.7 + label.font = giniConfiguration.customFont.isEnabled ? + giniConfiguration.customFont.with(weight: .thin, size: 12, style: .footnote) : + giniConfiguration.reviewTextBottomFont + return label }() - - private lazy var collectionViewHeightConstraint = collectionView.heightAnchor.constraint( - equalToConstant: view.frame.height * 0.5) - - private var currentPage: Int = 0 { - didSet { - setCellStatus(for: currentPage, isActive: true) - } + + // Properties + fileprivate var imageViewBottomConstraint: NSLayoutConstraint! + fileprivate var imageViewLeadingConstraint: NSLayoutConstraint! + fileprivate var imageViewTopConstraint: NSLayoutConstraint! + fileprivate var imageViewTrailingConstraint: NSLayoutConstraint! + fileprivate var currentDocument: GiniCaptureDocument? + fileprivate var giniConfiguration: GiniConfiguration + + // Images + fileprivate var rotateButtonImage: UIImage? { + return UIImageNamedPreferred(named: "reviewRotateButton") } - - // This is needed in order to "catch" the screen rotation on the modally presented viewcontroller - private var previousScreenHeight: CGFloat = UIScreen.main.bounds.height - - // MARK: - Constraints - - private lazy var scrollViewConstraints: [NSLayoutConstraint] = [ - scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ] - - private lazy var contenViewConstraints: [NSLayoutConstraint] = [ - contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), - contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.heightAnchor), - contentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.bottomAnchor) - ] - - private lazy var tipLabelConstraints: [NSLayoutConstraint] = [ - tipLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.padding), - tipLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - tipLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - tipLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: Constants.titleHeight), - tipLabel.heightAnchor.constraint(lessThanOrEqualToConstant: Constants.maxTitleHeight)] - - private lazy var collectionViewConstraints: [NSLayoutConstraint] = [ - collectionView.topAnchor.constraint(equalTo: tipLabel.bottomAnchor, constant: Constants.padding), - collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - collectionViewHeightConstraint] - - private lazy var pageControlConstraints: [NSLayoutConstraint] = [ - pageControl.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: Constants.padding * 2), - pageControl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - pageControl.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)] - - private lazy var processButtonConstraints: [NSLayoutConstraint] = [ - processButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor), - processButton.widthAnchor.constraint(greaterThanOrEqualToConstant: Constants.buttonSize.width), - processButton.leadingAnchor.constraint(equalTo: buttonContainer.leadingAnchor), - processButton.heightAnchor.constraint(greaterThanOrEqualToConstant: Constants.buttonSize.height), - processButton.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: -8), - processButton.trailingAnchor.constraint(lessThanOrEqualTo: buttonContainer.trailingAnchor)] - - private lazy var addPagesButtonConstraints: [NSLayoutConstraint] = [ - addPagesButton.centerYAnchor.constraint(equalTo: processButton.centerYAnchor), - addPagesButton.leadingAnchor.constraint(equalTo: processButton.trailingAnchor, - constant: Constants.padding), - addPagesButton.trailingAnchor.constraint(equalTo: buttonContainer.trailingAnchor)] - - private lazy var buttonContainerConstraints: [NSLayoutConstraint] = [ - buttonContainer.topAnchor.constraint(equalTo: pageControl.bottomAnchor, constant: Constants.padding * 2), - buttonContainer.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - buttonContainer.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, - constant: -Constants.bottomPadding), - buttonContainer.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor) - ] - - // MARK: - Init - + /** - Called to initialize the ReviewViewController - - - parameter pages: the documents to be initalized with - - parameter giniConfiguration: the configuration of the SDK - - - note: Internal usage only. + Designated initializer for ReviewViewController + + - parameter document: Document to be reviewed + - paramter giniConfiguration: `GiniConfiguration` + + - note: Component API only. */ - - public init(pages: [GiniCapturePage], giniConfiguration: GiniConfiguration) { - self.pages = pages + public init(document: GiniCaptureDocument, giniConfiguration: GiniConfiguration) { self.giniConfiguration = giniConfiguration + self.currentDocument = document super.init(nibName: nil, bundle: nil) } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(imageDocuments:) has not been implemented") - } -} - -// MARK: - BottomNavigation - -extension ReviewViewController { - private func configureBottomNavigationBar() { - if giniConfiguration.bottomNavigationBarEnabled { - if let bottomBar = giniConfiguration.reviewNavigationBarBottomAdapter { - navigationBarBottomAdapter = bottomBar - } else { - navigationBarBottomAdapter = DefaultReviewBottomNavigationBarAdapter() - } - navigationBarBottomAdapter?.setMainButtonClickedActionCallback { [weak self] in - guard let self = self else { return } - self.delegate?.reviewDidTapProcess(self) - } - navigationBarBottomAdapter?.setSecondaryButtonClickedActionCallback { [weak self] in - guard let self = self else { return } - self.setCellStatus(for: self.currentPage, isActive: false) - self.delegate?.reviewDidTapAddImage(self) - } - - if let navigationBar = - navigationBarBottomAdapter?.injectedView() { - view.addSubview(navigationBar) - layoutBottomNavigationBar(navigationBar) - } - } - } - - private func layoutBottomNavigationBar(_ navigationBar: UIView) { - navigationBar.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(navigationBar) - NSLayoutConstraint.activate([ - navigationBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), - navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), - navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), - navigationBar.heightAnchor.constraint(equalToConstant: Constants.bottomNavigationBarHeight) - ]) - view.bringSubviewToFront(navigationBar) - view.layoutSubviews() + + /** + Returns an object initialized from data in a given unarchiver. + + - warning: Not implemented. + */ + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") } + + public override func loadView() { + super.loadView() + edgesForExtendedLayout = [] + view.backgroundColor = .black -} - -// MARK: - UIViewController - -extension ReviewViewController { - override public func viewDidLoad() { - super.viewDidLoad() - - setupView() + scrollView.delegate = self + imageView.image = currentDocument?.previewImage + rotateButton.setImage(rotateButtonImage, for: .normal) + + // Configure view hierachy + view.addSubview(scrollView) + view.addSubview(topView) + view.addSubview(bottomView) + scrollView.addSubview(imageView) + bottomView.addSubview(rotateButton) + bottomView.addSubview(bottomLabel) + addConstraints() - configureBottomNavigationBar() - addLoadingView() - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if !giniConfiguration.multipageEnabled || pages.count == 1 { - setCellStatus(for: 0, isActive: true) - } else { - if resetToEnd { - resetToEnd = false - } - setCellStatus(for: currentPage, isActive: true) - } - collectionView.reloadData() + view.layoutIfNeeded() } - - public override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - collectionView.collectionViewLayout.invalidateLayout() - } - + + /** + Called to notify the view controller that its view has just laid out its subviews. + */ public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - let size = calculatedCellSize() - collectionViewHeightConstraint.constant = size.height + 4 - if UIDevice.current.isIpad { - // cellSize needs to be updated when the screen is rotated - self.cellSize = size - - DispatchQueue.main.async { - guard self.previousScreenHeight != UIScreen.main.bounds.height else { return } - guard self.pages.count > 1 else { return } - self.setCellStatus(for: self.currentPage, isActive: false) - self.collectionView.reloadData() - - self.collectionView.scrollToItem(at: IndexPath(row: self.currentPage, section: 0), - at: .centeredHorizontally, animated: true) - - self.previousScreenHeight = UIScreen.main.bounds.height - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.setCellStatus(for: self.currentPage, isActive: true) - } - } - } - } - - private func setupView() { - title = NSLocalizedStringPreferredFormat("ginicapture.multipagereview.title", - comment: "Screen title") - view.backgroundColor = GiniColor(light: UIColor.GiniCapture.light2, dark: UIColor.GiniCapture.dark2).uiColor() - - view.addSubview(scrollView) - scrollView.addSubview(contentView) - contentView.addSubview(tipLabel) - contentView.addSubview(collectionView) - contentView.addSubview(pageControl) - if !giniConfiguration.bottomNavigationBarEnabled { - contentView.addSubview(buttonContainer) - buttonContainer.addSubview(processButton) - if giniConfiguration.multipageEnabled { - buttonContainer.addSubview(addPagesButton) - } - } - edgesForExtendedLayout = [] - } - - // MARK: - Loading indicator - - private func addLoadingView() { - guard !giniConfiguration.bottomNavigationBarEnabled else { return } - let loadingIndicator: UIView - - if let customLoadingIndicator = giniConfiguration.onButtonLoadingIndicator?.injectedView() { - loadingIndicator = customLoadingIndicator - } else { - loadingIndicator = loadingIndicatorView - } - - loadingIndicator.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(loadingIndicator) - view.bringSubviewToFront(loadingIndicator) - - NSLayoutConstraint.activate([ - loadingIndicator.centerXAnchor.constraint(equalTo: processButton.centerXAnchor), - loadingIndicator.centerYAnchor.constraint(equalTo: processButton.centerYAnchor), - loadingIndicator.widthAnchor.constraint(equalToConstant: 45), - loadingIndicator.heightAnchor.constraint(equalToConstant: 45) - ]) + + updateMinZoomScaleForSize(scrollView.bounds.size) + + // On initialization imageView's frame is (0,0) so the image needs to be centered + // inside the ScrollView when its size has changed + self.updateConstraintsForSize(scrollView.bounds.size) } - - private func showAnimation() { - if let loadingIndicator = giniConfiguration.onButtonLoadingIndicator { - loadingIndicator.startAnimation() - } else { - loadingIndicatorView.startAnimating() - } - } - - private func hideAnimation() { - if let loadingIndicator = giniConfiguration.onButtonLoadingIndicator { - loadingIndicator.stopAnimation() - } else { - loadingIndicatorView.stopAnimating() - } - } - + /** - Updates the collections with the given pages. + Notifies the view controller that its view was added to a view hierarchy. - - parameter pages: Pages to be used in the collections. + - parameter animated: If true, the view was added to the window using an animation. */ - - public func updateCollections(with pages: [GiniCapturePage], finishedUpload: Bool = true) { + public override func viewDidAppear(_ animated: Bool) { DispatchQueue.main.async { - if self.giniConfiguration.bottomNavigationBarEnabled { - self.navigationBarBottomAdapter?.set(loadingState: !finishedUpload) - } - - if self.giniConfiguration.multipageEnabled { - if finishedUpload { - self.processButton.alpha = 1 - self.processButton.isEnabled = true - self.hideAnimation() - return - } - - self.processButton.alpha = 0.3 - self.processButton.isEnabled = false - self.showAnimation() - } - } - - self.pages = pages - guard !finishedUpload else { return } - - collectionView.reloadData() - // Update cell status only if pages not empty - if pages.isNotEmpty { - guard pages.count > 1 else { return } - DispatchQueue.main.async { - self.setCellStatus(for: self.currentPage, isActive: false) - - self.collectionView.scrollToItem(at: IndexPath(row: self.pages.count - 1, section: 0), - at: .centeredHorizontally, animated: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - self.setCurrentPage(basedOn: self.collectionView) - } - } + (self.topView as? NoticeView)?.show() } } -} - -// MARK: - Private methods - -extension ReviewViewController { - private func addConstraints() { - collectionViewHeightConstraint.priority = .defaultLow - - NSLayoutConstraint.activate(scrollViewConstraints) - NSLayoutConstraint.activate(contenViewConstraints) - NSLayoutConstraint.activate(tipLabelConstraints) - NSLayoutConstraint.activate(collectionViewConstraints) - NSLayoutConstraint.activate(pageControlConstraints) - - if !giniConfiguration.bottomNavigationBarEnabled { - NSLayoutConstraint.activate(buttonContainerConstraints) - NSLayoutConstraint.activate(processButtonConstraints) - if giniConfiguration.multipageEnabled { - NSLayoutConstraint.activate(addPagesButtonConstraints) - } + + private func didReview(_ imageDocument: GiniImageDocument) { + if let delegate = delegate { + delegate.review(self, didReview: imageDocument) } else { - NSLayoutConstraint.activate([ - pageControl.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, - constant: -Constants.pageControlBottomPadding), - collectionView.bottomAnchor.constraint(greaterThanOrEqualTo: pageControl.topAnchor, - constant: -Constants.padding * 2) - ]) + assertionFailure("The ReviewViewControllerDelegate has not been assigned") } } - - @objc - private func pageControlTapHandler(sender: UIPageControl) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01, execute: { [weak self] in - self?.collectionView.scrollToItem(at: IndexPath(row: sender.currentPage, section: 0), - at: .centeredHorizontally, animated: true) - guard let self = self else { return } - self.setCellStatus(for: self.currentPage, isActive: false) - self.currentPage = sender.currentPage - }) - } - - @objc - private func didTapProcessDocument() { - delegate?.reviewDidTapProcess(self) - } - - @objc - private func swipeHandler(sender: UISwipeGestureRecognizer) { - guard pages.count > 1 else { return } - if sender.direction == .left { - guard currentPage < pages.count - 1 else { return } - setCellStatus(for: currentPage, isActive: false) - currentPage += 1 - collectionView.scrollToItem(at: IndexPath(row: currentPage, section: 0), - at: .centeredHorizontally, animated: true) - pageControl.currentPage = currentPage - } else if sender.direction == .right { - guard currentPage > 0 else { return } - setCellStatus(for: currentPage, isActive: false) - currentPage -= 1 - collectionView.scrollToItem(at: IndexPath(row: currentPage, section: 0), - at: .centeredHorizontally, animated: true) - pageControl.currentPage = currentPage - } + + // MARK: Rotation handling + @objc fileprivate func rotate(_ sender: AnyObject) { + guard let rotatedImage = imageView.image?.rotated90Degrees() else { return } + guard let imageDocument = currentDocument as? GiniImageDocument else { return } + + imageView.image = rotatedImage + imageDocument.rotatePreviewImage90Degrees() + didReview(imageDocument) } - - private func calculatedCellSize() -> CGSize { - let a4Ratio = 1.4142 - if UIDevice.current.isIpad { - var height = self.view.bounds.height - 260 - if giniConfiguration.bottomNavigationBarEnabled { - height -= Constants.bottomNavigationBarHeight - height -= Constants.padding - } - let width = height / a4Ratio - return CGSize(width: width, height: height) + + // MARK: Zoom handling + @objc fileprivate func handleDoubleTap(_ sender: UITapGestureRecognizer) { + if scrollView.zoomScale > scrollView.minimumZoomScale { + scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true) } else { - if view.safeAreaInsets.bottom > 0 { - let height = self.view.bounds.height * 0.6 - let width = height / a4Ratio - let cellSize = CGSize(width: width, height: height) - return cellSize - } else { - let height = self.view.bounds.height * 0.5 - let width = height / a4Ratio - let cellSize = CGSize(width: width, height: height) - return cellSize - } - + scrollView.setZoomScale(scrollView.maximumZoomScale, animated: true) } } -} - -// MARK: - Toolbar actions - -extension ReviewViewController { - private func deleteItem(at indexPath: IndexPath) { - let pageToDelete = pages[indexPath.row] - pages.remove(at: indexPath.row) - collectionView.deleteItems(at: [indexPath]) - delegate?.review(self, didDelete: pageToDelete) - } -} - -// MARK: UICollectionViewDataSource - -extension ReviewViewController: UICollectionViewDataSource { - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - pageControl.numberOfPages = pages.count - pageControl.isHidden = !(pages.count > 1) - - return pages.count - } - - public func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let page = pages[indexPath.row] - if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReviewCollectionCell.reuseIdentifier, - for: indexPath) as? ReviewCollectionCell { - cell.delegate = self - cell.isActive = currentPage == indexPath.row - return presenter.setUp(cell, with: page, at: indexPath) - } - fatalError("ReviewCollectionCell wasn't initialized") + + fileprivate func updateMinZoomScaleForSize(_ size: CGSize) { + guard let image = imageView.image else { return } + let widthScale = size.width / image.size.width + let heightScale = size.height / image.size.height + let minScale = min(widthScale, heightScale) + scrollView.minimumZoomScale = minScale + scrollView.zoomScale = minScale } -} - -// MARK: - ReviewCollectionsAdapterDelegate - -extension ReviewViewController: ReviewCollectionCellPresenterDelegate { - func multipage(_ reviewCollectionCellPresenter: ReviewCollectionCellPresenter, - didUpdateCellAt indexPath: IndexPath) { - collectionView.reloadItems(at: [indexPath]) + + fileprivate func updateConstraintsForSize(_ size: CGSize) { + let yOffset = max(0, (size.height - imageView.frame.height) / 2) + imageViewTopConstraint.constant = yOffset + imageViewBottomConstraint.constant = yOffset + let xOffset = max(0, (size.width - imageView.frame.width) / 2) + imageViewLeadingConstraint.constant = xOffset + imageViewTrailingConstraint.constant = xOffset + + view.layoutIfNeeded() } - - func multipage(_ reviewCollectionCellPresenter: ReviewCollectionCellPresenter, - didUpdateElementIn collectionView: UICollectionView, - at indexPath: IndexPath) { - collectionView.reloadItems(at: [indexPath]) + + // MARK: Constraints + fileprivate func addConstraints() { + // Scroll view + Constraints.active(item: scrollView, attr: .top, relatedBy: .equal, to: view.safeAreaLayoutGuide, attr: .top) + Constraints.active(item: scrollView, attr: .trailing, relatedBy: .equal, to: view, attr: .trailing) + Constraints.active(item: scrollView, attr: .bottom, relatedBy: .equal, to: view.safeAreaLayoutGuide, attr: .bottom) + Constraints.active(item: scrollView, attr: .leading, relatedBy: .equal, to: view, attr: .leading) + + // Image view + imageViewTopConstraint = NSLayoutConstraint(item: imageView, attribute: .top, relatedBy: .equal, + toItem: scrollView, attribute: .top, multiplier: 1, constant: 0) + imageViewTrailingConstraint = NSLayoutConstraint(item: imageView, attribute: .trailing, relatedBy: .equal, + toItem: scrollView, attribute: .trailing, multiplier: 1, + constant: 0) + imageViewBottomConstraint = NSLayoutConstraint(item: imageView, attribute: .bottom, relatedBy: .equal, + toItem: scrollView, attribute: .bottom, multiplier: 1, + constant: 0) + imageViewLeadingConstraint = NSLayoutConstraint(item: imageView, attribute: .leading, relatedBy: .equal, + toItem: scrollView, attribute: .leading, multiplier: 1, + constant: 0) + Constraints.active(constraint: imageViewTopConstraint) + Constraints.active(constraint: imageViewTrailingConstraint) + Constraints.active(constraint: imageViewBottomConstraint) + Constraints.active(constraint: imageViewLeadingConstraint) + + // Top view + Constraints.pin(view: topView, toSuperView: view, positions: [.top, .left, .right]) + + // Bottom view + Constraints.active(item: bottomView, attr: .top, relatedBy: .equal, to: scrollView, attr: .bottom, + priority: 750) + Constraints.active(item: bottomView, attr: .trailing, relatedBy: .equal, to: view, attr: .trailing) + Constraints.active(item: bottomView, attr: .bottom, relatedBy: .equal, to: view.safeAreaLayoutGuide, attr: .bottom) + Constraints.active(item: bottomView, attr: .leading, relatedBy: .equal, to: view, attr: .leading) + + // Rotate button + Constraints.active(item: rotateButton, attr: .leading, relatedBy: .equal, to: bottomView, attr: .leading, + constant: 15) + Constraints.active(item: rotateButton, attr: .width, relatedBy: .equal, to: nil, attr: .width, constant: 33) + Constraints.active(item: rotateButton, attr: .height, relatedBy: .equal, to: nil, attr: .height, constant: 33) + Constraints.active(item: rotateButton, attr: .centerY, relatedBy: .equal, to: bottomView, attr: .centerY) + Constraints.active(item: rotateButton, attr: .top, relatedBy: .equal, to: bottomView, attr: .top, constant: 10) + Constraints.active(item: rotateButton, attr: .bottom, relatedBy: .equal, to: bottomView, attr: .bottom, + constant: -10) + + // Bottom label + Constraints.active(item: bottomLabel, attr: .trailing, relatedBy: .equal, to: bottomView, attr: .trailing, + constant: -20) + Constraints.active(item: bottomLabel, attr: .leading, relatedBy: .equal, to: rotateButton, attr: .trailing, + constant: 30, priority: 999) + Constraints.active(item: bottomLabel, attr: .height, relatedBy: .equal, to: nil, attr: .height, constant: 33) + Constraints.active(item: bottomLabel, attr: .centerY, relatedBy: .equal, to: bottomView, attr: .centerY) } + } -// MARK: UICollectionViewDelegateFlowLayout - -extension ReviewViewController: UICollectionViewDelegateFlowLayout { - public func collectionView(_ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath) -> CGSize { - return cellSize - } - - public func collectionView(_ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetForSectionAt section: Int) -> UIEdgeInsets { - let margin = (self.view.bounds.width - cellSize.width) / 2 - return UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - } - - public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let page = pages[indexPath.row] - delegate?.review(self, didSelectPage: page) - } - - private func setCurrentPage(basedOn scrollView: UIScrollView) { - guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout - else { return } - let offset = scrollView.contentOffset - let cellWidthIncludingSpacing = cellSize.width + layout.minimumLineSpacing - let index = (offset.x + scrollView.contentInset.left) / cellWidthIncludingSpacing - currentPage = Int(round(index)) - self.pageControl.currentPage = currentPage - } - - private func setCellStatus(for index: Int, isActive: Bool) { - let indexToSet = min(index, pages.count - 1) - - let cell = collectionView.cellForItem(at: IndexPath(row: indexToSet, section: 0)) as? ReviewCollectionCell - cell?.isActive = isActive +extension ReviewViewController: UIScrollViewDelegate { + + /** + Asks the delegate for the view to scale when zooming is about to occur in the scroll view. + + - parameter scrollView: The scroll view object displaying the content view. + - returns: A `UIView` object that will be scaled as a result of the zooming gesture. + */ + public func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return imageView } -} - -// MARK: ReviewCollectionViewDelegate - -extension ReviewViewController: ReviewCollectionViewDelegate { - func didTapDelete(on cell: ReviewCollectionCell) { - guard let indexpath = collectionView.indexPath(for: cell) else { return } - deleteItem(at: indexpath) - - setCurrentPage(basedOn: collectionView) + + /** + Informs the delegate that the scroll view’s zoom factor has changed. + + - parameter scrollView: The scroll-view object whose zoom factor has changed. + */ + public func scrollViewDidZoom(_ scrollView: UIScrollView) { + DispatchQueue.main.async { + self.updateConstraintsForSize(scrollView.bounds.size) + } } + } -// MARK: Constants - -extension ReviewViewController { - private enum Constants { - static let padding: CGFloat = 16 - static let bottomPadding: CGFloat = 50 - static let pageControlBottomPadding: CGFloat = 130 - static let buttonSize: CGSize = CGSize(width: 126, height: 50) - static let titleHeight: CGFloat = 18 - static let maxTitleHeight: CGFloat = 100 - static let bottomNavigationBarHeight: CGFloat = 114 - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewZoomViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewZoomViewController.swift deleted file mode 100644 index 3034f22..0000000 --- a/Sources/GiniCaptureSDK/Core/Screens/Review/ReviewZoomViewController.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// ReviewZoomViewController.swift -// -// -// Created by David Vizaknai on 04.10.2022. -// - -import UIKit - -final class ReviewZoomViewController: UIViewController { - private lazy var scrollView = UIScrollView() - private lazy var imageView = UIImageView() - private lazy var closeButton: UIButton = { - let closeButton = UIButton(type: .custom) - closeButton.translatesAutoresizingMaskIntoConstraints = false - closeButton.setImage(UIImageNamedPreferred(named: "close_icon"), for: .normal) - closeButton.imageView?.contentMode = .scaleAspectFit - closeButton.addTarget(self, action: #selector(didTapCloseButton), for: .touchUpInside) - return closeButton - }() - private var page: GiniCapturePage - - // MARK: - Init - - init(page: GiniCapturePage) { - self.page = page - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - View lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - setupView() - setupLayout() - setupImage(page.document.previewImage) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - adjustContentSize() - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - scrollView.setZoomScale(1, animated: false) - } - - // MARK: - Setup View - - private func setupView() { - view.backgroundColor = UIColor.GiniCapture.dark1 - - scrollView.backgroundColor = view.backgroundColor - scrollView.minimumZoomScale = 1.0 - scrollView.maximumZoomScale = 2.0 - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.delegate = self - view.addSubview(scrollView) - - imageView.contentMode = .scaleAspectFit - scrollView.addSubview(imageView) - - view.addSubview(closeButton) - view.bringSubviewToFront(closeButton) - - let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(didRecognizeDoubleTap)) - doubleTapRecognizer.numberOfTapsRequired = 2 - scrollView.addGestureRecognizer(doubleTapRecognizer) - } - - private func setupLayout() { - scrollView.frame = view.bounds - scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - - imageView.frame = scrollView.bounds - - NSLayoutConstraint.activate([ - closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), - closeButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - closeButton.heightAnchor.constraint(equalToConstant: 44), - closeButton.widthAnchor.constraint(equalToConstant: 44) - ]) - } - - // MARK: - Setup content - - private func setupImage(_ image: UIImage?) { - imageView.image = image - } - - // MARK: - Callbacks - - @objc - private func didRecognizeDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) { - let shouldZoomOut = scrollView.zoomScale != 1.0 - if shouldZoomOut { - scrollView.setZoomScale(1.0, animated: true) - } else { - let tapPoint = gestureRecognizer.location(in: imageView) - let newZoomScale = scrollView.maximumZoomScale - let zoomedRectSize = CGSize(width: scrollView.bounds.size.width / newZoomScale, - height: scrollView.bounds.size.height / newZoomScale) - let zoomedRectOrigin = CGPoint(x: tapPoint.x - zoomedRectSize.width / 2, - y: tapPoint.y - zoomedRectSize.height / 2) - let rectToZoom = CGRect(origin: zoomedRectOrigin, size: zoomedRectSize) - scrollView.zoom(to: rectToZoom, animated: true) - } - } - - @objc - private func didTapCloseButton() { - dismiss(animated: true) - } - - // MARK: - Utilities - - private func adjustContentSize() { - guard let image = self.imageView.image else { - imageView.frame = scrollView.bounds - return - } - - let fitSize = scrollView.frame.size - let widthRatio = fitSize.width / image.size.width - let heightRatio = fitSize.height / image.size.height - let ratio = min(widthRatio, heightRatio) - let imageSize = CGSize(width: image.size.width * ratio, height: image.size.height * ratio) - imageView.frame = CGRect(origin: CGPoint.zero, size: imageSize) - scrollView.contentSize = imageSize - adjustImageToCenter() - } - - private func adjustImageToCenter() { - let scrollViewSize = scrollView.bounds.size - var contentFrame = imageView.frame - - let yOffset = max(0, (scrollViewSize.height - contentFrame.height) / 2) - contentFrame.origin.y = yOffset - - let xOffset = max(0, (scrollViewSize.width - contentFrame.width) / 2) - contentFrame.origin.x = xOffset - - imageView.frame = contentFrame - } -} - -// MARK: - UIScrollViewDelegate - -extension ReviewZoomViewController: UIScrollViewDelegate { - func viewForZooming(in scrollView: UIScrollView) -> UIView? { - imageView - } - - func scrollViewDidZoom(_ scrollView: UIScrollView) { - adjustImageToCenter() - } -} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift index ba7f1c5..708a6ef 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift @@ -12,17 +12,10 @@ import Foundation extension GiniScreenAPICoordinator { func createAnalysisScreen(withDocument document: GiniCaptureDocument) -> AnalysisViewController { let viewController = AnalysisViewController(document: document) - - let cancelButton = GiniBarButton(ofType: .cancel) - cancelButton.addAction(self, #selector(back)) - - if giniConfiguration.bottomNavigationBarEnabled { - viewController.navigationItem.setHidesBackButton(true, animated: false) - viewController.navigationItem.rightBarButtonItem = cancelButton.barButton - } else { - viewController.navigationItem.leftBarButtonItem = cancelButton.barButton - } - + viewController.setupNavigationItem(usingResources: self.cancelButtonResource, + selector: #selector(back), + position: .left, + target: self) return viewController } } @@ -30,136 +23,71 @@ extension GiniScreenAPICoordinator { // MARK: - ImageAnalysisNoResults screen extension GiniScreenAPICoordinator { - func createImageAnalysisNoResultsScreen( - type: NoResultScreenViewController.NoResultType - ) -> NoResultScreenViewController { - let viewModel: BottomButtonsViewModel - let viewController: NoResultScreenViewController - switch type { - case .image: - if pages.contains(where: { $0.document.isImported == false }) { - // if there is a photo captured with camera - viewModel = BottomButtonsViewModel( - retakeBlock: { [weak self] in - self?.pages = [] - self?.backToCamera() - }, - manuallyPressed: { [weak self] in - if let delegate = self?.visionDelegate { - delegate.didPressEnterManually() - } else { - self?.screenAPINavigationController.dismiss(animated: true) - } - }, cancelPressed: { [weak self] in - self?.closeScreenApi() - }) - } else { - viewModel = BottomButtonsViewModel( - manuallyPressed: { [weak self] in - if let delegate = self?.visionDelegate { - delegate.didPressEnterManually() - } else { - self?.screenAPINavigationController.dismiss(animated: true) - } - }, cancelPressed: { [weak self] in - self?.closeScreenApi() - }) + func createImageAnalysisNoResultsScreen() -> ImageAnalysisNoResultsViewController { + let imageAnalysisNoResultsViewController: ImageAnalysisNoResultsViewController + let isCameraViewControllerLoaded: Bool = { + guard let cameraViewController = cameraViewController else { + return false } - default: - viewModel = BottomButtonsViewModel( - manuallyPressed: { [weak self] in - self?.screenAPINavigationController.dismiss(animated: true) - }, cancelPressed: { [weak self] in - self?.closeScreenApi() - }) + return screenAPINavigationController.viewControllers.contains(cameraViewController) + }() + + if isCameraViewControllerLoaded { + imageAnalysisNoResultsViewController = ImageAnalysisNoResultsViewController() + imageAnalysisNoResultsViewController.setupNavigationItem(usingResources: backButtonResource, + selector: #selector(backToCamera), + position: .left, + target: self) + } else { + imageAnalysisNoResultsViewController = ImageAnalysisNoResultsViewController(bottomButtonText: nil, + bottomButtonIcon: nil) + imageAnalysisNoResultsViewController.setupNavigationItem(usingResources: closeButtonResource, + selector: #selector(closeScreenApi), + position: .left, + target: self) } - viewController = NoResultScreenViewController( - giniConfiguration: giniConfiguration, - type: type, - viewModel: viewModel) - - return viewController + + imageAnalysisNoResultsViewController.didTapBottomButton = { [weak self] in + self?.backToCamera() + } + + return imageAnalysisNoResultsViewController } } // MARK: - AnalysisDelegate extension GiniScreenAPICoordinator: AnalysisDelegate { - - public func displayError( - errorType: ErrorType, - animated: Bool - ) { - let viewModel: BottomButtonsViewModel - switch pages.type { - case .image: - if self.pages.contains(where: { $0.document.isImported == false }) { - // if there is a photo captured with camera - viewModel = BottomButtonsViewModel( - retakeBlock: { [weak self] in - self?.pages = [] - self?.backToCamera() - }, - manuallyPressed: { [weak self] in - if let delegate = self?.visionDelegate { - delegate.didPressEnterManually() - } else { - self?.screenAPINavigationController.dismiss(animated: animated) - } - }, cancelPressed: { [weak self] in - self?.closeScreenApi() + public func displayError(withMessage message: String?, andAction action: (() -> Void)?) { + DispatchQueue.main.async { [weak self] in + guard let self = self, + let message = message, + let action = action else { return } + + if let analysisViewController = self.analysisViewController { + analysisViewController.showError(with: message, action: { [weak self] in + guard let self = self else { return } + self.analysisErrorAndAction = nil + action() }) } else { - viewModel = BottomButtonsViewModel( - manuallyPressed: { [weak self] in - if let delegate = self?.visionDelegate { - delegate.didPressEnterManually() - } else { - self?.screenAPINavigationController.dismiss(animated: animated) - } - }, cancelPressed: { [weak self] in - self?.closeScreenApi() - }) + self.analysisErrorAndAction = (message, action) } - default: - viewModel = BottomButtonsViewModel( - manuallyPressed: { [weak self] in - self?.screenAPINavigationController.dismiss(animated: true) - }, cancelPressed: { [weak self] in - self?.closeScreenApi() - }) - } - - let viewController = ErrorScreenViewController( - giniConfiguration: giniConfiguration, - type: errorType, - documentType: pages.type ?? .pdf, - viewModel: viewModel) - screenAPINavigationController.pushViewController(viewController, animated: animated) - } - - public func tryDisplayNoResultsScreen() { - var shouldDisplay = false - var noResultType: NoResultScreenViewController.NoResultType? - switch pages.type { - case .image: - noResultType = .image - shouldDisplay = true - case .pdf: - noResultType = .pdf - shouldDisplay = true - default: - shouldDisplay = false } - - if shouldDisplay, let type = noResultType { - let noResultsScreen = self.createImageAnalysisNoResultsScreen(type: type) - DispatchQueue.main.async { - self.imageAnalysisNoResultsViewController = noResultsScreen - self.screenAPINavigationController.pushViewController( - noResultsScreen, animated: true) + } + + public func tryDisplayNoResultsScreen() -> Bool { + if pages.type == .image { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.imageAnalysisNoResultsViewController = self.createImageAnalysisNoResultsScreen() + self.screenAPINavigationController.pushViewController(self.imageAnalysisNoResultsViewController!, + animated: true) } + + return true } - } + return false + } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift index 25bf20e..63da043 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift @@ -6,7 +6,6 @@ // import UIKit -import GiniBankAPILibrary /** The UploadDelegate protocol defines methods that allow you to notify the _Gini Capture SDK_ when a document upload @@ -18,10 +17,9 @@ import GiniBankAPILibrary } extension GiniScreenAPICoordinator: CameraViewControllerDelegate { - - public func camera(_ viewController: CameraScreen, didCapture document: GiniCaptureDocument) { + public func camera(_ viewController: CameraViewController, didCapture document: GiniCaptureDocument) { let loadingView = viewController.addValidationLoadingView() - + validate([document]) { result in loadingView.removeFromSuperview() switch result { @@ -29,11 +27,14 @@ extension GiniScreenAPICoordinator: CameraViewControllerDelegate { let validatedPage = validatedPages[0] self.addToDocuments(new: [validatedPage]) self.didCaptureAndValidate(document) - if document.type == .qrcode { - // Skip the analysis screen and validate the QR code on the same screen - return + + // In case that there is more than one image already captured, an animation is shown instead of + // going to next screen + if let imageDocument = document as? GiniImageDocument, self.pages.count > 1 { + viewController.animateToControlsView(imageDocument: imageDocument) + } else { + self.showNextScreenAfterPicking(pages: [validatedPage]) } - self.showNextScreenAfterPicking(pages: [validatedPage]) case .failure(let error): var errorMessage = String(describing: error) @@ -41,16 +42,17 @@ extension GiniScreenAPICoordinator: CameraViewControllerDelegate { (error == .maxFilesPickedCountExceeded || error == .mixedDocumentsUnsupported) { errorMessage = error.message viewController.showErrorDialog(for: error) { - self.showReview() + self.showMultipageReview() } } + let errorLog = ErrorLog(description: errorMessage, error: error) self.giniConfiguration.errorLogger.handleErrorLog(error: errorLog) } } } - - public func camera(_ viewController: CameraScreen, didSelect documentPicker: DocumentPickerType) { + + public func camera(_ viewController: CameraViewController, didSelect documentPicker: DocumentPickerType) { switch documentPicker { case .gallery: documentPickerCoordinator.showGalleryPicker(from: viewController) @@ -59,86 +61,57 @@ extension GiniScreenAPICoordinator: CameraViewControllerDelegate { documentPickerCoordinator.showDocumentPicker(from: viewController) } } - - public func cameraDidAppear(_ viewController: CameraScreen) { + + public func cameraDidAppear(_ viewController: CameraViewController) { + if shouldShowOnBoarding() { - showOnboardingScreen(cameraViewController: viewController, completion: { + showOnboardingScreen { viewController.setupCamera() - }) + } } else { viewController.setupCamera() } } - - public func cameraDidTapReviewButton(_ viewController: CameraScreen) { - popBackToReview() - } - - private func createCameraButtonsViewModel() -> CameraButtonsViewModel { - let cameraButtonsViewModel = CameraButtonsViewModel( - trackingDelegate: trackingDelegate - ) - cameraButtonsViewModel.helpAction = { [weak self] in - self?.showHelpMenuScreen() - } - cameraButtonsViewModel.cancelAction = { [weak self] in - self?.closeScreenApi() - } - return cameraButtonsViewModel + + public func cameraDidTapMultipageReviewButton(_ viewController: CameraViewController) { + showMultipageReview() } - - func createCameraViewController() -> CameraScreen { - let cameraButtonsViewModel = createCameraButtonsViewModel() - - let cameraViewController = Camera2ViewController( - giniConfiguration: giniConfiguration, - viewModel: cameraButtonsViewModel - ) + + func createCameraViewController() -> CameraViewController { + let cameraViewController = CameraViewController(giniConfiguration: giniConfiguration) cameraViewController.delegate = self - documentPickerCoordinator.setupDragAndDrop(in: cameraViewController.view) + cameraViewController.trackingDelegate = trackingDelegate cameraViewController.title = .localized(resource: NavigationBarStrings.cameraTitle) - cameraButtonsViewModel.backButtonAction = { [weak cameraViewController, weak self] in - if let strongSelf = self, strongSelf.pages.count > 0 { - if let cameraViewController = cameraViewController { - self?.cameraDidTapReviewButton(cameraViewController) - } - } else { - self?.closeScreenApi() - } - } - - if !giniConfiguration.bottomNavigationBarEnabled { - if pages.count > 0 { - let buttonTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.analysis.backToReview", - comment: "Review") - let backButton = GiniBarButton(ofType: .back(title: buttonTitle)) - backButton.addAction(self, #selector(popBackToReview)) - cameraViewController.navigationItem.leftBarButtonItem = backButton.barButton - } else { - let cancelButton = GiniBarButton(ofType: .cancel) - cancelButton.addAction(self, #selector(back)) - cameraViewController.navigationItem.leftBarButtonItem = cancelButton.barButton - } - - let helpButton = GiniBarButton(ofType: .help) - helpButton.addAction(self, #selector(showHelpMenuScreen)) - cameraViewController.navigationItem.rightBarButtonItem = helpButton.barButton - } - + + cameraViewController.setupNavigationItem(usingResources: closeButtonResource, + selector: #selector(back), + position: .left, + target: self) + + cameraViewController.setupNavigationItem(usingResources: helpButtonResource, + selector: #selector(showHelpMenuScreen), + position: .right, + target: self) + if giniConfiguration.fileImportSupportedTypes != .none { documentPickerCoordinator.delegate = self + if documentPickerCoordinator.isGalleryPermissionGranted { documentPickerCoordinator.startCaching() } + + if #available(iOS 11.0, *) { + documentPickerCoordinator.setupDragAndDrop(in: cameraViewController.view) + } } - self.cameraScreen = cameraViewController + return cameraViewController } - + fileprivate func didCaptureAndValidate(_ document: GiniCaptureDocument) { visionDelegate?.didCapture(document: document, networkDelegate: self) } - + private func shouldShowOnBoarding() -> Bool { if giniConfiguration.onboardingShowAtFirstLaunch && !UserDefaults.standard.bool(forKey: "ginicapture.defaults.onboardingShowed") { @@ -147,25 +120,37 @@ extension GiniScreenAPICoordinator: CameraViewControllerDelegate { } else if giniConfiguration.onboardingShowAtLaunch { return true } - + return false } - - private func showOnboardingScreen( - cameraViewController: CameraScreen, - completion: @escaping () -> Void) { - cameraViewController.hideCaptureButton() - - let vc = OnboardingViewController() - cameraViewController.showCaptureButton() - - completion() - let navigationController = UINavigationController(rootViewController: vc) - if giniConfiguration.customNavigationController == nil { - navigationController.applyStyle(withConfiguration: giniConfiguration) + + private func showOnboardingScreen(completion: @escaping () -> Void) { + cameraViewController?.hideCameraOverlay() + cameraViewController?.hideCaptureButton() + cameraViewController?.hideFileImportTip() + cameraViewController?.hideQrCodeTip() + + let vc = OnboardingContainerViewController(trackingDelegate: trackingDelegate) { [weak self] in + + guard let cameraViewController = self?.cameraViewController else { return } + + cameraViewController.showCameraOverlay() + cameraViewController.showCaptureButton() + if let config = self?.giniConfiguration { + if config.fileImportSupportedTypes != GiniConfiguration.GiniCaptureImportFileTypes.none { + cameraViewController.showFileImportTip() + } else if config.qrCodeScanningEnabled { + cameraViewController.showQrCodeTip() + } + } + + completion() } + + let navigationController = UINavigationController(rootViewController: vc) + navigationController.applyStyle(withConfiguration: giniConfiguration) navigationController.modalPresentationStyle = .overCurrentContext - + // Since the onboarding appears on startup, it could be the case where there are two consecutive 'coverVertical' // modal transitions. When the Screen API is embedded in a UINavigationController, it still has that // transition but it's not used. @@ -174,43 +159,45 @@ extension GiniScreenAPICoordinator: CameraViewControllerDelegate { !(rootContainerViewController.parent is UINavigationController) { navigationController.modalTransitionStyle = .crossDissolve } - + screenAPINavigationController.present(navigationController, animated: true, completion: nil) } - + func showNextScreenAfterPicking(pages: [GiniCapturePage]) { let visionDocuments = pages.map { $0.document } - - // Creating an array of GiniImageDocuments and filtering it for 'isFromOtherApp' - if visionDocuments.compactMap({ $0 as? GiniImageDocument }).filter({ $0.isFromOtherApp }).isNotEmpty { - showAnalysisScreen() - } else { - if let documentsType = visionDocuments.type { - switch documentsType { - case .image: - showReview() - case .qrcode, .pdf: - showAnalysisScreen() + if let documentsType = visionDocuments.type { + switch documentsType { + case .image: + if let imageDocuments = visionDocuments as? [GiniImageDocument], + let lastDocument = imageDocuments.last { + if self.giniConfiguration.multipageEnabled { + showMultipageReview() + } else { + reviewViewController = createReviewScreen(withDocument: lastDocument) + screenAPINavigationController.pushViewController(reviewViewController!, + animated: true) + } } + case .qrcode, .pdf: + showAnalysisScreen() } } } + } // MARK: - DocumentPickerCoordinatorDelegate extension GiniScreenAPICoordinator: DocumentPickerCoordinatorDelegate { - - public func documentPicker( - _ coordinator: DocumentPickerCoordinator, - didPick documents: [GiniCaptureDocument]) { - + + public func documentPicker(_ coordinator: DocumentPickerCoordinator, + didPick documents: [GiniCaptureDocument]) { + self.validate(documents) { result in switch result { case .success(let validatedDocuments): coordinator.dismissCurrentPicker { self.addToDocuments(new: validatedDocuments) - errorOccurred = false validatedDocuments.forEach { validatedDocument in if validatedDocument.error == nil { self.didCaptureAndValidate(validatedDocument.document) @@ -220,43 +207,46 @@ extension GiniScreenAPICoordinator: DocumentPickerCoordinatorDelegate { } case .failure(let error): var positiveAction: (() -> Void)? - + if let error = error as? FilePickerError { switch error { - case .maxFilesPickedCountExceeded, .mixedDocumentsUnsupported, .multiplePdfsUnsupported: + case .maxFilesPickedCountExceeded, .mixedDocumentsUnsupported: if self.pages.isNotEmpty { positiveAction = { coordinator.dismissCurrentPicker { - self.showReview() + self.showMultipageReview() } } } + case .photoLibraryAccessDenied, .failedToOpenDocument: break } } + if coordinator.currentPickerDismissesAutomatically { - self.cameraScreen?.showErrorDialog(for: error, + self.cameraViewController?.showErrorDialog(for: error, positiveAction: positiveAction) } else { coordinator.currentPickerViewController?.showErrorDialog(for: error, positiveAction: positiveAction) } } + } } - + public func documentPicker(_ coordinator: DocumentPickerCoordinator, failedToPickDocumentsAt urls: [URL]) { let error = FilePickerError.failedToOpenDocument if coordinator.currentPickerDismissesAutomatically { - self.cameraScreen?.showErrorDialog(for: error, + self.cameraViewController?.showErrorDialog(for: error, positiveAction: nil) } else { coordinator.currentPickerViewController?.showErrorDialog(for: error, positiveAction: nil) } } - + fileprivate func addDropInteraction(forView view: UIView, with delegate: UIDropInteractionDelegate) { let dropInteraction = UIDropInteraction(delegate: delegate) view.addInteraction(dropInteraction) @@ -266,36 +256,31 @@ extension GiniScreenAPICoordinator: DocumentPickerCoordinatorDelegate { // MARK: - Validation extension GiniScreenAPICoordinator { - fileprivate func validate(_ documents: [GiniCaptureDocument], completion: @escaping (Result<[GiniCapturePage], Error>) -> Void) { - + guard !(documents + pages.map {$0.document}).containsDifferentTypes else { completion(.failure(FilePickerError.mixedDocumentsUnsupported)) return } - - guard (documents.filter({ $0.type == .pdf }) + - pages.map({ $0.document }).filter({ $0.type == .pdf })).count <= 1 else { - completion(.failure(FilePickerError.multiplePdfsUnsupported)) - return - } - + guard (documents.count + pages.count) <= GiniCaptureDocumentValidator.maxPagesCount else { completion(.failure(FilePickerError.maxFilesPickedCountExceeded)) return } + self.validate(importedDocuments: documents) { validatedDocuments in let elementsWithError = validatedDocuments.filter { $0.error != nil } if let firstElement = elementsWithError.first, - let error = firstElement.error { + let error = firstElement.error, + (!self.giniConfiguration.multipageEnabled || firstElement.document.type != .image) { completion(.failure(error)) } else { completion(.success(validatedDocuments)) } } } - + private func validate(importedDocuments documents: [GiniCaptureDocument], completion: @escaping ([GiniCapturePage]) -> Void) { DispatchQueue.global().async { @@ -310,7 +295,7 @@ extension GiniScreenAPICoordinator { } pages.append(GiniCapturePage(document: document, error: documentError)) } - + DispatchQueue.main.async { completion(pages) } @@ -327,18 +312,26 @@ extension GiniScreenAPICoordinator: UploadDelegate { self.update(document, withError: nil, isUploaded: true) } } - + public func uploadDidFail(for document: GiniCaptureDocument, with error: Error) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.update(document, withError: error, isUploaded: false) - - let errorLog = ErrorLog( - description: String(describing: error), - error: error) - self.giniConfiguration.errorLogger.handleErrorLog(error: errorLog) - guard let giniError = error as? GiniError, giniError != .requestCancelled else { return } - self.displayError(errorType: ErrorType(error: giniError), animated: true) + + if document.type != .image || !self.giniConfiguration.multipageEnabled { + var errorMessage = String(describing: error) + + if let error = error as? GiniCaptureError { + errorMessage = error.message + self.displayError(withMessage: error.message, andAction: { [weak self] in + guard let self = self else { return } + self.didCaptureAndValidate(document) + }) + } + + let errorLog = ErrorLog(description: errorMessage, error: error) + self.giniConfiguration.errorLogger.handleErrorLog(error: errorLog) + } } } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Review.swift b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Review.swift index ca53a46..334e68a 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Review.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Review.swift @@ -7,70 +7,105 @@ import UIKit -// MARK: - Review screen +// MARK: - Review Screen extension GiniScreenAPICoordinator: ReviewViewControllerDelegate { - public func review( - _ controller: ReviewViewController, - didDelete page: GiniCapturePage) { + + public func review(_ viewController: ReviewViewController, didReview document: GiniCaptureDocument) { + updateDocument(for: document) + } + + func createReviewScreen(withDocument document: GiniCaptureDocument, + isFirstScreen: Bool = false) -> ReviewViewController { + let reviewViewController = ReviewViewController(document: document, + giniConfiguration: giniConfiguration) + reviewViewController.delegate = self + reviewViewController.title = .localized(resource: NavigationBarStrings.reviewTitle) + reviewViewController.setupNavigationItem(usingResources: nextButtonResource, + selector: #selector(showAnalysisScreen), + position: .right, + target: self) + + let backResource = isFirstScreen ? closeButtonResource : backButtonResource + reviewViewController.setupNavigationItem(usingResources: backResource, + selector: #selector(back), + position: .left, + target: self) + + return reviewViewController + } +} + +// MARK: - Multipage Review screen + +extension GiniScreenAPICoordinator: MultipageReviewViewControllerDelegate { + public func multipageReview(_ controller: MultipageReviewViewController, + didRotate page: GiniCapturePage) { + updateDocument(for: page.document) + } + + public func multipageReview(_ controller: MultipageReviewViewController, + didDelete page: GiniCapturePage) { removeFromDocuments(document: page.document) visionDelegate?.didCancelReview(for: page.document) - + if pages.isEmpty { - backToCamera() + closeMultipageScreen() } } - - public func review( - _ viewController: ReviewViewController, - didTapRetryUploadFor page: GiniCapturePage) { + + public func multipageReview(_ controller: MultipageReviewViewController, + didReorder pages: [GiniCapturePage]) { + replaceDocuments(with: pages) + } + + public func multipageReview(_ viewController: MultipageReviewViewController, + didTapRetryUploadFor page: GiniCapturePage) { update(page.document, withError: nil, isUploaded: false) visionDelegate?.didCapture(document: page.document, networkDelegate: self) } - - public func reviewDidTapAddImage(_ controller: ReviewViewController) { - backToCamera() + + public func multipageReviewDidTapAddImage(_ controller: MultipageReviewViewController) { + closeMultipageScreen() } - func createReviewScreenContainer(with pages: [GiniCapturePage]) - -> ReviewViewController { - let vc = ReviewViewController(pages: pages, - giniConfiguration: giniConfiguration) + func createMultipageReviewScreenContainer(with pages: [GiniCapturePage]) + -> MultipageReviewViewController { + let vc = MultipageReviewViewController(pages: pages, + giniConfiguration: giniConfiguration) vc.delegate = self - - let cancelButton = GiniBarButton(ofType: .cancel) - cancelButton.addAction(self, #selector(closeScreen)) - - if giniConfiguration.bottomNavigationBarEnabled { - vc.navigationItem.rightBarButtonItem = cancelButton.barButton - } else { - vc.navigationItem.leftBarButtonItem = cancelButton.barButton - } + vc.setupNavigationItem(usingResources: backButtonResource, + selector: #selector(closeMultipageScreen), + position: .left, + target: self) + + vc.setupNavigationItem(usingResources: nextButtonResource, + selector: #selector(showAnalysisScreen), + position: .right, + target: self) + + vc.navigationItem.rightBarButtonItem?.isEnabled = false return vc } - - @objc fileprivate func closeScreen() { + + @objc fileprivate func closeMultipageScreen() { + trackingDelegate?.onReviewScreenEvent(event: Event(type: .back)) - screenAPINavigationController.dismiss(animated: true) - } - - public func reviewDidTapProcess(_ viewController: ReviewViewController) { - showAnalysisScreen() + + self.screenAPINavigationController.popViewController(animated: true) } - - @objc func popBackToReview() { - if let reviewVC = screenAPINavigationController.viewControllers.first as? ReviewViewController { - reviewVC.resetToEnd = true + + func showMultipageReview() { + if !screenAPINavigationController.viewControllers.contains(multiPageReviewViewController) { + screenAPINavigationController.pushViewController(multiPageReviewViewController, + animated: true) } - showReview() - } - - @objc func showReview() { - screenAPINavigationController.popToRootViewController(animated: true) } - - public func review(_ viewController: ReviewViewController, didSelectPage page: GiniCapturePage) { - let viewController = ReviewZoomViewController(page: page) - self.screenAPINavigationController.present(viewController, animated: true) + + func refreshMultipageReviewNextButton(with pages: [GiniCapturePage]) { + + multiPageReviewViewController.navigationItem + .rightBarButtonItem? + .isEnabled = pages.allSatisfy { $0.isUploaded } } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift index 0021406..39aa865 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift @@ -13,43 +13,53 @@ protocol Coordinator: AnyObject { } open class GiniScreenAPICoordinator: NSObject, Coordinator { - + var rootViewController: UIViewController { return screenAPINavigationController } - + public lazy var screenAPINavigationController: UINavigationController = { - var navigationController: UINavigationController - if let customNavigationController = giniConfiguration.customNavigationController { - navigationController = customNavigationController - } else { - navigationController = UINavigationController() - navigationController.applyStyle(withConfiguration: self.giniConfiguration) - } + let navigationController = UINavigationController() navigationController.delegate = self + navigationController.applyStyle(withConfiguration: self.giniConfiguration) return navigationController }() - + // Tracking public weak var trackingDelegate: GiniCaptureTrackingDelegate? - + // Screens var analysisViewController: AnalysisViewController? - weak var cameraScreen: CameraScreen? - var imageAnalysisNoResultsViewController: NoResultScreenViewController? - lazy var reviewViewController: ReviewViewController = { - return self.createReviewScreenContainer(with: []) + var cameraViewController: CameraViewController? + var imageAnalysisNoResultsViewController: ImageAnalysisNoResultsViewController? + var reviewViewController: ReviewViewController? + lazy var multiPageReviewViewController: MultipageReviewViewController = { + return self.createMultipageReviewScreenContainer(with: []) }() lazy var documentPickerCoordinator: DocumentPickerCoordinator = { return DocumentPickerCoordinator(giniConfiguration: giniConfiguration) }() - + // Properties public var giniConfiguration: GiniConfiguration public var pages: [GiniCapturePage] = [] public weak var visionDelegate: GiniCaptureDelegate? - + + // When there was an error uploading a document or analyzing it and the analysis screen + // had not been initialized yet, both the error message and action has to be saved to show in the analysis screen. + var analysisErrorAndAction: (message: String, action: () -> Void)? + // Resources + fileprivate(set) lazy var backButtonResource = + GiniPreferredButtonResource(image: "navigationReviewBack", + title: "ginicapture.navigationbar.review.back", + comment: "Button title in the navigation bar for the back button on the review screen", + configEntry: self.giniConfiguration.navigationBarReviewTitleBackButton) + fileprivate(set) lazy var backToCameraFromHelpMenuButtonResource = + GiniPreferredButtonResource(image: "navigationHelpBack", + title: "ginicapture.navigationbar.help.backToCamera", + comment: "Button title in the navigation bar for the back button on the help screen", + configEntry: self.giniConfiguration.navigationBarHelpMenuTitleBackToCameraButton) fileprivate(set) lazy var cancelButtonResource = giniConfiguration.cancelButtonResource ?? GiniPreferredButtonResource(image: "navigationAnalysisBack", @@ -59,48 +69,46 @@ open class GiniScreenAPICoordinator: NSObject, Coordinator { configEntry: self.giniConfiguration.navigationBarAnalysisTitleBackButton) fileprivate(set) lazy var closeButtonResource = giniConfiguration.closeButtonResource ?? - GiniPreferredButtonResource( - image: "navigationCameraClose", - title: "ginicapture.navigationbar.camera.close", - comment: "Button title in the navigation bar for the close button on the camera screen", - configEntry: giniConfiguration.navigationBarCameraTitleCloseButton) + GiniPreferredButtonResource(image: "navigationCameraClose", + title: "ginicapture.navigationbar.camera.close", + comment: "Button title in the navigation bar for the close button on the camera screen", + configEntry: self.giniConfiguration.navigationBarCameraTitleCloseButton) fileprivate(set) lazy var helpButtonResource = giniConfiguration.helpButtonResource ?? - GiniPreferredButtonResource( - image: "navigationCameraHelp", - title: "ginicapture.navigationbar.camera.help", - comment: "Button title in the navigation bar for the help button on the camera screen", - configEntry: giniConfiguration.navigationBarCameraTitleHelpButton) + GiniPreferredButtonResource(image: "navigationCameraHelp", + title: "ginicapture.navigationbar.camera.help", + comment: "Button title in the navigation bar for the help button on the camera screen", + configEntry: self.giniConfiguration.navigationBarCameraTitleHelpButton) fileprivate(set) lazy var nextButtonResource = giniConfiguration.nextButtonResource ?? - GiniPreferredButtonResource( - image: "navigationReviewContinue", - title: "ginicapture.navigationbar.review.continue", - comment: "Button title in the navigation bar for " + + GiniPreferredButtonResource(image: "navigationReviewContinue", + title: "ginicapture.navigationbar.review.continue", + comment: "Button title in the navigation bar for " + "the continue button on the review screen", - configEntry: giniConfiguration.navigationBarReviewTitleContinueButton) - + configEntry: self.giniConfiguration.navigationBarReviewTitleContinueButton) + fileprivate lazy var backToHelpMenuButtonResource = + GiniPreferredButtonResource(image: "arrowBack", + title: "ginicapture.navigationbar.help.backToMenu", + comment: "Button title in the navigation bar for the back button on the help screen", + configEntry: self.giniConfiguration.navigationBarHelpScreenTitleBackToMenuButton) + public init(withDelegate delegate: GiniCaptureDelegate?, giniConfiguration: GiniConfiguration) { self.visionDelegate = delegate self.giniConfiguration = giniConfiguration super.init() } - - public func start( - withDocuments documents: [GiniCaptureDocument]?, - animated: Bool = false - ) -> UIViewController { + + public func start(withDocuments documents: [GiniCaptureDocument]?) -> UIViewController { var viewControllers: [UIViewController] = [] if let documents = documents, !documents.isEmpty { - var errorMessage: String? - + var errorMessage: String? = nil + if documents.count > 1, !giniConfiguration.multipageEnabled { - errorMessage = "You are trying to import several files from" + - " other app when the Multipage feature is not " + - "enabled. To enable it just set `multipageEnabled`" + - " to `true` in the `GiniConfiguration`" + errorMessage = "You are trying to import several files from other app when the Multipage feature is not " + + "enabled. To enable it just set `multipageEnabled` to `true` in the `GiniConfiguration`" + } if !documents.containsDifferentTypes { @@ -117,35 +125,38 @@ open class GiniScreenAPICoordinator: NSObject, Coordinator { errorMessage = "You are trying to import both PDF and images at the same time. " + "For now it is only possible to import either images or one PDF" } - + if let errorMessage = errorMessage { let errorLog = ErrorLog(description: errorMessage) giniConfiguration.errorLogger.handleErrorLog(error: errorLog) fatalError(errorMessage) } } else { - let cameraViewController = createCameraViewController() - cameraScreen = cameraViewController - viewControllers = [reviewViewController, cameraViewController] + self.cameraViewController = self.createCameraViewController() + viewControllers = [self.cameraViewController!] } - self.screenAPINavigationController.setViewControllers(viewControllers, animated: animated) + self.screenAPINavigationController.setViewControllers(viewControllers, animated: false) return ContainerNavigationController(rootViewController: self.screenAPINavigationController, parent: self) } - + private func initialViewControllers(with pages: [GiniCapturePage]) -> [UIViewController] { - // Creating an array of GiniImageDocuments and filtering it for 'isFromOtherApp' - if pages.compactMap({ $0.document as? GiniImageDocument }).filter({ $0.isFromOtherApp }).isNotEmpty { - self.analysisViewController = createAnalysisScreen(withDocument: pages[0].document) - return [self.analysisViewController!] - } - if pages.type == .image { - reviewViewController = - createReviewScreenContainer(with: pages) - - return [reviewViewController] + if giniConfiguration.multipageEnabled { + self.cameraViewController = self.createCameraViewController() + self.cameraViewController? + .replaceCapturedStackImages(with: pages.compactMap { $0.document.previewImage }) + + self.multiPageReviewViewController = + createMultipageReviewScreenContainer(with: pages) + + return [self.cameraViewController!, self.multiPageReviewViewController] + } else { + self.cameraViewController = self.createCameraViewController() + self.reviewViewController = self.createReviewScreen(withDocument: pages[0].document) + return [self.cameraViewController!, self.reviewViewController!] + } } else { self.analysisViewController = createAnalysisScreen(withDocument: pages[0].document) return [self.analysisViewController!] @@ -158,37 +169,43 @@ open class GiniScreenAPICoordinator: NSObject, Coordinator { extension GiniScreenAPICoordinator { func addToDocuments(new pages: [GiniCapturePage]) { self.pages.append(contentsOf: pages) - - if pages.type == .image { - reviewViewController.updateCollections(with: self.pages, finishedUpload: false) + + if giniConfiguration.multipageEnabled, pages.type == .image { + refreshMultipageReviewNextButton(with: self.pages) + multiPageReviewViewController.updateCollections(with: self.pages) } } - + func removeFromDocuments(document: GiniCaptureDocument) { pages.remove(document) + + if giniConfiguration.multipageEnabled, pages.type == .image { + refreshMultipageReviewNextButton(with: pages) + } } - + func updateDocument(for document: GiniCaptureDocument) { if let index = pages.index(of: document) { pages[index].document = document } } - + func update(_ document: GiniCaptureDocument, withError error: Error?, isUploaded: Bool) { if let index = pages.index(of: document) { pages[index].isUploaded = isUploaded pages[index].error = error } - - if pages.type == .image { - reviewViewController.updateCollections(with: self.pages, finishedUpload: true) + + if giniConfiguration.multipageEnabled, pages.type == .image { + refreshMultipageReviewNextButton(with: pages) + multiPageReviewViewController.updateCollections(with: pages) } } - + func replaceDocuments(with pages: [GiniCapturePage]) { self.pages = pages } - + func clearDocuments() { pages.removeAll() } @@ -197,144 +214,149 @@ extension GiniScreenAPICoordinator { // MARK: - Button actions extension GiniScreenAPICoordinator { - + @objc func back() { + switch screenAPINavigationController.topViewController { - case is CameraScreen: + case is CameraViewController: trackingDelegate?.onCameraScreenEvent(event: Event(type: .exit)) - if pages.count > 0 { - if screenAPINavigationController.viewControllers.count > 1 { - screenAPINavigationController.popViewController(animated: true) - } else { - screenAPINavigationController.dismiss(animated: true) - } - } else { - closeScreenApi() - } case is AnalysisViewController: trackingDelegate?.onAnalysisScreenEvent(event: Event(type: .cancel)) - screenAPINavigationController.dismiss(animated: true) default: - if screenAPINavigationController.viewControllers.count > 1 { - screenAPINavigationController.popViewController(animated: true) - } else { - screenAPINavigationController.dismiss(animated: true) - } + break + } + + if self.screenAPINavigationController.viewControllers.count == 1 { + self.closeScreenApi() + } else { + self.screenAPINavigationController.popViewController(animated: true) } } - + @objc func closeScreenApi() { self.visionDelegate?.didCancelCapturing() } - + @objc func showHelpMenuScreen() { - let helpMenuViewController = HelpMenuViewController( - giniConfiguration: giniConfiguration - ) - helpMenuViewController.delegate = self + trackingDelegate?.onCameraScreenEvent(event: Event(type: .help)) - - let backButtonTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.help.backToCamera", - comment: "Camera") - let barButton = GiniBarButton(ofType: .back(title: backButtonTitle)) - barButton.addAction(self, #selector(back)) - helpMenuViewController.navigationItem.leftBarButtonItem = barButton.barButton - - // In case of 1 menu item it's better to show the item immediately without any selection - - if helpMenuViewController.dataSource.items.count == 1 { + + let helpMenuViewController = HelpMenuViewController(giniConfiguration: giniConfiguration) + helpMenuViewController.delegate = self + helpMenuViewController.setupNavigationItem(usingResources: backToCameraFromHelpMenuButtonResource, + selector: #selector(back), + position: .left, + target: self) + if helpMenuViewController.menuItems.count == 1 { screenAPINavigationController - .pushViewController(helpItemViewController(for: helpMenuViewController.dataSource.items[0]), + .pushViewController(helpItemViewController(for: helpMenuViewController.menuItems[0]), animated: true) } else { screenAPINavigationController .pushViewController(helpMenuViewController, animated: true) } } - + @objc func showAnalysisScreen() { - if screenAPINavigationController.topViewController is ReviewViewController { + + if screenAPINavigationController.topViewController is MultipageReviewViewController || + screenAPINavigationController.topViewController is ReviewViewController { trackingDelegate?.onReviewScreenEvent(event: Event(type: .next)) } - + guard let firstDocument = pages.first?.document else { return } - + if pages.type == .image { visionDelegate?.didReview(documents: pages.map { $0.document }, networkDelegate: self) } analysisViewController = createAnalysisScreen(withDocument: firstDocument) analysisViewController?.trackingDelegate = trackingDelegate + + if let (message, action) = analysisErrorAndAction { + displayError(withMessage: message, andAction: action) + } + self.screenAPINavigationController.pushViewController(analysisViewController!, animated: true) } - + @objc func backToCamera() { - _ = start(withDocuments: nil, animated: true) + if let cameraViewController = cameraViewController { + screenAPINavigationController.popToViewController(cameraViewController, animated: true) + } } } // MARK: - Navigation delegate extension GiniScreenAPICoordinator: UINavigationControllerDelegate { - public func navigationController( - _ navigationController: UINavigationController, - animationControllerFor operation: UINavigationController.Operation, - from fromVC: UIViewController, - to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { + public func navigationController(_ navigationController: UINavigationController, + animationControllerFor operation: UINavigationController.Operation, + from fromVC: UIViewController, + to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if fromVC is AnalysisViewController { analysisViewController = nil if operation == .pop { visionDelegate?.didCancelAnalysis() } } - - if toVC is CameraScreen && - (fromVC is AnalysisViewController || - fromVC is NoResultScreenViewController) { + + if fromVC is ReviewViewController && operation == .pop { + reviewViewController = nil + if let firstDocument = pages.first?.document { + visionDelegate?.didCancelReview(for: firstDocument) + } + } + + if toVC is CameraViewController && + (fromVC is ReviewViewController || + fromVC is AnalysisViewController || + fromVC is ImageAnalysisNoResultsViewController) { // When going directly from the analysis or from the single page review screen to the camera the pages // collection should be cleared, since the document processed in that cases is not going to be reused clearDocuments() } - - if fromVC is ReviewViewController, let cameraVC = toVC as? CameraScreen { + + if fromVC is MultipageReviewViewController, let cameraVC = toVC as? CameraViewController { cameraVC.replaceCapturedStackImages(with: pages.compactMap { $0.document.previewImage }) } - + return nil } - + } // MARK: - HelpMenuViewControllerDelegate extension GiniScreenAPICoordinator: HelpMenuViewControllerDelegate { - func help(_ menuViewController: HelpMenuViewController, didSelect item: HelpMenuItem) { + public func help(_ menuViewController: HelpMenuViewController, didSelect item: HelpMenuViewController.Item) { screenAPINavigationController.pushViewController(helpItemViewController(for: item), animated: true) } - - func helpItemViewController(for item: HelpMenuItem) -> UIViewController { + + func helpItemViewController(for item: HelpMenuViewController.Item) -> UIViewController { var viewController: UIViewController - switch item { case .noResultsTips: - let title: String = .localized(resource: ImageAnalysisNoResultsStrings.titleText) - viewController = HelpTipsViewController(giniConfiguration: giniConfiguration) - viewController.title = title - case .openWithTutorial: - viewController = HelpImportViewController(giniConfiguration: giniConfiguration) - case .supportedFormats: - viewController = HelpFormatsViewController(giniConfiguration: giniConfiguration) + let imageNoResultViewController = item.viewController as? ImageAnalysisNoResultsViewController + imageNoResultViewController?.didTapBottomButton = { [weak self] in + guard let self = self, let cameraViewController = self.cameraViewController else { return } + self.screenAPINavigationController.popToViewController(cameraViewController, animated: true) + } + + viewController = imageNoResultViewController! + case .openWithTutorial, .supportedFormats: + viewController = item.viewController case .custom(_, let customViewController): viewController = customViewController } - - let backButtonTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.help.backToMenu", - comment: "Help") - let barButton = GiniBarButton(ofType: .back(title: backButtonTitle)) - barButton.addAction(self, #selector(back)) - viewController.navigationItem.leftBarButtonItem = barButton.barButton + + viewController.setupNavigationItem(usingResources: backToHelpMenuButtonResource, + selector: #selector(back), + position: .left, + target: self) + return viewController } } diff --git a/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift b/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift index e1d4bbe..e2e2ad8 100644 --- a/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift +++ b/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift @@ -5,4 +5,4 @@ // Created by Nadya Karaban on 29.10.21. // -public let GiniCaptureSDKVersion = "3.0.0-beta07" +public let GiniCaptureSDKVersion = "1.11.1" diff --git a/Sources/GiniCaptureSDK/Networking/AnalysisResult.swift b/Sources/GiniCaptureSDK/Networking/AnalysisResult.swift index 79810a2..098f46d 100644 --- a/Sources/GiniCaptureSDK/Networking/AnalysisResult.swift +++ b/Sources/GiniCaptureSDK/Networking/AnalysisResult.swift @@ -21,7 +21,7 @@ import GiniBankAPILibrary * [here](https://pay-api.gini.net/documentation/#specific-extractions), * it can also return the epsPaymentQRCodeUrl extraction, obtained from a EPS QR code. */ - public let extractions: [String: Extraction] + public var extractions: [String: Extraction] /** * Line item compound extractions obtained in the analysis. diff --git a/Sources/GiniCaptureSDK/Networking/DocumentService.swift b/Sources/GiniCaptureSDK/Networking/DocumentService.swift index a63b3f7..e73c095 100644 --- a/Sources/GiniCaptureSDK/Networking/DocumentService.swift +++ b/Sources/GiniCaptureSDK/Networking/DocumentService.swift @@ -8,12 +8,6 @@ import UIKit import GiniBankAPILibrary -/** - Static veriable for synchronization to prevent display of multiple error screens at the same time -*/ - -var errorOccurred = false - public final class DocumentService: DocumentServiceProtocol { var partialDocuments: [String: PartialDocument] = [:] @@ -47,21 +41,11 @@ public final class DocumentService: DocumentServiceProtocol { completion?(.success(createdDocument)) case .failure(let error): - DispatchQueue.main.async { - guard errorOccurred == false else { - return - } - errorOccurred = true - DispatchQueue.global().async { - completion?(.failure(error)) - } - } + completion?(.failure(error)) } } } - - public func startAnalysis(completion: @escaping AnalysisCompletion) { let partialDocumentsInfoSorted = partialDocuments .lazy @@ -76,14 +60,8 @@ public final class DocumentService: DocumentServiceProtocol { self.document = createdDocument completion(.success(extractionResult)) case let .failure(error): - DispatchQueue.main.async { - guard errorOccurred == false else { - return - } - errorOccurred = true - DispatchQueue.global().async { - completion(.failure(error)) - } + if error != .requestCancelled { + completion(.failure(error)) } } } diff --git a/Sources/GiniCaptureSDK/Networking/Extensions/GiniCapture+GiniCaptureDelegate.swift b/Sources/GiniCaptureSDK/Networking/Extensions/GiniCapture+GiniCaptureDelegate.swift index 8da3b89..a979028 100644 --- a/Sources/GiniCaptureSDK/Networking/Extensions/GiniCapture+GiniCaptureDelegate.swift +++ b/Sources/GiniCaptureSDK/Networking/Extensions/GiniCapture+GiniCaptureDelegate.swift @@ -22,6 +22,8 @@ extension GiniCapture { - parameter userApi: The Gini user backend API to use. Supply .custom("domain") in order to specify a custom domain. - parameter trackingDelegate: A delegate object to receive user events + - note: Screen API only. + - returns: A presentable view controller. */ public class func viewController(withClient client: Client, diff --git a/Sources/GiniCaptureSDK/Networking/GiniNetworkingScreenAPICoordinator.swift b/Sources/GiniCaptureSDK/Networking/GiniNetworkingScreenAPICoordinator.swift index e1a6ba5..58727a1 100644 --- a/Sources/GiniCaptureSDK/Networking/GiniNetworkingScreenAPICoordinator.swift +++ b/Sources/GiniCaptureSDK/Networking/GiniNetworkingScreenAPICoordinator.swift @@ -17,24 +17,29 @@ import GiniBankAPILibrary Called when the analysis finished with results - parameter result: Contains the analysis result + - parameter sendFeedbackBlock: Block used to send feeback once the results have been corrected */ - func giniCaptureAnalysisDidFinishWith(result: AnalysisResult) - + func giniCaptureAnalysisDidFinishWith(result: AnalysisResult, + sendFeedbackBlock: @escaping ([String: Extraction]) -> Void) + /** - Called when the analysis was cancelled. + Called when the analysis finished without results. + + - parameter showingNoResultsScreen: Indicated if the `ImageAnalysisNoResultsViewController` has been shown */ - func giniCaptureDidCancelAnalysis() - + func giniCaptureAnalysisDidFinishWithoutResults(_ showingNoResultsScreen: Bool) + /** - Called when the 'Enter Manually' was pressed within No Result/ Error screen + Called when the analysis was cancelled. */ - func giniCaptureDidEnterManually() + func giniCaptureDidCancelAnalysis() } public class GiniNetworkingScreenAPICoordinator: GiniScreenAPICoordinator { + public weak var resultsDelegate: GiniCaptureResultsDelegate? public let documentService: DocumentServiceProtocol - + public init(client: Client, resultsDelegate: GiniCaptureResultsDelegate, giniConfiguration: GiniConfiguration, @@ -49,8 +54,7 @@ import GiniBankAPILibrary for: api) super.init(withDelegate: nil, giniConfiguration: giniConfiguration) - - self.giniConfiguration.documentService = documentService + self.visionDelegate = self self.resultsDelegate = resultsDelegate self.trackingDelegate = trackingDelegate @@ -66,8 +70,7 @@ import GiniBankAPILibrary super.init(withDelegate: nil, giniConfiguration: giniConfiguration) - - self.giniConfiguration.documentService = documentService + self.visionDelegate = self self.resultsDelegate = resultsDelegate self.trackingDelegate = trackingDelegate @@ -119,10 +122,16 @@ import GiniBankAPILibrary let result = AnalysisResult(extractions: extractions, lineItems: result.lineItems, images: images, document: document, candidates: result.candidates) - - self.resultsDelegate?.giniCaptureAnalysisDidFinishWith(result: result) + + let documentService = self.documentService + + self.resultsDelegate?.giniCaptureAnalysisDidFinishWith(result: result) { updatedExtractions in + documentService.sendFeedback(with: updatedExtractions.map { $0.value }, updatedCompoundExtractions: nil) + documentService.resetToInitialState() + } } else { - analysisDelegate.tryDisplayNoResultsScreen() + self.resultsDelegate? + .giniCaptureAnalysisDidFinishWithoutResults(analysisDelegate.tryDisplayNoResultsScreen()) self.documentService.resetToInitialState() } } @@ -138,8 +147,13 @@ extension GiniNetworkingScreenAPICoordinator { case .success(let extractions): self.deliver(result: extractions, and: self.documentService.document, to: networkDelegate) case .failure(let error): + guard error != .requestCancelled else { return } - networkDelegate.displayError(errorType: ErrorType(error: error), animated: true) + + networkDelegate.displayError(withMessage: .localized(resource: AnalysisStrings.analysisErrorMessage), + andAction: { + self.startAnalysis(networkDelegate: networkDelegate) + }) } } } @@ -157,15 +171,20 @@ extension GiniNetworkingScreenAPICoordinator { } } - fileprivate func uploadAndStartAnalysis( - document: GiniCaptureDocument, - networkDelegate: GiniCaptureNetworkDelegate, - uploadDidFail: @escaping () -> Void) { + fileprivate func uploadAndStartAnalysis(document: GiniCaptureDocument, + networkDelegate: GiniCaptureNetworkDelegate, + uploadDidFail: @escaping () -> Void) { self.upload(document: document, didComplete: { _ in self.startAnalysis(networkDelegate: networkDelegate) }, didFail: { _, error in - guard let giniError = error as? GiniError, giniError != .requestCancelled else { return } - networkDelegate.displayError(errorType: ErrorType(error: giniError), animated: true) + let error = error as? GiniCaptureError ?? AnalysisError.documentCreation + + guard let analysisError = error as? AnalysisError, case analysisError = AnalysisError.cancelled else { + networkDelegate.displayError(withMessage: error.message, andAction: { + uploadDidFail() + }) + return + } }) } } @@ -173,11 +192,6 @@ extension GiniNetworkingScreenAPICoordinator { // MARK: - GiniCaptureDelegate extension GiniNetworkingScreenAPICoordinator: GiniCaptureDelegate { - - public func didPressEnterManually() { - resultsDelegate?.giniCaptureDidEnterManually() - } - public func didCancelCapturing() { resultsDelegate?.giniCaptureDidCancelAnalysis() } @@ -252,5 +266,4 @@ extension GiniNetworkingScreenAPICoordinator: GiniCaptureDelegate { documentService.resetToInitialState() } } - } diff --git a/Sources/GiniCaptureSDK/Resources/AlbumsHeaderView.xib b/Sources/GiniCaptureSDK/Resources/AlbumsHeaderView.xib index 9d7754d..1ebea95 100644 --- a/Sources/GiniCaptureSDK/Resources/AlbumsHeaderView.xib +++ b/Sources/GiniCaptureSDK/Resources/AlbumsHeaderView.xib @@ -1,9 +1,9 @@ - + - + @@ -16,7 +16,7 @@ - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/Camera/CameraBottomNavigationBar.xib b/Sources/GiniCaptureSDK/Resources/Camera/CameraBottomNavigationBar.xib deleted file mode 100644 index 3248808..0000000 --- a/Sources/GiniCaptureSDK/Resources/Camera/CameraBottomNavigationBar.xib +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/Camera/CameraPhone.xib b/Sources/GiniCaptureSDK/Resources/Camera/CameraPhone.xib deleted file mode 100644 index c5c829d..0000000 --- a/Sources/GiniCaptureSDK/Resources/Camera/CameraPhone.xib +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/Camera/CameraiPad.xib b/Sources/GiniCaptureSDK/Resources/Camera/CameraiPad.xib deleted file mode 100644 index 6c69727..0000000 --- a/Sources/GiniCaptureSDK/Resources/Camera/CameraiPad.xib +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/CaptureSuggestionsViewContainer.xib b/Sources/GiniCaptureSDK/Resources/CaptureSuggestionsViewContainer.xib deleted file mode 100644 index ab400f2..0000000 --- a/Sources/GiniCaptureSDK/Resources/CaptureSuggestionsViewContainer.xib +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/Error/Empty.xib b/Sources/GiniCaptureSDK/Resources/Error/Empty.xib deleted file mode 100644 index a6037d0..0000000 --- a/Sources/GiniCaptureSDK/Resources/Error/Empty.xib +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.swift b/Sources/GiniCaptureSDK/Resources/GiniColors.swift deleted file mode 100644 index b43bdd8..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// UIColor+Gini.swift -// -// -// Created by David Vizaknai on 24.08.2022. -// - -import UIKit - -extension UIColor { - struct GiniCapture { - static let accent1 = UIColorPreferred(named: "Accent01") - static let accent2 = UIColorPreferred(named: "Accent02") - static let accent3 = UIColorPreferred(named: "Accent03") - static let accent4 = UIColorPreferred(named: "Accent04") - static let accent5 = UIColorPreferred(named: "Accent05") - - static let dark1 = UIColorPreferred(named: "Dark01") - static let dark2 = UIColorPreferred(named: "Dark02") - static let dark3 = UIColorPreferred(named: "Dark03") - static let dark4 = UIColorPreferred(named: "Dark04") - static let dark5 = UIColorPreferred(named: "Dark05") - static let dark6 = UIColorPreferred(named: "Dark06") - static let dark7 = UIColorPreferred(named: "Dark07") - - static let error1 = UIColorPreferred(named: "Error01") - static let error2 = UIColorPreferred(named: "Error02") - static let error3 = UIColorPreferred(named: "Error03") - static let error4 = UIColorPreferred(named: "Error04") - static let error5 = UIColorPreferred(named: "Error05") - - static let light1 = UIColorPreferred(named: "Light01") - static let light2 = UIColorPreferred(named: "Light02") - static let light3 = UIColorPreferred(named: "Light03") - static let light4 = UIColorPreferred(named: "Light04") - static let light5 = UIColorPreferred(named: "Light05") - static let light6 = UIColorPreferred(named: "Light06") - - static let success1 = UIColorPreferred(named: "Success01") - static let success2 = UIColorPreferred(named: "Success02") - static let success3 = UIColorPreferred(named: "Success03") - static let success4 = UIColorPreferred(named: "Success04") - static let success5 = UIColorPreferred(named: "Success05") - - static let warning1 = UIColorPreferred(named: "Warning01") - static let warning2 = UIColorPreferred(named: "Warning02") - static let warning3 = UIColorPreferred(named: "Warning03") - static let warning4 = UIColorPreferred(named: "Warning04") - static let warning5 = UIColorPreferred(named: "Warning05") - - // GiniColors OLD - static let systemWhite = UIColorPreferred(named: "systemWhite") - static let label = UIColorPreferred(named: "label") - static let labelWhite = UIColorPreferred(named: "labelWhite") - static let grayLabel = UIColorPreferred(named: "grayLabel") - static let errorBackground = UIColorPreferred(named: "errorBackground") - static let helpBackground = UIColorPreferred(named: "helpBackground") - static let separator = UIColorPreferred(named: "separator") - static let subheadline = UIColorPreferred(named: "subheadline") - static let systemBlue = UIColorPreferred(named: "systemBlue") - static let systemGray = UIColorPreferred(named: "systemGray") - static let systemGray02 = UIColorPreferred(named: "systemGray02") - static let systemGray03 = UIColorPreferred(named: "systemGray03") - static let systemGray04 = UIColorPreferred(named: "systemGray04") - static let systemGray05 = UIColorPreferred(named: "systemGray05") - static let systemGray06 = UIColorPreferred(named: "systemGray06") - static let systemGreen = UIColorPreferred(named: "systemGreen") - static let systemIndigo = UIColorPreferred(named: "systemIndigo") - static let systemOrange = UIColorPreferred(named: "systemOrange") - static let systemPink = UIColorPreferred(named: "systemPink") - static let systemPurple = UIColorPreferred(named: "systemPurple") - static let systemRed = UIColorPreferred(named: "systemRed") - static let systemTeal = UIColorPreferred(named: "systemTeal") - static let systemYellow = UIColorPreferred(named: "systemYellow") - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent01.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent01.colorset/Contents.json deleted file mode 100644 index a7bb3ef..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent01.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.518", - "red" : "0.039" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent02.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent02.colorset/Contents.json deleted file mode 100644 index 39b6137..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent02.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.992", - "green" : "0.576", - "red" : "0.192" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent03.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent03.colorset/Contents.json deleted file mode 100644 index de9fbf0..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent03.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.984", - "green" : "0.675", - "red" : "0.384" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent04.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent04.colorset/Contents.json deleted file mode 100644 index daaef64..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent04.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.769", - "red" : "0.576" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent05.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent05.colorset/Contents.json deleted file mode 100644 index fdd6cd9..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Accent05.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.969", - "green" : "0.867", - "red" : "0.769" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark01.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark01.colorset/Contents.json deleted file mode 100644 index 8ec08b9..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark01.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.000", - "red" : "0.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark02.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark02.colorset/Contents.json deleted file mode 100644 index 407ab38..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark02.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.082", - "green" : "0.075", - "red" : "0.075" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark03.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark03.colorset/Contents.json deleted file mode 100644 index b2a007d..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark03.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.118", - "green" : "0.110", - "red" : "0.110" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark04.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark04.colorset/Contents.json deleted file mode 100644 index 5776044..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark04.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.235", - "green" : "0.227", - "red" : "0.227" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark05.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark05.colorset/Contents.json deleted file mode 100644 index 70b13b0..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark05.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.290", - "green" : "0.282", - "red" : "0.282" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark06.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark06.colorset/Contents.json deleted file mode 100644 index 5fa4eb2..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark06.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.400", - "green" : "0.388", - "red" : "0.388" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark07.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark07.colorset/Contents.json deleted file mode 100644 index 4b7689a..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Dark07.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.576", - "green" : "0.557", - "red" : "0.557" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error01.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error01.colorset/Contents.json deleted file mode 100644 index 3a63bd5..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error01.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.102", - "green" : "0.102", - "red" : "0.133" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error02.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error02.colorset/Contents.json deleted file mode 100644 index e96b22e..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error02.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.107", - "green" : "0.153", - "red" : "0.613" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error03.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error03.colorset/Contents.json deleted file mode 100644 index 6934603..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error03.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.188", - "green" : "0.231", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error04.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error04.colorset/Contents.json deleted file mode 100644 index aca7e5d..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error04.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.817", - "green" : "0.817", - "red" : "0.925" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error05.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error05.colorset/Contents.json deleted file mode 100644 index ef5470d..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Error05.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.871", - "green" : "0.871", - "red" : "0.933" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light01.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light01.colorset/Contents.json deleted file mode 100644 index 97650a1..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light01.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light02.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light02.colorset/Contents.json deleted file mode 100644 index 7302fb4..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light02.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.969", - "green" : "0.949", - "red" : "0.949" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light03.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light03.colorset/Contents.json deleted file mode 100644 index c077fe7..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light03.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.918", - "green" : "0.898", - "red" : "0.898" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light04.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light04.colorset/Contents.json deleted file mode 100644 index fe749d6..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light04.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.839", - "green" : "0.820", - "red" : "0.820" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light05.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light05.colorset/Contents.json deleted file mode 100644 index 79ec23d..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light05.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.800", - "green" : "0.780", - "red" : "0.780" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light06.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light06.colorset/Contents.json deleted file mode 100644 index 2423bdf..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Light06.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.698", - "green" : "0.682", - "red" : "0.682" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success01.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success01.colorset/Contents.json deleted file mode 100644 index 4b476d4..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success01.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.107", - "green" : "0.133", - "red" : "0.102" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success02.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success02.colorset/Contents.json deleted file mode 100644 index d1cd4ba..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success02.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.099", - "green" : "0.514", - "red" : "0.026" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success03.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success03.colorset/Contents.json deleted file mode 100644 index 861cb3a..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success03.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.349", - "green" : "0.780", - "red" : "0.204" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success04.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success04.colorset/Contents.json deleted file mode 100644 index 797a572..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success04.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.833", - "green" : "0.925", - "red" : "0.817" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success05.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success05.colorset/Contents.json deleted file mode 100644 index 0b208f8..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Success05.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.882", - "green" : "0.933", - "red" : "0.871" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning01.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning01.colorset/Contents.json deleted file mode 100644 index e0d81c6..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning01.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.102", - "green" : "0.124", - "red" : "0.133" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning02.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning02.colorset/Contents.json deleted file mode 100644 index b4ced6d..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning02.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.041", - "green" : "0.486", - "red" : "0.659" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning03.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning03.colorset/Contents.json deleted file mode 100644 index f287ce1..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning03.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.800", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning04.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning04.colorset/Contents.json deleted file mode 100644 index 7da4668..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning04.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.817", - "green" : "0.894", - "red" : "0.925" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning05.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning05.colorset/Contents.json deleted file mode 100644 index f4e8819..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/Warning05.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.871", - "green" : "0.914", - "red" : "0.933" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/errorBackground.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/errorBackground.colorset/Contents.json deleted file mode 100644 index ff66020..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/errorBackground.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.914", - "green" : "0.914", - "red" : "0.965" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.150", - "blue" : "0.227", - "green" : "0.271", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/grayLabel.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/grayLabel.colorset/Contents.json deleted file mode 100644 index 522d36c..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/grayLabel.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.400", - "green" : "0.388", - "red" : "0.388" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/labelWhite.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/labelWhite.colorset/Contents.json deleted file mode 100644 index 22c4bb0..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColors.xcassets/labelWhite.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemGreen.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemGreen.colorset/Contents.json deleted file mode 100644 index beb495c..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemGreen.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.400", - "green" : "0.769", - "red" : "0.396" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.372", - "green" : "0.832", - "red" : "0.419" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemOrange.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemOrange.colorset/Contents.json deleted file mode 100644 index ac5d2eb..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemOrange.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.604", - "red" : "0.942" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.235", - "green" : "0.640", - "red" : "0.946" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemRed.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemRed.colorset/Contents.json deleted file mode 100644 index 1798415..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemRed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.239", - "green" : "0.304", - "red" : "0.919" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.271", - "green" : "0.331", - "red" : "0.923" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemTeal.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemTeal.colorset/Contents.json deleted file mode 100644 index aa402af..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemTeal.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.961", - "green" : "0.773", - "red" : "0.468" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.980", - "green" : "0.812", - "red" : "0.507" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemYellow.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemYellow.colorset/Contents.json deleted file mode 100644 index 30e0cd3..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/SystemYellow.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.274", - "green" : "0.806", - "red" : "0.966" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.290", - "green" : "0.845", - "red" : "0.974" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/helpBackground.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/helpBackground.colorset/Contents.json deleted file mode 100644 index 01bb123..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/helpBackground.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.969", - "green" : "0.949", - "red" : "0.949" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.082", - "green" : "0.075", - "red" : "0.075" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/label.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/label.colorset/Contents.json deleted file mode 100644 index d890719..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/label.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.000", - "red" : "0.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/separator.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/separator.colorset/Contents.json deleted file mode 100644 index bd0a4f1..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/separator.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.918", - "green" : "0.898", - "red" : "0.898" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.227", - "green" : "0.220", - "red" : "0.220" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/subheadline.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/subheadline.colorset/Contents.json deleted file mode 100644 index 7fec402..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/subheadline.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.576", - "green" : "0.557", - "red" : "0.557" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.576", - "green" : "0.557", - "red" : "0.557" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemBlue.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemBlue.colorset/Contents.json deleted file mode 100644 index 2bec941..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemBlue.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.965", - "green" : "0.472", - "red" : "0.204" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.965", - "green" : "0.509", - "red" : "0.232" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray.colorset/Contents.json deleted file mode 100644 index c662c51..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.698", - "green" : "0.682", - "red" : "0.682" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.573", - "green" : "0.557", - "red" : "0.557" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray02.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray02.colorset/Contents.json deleted file mode 100644 index a27a81f..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray02.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.698", - "green" : "0.682", - "red" : "0.682" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.400", - "green" : "0.388", - "red" : "0.388" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray03.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray03.colorset/Contents.json deleted file mode 100644 index ab3e747..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray03.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.796", - "green" : "0.780", - "red" : "0.780" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.290", - "green" : "0.282", - "red" : "0.282" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray04.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray04.colorset/Contents.json deleted file mode 100644 index dab8c6a..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray04.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.839", - "green" : "0.819", - "red" : "0.820" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.235", - "green" : "0.227", - "red" : "0.228" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray05.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray05.colorset/Contents.json deleted file mode 100644 index 939bbce..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray05.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.914", - "green" : "0.898", - "red" : "0.898" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.118", - "green" : "0.110", - "red" : "0.110" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray06.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray06.colorset/Contents.json deleted file mode 100644 index 6cb5783..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemGray06.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.965", - "green" : "0.949", - "red" : "0.949" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.118", - "green" : "0.110", - "red" : "0.110" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemIndigo.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemIndigo.colorset/Contents.json deleted file mode 100644 index 9315cd3..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemIndigo.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.808", - "green" : "0.338", - "red" : "0.343" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.871", - "green" : "0.360", - "red" : "0.367" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemPink.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemPink.colorset/Contents.json deleted file mode 100644 index 6c5d9b8..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemPink.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.353", - "green" : "0.270", - "red" : "0.919" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.353", - "green" : "0.270", - "red" : "0.919" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemPurple.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemPurple.colorset/Contents.json deleted file mode 100644 index b5c5744..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemPurple.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.843", - "green" : "0.341", - "red" : "0.641" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.918", - "green" : "0.375", - "red" : "0.700" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemWhite.colorset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemWhite.colorset/Contents.json deleted file mode 100644 index 730b270..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniColorsOld.xcassets/systemWhite.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.110", - "green" : "0.110", - "red" : "0.118" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/CameraDefaultReturnAssistantDocument.imageset/CameraDefaultReturnAssistantDocument.jpg b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/CameraDefaultReturnAssistantDocument.imageset/CameraDefaultReturnAssistantDocument.jpg deleted file mode 100644 index ce0b224..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/CameraDefaultReturnAssistantDocument.imageset/CameraDefaultReturnAssistantDocument.jpg and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/Contents.json deleted file mode 100644 index 4bc3550..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "alert-triangle.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "alert-triangle-1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/alert-triangle-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/alert-triangle-1.pdf deleted file mode 100644 index 5c62b1c..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/alert-triangle-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/alert-triangle.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/alert-triangle.pdf deleted file mode 100644 index f8ba560..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/alertTriangle.imageset/alert-triangle.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/arrowBack.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/arrowBack.imageset/Contents.json index 301c94b..6f71a59 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/arrowBack.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/arrowBack.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "filename" : "chevron.left.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ic_arrow_back.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "ic_arrow_back.pdf" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/arrowBack.imageset/chevron.left.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/arrowBack.imageset/chevron.left.pdf deleted file mode 100644 index 3f90b04..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/arrowBack.imageset/chevron.left.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_back.imageset/chevron.left.png b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_back.imageset/chevron.left.png deleted file mode 100644 index a969a5a..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_back.imageset/chevron.left.png and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_cancel.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_cancel.imageset/Contents.json deleted file mode 100644 index a19a549..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_cancel.imageset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_done.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_done.imageset/Contents.json deleted file mode 100644 index a19a549..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_done.imageset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_help.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_help.imageset/Contents.json deleted file mode 100644 index a19a549..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_help.imageset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Camera-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Camera-1.pdf deleted file mode 100644 index a40ca01..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Camera-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Camera.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Camera.pdf deleted file mode 100644 index dd299f3..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Camera.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Contents.json index b5b11ee..332e76c 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "filename" : "Camera-1.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Camera.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "snap_trigger_icon.pdf" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/snap_trigger_icon.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/snap_trigger_icon.pdf new file mode 100644 index 0000000..2275522 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraCaptureButton.imageset/snap_trigger_icon.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/Contents.json index 52cdeb1..1f83be2 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/Contents.json @@ -1,12 +1,22 @@ { "images" : [ { - "filename" : "test.jpg", - "idiom" : "universal" + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "testDocument.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "testDocument-1.jpg", + "scale" : "3x" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/test.jpg b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/test.jpg deleted file mode 100644 index 38d309a..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/test.jpg and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/testDocument-1.jpg b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/testDocument-1.jpg new file mode 100644 index 0000000..2f523ab Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/testDocument-1.jpg differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/testDocument.jpg b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/testDocument.jpg new file mode 100644 index 0000000..2f523ab Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraDefaultDocumentImage.imageset/testDocument.jpg differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraFocus.imageset/cameraFocus.png b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraFocus.imageset/cameraFocus.png deleted file mode 100644 index 1519f3f..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraFocus.imageset/cameraFocus.png and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/Contents.json index 137c50a..af3f8b8 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/Contents.json @@ -1,12 +1,23 @@ { "images" : [ { - "filename" : "cameraNotAuthorizedIcon.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "allow-camera.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "allow-camera@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "allow-camera@3x.png", + "scale" : "3x" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera.png b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera.png new file mode 100644 index 0000000..b8cf420 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera.png differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera@2x.png b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera@2x.png new file mode 100644 index 0000000..c974abe Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera@2x.png differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera@3x.png b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera@3x.png new file mode 100644 index 0000000..5b2d976 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/allow-camera@3x.png differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/cameraNotAuthorizedIcon.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/cameraNotAuthorizedIcon.pdf deleted file mode 100644 index 92e5f80..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraNotAuthorizedIcon.imageset/cameraNotAuthorizedIcon.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Contents.json index 572e85a..eeb3fcf 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Contents.json @@ -1,22 +1,22 @@ { "images" : [ { - "filename" : "Light.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "mobile_onboarding_screen_4_illustration.pdf" }, { + "idiom" : "universal", + "filename" : "mobile_onboarding_screen_4_illustration-dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } - ], - "filename" : "Light-1.pdf", - "idiom" : "universal" + ] } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Light-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Light-1.pdf deleted file mode 100644 index 981b2d4..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Light-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Light.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Light.pdf deleted file mode 100644 index e274535..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/Light.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/mobile_onboarding_screen_4_illustration-dark.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/mobile_onboarding_screen_4_illustration-dark.pdf new file mode 100644 index 0000000..4e8c5ee Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/mobile_onboarding_screen_4_illustration-dark.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/mobile_onboarding_screen_4_illustration.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/mobile_onboarding_screen_4_illustration.pdf new file mode 100644 index 0000000..c8a3fdd Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion1.imageset/mobile_onboarding_screen_4_illustration.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Contents.json index 8dc250d..dfc44ac 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Contents.json @@ -1,22 +1,22 @@ { "images" : [ { - "filename" : "Flatten.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "mobile_onboarding_screen_5_illustration_black.pdf" }, { + "idiom" : "universal", + "filename" : "mobile_onboarding_screen_5_illustration_black-dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } - ], - "filename" : "Flatten-1.pdf", - "idiom" : "universal" + ] } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Flatten-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Flatten-1.pdf deleted file mode 100644 index f54a4fb..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Flatten-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Flatten.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Flatten.pdf deleted file mode 100644 index 2cb1afb..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/Flatten.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/mobile_onboarding_screen_5_illustration_black-dark.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/mobile_onboarding_screen_5_illustration_black-dark.pdf new file mode 100644 index 0000000..9536162 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/mobile_onboarding_screen_5_illustration_black-dark.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/mobile_onboarding_screen_5_illustration_black.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/mobile_onboarding_screen_5_illustration_black.pdf new file mode 100644 index 0000000..8920446 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion2.imageset/mobile_onboarding_screen_5_illustration_black.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Contents.json index e1f456d..353a55e 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Contents.json @@ -1,22 +1,22 @@ { "images" : [ { - "filename" : "Frame.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "mobile_onboarding_screen_3_illustration_black.pdf" }, { + "idiom" : "universal", + "filename" : "mobile_onboarding_screen_3_illustration_black-dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } - ], - "filename" : "Frame-1.pdf", - "idiom" : "universal" + ] } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Frame-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Frame-1.pdf deleted file mode 100644 index 2ece0f7..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Frame-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Frame.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Frame.pdf deleted file mode 100644 index e730e3c..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/Frame.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/mobile_onboarding_screen_3_illustration_black-dark.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/mobile_onboarding_screen_3_illustration_black-dark.pdf new file mode 100644 index 0000000..abea0a6 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/mobile_onboarding_screen_3_illustration_black-dark.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/mobile_onboarding_screen_3_illustration_black.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/mobile_onboarding_screen_3_illustration_black.pdf new file mode 100644 index 0000000..d5ed1a6 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion3.imageset/mobile_onboarding_screen_3_illustration_black.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Contents.json index c95db4a..6696199 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Contents.json @@ -1,22 +1,36 @@ { "images" : [ { - "filename" : "Parallel.pdf", - "idiom" : "universal" + "idiom" : "iphone", + "filename" : "mobile_onboarding_screen_1_illustration_black.pdf" }, { + "idiom" : "iphone", + "filename" : "mobile_onboarding_screen_1_illustration_black-dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } - ], - "filename" : "Parallel-1.pdf", - "idiom" : "universal" + ] + }, + { + "idiom" : "ipad", + "filename" : "ipad_onboarding_screen_1_illustration_black.pdf" + }, + { + "idiom" : "ipad", + "filename" : "ipad_onboarding_screen_1_illustration_black-dark.pdf", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ] } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Parallel-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Parallel-1.pdf deleted file mode 100644 index 6195bb0..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Parallel-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Parallel.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Parallel.pdf deleted file mode 100644 index 54a47a8..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/Parallel.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/ipad_onboarding_screen_1_illustration_black-dark.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/ipad_onboarding_screen_1_illustration_black-dark.pdf new file mode 100644 index 0000000..8817773 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/ipad_onboarding_screen_1_illustration_black-dark.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/ipad_onboarding_screen_1_illustration_black.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/ipad_onboarding_screen_1_illustration_black.pdf new file mode 100644 index 0000000..a8a3939 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/ipad_onboarding_screen_1_illustration_black.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/mobile_onboarding_screen_1_illustration_black-dark.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/mobile_onboarding_screen_1_illustration_black-dark.pdf new file mode 100644 index 0000000..e5cb451 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/mobile_onboarding_screen_1_illustration_black-dark.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/mobile_onboarding_screen_1_illustration_black.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/mobile_onboarding_screen_1_illustration_black.pdf new file mode 100644 index 0000000..a38b100 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion4.imageset/mobile_onboarding_screen_1_illustration_black.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Contents.json index ece6600..4a1167b 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Contents.json @@ -1,22 +1,22 @@ { "images" : [ { - "filename" : "Multiple.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "black.pdf" }, { + "idiom" : "universal", + "filename" : "black-dark.pdf", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } - ], - "filename" : "Multiple-1.pdf", - "idiom" : "universal" + ] } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Multiple-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Multiple-1.pdf deleted file mode 100644 index a7e2177..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Multiple-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Multiple.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Multiple.pdf deleted file mode 100644 index 0db1af9..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/Multiple.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/black-dark.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/black-dark.pdf new file mode 100644 index 0000000..35dc95a Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/black-dark.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/black.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/black.pdf new file mode 100644 index 0000000..bc8b03e Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/captureSuggestion5.imageset/black.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/checkMarkBlue.imageset/checkMarkBlue.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/checkMarkBlue.imageset/checkMarkBlue.pdf deleted file mode 100644 index 3dbaaa5..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/checkMarkBlue.imageset/checkMarkBlue.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/close_icon.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/close_icon.imageset/Contents.json deleted file mode 100644 index 54f64ea..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/close_icon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "close_icon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/close_icon.imageset/close_icon.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/close_icon.imageset/close_icon.pdf deleted file mode 100644 index 2c26167..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/close_icon.imageset/close_icon.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/delete_icon.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/delete_icon.imageset/Contents.json deleted file mode 100644 index ff49b80..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/delete_icon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "delete_icon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/delete_icon.imageset/delete_icon.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/delete_icon.imageset/delete_icon.pdf deleted file mode 100644 index f2ae2c2..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/delete_icon.imageset/delete_icon.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorAuth.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorAuth.imageset/Contents.json deleted file mode 100644 index be58954..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorAuth.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "user-x.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorAuth.imageset/user-x.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorAuth.imageset/user-x.pdf deleted file mode 100644 index c26d318..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorAuth.imageset/user-x.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorCloud.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorCloud.imageset/Contents.json deleted file mode 100644 index c74caa7..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorCloud.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "cloud-off.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorCloud.imageset/cloud-off.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorCloud.imageset/cloud-off.pdf deleted file mode 100644 index 0f242b9..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorCloud.imageset/cloud-off.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorGlobe.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorGlobe.imageset/Contents.json deleted file mode 100644 index 79f849e..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorGlobe.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "globe.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorGlobe.imageset/globe.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorGlobe.imageset/globe.pdf deleted file mode 100644 index 05c3bc9..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorGlobe.imageset/globe.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorUpload.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorUpload.imageset/Contents.json deleted file mode 100644 index 8e00aee..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorUpload.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "upload.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorUpload.imageset/upload.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorUpload.imageset/upload.pdf deleted file mode 100644 index 19d634d..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/errorUpload.imageset/upload.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Contents.json index 431e7f3..11381b6 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Contents.json @@ -1,25 +1,15 @@ { "images" : [ { - "filename" : "Flash Icon Off 1.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Flash Icon Off.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "ic_flash_off.pdf" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Flash Icon Off 1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Flash Icon Off 1.pdf deleted file mode 100644 index 7010ef4..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Flash Icon Off 1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Flash Icon Off.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Flash Icon Off.pdf deleted file mode 100644 index c6fa210..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/Flash Icon Off.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/ic_flash_off.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/ic_flash_off.pdf new file mode 100644 index 0000000..5eec027 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOff.imageset/ic_flash_off.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Contents.json index 33ded50..495f259 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Contents.json @@ -1,25 +1,15 @@ { "images" : [ { - "filename" : "Flash Icon 1.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Flash Icon.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "On.pdf" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" }, "properties" : { "preserves-vector-representation" : true } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Flash Icon 1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Flash Icon 1.pdf deleted file mode 100644 index 967e5a2..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Flash Icon 1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Flash Icon.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Flash Icon.pdf deleted file mode 100644 index 967e5a2..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/Flash Icon.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/On.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/On.pdf new file mode 100644 index 0000000..6d7f036 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/flashOn.imageset/On.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/Contents.json deleted file mode 100644 index e9cd839..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "folder-1.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "folder.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/folder-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/folder-1.pdf deleted file mode 100644 index 573ba5f..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/folder-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/folder.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/folder.pdf deleted file mode 100644 index 573ba5f..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/folder.imageset/folder.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/greenCheckMark.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/greenCheckMark.imageset/Contents.json deleted file mode 100644 index 3519976..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/greenCheckMark.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "greenCheckMark.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/greenCheckMark.imageset/greenCheckMark.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/greenCheckMark.imageset/greenCheckMark.pdf deleted file mode 100644 index 66d7d45..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/greenCheckMark.imageset/greenCheckMark.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Contents.json deleted file mode 100644 index bd7090f..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Group 45-1.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Group 45.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Group 45-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Group 45-1.pdf deleted file mode 100644 index cad35ec..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Group 45-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Group 45.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Group 45.pdf deleted file mode 100644 index 95ca364..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport1.imageset/Group 45.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Contents.json deleted file mode 100644 index 5dd298b..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Group 76.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Group 46.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Group 46.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Group 46.pdf deleted file mode 100644 index 8cc9496..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Group 46.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Group 76.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Group 76.pdf deleted file mode 100644 index 84776e2..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport2.imageset/Group 76.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Contents.json deleted file mode 100644 index fb785c4..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Group 44.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Group 47.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Group 44.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Group 44.pdf deleted file mode 100644 index 9977582..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Group 44.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Group 47.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Group 47.pdf deleted file mode 100644 index 10a1c6c..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/helpImport3.imageset/Group 47.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/checkMarkBlue.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/navigationHelpBack.imageset/Contents.json similarity index 75% rename from Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/checkMarkBlue.imageset/Contents.json rename to Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/navigationHelpBack.imageset/Contents.json index 9c68bf9..dda850c 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/checkMarkBlue.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/navigationHelpBack.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "checkMarkBlue.pdf", + "filename" : "ic_back_camera.pdf", "idiom" : "universal" } ], diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/navigationHelpBack.imageset/ic_back_camera.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/navigationHelpBack.imageset/ic_back_camera.pdf new file mode 100644 index 0000000..b9cd2a3 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/navigationHelpBack.imageset/ic_back_camera.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/Contents.json index 2c904c5..f9f7dec 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "filename" : "x-circle.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "x-circle-1.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "tick_red.pdf" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/tick_red.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/tick_red.pdf new file mode 100644 index 0000000..f987e2a Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/tick_red.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/x-circle-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/x-circle-1.pdf deleted file mode 100644 index 1b36d0b..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/x-circle-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/x-circle.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/x-circle.pdf deleted file mode 100644 index fd2339f..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/nonSupportedFormatsIcon.imageset/x-circle.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Contents.json deleted file mode 100644 index c3dc661..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Onboarding.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Onboarding 1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Onboarding 1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Onboarding 1.pdf deleted file mode 100644 index 88df541..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Onboarding 1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Onboarding.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Onboarding.pdf deleted file mode 100644 index 6d72922..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingFlatPaper.imageset/Onboarding.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Contents.json deleted file mode 100644 index c3dc661..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Onboarding.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Onboarding 1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Onboarding 1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Onboarding 1.pdf deleted file mode 100644 index e334a30..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Onboarding 1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Onboarding.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Onboarding.pdf deleted file mode 100644 index eaa1c4d..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingGoodLighting.imageset/Onboarding.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Contents.json deleted file mode 100644 index c3dc661..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Onboarding.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Onboarding 1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Onboarding 1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Onboarding 1.pdf deleted file mode 100644 index 095356e..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Onboarding 1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Onboarding.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Onboarding.pdf deleted file mode 100644 index 26e5c15..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingMultiPages.imageset/Onboarding.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_back.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage1.imageset/Contents.json similarity index 65% rename from Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_back.imageset/Contents.json rename to Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage1.imageset/Contents.json index 317b150..1a5bf01 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/barButton_back.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage1.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "chevron.left.png", + "filename" : "mobile_onboarding_screen_3_illustration.pdf", "idiom" : "universal" } ], diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage1.imageset/mobile_onboarding_screen_3_illustration.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage1.imageset/mobile_onboarding_screen_3_illustration.pdf new file mode 100644 index 0000000..1d854fc Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage1.imageset/mobile_onboarding_screen_3_illustration.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage2.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage2.imageset/Contents.json new file mode 100644 index 0000000..5f9f27f --- /dev/null +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "mobile_onboarding_screen_5_illustration.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage2.imageset/mobile_onboarding_screen_5_illustration.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage2.imageset/mobile_onboarding_screen_5_illustration.pdf new file mode 100644 index 0000000..ffee7e2 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage2.imageset/mobile_onboarding_screen_5_illustration.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/Contents.json new file mode 100644 index 0000000..9d071b7 --- /dev/null +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "mobile_onboarding_screen_1_illustration.pdf", + "idiom" : "iphone" + }, + { + "filename" : "ipad_onboarding_screen_1_illustration.pdf", + "idiom" : "ipad" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/ipad_onboarding_screen_1_illustration.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/ipad_onboarding_screen_1_illustration.pdf new file mode 100644 index 0000000..95bba7d Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/ipad_onboarding_screen_1_illustration.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/mobile_onboarding_screen_1_illustration.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/mobile_onboarding_screen_1_illustration.pdf new file mode 100644 index 0000000..221d9e2 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage3.imageset/mobile_onboarding_screen_1_illustration.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/CameraDefaultReturnAssistantDocument.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage4.imageset/Contents.json similarity index 65% rename from Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/CameraDefaultReturnAssistantDocument.imageset/Contents.json rename to Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage4.imageset/Contents.json index ada148c..53253af 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/CameraDefaultReturnAssistantDocument.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage4.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "CameraDefaultReturnAssistantDocument.jpg", + "filename" : "ipad_onboarding_screen_4_illustration.pdf", "idiom" : "universal" } ], diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage4.imageset/ipad_onboarding_screen_4_illustration.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage4.imageset/ipad_onboarding_screen_4_illustration.pdf new file mode 100644 index 0000000..b5f4b0e Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage4.imageset/ipad_onboarding_screen_4_illustration.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraFocus.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage5.imageset/Contents.json similarity index 75% rename from Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraFocus.imageset/Contents.json rename to Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage5.imageset/Contents.json index 6c37465..a9cade0 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/cameraFocus.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage5.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "cameraFocus.png", + "filename" : "ic_multipage.pdf", "idiom" : "universal" } ], diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage5.imageset/ic_multipage.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage5.imageset/ic_multipage.pdf new file mode 100644 index 0000000..d3c3f28 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingPage5.imageset/ic_multipage.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Contents.json deleted file mode 100644 index 51f593b..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Onboarding-3.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Onboarding.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Onboarding-3.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Onboarding-3.pdf deleted file mode 100644 index e89e188..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Onboarding-3.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Onboarding.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Onboarding.pdf deleted file mode 100644 index 25258ae..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/onboardingQRCode.imageset/Onboarding.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/Contents.json deleted file mode 100644 index 93903e1..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "plus.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "plus_icon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/plus.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/plus.pdf deleted file mode 100644 index 354cdaf..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/plus.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/plus_icon.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/plus_icon.pdf deleted file mode 100644 index 3de3480..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/plus_icon.imageset/plus_icon.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/qrCodeFocus.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/qrCodeFocus.imageset/Contents.json deleted file mode 100644 index 1650c9d..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/qrCodeFocus.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "qrCodeFocus.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/qrCodeFocus.imageset/qrCodeFocus.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/qrCodeFocus.imageset/qrCodeFocus.pdf deleted file mode 100644 index c378e00..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/qrCodeFocus.imageset/qrCodeFocus.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/Contents.json index ca57cfe..1208fc3 100644 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/Contents.json +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "filename" : "check-circle-1.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "check-circle.pdf", - "idiom" : "universal" + "idiom" : "universal", + "filename" : "tick_green.pdf" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/check-circle-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/check-circle-1.pdf deleted file mode 100644 index e3ef559..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/check-circle-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/check-circle.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/check-circle.pdf deleted file mode 100644 index 8d6120a..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/check-circle.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/tick_green.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/tick_green.pdf new file mode 100644 index 0000000..a235469 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/supportedFormatsIcon.imageset/tick_green.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/toolTipCloseButton.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/toolTipCloseButton.imageset/Contents.json new file mode 100644 index 0000000..bf5e2c9 --- /dev/null +++ b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/toolTipCloseButton.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "close_button_icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/toolTipCloseButton.imageset/close_button_icon.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/toolTipCloseButton.imageset/close_button_icon.pdf new file mode 100644 index 0000000..6d02497 Binary files /dev/null and b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/toolTipCloseButton.imageset/close_button_icon.pdf differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/Contents.json b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/Contents.json deleted file mode 100644 index 40cb1b9..0000000 --- a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "upload-1.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "upload.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/upload-1.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/upload-1.pdf deleted file mode 100644 index b1d7ecc..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/upload-1.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/upload.pdf b/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/upload.pdf deleted file mode 100644 index b1d7ecc..0000000 Binary files a/Sources/GiniCaptureSDK/Resources/GiniImages.xcassets/uploadIcon.imageset/upload.pdf and /dev/null differ diff --git a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpFormatCell.xib b/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpFormatCell.xib deleted file mode 100644 index 7fcedff..0000000 --- a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpFormatCell.xib +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpFormatSectionHeader.xib b/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpFormatSectionHeader.xib deleted file mode 100644 index ec83f0f..0000000 --- a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpFormatSectionHeader.xib +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpImportCell.xib b/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpImportCell.xib deleted file mode 100644 index 72f18f4..0000000 --- a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpImportCell.xib +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpMenuCell.xib b/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpMenuCell.xib deleted file mode 100644 index 953f4d8..0000000 --- a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpMenuCell.xib +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpTipCell.xib b/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpTipCell.xib deleted file mode 100644 index 39ed1a6..0000000 --- a/Sources/GiniCaptureSDK/Resources/HelpScreen/HelpTipCell.xib +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/IconHeader.xib b/Sources/GiniCaptureSDK/Resources/IconHeader.xib deleted file mode 100644 index 4776d98..0000000 --- a/Sources/GiniCaptureSDK/Resources/IconHeader.xib +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingBottomNavigationBar.xib b/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingBottomNavigationBar.xib deleted file mode 100644 index f34aef1..0000000 --- a/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingBottomNavigationBar.xib +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingPageCell.xib b/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingPageCell.xib deleted file mode 100644 index 8cbf133..0000000 --- a/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingPageCell.xib +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingViewController.xib b/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingViewController.xib deleted file mode 100644 index 05302de..0000000 --- a/Sources/GiniCaptureSDK/Resources/OnboardingScreen/OnboardingViewController.xib +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/ReviewScreen/ReviewBottomNavigationBar.xib b/Sources/GiniCaptureSDK/Resources/ReviewScreen/ReviewBottomNavigationBar.xib deleted file mode 100644 index 470e78d..0000000 --- a/Sources/GiniCaptureSDK/Resources/ReviewScreen/ReviewBottomNavigationBar.xib +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings b/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings index a44fcb3..e7d15d6 100644 --- a/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings +++ b/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings @@ -6,7 +6,7 @@ Copyright © 2016 Gini. All rights reserved. */ -"ginicapture.navigationbar.camera.title" = "Aufnahme"; +"ginicapture.navigationbar.camera.title" = "Dokument fotografieren"; "ginicapture.navigationbar.onboarding.title" = "Anleitung"; "ginicapture.navigationbar.review.title" = "Überprüfen"; "ginicapture.navigationbar.analysis.title" = "Etwas Geduld, analysiere Foto"; @@ -17,36 +17,27 @@ "ginicapture.navigationbar.review.close" = ""; "ginicapture.navigationbar.review.continue" = "Weiter"; "ginicapture.navigationbar.analysis.back" = "Abbrechen"; -"ginicapture.navigationbar.help.backToCamera" = "Kamera"; -"ginicapture.navigationbar.help.backToMenu" = "Hilfe"; -"ginicapture.navigationbar.analysis.backToReview" = "Übersicht"; +"ginicapture.navigationbar.help.backToCamera" = ""; +"ginicapture.navigationbar.help.backToMenu" = ""; "ginicapture.camera.captureButton" = "Auslösen"; -"ginicapture.camera.notAuthorized.title" = "Kamerazugriff"; -"ginicapture.camera.notAuthorized.description" = "Bitte gestatte Zugriff auf deine Kamera, um Dokumente fotografieren zu können"; -"ginicapture.camera.notAuthorizedButton.noStatus" = "Zugriff erlauben"; +"ginicapture.camera.notAuthorized" = "Um gespeicherte Bilder zu analysieren, wird Zugriff auf Ihre Kamera benötigt"; +"ginicapture.camera.notAuthorizedButton" = "Zugriff erlauben"; "ginicapture.camera.captureFailed" = "Bilderfassung fehlgeschlagen"; "ginicapture.camera.filepicker.photoLibraryAccessDenied" = "Um gespeicherte Bilder zu analysieren, wird Zugriff auf Ihre Fotos benötigt."; "ginicapture.camera.filepicker.mixedDocumentsUnsupported" = "Es ist nur möglich einen Dateityp gleichzeitig auszuwählen."; "ginicapture.camera.filepicker.failedToOpenDocument" = "Die Datei konnte nicht geöffnet werden. Bitte verwenden Sie einen anderen Dateityp."; -"ginicapture.camera.filepicker.multiplePdfErrorMessage" = "Es kann nur eine PDF-Datei gleichzeitig ausgewählt werden."; "ginicapture.camera.filepicker.errorPopup.cancelButton" = "Abbrechen"; "ginicapture.camera.filepicker.errorPopup.confirmButton" = "OK"; "ginicapture.camera.filepicker.errorPopup.grantAccessButton" = "Zugriff erteilen"; -"ginicapture.camera.documentValidationError.general" = "Ihre Datei konnte nicht geöffnet werden. Bitte versuchen sie es mit einer anderenDatei."; +"ginicapture.camera.documentValidationError.general" = "Leider kann diese Datei nicht bearbeitet werden"; "ginicapture.camera.documentValidationError.excedeedFileSize" = "Diese Datei ist leider zu groß"; -"ginicapture.camera.documentValidationError.tooManyPages" = "Das Dokument darf nicht mehr als 10 Seiten haben."; -"ginicapture.camera.documentValidationError.wrongFormat" = "Nur Dateien vom Format jpg, png, gif oder pdf können analysiert werden."; -"ginicapture.camera.documentValidationError.pdfPasswordProtected" = "Passwortgeschützte Dokumente können nicht analysiert werden."; -"ginicapture.camera.fileImportButtonLabel" = "Dateien"; -"ginicapture.camera.flashButtonLabel.On.Voice.Over" = "Blitz an"; -"ginicapture.camera.flashButtonLabel.Off.Voice.Over" = "Blitz aus"; -"ginicapture.camera.flashButtonLabel.On" = "An"; -"ginicapture.camera.flashButtonLabel.Off" = "Aus"; -"ginicapture.camera.thumbnail.Voice.Over" = "Foto vorschaubild"; -"ginicapture.camera.capturebutton" = "Bild Aufnehmen"; -"ginicapture.camera.infoLabel" = "Rechnung oder QR Code scannen"; +"ginicapture.camera.documentValidationError.tooManyPages" = "Das Dokument darf nicht mehr als 10 Seiten haben"; +"ginicapture.camera.documentValidationError.wrongFormat" = "Nur Dateien vom Format jpg, tiff, png, gif oder pdf können analysiert werden"; +"ginicapture.camera.fileImportTip" = "Es können jetzt auch ganz einfach Dateien hochgeladen werden."; +"ginicapture.camera.fileImportButtonLabel" = "Importieren"; +"ginicapture.camera.qrCodeTip" = "Haben Sie schon den QR-Codeleser ausprobiert?"; "ginicapture.camera.capturedImagesStackLabel" = "Erfasste Dokumente"; "ginicapture.camera.qrCodeDetectedPopup.message" = "QR verwenden?"; "ginicapture.camera.unsupportedQrCodeDetectedPopup.message" = "Dieser QR-Code enthält keine Bezahlinformationen."; @@ -59,8 +50,8 @@ "ginicapture.camera.errorPopup.reviewPages" = "Seitenübersicht"; "ginicapture.camera.unknownError" = "Auf dem Kamerabildschirm ist ein unbekannter Fehler aufgetreten"; "ginicapture.camera.popupTitleImportPDF" = "PDF importieren"; -"ginicapture.camera.popupOptionPhotos" = "Fotos hochladen"; -"ginicapture.camera.popupOptionFiles" = "Dokument hochladen"; +"ginicapture.camera.popupOptionPhotos" = "Fotos"; +"ginicapture.camera.popupOptionFiles" = "Dokumente"; "ginicapture.camera.popupTitleImportPDForPhotos" = "Fotos oder PDF importieren"; "ginicapture.camera.popupCancel" = "Abbrechen"; @@ -70,37 +61,19 @@ "ginicapture.onboarding.fourthPage" = "Beim Abfotografieren der Rechnung oder des Überweisungsträgers auf gute Beleuchtung achten"; "ginicapture.onboarding.fifthPage" = "Jetzt ist es möglich, eine Rechnung mit mehreren Seiten zu analysieren"; -"ginicapture.onboarding.getstarted" = "Los geht’s"; -"ginicapture.onboarding.next" = "Weiter"; -"ginicapture.onboarding.skip" = "Überspringen"; -"ginicapture.onboarding.flatPaper.title" = "Dokument flach ausrichten"; -"ginicapture.onboarding.flatPaper.description" = "Das Dokument sollte flach liegen und sich im Rahmen befinden"; -"ginicapture.onboarding.goodLighting.title" = "Gute Lichtverhältnisse"; -"ginicapture.onboarding.goodLighting.description" = "Gutes Licht führt zu besseren Ergebnisse als mit Blitz"; -"ginicapture.onboarding.multiPages.title" = "Mehrere Seiten"; -"ginicapture.onboarding.multiPages.description" = "Es ist möglich mehrer Seiten eines Dokuments zu Scannen"; -"ginicapture.onboarding.qrCode.title" = "QR Codes scannen"; -"ginicapture.onboarding.qrCode.description" = "QR Codes werden automatisch erkannt und analysiert"; +"ginicapture.review.top" = "Dokument vollständig, scharf und in Leserichtung?"; +"ginicapture.review.bottom" = "Doppeltippen um Schärfe zu prüfen"; +"ginicapture.review.rotateButton" = "Dokument drehen"; "ginicapture.review.documentImageTitle" = "Dokument"; "ginicapture.review.unknownError" = "Auf dem Überprüfungsbildschirm ist ein unbekannter Fehler aufgetreten"; -"ginicapture.review.delete" = "Seite löschen"; -"ginicapture.analysis.screenTitle" = "Übersicht"; "ginicapture.analysis.loadingText" = "Dokument wird analysiert"; -"ginicapture.analysis.loadingText.pdf" = "%@\nwird analysiert"; -"ginicapture.analysis.section.header" = "Nützliche Tipps"; -"ginicapture.analysis.suggestion.1" = "Gute Lichtverhältnisse"; -"ginicapture.analysis.suggestion.1.details" = "Gutes Licht führt zu besseren Ergebnissen als mit Blitz"; - -"ginicapture.analysis.suggestion.2" = "Flach ausrichten"; -"ginicapture.analysis.suggestion.2.details" = "Flach gestrichene Dokumente erleichtern die Auswertung"; -"ginicapture.analysis.suggestion.3" = "Im Rahmen positionieren"; -"ginicapture.analysis.suggestion.3.details" = "Das gesamte Dokument sollte sich im Rahmen befinden"; -"ginicapture.analysis.suggestion.4" = "Parallel"; -"ginicapture.analysis.suggestion.4.details" = "Das Handy sollte parallel über dem Dokument gehalten werden"; -"ginicapture.analysis.suggestion.5" = "Mehrere Seiten"; -"ginicapture.analysis.suggestion.5.details" = "Es ist nun möglich mehrere Seiten eines Dokuments zu scannen"; - +"ginicapture.analysis.suggestion.header" = "Um schneller Ergebnisse zu erhalten, bitte:"; +"ginicapture.analysis.suggestion.1" = "Auf gute Beleuchtung achten"; +"ginicapture.analysis.suggestion.2" = "Seite glatt streichen"; +"ginicapture.analysis.suggestion.3" = "Gerät parallel über die Seite halten"; +"ginicapture.analysis.suggestion.4" = "Seite vollständig in den angezeigten Rahmen einpassen"; +"ginicapture.analysis.suggestion.5" = "Jetzt ist es möglich, eine Rechnung mit mehreren Seiten zu analysieren"; "ginicapture.analysis.error.actionTitle" = "Wiederholen"; "ginicapture.analysis.error.documentCreation" = "Beim Hochladen des Dokuments ist ein Fehler aufgetreten."; "ginicapture.analysis.error.analysis" = "Beim Analysieren des Dokuments ist ein Fehler aufgetreten."; @@ -115,79 +88,41 @@ "ginicapture.help.openWithTutorial.step2.title" = "Aktivieren Sie die Teilen-Funktion"; "ginicapture.help.openWithTutorial.step2.subTitle" = "Um die Datei an die %@ weiterzuleiten, verwenden Sie die Teilen-Funktion, dargestellt als ein Viereck mit hoch zeigendem Pfeil, und wählen Sie “Öffnen in...” oder “Datei freigeben”. Bitte wählen Sie dann die %@ aus der Liste aus, um den Analyse- und Überweisungsprozess zu starten."; "ginicapture.help.openWithTutorial.step3.title" = "Auf iPads können Sie auch “Drag-and-drop” nutzen"; -"ginicapture.help.openWithTutorial.step3.subTitle" = "Auf iPads können ab iOS 12 PDFs oder Fotos bequem aus der Datei-Browser-App per “Drag-and-drop” in die %@ gezogen werden, um den Überweisungsprozess zu starten. Hierfür öffnen Sie zunächst die %@ und bringen Sie die Datei-Browser-App als zweite App auf dem Screen an. Wählen Sie dann die gewünschte Datei aus und ziehen Sie diese zur %@ hinüber."; +"ginicapture.help.openWithTutorial.step3.subTitle" = "Auf iPads können ab iOS 11 PDFs oder Fotos bequem aus der Datei-Browser-App per “Drag-and-drop” in die %@ gezogen werden, um den Überweisungsprozess zu starten. Hierfür öffnen Sie zunächst die %@ und bringen Sie die Datei-Browser-App als zweite App auf dem Screen an. Wählen Sie dann die gewünschte Datei aus und ziehen Sie diese zur %@ hinüber."; "ginicapture.noresults.warning" = "Es konnten keine Daten ausgelesen werden. Bitte Aufnahme wiederholen."; "ginicapture.noresults.collection.header" = "Tipps für beste Ergebnisse:"; "ginicapture.noresults.gotocamera" = "Zur Kamera"; -"ginicapture.noresults.title" = "Tipps"; +"ginicapture.noresults.title" = "Tipps für Fotos"; "ginicapture.noresults.warningHelpMenu" = "Zur Erkennung aller Daten bitte folgende Tipps beim Abfotografieren beachten:"; -"ginicapture.noresult.enterManually" = "Manuell ausfüllen"; -"ginicapture.noresult.retakeImages" = "Foto wiederholen"; - -"ginicapture.noresult.title" = "Keine Ergebnisse"; -"ginicapture.noresult.header" = "Leider konnten keine Informationen aus dem Dokument ausgelesen werden"; - -"ginicapture.error.enterManually" = "Manuell ausfüllen"; -"ginicapture.error.backToCamera" = "Neues Photo"; -"ginicapture.error.title" = "Error"; -"ginicapture.error.connection.content" = "Es konnte keine Verbindung zum Internet hergestellt werden. Bitte überprüfen Sie Ihre Netzwerkverbindung und wiederholen Sie den Vorgang."; -"ginicapture.error.authentication.content" = "Upload des Dokuments ist fehlgeschlagen aufgrund eines Fehlers bei der Authentifizierung. Bitte versuchen Sie es später noch einmal."; -"ginicapture.error.serverError.content" = "Upload des Dokuments ist fehlgeschlagen aufgrund eines Serverfehlers. Bitte versuchen Sie es später noch einmal."; -"ginicapture.error.unexpected.content" = "Es scheint hier ist etwas schiefgelaufen. Bitte versuchen Sie es später noch einmal."; -"ginicapture.error.connection.title" = "Überprüfen Sie Ihre Internetverbindung"; -"ginicapture.error.authentication.title" = "Authentifizierung fehlgeschlagen"; -"ginicapture.error.serverError.title" = "Dienst nicht verfügbar"; -"ginicapture.error.unexpected.title" = "Ein unbekannter Fehler ist aufgetreten."; -"ginicapture.error.request.content" = "Das Dokument konnte nicht angenommen werden. Bitte überprüfen Sie, ob das Bild scharf ist, das Dokument Zahlungsinformationen enthält und im richtigen Dateiformat hochgeladen wird."; -"ginicapture.error.request.title" = "Upload des Dokuments fehlgeschlagen"; - -"ginicapture.help.import.title" = "Importieren"; -"ginicapture.help.import.selectInvoice.title" = "Rechnung auswählen"; -"ginicapture.help.import.selectInvoice.desc" = "Bitte die PDF-Rechnung in der Email-App, PDF-Ordner oder einer anderen App aus dem Handy auswählen. Um das Dokument nun an die Banking App zu senden, sollte die “Teilen mit” Funktion benutzt werden, welche mit einem Rechteck und Pfeil nach oben zeigend dargestellt ist."; -"ginicapture.help.import.selectInvoice.accessibility" = "Aktivieren Sie die Teilen-Funktion. Dazu wählen Sie eine Datei aus und verwenden anschließend die Teilen-Funktion, dargestellt als Viereck mit hoch-zeigendem Pfeil"; -"ginicapture.help.import.importtoapp.title" = "Aus der App importieren"; -"ginicapture.help.import.importtoapp.desc" = "Nun die Banking App aus der Liste auswählen, um das Dokument zum Analysieren weiterzuleiten."; -"ginicapture.help.import.importtoapp.accessibility" = "Banking App auswählen. Wählen Sie nun die gewünschte App aus mit der das Dokument geteilt werden soll"; -"ginicapture.help.import.draganddrop.title" = "Drag & drop auf dem iPad"; -"ginicapture.help.import.draganddrop.desc" = "Auf dem iPad können Sie ab iOS 12 oder neuer PDFs oder Fotos bequem aus der Datei-Browser-App per “Drag and Drop” in die Banking App gezogen werden, um den Überweisungsprozess zu starten. Hierzu öffnen Sie zunächst Ihre Banking App und die Datei-Browser-App parallel auf Ihrem Bildschirm. Ziehen Sie dann die gewünschte Datei rüber in die Banking App."; -"ginicapture.help.import.draganddrop.accessibility" = "Auf dem Tablet können Sie auch die zu teilende Datei mit Drag and Drop in die Banking App ziehen"; - -"ginicapture.help.supportedFormats.title" = "Formate"; -"ginicapture.help.supportedFormats.section.1.title" = "Unterstützte Formate"; -"ginicapture.help.supportedFormats.section.1.item.1" = "Computer-generierte Rechnungen und Überweisungsträger"; -"ginicapture.help.supportedFormats.section.1.item.2" = "1-seitige Fotos im Format jpeg, png oder gif"; -"ginicapture.help.supportedFormats.section.1.item.3" = "PDF Dokumente bis zu 10 Seiten"; +"ginicapture.help.supportedFormats.title" = "Unterstützte Formate"; +"ginicapture.help.supportedFormats.section.1.title" = "Folgende Formate werden unterstützt:"; +"ginicapture.help.supportedFormats.section.1.item.1" = "Computer-erstellte Überweisungsträger und Rechnungen"; +"ginicapture.help.supportedFormats.section.1.item.2" = "Einseitige Bilder im jpg, png, gif oder tiff Format"; +"ginicapture.help.supportedFormats.section.1.item.3" = "PDF Dokumente von bis zu 10 Seiten"; "ginicapture.help.supportedFormats.section.1.item.4" = "QR Codes"; -"ginicapture.help.supportedFormats.section.1.item.5" = "Monitoraufnahmen oder Screenshots"; -"ginicapture.help.supportedFormats.section.2.title" = "Nicht unterstütztes Format"; +"ginicapture.help.supportedFormats.section.2.title" = "Was nicht analysiert wird:"; "ginicapture.help.supportedFormats.section.2.item.1" = "Handschrift"; "ginicapture.help.supportedFormats.section.2.item.2" = "Fotos von Bildschirmen"; "ginicapture.help.menu.title" = "Hilfe"; -"ginicapture.help.menu.tips" = "Tipps für bessere Ergebnisse"; -"ginicapture.help.menu.formats" = "Unterstützte Formate"; -"ginicapture.help.menu.import" = "Dokumente von anderen Apps importieren"; +"ginicapture.help.menu.firstItem" = "Tipps für beste Ergebnisse aus Fotos"; +"ginicapture.help.menu.secondItem" = "Dokumente aus anderen Apps öffnen"; +"ginicapture.help.menu.thirdItem" = "Unterstützte Formate"; "ginicapture.albums.title" = "Alben"; -"ginicapture.imagepicker.openbutton" = "Fertig"; +"ginicapture.imagepicker.openbutton" = "Öffnen"; -"ginicapture.multipagereview.title" = "Übersicht"; -"ginicapture.multipagereview.description" = "Sind alle Zahlungsinformationen gut sichtbar?"; -"ginicapture.multipagereview.mainButtonTitle" = "Verarbeiten"; -"ginicapture.multipagereview.secondaryButtonTitle" = "Seite"; +"ginicapture.multipagereview.reorderContainerTooltipMessage" = "Die Seiten müssen in der richtigen Reihenfolge sein. Hier können Sie sie sortieren."; +"ginicapture.multipagereview.addButtonLabel" = "Seiten hinzufügen"; +"ginicapture.multipagereview.dragAndDropTip" = "Drag & Drop zum Sortieren"; +"ginicapture.multipagereview.title" = "%d von %d"; "ginicapture.multipagereview.error.retakeAction" = "Wiederholung"; "ginicapture.multipagereview.error.retryAction" = "Wiederholen"; -"ginicapture.images.backToAlbums" = "Alben"; "ginicapture.images.openWithTutorialStep1" = "openWithTutorialStep1"; "ginicapture.images.openWithTutorialStep2" = "openWithTutorialStep2"; "ginicapture.images.openWithTutorialStep3" = "openWithTutorialStep3"; "ginicapture.albums.selectMorePhotosButton" = "Weitere Bilder auswählen"; "ginicapture.albums.footer" = "Um weitere Fotos auszuwählen oder die Auswahl aufzuheben verwenden Sie \"Weitere Bilder auswählen\"."; - -"ginicapture.QRscanning.correct" = "QR Code erkannt"; -"ginicapture.QRscanning.incorrect.title" = "Unbekannter QR code"; -"ginicapture.QRscanning.incorrect.description" = "Dieser Code enthält keine Zahlungsinformationen. Bitte anderen QR Code oder Rechnung abfotografieren."; -"ginicapture.QRscanning.loading" = "Rechnung wird übermittelt"; diff --git a/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings b/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings index 9d382d1..a5b5d4c 100644 --- a/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings +++ b/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings @@ -6,7 +6,7 @@ Copyright © 2016 Gini. All rights reserved. */ -"ginicapture.navigationbar.camera.title" = "Scan"; +"ginicapture.navigationbar.camera.title" = "Photograph document"; "ginicapture.navigationbar.onboarding.title" = "Instructions"; "ginicapture.navigationbar.review.title" = "Check photo quality"; "ginicapture.navigationbar.analysis.title" = "Be patient, analysing photo"; @@ -17,36 +17,27 @@ "ginicapture.navigationbar.review.close" = ""; "ginicapture.navigationbar.review.continue" = "Next"; "ginicapture.navigationbar.analysis.back" = "Cancel"; -"ginicapture.navigationbar.help.backToCamera" = "Camera"; -"ginicapture.navigationbar.help.backToMenu" = "Help"; -"ginicapture.navigationbar.analysis.backToReview" = "Review"; +"ginicapture.navigationbar.help.backToCamera" = ""; +"ginicapture.navigationbar.help.backToMenu" = ""; "ginicapture.camera.captureButton" = "Capture"; -"ginicapture.camera.notAuthorized.title" = "Camera access"; -"ginicapture.camera.notAuthorized.description" = "We need access to your camera to enable you to capture your invoices"; -"ginicapture.camera.notAuthorizedButton.noStatus" = "Give access"; +"ginicapture.camera.notAuthorized" = "Permission to access the camera is required to analyse photos"; +"ginicapture.camera.notAuthorizedButton" = "Grant permission"; "ginicapture.camera.captureFailed" = "Image capture failed"; "ginicapture.camera.filepicker.photoLibraryAccessDenied" = "Permission to access your photos gallery is required to analyse saved photos"; "ginicapture.camera.filepicker.mixedDocumentsUnsupported" = "It is only possible to select one file type at the same time."; "ginicapture.camera.filepicker.failedToOpenDocument" = "The file could not be opened. Please use a different file type."; -"ginicapture.camera.filepicker.multiplePdfErrorMessage" = "It is only possible to select one PDF at a time."; "ginicapture.camera.filepicker.errorPopup.cancelButton" = "Cancel"; "ginicapture.camera.filepicker.errorPopup.confirmButton" = "OK"; "ginicapture.camera.filepicker.errorPopup.grantAccessButton" = "Grant permission"; -"ginicapture.camera.documentValidationError.general" = "The document could not be opened. Please try a different document."; -"ginicapture.camera.documentValidationError.excedeedFileSize" = "This file is too large."; +"ginicapture.camera.documentValidationError.general" = "Unfortunately, this file can not be edited"; +"ginicapture.camera.documentValidationError.excedeedFileSize" = "Unfortunately, this file is too big"; "ginicapture.camera.documentValidationError.tooManyPages" = "The document can only have a maximum of 10 pages."; -"ginicapture.camera.documentValidationError.wrongFormat" = "Only files of the formats jpg, png, gif or pdf can be analysed."; -"ginicapture.camera.documentValidationError.pdfPasswordProtected" = "Password protected documents cannot be analysed."; -"ginicapture.camera.fileImportButtonLabel" = "Files"; -"ginicapture.camera.flashButtonLabel.On.Voice.Over" = "Flash On"; -"ginicapture.camera.flashButtonLabel.Off.Voice.Over" = "Flash Off"; -"ginicapture.camera.flashButtonLabel.On" = "On"; -"ginicapture.camera.flashButtonLabel.Off" = "Off"; -"ginicapture.camera.thumbnail.Voice.Over" = "Photo review"; -"ginicapture.camera.capturebutton" = "Take picture"; -"ginicapture.camera.infoLabel" = "Scan an invoice or a QR code"; +"ginicapture.camera.documentValidationError.wrongFormat" = "Only files of the formats jpg, png, gif or pdf can be analysed"; +"ginicapture.camera.fileImportTip" = "Now documents can be conveniently uploaded for analysis."; +"ginicapture.camera.fileImportButtonLabel" = "Import"; +"ginicapture.camera.qrCodeTip" = "Have you checked out the QR code scan yet?"; "ginicapture.camera.capturedImagesStackLabel" = "Captured documents"; "ginicapture.camera.qrCodeDetectedPopup.message" = "Use QR code?"; "ginicapture.camera.unsupportedQrCodeDetectedPopup.message" = "This QR Code does not contain any payment information."; @@ -59,47 +50,30 @@ "ginicapture.camera.errorPopup.reviewPages" = "Review pages"; "ginicapture.camera.unknownError" = "An unknown error has occurred on the camera screen"; "ginicapture.camera.popupTitleImportPDF" = "Import PDF"; -"ginicapture.camera.popupOptionPhotos" = "Upload photo"; -"ginicapture.camera.popupOptionFiles" = "Upload files"; -"ginicapture.camera.popupTitleImportPDForPhotos" = ""; +"ginicapture.camera.popupOptionPhotos" = "Gallery"; +"ginicapture.camera.popupOptionFiles" = "Documents"; +"ginicapture.camera.popupTitleImportPDForPhotos" = "Import Photos or PDF files"; "ginicapture.camera.popupCancel" = "Cancel"; -"ginicapture.onboarding.getstarted" = "Get Started"; -"ginicapture.onboarding.next" = "Next"; -"ginicapture.onboarding.skip" = "Skip"; "ginicapture.onboarding.firstPage" = "Hold device parallel over the page"; "ginicapture.onboarding.secondPage" = "Flatten the page"; "ginicapture.onboarding.thirdPage" = "Capture the entire page within the frame shown on the camera screen"; "ginicapture.onboarding.fourthPage" = "Ensure good lighting when photographing the invoice or remittance slip"; "ginicapture.onboarding.fifthPage" = "It's now possible to analyse an invoice with multiple pages!"; -"ginicapture.onboarding.flatPaper.title" = "Flat paper within the frame"; -"ginicapture.onboarding.flatPaper.description" = "Ensure that the document is flat, and positioned within the frame"; -"ginicapture.onboarding.goodLighting.title" = "Good lighting"; -"ginicapture.onboarding.goodLighting.description" = "Proper lighting leads to better results than flash"; -"ginicapture.onboarding.multiPages.title" = "Add multiple pages"; -"ginicapture.onboarding.multiPages.description" = "It’s now possible to analyse an invoice with multiple pages"; -"ginicapture.onboarding.qrCode.title" = "QR codes supported"; -"ginicapture.onboarding.qrCode.description" = "Besides invoices, QR codes -are also supported"; +"ginicapture.review.top" = "Is the document complete, in focus and in the correct orientation?"; +"ginicapture.review.bottom" = "Double-tap to check the focus"; +"ginicapture.review.rotateButton" = "Rotate document"; "ginicapture.review.documentImageTitle" = "Document"; "ginicapture.review.unknownError" = "An unknown error has occurred on the review screen"; -"ginicapture.review.delete" = "Delete page"; -"ginicapture.analysis.screenTitle" = "Review"; -"ginicapture.analysis.loadingText" = "Analyzing documents"; -"ginicapture.analysis.loadingText.pdf" = "Analyzing\n%@"; -"ginicapture.analysis.suggestion.1" = "Good lighting"; -"ginicapture.analysis.suggestion.1.details" = "Proper lighting leads to better results than flash"; +"ginicapture.analysis.loadingText" = "Document is being analysed"; +"ginicapture.analysis.suggestion.header" = "For faster results, please:"; +"ginicapture.analysis.suggestion.1" = "Ensure good lighting"; "ginicapture.analysis.suggestion.2" = "Flatten the page"; -"ginicapture.analysis.suggestion.2.details" = "A flat paper helps to receive better results"; -"ginicapture.analysis.suggestion.3" = "Position in the frame"; -"ginicapture.analysis.suggestion.3.details" = "Capture the entire page within the frame shown on the camera screen"; -"ginicapture.analysis.suggestion.4" = "Parallel"; -"ginicapture.analysis.suggestion.4.details" = "Hold the device parallel over the page"; -"ginicapture.analysis.suggestion.5" = "Multiple pages"; -"ginicapture.analysis.suggestion.5.details" = "It’s now possible to analyse an invoice with multiple pages"; -"ginicapture.analysis.section.header" = "Useful Tips"; +"ginicapture.analysis.suggestion.3" = "Hold device parallel over the page"; +"ginicapture.analysis.suggestion.4" = "Capture the entire page within the frame shown on the camera screen"; +"ginicapture.analysis.suggestion.5" = "It's now possible to analyse an invoice with multiple pages"; "ginicapture.analysis.error.actionTitle" = "Retry"; "ginicapture.analysis.error.documentCreation" = "An error occurred while uploading the document."; "ginicapture.analysis.error.analysis" = "An error occurred while parsing the document."; @@ -114,79 +88,41 @@ are also supported"; "ginicapture.help.openWithTutorial.step2.title" = "Select the send function"; "ginicapture.help.openWithTutorial.step2.subTitle" = "To redirect the file to %@, use the \"Share\" function, represented as a square with the arrow pointing up, and choose \"Open in ...\" or \"Share File\". Please select %@ from the list to start the analysis and transfer process."; "ginicapture.help.openWithTutorial.step3.title" = "On iPad you can also use \"Drag-and-drop\""; -"ginicapture.help.openWithTutorial.step3.subTitle" = "If your iPad is running iOS 12 or newer, you can conveniently drag-and-drop PDFs from the iOS Files app to %@ to start a payment. To do this, open %@ and open the Files app as a second app on the screen. Select the desired invoice file in the Files app and drag this over to %@."; +"ginicapture.help.openWithTutorial.step3.subTitle" = "If your iPad is running iOS 11 or newer, you can conveniently drag-and-drop PDFs from the iOS Files app to %@ to start a payment. To do this, open %@ and open the Files app as a second app on the screen. Select the desired invoice file in the Files app and drag this over to %@."; "ginicapture.noresults.warning" = "No information could be extracted. Please take a new photo."; "ginicapture.noresults.collection.header" = "Tips for best results:"; "ginicapture.noresults.gotocamera" = "Back to the camera"; -"ginicapture.noresults.title" = "Tips"; +"ginicapture.noresults.title" = "Tips for photos"; "ginicapture.noresults.warningHelpMenu" = "For best extraction results, please apply the following tips for photos:"; -"ginicapture.help.import.title" = "How to import"; -"ginicapture.help.import.selectInvoice.title" = "Select an invoice"; -"ginicapture.help.import.selectInvoice.desc" = "To do so, please select a PDF invoice from within your email app, PDF viewer or other app on your smartphone. To redirect the file to Your Bank, use the “Share” function, represented as a square with the arrow pointing up."; -"ginicapture.help.import.selectInvoice.accessibility" = "Activate share function. Therefore select file and press share button, represented as a square with the arrow pointing up"; -"ginicapture.help.import.importtoapp.title" = "Import to app"; -"ginicapture.help.import.importtoapp.desc" = "Please select your Banking App from the list to start the analysis and transfer process."; -"ginicapture.help.import.importtoapp.accessibility" = "Select Banking App from the list to start the analysis process"; -"ginicapture.help.import.draganddrop.title" = "Drag and drop on iPad"; -"ginicapture.help.import.draganddrop.desc" = "If your iPad is running iOS 12 or newer, you can -conveniently drag-and-drop PDFs from the iOS Files app to your Banking App to start a payment. To do this, open your Banking App and open the Files app as a second app on the screen. Select the desired invoice file in the Files app and drag this over to your Banking App."; -"ginicapture.help.import.draganddrop.accessibility" = "On tablet you can use the drag and drop function to move the file into the banking app"; - -"ginicapture.help.supportedFormats.title" = "Formats"; -"ginicapture.help.supportedFormats.section.1.title" = "SUPPORTED FORMATS"; +"ginicapture.help.supportedFormats.title" = "Supported formats"; +"ginicapture.help.supportedFormats.section.1.title" = "The following formats are supported:"; "ginicapture.help.supportedFormats.section.1.item.1" = "Computer-generated invoices and remittance slips"; "ginicapture.help.supportedFormats.section.1.item.2" = "1-sided photos of format jpeg, png or gif"; "ginicapture.help.supportedFormats.section.1.item.3" = "PDF documents of up to 10 pages"; "ginicapture.help.supportedFormats.section.1.item.4" = "QR Codes"; -"ginicapture.help.supportedFormats.section.1.item.5" = "Photos of monitors or screens"; -"ginicapture.help.supportedFormats.section.2.title" = "not supported formats"; +"ginicapture.help.supportedFormats.section.2.title" = "What is not supported:"; "ginicapture.help.supportedFormats.section.2.item.1" = "Handwriting"; "ginicapture.help.supportedFormats.section.2.item.2" = "Photos of monitors or screens"; "ginicapture.help.menu.title" = "Help"; -"ginicapture.help.menu.tips" = "Tips for best results from photos"; -"ginicapture.help.menu.formats" = "Supported formats"; -"ginicapture.help.menu.import" = "Import documents from other apps"; +"ginicapture.help.menu.firstItem" = "Tips for best results from photos"; +"ginicapture.help.menu.secondItem" = "Open document from other apps"; +"ginicapture.help.menu.thirdItem" = "Supported formats"; "ginicapture.albums.title" = "Albums"; -"ginicapture.imagepicker.openbutton" = "Done"; +"ginicapture.imagepicker.openbutton" = "Open"; -"ginicapture.multipagereview.title" = "Review"; -"ginicapture.multipagereview.description" = "Make sure the payment details are visible"; -"ginicapture.multipagereview.mainButtonTitle" = "Process"; -"ginicapture.multipagereview.secondaryButtonTitle" = "Pages"; +"ginicapture.multipagereview.reorderContainerTooltipMessage" = "The pages must be in the right order. You can sort them here."; +"ginicapture.multipagereview.addButtonLabel" = "Add a page"; +"ginicapture.multipagereview.dragAndDropTip" = "Drag & Drop to sort"; +"ginicapture.multipagereview.title" = "%d of %d"; "ginicapture.multipagereview.error.retakeAction" = "Retake"; "ginicapture.multipagereview.error.retryAction" = "Retry"; -"ginicapture.noresult.enterManually" = "Enter manually"; -"ginicapture.noresult.retakeImages" = "Retake images"; -"ginicapture.noresult.title" = "No results"; -"ginicapture.noresult.header" = "Could not retrieve any information from your documents."; - -"ginicapture.error.enterManually" = "Enter manually"; -"ginicapture.error.backToCamera" = "Back to camera"; -"ginicapture.error.title" = "Error"; -"ginicapture.error.connection.content" = "Please check your internet connection and try again later on."; -"ginicapture.error.authentication.content" = "Upload failed due to authentication error. Please try again later."; -"ginicapture.error.serverError.content" = "Upload failed due to a server error. Please try again later."; -"ginicapture.error.unexpected.content" = "It seems something went wrong. Please try again later."; -"ginicapture.error.connection.title" = "There was a problem connecting to the internet"; -"ginicapture.error.authentication.title" = "There has been an authentication error"; -"ginicapture.error.serverError.title" = "Service unavailable"; -"ginicapture.error.unexpected.title" = "An unknown error occurred."; -"ginicapture.error.request.content" = "The document couldn’t be accepted. Please check if the image is sharp, the document contains payment information and has the right file type."; -"ginicapture.error.request.title" = "There was a problem with the upload"; - -"ginicapture.images.backToAlbums" = "Albums"; "ginicapture.images.openWithTutorialStep1" = "openWithTutorialStep1_en"; "ginicapture.images.openWithTutorialStep2" = "openWithTutorialStep2_en"; "ginicapture.images.openWithTutorialStep3" = "openWithTutorialStep3_en"; "ginicapture.albums.selectMorePhotosButton" = "Select more photos"; "ginicapture.albums.footer" = "Use \"Select more photos\" button to select more photos or deselect to remove access."; - -"ginicapture.QRscanning.correct" = "QR code detected"; -"ginicapture.QRscanning.incorrect.title" = "Unkown QR code"; -"ginicapture.QRscanning.incorrect.description" = "This code does not contain any payment details. Please use another QR code or take an image of your invoice."; -"ginicapture.QRscanning.loading" = "Retrieving invoice"; diff --git a/Tests/GiniCaptureSDKTests/CameraButtonsViewControllerDelegateMock.swift b/Tests/GiniCaptureSDKTests/CameraButtonsViewControllerDelegateMock.swift new file mode 100644 index 0000000..bc49d14 --- /dev/null +++ b/Tests/GiniCaptureSDKTests/CameraButtonsViewControllerDelegateMock.swift @@ -0,0 +1,20 @@ +// +// CameraButtonsViewControllerDelegateMock.swift +// GiniCapture-Unit-Tests +// +// Created by Enrique del Pozo Gómez on 2/19/19. +// + +import XCTest +@testable import GiniCaptureSDK + +final class CameraButtonViewControllerDelegateMock: CameraButtonsViewControllerDelegate { + + var selectedButton: CameraButtonsViewController.Button? + + func cameraButtons(_ viewController: CameraButtonsViewController, + didTapOn button: CameraButtonsViewController.Button) { + selectedButton = button + } + +} diff --git a/Tests/GiniCaptureSDKTests/CameraButtonsViewControllerTests.swift b/Tests/GiniCaptureSDKTests/CameraButtonsViewControllerTests.swift new file mode 100644 index 0000000..a1aecce --- /dev/null +++ b/Tests/GiniCaptureSDKTests/CameraButtonsViewControllerTests.swift @@ -0,0 +1,299 @@ +// +// CameraButtonsViewControllerTests.swift +// GiniCapture-Unit-Tests +// +// Created by Enrique del Pozo Gómez on 2/19/19. +// + +import XCTest +@testable import GiniCaptureSDK + +//swiftlint:disable type_body_length +final class CameraButtonsViewControllerTests: XCTestCase { + + var cameraButtonsViewController: CameraButtonsViewController! + var giniConfiguration: GiniConfiguration! + var screenAPICoordinator: GiniScreenAPICoordinator! + let visionDelegateMock = GiniCaptureDelegateMock() + let delegateMock = CameraButtonViewControllerDelegateMock() + lazy var imageData: Data = { + let image = GiniCaptureTestsHelper.loadImage(named: "invoice") + let imageData = image.jpegData(compressionQuality: 0.9)! + return imageData + }() + + override func setUp() { + super.setUp() + giniConfiguration = GiniConfiguration.shared + giniConfiguration.multipageEnabled = true + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration) + cameraButtonsViewController.delegate = delegateMock + } + +//TODO: Fix the test capture button is not initialized yet + +// func testCaptureButtonDelegateAction() { +// cameraButtonsViewController.captureButton.simulateEvent(.touchUpInside) +// +// XCTAssertEqual(delegateMock.selectedButton, .capture, "capture button should trigger the delegate method") +// } + + func testImagesStackButtonDelegateAction() { + cameraButtonsViewController.capturedImagesStackView.didTapImageStackButton?() + + XCTAssertEqual(delegateMock.selectedButton, .imagesStack, + "images stack button should trigger the delegate method") + } + + func testImportButtonDelegateAction() { + cameraButtonsViewController.fileImportButtonView.didTapButton?() + + XCTAssertEqual(delegateMock.selectedButton, .fileImport, + "file import button should trigger the delegate method") + } + +//TODO: Fix the test flash button is not initialized yet (lazy and nil) + +// func testFlashToggleButtonDelegateAction() { +// cameraButtonsViewController.flashToggleButton.sendActions(for: .touchUpInside) +// +// XCTAssertEqual(delegateMock.selectedButton, .flashToggle(false), +// "flash toggle button should trigger the delegate method and pass false") +// } + +// func testFlashToggleButtonReactivateDelegateAction() { +// cameraButtonsViewController.flashToggleButton.simulateEvent(.touchUpInside) +// cameraButtonsViewController.flashToggleButton.simulateEvent(.touchUpInside) +// +// XCTAssertEqual(delegateMock.selectedButton, .flashToggle(true), +// "flash toggle button should trigger the delegate method and pass true when tapped twice") +// } + + func testLayoutWhenNoButtonsOnIpad() { + let giniConfiguration = GiniConfiguration() + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IpadDevice()) + _ = cameraButtonsViewController.view + + XCTAssertTrue(cameraButtonsViewController.rightStackView.arrangedSubviews.isEmpty, + "right stack view should not contain views") + XCTAssertTrue(cameraButtonsViewController.rightStackView.arrangedSubviews.isEmpty, + "left stack view should not contain views") + } + + func testLayoutWhenOnlyFileImportEnabledOnIpad() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.fileImportSupportedTypes = .pdf_and_images + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IpadDevice()) + _ = cameraButtonsViewController.view + cameraButtonsViewController.addFileImportButton() + + let innerRightVerticalStackview = cameraButtonsViewController.rightStackView + .arrangedSubviews.first as? UIStackView + let innerLeftVerticalStackview = cameraButtonsViewController.leftStackView + .arrangedSubviews.first as? UIStackView + + XCTAssertNil(innerRightVerticalStackview, "right stack view should not contain an inner stack view") + XCTAssertNotNil(innerLeftVerticalStackview, "left stack view should contain an inner stack view") + XCTAssertTrue(innerLeftVerticalStackview?.arrangedSubviews + .contains(cameraButtonsViewController.fileImportButtonView) ?? false, + "the inner stck view should contain the file import view") + } + + func testLayoutWhenFileImportAndFlashAreEnabledOnIpad() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.fileImportSupportedTypes = .pdf_and_images + giniConfiguration.flashToggleEnabled = true + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IpadDevice()) + _ = cameraButtonsViewController.view + cameraButtonsViewController.addFileImportButton() + + let innerRightVerticalStackview = cameraButtonsViewController.rightStackView + .arrangedSubviews.first as? UIStackView + let innerLeftVerticalStackview = cameraButtonsViewController.leftStackView + .arrangedSubviews.first as? UIStackView + + XCTAssertNotNil(innerRightVerticalStackview, "right stack view should not contain an inner stack view") + XCTAssertNotNil(innerLeftVerticalStackview, "left stack view should contain an inner stack view") + XCTAssertEqual(innerRightVerticalStackview?.arrangedSubviews.count, 1, + "the inner stack view should contain only the flash toggle button") + XCTAssertEqual(innerRightVerticalStackview?.arrangedSubviews[0], + cameraButtonsViewController.flashToggleButtonContainerView, + "the inner stack view should contain the flash toggle button") + XCTAssertEqual(innerLeftVerticalStackview?.arrangedSubviews[0], + cameraButtonsViewController.fileImportButtonView, + "the inner stack view should contain the file import view") + } + + func testLayoutWhenFileImportMultipageAndFlashAreEnabledOnIpad() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.fileImportSupportedTypes = .pdf_and_images + giniConfiguration.flashToggleEnabled = true + giniConfiguration.multipageEnabled = true + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IpadDevice()) + _ = cameraButtonsViewController.view + cameraButtonsViewController.addFileImportButton() + + let innerRightVerticalStackview = cameraButtonsViewController.rightStackView + .arrangedSubviews.first as? UIStackView + let innerLeftVerticalStackview = cameraButtonsViewController.leftStackView + .arrangedSubviews.first as? UIStackView + + XCTAssertNotNil(innerRightVerticalStackview, "right stack view should not contain an inner stack view") + XCTAssertNotNil(innerLeftVerticalStackview, "left stack view should contain an inner stack view") + XCTAssertEqual(innerRightVerticalStackview?.arrangedSubviews[0], + cameraButtonsViewController.capturedImagesStackView, + "the inner stack view should contain the captured images stack button") + XCTAssertEqual(innerRightVerticalStackview?.arrangedSubviews[1], + cameraButtonsViewController.flashToggleButtonContainerView, + "the inner stack view should contain the flash toggle button") + XCTAssertEqual(innerLeftVerticalStackview?.arrangedSubviews[0], + cameraButtonsViewController.fileImportButtonView, + "the inner stack view should contain the file import view") + } + + func testLayoutWhenMultipageAndFlashAreEnabledOnIpad() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.flashToggleEnabled = true + giniConfiguration.multipageEnabled = true + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IpadDevice()) + _ = cameraButtonsViewController.view + + let innerRightVerticalStackview = cameraButtonsViewController.rightStackView + .arrangedSubviews.first as? UIStackView + let innerLeftVerticalStackview = cameraButtonsViewController.leftStackView + .arrangedSubviews.first as? UIStackView + + XCTAssertNotNil(innerRightVerticalStackview, "right stack view should not contain an inner stack view") + XCTAssertNil(innerLeftVerticalStackview, "left stack view should not contain an inner stack view") + XCTAssertEqual(innerRightVerticalStackview?.arrangedSubviews[0], + cameraButtonsViewController.capturedImagesStackView, + "the inner stack view should contain the captured images stack button") + XCTAssertEqual(innerRightVerticalStackview?.arrangedSubviews[1], + cameraButtonsViewController.flashToggleButtonContainerView, + "the inner stack view should contain the flash toggle button") + } + + func testLayoutWhenNoButtonsOnIphone() { + let giniConfiguration = GiniConfiguration() + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IphoneDevice()) + _ = cameraButtonsViewController.view + + XCTAssertTrue(cameraButtonsViewController.rightStackView.arrangedSubviews.isEmpty, + "right stack view should not contain views") + XCTAssertTrue(cameraButtonsViewController.rightStackView.arrangedSubviews.isEmpty, + "left stack view should not contain views") + } + + func testLayoutWhenOnlyFileImportEnabledOnIphone() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.fileImportSupportedTypes = .pdf_and_images + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IphoneDevice()) + _ = cameraButtonsViewController.view + cameraButtonsViewController.addFileImportButton() + + XCTAssertTrue(cameraButtonsViewController.rightStackView.arrangedSubviews.isEmpty, + "right stack view should not contain views") + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews.count, 1, + "the inner stack view should contain only the file import button") + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews[0], + cameraButtonsViewController.fileImportButtonView, + "the inner stack view should contain the file import button") + } + + func testLayoutWhenFileImportAndFlashAreEnabledOnIphone() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.fileImportSupportedTypes = .pdf_and_images + giniConfiguration.flashToggleEnabled = true + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IphoneDevice()) + _ = cameraButtonsViewController.view + cameraButtonsViewController.addFileImportButton() + + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews.count, 1, + "the inner stack view should contain only the file import button") + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews[0], + cameraButtonsViewController.fileImportButtonView, + "the inner stack view should contain the file import button") + XCTAssertEqual(cameraButtonsViewController.rightStackView.arrangedSubviews.count, 1, + "the inner stack view should contain only the flash button") + XCTAssertEqual(cameraButtonsViewController.rightStackView.arrangedSubviews[0], + cameraButtonsViewController.flashToggleButtonContainerView, + "the inner stack view should contain the flash button") + } + + func testLayoutWhenFileImportMultipageAndFlashAreEnabledOnIphone() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.fileImportSupportedTypes = .pdf_and_images + giniConfiguration.flashToggleEnabled = true + giniConfiguration.multipageEnabled = true + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IphoneDevice()) + _ = cameraButtonsViewController.view + cameraButtonsViewController.addFileImportButton() + + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews.count, 2, + "the inner stack view should contain only the file import button") + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews[0], + cameraButtonsViewController.fileImportButtonView, + "the inner stack view should contain the file import button") + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews[1], + cameraButtonsViewController.flashToggleButtonContainerView, + "the inner stack view should contain the flash button") + XCTAssertEqual(cameraButtonsViewController.rightStackView.arrangedSubviews.count, 1, + "the inner stack view should contain only the captured images stack button") + XCTAssertEqual(cameraButtonsViewController.rightStackView.arrangedSubviews[0], + cameraButtonsViewController.capturedImagesStackView, + "the inner stack view should contain the captured images stack button") + } + + func testLayoutWhenMultipageAndFlashAreEnabledOnIphone() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.flashToggleEnabled = true + giniConfiguration.multipageEnabled = true + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IphoneDevice()) + _ = cameraButtonsViewController.view + + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews.count, 1, + "the inner stack view should contain only the file import button") + XCTAssertEqual(cameraButtonsViewController.leftStackView.arrangedSubviews[0], + cameraButtonsViewController.flashToggleButtonContainerView, + "the inner stack view should contain the flash button") + XCTAssertEqual(cameraButtonsViewController.rightStackView.arrangedSubviews.count, 1, + "the inner stack view should contain only the captured images stack button") + XCTAssertEqual(cameraButtonsViewController.rightStackView.arrangedSubviews[0], + cameraButtonsViewController.capturedImagesStackView, + "the inner stack view should contain the captured images stack button") + } + + func testFlashStatusWhenOnByDefault() { + let giniConfiguration = GiniConfiguration() + + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IphoneDevice()) + + XCTAssertTrue(cameraButtonsViewController.flashToggleButton.isSelected, + "the flash toggle should be selected when flash in on by default") + + } + + func testFlashStatusWhenOffByDefault() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.flashOnByDefault = false + + cameraButtonsViewController = CameraButtonsViewController(giniConfiguration: giniConfiguration, + currentDevice: IphoneDevice()) + + XCTAssertFalse(cameraButtonsViewController.flashToggleButton.isSelected, + "the flash toggle should be selected when flash in off by default") + + } + +} diff --git a/Tests/GiniCaptureSDKTests/CameraMock.swift b/Tests/GiniCaptureSDKTests/CameraMock.swift index 47b9bbb..e011392 100644 --- a/Tests/GiniCaptureSDKTests/CameraMock.swift +++ b/Tests/GiniCaptureSDKTests/CameraMock.swift @@ -10,6 +10,7 @@ import AVFoundation @testable import GiniCaptureSDK final class CameraMock: CameraProtocol { + var didDetectInvalidQR: ((GiniCaptureSDK.GiniQRCodeDocument) -> Void)? enum CameraAuthState { @@ -49,7 +50,7 @@ final class CameraMock: CameraProtocol { } func setupQRScanningOutput(completion: @escaping ((GiniCaptureSDK.CameraError?) -> Void)) { - + } func start() { diff --git a/Tests/GiniCaptureSDKTests/CameraViewControllerTests.swift b/Tests/GiniCaptureSDKTests/CameraViewControllerTests.swift new file mode 100644 index 0000000..a476071 --- /dev/null +++ b/Tests/GiniCaptureSDKTests/CameraViewControllerTests.swift @@ -0,0 +1,80 @@ +// +// CameraViewControllerTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 10/5/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import XCTest +import AVFoundation +@testable import GiniCaptureSDK + +final class CameraViewControllerTests: XCTestCase { + + var cameraViewController: CameraViewController! + var giniConfiguration: GiniConfiguration! + var screenAPICoordinator: GiniScreenAPICoordinator! + let visionDelegateMock = GiniCaptureDelegateMock() + lazy var imageData: Data = { + let image = GiniCaptureTestsHelper.loadImage(named: "invoice") + let imageData = image.jpegData(compressionQuality: 0.9)! + return imageData + }() + + override func setUp() { + super.setUp() + giniConfiguration = GiniConfiguration.shared + giniConfiguration.multipageEnabled = true + cameraViewController = CameraViewController(giniConfiguration: giniConfiguration) + screenAPICoordinator = GiniScreenAPICoordinator(withDelegate: visionDelegateMock, + giniConfiguration: self.giniConfiguration) + cameraViewController.delegate = screenAPICoordinator + } + + func testInitialization() { + XCTAssertNotNil(cameraViewController, "view controller should not be nil") + } + + func testTooltipWhenFileImportDisabled() { + ToolTipView.shouldShowFileImportToolTip = true + giniConfiguration.fileImportSupportedTypes = .none + cameraViewController = CameraViewController(giniConfiguration: giniConfiguration) + _ = cameraViewController.view + + XCTAssertNil(cameraViewController.fileImportToolTipView, + "ToolTipView should not be created when file import is disabled.") + } + + func testCaptureButtonDisabledWhenToolTipIsShown() { + ToolTipView.shouldShowFileImportToolTip = true + giniConfiguration.fileImportSupportedTypes = .pdf_and_images + + // Disable onboarding on launch + giniConfiguration.onboardingShowAtLaunch = false + giniConfiguration.onboardingShowAtFirstLaunch = false + cameraViewController = CameraViewController(giniConfiguration: giniConfiguration) + + _ = cameraViewController.view + + XCTAssertFalse(cameraViewController.cameraButtonsViewController.captureButton.isEnabled, + "capture button should be disaled when tooltip is shown") + } + + func testOpaqueViewWhenToolTipIsShown() { + ToolTipView.shouldShowFileImportToolTip = true + GiniConfiguration.shared.fileImportSupportedTypes = .pdf_and_images + GiniConfiguration.shared.toolTipOpaqueBackgroundStyle = .dimmed + + // Disable onboarding on launch + GiniConfiguration.shared.onboardingShowAtLaunch = false + GiniConfiguration.shared.onboardingShowAtFirstLaunch = false + + cameraViewController = CameraViewController.init(giniConfiguration: GiniConfiguration.shared) + _ = cameraViewController.view + + XCTAssertEqual(cameraViewController.opaqueView?.backgroundColor, UIColor.black.withAlphaComponent(0.8)) + } + +} + diff --git a/Tests/GiniCaptureSDKTests/CapturedImagesStackViewTests.swift b/Tests/GiniCaptureSDKTests/CapturedImagesStackViewTests.swift new file mode 100644 index 0000000..ad420ac --- /dev/null +++ b/Tests/GiniCaptureSDKTests/CapturedImagesStackViewTests.swift @@ -0,0 +1,69 @@ +// +// CapturedImagesStackViewTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 5/7/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import XCTest +@testable import GiniCaptureSDK +final class CapturedImagesStackViewTests: XCTestCase { + + var capturedImagesStackView: CapturedImagesStackView! + + override func setUp() { + super.setUp() + capturedImagesStackView = CapturedImagesStackView() + } + + func testCaptureStackWhenNoImages() { + capturedImagesStackView.replaceStackImages(with: []) + + XCTAssertTrue(capturedImagesStackView.isHidden, + "capturedImagesStackView should be hidden when there are no images") + + } + + func testCaptureStackVisibilityWhenOneImageCaptured() { + let images = [GiniCaptureTestsHelper.loadImage(named: "invoice")] + + capturedImagesStackView.replaceStackImages(with: images) + + XCTAssertFalse(capturedImagesStackView.isHidden, + "capturedImagesStackView should not be hidden when it is filled") + XCTAssertTrue(capturedImagesStackView.thumbnailStackBackgroundView.isHidden, + "thumbnailStackBackgroundView should be hidden when there is only 1 image") + + } + + func testCaptureStackVisibilityWhenTwoImageCaptured() { + let images = [GiniCaptureTestsHelper.loadImage(named: "invoice"), + GiniCaptureTestsHelper.loadImage(named: "invoice2")] + capturedImagesStackView.replaceStackImages(with: images) + + XCTAssertFalse(capturedImagesStackView.thumbnailStackBackgroundView.isHidden, + "thumbnailStackBackgroundView should not be hidden when there are 2 images") + + } + + func testCaptureStackWhenTwoImageCaptured() { + let images = [GiniCaptureTestsHelper.loadImage(named: "invoice"), + GiniCaptureTestsHelper.loadImage(named: "invoice2")] + capturedImagesStackView.replaceStackImages(with: images) + + XCTAssertEqual(capturedImagesStackView.thumbnailButton.image(for: .normal), images[1], + "thumbnailButton image should match last image in array") + + } + + func testIndicatorLabelTextColor() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.imagesStackIndicatorLabelTextcolor = .black + let stackView = CapturedImagesStackView(giniConfiguration: giniConfiguration) + + XCTAssertEqual(stackView.stackIndicatorLabel.textColor, giniConfiguration.imagesStackIndicatorLabelTextcolor, + "stack indicator label text color should match the one specified in the configuration") + } + +} diff --git a/Tests/GiniCaptureSDKTests/GalleryCoordinatorTests.swift b/Tests/GiniCaptureSDKTests/GalleryCoordinatorTests.swift index 368eba9..3fd6649 100644 --- a/Tests/GiniCaptureSDKTests/GalleryCoordinatorTests.swift +++ b/Tests/GiniCaptureSDKTests/GalleryCoordinatorTests.swift @@ -41,7 +41,7 @@ final class GalleryCoordinatorTests: XCTestCase { let delegate = GalleryCoordinatorDelegateMock() coordinator.delegate = delegate selectImage(at: IndexPath(row: 0, section: 0), in: galleryManager.albums[2]) { _ in - self.coordinator.cancelAction() + _ = self.coordinator.cancelButton.target?.perform(self.coordinator.cancelButton.action) XCTAssertTrue(delegate.didCancelGallery, "gallery image picking should be cancel after tapping cancel button") @@ -57,7 +57,8 @@ final class GalleryCoordinatorTests: XCTestCase { selectImage(at: IndexPath(row: 0, section: 0), in: galleryManager.albums[2]) { _ in DispatchQueue.main.async { self.selectImage(at: IndexPath(row: 1, section: 0), in: self.galleryManager.albums[2]) { _ in - self.coordinator.openImages() + let innerButton = self.coordinator.openImagesButton.customView as? UIButton + innerButton?.sendActions(for: .touchUpInside) let expect = self.expectation(for: NSPredicate(value: true), evaluatedWith: delegate.didOpenImages, diff --git a/Tests/GiniCaptureSDKTests/GiniCaptureDelegateMock.swift b/Tests/GiniCaptureSDKTests/GiniCaptureDelegateMock.swift index ec36de8..9aba4b2 100644 --- a/Tests/GiniCaptureSDKTests/GiniCaptureDelegateMock.swift +++ b/Tests/GiniCaptureSDKTests/GiniCaptureDelegateMock.swift @@ -9,9 +9,6 @@ import Foundation @testable import GiniCaptureSDK final class GiniCaptureDelegateMock: GiniCaptureDelegate { - func didPressEnterManually() { - } - func didCapture(document: GiniCaptureDocument, networkDelegate: GiniCaptureNetworkDelegate) { } diff --git a/Tests/GiniCaptureSDKTests/GiniCaptureDocumentValidatorTests.swift b/Tests/GiniCaptureSDKTests/GiniCaptureDocumentValidatorTests.swift index 18cb7cc..799e2cc 100644 --- a/Tests/GiniCaptureSDKTests/GiniCaptureDocumentValidatorTests.swift +++ b/Tests/GiniCaptureSDKTests/GiniCaptureDocumentValidatorTests.swift @@ -7,7 +7,6 @@ // import XCTest -import PDFKit @testable import GiniCaptureSDK final class GiniCaptureDocumentValidatorTests: XCTestCase { @@ -16,7 +15,7 @@ final class GiniCaptureDocumentValidatorTests: XCTestCase { func testExcedeedMaxFileSize() { let higherThan10MBData = generateFakeData(megaBytes: 12) - let pdfDocument = GiniPDFDocument(data: higherThan10MBData, fileName: nil) + let pdfDocument = GiniPDFDocument(data: higherThan10MBData) XCTAssertThrowsError(try GiniCaptureDocumentValidator.validate(pdfDocument, withConfig: giniConfiguration), @@ -29,7 +28,7 @@ final class GiniCaptureDocumentValidatorTests: XCTestCase { func testNotExcedeedMaxFileSize() { let lowerThanOrEqualTo10MBData = generateFakeData(megaBytes: 10) - let pdfDocument = GiniPDFDocument(data: lowerThanOrEqualTo10MBData, fileName: nil) + let pdfDocument = GiniPDFDocument(data: lowerThanOrEqualTo10MBData) XCTAssertThrowsError(try GiniCaptureDocumentValidator.validate(pdfDocument, withConfig: giniConfiguration), @@ -49,7 +48,7 @@ final class GiniCaptureDocumentValidatorTests: XCTestCase { } func testEmptyFileValidation() { - let pdfDocument = GiniPDFDocument(data: Data(count: 0), fileName: nil) + let pdfDocument = GiniPDFDocument(data: Data(count: 0)) XCTAssertThrowsError(try GiniCaptureDocumentValidator.validate(pdfDocument, withConfig: giniConfiguration), @@ -59,70 +58,9 @@ final class GiniCaptureDocumentValidatorTests: XCTestCase { } } - func testProtectedPdfFileSize() { - let pdfData = generateSamplePDF() - let documentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false) - let encryptedFileURL = documentDirectory.appendingPathComponent("encrypted_pdf_file") - if let pdfDocument = PDFDocument(data: pdfData) { - // write with password protection - pdfDocument.write( - to: encryptedFileURL, - withOptions: [ - PDFDocumentWriteOption.userPasswordOption : "pwd", - PDFDocumentWriteOption.ownerPasswordOption : "pwd" - ]) - // get encrypted pdf - guard let encryptedPDFDoc = PDFDocument(url: encryptedFileURL) else { - return - } - - XCTAssert(encryptedPDFDoc.isEncrypted == true) - XCTAssert(encryptedPDFDoc.isLocked == true) - - if let data = try? Data(contentsOf: encryptedFileURL) { - let pdfDocument = GiniPDFDocument(data: data, fileName: nil) - XCTAssertThrowsError( - try GiniCaptureDocumentValidator.validate( - pdfDocument, - withConfig: giniConfiguration - ), - "Password protected files should not be valid") { error in - XCTAssert( - error as? DocumentValidationError == .pdfPasswordProtected, - "should indicate that the file is protected") - } - } - } - } - fileprivate func generateFakeData(megaBytes lengthInMB: Int) -> Data { let length = lengthInMB * 1000000 return Data(count: length) } - fileprivate func generateSamplePDF() -> Data { - let pdfMetaData = [ - kCGPDFContextCreator: "Test Builder", - kCGPDFContextAuthor: "Gini" - ] - let format = UIGraphicsPDFRendererFormat() - format.documentInfo = pdfMetaData as [String: Any] - - let pageWidth = 8.5 * 72.0 - let pageHeight = 11 * 72.0 - let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight) - - let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format) - let data = renderer.pdfData { (context) in - context.beginPage() - let attributes = [ - NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 72) - ] - let text = "I'm a PDF!" - text.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes) - } - - return data - } - } diff --git a/Tests/GiniCaptureSDKTests/GiniCaptureFontTests.swift b/Tests/GiniCaptureSDKTests/GiniCaptureFontTests.swift index 45a4018..2e7a483 100644 --- a/Tests/GiniCaptureSDKTests/GiniCaptureFontTests.swift +++ b/Tests/GiniCaptureSDKTests/GiniCaptureFontTests.swift @@ -20,23 +20,31 @@ final class GiniCaptureFontTests: XCTestCase { } func testRegularDynamicFontGeneration() { - let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.regular) - XCTAssertEqual(dynamicFont, font.with(weight: .regular, size: 14, style: .body)) + if #available(iOS 11.0, *) { + let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.regular) + XCTAssertEqual(dynamicFont, font.with(weight: .regular, size: 14, style: .body)) + } } func testBoldDynamicFontGeneration() { - let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.bold) - XCTAssertEqual(dynamicFont, font.with(weight: .bold, size: 14, style: .body)) + if #available(iOS 12.0, *) { + let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.bold) + XCTAssertEqual(dynamicFont, font.with(weight: .bold, size: 14, style: .body)) + } } func testThinDynamicFontGeneration() { - let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.thin) - XCTAssertEqual(dynamicFont, font.with(weight: .thin, size: 14, style: .body)) + if #available(iOS 11.0, *) { + let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.thin) + XCTAssertEqual(dynamicFont, font.with(weight: .thin, size: 14, style: .body)) + } } func testLightDynamicFontGeneration() { - let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.light) - XCTAssertEqual(dynamicFont, font.with(weight: .light, size: 14, style: .body)) + if #available(iOS 11.0, *) { + let dynamicFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: font.light) + XCTAssertEqual(dynamicFont, font.with(weight: .light, size: 14, style: .body)) + } } } diff --git a/Tests/GiniCaptureSDKTests/GiniCaptureTestsHelper.swift b/Tests/GiniCaptureSDKTests/GiniCaptureTestsHelper.swift index 68a4629..2637d7c 100644 --- a/Tests/GiniCaptureSDKTests/GiniCaptureTestsHelper.swift +++ b/Tests/GiniCaptureSDKTests/GiniCaptureTestsHelper.swift @@ -36,13 +36,13 @@ final class GiniCaptureTestsHelper { class func loadPDFDocument(named name: String) -> GiniPDFDocument { let data = fileData(named: name, fileExtension: "pdf")! let builder = GiniCaptureDocumentBuilder(documentSource: .external) - return (builder.build(with: data, fileName: "\(name).pdf") as? GiniPDFDocument)! + return (builder.build(with: data) as? GiniPDFDocument)! } class func loadImageDocument(named name: String, fileExtension: String = "jpg") -> GiniImageDocument { let data = fileData(named: name, fileExtension: fileExtension)! let builder = GiniCaptureDocumentBuilder(documentSource: .external) - return (builder.build(with: data, fileName: "\(name).pdf") as? GiniImageDocument)! + return (builder.build(with: data) as? GiniImageDocument)! } class private func loadPage(named name: String, @@ -50,7 +50,7 @@ final class GiniCaptureTestsHelper { let data = fileData(named: name, fileExtension: fileExtension)! let builder = GiniCaptureDocumentBuilder(documentSource: .external) - return GiniCapturePage(document: builder.build(with: data, fileName: "\(name).pdf")!) + return GiniCapturePage(document: builder.build(with: data)!) } class func loadImagePage(named name: String, fileExtension: String = "jpg") -> GiniCapturePage { diff --git a/Tests/GiniCaptureSDKTests/GiniQRCodeDocumentTests.swift b/Tests/GiniCaptureSDKTests/GiniQRCodeDocumentTests.swift index 735a2c6..0e2ed72 100644 --- a/Tests/GiniCaptureSDKTests/GiniQRCodeDocumentTests.swift +++ b/Tests/GiniCaptureSDKTests/GiniQRCodeDocumentTests.swift @@ -49,6 +49,24 @@ final class GiniQRCodeDocumentTests: XCTestCase { "bic should match") } + + func testEPC06912QRCodeWithDoubleNewLineExtractions() { + let scannedString = "BCD\r\n001\r\n1\r\nSCT\r\nGENODEF1AB1\r\r\nADJULEX Rechtsanwaelte Feldmann, Klug & Partner\r\r\nDE72795625140001046462\r\r\nEUR54.15\r\n\r\n3372/12 RgNr.: 2201207\r\n" + let qrDocument = GiniQRCodeDocument(scannedString: scannedString) + XCTAssertNoThrow(try GiniCaptureDocumentValidator.validate(qrDocument, + withConfig: giniConfiguration), + "should throw an error since is valid") + XCTAssertEqual(qrDocument.extractedParameters["amountToPay"], "54.15:EUR", + "amountToPay should match") + XCTAssertEqual(qrDocument.extractedParameters["paymentRecipient"], "ADJULEX Rechtsanwaelte Feldmann, Klug & Partner", + "paymentRecipient should match") + XCTAssertEqual(qrDocument.extractedParameters["paymentReference"], "3372/12 RgNr.: 2201207", + "paymentReference should match") + XCTAssertEqual(qrDocument.extractedParameters["iban"], "DE72795625140001046462", + "iban should match") + XCTAssertEqual(qrDocument.extractedParameters["bic"], "GENODEF1AB1", + "bic should match") + } func testNotValidQRCodeFormat() { let qrDocument = GiniQRCodeDocument(scannedString: "invalidQRCodeFormat") @@ -74,4 +92,12 @@ final class GiniQRCodeDocumentTests: XCTestCase { withConfig: giniConfiguration), "validation should throw a DocumentaValidationError") } + + func testValidStuzzaQR(){ + let scannedString = "BCD\n001\n1\nSCT\nABCDATWW\nExample with fictive data\nAT611904300234573201\nEUR24.2" + let qrDocument = GiniQRCodeDocument(scannedString: scannedString) + XCTAssertNoThrow(try GiniCaptureDocumentValidator.validate(qrDocument, + withConfig: giniConfiguration), + "should not throw an error since qr code is valid") + } } diff --git a/Tests/GiniCaptureSDKTests/GiniScreenAPICoordinatorTests.swift b/Tests/GiniCaptureSDKTests/GiniScreenAPICoordinatorTests.swift index 7f881fb..c64383e 100644 --- a/Tests/GiniCaptureSDKTests/GiniScreenAPICoordinatorTests.swift +++ b/Tests/GiniCaptureSDKTests/GiniScreenAPICoordinatorTests.swift @@ -25,16 +25,16 @@ final class GiniScreenAPICoordinatorTests: XCTestCase { let rootViewController = coordinator.start(withDocuments: nil) _ = rootViewController.view let screenNavigator = rootViewController.children.first as? UINavigationController - XCTAssertEqual(screenNavigator?.viewControllers.count, 2, - "there should be two view controllers in the nav stack, Review and Camera screens") + XCTAssertEqual(screenNavigator?.viewControllers.count, 1, + "there should be only one view controller in the nav stack") } func testNavControllerTypesAfterStartWithoutDocuments() { let rootViewController = coordinator.start(withDocuments: nil) _ = rootViewController.view let screenNavigator = rootViewController.children.first as? UINavigationController - XCTAssertNotNil(screenNavigator?.viewControllers.first as? ReviewViewController, - "first view controller is not a ReviewViewController") + XCTAssertNotNil(screenNavigator?.viewControllers.first as? CameraViewController, + "first view controller is not a CameraViewController") } func testNavControllerCountAfterStartWithImages() { @@ -44,26 +44,22 @@ final class GiniScreenAPICoordinatorTests: XCTestCase { let rootViewController = coordinator.start(withDocuments: capturedImages) _ = rootViewController.view let screenNavigator = rootViewController.children.first as? UINavigationController - XCTAssertEqual(screenNavigator?.viewControllers.count, 1, + XCTAssertEqual(screenNavigator?.viewControllers.count, 2, "there should be 2 view controllers in the nav stack") } func testNavControllerTypesAfterStartWithImages() { - let document1 = GiniCaptureTestsHelper.loadImageDocument(named: "invoice") - document1.isImported = false - let document2 = GiniCaptureTestsHelper.loadImageDocument(named: "invoice2") - document2.isImported = false - let capturedImages = [document1, document2] + let capturedImages = [GiniCaptureTestsHelper.loadImageDocument(named: "invoice"), + GiniCaptureTestsHelper.loadImageDocument(named: "invoice2")] let rootViewController = coordinator.start(withDocuments: capturedImages) _ = rootViewController.view let screenNavigator = rootViewController.children.first as? UINavigationController - XCTAssertNotNil(screenNavigator?.viewControllers.last as? ReviewViewController, - "first view controller is not a ReviewViewController") - XCTAssertEqual(screenNavigator?.viewControllers.count, 1, - "there should be only one view controller in the nav stack") - + XCTAssertNotNil(screenNavigator?.viewControllers.first as? CameraViewController, + "first view controller is not a CameraViewController") + XCTAssertNotNil(screenNavigator?.viewControllers.last as? MultipageReviewViewController, + "last view controller is not a MultipageReviewController") } func testNavControllerCountAfterStartWithAPDF() { @@ -89,84 +85,55 @@ final class GiniScreenAPICoordinatorTests: XCTestCase { func testNavControllerTypesAfterStartWithImageAndMultipageDisabled() { giniConfiguration.multipageEnabled = false - let document = GiniCaptureTestsHelper.loadImageDocument(named: "invoice") - document.isImported = false - let capturedImages = [document] + let capturedImages = [GiniCaptureTestsHelper.loadImageDocument(named: "invoice")] let rootViewController = coordinator.start(withDocuments: capturedImages) _ = rootViewController.view let screenNavigator = rootViewController.children.first as? UINavigationController + XCTAssertNotNil(screenNavigator?.viewControllers.last as? ReviewViewController, "first view controller is not a ReviewViewController") } + + func testDocumentCollectionAfterRotateImageInMultipage() { + let capturedImageDocument = GiniCaptureTestsHelper.loadImagePage(named: "invoice") + coordinator.addToDocuments(new: [capturedImageDocument]) + + (coordinator.multiPageReviewViewController + .pages[0] + .document as? GiniImageDocument)? + .rotatePreviewImage90Degrees() + coordinator.multipageReview(coordinator.multiPageReviewViewController, + didRotate: coordinator.multiPageReviewViewController.pages[0]) + let imageDocument = coordinator.pages[0].document as? GiniImageDocument + XCTAssertEqual(imageDocument?.rotationDelta, 90, + "the image document rotation delta should have been updated after rotation") + } + func testDocumentCollectionAfterRemoveImageInMultipage() { let capturedImageDocument = GiniCaptureTestsHelper.loadImagePage(named: "invoice") coordinator.addToDocuments(new: [capturedImageDocument]) - coordinator.review(coordinator.reviewViewController, - didDelete: coordinator.reviewViewController.pages[0]) + coordinator.multipageReview(coordinator.multiPageReviewViewController, + didDelete: coordinator.multiPageReviewViewController.pages[0]) XCTAssertTrue(coordinator.pages.isEmpty, "vision documents collection should be empty after delete " + "the image in the multipage review view controller") } - func testErrorTypeNoResponse() { - giniConfiguration.multipageEnabled = false - let capturedImages = [GiniCaptureTestsHelper.loadImageDocument(named: "invoice")] - - let rootViewController = coordinator.start(withDocuments: capturedImages) - _ = rootViewController.view - let errorType = ErrorType(error: .noResponse) - coordinator.displayError(errorType: errorType, animated: false) - let screenNavigator = rootViewController.children.first as? UINavigationController - let errorScreen = screenNavigator?.viewControllers.last as? ErrorScreenViewController - errorScreen?.setupView() - XCTAssertNotNil( - errorScreen, - "first view controller is not a ErrorScreenViewController") - XCTAssertTrue(errorScreen?.errorHeader.headerLabel.text == ErrorType.connection.title(), "Error title should match no response error type") - XCTAssertTrue(errorScreen?.errorContent.text == ErrorType.connection.content(), "Error content should match no response error type") + func testMultipageImageDocumentWhenSortingDocuments() { + let capturedImageDocument = [GiniCaptureTestsHelper.loadImagePage(named: "invoice"), + GiniCaptureTestsHelper.loadImagePage(named: "invoice")] + let firstItemId = capturedImageDocument.first?.document.id + coordinator.addToDocuments(new: capturedImageDocument) - } - - func testErrorTooManyRequests() { - giniConfiguration.multipageEnabled = false - let capturedImages = [GiniCaptureTestsHelper.loadImageDocument(named: "invoice")] - - let rootViewController = coordinator.start(withDocuments: capturedImages) - _ = rootViewController.view - let response = HTTPURLResponse(url: URL(string: "example")!, statusCode: 429, httpVersion: "", headerFields: [:]) - let errorType = ErrorType(error: .tooManyRequests(response: response, data: Data())) - coordinator.displayError(errorType: errorType, animated: false) - let screenNavigator = rootViewController.children.first as? UINavigationController - let errorScreen = screenNavigator?.viewControllers.last as? ErrorScreenViewController - errorScreen?.setupView() - XCTAssertNotNil( - errorScreen, - "first view controller is not a ErrorScreenViewController") - XCTAssertTrue(errorScreen?.errorHeader.headerLabel.text == ErrorType.request.title(), "Error title should match no response error type") - XCTAssertTrue(errorScreen?.errorContent.text == ErrorType.request.content(), "Error content should match no response error type") + var reorderedItems = capturedImageDocument + reorderedItems.swapAt(0, 1) - } - - func testErrorServerError() { - giniConfiguration.multipageEnabled = false - let capturedImages = [GiniCaptureTestsHelper.loadImageDocument(named: "invoice")] - - let rootViewController = coordinator.start(withDocuments: capturedImages) - _ = rootViewController.view - let response = HTTPURLResponse(url: URL(string: "example")!, statusCode: 501, httpVersion: "", headerFields: [:]) - let errorType = ErrorType(error: .notAcceptable(response: response, data: Data())) - coordinator.displayError(errorType: errorType, animated: false) - let screenNavigator = rootViewController.children.first as? UINavigationController - let errorScreen = screenNavigator?.viewControllers.last as? ErrorScreenViewController - errorScreen?.setupView() - XCTAssertNotNil( - errorScreen, - "first view controller is not a ErrorScreenViewController") - XCTAssertTrue(errorScreen?.errorHeader.headerLabel.text == ErrorType.serverError.title(), "Error title should match server error type") - XCTAssertTrue(errorScreen?.errorContent.text == ErrorType.serverError.content(), "Error content should match server error type") + coordinator.multipageReview(coordinator.multiPageReviewViewController, didReorder: reorderedItems) + + XCTAssertTrue(coordinator.pages.last?.document.id == firstItemId, "last items should be the one moved") } } diff --git a/Tests/GiniCaptureSDKTests/HelpMenuViewControllerTests.swift b/Tests/GiniCaptureSDKTests/HelpMenuViewControllerTests.swift index 962f011..90b6d05 100644 --- a/Tests/GiniCaptureSDKTests/HelpMenuViewControllerTests.swift +++ b/Tests/GiniCaptureSDKTests/HelpMenuViewControllerTests.swift @@ -59,7 +59,7 @@ final class HelpMenuViewControllerTests: XCTestCase { _ = helpMenuViewController.view let itemsCount = items.count - let tableRowsCount = helpMenuViewController.dataSource.items.count + let tableRowsCount = helpMenuViewController.menuItems.count XCTAssertEqual(itemsCount, tableRowsCount, "items count should be equal to the datasource items count") } @@ -71,7 +71,7 @@ final class HelpMenuViewControllerTests: XCTestCase { _ = helpMenuViewController.view let itemsCount = items.count - let tableRowsCount = helpMenuViewController.dataSource.items.count + let tableRowsCount = helpMenuViewController.menuItems.count XCTAssertEqual(itemsCount, tableRowsCount, "items count should be equal to the datasource items count") } @@ -83,19 +83,19 @@ final class HelpMenuViewControllerTests: XCTestCase { _ = helpMenuViewController.view let itemsCount = items.count - let tableRowsCount = helpMenuViewController.dataSource.items.count + let tableRowsCount = helpMenuViewController.menuItems.count XCTAssertEqual(itemsCount, tableRowsCount, "items count should be equal to the datasource items count") } func testCellContent() { let indexPath = IndexPath(row: 0, section: 0) - let itemText = helpMenuViewController.dataSource.items[indexPath.row].title + let itemText = helpMenuViewController.menuItems[indexPath.row].title let cellAccesoryType = UITableViewCell.AccessoryType.disclosureIndicator - let cell = helpMenuViewController.dataSource.tableView(helpMenuViewController.tableView, cellForRowAt: indexPath) as! HelpMenuCell + let cell = helpMenuViewController.tableView(helpMenuViewController.tableView, cellForRowAt: indexPath) - XCTAssertEqual(itemText, cell.titleLabel.text, + XCTAssertEqual(itemText, cell.textLabel?.text, "cell text in the first row should be the same as the first item text") XCTAssertEqual(cellAccesoryType, @@ -103,4 +103,10 @@ final class HelpMenuViewControllerTests: XCTestCase { "cell accesory type should be and a disclosure indicator") } + func testTableRowheight() { + let tableRowHeight = helpMenuViewController.tableView.rowHeight + + XCTAssertEqual(tableRowHeight, helpMenuViewController.tableRowHeight, "table row height should match") + } + } diff --git a/Tests/GiniCaptureSDKTests/ImagePickerCollectionViewCellTests.swift b/Tests/GiniCaptureSDKTests/ImagePickerCollectionViewCellTests.swift new file mode 100644 index 0000000..f52586f --- /dev/null +++ b/Tests/GiniCaptureSDKTests/ImagePickerCollectionViewCellTests.swift @@ -0,0 +1,36 @@ +// +// ImagePickerCollectionViewCellTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 5/24/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import XCTest +@testable import GiniCaptureSDK +final class ImagePickerCollectionViewCellTests: XCTestCase { + + let cell = ImagePickerCollectionViewCell(frame: .zero) + + func testCellCheckIndicatorBackgroundOnSelected() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.galleryPickerItemSelectedBackgroundCheckColor = .black + + cell.changeCheckCircle(to: true, giniConfiguration: giniConfiguration) + + XCTAssertEqual(cell.checkImage.alpha, 1, "check image should be visible when cell is selected") + XCTAssertEqual(cell.checkCircleBackground.backgroundColor, + giniConfiguration.galleryPickerItemSelectedBackgroundCheckColor, + "check circle background should match the one specified in the GiniConfiguration") + } + + func testCellCheckIndicatorBackgroundOnDeselected() { + let giniConfiguration = GiniConfiguration() + + cell.changeCheckCircle(to: false, giniConfiguration: giniConfiguration) + XCTAssertEqual(cell.checkImage.alpha, 0, "check image should not be visible when cell is selected") + XCTAssertEqual(cell.checkCircleBackground.backgroundColor, + UIColor.clear, "check circle background should be transparent when cell is not selected") + + } +} diff --git a/Tests/GiniCaptureSDKTests/MultipageCollectionCellPresenterTests.swift b/Tests/GiniCaptureSDKTests/MultipageCollectionCellPresenterTests.swift new file mode 100644 index 0000000..6f81fc5 --- /dev/null +++ b/Tests/GiniCaptureSDKTests/MultipageCollectionCellPresenterTests.swift @@ -0,0 +1,96 @@ +// +// MultipageCollectionCellPresenterTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 6/4/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import XCTest +@testable import GiniCaptureSDK +final class MultipageCollectionCellPresenterTests: XCTestCase { + + var presenter: MultipageReviewCollectionCellPresenter! + var giniConfiguration: GiniConfiguration! + var testPage = GiniCaptureTestsHelper.loadImagePage(named: "invoice") + var setUpPageCollectionCell: MultipageReviewPagesCollectionCell { + let cell = presenter + .setUp(.pages(MultipageReviewPagesCollectionCell(frame: .zero)), + with: testPage, + isSelected: true, + at: IndexPath(row: 0, section: 0)) as? MultipageReviewPagesCollectionCell + return cell! + } + + var setUpMainCollectionCell: MultipageReviewMainCollectionCell { + let cell = presenter + .setUp(.main(MultipageReviewMainCollectionCell(frame: .zero), { _ in}), + with: testPage, + isSelected: true, + at: IndexPath(row: 0, section: 0)) as? MultipageReviewMainCollectionCell + return cell! + } + + override func setUp() { + super.setUp() + giniConfiguration = GiniConfiguration() + presenter = MultipageReviewCollectionCellPresenter(giniConfiguration: giniConfiguration) + } + + func testPageIndicatorLabel() { + giniConfiguration.multipagePageIndicatorColor = .black + XCTAssertEqual(setUpPageCollectionCell.pageIndicatorLabel.textColor, + giniConfiguration.multipagePageIndicatorColor, + "page cell indicator color should match the one specified in the configuration") + } + + func testPageBottomContainerColor() { + giniConfiguration.multipagePageBackgroundColor = GiniColor.init(lightModeColor: .red, darkModeColor: .red) + XCTAssertEqual(setUpPageCollectionCell.bottomContainer.backgroundColor?.cgColor, + UIColor.from(giniColor: giniConfiguration.multipagePageBackgroundColor).cgColor, + "page cell background color should match the one specified in the configuration") + } + + func testPageSelectedIndicatorColor() { + giniConfiguration.multipagePageSelectedIndicatorColor = .red + + XCTAssertEqual(setUpPageCollectionCell.pageSelectedLine.backgroundColor, + giniConfiguration.multipagePageSelectedIndicatorColor, + "selected line indicator background color should match the one specified in the configuration") + } + + func testPageDraggableIconColor() { + giniConfiguration.multipageDraggableIconColor = .red + + XCTAssertEqual(setUpPageCollectionCell.draggableIcon.tintColor, + giniConfiguration.multipageDraggableIconColor, + "page draggable icon tint color should match the one specified in the configuration") + } + + func testPagesCollectionCellImage() { + presenter.thumbnails[testPage.document.id, default: [:]][.small] = testPage.document.previewImage + + XCTAssertEqual(setUpPageCollectionCell.documentImage.image, testPage.document.previewImage, + "Pages collection cells image content mode should match the one passed in the initializer") + } + + func testPagesCollectionCellImageContentMode() { + presenter.thumbnails[testPage.document.id, default: [:]][.small] = testPage.document.previewImage + + XCTAssertEqual(setUpPageCollectionCell.documentImage.contentMode, UIView.ContentMode.scaleAspectFill, + "Pages collection cells image content mode should match the one passed in the initializer") + } + + func testMainCollectionCellImage() { + presenter.thumbnails[testPage.document.id, default: [:]][.big] = testPage.document.previewImage + + XCTAssertEqual(setUpMainCollectionCell.documentImage.image, testPage.document.previewImage, + "Pages collection cells image content mode should match the one passed in the initializer") + } + + func testMainCollectionCellImageContentMode() { + XCTAssertEqual(setUpMainCollectionCell.documentImage.contentMode, UIView.ContentMode.scaleAspectFit, + "Main collection cells image content mode should match the one passed in the initializer") + } + +} diff --git a/Tests/GiniCaptureSDKTests/MultipageReviewVCDelegateMock.swift b/Tests/GiniCaptureSDKTests/MultipageReviewVCDelegateMock.swift new file mode 100644 index 0000000..d39d1ad --- /dev/null +++ b/Tests/GiniCaptureSDKTests/MultipageReviewVCDelegateMock.swift @@ -0,0 +1,34 @@ +// +// MultipageReviewViewControllerDelegateMock.swift +// GiniCapture_Example +// +// Created by Enrique del Pozo Gómez on 3/26/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import Foundation +@testable import GiniCaptureSDK +final class MultipageReviewVCDelegateMock: MultipageReviewViewControllerDelegate { + func multipageReview(_ viewController: MultipageReviewViewController, + didTapRetryUploadFor page: GiniCapturePage) { + + } + + func multipageReviewDidTapAddImage(_ controller: MultipageReviewViewController) { + + } + + var updatedDocuments: [GiniCapturePage] = [] + + func multipageReview(_ controller: MultipageReviewViewController, didReorder pages: [GiniCapturePage]) { + updatedDocuments = pages + } + + func multipageReview(_ controller: MultipageReviewViewController, didDelete pages: GiniCapturePage) { + + } + + func multipageReview(_ controller: MultipageReviewViewController, didRotate pages: GiniCapturePage) { + + } +} diff --git a/Tests/GiniCaptureSDKTests/MultipageReviewViewControllerTests.swift b/Tests/GiniCaptureSDKTests/MultipageReviewViewControllerTests.swift new file mode 100644 index 0000000..013b5b2 --- /dev/null +++ b/Tests/GiniCaptureSDKTests/MultipageReviewViewControllerTests.swift @@ -0,0 +1,284 @@ +// +// MultipageReviewViewControllerTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 1/30/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import XCTest +@testable import GiniCaptureSDK +final class MultipageReviewViewControllerTests: XCTestCase { + + let giniConfiguration = GiniConfiguration.shared + lazy var multipageReviewViewController: MultipageReviewViewController = { + let vc = MultipageReviewViewController(pages: self.imagePages, + giniConfiguration: self.giniConfiguration) + _ = vc.view + return vc + }() + + var imagePages: [GiniCapturePage] = [ + GiniCaptureTestsHelper.loadImagePage(named: "invoice"), + GiniCaptureTestsHelper.loadImagePage(named: "invoice2"), + GiniCaptureTestsHelper.loadImagePage(named: "invoice3") + ] + + func testCollectionsItemsCount() { + XCTAssertEqual(multipageReviewViewController.mainCollection.numberOfItems(inSection: 0), 3, + "main collection items count should be 3") + + XCTAssertEqual(multipageReviewViewController.pagesCollection.numberOfItems(inSection: 0), 3, + "pages collection items count should be 3") + } + + func testMainCollectionCellSize() { + multipageReviewViewController.view.setNeedsLayout() + multipageReviewViewController.view.layoutIfNeeded() + + let firstCellIndexPath = IndexPath(row: 0, section: 0) + let cellSize = multipageReviewViewController.collectionView(multipageReviewViewController.mainCollection, + layout: multipageReviewViewController.mainCollection.collectionViewLayout, + sizeForItemAt: firstCellIndexPath) + + XCTAssertEqual(cellSize, multipageReviewViewController.mainCollection.frame.size, + "First cell image should match the one passed in the initializer") + } + + func testMainCollectionInsets() { + let collectionInsets = multipageReviewViewController + .collectionView(multipageReviewViewController.mainCollection, + layout: multipageReviewViewController.mainCollection.collectionViewLayout, + insetForSectionAt: 0) + + XCTAssertEqual(collectionInsets, .zero, + "Main collection insets should be zero") + } + + func testPagesCollectionCellsIndex() { + let firstCell = multipageReviewViewController + .collectionView(multipageReviewViewController.pagesCollection, + cellForItemAt: IndexPath(row: 0, section: 0)) as? MultipageReviewPagesCollectionCell + let secondCell = multipageReviewViewController + .collectionView(multipageReviewViewController.pagesCollection, + cellForItemAt: IndexPath(row: 1, section: 0)) as? MultipageReviewPagesCollectionCell + let thirdCell = multipageReviewViewController + .collectionView(multipageReviewViewController.pagesCollection, + cellForItemAt: IndexPath(row: 2, section: 0)) as? MultipageReviewPagesCollectionCell + XCTAssertEqual(firstCell?.pageIndicatorLabel.text, "1", + "First cell indicator should match its position") + XCTAssertEqual(secondCell?.pageIndicatorLabel.text, "2", + "Second cell indicator should match its position") + XCTAssertEqual(thirdCell?.pageIndicatorLabel.text, "3", + "Third cell indicator should match its position") + } + + func testPagesCollectionCellSize() { + multipageReviewViewController.view.setNeedsLayout() + multipageReviewViewController.view.layoutIfNeeded() + + let firstCellIndexPath = IndexPath(row: 0, section: 0) + let cellSize = multipageReviewViewController.collectionView(multipageReviewViewController.pagesCollection, + layout: multipageReviewViewController.pagesCollection.collectionViewLayout, + sizeForItemAt: firstCellIndexPath) + + let size = MultipageReviewPagesCollectionCell.size(in: multipageReviewViewController.pagesCollection) + + XCTAssertEqual(cellSize, size, + "Pages collection cells should have the value declared in the class") + } + + func testPagesCollectionInsets() { + let collectionInsets = multipageReviewViewController + .collectionView(multipageReviewViewController.pagesCollection, + layout: multipageReviewViewController.pagesCollection.collectionViewLayout, + insetForSectionAt: 0) + + XCTAssertEqual(collectionInsets, multipageReviewViewController.pagesCollectionInsets, + "Main collection insets should be zero") + } + + func testToolBarItemsOnInitialization() { + guard let items = self.multipageReviewViewController.toolBar.items else { + assertionFailure("MultipageReviewViewController toolbar items are nil") + return + } + XCTAssertEqual(items[1], self.multipageReviewViewController.rotateButton, + "First toolbar item should be the rotateButton") + XCTAssertEqual(items[4], self.multipageReviewViewController.deleteButton, + "Fifth toolbar item should be the deleteButton") + } + + func testToolBarTintColor() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.multipagePagesContainerAndToolBarColor = GiniColor(lightModeColor: .black, darkModeColor: .black) + let multipageReviewViewController = MultipageReviewViewController(pages: [], + giniConfiguration: giniConfiguration) + + XCTAssertEqual(multipageReviewViewController.toolBar.barTintColor?.cgColor, + UIColor.from(giniColor: giniConfiguration.multipagePagesContainerAndToolBarColor).cgColor, + "toolbar tint color should match the one specified in the configuration") + } + + func testPagesContainerBackgroundColor() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.multipagePagesContainerAndToolBarColor = GiniColor(lightModeColor: .black, darkModeColor: .black) + let multipageReviewViewController = MultipageReviewViewController(pages: [], + giniConfiguration: giniConfiguration) + + XCTAssertEqual(multipageReviewViewController.pagesCollectionContainer.backgroundColor?.cgColor, + UIColor.from(giniColor: giniConfiguration.multipagePagesContainerAndToolBarColor).cgColor, + "pages container background color should match the one specified in the gini configuration") + } + + func testToolbarItemsColor() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.multipageToolbarItemsColor = .black + let multipageReviewViewController = MultipageReviewViewController(pages: [], + giniConfiguration: giniConfiguration) + + _ = multipageReviewViewController.view + XCTAssertEqual((multipageReviewViewController.deleteButton.customView as? UIButton)?.tintColor, + giniConfiguration.multipageToolbarItemsColor, + "delete button tint color should match the one specified in the gini configuration") + XCTAssertEqual((multipageReviewViewController.rotateButton.customView as? UIButton)?.tintColor, + giniConfiguration.multipageToolbarItemsColor, + "rotate button tint color should match the one specified in the gini configuration") + } + +// MARK: - Fix the test with tap event simulation + +// func testDatasourceOnDelete() { +// let vc = MultipageReviewViewController(pages: imagePages, +// giniConfiguration: giniConfiguration) +// _ = vc.view +// vc.view.setNeedsLayout() +// vc.view.layoutIfNeeded() +// if let button = (vc.deleteButton.customView as? UIButton){ +// button.simulateEvent(.touchUpInside) +// } +// +// +// //(vc.deleteButton.customView as? UIButton)?.sendActions(for: .touchUpInside) +// +// XCTAssertEqual(vc.mainCollection.numberOfItems(inSection: 0), 2, +// "main collection items count should be 2") +// XCTAssertEqual(vc.pagesCollection.numberOfItems(inSection: 0), 2, +// "pages collection items count should be 2") +// } + + func testDeleteButtonDisabledWhenToolTipIsShown() { + ToolTipView.shouldShowReorderPagesButtonToolTip = true + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + multipageReviewViewController.viewDidAppear(false) + + XCTAssertFalse(multipageReviewViewController.deleteButton.isEnabled, + "delete button should be disabled when tooltip is shown") + + } + + func testDeleteButtonEnabledWhenToolTipIsNotShown() { + ToolTipView.shouldShowReorderPagesButtonToolTip = false + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + multipageReviewViewController.viewDidAppear(false) + + XCTAssertTrue(multipageReviewViewController.deleteButton.isEnabled, + "delete button should be disabled when tooltip is shown") + + } + + func testRotateButtonDisabledWhenToolTipIsShown() { + ToolTipView.shouldShowReorderPagesButtonToolTip = true + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + multipageReviewViewController.viewDidAppear(false) + + XCTAssertFalse(multipageReviewViewController.rotateButton.isEnabled, + "rotate button should be disabled when tooltip is shown") + + } + + func testRotateButtonEnabledWhenToolTipIsNotShown() { + ToolTipView.shouldShowReorderPagesButtonToolTip = false + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + multipageReviewViewController.viewDidAppear(false) + + XCTAssertTrue(multipageReviewViewController.rotateButton.isEnabled, + "rotate button should be disabled when tooltip is shown") + + } + + func testToolTipShouldAppearTheFirstTime() { + ToolTipView.shouldShowReorderPagesButtonToolTip = true + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + + XCTAssertNotNil(multipageReviewViewController.reorderContainerTooltipView, + "rotate button should be disabled when tooltip is shown") + + } + + func testToolTipShouldNotAppearWhenItWasShownBefore() { + ToolTipView.shouldShowReorderPagesButtonToolTip = false + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + + XCTAssertNil(multipageReviewViewController.reorderContainerTooltipView, + "rotate button should be disabled when tooltip is shown") + + } + + func testSelectingItemAtNegativePositionSelectsFirstItem() { + ToolTipView.shouldShowReorderPagesButtonToolTip = false + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + + multipageReviewViewController.selectItem(at: -1) + + XCTAssertEqual(multipageReviewViewController.pagesCollection.indexPathsForSelectedItems?[0].row, 0, + "pages collection selected item position should be 0") + } + + func testSelectingItemAtOutOfUpperBoundPositionSelectsFirstItem() { + ToolTipView.shouldShowReorderPagesButtonToolTip = false + + multipageReviewViewController = MultipageReviewViewController(pages: imagePages, + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + + multipageReviewViewController.selectItem(at: imagePages.count) + + XCTAssertEqual(multipageReviewViewController.pagesCollection.indexPathsForSelectedItems?[0].row, 0, + "pages collection selected item position should be 0") + } + + func testSelectingItemDoesNothingIfPagesIsEmpty() { + ToolTipView.shouldShowReorderPagesButtonToolTip = false + + multipageReviewViewController = MultipageReviewViewController(pages: [], + giniConfiguration: giniConfiguration) + _ = multipageReviewViewController.view + + multipageReviewViewController.selectItem(at: 1) + + XCTAssertEqual(multipageReviewViewController.pagesCollection.indexPathsForSelectedItems?.count, 0, + "pages collection has no selected items") + } +} diff --git a/Tests/GiniCaptureSDKTests/OnboardingViewControllerTests.swift b/Tests/GiniCaptureSDKTests/OnboardingViewControllerTests.swift index e1ce3a4..f58e3cb 100644 --- a/Tests/GiniCaptureSDKTests/OnboardingViewControllerTests.swift +++ b/Tests/GiniCaptureSDKTests/OnboardingViewControllerTests.swift @@ -8,21 +8,21 @@ import XCTest @testable import GiniCaptureSDK -//final class OnboardingViewControllerTests: XCTestCase { -// -// var vc: OnboardingViewController! -// -// override func setUp() { -// super.setUp() -// vc = OnboardingViewController(scrollViewDelegate: nil) -// } -// -// func testConvenientInitialization() { -// XCTAssertNotNil(vc, "view controller should not be nil") -// XCTAssert(vc.pages == GiniConfiguration.shared.onboardingPages, "default pages should be set") -// } -// -// func testScrollViewAccessibility() { -// XCTAssertNotNil(vc.scrollView, "scroll view should be accessible and not nil") -// } -//} +final class OnboardingViewControllerTests: XCTestCase { + + var vc: OnboardingViewController! + + override func setUp() { + super.setUp() + vc = OnboardingViewController(scrollViewDelegate: nil) + } + + func testConvenientInitialization() { + XCTAssertNotNil(vc, "view controller should not be nil") + XCTAssert(vc.pages == GiniConfiguration.shared.onboardingPages, "default pages should be set") + } + + func testScrollViewAccessibility() { + XCTAssertNotNil(vc.scrollView, "scroll view should be accessible and not nil") + } +} diff --git a/Tests/GiniCaptureSDKTests/OpaqueViewTests.swift b/Tests/GiniCaptureSDKTests/OpaqueViewTests.swift new file mode 100644 index 0000000..ae00b47 --- /dev/null +++ b/Tests/GiniCaptureSDKTests/OpaqueViewTests.swift @@ -0,0 +1,27 @@ +// +// OpaqueViewFactoryTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 6/13/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import XCTest +@testable import GiniCaptureSDK +final class OpaqueViewFactoryTests: XCTestCase { + + func testBlurStyle() { + let opaqueView = OpaqueViewFactory.create(with: .blurred(style: .light)) as? UIVisualEffectView + let blurEffect = opaqueView?.effect as? UIBlurEffect + + XCTAssertNotNil(opaqueView) + XCTAssertNotNil(blurEffect) + } + + func testDarkStyle() { + let opaqueView = OpaqueViewFactory.create(with: .dimmed) + + XCTAssertEqual(opaqueView.backgroundColor, UIColor.black.withAlphaComponent(0.8)) + } + +} diff --git a/Tests/GiniCaptureSDKTests/OpenWithTutorialViewControllerTests.swift b/Tests/GiniCaptureSDKTests/OpenWithTutorialViewControllerTests.swift new file mode 100644 index 0000000..ce0208c --- /dev/null +++ b/Tests/GiniCaptureSDKTests/OpenWithTutorialViewControllerTests.swift @@ -0,0 +1,134 @@ +// +// OpenWithTutorialViewControllerTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 10/20/17. +// Copyright © 2017 Gini GmbH. All rights reserved. +// + +import XCTest +@testable import GiniCaptureSDK +final class OpenWithTutorialViewControllerTests: XCTestCase { + + let openWithTutorialViewController = OpenWithTutorialViewController() + + lazy var items: [OpenWithTutorialStep] = [ + (NSLocalizedStringPreferredFormat("ginicapture.help.openWithTutorial.step1.title", + comment: "first step title for open with tutorial"), + NSLocalizedStringPreferredFormat("ginicapture.help.openWithTutorial.step1.subTitle", + comment: "first step subtitle for open with tutorial"), + UIImageNamedPreferred(named: .localized(resource: ImageAssetsStrings.openWithTutorialStep1))), + (NSLocalizedStringPreferredFormat("ginicapture.help.openWithTutorial.step2.title", + comment: "second step title for open with tutorial"), + String(format: NSLocalizedStringPreferredFormat("ginicapture.help.openWithTutorial.step2.subTitle", + comment: "second step subtitle for open with tutorial"), + self.openWithTutorialViewController.appName, self.openWithTutorialViewController.appName), + UIImageNamedPreferred(named: .localized(resource: ImageAssetsStrings.openWithTutorialStep2))), + (NSLocalizedStringPreferredFormat("ginicapture.help.openWithTutorial.step3.title", + comment: "third step title for open with tutorial"), + String(format: NSLocalizedStringPreferredFormat("ginicapture.help.openWithTutorial.step3.subTitle", + comment: "third step subtitle for open with tutorial"), + self.openWithTutorialViewController.appName, + self.openWithTutorialViewController.appName, + self.openWithTutorialViewController.appName), + UIImageNamedPreferred(named: .localized(resource: ImageAssetsStrings.openWithTutorialStep3))) + ] + + override func setUp() { + super.setUp() + _ = openWithTutorialViewController.view + } + + func testSectionCount() { + let sectionsCount = openWithTutorialViewController + .numberOfSections(in: openWithTutorialViewController.collectionView!) + + XCTAssertEqual(sectionsCount, 1, "sections count should always be 1") + } + + func testCollectionItemsCount() { + let collectionSection0ItemsCount = openWithTutorialViewController + .collectionView(openWithTutorialViewController.collectionView!, numberOfItemsInSection: 0) + + XCTAssertEqual(items.count, collectionSection0ItemsCount, + "the items count in section 0 should be the same as the one declared on initialization") + } + + func testFirstStepProperties() { + let indexPath = IndexPath(row: 0, section: 0) + let item = items[indexPath.row] + + let cell = openWithTutorialViewController + .collectionView(openWithTutorialViewController.collectionView!, + cellForItemAt: indexPath) as? OpenWithTutorialCollectionCell + + XCTAssertEqual(cell!.stepIndicator.text, String(describing: indexPath.row + 1), + "step indicator for first step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepTitle.text, item.title, + "step title for first step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepSubTitle.text, item.subtitle, + "step subtitle for first step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepImage.image, item.image, + "step image for first step should be the same as the one declared on initialisation") + } + + func testSecondStepProperties() { + let indexPath = IndexPath(row: 1, section: 0) + let item = items[indexPath.row] + + let cell = openWithTutorialViewController + .collectionView(openWithTutorialViewController.collectionView!, + cellForItemAt: indexPath) as? OpenWithTutorialCollectionCell + + XCTAssertEqual(cell!.stepIndicator.text, String(describing: indexPath.row + 1), + "step indicator for second step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepTitle.text, item.title, + "step title for second step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepSubTitle.text, item.subtitle, + "step subtitle for second step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepImage.image, item.image, + "step image for second step should be the same as the one declared on initialisation") + } + + func testThirdStepProperties() { + let indexPath = IndexPath(row: 2, section: 0) + let item = items[indexPath.row] + + let cell = openWithTutorialViewController + .collectionView(openWithTutorialViewController.collectionView!, + cellForItemAt: indexPath) as? OpenWithTutorialCollectionCell + + XCTAssertEqual(cell!.stepIndicator.text, String(describing: indexPath.row + 1), + "step indicator for third step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepTitle.text, item.title, + "step title for third step should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepSubTitle.text, item.subtitle, + "step for third step subtitle should be the same as the one declared on initialisation") + XCTAssertEqual(cell!.stepImage.image, item.image, + "step image for third step should be the same as the one declared on initialisation") + } + + func testHeaderDissapearInLandscape() { + let collectionView = openWithTutorialViewController.collectionView! + openWithTutorialViewController.view.frame.size = CGSize(width: 1, height: 0) // Simulate landscape + + let headerSize = openWithTutorialViewController + .collectionView(collectionView, + layout: collectionView.collectionViewLayout, + referenceSizeForHeaderInSection: 0) + + XCTAssertEqual(headerSize.height, 0, "header size should be 0 on landscape mode") + + } + + func testItemsWhenDragAndDropTipDoesNotAppear() { + let giniConfiguration = GiniConfiguration() + giniConfiguration.shouldShowDragAndDropTutorial = false + + let openWithTutorialViewController = OpenWithTutorialViewController(giniConfiguration: giniConfiguration) + let dragAndDropStepImage = UIImage(named: "openWithTutorialStep3", in: giniCaptureBundle(), compatibleWith: nil) + + XCTAssertFalse(openWithTutorialViewController.items.map { $0.image }.contains(dragAndDropStepImage), + "open with items should not contain drag and drop image") + } +} diff --git a/Tests/GiniCaptureSDKTests/PageStateViewTests.swift b/Tests/GiniCaptureSDKTests/PageStateViewTests.swift new file mode 100644 index 0000000..2955c83 --- /dev/null +++ b/Tests/GiniCaptureSDKTests/PageStateViewTests.swift @@ -0,0 +1,50 @@ +// +// PageStateViewTests.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 4/13/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import XCTest +@testable import GiniCaptureSDK +final class PageStateViewTests: XCTestCase { + + var statusView: PageStateView! + + override func setUp() { + super.setUp() + statusView = PageStateView(frame: .zero) + } + + func testLoadingState() { + statusView.update(to: .loading) + XCTAssertNil(statusView.icon.image, "icon image should be nil when it is loading") + XCTAssertTrue(statusView.loadingIndicator.isAnimating, "loading indicator should be animating when loading") + } + + func testSuccessState() { + statusView.update(to: .succeeded) + XCTAssertEqual(statusView.backgroundColor, Colors.Gini.springGreen, "background color should be green") + XCTAssertEqual(statusView.icon.image, + UIImage(named: "successfullUploadIcon", + in: giniCaptureBundle(), + compatibleWith: nil), + "icon image should match successfullUploadIcon asset") + XCTAssertFalse(statusView.loadingIndicator.isAnimating, + "loading indicator should not be animating when loading") + } + + func testFailureState() { + statusView.update(to: .failed) + XCTAssertEqual(statusView.backgroundColor, Colors.Gini.springGreen, "background color should be green") + XCTAssertEqual(statusView.icon.image, + UIImage(named: "failureUploadIcon", + in: giniCaptureBundle(), + compatibleWith: nil), + "icon image should match failureUploadIcon asset") + XCTAssertFalse(statusView.loadingIndicator.isAnimating, + "loading indicator should not be animating when loading") + } + +} diff --git a/Tests/GiniCaptureSDKTests/ReviewCollectionCellPresenterTests.swift b/Tests/GiniCaptureSDKTests/ReviewCollectionCellPresenterTests.swift deleted file mode 100644 index 79a77a5..0000000 --- a/Tests/GiniCaptureSDKTests/ReviewCollectionCellPresenterTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// MultipageCollectionCellPresenterTests.swift -// GiniCapture_Tests -// -// Created by Enrique del Pozo Gómez on 6/4/18. -// Copyright © 2018 Gini GmbH. All rights reserved. -// - -import XCTest -@testable import GiniCaptureSDK -final class ReviewCollectionCellPresenterTests: XCTestCase { - - var presenter: ReviewCollectionCellPresenter! - var giniConfiguration: GiniConfiguration! - var testPage = GiniCaptureTestsHelper.loadImagePage(named: "invoice") - - var setUpMainCollectionCell: ReviewCollectionCell { - let cell = presenter.setUp(ReviewCollectionCell(frame: .zero), - with: testPage, at: IndexPath(row: 0, section: 0)) - as? ReviewCollectionCell - return cell! - } - - override func setUp() { - super.setUp() - giniConfiguration = GiniConfiguration() - presenter = ReviewCollectionCellPresenter(giniConfiguration: giniConfiguration) - } - - func testMainCollectionCellImage() { - presenter.thumbnails[testPage.document.id, default: [:]][.big] = testPage.document.previewImage - - XCTAssertEqual(setUpMainCollectionCell.documentImageView.image, testPage.document.previewImage, - "Pages collection cells image content mode should match the one passed in the initializer") - } - - func testMainCollectionCellImageContentMode() { - XCTAssertEqual(setUpMainCollectionCell.documentImageView.contentMode, UIView.ContentMode.scaleAspectFit, - "Main collection cells image content mode should match the one passed in the initializer") - } - -} diff --git a/Tests/GiniCaptureSDKTests/ReviewVCDelegateMock.swift b/Tests/GiniCaptureSDKTests/ReviewVCDelegateMock.swift deleted file mode 100644 index 970f2c6..0000000 --- a/Tests/GiniCaptureSDKTests/ReviewVCDelegateMock.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ReviewViewControllerDelegateMock.swift -// GiniCapture_Example -// -// Created by Enrique del Pozo Gómez on 3/26/18. -// Copyright © 2018 Gini GmbH. All rights reserved. -// - -import Foundation -@testable import GiniCaptureSDK -final class ReviewVCDelegateMock: ReviewViewControllerDelegate { - func review(_ viewController: ReviewViewController, - didTapRetryUploadFor page: GiniCapturePage) { - - } - - func reviewDidTapAddImage(_ controller: ReviewViewController) { - - } - - var updatedDocuments: [GiniCapturePage] = [] - - func review(_ controller: ReviewViewController, didDelete pages: GiniCapturePage) { - - } - - func reviewDidTapProcess(_ viewController: ReviewViewController) { - - } - - func review(_ viewController: ReviewViewController, didSelectPage page: GiniCapturePage) { - - } -} diff --git a/Tests/GiniCaptureSDKTests/ReviewViewControllerDelegateMock.swift b/Tests/GiniCaptureSDKTests/ReviewViewControllerDelegateMock.swift new file mode 100644 index 0000000..12f4042 --- /dev/null +++ b/Tests/GiniCaptureSDKTests/ReviewViewControllerDelegateMock.swift @@ -0,0 +1,19 @@ +// +// ReviewViewControllerDelegateMock.swift +// GiniCapture_Tests +// +// Created by Enrique del Pozo Gómez on 5/11/18. +// Copyright © 2018 Gini GmbH. All rights reserved. +// + +import Foundation +import GiniCaptureSDK + +final class ReviewViewControllerDelegateMock: ReviewViewControllerDelegate { + + var isDocumentReviewed = false + + func review(_ viewController: ReviewViewController, didReview document: GiniCaptureDocument) { + isDocumentReviewed = true + } +} diff --git a/Tests/GiniCaptureSDKTests/ReviewViewControllerTests.swift b/Tests/GiniCaptureSDKTests/ReviewViewControllerTests.swift index b571c34..3eebab0 100644 --- a/Tests/GiniCaptureSDKTests/ReviewViewControllerTests.swift +++ b/Tests/GiniCaptureSDKTests/ReviewViewControllerTests.swift @@ -2,117 +2,45 @@ // ReviewViewControllerTests.swift // GiniCapture_Tests // -// Created by Enrique del Pozo Gómez on 1/30/18. +// Created by Enrique del Pozo Gómez on 5/11/18. // Copyright © 2018 Gini GmbH. All rights reserved. // import XCTest @testable import GiniCaptureSDK -final class ReviewViewControllerTests: XCTestCase { - - let giniConfiguration = GiniConfiguration.shared - lazy var reviewViewController: ReviewViewController = { - let vc = ReviewViewController(pages: self.imagePages, - giniConfiguration: self.giniConfiguration) - _ = vc.view - return vc - }() - - var imagePages: [GiniCapturePage] = [ - GiniCaptureTestsHelper.loadImagePage(named: "invoice"), - GiniCaptureTestsHelper.loadImagePage(named: "invoice2"), - GiniCaptureTestsHelper.loadImagePage(named: "invoice3") - ] - - func testCollectionsItemsCount() { - XCTAssertEqual(reviewViewController.collectionView.numberOfItems(inSection: 0), 3, - "main collection items count should be 3") - } - - func testMainCollectionCellSize() { - reviewViewController.view.setNeedsLayout() - reviewViewController.view.layoutIfNeeded() - - let firstCellIndexPath = IndexPath(row: 0, section: 0) - let cellSize = reviewViewController.collectionView(reviewViewController.collectionView, - layout: reviewViewController.collectionView.collectionViewLayout, - sizeForItemAt: firstCellIndexPath) - - let itemSize: CGSize = { - let a4Ratio = 1.4142 - if UIDevice.current.isIpad { - let height = reviewViewController.view.bounds.height - 260 - let width = height / a4Ratio - return CGSize(width: width, height: height) - } else { - if reviewViewController.view.safeAreaInsets.bottom > 0 { - let height = reviewViewController.view.bounds.height * 0.6 - let width = height / a4Ratio - let cellSize = CGSize(width: width, height: height) - return cellSize - } else { - let height = reviewViewController.view.bounds.height * 0.5 - let width = height / a4Ratio - let cellSize = CGSize(width: width, height: height) - return cellSize - } - } - }() - XCTAssertEqual(cellSize, itemSize, - "First cell image should match the one passed in the initializer") - } - - func testMainCollectionInsets() { - let collectionInsets = reviewViewController - .collectionView(reviewViewController.collectionView, - layout: reviewViewController.collectionView.collectionViewLayout, - insetForSectionAt: 0) - - let itemSize: CGSize = { - let a4Ratio = 1.4142 - if UIDevice.current.isIpad { - let height = reviewViewController.view.bounds.height - 260 - let width = height / a4Ratio - return CGSize(width: width, height: height) - } else { - if reviewViewController.view.safeAreaInsets.bottom > 0 { - let height = reviewViewController.view.bounds.height * 0.6 - let width = height / a4Ratio - let cellSize = CGSize(width: width, height: height) - return cellSize - } else { - let height = reviewViewController.view.bounds.height * 0.5 - let width = height / a4Ratio - let cellSize = CGSize(width: width, height: height) - return cellSize - } +extension UIControl { + func simulateEvent(_ event: UIControl.Event) { + for target in allTargets { + let target = target as NSObjectProtocol + for actionName in actions(forTarget: target, forControlEvent: event) ?? [] { + let selector = Selector(actionName) + target.perform(selector) } - }() - - let margin = (reviewViewController.view.bounds.width - itemSize.width) / 2 - let calculatedInset = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - XCTAssertEqual(collectionInsets, calculatedInset, "Main collection insets should be zero") + } } +} // MARK: - Fix the test with tap event simulation -// func testDatasourceOnDelete() { -// let vc = ReviewViewController(pages: imagePages, -// giniConfiguration: giniConfiguration) -// _ = vc.view -// vc.view.setNeedsLayout() -// vc.view.layoutIfNeeded() -// if let button = (vc.deleteButton.customView as? UIButton){ -// button.simulateEvent(.touchUpInside) -// } +//final class ReviewViewControllerTests: XCTestCase { // +// +// var reviewViewController: ReviewViewController! +// var reviewViewControllerDelegateMock: ReviewViewControllerDelegateMock! + +// func testDidReviewOnRotationWithDelegate() { +// let document = GiniCaptureTestsHelper.loadImageDocument(named: "invoice") +// reviewViewController = ReviewViewController(document: document, giniConfiguration: GiniConfiguration()) +// _ = reviewViewController.view // -// //(vc.deleteButton.customView as? UIButton)?.sendActions(for: .touchUpInside) +// reviewViewControllerDelegateMock = ReviewViewControllerDelegateMock() +// reviewViewController.delegate = reviewViewControllerDelegateMock +// let reviewButton = reviewViewController.rotateButton +// reviewButton.sendActions(for: .touchUpInside) // -// XCTAssertEqual(vc.mainCollection.numberOfItems(inSection: 0), 2, -// "main collection items count should be 2") -// XCTAssertEqual(vc.pagesCollection.numberOfItems(inSection: 0), 2, -// "pages collection items count should be 2") +// XCTAssertTrue(reviewViewControllerDelegateMock.isDocumentReviewed, +// "after tapping rotate button the document should have been modified and therefore the delegate" + +// "should be notified") // } -} +//} diff --git a/Tests/GiniCaptureSDKTests/SupportedFormatsViewControllerTests.swift b/Tests/GiniCaptureSDKTests/SupportedFormatsViewControllerTests.swift index 9a176a0..4759419 100644 --- a/Tests/GiniCaptureSDKTests/SupportedFormatsViewControllerTests.swift +++ b/Tests/GiniCaptureSDKTests/SupportedFormatsViewControllerTests.swift @@ -10,37 +10,26 @@ import XCTest @testable import GiniCaptureSDK final class SupportedFormatsViewControllerTests: XCTestCase { - var supportedFormatsViewController = HelpFormatsViewController( - giniConfiguration: .shared - ) - + var supportedFormatsViewController = SupportedFormatsViewController() let initialGiniConfiguration = GiniConfiguration.shared - lazy var sections: [HelpFormatsCollectionSection] = { - var sections: [HelpFormatsCollectionSection] = [ - (NSLocalizedString( - "ginicapture.help.supportedFormats.section.1.title", - bundle: giniCaptureBundle(), - comment: ""), - [ - NSLocalizedString( - "ginicapture.help.supportedFormats.section.1.item.1", - bundle: giniCaptureBundle(), - comment: "")], - UIImageNamedPreferred(named: "supportedFormatsIcon")), - (NSLocalizedString( - "ginicapture.help.supportedFormats.section.2.title", - bundle: giniCaptureBundle(), - comment: ""), - [ - NSLocalizedString( - "ginicapture.help.supportedFormats.section.2.item.1", - bundle: giniCaptureBundle(), - comment: "")], - UIImageNamedPreferred(named: "nonSupportedFormatsIcon")) - ] - return sections - }() + var sections: [SupportedFormatCollectionSection] = [ + (.localized(resource: HelpStrings.supportedFormatsSection1Title), + [.localized(resource: HelpStrings.supportedFormatsSection1Item1Text), + .localized(resource: HelpStrings.supportedFormatsSection1Item2Text), + .localized(resource: HelpStrings.supportedFormatsSection1Item3Text)], + UIImage(named: "supportedFormatsIcon", + in: giniCaptureBundle(), + compatibleWith: nil), + GiniConfiguration.shared.supportedFormatsIconColor), + (.localized(resource: HelpStrings.supportedFormatsSection2Title), + [.localized(resource: HelpStrings.supportedFormatsSection2Item1Text), + .localized(resource: HelpStrings.supportedFormatsSection2Item2Text)], + UIImage(named: "nonSupportedFormatsIcon", + in: giniCaptureBundle(), + compatibleWith: nil), + GiniConfiguration.shared.nonSupportedFormatsIconColor) + ] override func setUp() { super.setUp() @@ -49,7 +38,8 @@ final class SupportedFormatsViewControllerTests: XCTestCase { func testSectionsCount() { let sectionsCount = sections.count - let tableSectionsCount = supportedFormatsViewController.dataSource.numberOfSections(in: supportedFormatsViewController.tableView) + let tableSectionsCount = supportedFormatsViewController + .numberOfSections(in: supportedFormatsViewController.tableView) XCTAssertEqual(sectionsCount, tableSectionsCount, "sections count and table sections count should be always equal") @@ -57,8 +47,8 @@ final class SupportedFormatsViewControllerTests: XCTestCase { func testSectionItemsCount() { - let section2ItemsCount = sections[1].formats.count - let tableSection2ItemsCount = supportedFormatsViewController.dataSource + let section2ItemsCount = sections[1].items.count + let tableSection2ItemsCount = supportedFormatsViewController .tableView(supportedFormatsViewController.tableView, numberOfRowsInSection: 1) @@ -67,39 +57,197 @@ final class SupportedFormatsViewControllerTests: XCTestCase { "items count inside section 2 and table section 2 items count should be always equal") } + func testFirstSectionProperties() { + setFileImportSupportedTypes(to: .pdf_and_images) + supportedFormatsViewController = SupportedFormatsViewController() + + let indexPath = IndexPath(row: 0, section: 0) + let section = sections[indexPath.section] + let sectionImage = section.itemsImage + let sectionImageBackgroundColor = section.itemsImageBackgroundColor + let sectionItemsCount = section.items.count + let sectionTitle = section.title + + let cell = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, cellForRowAt: indexPath) + as? SupportedFormatsTableViewCell + let headerTitle = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, titleForHeaderInSection: indexPath.section) + let tableViewSectionItemsCount = supportedFormatsViewController + .tableView + .numberOfRows(inSection: indexPath.section) + + XCTAssertNotNil(cell, "cell in this table view should always be of type SupportedFormatsTableViewCell") + XCTAssertEqual(sectionImage, cell?.imageView?.image, + "cell image should be equal to section image since it is the same for each item in the section") + XCTAssertEqual(sectionImageBackgroundColor, cell?.imageBackgroundView.backgroundColor, + "cell image background color should be equal to section image background " + + "colorsince it is the same for each item in the section") + XCTAssertEqual(sectionImage, cell?.imageView?.image, + "cell image should be equal to section image since it is the same for each item in the section") + XCTAssertEqual(sectionTitle, headerTitle, "header title should be equal to section title") + XCTAssertEqual(sectionItemsCount, tableViewSectionItemsCount, + "section items count and table section items count should be always equal") + } + func testFirstSectionItemsCountFileImportDisabled() { setFileImportSupportedTypes(to: .none) - supportedFormatsViewController = HelpFormatsViewController(giniConfiguration: .shared) + supportedFormatsViewController = SupportedFormatsViewController() _ = supportedFormatsViewController.view - let section1items = supportedFormatsViewController.dataSource.tableView( - supportedFormatsViewController.tableView, - numberOfRowsInSection: 0 - ) + let section1items = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, + numberOfRowsInSection: 0) - XCTAssertEqual(section1items, 2, "items count in section 1 should be 1 when file import is disabled") + XCTAssertEqual(section1items, 1, "items count in section 1 should be 1 when file import is disabled") } func testFirstSectionItemsCountFileImportDisabledForImages() { setFileImportSupportedTypes(to: .pdf) - supportedFormatsViewController = HelpFormatsViewController(giniConfiguration: .shared) + supportedFormatsViewController = SupportedFormatsViewController() _ = supportedFormatsViewController.view - let section1items = supportedFormatsViewController.dataSource.tableView(supportedFormatsViewController.tableView, + let section1items = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, numberOfRowsInSection: 0) - XCTAssertEqual(section1items, 3, + XCTAssertEqual(section1items, 2, "items count in section 1 should be 2 when file import is enabled only for pdfs") } + func testSecondSectionProperties() { + let indexPath = IndexPath(row: 0, section: 1) + let section = sections[indexPath.section] + let sectionImage = section.itemsImage + let sectionImageBackgroundColor = section.itemsImageBackgroundColor + let sectionItemsCount = section.items.count + let sectionTitle = section.title + + let cell = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, cellForRowAt: indexPath) + as? SupportedFormatsTableViewCell + let headerTitle = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, titleForHeaderInSection: indexPath.section) + let tableViewSectionItemsCount = supportedFormatsViewController + .tableView + .numberOfRows(inSection: indexPath.section) + + XCTAssertNotNil(cell, "cell in this table view should always be of type SupportedFormatsTableViewCell") + XCTAssertEqual(sectionImage, cell?.imageView?.image, + "cell image should be equal to section image since it is the same for each item in the section") + XCTAssertEqual(sectionImageBackgroundColor, cell?.imageBackgroundView.backgroundColor, + "cell image background color should be equal to section image background " + + "colorsince it is the same for each item in the section") + XCTAssertEqual(sectionImage, cell?.imageView?.image, + "cell image should be equal to section image since it is the same for each item in the section") + XCTAssertEqual(sectionTitle, headerTitle, + "header title should be equal to section title") + XCTAssertEqual(sectionItemsCount, tableViewSectionItemsCount, + "section items count and table section items count should be always equal") + } + + func testFirstSupportedFormatCellText() { + setFileImportSupportedTypes(to: .pdf_and_images) + supportedFormatsViewController = SupportedFormatsViewController() + + let indexPath = IndexPath(row: 0, section: 0) + let textForItem0tSection0 = sections[indexPath.section].items[indexPath.row] + let textForCellAtIndexPath = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, + cellForRowAt: indexPath).textLabel?.text + + XCTAssertEqual(textForCellAtIndexPath, textForItem0tSection0, + "text for item 0 at section 0 should be equal to the one declared on initialization") + } + + func testSecondSupportedFormatCellText() { + setFileImportSupportedTypes(to: .pdf_and_images) + supportedFormatsViewController = SupportedFormatsViewController() + + let indexPath = IndexPath(row: 1, section: 0) + let textForItem0tSection0 = sections[indexPath.section].items[indexPath.row] + let textForCellAtIndexPath = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, + cellForRowAt: indexPath).textLabel?.text + + XCTAssertEqual(textForCellAtIndexPath, textForItem0tSection0, + "text for item 1 at section 0 should be equal to the one declared on initialization") + } + + func testThirdSupportedFormatCellText() { + setFileImportSupportedTypes(to: .pdf_and_images) + supportedFormatsViewController = SupportedFormatsViewController() + + let indexPath = IndexPath(row: 2, section: 0) + let textForItem0tSection0 = sections[indexPath.section].items[indexPath.row] + let textForCellAtIndexPath = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, + cellForRowAt: indexPath).textLabel?.text + + XCTAssertEqual(textForCellAtIndexPath, textForItem0tSection0, + "text for item 2 at section 0 should be equal to the one declared on initialization") + } + + func testFirstUnSupportedFormatCellText() { + let indexPath = IndexPath(row: 0, section: 1) + let textForItem0tSection0 = sections[indexPath.section].items[indexPath.row] + let textForCellAtIndexPath = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, + cellForRowAt: indexPath).textLabel?.text + + XCTAssertEqual(textForCellAtIndexPath, textForItem0tSection0, + "text for item 0 at section 1 should be equal to the one declared on initialization") + } + + func testSecondUnSupportedFormatCellText() { + let indexPath = IndexPath(row: 1, section: 1) + let textForItem0tSection0 = sections[indexPath.section].items[indexPath.row] + let textForCellAtIndexPath = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, + cellForRowAt: indexPath).textLabel?.text + + XCTAssertEqual(textForCellAtIndexPath, textForItem0tSection0, + "text for item 1 at section 1 should be equal to the one declared on initialization") + } + + func testSectionHeaderHeight() { + let sectionHeaderHeight = supportedFormatsViewController.sectionHeight + + let tableSectionHeaderHeight = supportedFormatsViewController.tableView.sectionHeaderHeight + + XCTAssertEqual(sectionHeaderHeight, tableSectionHeaderHeight, + "table view section header height should be equal to the one declare on initialization") + } + + func testRowHeight() { + let rowHeight = supportedFormatsViewController.rowHeight + + let tableRowHeight = supportedFormatsViewController.tableView.rowHeight + + XCTAssertEqual(rowHeight, tableRowHeight, + "table view row height should be equal to the one declare on initialization") + } + + func testSectionTitle() { + let section1Title = sections[0].title + let tableSection1Title = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, + titleForHeaderInSection: 0) + + XCTAssertEqual(section1Title, tableSection1Title, + "table view section 1 title should be equal to the one declare on initialization") + } + func testRowSelectionDisabled() { let selectionState = supportedFormatsViewController.tableView.allowsSelection XCTAssertFalse(selectionState, "table view cell selection should not be allowed") } + func testSectionImageItemBackgroundColor() { + let indexPath = IndexPath(row: 0, section: 0) + + let sectionImageItemBackgroundColor = supportedFormatsViewController + .sections[indexPath.section].itemsImageBackgroundColor + + let cell = supportedFormatsViewController.tableView(supportedFormatsViewController.tableView, cellForRowAt: indexPath) + as? SupportedFormatsTableViewCell + let cellImageBackgroundColor = cell!.imageBackgroundView.backgroundColor + + XCTAssertEqual(sectionImageItemBackgroundColor, cellImageBackgroundColor, + "cell image background color should be the same as the one declared on initialization") + } + override func tearDown() { super.tearDown() GiniConfiguration.shared = initialGiniConfiguration