├── .gitignore ├── ImagePicker.sln ├── Images ├── demo.gif ├── icon.png └── logo.png ├── LICENSE ├── README.md ├── azure-pipelines ├── dev.yml ├── nuget.yml └── templates │ ├── setup-dotnet.yml │ └── vars.yml ├── samples ├── AppDelegate.cs ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ └── iTunesArtwork@2x.png │ ├── Contents.json │ ├── background-rounded.imageset │ │ ├── Contents.json │ │ └── background-rounded.pdf │ ├── button-camera.imageset │ │ ├── Contents.json │ │ └── button-camera.pdf │ ├── button-photo-library.imageset │ │ ├── Contents.json │ │ └── button-photo-library.pdf │ ├── gradient.imageset │ │ ├── Contents.json │ │ ├── gradient.png │ │ ├── gradient@2x.png │ │ └── gradient@3x.png │ ├── ic-check.imageset │ │ ├── Contents.json │ │ └── ic-check.pdf │ ├── icon-badge-livephoto.imageset │ │ ├── Contents.json │ │ └── icon-badge-livephoto.pdf │ ├── icon-badge-video.imageset │ │ ├── Contents.json │ │ └── icon-badge-video.pdf │ ├── icon-check-background.imageset │ │ ├── Contents.json │ │ └── icon-ckeck-background.pdf │ ├── icon-depth.imageset │ │ ├── Contents.json │ │ └── icon-depth.pdf │ ├── icon-flip-camera.imageset │ │ ├── Contents.json │ │ └── flipCamera.pdf │ ├── icon-live-off.imageset │ │ ├── Contents.json │ │ └── icon-live-off.pdf │ ├── icon-live-on.imageset │ │ ├── Contents.json │ │ └── icon-live-on.pdf │ ├── icon-live.imageset │ │ ├── Contents.json │ │ └── icon-live.pdf │ └── icon-pano.imageset │ │ ├── Contents.json │ │ └── icon-pano.pdf ├── CustomViews │ ├── CustomImageCell.cs │ ├── CustomImageCell.designer.cs │ ├── CustomImageCell.xib │ ├── CustomVideoCell.cs │ ├── CustomVideoCell.designer.cs │ ├── CustomVideoCell.xib │ ├── IconWithTextCell.cs │ ├── IconWithTextCell.designer.cs │ └── IconWithTextCell.xib ├── Entitlements.plist ├── ImagePickerConfigurationHandlerClass.cs ├── ImagePickerControllerDataSource.cs ├── ImagePickerControllerDelegate.cs ├── Info.plist ├── LaunchScreen.storyboard ├── Main.cs ├── Main.storyboard ├── Models │ ├── CellItemModel.cs │ └── Enums │ │ ├── AssetsSource.cs │ │ ├── CameraItemConfig.cs │ │ └── SelectorArgument.cs ├── Softeq.ImagePicker.Sample.csproj ├── ViewController.cs └── ViewController.designer.cs └── src ├── Assets └── Assets.xcassets │ ├── Contents.json │ ├── background-rounded.imageset │ ├── Contents.json │ └── background-rounded.pdf │ ├── button-camera.imageset │ ├── Contents.json │ └── button-camera.pdf │ ├── button-photo-library.imageset │ ├── Contents.json │ └── button-photo-library.pdf │ ├── gradient.imageset │ ├── Contents.json │ ├── gradient.png │ ├── gradient@2x.png │ └── gradient@3x.png │ ├── icon-badge-livephoto.imageset │ ├── Contents.json │ └── icon-badge-livephoto.pdf │ ├── icon-badge-video.imageset │ ├── Contents.json │ └── icon-badge-video.pdf │ ├── icon-check-background.imageset │ ├── Contents.json │ └── icon-ckeck-background.pdf │ ├── icon-check.imageset │ ├── Contents.json │ └── icon-check.pdf │ ├── icon-flip-camera.imageset │ ├── Contents.json │ └── flipCamera.pdf │ ├── icon-live-off.imageset │ ├── Contents.json │ └── icon-live-off.pdf │ └── icon-live-on.imageset │ ├── Contents.json │ └── icon-live-on.pdf ├── Defines.cs ├── GlobalUsings.cs ├── ImagePickerAssetModel.cs ├── ImagePickerDataSource.cs ├── ImagePickerDelegate.cs ├── ImagePickerLayout.cs ├── Infrastructure ├── Enums │ ├── CameraMode.cs │ ├── LivePhotoMode.cs │ ├── SessionSetupResult.cs │ └── VideoDisplayMode.cs ├── Extensions │ ├── UICollectionViewExtensions.cs │ └── UIImageExtensions.cs ├── ImagePickerException.cs └── Interfaces │ ├── ICameraCollectionViewCellDelegate.cs │ ├── ICaptureSessionDelegate.cs │ ├── ICaptureSessionVideoRecordingDelegate.cs │ ├── IImagePickerDelegate.cs │ └── ISessionPhotoCapturingDelegate.cs ├── LayoutModel.cs ├── Media ├── AVPreviewView.cs ├── Capture │ ├── AudioCaptureSession.cs │ ├── CaptureNotificationCenterHandler.cs │ ├── CaptureSession.cs │ ├── PhotoCaptureSession.cs │ ├── VideoCaptureSession.cs │ └── VideoDeviceInputManager.cs ├── CaptureFactory.cs ├── Delegates │ ├── CaptureSessionDelegate.cs │ ├── CaptureSessionVideoRecordingDelegate.cs │ ├── PhotoCaptureDelegate.cs │ ├── SessionPhotoCapturingDelegate.cs │ └── VideoCaptureDelegate.cs ├── PHAssetManager.cs └── SessionPresetConfiguration.cs ├── Operations ├── CollectionViewBatchAnimation.cs └── CollectionViewUpdatesCoordinator.cs ├── Public ├── Appearance.cs ├── CameraCollectionViewCell.cs ├── CaptureSettings.cs ├── CellRegistrator.cs ├── Delegates │ ├── CameraCollectionViewCellDelegate.cs │ └── ImagePickerControllerDelegate.cs ├── ImagePickerController.cs ├── ImagePickerControllerDataSource.cs ├── ImagePickerControllerPublicApi.cs └── LayoutConfiguration.cs ├── Softeq.ImagePicker.csproj └── Views ├── ActionCell.cs ├── ActionCell.designer.cs ├── ActionCell.xib ├── AssetCell.cs ├── CustomControls ├── CarvedLabel.cs ├── CheckView.cs ├── LayersState.cs ├── RecordButton.cs ├── RecordDurationLabel.cs ├── ShutterButton.cs └── StationaryButton.cs ├── ImagePickerAssetCell.cs ├── ImagePickerView.cs ├── ImagePickerView.designer.cs ├── ImagePickerView.xib ├── LivePhotoCameraCell.cs ├── LivePhotoCameraCell.designer.cs ├── LivePhotoCameraCell.xib ├── VideoAssetCell.cs ├── VideoCameraCell.cs ├── VideoCameraCell.designer.cs └── VideoCameraCell.xib /.gitignore: -------------------------------------------------------------------------------- 1 | # Autosave files 2 | *~ 3 | 4 | # build 5 | [Oo]bj/ 6 | [Bb]in/ 7 | packages/ 8 | TestResults/ 9 | 10 | # globs 11 | Makefile.in 12 | *.DS_Store 13 | *.sln.cache 14 | *.suo 15 | *.cache 16 | *.pidb 17 | *.userprefs 18 | *.usertasks 19 | config.log 20 | config.make 21 | config.status 22 | aclocal.m4 23 | install-sh 24 | autom4te.cache/ 25 | *.user 26 | *.tar.gz 27 | tarballs/ 28 | test-results/ 29 | Thumbs.db 30 | .vs/ 31 | 32 | # Mac bundle stuff 33 | *.dmg 34 | *.app 35 | 36 | # resharper 37 | *_Resharper.* 38 | *.Resharper 39 | 40 | # dotCover 41 | *.dotCover 42 | 43 | .idea/ 44 | -------------------------------------------------------------------------------- /ImagePicker.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Softeq.ImagePicker.Sample", "samples\Softeq.ImagePicker.Sample.csproj", "{A59CC788-2B05-4288-A942-D6A88BD88F36}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Softeq.ImagePicker", "src\Softeq.ImagePicker.csproj", "{78713D60-595D-4F7B-B9D7-2E81746A11E3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 11 | Release|iPhone = Release|iPhone 12 | Release|iPhoneSimulator = Release|iPhoneSimulator 13 | Debug|iPhone = Debug|iPhone 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator 17 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator 18 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Release|iPhone.ActiveCfg = Release|iPhone 19 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Release|iPhone.Build.0 = Release|iPhone 20 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator 21 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator 22 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Debug|iPhone.ActiveCfg = Debug|iPhone 23 | {A59CC788-2B05-4288-A942-D6A88BD88F36}.Debug|iPhone.Build.0 = Debug|iPhone 24 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 25 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 26 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Release|iPhone.ActiveCfg = Release|Any CPU 27 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Release|iPhone.Build.0 = Release|Any CPU 28 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 29 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 30 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Debug|iPhone.ActiveCfg = Debug|Any CPU 31 | {78713D60-595D-4F7B-B9D7-2E81746A11E3}.Debug|iPhone.Build.0 = Debug|Any CPU 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /Images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/Images/demo.gif -------------------------------------------------------------------------------- /Images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/Images/icon.png -------------------------------------------------------------------------------- /Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/Images/logo.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Softeq Development Corp. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /azure-pipelines/dev.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | batch: true 3 | branches: 4 | include: 5 | - master 6 | - release/* 7 | paths: 8 | include: 9 | - '*' 10 | exclude: 11 | - '**/*.md' 12 | # pr: by default runs on each commit (with autoCancel) for all branches 13 | 14 | variables: 15 | - template: templates/vars.yml 16 | 17 | jobs: 18 | - job: macOS 19 | pool: 20 | vmImage: $(MACOS_VM_IMAGE) 21 | steps: 22 | - template: templates/setup-dotnet.yml 23 | 24 | - task: Bash@3 25 | displayName: Build iOS App 26 | inputs: 27 | targetType: 'inline' 28 | script: | 29 | dotnet build -c Debug -v Detailed 30 | -------------------------------------------------------------------------------- /azure-pipelines/nuget.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | variables: 5 | - template: templates/vars.yml 6 | 7 | jobs: 8 | - job: macOS 9 | pool: 10 | vmImage: $(MACOS_VM_IMAGE) 11 | steps: 12 | - template: templates/setup-dotnet.yml 13 | 14 | - task: Bash@3 15 | displayName: Pack Library 16 | inputs: 17 | targetType: 'inline' 18 | script: | 19 | cd src && dotnet pack -c Release -o . 20 | 21 | - task: CopyFiles@2 22 | inputs: 23 | contents: '**/*.nupkg' 24 | targetFolder: $(Build.ArtifactStagingDirectory) 25 | 26 | - task: PublishBuildArtifacts@1 27 | inputs: 28 | ArtifactName: 'drop' 29 | -------------------------------------------------------------------------------- /azure-pipelines/templates/setup-dotnet.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: version 3 | type: string 4 | default: 6.0.402 5 | 6 | steps: 7 | - task: UseDotNet@2 8 | displayName: Use .NET Version 9 | inputs: 10 | packageType: 'sdk' 11 | version: ${{ parameters.version }} 12 | 13 | - task: Bash@3 14 | displayName: Install .NET Workloads 15 | inputs: 16 | targetType: 'inline' 17 | script: | 18 | dotnet workload install ios -------------------------------------------------------------------------------- /azure-pipelines/templates/vars.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | MACOS_VM_IMAGE: 'macos-12' -------------------------------------------------------------------------------- /samples/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | using UIKit; 3 | 4 | namespace Softeq.ImagePicker.Sample; 5 | 6 | // The UIApplicationDelegate for the application. This class is responsible for launching the 7 | // User Interface of the application, as well as listening (and optionally responding) to application events from iOS. 8 | [Register(nameof(AppDelegate))] 9 | public class AppDelegate : UIApplicationDelegate 10 | { 11 | // class-level declarations 12 | 13 | public override UIWindow? Window { get; set; } 14 | 15 | public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) 16 | { 17 | // Override point for customization after application launch. 18 | // If not required for your application you can safely delete this method 19 | return true; 20 | } 21 | 22 | public override void OnResignActivation(UIApplication application) 23 | { 24 | // Invoked when the application is about to move from active to inactive state. 25 | // This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) 26 | // or when the user quits the application and it begins the transition to the background state. 27 | // Games should use this method to pause the game. 28 | } 29 | 30 | public override void DidEnterBackground(UIApplication application) 31 | { 32 | // Use this method to release shared resources, save user data, invalidate timers and store the application state. 33 | // If your application supports background exection this method is called instead of WillTerminate when the user quits. 34 | } 35 | 36 | public override void WillEnterForeground(UIApplication application) 37 | { 38 | // Called as part of the transiton from background to active state. 39 | // Here you can undo many of the changes made on entering the background. 40 | } 41 | 42 | public override void OnActivated(UIApplication application) 43 | { 44 | // Restart any tasks that were paused (or not yet started) while the application was inactive. 45 | // If the application was previously in the background, optionally refresh the user interface. 46 | } 47 | 48 | public override void WillTerminate(UIApplication application) 49 | { 50 | // Called when the application is about to terminate. Save data, if needed. See also DidEnterBackground. 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "Icon-App-20x20@2x.png", 5 | "size": "20x20", 6 | "scale": "2x", 7 | "idiom": "iphone" 8 | }, 9 | { 10 | "filename": "Icon-App-20x20@3x.png", 11 | "size": "20x20", 12 | "scale": "3x", 13 | "idiom": "iphone" 14 | }, 15 | { 16 | "filename": "Icon-App-29x29@2x.png", 17 | "size": "29x29", 18 | "scale": "2x", 19 | "idiom": "iphone" 20 | }, 21 | { 22 | "filename": "Icon-App-29x29@3x.png", 23 | "size": "29x29", 24 | "scale": "3x", 25 | "idiom": "iphone" 26 | }, 27 | { 28 | "filename": "Icon-App-40x40@2x.png", 29 | "size": "40x40", 30 | "scale": "2x", 31 | "idiom": "iphone" 32 | }, 33 | { 34 | "filename": "Icon-App-40x40@3x.png", 35 | "size": "40x40", 36 | "scale": "3x", 37 | "idiom": "iphone" 38 | }, 39 | { 40 | "filename": "Icon-App-60x60@2x.png", 41 | "size": "60x60", 42 | "scale": "2x", 43 | "idiom": "iphone" 44 | }, 45 | { 46 | "filename": "Icon-App-60x60@3x.png", 47 | "size": "60x60", 48 | "scale": "3x", 49 | "idiom": "iphone" 50 | }, 51 | { 52 | "size": "20x20", 53 | "scale": "1x", 54 | "idiom": "ipad" 55 | }, 56 | { 57 | "size": "20x20", 58 | "scale": "2x", 59 | "idiom": "ipad" 60 | }, 61 | { 62 | "size": "29x29", 63 | "scale": "1x", 64 | "idiom": "ipad" 65 | }, 66 | { 67 | "size": "29x29", 68 | "scale": "2x", 69 | "idiom": "ipad" 70 | }, 71 | { 72 | "size": "40x40", 73 | "scale": "1x", 74 | "idiom": "ipad" 75 | }, 76 | { 77 | "size": "40x40", 78 | "scale": "2x", 79 | "idiom": "ipad" 80 | }, 81 | { 82 | "size": "83.5x83.5", 83 | "scale": "2x", 84 | "idiom": "ipad" 85 | }, 86 | { 87 | "size": "76x76", 88 | "scale": "1x", 89 | "idiom": "ipad" 90 | }, 91 | { 92 | "size": "76x76", 93 | "scale": "2x", 94 | "idiom": "ipad" 95 | }, 96 | { 97 | "filename": "iTunesArtwork@2x.png", 98 | "size": "1024x1024", 99 | "scale": "1x", 100 | "idiom": "ios-marketing" 101 | }, 102 | { 103 | "size": "60x60", 104 | "scale": "2x", 105 | "idiom": "car" 106 | }, 107 | { 108 | "size": "60x60", 109 | "scale": "3x", 110 | "idiom": "car" 111 | }, 112 | { 113 | "role": "notificationCenter", 114 | "size": "24x24", 115 | "subtype": "38mm", 116 | "scale": "2x", 117 | "idiom": "watch" 118 | }, 119 | { 120 | "role": "notificationCenter", 121 | "size": "27.5x27.5", 122 | "subtype": "42mm", 123 | "scale": "2x", 124 | "idiom": "watch" 125 | }, 126 | { 127 | "role": "companionSettings", 128 | "size": "29x29", 129 | "scale": "2x", 130 | "idiom": "watch" 131 | }, 132 | { 133 | "role": "companionSettings", 134 | "size": "29x29", 135 | "scale": "3x", 136 | "idiom": "watch" 137 | }, 138 | { 139 | "role": "appLauncher", 140 | "size": "40x40", 141 | "subtype": "38mm", 142 | "scale": "2x", 143 | "idiom": "watch" 144 | }, 145 | { 146 | "role": "appLauncher", 147 | "size": "44x44", 148 | "subtype": "40mm", 149 | "scale": "2x", 150 | "idiom": "watch" 151 | }, 152 | { 153 | "role": "appLauncher", 154 | "size": "50x50", 155 | "subtype": "44mm", 156 | "scale": "2x", 157 | "idiom": "watch" 158 | }, 159 | { 160 | "role": "quickLook", 161 | "size": "86x86", 162 | "subtype": "38mm", 163 | "scale": "2x", 164 | "idiom": "watch" 165 | }, 166 | { 167 | "role": "quickLook", 168 | "size": "98x98", 169 | "subtype": "42mm", 170 | "scale": "2x", 171 | "idiom": "watch" 172 | }, 173 | { 174 | "role": "quickLook", 175 | "size": "108x108", 176 | "subtype": "44mm", 177 | "scale": "2x", 178 | "idiom": "watch" 179 | }, 180 | { 181 | "size": "1024x1024", 182 | "scale": "1x", 183 | "idiom": "watch-marketing" 184 | }, 185 | { 186 | "size": "16x16", 187 | "scale": "1x", 188 | "idiom": "mac" 189 | }, 190 | { 191 | "size": "16x16", 192 | "scale": "2x", 193 | "idiom": "mac" 194 | }, 195 | { 196 | "size": "32x32", 197 | "scale": "1x", 198 | "idiom": "mac" 199 | }, 200 | { 201 | "size": "32x32", 202 | "scale": "2x", 203 | "idiom": "mac" 204 | }, 205 | { 206 | "size": "128x128", 207 | "scale": "1x", 208 | "idiom": "mac" 209 | }, 210 | { 211 | "size": "128x128", 212 | "scale": "2x", 213 | "idiom": "mac" 214 | }, 215 | { 216 | "size": "256x256", 217 | "scale": "1x", 218 | "idiom": "mac" 219 | }, 220 | { 221 | "size": "256x256", 222 | "scale": "2x", 223 | "idiom": "mac" 224 | }, 225 | { 226 | "size": "512x512", 227 | "scale": "1x", 228 | "idiom": "mac" 229 | }, 230 | { 231 | "size": "512x512", 232 | "scale": "2x", 233 | "idiom": "mac" 234 | } 235 | ], 236 | "info": { 237 | "version": 1, 238 | "author": "xcode" 239 | }, 240 | "properties": { 241 | "pre-rendered": true 242 | } 243 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/background-rounded.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "background-rounded.pdf", 6 | "resizing" : { 7 | "mode" : "9-part", 8 | "center" : { 9 | "mode" : "tile", 10 | "width" : 1, 11 | "height" : 1 12 | }, 13 | "cap-insets" : { 14 | "bottom" : 12, 15 | "top" : 12, 16 | "right" : 12, 17 | "left" : 12 18 | } 19 | } 20 | } 21 | ], 22 | "info" : { 23 | "version" : 1, 24 | "author" : "xcode" 25 | }, 26 | "properties" : { 27 | "template-rendering-intent" : "template" 28 | } 29 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/background-rounded.imageset/background-rounded.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/background-rounded.imageset/background-rounded.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/button-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-camera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/button-camera.imageset/button-camera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/button-camera.imageset/button-camera.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/button-photo-library.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-photo-library.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/gradient.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "gradient.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "gradient@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "gradient@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/gradient.imageset/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/gradient.imageset/gradient.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/gradient.imageset/gradient@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/gradient.imageset/gradient@2x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/gradient.imageset/gradient@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/gradient.imageset/gradient@3x.png -------------------------------------------------------------------------------- /samples/Assets.xcassets/ic-check.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic-check.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/ic-check.imageset/ic-check.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/ic-check.imageset/ic-check.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-badge-livephoto.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-badge-livephoto.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-badge-livephoto.imageset/icon-badge-livephoto.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-badge-livephoto.imageset/icon-badge-livephoto.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-badge-video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-badge-video.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-badge-video.imageset/icon-badge-video.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-badge-video.imageset/icon-badge-video.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-check-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-ckeck-background.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-check-background.imageset/icon-ckeck-background.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-check-background.imageset/icon-ckeck-background.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-depth.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-depth.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-depth.imageset/icon-depth.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-depth.imageset/icon-depth.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-flip-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "flipCamera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-flip-camera.imageset/flipCamera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-flip-camera.imageset/flipCamera.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-live-off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live-off.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-live-off.imageset/icon-live-off.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-live-off.imageset/icon-live-off.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-live-on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live-on.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-live-on.imageset/icon-live-on.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-live-on.imageset/icon-live-on.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-live.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-live.imageset/icon-live.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-live.imageset/icon-live.pdf -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-pano.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-pano.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/Assets.xcassets/icon-pano.imageset/icon-pano.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/samples/Assets.xcassets/icon-pano.imageset/icon-pano.pdf -------------------------------------------------------------------------------- /samples/CustomViews/CustomImageCell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | using Softeq.ImagePicker.Views; 4 | using UIKit; 5 | 6 | namespace Softeq.ImagePicker.Sample.CustomViews; 7 | 8 | [Register(nameof(CustomImageCell))] 9 | public partial class CustomImageCell : ImagePickerAssetCell 10 | { 11 | 12 | public UIImageView SubtypeImage => SubtypeImageView; 13 | 14 | public CustomImageCell(IntPtr handle) : base(handle) 15 | { 16 | } 17 | 18 | public override UIImageView ImageView => InternalImageView; 19 | 20 | public override bool Selected 21 | { 22 | get => base.Selected; 23 | set 24 | { 25 | base.Selected = value; 26 | SelectedImageView.Hidden = !Selected; 27 | } 28 | } 29 | 30 | [Export("awakeFromNib")] 31 | public override void AwakeFromNib() 32 | { 33 | base.AwakeFromNib(); 34 | 35 | SubtypeImageView.BackgroundColor = UIColor.Clear; 36 | 37 | SelectedImageView.Hidden = !Selected; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/CustomViews/CustomImageCell.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio to store outlets and 4 | // actions made in the UI designer. If it is removed, they will be lost. 5 | // Manual changes to this file may not be handled correctly. 6 | // 7 | 8 | using Foundation; 9 | 10 | namespace Softeq.ImagePicker.Sample.CustomViews 11 | { 12 | partial class CustomImageCell 13 | { 14 | [Outlet] 15 | UIKit.UIImageView InternalImageView { get; set; } 16 | 17 | [Outlet] 18 | UIKit.UIImageView SelectedImageView { get; set; } 19 | 20 | [Outlet] 21 | UIKit.UIImageView SubtypeImageView { get; set; } 22 | 23 | void ReleaseDesignerOutlets () 24 | { 25 | if (InternalImageView != null) { 26 | InternalImageView.Dispose (); 27 | InternalImageView = null; 28 | } 29 | 30 | if (SelectedImageView != null) { 31 | SelectedImageView.Dispose (); 32 | SelectedImageView = null; 33 | } 34 | 35 | if (SubtypeImageView != null) { 36 | SubtypeImageView.Dispose (); 37 | SubtypeImageView = null; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/CustomViews/CustomImageCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /samples/CustomViews/CustomVideoCell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | using Softeq.ImagePicker.Views; 4 | using UIKit; 5 | 6 | namespace Softeq.ImagePicker.Sample.CustomViews; 7 | 8 | [Register(nameof(CustomVideoCell))] 9 | public partial class CustomVideoCell : ImagePickerAssetCell 10 | { 11 | public override UIImageView ImageView => InternalImageView; 12 | public UILabel Label => InternalLabel; 13 | 14 | public CustomVideoCell(IntPtr handle) : base(handle) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /samples/CustomViews/CustomVideoCell.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio to store outlets and 4 | // actions made in the UI designer. If it is removed, they will be lost. 5 | // Manual changes to this file may not be handled correctly. 6 | // 7 | 8 | using Foundation; 9 | 10 | namespace Softeq.ImagePicker.Sample.CustomViews 11 | { 12 | partial class CustomVideoCell 13 | { 14 | [Outlet] 15 | UIKit.UIImageView InternalImageView { get; set; } 16 | 17 | [Outlet] 18 | UIKit.UILabel InternalLabel { get; set; } 19 | 20 | void ReleaseDesignerOutlets () 21 | { 22 | if (InternalImageView != null) { 23 | InternalImageView.Dispose (); 24 | InternalImageView = null; 25 | } 26 | 27 | if (InternalLabel != null) { 28 | InternalLabel.Dispose (); 29 | InternalLabel = null; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/CustomViews/CustomVideoCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /samples/CustomViews/IconWithTextCell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | using UIKit; 4 | 5 | namespace Softeq.ImagePicker.Sample.CustomViews; 6 | 7 | [Register(nameof(IconWithTextCell))] 8 | public partial class IconWithTextCell : UICollectionViewCell 9 | { 10 | public UILabel Label => TitleLabel; 11 | public UIImageView ImageView => InternalImageView; 12 | 13 | public IconWithTextCell(IntPtr handle) : base(handle) 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /samples/CustomViews/IconWithTextCell.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio to store outlets and 4 | // actions made in the UI designer. If it is removed, they will be lost. 5 | // Manual changes to this file may not be handled correctly. 6 | // 7 | 8 | using Foundation; 9 | 10 | namespace Softeq.ImagePicker.Sample.CustomViews 11 | { 12 | partial class IconWithTextCell 13 | { 14 | [Outlet] 15 | UIKit.NSLayoutConstraint BottomOffset { get; set; } 16 | 17 | [Outlet] 18 | UIKit.UIImageView InternalImageView { get; set; } 19 | 20 | [Outlet] public UIKit.UILabel TitleLabel { get; set; } 21 | 22 | [Outlet] 23 | UIKit.NSLayoutConstraint TopOffset { get; set; } 24 | 25 | void ReleaseDesignerOutlets () 26 | { 27 | if (InternalImageView != null) { 28 | InternalImageView.Dispose (); 29 | InternalImageView = null; 30 | } 31 | 32 | if (TitleLabel != null) { 33 | TitleLabel.Dispose (); 34 | TitleLabel = null; 35 | } 36 | 37 | if (TopOffset != null) { 38 | TopOffset.Dispose (); 39 | TopOffset = null; 40 | } 41 | 42 | if (BottomOffset != null) { 43 | BottomOffset.Dispose (); 44 | BottomOffset = null; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/Entitlements.plist: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/ImagePickerControllerDataSource.cs: -------------------------------------------------------------------------------- 1 | using CoreGraphics; 2 | using Photos; 3 | using UIKit; 4 | 5 | namespace Softeq.ImagePicker.Sample; 6 | 7 | public class ImagePickerControllerDataSource : Softeq.ImagePicker.Public.ImagePickerControllerDataSource 8 | { 9 | public override UIView ImagePicker(PHAuthorizationStatus status) 10 | { 11 | var infoLabel = new UILabel(CGRect.Empty) 12 | { 13 | BackgroundColor = UIColor.Green, 14 | TextAlignment = UITextAlignment.Center, 15 | Lines = 0 16 | }; 17 | switch (status) 18 | { 19 | case PHAuthorizationStatus.Restricted: 20 | infoLabel.Text = "Access is restricted\n\nPlease open Settings app and update privacy settings."; 21 | break; 22 | case PHAuthorizationStatus.Denied: 23 | infoLabel.Text = 24 | "Access is denied by user\n\nPlease open Settings app and update privacy settings."; 25 | break; 26 | } 27 | 28 | return infoLabel; 29 | } 30 | } -------------------------------------------------------------------------------- /samples/ImagePickerControllerDelegate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Foundation; 4 | using Photos; 5 | using Softeq.ImagePicker.Infrastructure.Extensions; 6 | using Softeq.ImagePicker.Public; 7 | using Softeq.ImagePicker.Sample.CustomViews; 8 | using Softeq.ImagePicker.Views; 9 | using UIKit; 10 | 11 | namespace Softeq.ImagePicker.Sample; 12 | 13 | public class ImagePickerControllerDelegate : Softeq.ImagePicker.Public.Delegates.ImagePickerControllerDelegate 14 | { 15 | public Action? DidSelectActionItemAction { get; set; } 16 | public Action>? DidSelectAssetAction { get; set; } 17 | public Action>? DidDeselectAssetAction { get; set; } 18 | public Action? DidTakeAssetAction { get; set; } 19 | 20 | public override void DidSelectActionItemAt(ImagePickerController controller, int index) 21 | { 22 | DidSelectActionItemAction?.Invoke(index); 23 | } 24 | 25 | public override void DidSelectAsset(ImagePickerController controller, PHAsset asset) 26 | { 27 | DidSelectAssetAction?.Invoke(controller.SelectedAssets); 28 | } 29 | 30 | public override void DidDeselectAsset(ImagePickerController controller, PHAsset asset) 31 | { 32 | DidDeselectAssetAction?.Invoke(controller.SelectedAssets); 33 | } 34 | 35 | public override void DidTake(UIImage image) 36 | { 37 | DidTakeAssetAction?.Invoke(image); 38 | } 39 | 40 | public override void WillDisplayActionItem(ImagePickerController controller, UICollectionViewCell cell, 41 | int index) 42 | { 43 | if (cell is IconWithTextCell iconWithTextCell) 44 | { 45 | iconWithTextCell.TitleLabel.TextColor = UIColor.Black; 46 | 47 | switch (index) 48 | { 49 | case 0: 50 | iconWithTextCell.TitleLabel.Text = "Camera"; 51 | iconWithTextCell.ImageView.Image = UIImageExtensions.FromBundle(BundleAssets.ButtonCamera); 52 | break; 53 | case 1: 54 | iconWithTextCell.TitleLabel.Text = "Photos"; 55 | iconWithTextCell.ImageView.Image = 56 | UIImageExtensions.FromBundle(BundleAssets.ButtonPhotoLibrary); 57 | break; 58 | } 59 | } 60 | } 61 | 62 | public override void WillDisplayAssetItem(ImagePickerController controller, ImagePickerAssetCell cell, 63 | PHAsset asset) 64 | { 65 | switch (cell) 66 | { 67 | case var _ when cell is CustomVideoCell videoCell: 68 | videoCell.Label.Text = GetDurationFormatter().StringFromTimeInterval(asset.Duration); 69 | break; 70 | case var _ when cell is CustomImageCell imageCell: 71 | switch (asset.MediaSubtypes) 72 | { 73 | case PHAssetMediaSubtype.PhotoLive: 74 | imageCell.SubtypeImage.Image = UIImage.FromBundle("icon-live"); 75 | break; 76 | case PHAssetMediaSubtype.PhotoPanorama: 77 | imageCell.SubtypeImage.Image = UIImage.FromBundle("icon-pano"); 78 | break; 79 | default: 80 | { 81 | if (UIDevice.CurrentDevice.CheckSystemVersion(10, 2) && 82 | asset.MediaSubtypes == PHAssetMediaSubtype.PhotoDepthEffect) 83 | { 84 | imageCell.SubtypeImage.Image = UIImage.FromBundle("icon-depth"); 85 | } 86 | 87 | break; 88 | } 89 | } 90 | 91 | break; 92 | } 93 | } 94 | 95 | private static NSDateComponentsFormatter GetDurationFormatter() 96 | { 97 | var formatter = new NSDateComponentsFormatter 98 | { 99 | UnitsStyle = NSDateComponentsFormatterUnitsStyle.Positional, 100 | AllowedUnits = NSCalendarUnit.Minute | NSCalendarUnit.Second, 101 | ZeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehavior.Pad 102 | }; 103 | return formatter; 104 | } 105 | } -------------------------------------------------------------------------------- /samples/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.softeq.imagepicker 7 | CFBundleShortVersionString 8 | 1.0 9 | CFBundleVersion 10 | 1.0 11 | LSRequiresIPhoneOS 12 | 13 | MinimumOSVersion 14 | 10.1 15 | UIDeviceFamily 16 | 17 | 1 18 | 2 19 | 20 | UILaunchStoryboardName 21 | LaunchScreen 22 | UIMainStoryboardFile 23 | Main 24 | UIRequiredDeviceCapabilities 25 | 26 | armv7 27 | 28 | UISupportedInterfaceOrientations 29 | 30 | UIInterfaceOrientationPortrait 31 | UIInterfaceOrientationLandscapeLeft 32 | UIInterfaceOrientationLandscapeRight 33 | 34 | NSPhotoLibraryUsageDescription 35 | App uses access to Photos when picking images 36 | NSCameraUsageDescription 37 | Camera is used by Image picker when taking new photos or recording videos 38 | NSMicrophoneUsageDescription 39 | Microphone is used by Image picker when recording videos 40 | CFBundleName 41 | Image Picker 42 | XSAppIconAssets 43 | Assets.xcassets/AppIcon.appiconset 44 | 45 | 46 | -------------------------------------------------------------------------------- /samples/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /samples/Main.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace Softeq.ImagePicker.Sample 4 | { 5 | public class Application 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, typeof(AppDelegate)); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /samples/Models/CellItemModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | using Softeq.ImagePicker.Sample.Models.Enums; 4 | using UIKit; 5 | 6 | namespace Softeq.ImagePicker.Sample.Models; 7 | 8 | public class CellItemModel 9 | { 10 | public string Title { get; } 11 | public Action Selector { get; } 12 | public Action ConfigBlock { get; } 13 | public SelectorArgument SelectorArgument { get; set; } 14 | 15 | public CellItemModel(string title, Action selector, Action configBlock) 16 | { 17 | Title = title; 18 | Selector = selector; 19 | ConfigBlock = configBlock; 20 | } 21 | } -------------------------------------------------------------------------------- /samples/Models/Enums/AssetsSource.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Sample.Models.Enums; 2 | 3 | public enum AssetsSource 4 | { 5 | RecentlyAdded = 0, 6 | OnlyVideos = 1, 7 | OnlySelfies = 2 8 | } -------------------------------------------------------------------------------- /samples/Models/Enums/CameraItemConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Sample.Models.Enums; 2 | 3 | public enum CameraItemConfig 4 | { 5 | Enabled = 0, 6 | Disabled = 1 7 | } -------------------------------------------------------------------------------- /samples/Models/Enums/SelectorArgument.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Sample.Models.Enums; 2 | 3 | public enum SelectorArgument 4 | { 5 | IndexPath, 6 | None 7 | } -------------------------------------------------------------------------------- /samples/Softeq.ImagePicker.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0-ios 4 | Exe 5 | enable 6 | 10.1 7 | iPhone;iPhoneSimulator 8 | iossimulator-x64 9 | 10 | 11 | false 12 | ios-arm64 13 | 14 | 15 | false 16 | 17 | 18 | false 19 | ios-arm64 20 | 21 | 22 | false 23 | 24 | 25 | 26 | ViewController.cs 27 | 28 | 29 | 30 | 31 | 32 | Assets.xcassets\Contents.json 33 | 34 | 35 | Assets.xcassets\AppIcon.appiconset\Contents.json 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /samples/ViewController.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio from the outlets and 4 | // actions declared in your storyboard file. 5 | // Manual changes to this file will not be maintained. 6 | // 7 | 8 | using Foundation; 9 | 10 | namespace Softeq.ImagePicker.Sample 11 | { 12 | [Register ("ViewController")] 13 | partial class ViewController 14 | { 15 | void ReleaseDesignerOutlets () 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/background-rounded.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "background-rounded.pdf", 6 | "resizing" : { 7 | "mode" : "9-part", 8 | "center" : { 9 | "mode" : "tile", 10 | "width" : 1, 11 | "height" : 1 12 | }, 13 | "cap-insets" : { 14 | "bottom" : 12, 15 | "top" : 12, 16 | "right" : 12, 17 | "left" : 12 18 | } 19 | } 20 | } 21 | ], 22 | "info" : { 23 | "version" : 1, 24 | "author" : "xcode" 25 | }, 26 | "properties" : { 27 | "template-rendering-intent" : "template" 28 | } 29 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/background-rounded.imageset/background-rounded.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/background-rounded.imageset/background-rounded.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/button-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-camera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/button-camera.imageset/button-camera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/button-camera.imageset/button-camera.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/button-photo-library.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-photo-library.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/gradient.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "gradient.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "gradient@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "gradient@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/gradient.imageset/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/gradient.imageset/gradient.png -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/gradient.imageset/gradient@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/gradient.imageset/gradient@2x.png -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/gradient.imageset/gradient@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/gradient.imageset/gradient@3x.png -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-badge-livephoto.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-badge-livephoto.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-badge-livephoto.imageset/icon-badge-livephoto.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/icon-badge-livephoto.imageset/icon-badge-livephoto.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-badge-video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-badge-video.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-badge-video.imageset/icon-badge-video.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/icon-badge-video.imageset/icon-badge-video.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-check-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-ckeck-background.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-check-background.imageset/icon-ckeck-background.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/icon-check-background.imageset/icon-ckeck-background.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-check.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-check.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-check.imageset/icon-check.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/icon-check.imageset/icon-check.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-flip-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "flipCamera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-flip-camera.imageset/flipCamera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/icon-flip-camera.imageset/flipCamera.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-live-off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live-off.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-live-off.imageset/icon-live-off.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/icon-live-off.imageset/icon-live-off.pdf -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-live-on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live-on.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /src/Assets/Assets.xcassets/icon-live-on.imageset/icon-live-on.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Softeq/ImagePicker-xamarin-ios/e8f7d8eab7e753004e71fb862f8cfec989a21963/src/Assets/Assets.xcassets/icon-live-on.imageset/icon-live-on.pdf -------------------------------------------------------------------------------- /src/Defines.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker; 2 | 3 | public static class Defines 4 | { 5 | public static class Common 6 | { 7 | public const int SecondsInHour = 3600; 8 | public const int SecondsInMinute = 60; 9 | } 10 | 11 | //TODO: Move all colors to the ColorSet and up IOS version to 11 12 | public static class Colors 13 | { 14 | public static readonly UIColor OrangeColor = UIColor.FromRGBA(234 / 255f, 53 / 255f, 52 / 255f, 1); 15 | public static readonly UIColor YellowColor = UIColor.FromRGBA(245 / 255f, 203 / 255f, 47 / 255f, 1); 16 | public static readonly UIColor GrayColor = UIColor.FromRGBA(208 / 255f, 213 / 255f, 218 / 255f, 1); 17 | } 18 | } -------------------------------------------------------------------------------- /src/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using ObjCRuntime; 2 | global using CoreFoundation; 3 | global using CoreMedia; 4 | global using CoreAnimation; 5 | global using AVFoundation; 6 | global using Photos; -------------------------------------------------------------------------------- /src/ImagePickerAssetModel.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker; 2 | 3 | public class ImagePickerAssetModel 4 | { 5 | private const int FetchLimit = 1000; 6 | private PHFetchResult _updatedFetchResult; 7 | private readonly Lazy _defaultFetchResult = new Lazy(FetchAssets); 8 | 9 | public PHCachingImageManager ImageManager { get; } 10 | public CGSize ThumbnailSize { get; set; } 11 | public PHFetchResult FetchResult => _updatedFetchResult ?? _defaultFetchResult.Value; 12 | 13 | public ImagePickerAssetModel() 14 | { 15 | ImageManager = new PHCachingImageManager(); 16 | } 17 | 18 | public void UpdateFetchResult(PHFetchResult fetchResult) 19 | { 20 | _updatedFetchResult = fetchResult; 21 | } 22 | 23 | private static PHFetchResult FetchAssets() 24 | { 25 | const string sortType = "creationDate"; 26 | 27 | var assetsOptions = new PHFetchOptions 28 | { 29 | SortDescriptors = new[] 30 | { 31 | new NSSortDescriptor(sortType, false) 32 | }, 33 | FetchLimit = FetchLimit 34 | }; 35 | 36 | return PHAssetCollection.FetchAssetCollections(PHAssetCollectionType.SmartAlbum, 37 | PHAssetCollectionSubtype.SmartAlbumUserLibrary, null).firstObject is PHAssetCollection assetCollection 38 | ? PHAsset.FetchAssets(assetCollection, assetsOptions) 39 | : PHAsset.FetchAssets(assetsOptions); 40 | } 41 | } -------------------------------------------------------------------------------- /src/ImagePickerDataSource.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure; 2 | using Softeq.ImagePicker.Public; 3 | using Softeq.ImagePicker.Views; 4 | 5 | namespace Softeq.ImagePicker; 6 | 7 | /// 8 | /// Datasource for a collection view that is used by Image Picker VC. 9 | /// 10 | public class ImagePickerDataSource : UICollectionViewDataSource 11 | { 12 | private LayoutModel _layoutModel; 13 | public CellRegistrator CellRegistrator; 14 | public ImagePickerAssetModel AssetsModel { get; } 15 | 16 | public ImagePickerDataSource(ImagePickerAssetModel assetsModel) 17 | { 18 | AssetsModel = assetsModel; 19 | _layoutModel = new LayoutModel(new LayoutConfiguration()); 20 | } 21 | 22 | public override nint GetItemsCount(UICollectionView collectionView, nint section) 23 | { 24 | return _layoutModel.NumberOfItems((int)section); 25 | } 26 | 27 | public override nint NumberOfSections(UICollectionView collectionView) 28 | { 29 | return _layoutModel.NumberOfSections; 30 | } 31 | 32 | public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath) 33 | { 34 | if (CellRegistrator == null) 35 | { 36 | throw new ImagePickerException("cells registrator must be set at this moment"); 37 | } 38 | 39 | switch (indexPath.Section) 40 | { 41 | case 0: 42 | return GetActionCell(collectionView, indexPath); 43 | case 1: 44 | return GetCameraCell(collectionView, indexPath); 45 | case 2: 46 | return GetAssetCell(collectionView, indexPath); 47 | default: throw new ImagePickerException("only 3 sections are supported"); 48 | } 49 | } 50 | 51 | public void UpdateLayoutModel(LayoutModel layoutModel) 52 | { 53 | _layoutModel = layoutModel; 54 | } 55 | 56 | private UICollectionViewCell GetActionCell(UICollectionView collectionView, NSIndexPath indexPath) 57 | { 58 | var identifier = CellRegistrator.CellIdentifier(indexPath.Row); 59 | 60 | if (identifier == null) 61 | { 62 | throw new ArgumentException( 63 | $"there is an action item at index {indexPath.Row} but no cell is registered."); 64 | } 65 | 66 | return collectionView.DequeueReusableCell(identifier, indexPath) as UICollectionViewCell; 67 | } 68 | 69 | private UICollectionViewCell GetCameraCell(UICollectionView collectionView, NSIndexPath indexPath) 70 | { 71 | if (collectionView.DequeueReusableCell(CellRegistrator.CellIdentifierForCameraItem, indexPath) is 72 | CameraCollectionViewCell result) 73 | { 74 | return result; 75 | } 76 | 77 | throw new ArgumentException( 78 | "there is a camera item but no cell class `CameraCollectionViewCell` is registered."); 79 | } 80 | 81 | private UICollectionViewCell GetAssetCell(UICollectionView collectionView, NSIndexPath indexPath) 82 | { 83 | var asset = (PHAsset)AssetsModel.FetchResult.ObjectAt(indexPath.Item); 84 | 85 | var cellIdentifier = CellRegistrator.CellIdentifier(asset.MediaType) ?? 86 | CellRegistrator.CellIdentifierForAssetItems; 87 | 88 | if (!(collectionView.DequeueReusableCell(cellIdentifier, indexPath) is ImagePickerAssetCell cell)) 89 | { 90 | throw new ArgumentException( 91 | $"asset item cell must conform to {nameof(ImagePickerAssetCell)} protocol"); 92 | } 93 | 94 | // Request an image for the asset from the PHCachingImageManager. 95 | cell.RepresentedAssetIdentifier = asset.LocalIdentifier; 96 | 97 | AssetsModel.ImageManager.RequestImageForAsset(asset, AssetsModel.ThumbnailSize, 98 | PHImageContentMode.AspectFill, 99 | null, (image, info) => 100 | { 101 | // The cell may have been recycled by the time this handler gets called; 102 | // set the cell's thumbnail image only if it's still showing the same asset. 103 | if (cell.RepresentedAssetIdentifier == asset.LocalIdentifier && image != null) 104 | { 105 | cell.ImageView.Image = image; 106 | } 107 | }); 108 | 109 | return cell; 110 | } 111 | } -------------------------------------------------------------------------------- /src/ImagePickerDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure; 2 | using Softeq.ImagePicker.Infrastructure.Interfaces; 3 | using Softeq.ImagePicker.Public; 4 | using Softeq.ImagePicker.Views; 5 | 6 | namespace Softeq.ImagePicker; 7 | 8 | public class ImagePickerDelegate : UICollectionViewDelegateFlowLayout 9 | { 10 | private readonly IImagePickerDelegate _delegate; 11 | 12 | public ImagePickerLayout Layout { get; } 13 | 14 | public ImagePickerDelegate(ImagePickerLayout layout, IImagePickerDelegate imagePickerDelegate = null) 15 | { 16 | Layout = layout; 17 | _delegate = imagePickerDelegate; 18 | } 19 | 20 | public override CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, 21 | NSIndexPath indexPath) 22 | { 23 | return Layout.CollectionView(collectionView, layout, indexPath); 24 | } 25 | 26 | public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout, 27 | nint section) 28 | { 29 | return Layout.CollectionView(collectionView, layout, (int)section); 30 | } 31 | 32 | public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath) 33 | { 34 | if (indexPath.Section == Layout.Configuration.SectionIndexForAssets) 35 | { 36 | _delegate?.DidSelectAssetItemAt(indexPath.Row); 37 | } 38 | } 39 | 40 | public override void ItemDeselected(UICollectionView collectionView, NSIndexPath indexPath) 41 | { 42 | if (indexPath.Section == Layout.Configuration.SectionIndexForAssets) 43 | { 44 | _delegate?.DidDeselectAssetItemAt(indexPath.Row); 45 | } 46 | } 47 | 48 | public override bool ShouldSelectItem(UICollectionView collectionView, NSIndexPath indexPath) 49 | { 50 | return ShouldSelectItem(indexPath.Section, Layout.Configuration); 51 | } 52 | 53 | public override bool ShouldHighlightItem(UICollectionView collectionView, NSIndexPath indexPath) 54 | { 55 | return ShouldHighlightItem(indexPath.Section, Layout.Configuration); 56 | } 57 | 58 | public override void ItemHighlighted(UICollectionView collectionView, NSIndexPath indexPath) 59 | { 60 | if (indexPath.Section == Layout.Configuration.SectionIndexForActions) 61 | { 62 | _delegate?.DidSelectActionItemAt(indexPath.Row); 63 | } 64 | } 65 | 66 | public override void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, 67 | NSIndexPath indexPath) 68 | { 69 | switch (indexPath.Section) 70 | { 71 | case var section when section == Layout.Configuration.SectionIndexForActions: 72 | _delegate?.WillDisplayActionCell(cell, indexPath.Row); 73 | break; 74 | case var section when section == Layout.Configuration.SectionIndexForCamera: 75 | _delegate?.WillDisplayCameraCell(cell as CameraCollectionViewCell); 76 | break; 77 | case var section when section == Layout.Configuration.SectionIndexForAssets: 78 | _delegate?.WillDisplayAssetCell(cell as ImagePickerAssetCell, indexPath.Row); 79 | break; 80 | default: throw new ImagePickerException("index path not supported"); 81 | } 82 | } 83 | 84 | public override void CellDisplayingEnded(UICollectionView collectionView, UICollectionViewCell cell, 85 | NSIndexPath indexPath) 86 | { 87 | switch (indexPath.Section) 88 | { 89 | case var section when section == Layout.Configuration.SectionIndexForCamera: 90 | _delegate?.DidEndDisplayingCameraCell(cell as CameraCollectionViewCell); 91 | break; 92 | case var section when section == Layout.Configuration.SectionIndexForActions || 93 | section == Layout.Configuration.SectionIndexForAssets: 94 | break; 95 | default: throw new ImagePickerException("index path not supported"); 96 | } 97 | } 98 | 99 | public override void Scrolled(UIScrollView scrollView) 100 | { 101 | _delegate?.DidScroll(scrollView); 102 | } 103 | 104 | /// 105 | /// We allow selecting only asset items, action items are only highlighted, 106 | /// camera item is untouched. 107 | /// 108 | private static bool ShouldSelectItem(int section, LayoutConfiguration layoutConfiguration) 109 | { 110 | if (layoutConfiguration.SectionIndexForActions == section || 111 | layoutConfiguration.SectionIndexForCamera == section) 112 | { 113 | return false; 114 | } 115 | 116 | return true; 117 | } 118 | 119 | private static bool ShouldHighlightItem(int section, LayoutConfiguration layoutConfiguration) 120 | { 121 | return layoutConfiguration.SectionIndexForCamera != section; 122 | } 123 | } -------------------------------------------------------------------------------- /src/ImagePickerLayout.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure; 2 | using Softeq.ImagePicker.Public; 3 | 4 | namespace Softeq.ImagePicker; 5 | 6 | /// 7 | /// A helper class that contains all code and logic when doing layout of collection 8 | /// view cells. This is used solely by collection view's delegate. Typically 9 | /// this code should be part of regular subclass of UICollectionViewLayout, however, 10 | /// since we are using UICollectionViewFlowLayout we have to do this workaround. 11 | /// 12 | public class ImagePickerLayout 13 | { 14 | public readonly LayoutConfiguration Configuration; 15 | 16 | public ImagePickerLayout(LayoutConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | /// Returns size for item considering number of rows and scroll direction, if preferredWidthOrHeight is nil, square size is returned 22 | public CGSize SizeForItem(int numberOfItemsInRow, nfloat? preferredWidthOrHeight, 23 | UICollectionView collectionView, 24 | UICollectionViewScrollDirection scrollDirection) 25 | { 26 | switch (scrollDirection) 27 | { 28 | case UICollectionViewScrollDirection.Horizontal: 29 | var itemHeight = collectionView.Frame.Height; 30 | itemHeight -= collectionView.ContentInset.Top + collectionView.ContentInset.Bottom; 31 | itemHeight -= (numberOfItemsInRow - 1) * Configuration.InterItemSpacing; 32 | itemHeight /= numberOfItemsInRow; 33 | return new CGSize(preferredWidthOrHeight ?? itemHeight, itemHeight); 34 | 35 | case UICollectionViewScrollDirection.Vertical: 36 | var itemWidth = collectionView.Frame.Width; 37 | itemWidth -= collectionView.ContentInset.Left + collectionView.ContentInset.Right; 38 | itemWidth -= (numberOfItemsInRow - 1) * Configuration.InterItemSpacing; 39 | itemWidth /= numberOfItemsInRow; 40 | return new CGSize(itemWidth, preferredWidthOrHeight ?? itemWidth); 41 | default: 42 | throw new ArgumentException("Should be invoked only with UICollectionViewScrollDirection"); 43 | } 44 | } 45 | 46 | public CGSize CollectionView(UICollectionView collectionView, UICollectionViewLayout collectionViewLayout, 47 | NSIndexPath indexPath) 48 | { 49 | if (!(collectionViewLayout is UICollectionViewFlowLayout layout)) 50 | { 51 | throw new ImagePickerException("currently only UICollectionViewFlowLayout is supported"); 52 | } 53 | 54 | var layoutModel = new LayoutModel(Configuration); 55 | 56 | switch (indexPath.Section) 57 | { 58 | case 0: 59 | //this will make sure that action item is either square if there are 2 items, 60 | //or a rectangle if there is only 1 item 61 | //let width = sizeForItem(numberOfItemsInRow: 2, preferredWidthOrHeight: nil, collectionView: collectionView, scrollDirection: layout.scrollDirection).width 62 | nfloat ratio = 0.25f; 63 | nfloat width = collectionView.Frame.Width * ratio; 64 | return SizeForItem(layoutModel.NumberOfItems(Configuration.SectionIndexForActions), 65 | width, collectionView, layout.ScrollDirection); 66 | 67 | case 1: 68 | //lets keep this ratio so camera item is a nice rectangle 69 | 70 | var traitCollection = collectionView.TraitCollection; 71 | 72 | ratio = 160f / 212f; 73 | 74 | switch (traitCollection.UserInterfaceIdiom) 75 | { 76 | case var _ 77 | when traitCollection.HorizontalSizeClass == UIUserInterfaceSizeClass.Unspecified && 78 | traitCollection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact: 79 | case var _ when traitCollection.HorizontalSizeClass == UIUserInterfaceSizeClass.Regular && 80 | traitCollection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact: 81 | case var _ when traitCollection.HorizontalSizeClass == UIUserInterfaceSizeClass.Compact && 82 | traitCollection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact: 83 | ratio = 1 / ratio; 84 | break; 85 | } 86 | 87 | var widthOrHeight = collectionView.Frame.Height * ratio; 88 | return SizeForItem(layoutModel.NumberOfItems(Configuration.SectionIndexForCamera), 89 | widthOrHeight, collectionView, layout.ScrollDirection); 90 | case 2: 91 | //make sure there is at least 1 item, othewise invalid layout 92 | if (Configuration.NumberOfAssetItemsInRow < 0) 93 | { 94 | throw new ImagePickerException( 95 | "invalid layout - numberOfAssetItemsInRow must be > 0, check your layout configuration "); 96 | } 97 | 98 | return SizeForItem(Configuration.NumberOfAssetItemsInRow, null, collectionView, 99 | layout.ScrollDirection); 100 | default: 101 | throw new ImagePickerException("unexpected sections count"); 102 | } 103 | } 104 | 105 | public UIEdgeInsets CollectionView(UICollectionView collectionView, UICollectionViewLayout collectionViewLayout, 106 | int section) 107 | { 108 | if (!(collectionViewLayout is UICollectionViewFlowLayout layout)) 109 | { 110 | throw new ImagePickerException("currently only UICollectionViewFlowLayout is supported"); 111 | } 112 | 113 | // helper method that creates edge insets considering scroll direction 114 | UIEdgeInsets SectionInsets(nfloat inset) 115 | { 116 | switch (layout.ScrollDirection) 117 | { 118 | case UICollectionViewScrollDirection.Horizontal: 119 | return new UIEdgeInsets(0, 0, 0, inset); 120 | case UICollectionViewScrollDirection.Vertical: 121 | return new UIEdgeInsets(0, 0, inset, 0); 122 | default: 123 | throw new ImagePickerException("unexpected enum"); 124 | } 125 | } 126 | 127 | var layoutModel = new LayoutModel(Configuration); 128 | 129 | switch (section) 130 | { 131 | case 0 when layoutModel.NumberOfItems(section) > 0: 132 | return SectionInsets(Configuration.ActionSectionSpacing); 133 | case 1 when layoutModel.NumberOfItems(section) > 0: 134 | return SectionInsets(Configuration.CameraSectionSpacing); 135 | default: 136 | return UIEdgeInsets.Zero; 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/Infrastructure/Enums/CameraMode.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Infrastructure.Enums; 2 | 3 | public enum CameraMode 4 | { 5 | /// 6 | /// If you support only photos use this preset. Default value. 7 | /// 8 | Photo, 9 | 10 | /// 11 | /// If you know you will use live photos use this preset. 12 | /// 13 | PhotoAndLivePhoto, 14 | 15 | /// 16 | /// If you wish to record videos or take photos. 17 | /// 18 | PhotoAndVideo 19 | } -------------------------------------------------------------------------------- /src/Infrastructure/Enums/LivePhotoMode.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Infrastructure.Enums; 2 | 3 | public enum LivePhotoMode 4 | { 5 | On, 6 | Off 7 | } -------------------------------------------------------------------------------- /src/Infrastructure/Enums/SessionSetupResult.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Infrastructure.Enums; 2 | 3 | public enum SessionSetupResult 4 | { 5 | Success, 6 | NotAuthorized, 7 | ConfigurationFailed 8 | } -------------------------------------------------------------------------------- /src/Infrastructure/Enums/VideoDisplayMode.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Infrastructure.Enums; 2 | 3 | public enum VideoDisplayMode 4 | { 5 | /// 6 | /// Preserve aspect ratio, fit within layer bounds. 7 | /// 8 | AspectFit, 9 | 10 | /// 11 | /// Preserve aspect ratio, fill view bounds. 12 | /// 13 | AspectFill, 14 | 15 | /// 16 | /// Stretch to fill layer bounds 17 | /// 18 | Resize 19 | } -------------------------------------------------------------------------------- /src/Infrastructure/Extensions/UICollectionViewExtensions.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Public; 2 | 3 | namespace Softeq.ImagePicker.Infrastructure.Extensions; 4 | 5 | public static class UICollectionViewExtensions 6 | { 7 | public static CameraCollectionViewCell GetCameraCell(this UICollectionView collectionView, 8 | LayoutConfiguration layout) 9 | { 10 | return collectionView.CellForItem(NSIndexPath.FromItemSection(0, layout.SectionIndexForCamera)) as 11 | CameraCollectionViewCell; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Infrastructure/Extensions/UIImageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Softeq.ImagePicker.Infrastructure.Extensions; 4 | 5 | public static class UIImageExtensions 6 | { 7 | public static UIImage FromBundle(BundleAssets asset) 8 | { 9 | return UIImage.FromBundle(GetDescription(asset)); 10 | } 11 | 12 | static string GetDescription(Enum en) 13 | { 14 | var type = en.GetType(); 15 | 16 | var memInfo = type.GetMember(en.ToString()); 17 | 18 | var attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false); 19 | 20 | return ((DescriptionAttribute)attrs[0]).Description; 21 | } 22 | } 23 | 24 | public enum BundleAssets 25 | { 26 | [Description("button-camera")] ButtonCamera, 27 | [Description("button-photo-library")] ButtonPhotoLibrary, 28 | [Description("icon-check-background")] IconCheckBackground, 29 | [Description("icon-check")] IconCheck, 30 | [Description("gradient")] Gradient, 31 | [Description("icon-badge-livephoto")] IconBadgeLivePhoto, 32 | [Description("icon-badge-video")] IconBadgeVideo, 33 | } -------------------------------------------------------------------------------- /src/Infrastructure/ImagePickerException.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Infrastructure; 2 | 3 | public class ImagePickerException : Exception 4 | { 5 | public ImagePickerException(string errorMessage) : base(errorMessage) 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Infrastructure/Interfaces/ICameraCollectionViewCellDelegate.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Infrastructure.Interfaces; 2 | 3 | public interface ICameraCollectionViewCellDelegate 4 | { 5 | void TakePicture(); 6 | void TakeLivePhoto(); 7 | void StartVideoRecording(); 8 | void StopVideoRecording(); 9 | void FlipCamera(Action action); 10 | } -------------------------------------------------------------------------------- /src/Infrastructure/Interfaces/ICaptureSessionDelegate.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Infrastructure.Interfaces; 2 | 3 | /// 4 | /// Groups a method that informs a delegate about progress and state of video recording. 5 | /// 6 | public interface ICaptureSessionDelegate 7 | { 8 | /// 9 | /// called when session is successfully configured and started running 10 | /// 11 | void CaptureSessionDidResume(); 12 | 13 | /// 14 | /// called when session is was manually suspended 15 | /// 16 | void CaptureSessionDidSuspend(); 17 | 18 | /// 19 | /// capture session was running but did fail due to any AV error reason. 20 | /// 21 | /// Error. 22 | void DidFail(AVError error); 23 | 24 | /// 25 | /// called when creating and configuring session but something failed (e.g. input or output could not be added, etc 26 | /// 27 | void DidFailConfiguringSession(); 28 | 29 | /// 30 | /// called when user denied access to video device when prompted 31 | /// 32 | /// Status. 33 | void CaptureGrantedSession(AVAuthorizationStatus status); 34 | 35 | /// 36 | /// Called when user grants access to video device when prompted 37 | /// 38 | /// Status. 39 | void CaptureFailedSession(AVAuthorizationStatus status); 40 | 41 | /// 42 | /// called when session is interrupted due to various reasons, for example when a phone call or user starts an audio using control center, etc. 43 | /// 44 | /// Reason. 45 | void WasInterrupted(NSString reason); 46 | 47 | /// 48 | /// called when and interruption is ended and the session was automatically resumed. 49 | /// 50 | void CaptureSessionInterruptionDidEnd(); 51 | } -------------------------------------------------------------------------------- /src/Infrastructure/Interfaces/ICaptureSessionVideoRecordingDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Media.Capture; 2 | 3 | namespace Softeq.ImagePicker.Infrastructure.Interfaces; 4 | 5 | /// 6 | /// Groups a method that informs a delegate about progress and state of photo capturing. 7 | /// 8 | public interface ICaptureSessionVideoRecordingDelegate 9 | { 10 | /// 11 | /// called when video file recording output is added to the session 12 | /// 13 | /// Session. 14 | void DidBecomeReadyForVideoRecording(VideoCaptureSession session); 15 | 16 | /// 17 | /// called when recording started 18 | /// 19 | /// Session. 20 | void DidStartVideoRecording(VideoCaptureSession session); 21 | 22 | /// 23 | /// called when cancel recording as a result of calling `cancelVideoRecording` func. 24 | /// 25 | /// Session. 26 | void DidCancelVideoRecording(VideoCaptureSession session); 27 | 28 | /// 29 | /// called when a recording was successfully finished 30 | /// 31 | /// Session. 32 | /// Video URL. 33 | void DidFinishVideoRecording(VideoCaptureSession session, NSUrl videoUrl); 34 | 35 | /// 36 | /// called when a recording was finished prematurely due to a system interruption 37 | /// (empty disk, app put on bg, etc). Video is however saved on provided URL or in assets library if turned on. 38 | /// 39 | /// Session. 40 | /// Video URL. 41 | /// Reason. 42 | void DidInterruptVideoRecording(VideoCaptureSession session, NSUrl videoUrl, NSError reason); 43 | 44 | /// 45 | /// called when a recording failed 46 | /// 47 | /// Session. 48 | /// Error. 49 | void DidFailVideoRecording(VideoCaptureSession session, NSError error); 50 | } -------------------------------------------------------------------------------- /src/Infrastructure/Interfaces/IImagePickerDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Public; 2 | using Softeq.ImagePicker.Views; 3 | 4 | namespace Softeq.ImagePicker.Infrastructure.Interfaces; 5 | 6 | public interface IImagePickerDelegate 7 | { 8 | /// 9 | /// Called when user selects one of action items 10 | /// 11 | /// Index. 12 | void DidSelectActionItemAt(int index); 13 | 14 | /// 15 | /// Called when user selects one of asset items 16 | /// 17 | /// Index. 18 | void DidSelectAssetItemAt(int index); 19 | 20 | /// 21 | /// Called when user deselects one of selected asset items 22 | /// 23 | /// Index. 24 | void DidDeselectAssetItemAt(int index); 25 | 26 | /// 27 | /// Called when action item is about to be displayed 28 | /// 29 | /// Cell. 30 | /// Index. 31 | void WillDisplayActionCell(UICollectionViewCell cell, int index); 32 | 33 | /// 34 | /// Called when camera item is about to be displayed 35 | /// 36 | /// Cell. 37 | void WillDisplayCameraCell(CameraCollectionViewCell cell); 38 | 39 | /// 40 | /// Called when camera item ended displaying 41 | /// 42 | /// Cell. 43 | void DidEndDisplayingCameraCell(CameraCollectionViewCell cell); 44 | 45 | void WillDisplayAssetCell(ImagePickerAssetCell cell, int index); 46 | 47 | void DidScroll(UIScrollView scrollView); 48 | } -------------------------------------------------------------------------------- /src/Infrastructure/Interfaces/ISessionPhotoCapturingDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Media.Capture; 2 | 3 | namespace Softeq.ImagePicker.Infrastructure.Interfaces; 4 | 5 | public interface ISessionPhotoCapturingDelegate 6 | { 7 | /// 8 | /// called as soon as the photo was taken, use this to update UI - for example show flash animation or live photo icon 9 | /// 10 | /// Settings. 11 | void WillCapturePhotoWith(AVCapturePhotoSettings settings); 12 | 13 | /// 14 | /// called when captured photo is processed and ready for use 15 | /// 16 | /// Did capture photo data. 17 | /// Settings. 18 | void DidCapturePhotoData(NSData didCapturePhotoData, AVCapturePhotoSettings settings); 19 | 20 | /// 21 | /// called when captured photo is processed and ready for use 22 | /// 23 | /// Error. 24 | void DidFailCapturingPhotoWith(NSError error); 25 | 26 | /// 27 | /// called when number of processing live photos changed, see inProgressLivePhotoCapturesCount for current count 28 | /// 29 | /// Session. 30 | void CaptureSessionDidChangeNumberOfProcessingLivePhotos(PhotoCaptureSession session); 31 | } -------------------------------------------------------------------------------- /src/LayoutModel.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Public; 2 | 3 | namespace Softeq.ImagePicker; 4 | 5 | public class LayoutModel 6 | { 7 | private readonly int[] _sections = { 0, 0, 0 }; 8 | public int NumberOfSections => _sections.Length; 9 | 10 | public LayoutModel(LayoutConfiguration configuration, int assets = 0) 11 | { 12 | var actionItems = configuration.ShowsFirstActionItem ? 1 : 0; 13 | actionItems += configuration.ShowsSecondActionItem ? 1 : 0; 14 | _sections[configuration.SectionIndexForActions] = actionItems; 15 | _sections[configuration.SectionIndexForCamera] = configuration.ShowsCameraItem ? 1 : 0; 16 | _sections[configuration.SectionIndexForAssets] = assets; 17 | } 18 | 19 | public int NumberOfItems(int section) 20 | { 21 | return _sections[section]; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Media/AVPreviewView.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Enums; 2 | 3 | namespace Softeq.ImagePicker.Media; 4 | 5 | /// 6 | /// A view whose layer is AVCaptureVideoPreviewLayer so it's used for previewing output from a capture session. 7 | /// 8 | public class AVPreviewView : UIView 9 | { 10 | private VideoDisplayMode _displayMode = VideoDisplayMode.AspectFill; 11 | public AVCaptureVideoPreviewLayer PreviewLayer => Layer as AVCaptureVideoPreviewLayer; 12 | 13 | public AVCaptureSession Session 14 | { 15 | get => PreviewLayer.Session; 16 | set 17 | { 18 | if (PreviewLayer.Session != null && PreviewLayer.Session.Equals(value)) 19 | { 20 | return; 21 | } 22 | 23 | PreviewLayer.Session = value; 24 | } 25 | } 26 | 27 | public VideoDisplayMode DisplayMode 28 | { 29 | get => _displayMode; 30 | set 31 | { 32 | _displayMode = value; 33 | ApplyVideoDisplayMode(); 34 | } 35 | } 36 | 37 | [Export("layerClass")] 38 | public static Class LayerClass() 39 | { 40 | return new Class(typeof(AVCaptureVideoPreviewLayer)); 41 | } 42 | 43 | public AVPreviewView(CGRect frame) : base(frame) 44 | { 45 | ApplyVideoDisplayMode(); 46 | } 47 | 48 | private void ApplyVideoDisplayMode() 49 | { 50 | switch (DisplayMode) 51 | { 52 | case VideoDisplayMode.AspectFill: 53 | PreviewLayer.VideoGravity = AVLayerVideoGravity.ResizeAspectFill; 54 | break; 55 | case VideoDisplayMode.AspectFit: 56 | PreviewLayer.VideoGravity = AVLayerVideoGravity.ResizeAspect; 57 | break; 58 | case VideoDisplayMode.Resize: 59 | PreviewLayer.VideoGravity = AVLayerVideoGravity.Resize; 60 | break; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Media/Capture/AudioCaptureSession.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Media.Capture; 2 | 3 | public class AudioCaptureSession 4 | { 5 | public void ConfigureSession(AVCaptureSession session) 6 | { 7 | Console.WriteLine("capture session: configuring - adding audio input"); 8 | 9 | // Add audio input, if fails no need to fail whole configuration 10 | var audioDevice = AVCaptureDevice.GetDefaultDevice(AVMediaTypes.Audio); 11 | var audioDeviceInput = AVCaptureDeviceInput.FromDevice(audioDevice); 12 | 13 | if (session.CanAddInput(audioDeviceInput)) 14 | { 15 | session.AddInput(audioDeviceInput); 16 | } 17 | else 18 | { 19 | Console.WriteLine("capture session: could not add audio device input to the session"); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Media/Capture/CaptureNotificationCenterHandler.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Interfaces; 2 | 3 | namespace Softeq.ImagePicker.Media.Capture; 4 | 5 | public class CaptureNotificationCenterHandler : NSObject 6 | { 7 | private bool _addedObservers; 8 | private NSObject _wasInterruptedNotification; 9 | private NSObject _interruptionEndedNotification; 10 | private readonly IntPtr _sessionRunningObserveContext = IntPtr.Zero; 11 | private readonly ICaptureSessionDelegate _delegate; 12 | 13 | private const string RunningObserverKeyPath = "running"; 14 | 15 | public CaptureNotificationCenterHandler(ICaptureSessionDelegate captureSessionDelegate) 16 | { 17 | _delegate = captureSessionDelegate; 18 | } 19 | 20 | public void AddObservers(AVCaptureSession session) 21 | { 22 | if (_addedObservers) 23 | { 24 | return; 25 | } 26 | 27 | session.AddObserver(this, RunningObserverKeyPath, NSKeyValueObservingOptions.New, _sessionRunningObserveContext); 28 | 29 | _wasInterruptedNotification = NSNotificationCenter.DefaultCenter.AddObserver( 30 | AVCaptureSession.WasInterruptedNotification, 31 | SessionWasInterrupted, session); 32 | _interruptionEndedNotification = NSNotificationCenter.DefaultCenter.AddObserver( 33 | AVCaptureSession.InterruptionEndedNotification, 34 | SessionInterruptionEnded, session); 35 | 36 | _addedObservers = true; 37 | } 38 | 39 | public void RemoveObservers(AVCaptureSession session) 40 | { 41 | if (_addedObservers != true) 42 | { 43 | return; 44 | } 45 | 46 | NSNotificationCenter.DefaultCenter.RemoveObserver(this); 47 | NSNotificationCenter.DefaultCenter.RemoveObserver(_wasInterruptedNotification); 48 | NSNotificationCenter.DefaultCenter.RemoveObserver(_interruptionEndedNotification); 49 | session.RemoveObserver(this, RunningObserverKeyPath, _sessionRunningObserveContext); 50 | 51 | _addedObservers = false; 52 | } 53 | 54 | private void SessionWasInterrupted(NSNotification notification) 55 | { 56 | /* 57 | In some scenarios we want to enable the user to resume the session running. 58 | For example, if music playback is initiated via control center while 59 | using AVCam, then the user can let AVCam resume 60 | the session running, which will stop music playback. Note that stopping 61 | music playback in control center will not automatically resume the session 62 | running. Also note that it is not always possible to resume, see `resumeInterruptedSession(_:)`. 63 | */ 64 | if (notification.UserInfo.ContainsKey(AVCaptureSession.InterruptionReasonKey) && 65 | !string.IsNullOrEmpty(AVCaptureSession.InterruptionReasonKey)) 66 | { 67 | Console.WriteLine( 68 | $"capture session: session was interrupted with reason {notification.UserInfo[AVCaptureSession.InterruptionReasonKey]}"); 69 | DispatchQueue.MainQueue.DispatchAsync(() => 70 | { 71 | _delegate?.WasInterrupted(AVCaptureSession.InterruptionReasonKey); 72 | }); 73 | } 74 | else 75 | { 76 | Console.WriteLine("capture session: session was interrupted due to unknown reason"); 77 | } 78 | } 79 | 80 | public override void ObserveValue(NSString keyPath, NSObject ofObject, NSDictionary change, IntPtr context) 81 | { 82 | if (context == _sessionRunningObserveContext) 83 | { 84 | var observableChange = new NSObservedChange(change); 85 | 86 | var isSessionRunning = (observableChange.NewValue as NSNumber)?.BoolValue; 87 | 88 | if (isSessionRunning == null) 89 | { 90 | return; 91 | } 92 | 93 | DispatchQueue.MainQueue.DispatchAsync(() => 94 | { 95 | Console.WriteLine($"capture session: is running - ${isSessionRunning}"); 96 | if (isSessionRunning.Value) 97 | { 98 | _delegate?.CaptureSessionDidResume(); 99 | } 100 | else 101 | { 102 | _delegate?.CaptureSessionDidSuspend(); 103 | } 104 | }); 105 | } 106 | else 107 | { 108 | base.ObserveValue(keyPath, ofObject, change, context); 109 | } 110 | } 111 | 112 | private void SessionInterruptionEnded(NSNotification notification) 113 | { 114 | Console.WriteLine("capture session: interruption ended"); 115 | DispatchQueue.MainQueue.DispatchAsync(() => { _delegate?.CaptureSessionInterruptionDidEnd(); }); 116 | } 117 | } -------------------------------------------------------------------------------- /src/Media/Capture/VideoCaptureSession.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure; 2 | using Softeq.ImagePicker.Infrastructure.Enums; 3 | using Softeq.ImagePicker.Infrastructure.Interfaces; 4 | using Softeq.ImagePicker.Media.Delegates; 5 | 6 | namespace Softeq.ImagePicker.Media.Capture; 7 | 8 | public class VideoCaptureSession 9 | { 10 | private AVCaptureMovieFileOutput _videoFileOutput; 11 | private readonly ICaptureSessionVideoRecordingDelegate _videoRecordingDelegate; 12 | private VideoCaptureDelegate _videoCaptureDelegate; 13 | private AudioCaptureSession _audioCaptureSession; 14 | private readonly VideoDeviceInputManager _videoDeviceInputManager; 15 | private readonly DispatchQueue _sessionQueue; 16 | 17 | public bool IsRecordingVideo => _videoFileOutput?.Recording ?? false; 18 | 19 | public VideoCaptureSession(Action action, 20 | ICaptureSessionVideoRecordingDelegate videoRecordingDelegate, DispatchQueue queue) 21 | { 22 | _videoRecordingDelegate = videoRecordingDelegate; 23 | _videoDeviceInputManager = new VideoDeviceInputManager(action); 24 | _sessionQueue = queue; 25 | } 26 | 27 | public SessionSetupResult ConfigureSession(AVCaptureSession session) 28 | { 29 | var inputDeviceConfigureResult = _videoDeviceInputManager.ConfigureVideoDeviceInput(session); 30 | 31 | if (inputDeviceConfigureResult != SessionSetupResult.Success) 32 | { 33 | return inputDeviceConfigureResult; 34 | } 35 | 36 | // Add movie file output. 37 | Console.WriteLine("capture session: configuring - adding movie file input"); 38 | 39 | var movieFileOutput = new AVCaptureMovieFileOutput(); 40 | if (session.CanAddOutput(movieFileOutput)) 41 | { 42 | session.AddOutput(movieFileOutput); 43 | _videoFileOutput = movieFileOutput; 44 | 45 | DispatchQueue.MainQueue.DispatchAsync(() => 46 | { 47 | _videoRecordingDelegate?.DidBecomeReadyForVideoRecording(this); 48 | }); 49 | } 50 | else 51 | { 52 | Console.WriteLine("capture session: could not add video output to the session"); 53 | return SessionSetupResult.ConfigurationFailed; 54 | } 55 | 56 | _audioCaptureSession = new AudioCaptureSession(); 57 | _audioCaptureSession.ConfigureSession(session); 58 | 59 | return SessionSetupResult.Success; 60 | } 61 | 62 | public void StartVideoRecording(bool shouldSaveVideoToLibrary) 63 | { 64 | if (_videoFileOutput == null) 65 | { 66 | Console.WriteLine("capture session: trying to record a video but no movie file output is set"); 67 | return; 68 | } 69 | 70 | _sessionQueue.DispatchAsync(() => 71 | { 72 | // if already recording do nothing 73 | if (_videoFileOutput.Recording) 74 | { 75 | Console.WriteLine( 76 | "capture session: trying to record a video but there is one already being recorded"); 77 | return; 78 | } 79 | 80 | // start recording to a temporary file. 81 | var outputFileName = new NSUuid().AsString(); 82 | 83 | var outputUrl = NSFileManager.DefaultManager.GetTemporaryDirectory().Append(outputFileName, false) 84 | .AppendPathExtension("mov"); 85 | 86 | var recordingDelegate = new VideoCaptureDelegate(DidStartCaptureAction, 87 | captureDelegate => DidFinishCaptureAction(captureDelegate, outputUrl), 88 | (captureDelegate, error) => DidCaptureFail(captureDelegate, error, outputUrl)) 89 | { 90 | ShouldSaveVideoToLibrary = shouldSaveVideoToLibrary 91 | }; 92 | 93 | _videoFileOutput.StartRecordingToOutputFile(outputUrl, recordingDelegate); 94 | 95 | _videoCaptureDelegate = recordingDelegate; 96 | }); 97 | } 98 | 99 | private void DidCaptureFail(VideoCaptureDelegate captureDelegate, NSError error, NSUrl outputUrl) 100 | { 101 | // we need to remove reference to the delegate so it can be deallocated 102 | _videoCaptureDelegate = null; 103 | 104 | DispatchQueue.MainQueue.DispatchAsync(() => 105 | { 106 | if (captureDelegate.RecordingWasInterrupted) 107 | { 108 | _videoRecordingDelegate?.DidInterruptVideoRecording(this, outputUrl, error); 109 | } 110 | else 111 | { 112 | _videoRecordingDelegate?.DidFailVideoRecording(this, error); 113 | } 114 | }); 115 | } 116 | 117 | private void DidStartCaptureAction() 118 | { 119 | DispatchQueue.MainQueue.DispatchAsync(() => { _videoRecordingDelegate?.DidStartVideoRecording(this); }); 120 | } 121 | 122 | private void DidFinishCaptureAction(VideoCaptureDelegate captureDelegate, NSUrl outputUrl) 123 | { 124 | DispatchQueue.MainQueue.DispatchAsync(() => 125 | { 126 | if (captureDelegate?.IsBeingCancelled == true) 127 | { 128 | _videoRecordingDelegate?.DidCancelVideoRecording(this); 129 | } 130 | else 131 | { 132 | _videoRecordingDelegate?.DidFinishVideoRecording(this, outputUrl); 133 | } 134 | }); 135 | } 136 | 137 | /// 138 | /// If there is any recording in progress it will be stopped. - parameter cancel: if true, recorded file will be deleted and corresponding delegate method will be called. 139 | /// 140 | /// If set to true cancel. 141 | public void StopVideoRecording(bool cancel = false) 142 | { 143 | if (_videoFileOutput == null) 144 | { 145 | Console.WriteLine("capture session: trying to stop a video recording but no movie file output is set"); 146 | return; 147 | } 148 | 149 | _sessionQueue.DispatchAsync(() => 150 | { 151 | if (_videoFileOutput.Recording == false) 152 | { 153 | Console.WriteLine( 154 | "capture session: trying to stop a video recording but no recording is in progress"); 155 | return; 156 | } 157 | 158 | if (_videoCaptureDelegate == null) 159 | { 160 | throw new ImagePickerException( 161 | "capture session: trying to stop a video recording but video capture delegate is nil"); 162 | } 163 | 164 | _videoCaptureDelegate.IsBeingCancelled = cancel; 165 | _videoFileOutput.StopRecording(); 166 | }); 167 | } 168 | 169 | public void ChangeCamera(AVCaptureSession session) 170 | { 171 | _videoDeviceInputManager.ConfigureVideoDeviceInput(session); 172 | 173 | var connection = _videoFileOutput?.ConnectionFromMediaType(AVMediaTypes.Video.GetConstant()); 174 | 175 | if (connection?.SupportsVideoStabilization == true) 176 | { 177 | connection.PreferredVideoStabilizationMode = AVCaptureVideoStabilizationMode.Auto; 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /src/Media/Capture/VideoDeviceInputManager.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Enums; 2 | 3 | namespace Softeq.ImagePicker.Media.Capture; 4 | 5 | public class VideoDeviceInputManager 6 | { 7 | private AVCaptureDeviceInput _videoDeviceInput; 8 | private NSObject _runtimeErrorNotification; 9 | 10 | private readonly AVCaptureDeviceDiscoverySession _videoDeviceDiscoverySession = 11 | AVCaptureDeviceDiscoverySession.Create(new[] 12 | { 13 | AVCaptureDeviceType.BuiltInWideAngleCamera, 14 | AVCaptureDeviceType.BuiltInDuoCamera 15 | }, 16 | AVMediaTypes.Video, AVCaptureDevicePosition.Unspecified); 17 | 18 | private readonly Action _sessionRuntimeErrorHandler; 19 | 20 | public VideoDeviceInputManager(Action sessionRuntimeErrorHandler) 21 | { 22 | _sessionRuntimeErrorHandler = sessionRuntimeErrorHandler; 23 | } 24 | 25 | public SessionSetupResult ConfigureVideoDeviceInput(AVCaptureSession session) 26 | { 27 | var videoDevice = GetVideoDevice(); 28 | 29 | if (videoDevice == null) 30 | { 31 | Console.WriteLine("capture session: could not create capture device"); 32 | return SessionSetupResult.ConfigurationFailed; 33 | } 34 | 35 | var videoDeviceInput = new AVCaptureDeviceInput(videoDevice, out var error); 36 | 37 | if (error != null) 38 | { 39 | Console.WriteLine($"Error occured while creating video device input: {error}"); 40 | return SessionSetupResult.ConfigurationFailed; 41 | } 42 | 43 | if (_videoDeviceInput != null) 44 | { 45 | NSNotificationCenter.DefaultCenter.RemoveObserver(_runtimeErrorNotification); 46 | session.RemoveInput(_videoDeviceInput); 47 | } 48 | 49 | if (TryToAddInput(session, videoDeviceInput)) 50 | { 51 | _videoDeviceInput = videoDeviceInput; 52 | } 53 | else if (!TryToAddInput(session, _videoDeviceInput)) 54 | { 55 | return SessionSetupResult.ConfigurationFailed; 56 | } 57 | 58 | _runtimeErrorNotification = NSNotificationCenter.DefaultCenter.AddObserver( 59 | AVCaptureSession.RuntimeErrorNotification, 60 | _sessionRuntimeErrorHandler, _videoDeviceInput.Device); 61 | 62 | return SessionSetupResult.Success; 63 | } 64 | 65 | private bool TryToAddInput(AVCaptureSession session, AVCaptureDeviceInput videoDeviceInput) 66 | { 67 | if (videoDeviceInput == null) 68 | { 69 | return false; 70 | } 71 | 72 | if (_videoDeviceInput != null) 73 | { 74 | session.RemoveInput(_videoDeviceInput); 75 | } 76 | 77 | if (!session.CanAddInput(videoDeviceInput)) 78 | { 79 | return false; 80 | } 81 | 82 | session.AddInput(videoDeviceInput); 83 | 84 | return true; 85 | } 86 | 87 | private AVCaptureDevice GetVideoDevice() 88 | { 89 | if (_videoDeviceInput == null) 90 | { 91 | return GetDefaultDevice(); 92 | } 93 | 94 | AVCaptureDevicePosition preferredPosition; 95 | AVCaptureDeviceType preferredDeviceType; 96 | 97 | var currentVideoDevice = _videoDeviceInput.Device; 98 | var currentPosition = currentVideoDevice.Position; 99 | 100 | switch (currentPosition) 101 | { 102 | case AVCaptureDevicePosition.Unspecified: 103 | case AVCaptureDevicePosition.Front: 104 | preferredPosition = AVCaptureDevicePosition.Back; 105 | preferredDeviceType = AVCaptureDeviceType.BuiltInDuoCamera; 106 | break; 107 | case AVCaptureDevicePosition.Back: 108 | preferredPosition = AVCaptureDevicePosition.Front; 109 | preferredDeviceType = AVCaptureDeviceType.BuiltInWideAngleCamera; 110 | break; 111 | default: 112 | throw new ArgumentOutOfRangeException(); 113 | } 114 | 115 | var devices = _videoDeviceDiscoverySession.Devices; 116 | 117 | // First, look for a device with both the preferred position and device type. Otherwise, look for a device with only the preferred position. 118 | var videoDevice = 119 | devices.FirstOrDefault(x => x.Position == preferredPosition && x.DeviceType == preferredDeviceType); 120 | 121 | return videoDevice ?? devices.FirstOrDefault(x => x.Position == preferredPosition); 122 | } 123 | 124 | private static AVCaptureDevice GetDefaultDevice() 125 | { 126 | var device = AVCaptureDevice.GetDefaultDevice(AVCaptureDeviceType.BuiltInDualCamera, AVMediaTypes.Video, 127 | AVCaptureDevicePosition.Back); 128 | 129 | if (device != null) 130 | { 131 | return device; 132 | } 133 | 134 | device = AVCaptureDevice.GetDefaultDevice(AVCaptureDeviceType.BuiltInWideAngleCamera, AVMediaTypes.Video, 135 | AVCaptureDevicePosition.Back); 136 | 137 | if (device != null) 138 | { 139 | return device; 140 | } 141 | 142 | device = AVCaptureDevice.GetDefaultDevice(AVCaptureDeviceType.BuiltInWideAngleCamera, AVMediaTypes.Video, 143 | AVCaptureDevicePosition.Front); 144 | 145 | return device; 146 | } 147 | } -------------------------------------------------------------------------------- /src/Media/CaptureFactory.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Enums; 2 | using Softeq.ImagePicker.Media.Capture; 3 | using Softeq.ImagePicker.Media.Delegates; 4 | using Softeq.ImagePicker.Public; 5 | using Softeq.ImagePicker.Public.Delegates; 6 | 7 | namespace Softeq.ImagePicker.Media; 8 | 9 | public static class CaptureFactory 10 | { 11 | public static CaptureSession Create(Func getCameraCellFunc, 12 | ImagePickerControllerDelegate imagePickerDelegate, CameraMode mode) 13 | { 14 | var captureSessionDelegate = new CaptureSessionDelegate(getCameraCellFunc); 15 | 16 | CaptureSession session; 17 | switch (mode) 18 | { 19 | case CameraMode.Photo: 20 | case CameraMode.PhotoAndLivePhoto: 21 | session = new CaptureSession(captureSessionDelegate, 22 | new SessionPhotoCapturingDelegate(getCameraCellFunc, imagePickerDelegate)); 23 | break; 24 | case CameraMode.PhotoAndVideo: 25 | session = new CaptureSession(captureSessionDelegate, 26 | new CaptureSessionVideoRecordingDelegate(getCameraCellFunc)); 27 | break; 28 | default: 29 | throw new ArgumentOutOfRangeException(nameof(mode), mode, null); 30 | } 31 | 32 | session.PresetConfiguration = mode.CaptureSessionPresetConfiguration(); 33 | 34 | return session; 35 | } 36 | 37 | private static SessionPresetConfiguration CaptureSessionPresetConfiguration(this CameraMode mode) 38 | { 39 | switch (mode) 40 | { 41 | case CameraMode.Photo: 42 | return SessionPresetConfiguration.Photos; 43 | case CameraMode.PhotoAndLivePhoto: 44 | return SessionPresetConfiguration.LivePhotos; 45 | case CameraMode.PhotoAndVideo: 46 | return SessionPresetConfiguration.Videos; 47 | default: 48 | throw new ArgumentOutOfRangeException(nameof(mode), mode, null); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Media/Delegates/CaptureSessionDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Interfaces; 2 | using Softeq.ImagePicker.Public; 3 | 4 | namespace Softeq.ImagePicker.Media.Delegates; 5 | 6 | public class CaptureSessionDelegate : ICaptureSessionDelegate 7 | { 8 | private readonly Func _getCameraCellFunc; 9 | 10 | public CaptureSessionDelegate(Func getCameraCellFunc) 11 | { 12 | _getCameraCellFunc = getCameraCellFunc; 13 | } 14 | 15 | public void CaptureSessionDidResume() 16 | { 17 | Console.WriteLine("did resume"); 18 | UnblurCellIfNeeded(true); 19 | } 20 | 21 | public void CaptureSessionDidSuspend() 22 | { 23 | Console.WriteLine("did suspend"); 24 | BlurCellIfNeeded(true); 25 | } 26 | 27 | public void DidFail(AVError error) 28 | { 29 | Console.WriteLine("did fail"); 30 | } 31 | 32 | public void DidFailConfiguringSession() 33 | { 34 | Console.WriteLine("did fail configuring"); 35 | } 36 | 37 | public void CaptureGrantedSession(AVAuthorizationStatus status) 38 | { 39 | Console.WriteLine("did grant authorization to camera"); 40 | ReloadCameraCell(status); 41 | } 42 | 43 | public void CaptureFailedSession(AVAuthorizationStatus status) 44 | { 45 | Console.WriteLine("did fail authorization to camera"); 46 | ReloadCameraCell(status); 47 | } 48 | 49 | public void WasInterrupted(NSString reason) 50 | { 51 | Console.WriteLine("interrupted"); 52 | } 53 | 54 | public void CaptureSessionInterruptionDidEnd() 55 | { 56 | Console.WriteLine("interruption ended"); 57 | } 58 | 59 | private void ReloadCameraCell(AVAuthorizationStatus status) 60 | { 61 | var cameraCell = _getCameraCellFunc.Invoke(); 62 | 63 | if (cameraCell == null) 64 | { 65 | return; 66 | } 67 | 68 | cameraCell.AuthorizationStatus = status; 69 | } 70 | 71 | private void BlurCellIfNeeded(bool animated) 72 | { 73 | _getCameraCellFunc.Invoke()?.BlurIfNeeded(animated, null); 74 | } 75 | 76 | private void UnblurCellIfNeeded(bool animated) 77 | { 78 | _getCameraCellFunc.Invoke()?.UnblurIfNeeded(animated, null); 79 | } 80 | } -------------------------------------------------------------------------------- /src/Media/Delegates/CaptureSessionVideoRecordingDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Interfaces; 2 | using Softeq.ImagePicker.Media.Capture; 3 | using Softeq.ImagePicker.Public; 4 | 5 | namespace Softeq.ImagePicker.Media.Delegates; 6 | 7 | public class CaptureSessionVideoRecordingDelegate : ICaptureSessionVideoRecordingDelegate 8 | { 9 | private readonly Func _getCameraCellFunc; 10 | 11 | public CaptureSessionVideoRecordingDelegate(Func getCameraCellFunc) 12 | { 13 | _getCameraCellFunc = getCameraCellFunc; 14 | } 15 | 16 | public void DidBecomeReadyForVideoRecording(VideoCaptureSession session) 17 | { 18 | Console.WriteLine("ready for video recording"); 19 | _getCameraCellFunc.Invoke()?.VideoRecodingDidBecomeReady(); 20 | } 21 | 22 | public void DidStartVideoRecording(VideoCaptureSession session) 23 | { 24 | Console.WriteLine("did start video recording"); 25 | UpdateCameraCellRecordingStatusIfNeeded(true, true); 26 | } 27 | 28 | public void DidCancelVideoRecording(VideoCaptureSession session) 29 | { 30 | Console.WriteLine("did cancel video recording"); 31 | UpdateCameraCellRecordingStatusIfNeeded(false, true); 32 | } 33 | 34 | public void DidFinishVideoRecording(VideoCaptureSession session, NSUrl videoUrl) 35 | { 36 | Console.WriteLine("did finish video recording"); 37 | UpdateCameraCellRecordingStatusIfNeeded(false, true); 38 | } 39 | 40 | public void DidInterruptVideoRecording(VideoCaptureSession session, NSUrl videoUrl, NSError reason) 41 | { 42 | Console.WriteLine($"did interrupt video recording, reason: {reason}"); 43 | UpdateCameraCellRecordingStatusIfNeeded(false, true); 44 | } 45 | 46 | public void DidFailVideoRecording(VideoCaptureSession session, NSError error) 47 | { 48 | Console.WriteLine("did fail video recording"); 49 | UpdateCameraCellRecordingStatusIfNeeded(false, true); 50 | } 51 | 52 | private void UpdateCameraCellRecordingStatusIfNeeded(bool isRecording, bool animated) 53 | { 54 | _getCameraCellFunc.Invoke()?.UpdateRecordingVideoStatus(isRecording, animated); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Media/Delegates/PhotoCaptureDelegate.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Media.Delegates; 2 | 3 | public class PhotoCaptureDelegate : AVCapturePhotoCaptureDelegate 4 | { 5 | private readonly Action _willCapturePhotoAnimation; 6 | private readonly Action _capturingLivePhoto; 7 | 8 | private readonly Action _completed; 9 | private NSUrl _livePhotoCompanionMovieUrl; 10 | 11 | public bool ShouldSavePhotoToLibrary { get; set; } 12 | public NSData PhotoData { get; private set; } 13 | public AVCapturePhotoSettings RequestedPhotoSettings { get; } 14 | public NSError ProcessError { get; private set; } 15 | 16 | public PhotoCaptureDelegate(AVCapturePhotoSettings requestedPhotoSettings, Action willCapturePhotoAnimation, 17 | Action capturingLivePhoto, Action completed) 18 | { 19 | RequestedPhotoSettings = requestedPhotoSettings; 20 | _willCapturePhotoAnimation = willCapturePhotoAnimation; 21 | _capturingLivePhoto = capturingLivePhoto; 22 | _completed = completed; 23 | 24 | ShouldSavePhotoToLibrary = true; 25 | } 26 | 27 | public override void WillBeginCapture(AVCapturePhotoOutput captureOutput, 28 | AVCaptureResolvedPhotoSettings resolvedSettings) 29 | { 30 | if (resolvedSettings.LivePhotoMovieDimensions.Width > 0 && 31 | resolvedSettings.LivePhotoMovieDimensions.Height > 0) 32 | { 33 | _capturingLivePhoto.Invoke(true); 34 | } 35 | } 36 | 37 | public override void WillCapturePhoto(AVCapturePhotoOutput captureOutput, 38 | AVCaptureResolvedPhotoSettings resolvedSettings) 39 | { 40 | _willCapturePhotoAnimation?.Invoke(); 41 | } 42 | 43 | public override void DidFinishProcessingPhoto(AVCapturePhotoOutput captureOutput, 44 | CMSampleBuffer photoSampleBuffer, 45 | CMSampleBuffer previewPhotoSampleBuffer, AVCaptureResolvedPhotoSettings resolvedSettings, 46 | AVCaptureBracketedStillImageSettings bracketSettings, NSError error) 47 | { 48 | if (photoSampleBuffer != null) 49 | { 50 | PhotoData = AVCapturePhotoOutput.GetJpegPhotoDataRepresentation(photoSampleBuffer, 51 | previewPhotoSampleBuffer); 52 | } 53 | else if (error != null) 54 | { 55 | Console.WriteLine($"photo capture delegate: error capturing photo: {error}"); 56 | ProcessError = error; 57 | } 58 | } 59 | 60 | public override void DidFinishRecordingLivePhotoMovie(AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, 61 | AVCaptureResolvedPhotoSettings resolvedSettings) 62 | { 63 | _capturingLivePhoto?.Invoke(false); 64 | } 65 | 66 | public override void DidFinishProcessingLivePhotoMovie(AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, 67 | CMTime duration, 68 | CMTime photoDisplayTime, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error) 69 | { 70 | if (error != null) 71 | { 72 | Console.WriteLine($"photo capture delegate: error processing live photo companion movie: {error}"); 73 | return; 74 | } 75 | 76 | _livePhotoCompanionMovieUrl = outputFileUrl; 77 | } 78 | 79 | public override void DidFinishCapture(AVCapturePhotoOutput captureOutput, 80 | AVCaptureResolvedPhotoSettings resolvedSettings, 81 | NSError error) 82 | { 83 | if (ShouldSaveCaptureResult(error)) 84 | { 85 | PHAssetManager.PerformChangesWithAuthorization(TryToAddPhotoToLibrary, null, DidFinish); 86 | } 87 | } 88 | 89 | private bool ShouldSaveCaptureResult(NSError error) 90 | { 91 | if (error != null) 92 | { 93 | Console.WriteLine($"photo capture delegate: Error capturing photo: {error}"); 94 | DidFinish(); 95 | return false; 96 | } 97 | 98 | if (PhotoData == null) 99 | { 100 | Console.WriteLine("photo capture delegate: No photo data resource"); 101 | DidFinish(); 102 | return false; 103 | } 104 | 105 | if (!ShouldSavePhotoToLibrary) 106 | { 107 | Console.WriteLine("photo capture delegate: photo did finish without saving to photo library"); 108 | DidFinish(); 109 | return false; 110 | } 111 | 112 | return true; 113 | } 114 | 115 | private void TryToAddPhotoToLibrary() 116 | { 117 | var creationRequest = PHAssetCreationRequest.CreationRequestForAsset(); 118 | creationRequest.AddResource(PHAssetResourceType.Photo, PhotoData, null); 119 | 120 | if (_livePhotoCompanionMovieUrl == null) 121 | { 122 | return; 123 | } 124 | 125 | var livePhotoCompanionMovieFileResourceOptions = new PHAssetResourceCreationOptions 126 | { 127 | ShouldMoveFile = true 128 | }; 129 | 130 | creationRequest.AddResource(PHAssetResourceType.PairedVideo, _livePhotoCompanionMovieUrl, 131 | livePhotoCompanionMovieFileResourceOptions); 132 | } 133 | 134 | private void DidFinish() 135 | { 136 | if (_livePhotoCompanionMovieUrl?.Path != null && 137 | NSFileManager.DefaultManager.FileExists(_livePhotoCompanionMovieUrl.Path)) 138 | { 139 | NSFileManager.DefaultManager.Remove(_livePhotoCompanionMovieUrl, out var nsError); 140 | 141 | if (nsError != null) 142 | { 143 | Console.WriteLine( 144 | $"photo capture delegate: Could not remove file at url: ${_livePhotoCompanionMovieUrl}"); 145 | } 146 | } 147 | 148 | _completed?.Invoke(this); 149 | } 150 | } -------------------------------------------------------------------------------- /src/Media/Delegates/SessionPhotoCapturingDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Interfaces; 2 | using Softeq.ImagePicker.Media.Capture; 3 | using Softeq.ImagePicker.Public; 4 | using Softeq.ImagePicker.Public.Delegates; 5 | 6 | namespace Softeq.ImagePicker.Media.Delegates; 7 | 8 | public class SessionPhotoCapturingDelegate : ISessionPhotoCapturingDelegate 9 | { 10 | private readonly Func _getCameraCellFunc; 11 | private readonly ImagePickerControllerDelegate _imagePickerControllerDelegate; 12 | 13 | public SessionPhotoCapturingDelegate(Func getCameraCellFunc, 14 | ImagePickerControllerDelegate @delegate) 15 | { 16 | _getCameraCellFunc = getCameraCellFunc; 17 | _imagePickerControllerDelegate = @delegate; 18 | } 19 | 20 | public void WillCapturePhotoWith(AVCapturePhotoSettings settings) 21 | { 22 | Console.WriteLine($"will capture photo {settings.UniqueID}"); 23 | } 24 | 25 | public void DidCapturePhotoData(NSData didCapturePhotoData, 26 | AVCapturePhotoSettings settings) 27 | { 28 | Console.WriteLine($"did capture photo {settings.UniqueID}"); 29 | _imagePickerControllerDelegate?.DidTake(UIImage.LoadFromData(didCapturePhotoData)); 30 | didCapturePhotoData.Dispose(); 31 | } 32 | 33 | public void DidFailCapturingPhotoWith(NSError error) 34 | { 35 | Console.WriteLine($"did fail capturing: {error}"); 36 | } 37 | 38 | public void CaptureSessionDidChangeNumberOfProcessingLivePhotos(PhotoCaptureSession session) 39 | { 40 | var cameraCell = _getCameraCellFunc?.Invoke(); 41 | 42 | if (cameraCell == null) 43 | { 44 | return; 45 | } 46 | 47 | var count = session.InProgressLivePhotoCapturesCount; 48 | cameraCell.UpdateLivePhotoStatus(count > 0, true); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Media/Delegates/VideoCaptureDelegate.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Media.Delegates; 2 | 3 | public class VideoCaptureDelegate : AVCaptureFileOutputRecordingDelegate 4 | { 5 | private nint? _backgroundRecordingId; 6 | private readonly Action _didStart; 7 | private readonly Action _didFinish; 8 | private readonly Action _didFail; 9 | 10 | /// 11 | /// set this to false if you don't wish to save video to photo library 12 | /// 13 | public bool ShouldSaveVideoToLibrary = true; 14 | 15 | /// 16 | /// true if user manually requested to cancel recording (stop without saving) 17 | /// 18 | public bool IsBeingCancelled = false; 19 | 20 | /// 21 | /// if system interrupts recording due to various reasons (empty space, phone call, background, ...) 22 | /// 23 | public bool RecordingWasInterrupted = false; 24 | 25 | /// 26 | /// non null if failed or interrupted, null if cancelled 27 | /// 28 | /// Did start. 29 | /// Did finish. 30 | /// Did fail. 31 | public VideoCaptureDelegate(Action didStart, Action didFinish, 32 | Action didFail) 33 | { 34 | _didStart = didStart; 35 | _didFinish = didFinish; 36 | _didFail = didFail; 37 | 38 | if (UIDevice.CurrentDevice.IsMultitaskingSupported) 39 | { 40 | /* 41 | Setup background task. 42 | This is needed because the `capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)` 43 | callback is not received until AVCam returns to the foreground unless you request background execution time. 44 | This also ensures that there will be time to write the file to the photo library when AVCam is backgrounded. 45 | To conclude this background execution, endBackgroundTask(_:) is called in 46 | `capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)` after the recorded file has been saved. 47 | */ 48 | _backgroundRecordingId = UIApplication.SharedApplication.BeginBackgroundTask(null); 49 | } 50 | } 51 | 52 | public override void DidStartRecording(AVCaptureFileOutput captureOutput, NSUrl outputFileUrl, 53 | NSObject[] connections) 54 | { 55 | _didStart?.Invoke(); 56 | } 57 | 58 | public override void FinishedRecording(AVCaptureFileOutput captureOutput, NSUrl outputFileUrl, 59 | NSObject[] connections, 60 | NSError error) 61 | { 62 | if (error != null) 63 | { 64 | HandleCaptureResultWithError(error, outputFileUrl); 65 | } 66 | else if (IsBeingCancelled) 67 | { 68 | CleanUp(false, outputFileUrl); 69 | _didFinish.Invoke(this); 70 | } 71 | else 72 | { 73 | CleanUp(ShouldSaveVideoToLibrary, outputFileUrl); 74 | _didFinish.Invoke(this); 75 | } 76 | } 77 | 78 | private void SaveVideoToLibrary(NSUrl outputFileUrl) 79 | { 80 | var creationRequest = PHAssetCreationRequest.CreationRequestForAsset(); 81 | var videoResourceOptions = new PHAssetResourceCreationOptions { ShouldMoveFile = true }; 82 | creationRequest.AddResource(PHAssetResourceType.Video, outputFileUrl, 83 | videoResourceOptions); 84 | } 85 | 86 | private void HandleCaptureResultWithError(NSError error, NSUrl outputFileUrl) 87 | { 88 | Console.WriteLine($"capture session: movie recording failed error: {error}"); 89 | 90 | //this can be true even if recording is stopped due to a reason (no disk space, ...) so the video can still be delivered. 91 | var successfullyFinished = 92 | (error.UserInfo[AVErrorKeys.RecordingSuccessfullyFinished] as NSNumber)?.BoolValue; 93 | 94 | if (successfullyFinished == true) 95 | { 96 | CleanUp(ShouldSaveVideoToLibrary, outputFileUrl); 97 | _didFail.Invoke(this, error); 98 | } 99 | else 100 | { 101 | CleanUp(false, outputFileUrl); 102 | _didFail.Invoke(this, error); 103 | } 104 | } 105 | 106 | private void CleanUp(bool saveToAssets, NSUrl outputFileUrl) 107 | { 108 | if (_backgroundRecordingId != null) 109 | { 110 | if (_backgroundRecordingId != UIApplication.BackgroundTaskInvalid) 111 | { 112 | UIApplication.SharedApplication.EndBackgroundTask(_backgroundRecordingId.Value); 113 | } 114 | 115 | _backgroundRecordingId = UIApplication.BackgroundTaskInvalid; 116 | } 117 | 118 | if (!saveToAssets) 119 | { 120 | DeleteFileIfNeeded(outputFileUrl); 121 | return; 122 | } 123 | 124 | PHAssetManager.PerformChangesWithAuthorization(() => SaveVideoToLibrary(outputFileUrl), 125 | () => DeleteFileIfNeeded(outputFileUrl), null); 126 | } 127 | 128 | private void DeleteFileIfNeeded(NSUrl outputFileUrl) 129 | { 130 | if (NSFileManager.DefaultManager.FileExists(outputFileUrl.Path)) 131 | { 132 | return; 133 | } 134 | 135 | NSFileManager.DefaultManager.Remove(outputFileUrl.Path, out var nsError); 136 | 137 | if (nsError != null) 138 | { 139 | Console.WriteLine($"capture session: could not remove recording at url: {outputFileUrl}"); 140 | Console.WriteLine($"capture session: error: {nsError}"); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/Media/PHAssetManager.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Media; 2 | 3 | public static class PHAssetManager 4 | { 5 | public static void PerformChangesWithAuthorization(Action authorizedAction, Action errorAction, 6 | Action completedAction) 7 | { 8 | PHPhotoLibrary.RequestAuthorization(status => 9 | { 10 | if (status == PHAuthorizationStatus.Authorized) 11 | { 12 | PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(authorizedAction, (_, error) => 13 | { 14 | if (error != null) 15 | { 16 | Console.WriteLine( 17 | $"capture session: Error occured while saving video or photo library: {error}"); 18 | errorAction?.Invoke(); 19 | } 20 | 21 | completedAction?.Invoke(); 22 | }); 23 | } 24 | else 25 | { 26 | errorAction?.Invoke(); 27 | } 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Media/SessionPresetConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Media; 2 | 3 | public enum SessionPresetConfiguration 4 | { 5 | Photos, 6 | LivePhotos, 7 | Videos 8 | } -------------------------------------------------------------------------------- /src/Operations/CollectionViewBatchAnimation.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Operations; 2 | 3 | public class CollectionViewBatchAnimation 4 | { 5 | private readonly UICollectionView _collectionView; 6 | private readonly int _sectionIndex; 7 | private readonly PHFetchResultChangeDetails _changes; 8 | 9 | public CollectionViewBatchAnimation(UICollectionView collectionView, int sectionIndex, 10 | PHFetchResultChangeDetails changes) 11 | { 12 | _collectionView = collectionView; 13 | _sectionIndex = sectionIndex; 14 | _changes = changes; 15 | } 16 | 17 | public void Execute() 18 | { 19 | // If we have incremental diffs, animate them in the collection view 20 | _collectionView.PerformBatchUpdates(() => 21 | { 22 | // For indexes to make sense, updates must be in this order: 23 | // delete, insert, reload, move 24 | if (_changes.RemovedIndexes?.Count > 0) 25 | { 26 | var result = new List(); 27 | _changes.RemovedIndexes.EnumerateIndexes((nuint idx, ref bool stop) => 28 | result.Add(NSIndexPath.FromItemSection((nint)idx, _sectionIndex))); 29 | 30 | _collectionView.DeleteItems(result.ToArray()); 31 | } 32 | 33 | if (_changes.InsertedIndexes?.Count > 0) 34 | { 35 | var result = new List(); 36 | 37 | _changes.InsertedIndexes.EnumerateIndexes((nuint idx, ref bool stop) => 38 | result.Add(NSIndexPath.FromItemSection((nint)idx, _sectionIndex))); 39 | 40 | _collectionView.InsertItems(result.ToArray()); 41 | } 42 | 43 | if (_changes.ChangedIndexes?.Count > 0) 44 | { 45 | var result = new List(); 46 | _changes.ChangedIndexes.EnumerateIndexes((nuint idx, ref bool stop) => 47 | result.Add(NSIndexPath.FromItemSection((nint)idx, _sectionIndex))); 48 | 49 | _collectionView.ReloadItems(result.ToArray()); 50 | } 51 | 52 | _changes.EnumerateMoves((fromIndex, toIndex) => 53 | { 54 | _collectionView.MoveItem(NSIndexPath.FromItemSection((nint)fromIndex, _sectionIndex), 55 | NSIndexPath.FromItemSection((nint)toIndex, _sectionIndex)); 56 | }); 57 | }, null); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Operations/CollectionViewUpdatesCoordinator.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Operations; 2 | 3 | public class CollectionViewUpdatesCoordinator 4 | { 5 | private readonly UICollectionView _сollectionView; 6 | 7 | private readonly NSOperationQueue _serialMainQueue; 8 | 9 | public CollectionViewUpdatesCoordinator(UICollectionView collectionView) 10 | { 11 | _serialMainQueue = new NSOperationQueue 12 | { 13 | MaxConcurrentOperationCount = 1, 14 | UnderlyingQueue = DispatchQueue.MainQueue 15 | }; 16 | 17 | _сollectionView = collectionView; 18 | } 19 | 20 | /// 21 | /// Provides opportunity to update collectionView's dataSource in underlying queue. 22 | /// 23 | /// Updates. 24 | public void PerformDataSourceUpdate(Action updates) 25 | { 26 | _serialMainQueue.AddOperation(updates); 27 | } 28 | 29 | /// 30 | /// Updates collection view. 31 | /// 32 | /// Changes. 33 | /// In section. 34 | public void PerformChanges(PHFetchResultChangeDetails changes, int inSection) 35 | { 36 | if (changes.HasIncrementalChanges) 37 | { 38 | var operation = new CollectionViewBatchAnimation(_сollectionView, inSection, changes); 39 | _serialMainQueue.AddOperation(operation.Execute); 40 | } 41 | else 42 | { 43 | _serialMainQueue.AddOperation(_сollectionView.ReloadData); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Public/Appearance.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Public; 2 | 3 | /// 4 | /// Provides access to styling attributes of Image Picker. 5 | /// 6 | public class Appearance 7 | { 8 | /// 9 | /// Image picker background color. 10 | /// 11 | /// The color of the background. 12 | public UIColor BackgroundColor { get; set; } = Defines.Colors.GrayColor; 13 | } -------------------------------------------------------------------------------- /src/Public/CameraCollectionViewCell.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Interfaces; 2 | using Softeq.ImagePicker.Media; 3 | 4 | namespace Softeq.ImagePicker.Public; 5 | 6 | [Register(nameof(CameraCollectionViewCell))] 7 | public class CameraCollectionViewCell : UICollectionViewCell 8 | { 9 | private AVAuthorizationStatus? _authorizationStatus; 10 | public readonly AVPreviewView PreviewView = new AVPreviewView(CGRect.Empty) { BackgroundColor = UIColor.Black }; 11 | 12 | private readonly UIImageView _imageView = new UIImageView(CGRect.Empty) 13 | { ContentMode = UIViewContentMode.ScaleAspectFill }; 14 | 15 | private UIVisualEffectView BlurView { get; set; } 16 | public bool IsVisualEffectViewUsedForBlurring { get; set; } 17 | public ICameraCollectionViewCellDelegate Delegate { get; set; } 18 | 19 | public CameraCollectionViewCell(IntPtr handle) : base(handle) 20 | { 21 | BackgroundView = PreviewView; 22 | PreviewView.AddSubview(_imageView); 23 | } 24 | 25 | public override void LayoutSubviews() 26 | { 27 | base.LayoutSubviews(); 28 | 29 | _imageView.Frame = PreviewView.Bounds; 30 | if (BlurView != null) 31 | { 32 | BlurView.Frame = PreviewView.Bounds; 33 | } 34 | } 35 | 36 | /// 37 | /// The cell can have multiple visual states based on authorization status. Use 38 | /// `updateCameraAuthorizationStatus()` func to update UI. 39 | /// 40 | /// The authorization status. 41 | public AVAuthorizationStatus? AuthorizationStatus 42 | { 43 | get => _authorizationStatus; 44 | set 45 | { 46 | _authorizationStatus = value; 47 | UpdateCameraAuthorizationStatus(); 48 | } 49 | } 50 | 51 | /// 52 | /// Called each time an authorization status to camera is changed. Update your cell's UI based on current value of `authorizationStatus` property. 53 | /// 54 | public void UpdateCameraAuthorizationStatus() 55 | { 56 | } 57 | 58 | /// 59 | /// If live photos are enabled this method is called each time user captures 60 | /// a live photo. Override this method to update UI based on live view status. 61 | /// 62 | /// If there is at least 1 live photo being processed 63 | /// If the UI change should be animated or not 64 | public virtual void UpdateLivePhotoStatus(bool isProcessing, bool shouldAnimate) 65 | { 66 | } 67 | 68 | /// 69 | /// If video recording is enabled this method is called each time user starts or stops 70 | /// a recording. Override this method to update UI based on recording status. 71 | /// 72 | /// If video is recording or nottrue is recording. 73 | /// If the UI change should be animated or not. 74 | public virtual void UpdateRecordingVideoStatus(bool isRecording, bool shouldAnimate) 75 | { 76 | } 77 | 78 | public virtual void VideoRecodingDidBecomeReady() 79 | { 80 | } 81 | 82 | /// 83 | /// Flips camera from front/rear or rear/front. Flip is always supplemented with an flip animation. 84 | /// 85 | /// A block is called as soon as camera is changed. 86 | public void FlipCamera(Action completion = null) 87 | { 88 | Delegate?.FlipCamera(completion); 89 | } 90 | 91 | public void TakePicture() 92 | { 93 | Delegate?.TakePicture(); 94 | } 95 | 96 | /// 97 | /// Takes a live photo. Please note that live photos must be enabled when configuring Image Picker. 98 | /// 99 | public void TakeLivePhoto() 100 | { 101 | Delegate?.TakeLivePhoto(); 102 | } 103 | 104 | public void StartVideoRecording() 105 | { 106 | Delegate?.StartVideoRecording(); 107 | } 108 | 109 | public void StopVideoRecording() 110 | { 111 | Delegate?.StopVideoRecording(); 112 | } 113 | 114 | public void BlurIfNeeded(bool animated, Action completion) 115 | { 116 | if (BlurView == null) 117 | { 118 | BlurView = new UIVisualEffectView(UIBlurEffect.FromStyle(UIBlurEffectStyle.Light)); 119 | PreviewView.AddSubview(BlurView); 120 | } 121 | 122 | BlurView.Frame = PreviewView.Bounds; 123 | 124 | BlurView.Alpha = 0; 125 | if (animated == false) 126 | { 127 | BlurView.Alpha = 1; 128 | completion?.Invoke(); 129 | } 130 | else 131 | { 132 | Animate(0.2, 0, UIViewAnimationOptions.AllowAnimatedContent, () => BlurView.Alpha = 1, 133 | completion); 134 | } 135 | } 136 | 137 | public void UnblurIfNeeded(bool animated, Action completion) 138 | { 139 | Action animationBlock = () => 140 | { 141 | if (BlurView != null) 142 | { 143 | BlurView.Alpha = 0; 144 | } 145 | }; 146 | 147 | if (animated == false) 148 | { 149 | animationBlock.Invoke(); 150 | completion?.Invoke(); 151 | } 152 | else 153 | { 154 | Animate(0.2, 0, UIViewAnimationOptions.AllowAnimatedContent, animationBlock, completion); 155 | } 156 | } 157 | 158 | public bool TouchIsCaptureEffective(CGPoint point) 159 | { 160 | return Bounds.Contains(point) && HitTest(point, null).Equals(ContentView); 161 | } 162 | } -------------------------------------------------------------------------------- /src/Public/CaptureSettings.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Enums; 2 | 3 | namespace Softeq.ImagePicker.Public; 4 | 5 | public class CaptureSettings 6 | { 7 | /// 8 | /// Capture session uses this preset when configuring. Select a preset of 9 | /// media types you wish to support. 10 | /// Currently you can not change preset at runtime 11 | /// 12 | public CameraMode CameraMode; 13 | 14 | /// 15 | /// Return true if captured photos will be saved to photo library. Image picker 16 | /// will prompt user with request for permissions when needed. Default value is false 17 | /// for photos. Live photos and videos are always true. 18 | /// Please note, that at current implementation this applies to photos only. For 19 | /// live photos and videos this is always true. 20 | /// 21 | public bool SavesCapturedPhotosToPhotoLibrary; 22 | 23 | public bool SavesCapturedLivePhotosToPhotoLibrary = true; 24 | public bool SavesCapturedVideosToPhotoLibrary = true; 25 | } -------------------------------------------------------------------------------- /src/Public/Delegates/CameraCollectionViewCellDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Enums; 2 | using Softeq.ImagePicker.Infrastructure.Interfaces; 3 | using Softeq.ImagePicker.Media.Capture; 4 | 5 | namespace Softeq.ImagePicker.Public.Delegates; 6 | 7 | public class CameraCollectionViewCellDelegate : ICameraCollectionViewCellDelegate 8 | { 9 | private readonly Func _getCameraCellFunc; 10 | private readonly CaptureSession _captureSession; 11 | private readonly CaptureSettings _captureSettings; 12 | 13 | public CameraCollectionViewCellDelegate(Func getCameraCellFunc, 14 | CaptureSession captureSession, CaptureSettings captureSettings) 15 | { 16 | _getCameraCellFunc = getCameraCellFunc; 17 | _captureSession = captureSession; 18 | _captureSettings = captureSettings; 19 | } 20 | 21 | public void TakePicture() 22 | { 23 | _captureSession.PhotoCaptureSession.CapturePhoto(LivePhotoMode.Off, 24 | _captureSettings.SavesCapturedPhotosToPhotoLibrary); 25 | } 26 | 27 | public void TakeLivePhoto() 28 | { 29 | _captureSession.PhotoCaptureSession.CapturePhoto(LivePhotoMode.On, 30 | _captureSettings.SavesCapturedLivePhotosToPhotoLibrary); 31 | } 32 | 33 | public void StartVideoRecording() 34 | { 35 | _captureSession.VideoCaptureSession?.StartVideoRecording(_captureSettings 36 | .SavesCapturedVideosToPhotoLibrary); 37 | } 38 | 39 | public void StopVideoRecording() 40 | { 41 | _captureSession.VideoCaptureSession?.StopVideoRecording(); 42 | } 43 | 44 | public void FlipCamera(Action completion) 45 | { 46 | if (_captureSession == null) 47 | { 48 | return; 49 | } 50 | 51 | var cameraCell = _getCameraCellFunc.Invoke(); 52 | if (cameraCell == null) 53 | { 54 | _captureSession.ChangeCamera(completion); 55 | return; 56 | } 57 | 58 | // 1. blur cell 59 | cameraCell.BlurIfNeeded(true, () => 60 | { 61 | { 62 | // 2. flip camera 63 | _captureSession.ChangeCamera(() => 64 | { 65 | UIView.Transition(cameraCell.PreviewView, 0.25, 66 | UIViewAnimationOptions.TransitionFlipFromLeft | UIViewAnimationOptions.AllowAnimatedContent, 67 | null, () => { cameraCell.UnblurIfNeeded(true, completion); }); 68 | }); 69 | } 70 | }); 71 | } 72 | } -------------------------------------------------------------------------------- /src/Public/Delegates/ImagePickerControllerDelegate.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Views; 2 | 3 | namespace Softeq.ImagePicker.Public.Delegates; 4 | 5 | /// 6 | /// Group of methods informing what image picker is currently doing 7 | /// 8 | public class ImagePickerControllerDelegate 9 | { 10 | /// 11 | /// Called when user taps on an action item, index is either 0 or 1 depending which was tapped 12 | /// 13 | /// Controller. 14 | /// Index. 15 | public virtual void DidSelectActionItemAt(ImagePickerController controller, int index) 16 | { 17 | } 18 | 19 | /// 20 | /// Called when user select an asset. 21 | /// 22 | /// Controller. 23 | /// Asset. 24 | public virtual void DidSelectAsset(ImagePickerController controller, PHAsset asset) 25 | { 26 | } 27 | 28 | /// 29 | /// Called when user unselect previously selected asset. 30 | /// 31 | /// Controller. 32 | /// Asset. 33 | public virtual void DidDeselectAsset(ImagePickerController controller, PHAsset asset) 34 | { 35 | } 36 | 37 | /// 38 | /// Called when user takes new photo. 39 | /// 40 | /// Image. 41 | public virtual void DidTake(UIImage image) 42 | { 43 | } 44 | 45 | /// 46 | /// Called right before an action item collection view cell is displayed. Use this method 47 | /// to configure your cell. 48 | /// 49 | /// Controller. 50 | /// Cell. 51 | /// Index. 52 | public virtual void WillDisplayActionItem(ImagePickerController controller, UICollectionViewCell cell, 53 | int index) 54 | { 55 | } 56 | 57 | /// 58 | /// Called right before an asset item collection view cell is displayed. Use this method 59 | /// to configure your cell based on asset media type, subtype, etc. 60 | /// 61 | /// Controller. 62 | /// Cell. 63 | /// Asset. 64 | public virtual void WillDisplayAssetItem(ImagePickerController controller, ImagePickerAssetCell cell, 65 | PHAsset asset) 66 | { 67 | } 68 | } -------------------------------------------------------------------------------- /src/Public/ImagePickerControllerDataSource.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Public; 2 | 3 | /// 4 | /// Image picker may ask for additional resources, implement this protocol to fully support 5 | /// all features. 6 | /// 7 | public abstract class ImagePickerControllerDataSource 8 | { 9 | /// 10 | /// Asks for a view that is placed as overlay view with permissions info 11 | /// when user did not grant or has restricted access to photo library. 12 | /// 13 | public abstract UIView ImagePicker(PHAuthorizationStatus status); 14 | } -------------------------------------------------------------------------------- /src/Public/ImagePickerControllerPublicApi.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Softeq.ImagePicker.Infrastructure; 3 | using Softeq.ImagePicker.Public.Delegates; 4 | using Softeq.ImagePicker.Views; 5 | 6 | namespace Softeq.ImagePicker.Public; 7 | 8 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] 9 | public partial class ImagePickerController 10 | { 11 | /// 12 | /// Use this object to configure layout of action, camera and asset items. 13 | /// 14 | public LayoutConfiguration LayoutConfiguration = new LayoutConfiguration().Default(); 15 | 16 | /// 17 | /// Use this to register a cell classes or nibs for each item types 18 | /// 19 | /// The cell registrator. 20 | public CellRegistrator CellRegistrator { get; } = new CellRegistrator(); 21 | 22 | /// 23 | /// Use these settings to configure how the capturing should behave 24 | /// 25 | /// The capture settings. 26 | public CaptureSettings CaptureSettings { get; } = new CaptureSettings(); 27 | 28 | /// 29 | /// Get informed about user interaction and changes 30 | /// 31 | /// The delegate. 32 | public ImagePickerControllerDelegate Delegate { get; set; } 33 | 34 | /// 35 | /// Provide additional data when requested by Image Picker 36 | /// 37 | public ImagePickerControllerDataSource DataSource; 38 | 39 | /// 40 | /// A collection view that is used for displaying content. 41 | /// 42 | /// The collection view. 43 | public UICollectionView CollectionView => ImagePickerView.UICollectionView; 44 | 45 | public ImagePickerView ImagePickerView => View as ImagePickerView; 46 | 47 | /// 48 | /// Fetch result of assets that will be used for picking. 49 | /// 50 | /// If you leave this nil or return nil from the block, assets from recently 51 | /// added smart album will be used. 52 | /// 53 | public Func AssetsFetchResultBlock; 54 | 55 | /// 56 | /// Instance appearance proxy object. Use this object to set appearance 57 | /// for this particular instance of Image Picker. 58 | /// 59 | /// The appearance. 60 | public Appearance Appearance { get; } = new Appearance(); 61 | 62 | /// 63 | /// Programatically select asset. 64 | /// 65 | /// Index. 66 | /// If set to true animated. 67 | /// Scroll position. 68 | public void SelectAsset(int index, bool animated, UICollectionViewScrollPosition scrollPosition) 69 | { 70 | var path = NSIndexPath.FromItemSection(index, LayoutConfiguration.SectionIndexForAssets); 71 | CollectionView.SelectItem(path, animated, scrollPosition); 72 | } 73 | 74 | /// 75 | /// Programatically deselect asset. 76 | /// 77 | /// Index. 78 | /// If set to true animated. 79 | public void DeselectAsset(int index, bool animated) 80 | { 81 | var path = NSIndexPath.FromItemSection(index, LayoutConfiguration.SectionIndexForAssets); 82 | CollectionView.DeselectItem(path, animated); 83 | } 84 | 85 | /// 86 | /// Programatically deselect all selected assets. 87 | /// 88 | /// If set to true animated. 89 | public void DeselectAllAssets(bool animated) 90 | { 91 | var items = CollectionView.GetIndexPathsForSelectedItems(); 92 | if (items == null) 93 | { 94 | return; 95 | } 96 | 97 | foreach (var selectedPath in items) 98 | { 99 | CollectionView.DeselectItem(selectedPath, animated); 100 | } 101 | } 102 | 103 | /// 104 | /// Access all currently selected images 105 | /// 106 | /// The selected assets. 107 | public IReadOnlyList SelectedAssets => 108 | CollectionView.GetIndexPathsForSelectedItems().Select(x => Asset(x.Row)).ToList(); 109 | 110 | /// 111 | /// Returns an array of assets at index set. An exception will be thrown if it fails 112 | /// 113 | /// The assets. 114 | /// Indexes. 115 | public PHAsset[] Assets(NSIndexSet indexes) 116 | { 117 | if (_collectionViewDataSource.AssetsModel.FetchResult == null) 118 | { 119 | throw new ImagePickerException($"Accessing assets at indexes {indexes} failed"); 120 | } 121 | 122 | return _collectionViewDataSource.AssetsModel.FetchResult.ObjectsAt(indexes); 123 | } 124 | 125 | /// 126 | /// Returns an asset at index. If there is no asset at the index, an exception will be thrown. 127 | /// 128 | /// The asset. 129 | /// Index. 130 | public PHAsset Asset(int index) 131 | { 132 | if (_collectionViewDataSource.AssetsModel.FetchResult == null) 133 | { 134 | throw new ImagePickerException($"Accessing asset at index {index} failed"); 135 | } 136 | 137 | return (PHAsset)_collectionViewDataSource.AssetsModel.FetchResult.ElementAt(index); 138 | } 139 | } -------------------------------------------------------------------------------- /src/Public/LayoutConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Public; 2 | 3 | public struct LayoutConfiguration 4 | { 5 | public bool ShowsFirstActionItem; 6 | public bool ShowsSecondActionItem; 7 | public string FirstNameOfActionItem; 8 | public string SecondNameOfActionItem; 9 | public bool ShowsCameraItem; 10 | 11 | public bool ShowsAssetItems; 12 | 13 | /// 14 | /// Scroll and layout direction 15 | /// 16 | public UICollectionViewScrollDirection ScrollDirection; 17 | 18 | /// 19 | /// Defines how many image assets will be in a row. Must be > 0 20 | /// 21 | public int NumberOfAssetItemsInRow; 22 | 23 | /// 24 | /// Spacing between items within a section 25 | /// 26 | public nfloat InterItemSpacing; 27 | 28 | /// 29 | /// Spacing between actions section and camera section 30 | /// 31 | public nfloat ActionSectionSpacing; 32 | 33 | /// 34 | /// Spacing between camera section and assets section 35 | /// 36 | public nfloat CameraSectionSpacing; 37 | 38 | public bool HasAnyAction() 39 | { 40 | return ShowsFirstActionItem || ShowsSecondActionItem; 41 | } 42 | 43 | public int SectionIndexForActions; 44 | 45 | public int SectionIndexForCamera; 46 | 47 | public int SectionIndexForAssets; 48 | 49 | public LayoutConfiguration Default() 50 | { 51 | ShowsFirstActionItem = true; 52 | ShowsSecondActionItem = true; 53 | ShowsCameraItem = true; 54 | ShowsAssetItems = true; 55 | ScrollDirection = UICollectionViewScrollDirection.Horizontal; 56 | NumberOfAssetItemsInRow = 2; 57 | InterItemSpacing = 1; 58 | ActionSectionSpacing = 1; 59 | CameraSectionSpacing = 10; 60 | SectionIndexForActions = 0; 61 | SectionIndexForCamera = 1; 62 | SectionIndexForAssets = 2; 63 | FirstNameOfActionItem = "Camera"; 64 | SecondNameOfActionItem = "Photos"; 65 | return this; 66 | } 67 | } -------------------------------------------------------------------------------- /src/Softeq.ImagePicker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0-ios10.2 4 | 10.2 5 | enable 6 | 7 | 8 | ImagePicker for Xamarin.iOS 9 | An easy to use drop-in framework providing user interface for taking pictures and videos and pick assets from Photo Library. User interface is designed to support inputView "keyboard - like" presentation for conversation user interfaces. 10 | Softeq Development Corporation 11 | Copyright © 2018 Softeq Development Corporation 12 | 1.1.3 13 | Softeq Development Corp. 14 | Softeq Development Corp. 15 | ImagePicker 16 | icon.png 17 | https://github.com/Softeq/ImagePicker-xamarin-ios 18 | LICENSE 19 | image picker;camera;ios;xamarin;softeq;library;uikit;ui;chat 20 | README.md 21 | Migration to .NET6 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Views/ActionCell.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Extensions; 2 | using Softeq.ImagePicker.Public; 3 | 4 | namespace Softeq.ImagePicker.Views; 5 | 6 | public partial class ActionCell : UICollectionViewCell 7 | { 8 | public ActionCell(IntPtr handle) : base(handle) 9 | { 10 | } 11 | 12 | [Export("awakeFromNib")] 13 | public override void AwakeFromNib() 14 | { 15 | base.AwakeFromNib(); 16 | ImageView.BackgroundColor = UIColor.Clear; 17 | } 18 | 19 | public void Update(int index, LayoutConfiguration layoutConfiguration) 20 | { 21 | var layoutModel = new LayoutModel(layoutConfiguration, 0); 22 | var actionCount = layoutModel.NumberOfItems(layoutConfiguration.SectionIndexForActions); 23 | 24 | TitleLabel.TextColor = UIColor.Black; 25 | 26 | if (index == 0) 27 | { 28 | TitleLabel.Text = layoutConfiguration.FirstNameOfActionItem; 29 | ImageView.Image = UIImageExtensions.FromBundle(BundleAssets.ButtonCamera); 30 | } 31 | else if (index == 1) 32 | { 33 | TitleLabel.Text = layoutConfiguration.SecondNameOfActionItem; 34 | ImageView.Image = UIImageExtensions.FromBundle(BundleAssets.ButtonPhotoLibrary); 35 | } 36 | 37 | var isFirst = index == 0; 38 | var isLast = index == actionCount - 1; 39 | 40 | switch (layoutConfiguration.ScrollDirection) 41 | { 42 | case UICollectionViewScrollDirection.Horizontal: 43 | TopOffset.Constant = isFirst ? 10 : 5; 44 | BottomOffset.Constant = isLast ? 10 : 5; 45 | LeadingOffset.Constant = 5; 46 | TrailingOffset.Constant = 5; 47 | break; 48 | case UICollectionViewScrollDirection.Vertical: 49 | TopOffset.Constant = 5; 50 | BottomOffset.Constant = 5; 51 | LeadingOffset.Constant = isFirst ? 10 : 5; 52 | TrailingOffset.Constant = isLast ? 10 : 5; 53 | break; 54 | default: 55 | throw new ArgumentOutOfRangeException(); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Views/ActionCell.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio from the outlets and 4 | // actions declared in your storyboard file. 5 | // Manual changes to this file will not be maintained. 6 | // 7 | 8 | using Foundation; 9 | 10 | namespace Softeq.ImagePicker.Views 11 | { 12 | [Register ("ActionCell")] 13 | partial class ActionCell 14 | { 15 | [Outlet] 16 | UIKit.NSLayoutConstraint BottomOffset { get; set; } 17 | 18 | 19 | [Outlet] 20 | UIKit.UIImageView ImageView { get; set; } 21 | 22 | 23 | [Outlet] 24 | UIKit.NSLayoutConstraint LeadingOffset { get; set; } 25 | 26 | 27 | [Outlet] 28 | UIKit.UILabel TitleLabel { get; set; } 29 | 30 | 31 | [Outlet] 32 | UIKit.NSLayoutConstraint TopOffset { get; set; } 33 | 34 | 35 | [Outlet] 36 | UIKit.NSLayoutConstraint TrailingOffset { get; set; } 37 | 38 | void ReleaseDesignerOutlets () 39 | { 40 | if (BottomOffset != null) { 41 | BottomOffset.Dispose (); 42 | BottomOffset = null; 43 | } 44 | 45 | if (ImageView != null) { 46 | ImageView.Dispose (); 47 | ImageView = null; 48 | } 49 | 50 | if (LeadingOffset != null) { 51 | LeadingOffset.Dispose (); 52 | LeadingOffset = null; 53 | } 54 | 55 | if (TitleLabel != null) { 56 | TitleLabel.Dispose (); 57 | TitleLabel = null; 58 | } 59 | 60 | if (TopOffset != null) { 61 | TopOffset.Dispose (); 62 | TopOffset = null; 63 | } 64 | 65 | if (TrailingOffset != null) { 66 | TrailingOffset.Dispose (); 67 | TrailingOffset = null; 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/Views/AssetCell.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Extensions; 2 | using Softeq.ImagePicker.Views.CustomControls; 3 | 4 | namespace Softeq.ImagePicker.Views; 5 | 6 | [Register(nameof(AssetCell))] 7 | public class AssetCell : ImagePickerAssetCell 8 | { 9 | private readonly CheckView _selectedImageView = new CheckView(CGRect.Empty); 10 | 11 | public sealed override UIImageView ImageView { get; } = new UIImageView(CGRect.Empty); 12 | 13 | public override string RepresentedAssetIdentifier { get; set; } 14 | 15 | public override bool Selected 16 | { 17 | get => base.Selected; 18 | set 19 | { 20 | base.Selected = value; 21 | _selectedImageView.Hidden = !base.Selected; 22 | UpdateState(); 23 | } 24 | } 25 | 26 | protected AssetCell(IntPtr handle) : base(handle) 27 | { 28 | ImageView.ContentMode = UIViewContentMode.ScaleAspectFill; 29 | ImageView.ClipsToBounds = true; 30 | ContentView.AddSubview(ImageView); 31 | 32 | _selectedImageView.Frame = new CGRect(0, 0, 31, 31); 33 | 34 | ContentView.AddSubview(_selectedImageView); 35 | _selectedImageView.Hidden = true; 36 | } 37 | 38 | public override void PrepareForReuse() 39 | { 40 | base.PrepareForReuse(); 41 | 42 | ImageView.Image = null; 43 | } 44 | 45 | public override void LayoutSubviews() 46 | { 47 | const int margin = 5; 48 | 49 | base.LayoutSubviews(); 50 | 51 | ImageView.Frame = Bounds; 52 | 53 | _selectedImageView.Frame = 54 | new CGRect(new CGPoint(Bounds.Width - _selectedImageView.Frame.Width - margin, margin), 55 | _selectedImageView.Frame.Size); 56 | } 57 | 58 | private void UpdateState() 59 | { 60 | if (_selectedImageView.Hidden) 61 | { 62 | return; 63 | } 64 | 65 | _selectedImageView.Image = UIImageExtensions.FromBundle(BundleAssets.IconCheckBackground); 66 | _selectedImageView.ForegroundImage = UIImageExtensions.FromBundle(BundleAssets.IconCheck); 67 | } 68 | } -------------------------------------------------------------------------------- /src/Views/CustomControls/CarvedLabel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Softeq.ImagePicker.Views.CustomControls; 4 | 5 | [Register(nameof(CarvedLabel)), DesignTimeVisible(true)] 6 | public sealed class CarvedLabel : UIView 7 | { 8 | private string _text; 9 | private UIFont _font; 10 | private nfloat _cornerRadius = 0f; 11 | private nfloat _verticalInset = 0f; 12 | private nfloat _horizontalInset = 0f; 13 | 14 | private NSAttributedString AttributedString => new NSAttributedString(Text ?? string.Empty, 15 | Font ?? UIFont.SystemFontOfSize(12, UIFontWeight.Regular)); 16 | 17 | [Export("text"), Browsable(true)] 18 | public string Text 19 | { 20 | get => _text; 21 | set 22 | { 23 | _text = value; 24 | InvalidateIntrinsicContentSize(); 25 | SetNeedsDisplay(); 26 | } 27 | } 28 | 29 | [Export("font"), Browsable(true)] 30 | public UIFont Font 31 | { 32 | get => _font; 33 | set 34 | { 35 | _font = value; 36 | InvalidateIntrinsicContentSize(); 37 | SetNeedsDisplay(); 38 | } 39 | } 40 | 41 | [Export("cornerRadius"), Browsable(true)] 42 | public nfloat CornerRadius 43 | { 44 | get => _cornerRadius; 45 | set 46 | { 47 | _cornerRadius = value; 48 | SetNeedsDisplay(); 49 | } 50 | } 51 | 52 | [Export("verticalInset"), Browsable(true)] 53 | public nfloat VerticalInset 54 | { 55 | get => _verticalInset; 56 | set 57 | { 58 | _verticalInset = value; 59 | InvalidateIntrinsicContentSize(); 60 | SetNeedsDisplay(); 61 | } 62 | } 63 | 64 | [Export("horizontalInset"), Browsable(true)] 65 | public nfloat HorizontalInset 66 | { 67 | get => _horizontalInset; 68 | set 69 | { 70 | _horizontalInset = value; 71 | InvalidateIntrinsicContentSize(); 72 | SetNeedsDisplay(); 73 | } 74 | } 75 | 76 | public override UIColor BackgroundColor => UIColor.Clear; 77 | 78 | public override CGSize IntrinsicContentSize => SizeThatFits(CGSize.Empty); 79 | 80 | public CarvedLabel(IntPtr handle) : base(handle) 81 | { 82 | //var _ = BackgroundColor; 83 | Opaque = false; 84 | } 85 | 86 | public override void Draw(CGRect rect) 87 | { 88 | var color = TintColor; 89 | color.SetFill(); 90 | 91 | var path = UIBezierPath.FromRoundedRect(rect, CornerRadius); 92 | path.Fill(); 93 | 94 | if (string.IsNullOrEmpty(Text)) 95 | { 96 | return; 97 | } 98 | 99 | var context = UIGraphics.GetCurrentContext(); 100 | 101 | var attributedString = AttributedString; 102 | var stringSize = attributedString.Size; 103 | 104 | var xOrigin = Math.Max(HorizontalInset, (rect.Width - stringSize.Width) / 2); 105 | var yOrigin = Math.Max(VerticalInset, (rect.Height - stringSize.Height) / 2); 106 | 107 | context.SaveState(); 108 | context.SetBlendMode(CGBlendMode.DestinationOut); 109 | attributedString.DrawString(new CGPoint(xOrigin, yOrigin)); 110 | context.RestoreState(); 111 | } 112 | 113 | public override CGSize SizeThatFits(CGSize size) 114 | { 115 | var stringSize = AttributedString.Size; 116 | return new CGSize(stringSize.Width + HorizontalInset * 2, stringSize.Height + VerticalInset * 2); 117 | } 118 | } -------------------------------------------------------------------------------- /src/Views/CustomControls/CheckView.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views.CustomControls; 2 | 3 | public sealed class CheckView : UIImageView 4 | { 5 | private readonly UIImageView _foregroundView = new UIImageView(CGRect.Empty); 6 | 7 | public UIImage ForegroundImage 8 | { 9 | get => _foregroundView.Image; 10 | set => _foregroundView.Image = value; 11 | } 12 | 13 | public CheckView(CGRect frame) : base(frame) 14 | { 15 | AddSubview(_foregroundView); 16 | ContentMode = UIViewContentMode.Center; 17 | _foregroundView.ContentMode = UIViewContentMode.Center; 18 | } 19 | 20 | public override void LayoutSubviews() 21 | { 22 | base.LayoutSubviews(); 23 | _foregroundView.Frame = Bounds; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Views/CustomControls/LayersState.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views.CustomControls; 2 | 3 | public enum LayersState 4 | { 5 | Initial, 6 | Pressed, 7 | Recording 8 | } -------------------------------------------------------------------------------- /src/Views/CustomControls/RecordButton.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views.CustomControls; 2 | 3 | [Register(nameof(RecordButton))] 4 | public sealed class RecordButton : StationaryButton 5 | { 6 | private float _outerBorderWidth = 3f; 7 | private float _innerBorderWidth = 1.5f; 8 | private float _pressDepthFactor = 0.9f; 9 | private bool _needsUpdateCircleLayers = true; 10 | private readonly CALayer _outerCircleLayer; 11 | private readonly CALayer _innerCircleLayer; 12 | private LayersState _layersState; 13 | 14 | private float InnerCircleLayerInset => OuterBorderWidth + InnerBorderWidth; 15 | 16 | public float OuterBorderWidth 17 | { 18 | get => _outerBorderWidth; 19 | set 20 | { 21 | _outerBorderWidth = value; 22 | SetNeedsUpdateCircleLayers(); 23 | } 24 | } 25 | 26 | public float InnerBorderWidth 27 | { 28 | get => _innerBorderWidth; 29 | set 30 | { 31 | _innerBorderWidth = value; 32 | SetNeedsUpdateCircleLayers(); 33 | } 34 | } 35 | 36 | public float PressDepthFactor 37 | { 38 | get => _pressDepthFactor; 39 | set 40 | { 41 | _pressDepthFactor = value; 42 | SetNeedsUpdateCircleLayers(); 43 | } 44 | } 45 | 46 | public override bool Highlighted 47 | { 48 | get => base.Highlighted; 49 | set 50 | { 51 | if (Selected == false && value != Highlighted && value == true) 52 | { 53 | UpdateCircleLayers(LayersState.Pressed, true); 54 | } 55 | 56 | base.Highlighted = value; 57 | } 58 | } 59 | 60 | public RecordButton(IntPtr handler) : base(handler) 61 | { 62 | BackgroundColor = UIColor.Clear; 63 | 64 | _outerCircleLayer = new CALayer 65 | { 66 | BackgroundColor = UIColor.Clear.CGColor, 67 | CornerRadius = Bounds.Width / 2, 68 | BorderWidth = OuterBorderWidth, 69 | BorderColor = TintColor!.CGColor, 70 | }; 71 | 72 | _innerCircleLayer = new CALayer() 73 | { 74 | BackgroundColor = UIColor.Red.CGColor 75 | }; 76 | 77 | Layer.AddSublayer(_outerCircleLayer); 78 | Layer.AddSublayer(_innerCircleLayer); 79 | 80 | CATransaction.DisableActions = true; 81 | CATransaction.Commit(); 82 | } 83 | 84 | protected override void SelectionDidChange(bool animated) 85 | { 86 | base.SelectionDidChange(animated); 87 | 88 | UpdateCircleLayers(Selected ? LayersState.Recording : LayersState.Initial, animated); 89 | } 90 | 91 | public override void LayoutSubviews() 92 | { 93 | base.LayoutSubviews(); 94 | 95 | if (!_needsUpdateCircleLayers) 96 | { 97 | return; 98 | } 99 | 100 | CATransaction.DisableActions = true; 101 | _outerCircleLayer.Frame = Bounds; 102 | _innerCircleLayer.Frame = Bounds.Inset(InnerCircleLayerInset, InnerCircleLayerInset); 103 | _innerCircleLayer.CornerRadius = Bounds.Inset(InnerCircleLayerInset, InnerCircleLayerInset).Width / 2; 104 | _needsUpdateCircleLayers = false; 105 | CATransaction.Commit(); 106 | } 107 | 108 | private void SetNeedsUpdateCircleLayers() 109 | { 110 | _needsUpdateCircleLayers = true; 111 | SetNeedsLayout(); 112 | } 113 | 114 | private void UpdateCircleLayers(LayersState state, bool animated) 115 | { 116 | if (_layersState == state) 117 | { 118 | return; 119 | } 120 | 121 | _layersState = state; 122 | 123 | switch (_layersState) 124 | { 125 | case LayersState.Initial: 126 | SetInnerLayer(false, animated); 127 | break; 128 | case LayersState.Pressed: 129 | SetInnerLayerPressed(animated); 130 | break; 131 | case LayersState.Recording: 132 | SetInnerLayer(true, animated); 133 | break; 134 | } 135 | } 136 | 137 | private void SetInnerLayerPressed(bool animated) 138 | { 139 | if (animated) 140 | { 141 | _innerCircleLayer.AddAnimation(TransformAnimation(PressDepthFactor, 0.25), null); 142 | } 143 | else 144 | { 145 | CATransaction.DisableActions = true; 146 | _innerCircleLayer.Transform.Scale(PressDepthFactor); 147 | 148 | CATransaction.Commit(); 149 | } 150 | } 151 | 152 | private void SetInnerLayer(bool recording, bool animated) 153 | { 154 | if (recording) 155 | { 156 | _innerCircleLayer.AddAnimation(TransformAnimation(0.5f, 0.15), null); 157 | _innerCircleLayer.CornerRadius = 8; 158 | } 159 | else 160 | { 161 | _innerCircleLayer.AddAnimation(TransformAnimation(1, 0.25), null); 162 | _innerCircleLayer.CornerRadius = 163 | Bounds.Inset(InnerCircleLayerInset, InnerCircleLayerInset).Width / 2; 164 | } 165 | } 166 | 167 | private CAAnimation TransformAnimation(float value, double duration) 168 | { 169 | const string keyPath = "transform.scale"; 170 | 171 | var animation = new CABasicAnimation 172 | { 173 | KeyPath = keyPath, 174 | From = _innerCircleLayer.PresentationLayer.ValueForKeyPath(new NSString(keyPath)), 175 | To = FromObject(value), 176 | Duration = duration, 177 | TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseInEaseOut), 178 | BeginTime = CAAnimation.CurrentMediaTime(), 179 | FillMode = CAFillMode.Forwards, 180 | RemovedOnCompletion = false 181 | }; 182 | 183 | return animation; 184 | } 185 | } -------------------------------------------------------------------------------- /src/Views/CustomControls/RecordDurationLabel.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views.CustomControls; 2 | 3 | [Register(nameof(RecordDurationLabel))] 4 | public sealed class RecordDurationLabel : UILabel 5 | { 6 | private double _backingSeconds; 7 | private NSTimer _secondTimer; 8 | private NSTimer _indicatorTimer; 9 | private const string AppearDisappearKeyPathString = "opacity"; 10 | 11 | private readonly Lazy _indicatorLayer = new Lazy(() => 12 | { 13 | var layer = new CALayer() 14 | { 15 | MasksToBounds = true, 16 | BackgroundColor = Defines.Colors.OrangeColor.CGColor, 17 | }; 18 | 19 | var layerFrame = layer.Frame; 20 | layerFrame.Size = new CGSize(6, 6); 21 | layer.Frame = layerFrame; 22 | layer.CornerRadius = layer.Frame.Width / 2; 23 | layer.Opacity = 0; 24 | 25 | return layer; 26 | }); 27 | 28 | public override void LayoutSubviews() 29 | { 30 | base.LayoutSubviews(); 31 | _indicatorLayer.Value.Position = new CGPoint(-7, Bounds.Height / 2); 32 | } 33 | 34 | public void Start() 35 | { 36 | if (_secondTimer != null) 37 | { 38 | return; 39 | } 40 | 41 | _secondTimer = NSTimer.CreateScheduledTimer(1, true, _ => 42 | { 43 | ++_backingSeconds; 44 | UpdateLabel(); 45 | }); 46 | _secondTimer.Tolerance += 1; 47 | 48 | _indicatorTimer = NSTimer.CreateScheduledTimer(1, true, nsTimer => { UpdateIndicator(0.2); }); 49 | _indicatorTimer.Tolerance = 0.1; 50 | 51 | UpdateIndicator(1); 52 | } 53 | 54 | public void Stop() 55 | { 56 | _secondTimer?.Invalidate(); 57 | _secondTimer = null; 58 | _backingSeconds = 0; 59 | UpdateLabel(); 60 | 61 | _indicatorTimer?.Invalidate(); 62 | _indicatorTimer = null; 63 | 64 | _indicatorLayer.Value.RemoveAllAnimations(); 65 | _indicatorLayer.Value.Opacity = 0; 66 | } 67 | 68 | private RecordDurationLabel(IntPtr handle) : base(handle) 69 | { 70 | Layer.AddSublayer(_indicatorLayer.Value); 71 | ClipsToBounds = false; 72 | } 73 | 74 | private void UpdateLabel() 75 | { 76 | Text = 77 | $"{_backingSeconds / Defines.Common.SecondsInHour:00}:" + 78 | $"{_backingSeconds / Defines.Common.SecondsInMinute % Defines.Common.SecondsInMinute:00}:" + 79 | $"{_backingSeconds % Defines.Common.SecondsInMinute:00}"; 80 | } 81 | 82 | private void UpdateIndicator(double appearDelay = 0) 83 | { 84 | const double disappearDelay = 0.25; 85 | const string animationKey = "blinkAnimationKey"; 86 | 87 | var appear = AppearAnimation(appearDelay); 88 | var disappear = DisappearAnimation(appear.BeginTime + appear.Duration + disappearDelay); 89 | 90 | var animation = new CAAnimationGroup 91 | { 92 | Animations = new[] { appear, disappear }, 93 | Duration = appear.Duration + disappear.Duration + appearDelay + disappearDelay, 94 | RemovedOnCompletion = true 95 | }; 96 | 97 | _indicatorLayer.Value.AddAnimation(animation, animationKey); 98 | } 99 | 100 | private CAAnimation AppearAnimation(double delay = 0) 101 | { 102 | var appear = new CABasicAnimation 103 | { 104 | KeyPath = AppearDisappearKeyPathString, 105 | From = FromObject(_indicatorLayer.Value.PresentationLayer.Opacity), 106 | To = FromObject(1), 107 | Duration = 0.15, 108 | TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseInEaseOut), 109 | BeginTime = delay, 110 | FillMode = CAFillMode.Forwards 111 | }; 112 | 113 | return appear; 114 | } 115 | 116 | private CAAnimation DisappearAnimation(double delay = 0) 117 | { 118 | var disappear = new CABasicAnimation 119 | { 120 | KeyPath = AppearDisappearKeyPathString, 121 | From = FromObject(_indicatorLayer.Value.PresentationLayer?.Opacity), 122 | To = FromObject(0), 123 | TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseIn), 124 | BeginTime = delay, 125 | Duration = 0.25 126 | }; 127 | 128 | return disappear; 129 | } 130 | } -------------------------------------------------------------------------------- /src/Views/CustomControls/ShutterButton.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views.CustomControls; 2 | 3 | [Register(nameof(ShutterButton))] 4 | public sealed class ShutterButton : UIButton 5 | { 6 | private readonly nfloat _outerBorderWidth = 3; 7 | private readonly nfloat _innerBorderWidth = 1.5f; 8 | private readonly nfloat _pressDepthFactor = 0.9f; 9 | private readonly CALayer _outerCircleLayer; 10 | private readonly CALayer _innerCircleLayer; 11 | private nfloat InnerCircleLayerInset => _outerBorderWidth + _innerBorderWidth; 12 | 13 | private const string PressAnimationKeyPath = "transform.scale"; 14 | 15 | public override bool Highlighted 16 | { 17 | get => base.Highlighted; 18 | set 19 | { 20 | base.Highlighted = value; 21 | SetInnerLayer(value, true); 22 | } 23 | } 24 | 25 | public ShutterButton(IntPtr handle) : base(handle) 26 | { 27 | BackgroundColor = UIColor.Clear; 28 | 29 | _outerCircleLayer = new CALayer 30 | { 31 | BackgroundColor = UIColor.Clear.CGColor, 32 | CornerRadius = Bounds.Width / 2, 33 | BorderWidth = _outerBorderWidth, 34 | BorderColor = TintColor.CGColor 35 | }; 36 | _innerCircleLayer = new CALayer 37 | { 38 | BackgroundColor = TintColor.CGColor 39 | }; 40 | 41 | Layer.AddSublayer(_outerCircleLayer); 42 | Layer.AddSublayer(_innerCircleLayer); 43 | 44 | CATransaction.DisableActions = true; 45 | CATransaction.Commit(); 46 | } 47 | 48 | public override void LayoutSubviews() 49 | { 50 | base.LayoutSubviews(); 51 | 52 | CATransaction.DisableActions = true; 53 | _outerCircleLayer.Frame = Bounds; 54 | 55 | _innerCircleLayer.Frame = Bounds.Inset(InnerCircleLayerInset, InnerCircleLayerInset); 56 | _innerCircleLayer.CornerRadius = 57 | Bounds.Inset(InnerCircleLayerInset, InnerCircleLayerInset).Width / 2; 58 | 59 | CATransaction.Commit(); 60 | } 61 | 62 | private void SetInnerLayer(bool tapped, bool animated) 63 | { 64 | if (animated) 65 | { 66 | var animation = new CABasicAnimation 67 | { 68 | KeyPath = PressAnimationKeyPath, 69 | Duration = 0.25 70 | }; 71 | 72 | if (tapped) 73 | { 74 | animation.From = 75 | _innerCircleLayer.PresentationLayer.ValueForKeyPath(new NSString(PressAnimationKeyPath)); 76 | animation.To = FromObject(_pressDepthFactor); 77 | } 78 | else 79 | { 80 | animation.From = FromObject(_pressDepthFactor); 81 | animation.To = FromObject(1.0); 82 | } 83 | 84 | animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseInEaseOut); 85 | animation.BeginTime = CAAnimation.CurrentMediaTime(); 86 | animation.FillMode = CAFillMode.Forwards; 87 | animation.RemovedOnCompletion = false; 88 | 89 | _innerCircleLayer.AddAnimation(animation, null); 90 | } 91 | else 92 | { 93 | CATransaction.DisableActions = true; 94 | _innerCircleLayer.SetValueForKeyPath(tapped ? FromObject(_pressDepthFactor) : FromObject(1), 95 | new NSString(PressAnimationKeyPath)); 96 | 97 | CATransaction.Commit(); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/Views/CustomControls/StationaryButton.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views.CustomControls; 2 | 3 | /// 4 | /// A button that keeps selected state when selected. 5 | /// 6 | [Register(nameof(StationaryButton))] 7 | public class StationaryButton : UIButton 8 | { 9 | public UIColor UnselectedTintColor; 10 | public UIColor SelectedTintColor; 11 | 12 | public override bool Highlighted 13 | { 14 | get => base.Highlighted; 15 | set 16 | { 17 | base.Highlighted = value; 18 | if (!Highlighted) 19 | { 20 | SetSelected(!Selected); 21 | } 22 | } 23 | } 24 | 25 | protected StationaryButton(IntPtr intPtr) : base(intPtr) 26 | { 27 | } 28 | 29 | [Export("awakeFromNib")] 30 | public override void AwakeFromNib() 31 | { 32 | base.AwakeFromNib(); 33 | UpdateTint(); 34 | } 35 | 36 | protected virtual void SelectionDidChange(bool animated) 37 | { 38 | UpdateTint(); 39 | } 40 | 41 | private void SetSelected(bool value) 42 | { 43 | if (Selected != value) 44 | { 45 | Selected = value; 46 | SelectionDidChange(true); 47 | } 48 | } 49 | 50 | private void UpdateTint() 51 | { 52 | TintColor = Selected ? SelectedTintColor : UnselectedTintColor; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Views/ImagePickerAssetCell.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views; 2 | 3 | public abstract class ImagePickerAssetCell : UICollectionViewCell 4 | { 5 | /// This image view will be used when setting an asset's image 6 | public abstract UIImageView ImageView { get; } 7 | 8 | /// This is a helper identifier that is used when properly displaying cells asynchronously 9 | public virtual string RepresentedAssetIdentifier { get; set; } 10 | 11 | protected ImagePickerAssetCell(IntPtr handle) : base(handle) 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /src/Views/ImagePickerView.cs: -------------------------------------------------------------------------------- 1 | namespace Softeq.ImagePicker.Views; 2 | 3 | public partial class ImagePickerView : UIView 4 | { 5 | public UICollectionView UICollectionView => CollectionView; 6 | 7 | protected internal ImagePickerView(IntPtr handle) : base(handle) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/Views/ImagePickerView.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio from the outlets and 4 | // actions declared in your storyboard file. 5 | // Manual changes to this file will not be maintained. 6 | // 7 | 8 | using Foundation; 9 | 10 | namespace Softeq.ImagePicker.Views 11 | { 12 | [Register ("ImagePickerView")] 13 | partial class ImagePickerView 14 | { 15 | [Outlet] 16 | UIKit.UICollectionView CollectionView { get; set; } 17 | 18 | void ReleaseDesignerOutlets () 19 | { 20 | if (CollectionView != null) { 21 | CollectionView.Dispose (); 22 | CollectionView = null; 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Views/ImagePickerView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Views/LivePhotoCameraCell.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Enums; 2 | using Softeq.ImagePicker.Public; 3 | 4 | namespace Softeq.ImagePicker.Views; 5 | 6 | public partial class LivePhotoCameraCell : CameraCollectionViewCell 7 | { 8 | public LivePhotoCameraCell(IntPtr handle) : base(handle) 9 | { 10 | } 11 | 12 | [Export("awakeFromNib")] 13 | public override void AwakeFromNib() 14 | { 15 | base.AwakeFromNib(); 16 | LiveIndicator.Alpha = 0; 17 | LiveIndicator.TintColor = Defines.Colors.YellowColor; 18 | 19 | EnableLivePhotoButton.UnselectedTintColor = UIColor.White; 20 | EnableLivePhotoButton.SelectedTintColor = Defines.Colors.YellowColor; 21 | } 22 | 23 | partial void SnapButtonTapped(NSObject sender) 24 | { 25 | if (EnableLivePhotoButton.Selected) 26 | { 27 | TakeLivePhoto(); 28 | } 29 | else 30 | { 31 | TakePicture(); 32 | } 33 | } 34 | 35 | partial void FlipButtonTapped(NSObject sender) 36 | { 37 | FlipCamera(); 38 | } 39 | 40 | public void UpdateWithCameraMode(CameraMode mode) 41 | { 42 | switch (mode) 43 | { 44 | case CameraMode.Photo: 45 | LiveIndicator.Hidden = true; 46 | EnableLivePhotoButton.Hidden = true; 47 | break; 48 | case CameraMode.PhotoAndLivePhoto: 49 | LiveIndicator.Hidden = false; 50 | EnableLivePhotoButton.Hidden = false; 51 | break; 52 | default: 53 | throw new ArgumentException($"Not supported {mode}"); 54 | } 55 | } 56 | 57 | public override void UpdateLivePhotoStatus(bool isProcessing, bool shouldAnimate) 58 | { 59 | Action updates = () => 60 | { 61 | LiveIndicator.Alpha = isProcessing ? 1 : 0; 62 | }; 63 | 64 | if (shouldAnimate) 65 | { 66 | Animate(0.25, updates); 67 | } 68 | else 69 | { 70 | updates.Invoke(); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/Views/LivePhotoCameraCell.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio to store outlets and 4 | // actions made in the UI designer. If it is removed, they will be lost. 5 | // Manual changes to this file may not be handled correctly. 6 | // 7 | 8 | using Foundation; 9 | using Softeq.ImagePicker.Views.CustomControls; 10 | 11 | namespace Softeq.ImagePicker.Views 12 | { 13 | [Register ("LivePhotoCameraCell")] 14 | partial class LivePhotoCameraCell 15 | { 16 | [Outlet] 17 | StationaryButton EnableLivePhotoButton { get; set; } 18 | 19 | [Outlet] 20 | CarvedLabel LiveIndicator { get; set; } 21 | 22 | [Outlet] 23 | ShutterButton SnapButton { get; set; } 24 | 25 | [Action ("FlipButtonTapped:")] 26 | partial void FlipButtonTapped (Foundation.NSObject sender); 27 | 28 | [Action ("SnapButtonTapped:")] 29 | partial void SnapButtonTapped (Foundation.NSObject sender); 30 | 31 | void ReleaseDesignerOutlets () 32 | { 33 | if (EnableLivePhotoButton != null) { 34 | EnableLivePhotoButton.Dispose (); 35 | EnableLivePhotoButton = null; 36 | } 37 | 38 | if (LiveIndicator != null) { 39 | LiveIndicator.Dispose (); 40 | LiveIndicator = null; 41 | } 42 | 43 | if (SnapButton != null) { 44 | SnapButton.Dispose (); 45 | SnapButton = null; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Views/VideoAssetCell.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Infrastructure.Extensions; 2 | 3 | namespace Softeq.ImagePicker.Views; 4 | 5 | [Register(nameof(VideoAssetCell))] 6 | public sealed class VideoAssetCell : AssetCell 7 | { 8 | private readonly UILabel _durationLabel; 9 | private readonly UIImageView _iconView; 10 | private readonly UIImageView _gradientView; 11 | private readonly NSDateComponentsFormatter _durationFormatter; 12 | 13 | public VideoAssetCell(IntPtr handle) : base(handle) 14 | { 15 | _durationFormatter = GetDurationFormatter(); 16 | 17 | _durationLabel = new UILabel(CGRect.Empty); 18 | _gradientView = new UIImageView(CGRect.Empty); 19 | _iconView = new UIImageView(CGRect.Empty); 20 | 21 | _gradientView.Hidden = true; 22 | 23 | _iconView.TintColor = UIColor.White; 24 | _iconView.ContentMode = UIViewContentMode.Center; 25 | 26 | _durationLabel.TextColor = UIColor.White; 27 | 28 | _durationLabel.Font = UIFont.SystemFontOfSize(12, UIFontWeight.Semibold); 29 | _durationLabel.TextAlignment = UITextAlignment.Right; 30 | 31 | ContentView.AddSubview(_gradientView); 32 | ContentView.AddSubview(_durationLabel); 33 | ContentView.AddSubview(_iconView); 34 | } 35 | 36 | public override void LayoutSubviews() 37 | { 38 | base.LayoutSubviews(); 39 | 40 | _gradientView.Frame = new CGRect(new CGPoint(0, Bounds.Height - 40), new CGSize(Bounds.Width, 40)); 41 | 42 | const int margin = 5; 43 | 44 | var frame = CGRect.Empty; 45 | frame.Size = new CGSize(50, 20); 46 | frame.Location = new CGPoint(ContentView.Bounds.Width - frame.Size.Width - margin, 47 | ContentView.Bounds.Height - frame.Size.Height - margin); 48 | _durationLabel.Frame = frame; 49 | 50 | frame.Size = new CGSize(21, 21); 51 | frame.Location = new CGPoint(margin, ContentView.Bounds.Height - frame.Height - margin); 52 | _iconView.Frame = frame; 53 | } 54 | 55 | public void Update(PHAsset asset) 56 | { 57 | switch (asset.MediaType) 58 | { 59 | case PHAssetMediaType.Image: 60 | UpdateImageAsset(asset); 61 | break; 62 | case PHAssetMediaType.Video: 63 | UpdateVideoAsset(asset); 64 | break; 65 | default: 66 | throw new ArgumentException("Support only video and image types"); 67 | } 68 | } 69 | 70 | private void UpdateVideoAsset(PHAsset asset) 71 | { 72 | _gradientView.Hidden = false; 73 | _gradientView.Image = 74 | UIImageExtensions.FromBundle(BundleAssets.Gradient) 75 | .CreateResizableImage(UIEdgeInsets.Zero, UIImageResizingMode.Stretch); 76 | _iconView.Hidden = false; 77 | _durationLabel.Hidden = false; 78 | _iconView.Image = UIImageExtensions.FromBundle(BundleAssets.IconBadgeVideo); 79 | _durationLabel.Text = _durationFormatter.StringFromTimeInterval(asset.Duration); 80 | } 81 | 82 | private void UpdateImageAsset(PHAsset asset) 83 | { 84 | if (asset.MediaSubtypes == PHAssetMediaSubtype.PhotoLive) 85 | { 86 | _gradientView.Hidden = false; 87 | _gradientView.Image = UIImageExtensions.FromBundle(BundleAssets.Gradient); 88 | _iconView.Hidden = false; 89 | _durationLabel.Hidden = true; 90 | _iconView.Image = UIImageExtensions.FromBundle(BundleAssets.IconBadgeLivePhoto); 91 | } 92 | else 93 | { 94 | _gradientView.Hidden = true; 95 | _iconView.Hidden = true; 96 | _durationLabel.Hidden = true; 97 | } 98 | } 99 | 100 | private NSDateComponentsFormatter GetDurationFormatter() 101 | { 102 | var formatter = new NSDateComponentsFormatter 103 | { 104 | UnitsStyle = NSDateComponentsFormatterUnitsStyle.Positional, 105 | AllowedUnits = NSCalendarUnit.Minute | NSCalendarUnit.Second, 106 | ZeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehavior.Pad 107 | }; 108 | return formatter; 109 | } 110 | } -------------------------------------------------------------------------------- /src/Views/VideoCameraCell.cs: -------------------------------------------------------------------------------- 1 | using Softeq.ImagePicker.Public; 2 | 3 | namespace Softeq.ImagePicker.Views; 4 | 5 | public partial class VideoCameraCell : CameraCollectionViewCell 6 | { 7 | public VideoCameraCell(IntPtr handle) : base(handle) 8 | { 9 | } 10 | 11 | [Export("awakeFromNib")] 12 | public override void AwakeFromNib() 13 | { 14 | base.AwakeFromNib(); 15 | RecordVideoButton.Enabled = false; 16 | RecordVideoButton.Alpha = 0; 17 | } 18 | 19 | partial void FlipButtonTapped(NSObject sender) 20 | { 21 | FlipCamera(); 22 | } 23 | 24 | partial void RecordButtonTapped(NSObject sender) 25 | { 26 | if ((sender as UIButton)?.Selected == true) 27 | { 28 | StopVideoRecording(); 29 | } 30 | else 31 | { 32 | StartVideoRecording(); 33 | } 34 | } 35 | 36 | public override void UpdateRecordingVideoStatus(bool isRecording, bool shouldAnimate) 37 | { 38 | RecordVideoButton.Selected = isRecording; 39 | 40 | if (isRecording) 41 | { 42 | RecordDurationLabel.Start(); 43 | } 44 | else 45 | { 46 | RecordDurationLabel.Stop(); 47 | } 48 | 49 | Action updates = () => FlipButton.Alpha = isRecording ? 0 : 1; 50 | 51 | if (shouldAnimate) 52 | { 53 | Animate(0.25, updates); 54 | } 55 | else 56 | { 57 | updates.Invoke(); 58 | } 59 | } 60 | 61 | public override void VideoRecodingDidBecomeReady() 62 | { 63 | RecordVideoButton.Enabled = true; 64 | Animate(0.25, () => 65 | { 66 | RecordVideoButton.Alpha = 1; 67 | }); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Views/VideoCameraCell.designer.cs: -------------------------------------------------------------------------------- 1 | // WARNING 2 | // 3 | // This file has been generated automatically by Visual Studio from the outlets and 4 | // actions declared in your storyboard file. 5 | // Manual changes to this file will not be maintained. 6 | // 7 | 8 | using Foundation; 9 | using Softeq.ImagePicker.Views.CustomControls; 10 | 11 | namespace Softeq.ImagePicker.Views 12 | { 13 | [Register ("VideoCameraCell")] 14 | partial class VideoCameraCell 15 | { 16 | [Outlet] 17 | UIKit.UIButton FlipButton { get; set; } 18 | 19 | 20 | [Outlet] 21 | RecordDurationLabel RecordDurationLabel { get; set; } 22 | 23 | 24 | [Outlet] 25 | RecordButton RecordVideoButton { get; set; } 26 | 27 | 28 | [Action ("FlipButtonTapped:")] 29 | partial void FlipButtonTapped (Foundation.NSObject sender); 30 | 31 | 32 | [Action ("RecordButtonTapped:")] 33 | partial void RecordButtonTapped (Foundation.NSObject sender); 34 | 35 | void ReleaseDesignerOutlets () 36 | { 37 | if (FlipButton != null) { 38 | FlipButton.Dispose (); 39 | FlipButton = null; 40 | } 41 | 42 | if (RecordDurationLabel != null) { 43 | RecordDurationLabel.Dispose (); 44 | RecordDurationLabel = null; 45 | } 46 | 47 | if (RecordVideoButton != null) { 48 | RecordVideoButton.Dispose (); 49 | RecordVideoButton = null; 50 | } 51 | } 52 | } 53 | } --------------------------------------------------------------------------------