├── .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 |
34 |
35 |
36 |
37 |
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 | }
--------------------------------------------------------------------------------