├── .ruby-version ├── .swift-version ├── Pod ├── Assets │ ├── .gitkeep │ ├── gridicons-audio.png │ ├── gridicons-camera.png │ ├── gridicons-pages.png │ ├── gridicons-audio@2x.png │ ├── gridicons-audio@3x.png │ ├── gridicons-camera@2x.png │ ├── gridicons-camera@3x.png │ ├── gridicons-pages@2x.png │ ├── gridicons-pages@3x.png │ ├── gridicons-camera-large.png │ ├── gridicons-video-camera.png │ ├── gridicons-camera-large@2x.png │ ├── gridicons-camera-large@3x.png │ ├── gridicons-video-camera@2x.png │ └── gridicons-video-camera@3x.png └── Classes │ ├── .gitkeep │ ├── WPMediaPickerResources.h │ ├── WPBadgeView.h │ ├── WPMediaCapturePreviewCollectionView.h │ ├── UIViewController+MediaAdditions.h │ ├── WPDateTimeHelpers.h │ ├── WPIndexMove.h │ ├── WPMediaPickerAlertHelper.h │ ├── WPMediaGroupTableViewCell.h │ ├── WPIndexMove.m │ ├── UIViewController+MediaAdditions.m │ ├── WPActionBar.h │ ├── WPMediaPicker.h │ ├── WPAssetViewController.h │ ├── WPImageExporter.h │ ├── WPMediaCapturePresenter.h │ ├── WPPHAssetDataSource.h │ ├── WPMediaCollectionViewCell.h │ ├── WPVideoPlayerView.h │ ├── WPMediaPickerResources.m │ ├── WPMediaPickerOptions.m │ ├── WPInputMediaPickerViewController.h │ ├── WPCarouselAssetsViewController.h │ ├── WPMediaPickerOptions.h │ ├── WPMediaGroupPickerViewController.h │ ├── WPMediaPickerAlertHelper.m │ ├── WPNavigationMediaPickerViewController.h │ ├── WPImageExporter.m │ ├── WPMediaGroupTableViewCell.m │ ├── WPDateTimeHelpers.m │ ├── WPActionBar.m │ ├── WPBadgeView.m │ ├── WPInputMediaPickerViewController.m │ ├── WPMediaCapturePresenter.m │ ├── WPCarouselAssetsViewController.m │ ├── WPMediaCapturePreviewCollectionView.m │ ├── WPMediaGroupPickerViewController.m │ ├── WPAssetViewController.m │ ├── WPVideoPlayerView.m │ └── WPMediaCollectionDataSource.h ├── .bundle └── config ├── Example ├── Tests │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── Tests-Prefix.pch │ ├── Tests-Info.plist │ ├── UnitTests.xctestplan │ └── WPDateTimeHelpersTests.m ├── WPMediaPicker │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── icon.imageset │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-76x76@3x.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-1024x1024.png │ │ │ ├── Icon-App-29x29@1x.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 │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024-1.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── DemoViewController.h │ ├── AppDelegate.h │ ├── PostProcessingViewController.h │ ├── SampleCustomHeaderView.h │ ├── WPPHAssetDataSource+Search.m │ ├── WPPHAssetDataSource+Search.h │ ├── SampleCellOverlayView.h │ ├── main.m │ ├── CustomPreviewViewController.h │ ├── WPMediaPicker-Prefix.pch │ ├── PostProcessingViewController.m │ ├── SampleCustomHeaderView.m │ ├── OptionsViewController.h │ ├── CustomPreviewViewController.m │ ├── WPMediaPicker-Info.plist │ ├── SampleCellOverlayView.m │ ├── AppDelegate.m │ ├── Launch Screen.storyboard │ └── OptionsViewController.m ├── Podfile ├── WPMediaPicker.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── WPMediaPicker-Example.xcscheme ├── WPMediaPicker.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── Podfile.lock ├── screenshots_1.jpg ├── Gemfile ├── .rubocop.yml ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .buildkite ├── publish-pod.sh └── pipeline.yml ├── fastlane └── Fastfile ├── .gitignore ├── WPMediaPicker.podspec ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md └── Gemfile.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.4 2 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /Pod/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Pod/Classes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: "vendor/bundle" 3 | -------------------------------------------------------------------------------- /Example/Tests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /screenshots_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/screenshots_1.jpg -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Pod/Assets/gridicons-audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-audio.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-camera.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-pages.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/DemoViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface DemoViewController : UITableViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Pod/Assets/gridicons-audio@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-audio@2x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-audio@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-audio@3x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-camera@2x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-camera@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-camera@3x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-pages@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-pages@2x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-pages@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-pages@3x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-camera-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-camera-large.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-video-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-video-camera.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-camera-large@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-camera-large@2x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-camera-large@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-camera-large@3x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-video-camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-video-camera@2x.png -------------------------------------------------------------------------------- /Pod/Assets/gridicons-video-camera@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Pod/Assets/gridicons-video-camera@3x.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'cocoapods', '~> 1.11' 6 | gem 'fastlane', '~> 2.189' 7 | gem 'rubocop', '~> 1.18' 8 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/AppDelegate.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface AppDelegate : UIResponder 4 | 5 | @property (strong, nonatomic) UIWindow *window; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Opt in to new cops by default 2 | AllCops: 3 | NewCops: enable 4 | 5 | # Allow the Podspec filename to match the project 6 | Naming/FileName: 7 | Exclude: 8 | - 'WPMediaPicker.podspec' 9 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/icon.imageset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/icon.imageset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/icon.imageset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/icon.imageset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/icon.imageset/Icon-App-76x76@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/icon.imageset/Icon-App-76x76@3x.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | 4 | ### Actual behavior 5 | 6 | 7 | ### Steps to reproduce the behavior 8 | 9 | 10 | ##### Tested on [device], iOS [version], WPMediaPicker [version] 11 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-1024x1024.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/PostProcessingViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PostProcessingViewController : UIViewController 4 | 5 | @property (nonatomic, copy) void (^onCompletion)(void); 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | To test: 4 | 5 | --- 6 | 7 | - [ ] I have considered if this change warrants release notes and have added them to the appropriate section in the `CHANGELOG.md` if necessary. 8 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-1024x1024-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-1024x1024-1.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-iOS/trunk/Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Example/WPMediaPicker/SampleCustomHeaderView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface SampleCustomHeaderView : UICollectionReusableView 6 | 7 | @end 8 | 9 | NS_ASSUME_NONNULL_END 10 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | platform :ios, '13.0' 4 | target 'WPMediaPicker' do 5 | pod 'WPMediaPicker', path: '../' 6 | 7 | target 'Tests' do 8 | inherit! :search_paths 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Example/Tests/Tests-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every test case source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | 9 | 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/WPPHAssetDataSource+Search.m: -------------------------------------------------------------------------------- 1 | #import "WPPHAssetDataSource+Search.h" 2 | 3 | @implementation WPPHAssetDataSource(Search) 4 | 5 | - (void)searchFor:(nullable NSString *)searchText { 6 | 7 | } 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/WPPHAssetDataSource+Search.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface WPPHAssetDataSource(Search) 5 | 6 | - (void)searchFor:(nullable NSString *)searchText; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Example/WPMediaPicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/SampleCellOverlayView.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface SampleCellOverlayView : UIView 5 | 6 | @property (nonatomic, copy) NSString *labelText; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaPickerResources.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import UIKit; 3 | 4 | @interface WPMediaPickerResources : NSObject 5 | 6 | + (NSBundle *)resourceBundle; 7 | 8 | + (UIImage *)imageNamed:(NSString *)imageName withExtension:(NSString *)extension; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /Pod/Classes/WPBadgeView.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | @interface WPBadgeView : UIView 5 | NS_ASSUME_NONNULL_BEGIN 6 | 7 | @property (nonatomic, strong) UILabel* label; 8 | @property (nonatomic) UIEdgeInsets insets; 9 | @property (nonatomic) CGFloat cornerRadius; 10 | 11 | NS_ASSUME_NONNULL_END 12 | @end 13 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaCapturePreviewCollectionView.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface WPMediaCapturePreviewCollectionView : UICollectionReusableView 4 | 5 | @property (nonatomic, assign) BOOL preferFrontCamera; 6 | 7 | - (void)stopCaptureOnCompletion:(void (^)(void))block; 8 | - (void)startCapture; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /Example/WPMediaPicker.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/WPMediaPicker.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/CustomPreviewViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | #import 4 | 5 | @interface CustomPreviewViewController : UIViewController 6 | 7 | @property (nonatomic, strong) id asset; 8 | 9 | - (instancetype)initWithAsset:(id)asset; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Pod/Classes/UIViewController+MediaAdditions.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface UIViewController (MediaAdditions) 6 | 7 | - (void)wpm_showAlertWithError:(NSError *)error okActionHandler:(void (^ __nullable)(UIAlertAction *action))handler; 8 | 9 | @end 10 | 11 | NS_ASSUME_NONNULL_END 12 | -------------------------------------------------------------------------------- /Example/WPMediaPicker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Pod/Classes/WPDateTimeHelpers.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WPDateTimeHelpers : NSObject 4 | 5 | + (NSString *)userFriendlyStringDateFromDate:(NSDate *)date; 6 | 7 | + (NSString *)userFriendlyStringTimeFromDate:(NSDate *)date; 8 | 9 | + (NSString *)stringFromTimeInterval:(NSTimeInterval)timeInterval; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Pod/Classes/WPIndexMove.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | #import "WPMediaCollectionDataSource.h" 4 | 5 | @interface WPIndexMove : NSObject 6 | 7 | @property (nonatomic, assign, readonly) NSUInteger from; 8 | @property (nonatomic, assign, readonly) NSUInteger to; 9 | 10 | - (instancetype)init:(NSUInteger)from to:(NSUInteger)to; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaPickerAlertHelper.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WPMediaPickerAlertHelper : NSObject 4 | 5 | + (nonnull UIAlertController *)buildAlertControllerWithError:(NSError * _Nullable)error 6 | okActionHandler:(void (^ __nullable)(UIAlertAction * _Nullable action))handler; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - WPMediaPicker (1.8.12) 3 | 4 | DEPENDENCIES: 5 | - WPMediaPicker (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | WPMediaPicker: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | WPMediaPicker: e9eaa804e1b0288d7969776608053ae0ea2941f2 13 | 14 | PODFILE CHECKSUM: 31590cb12765a73c9da27d6ea5b8b127c095d71d 15 | 16 | COCOAPODS: 1.14.2 17 | -------------------------------------------------------------------------------- /.buildkite/publish-pod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | PODSPEC_PATH="WPMediaPicker.podspec" 4 | SLACK_WEBHOOK=$PODS_SLACK_WEBHOOK 5 | 6 | echo "--- :rubygems: Setting up Gems" 7 | install_gems 8 | 9 | echo "--- :cocoapods: Publishing Pod to CocoaPods CDN" 10 | publish_pod $PODSPEC_PATH 11 | 12 | echo "--- :slack: Notifying Slack" 13 | slack_notify_pod_published $PODSPEC_PATH "$SLACK_WEBHOOK" 14 | -------------------------------------------------------------------------------- /Example/WPMediaPicker.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "test-collector-swift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/buildkite/test-collector-swift", 7 | "state" : { 8 | "revision" : "77c7f492f5c1c9ca159f73d18f56bbd1186390b0", 9 | "version" : "0.3.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/WPMediaPicker-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_5_0 10 | #warning "This project uses features only available in iOS SDK 5.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaGroupTableViewCell.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface WPMediaGroupTableViewCell : UITableViewCell 4 | 5 | @property (nonatomic, strong) UIImageView *imagePosterView; 6 | @property (nonatomic, strong) UILabel *titleLabel; 7 | @property (nonatomic, strong) UILabel *countLabel; 8 | @property (nonatomic, strong) UIColor *posterBackgroundColor UI_APPEARANCE_SELECTOR; 9 | @property (nonatomic, strong) NSString *groupIdentifier; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Pod/Classes/WPIndexMove.m: -------------------------------------------------------------------------------- 1 | #import "WPIndexMove.h" 2 | 3 | @interface WPIndexMove() 4 | 5 | @property (nonatomic, assign) NSUInteger from; 6 | @property (nonatomic, assign) NSUInteger to; 7 | 8 | @end 9 | 10 | @implementation WPIndexMove 11 | 12 | - (instancetype)init:(NSUInteger)from to:(NSUInteger)to 13 | { 14 | self = [super init]; 15 | if (self) { 16 | _from = from; 17 | _to = to; 18 | } 19 | return self; 20 | } 21 | @end 22 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon-App-76x76@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Icon-App-76x76@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Icon-App-76x76@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | default_platform(:ios) 4 | 5 | platform :ios do 6 | desc 'Builds the project and runs tests' 7 | lane :test do 8 | run_tests( 9 | workspace: 'Example/WPMediaPicker.xcworkspace', 10 | scheme: 'WPMediaPicker-Example', 11 | devices: ['iPhone 11'], 12 | deployment_target_version: '14.5', 13 | prelaunch_simulator: true, 14 | buildlog_path: File.join(__dir__, '.build', 'logs'), 15 | derived_data_path: File.join(__dir__, '.build', 'derived-data') 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Pod/Classes/UIViewController+MediaAdditions.m: -------------------------------------------------------------------------------- 1 | #import "UIViewController+MediaAdditions.h" 2 | #import "WPMediaCollectionDataSource.h" 3 | #import "WPMediaPickerAlertHelper.h" 4 | 5 | @implementation UIViewController (MediaAdditions) 6 | 7 | - (void)wpm_showAlertWithError:(NSError *)error okActionHandler:(void (^ __nullable)(UIAlertAction *action))handler { 8 | UIAlertController *alertController = [WPMediaPickerAlertHelper buildAlertControllerWithError:error okActionHandler:handler]; 9 | [self presentViewController:alertController animated:YES completion:nil]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | .build 6 | build/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | profile 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | 23 | # Fastlane 24 | fastlane/report.xml 25 | fastlane/test_output 26 | fastlane/README.md 27 | 28 | # We recommend against adding the Pods directory to your .gitignore. However 29 | # you should judge for yourself, the pros and cons are mentioned at: 30 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 31 | # 32 | Pods/ 33 | 34 | vendor/ 35 | -------------------------------------------------------------------------------- /Pod/Classes/WPActionBar.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WPActionBar : UIView 4 | 5 | /** 6 | The color for the top horizontal line. 7 | */ 8 | @property (nonatomic, strong) UIColor *lineColor UI_APPEARANCE_SELECTOR; 9 | 10 | /** 11 | The color for the action bar background. 12 | */ 13 | @property (nonatomic, strong) UIColor *barBackgroundColor UI_APPEARANCE_SELECTOR; 14 | 15 | /** 16 | Adds the given button to the left side of the bar 17 | 18 | @param button The button to add. 19 | */ 20 | - (void)addLeftButton:(UIButton *)button; 21 | 22 | /** 23 | Adds the given button to the right side of the bar 24 | 25 | @param button The button to add. 26 | */ 27 | - (void)addRightButton:(UIButton *)button; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaPicker.h: -------------------------------------------------------------------------------- 1 | #ifndef _WPMEDIAPICKER_ 2 | 3 | #import "WPAssetViewController.h" 4 | #import "WPCarouselAssetsViewController.h" 5 | #import "WPIndexMove.h" 6 | #import "WPInputMediaPickerViewController.h" 7 | #import "WPMediaCapturePreviewCollectionView.h" 8 | #import "WPMediaCollectionDataSource.h" 9 | #import "WPMediaCollectionViewCell.h" 10 | #import "WPMediaGroupPickerViewController.h" 11 | #import "WPMediaGroupTableViewCell.h" 12 | #import "WPMediaPickerViewController.h" 13 | #import "WPNavigationMediaPickerViewController.h" 14 | #import "WPPHAssetDataSource.h" 15 | #import "WPMediaCapturePreviewCollectionView.h" 16 | #import "WPVideoPlayerView.h" 17 | #import "WPActionBar.h" 18 | #import "WPMediaPickerAlertHelper.h" 19 | 20 | #endif /* _WPMEDIAPICKER_ */ 21 | -------------------------------------------------------------------------------- /Example/Tests/Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Pod/Classes/WPAssetViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "WPMediaCollectionDataSource.h" 3 | 4 | @class WPAssetViewController; 5 | 6 | @protocol WPAssetViewControllerDelegate 7 | 8 | - (void)assetViewController:(nonnull WPAssetViewController *)assetPreviewVC selectionChanged:(BOOL)selected; 9 | 10 | - (void)assetViewController:(nonnull WPAssetViewController *)assetPreviewVC failedWithError:(nonnull NSError *)error; 11 | 12 | @end 13 | 14 | @interface WPAssetViewController : UIViewController 15 | 16 | - (nonnull instancetype)initWithAsset:(nonnull id)asset; 17 | 18 | @property (nonatomic, strong, nonnull) id asset; 19 | @property (nonatomic, assign) BOOL selected; 20 | 21 | @property (nonatomic, weak, nullable) id delegate; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /Pod/Classes/WPImageExporter.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | This class implements two helper methods to facilitate export of UIImage to files. 5 | */ 6 | @interface WPImageExporter : NSObject 7 | 8 | /** 9 | Retrieve an URL for a file on the temporary folder using the extension provided 10 | 11 | @param fileExtension the extension to use. 12 | @return an URL for a temporary file. 13 | */ 14 | + (NSURL *)temporaryFileURLWithExtension:(NSString *)fileExtension; 15 | 16 | /** 17 | Writes an UIImage with the provided metadata to the designated URL using the JPG format. 18 | 19 | @param image the image to save 20 | @param metadata the metadata of the image 21 | @return the URL of the image if it was saved properly. 22 | */ 23 | + (BOOL)writeImage:(UIImage *)image withMetadata:(NSDictionary *)metadata toURL:(NSURL *)fileURL; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaCapturePresenter.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "WPMediaCollectionDataSource.h" 3 | 4 | @interface WPMediaCapturePresenter : NSObject 5 | 6 | /// Only image and video types are supported 7 | @property (nonatomic) WPMediaType mediaType; 8 | 9 | /// Present front camera if available 10 | @property (nonatomic) BOOL preferFrontCamera; 11 | 12 | /// Called when the capture view has been dismissed. 13 | /// mediaInfo will be populated if an image / video was captured. 14 | @property (nonatomic, copy, nullable) void (^completionBlock)(NSDictionary * _Nullable mediaInfo); 15 | 16 | + (BOOL)isCaptureAvailable; 17 | 18 | /// @param viewController The view controller to present the capture view from. 19 | - (nonnull instancetype)initWithPresentingViewController:(nonnull UIViewController *)viewController; 20 | 21 | /// Present the capture interface 22 | - (void)presentCapture; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/PostProcessingViewController.m: -------------------------------------------------------------------------------- 1 | #import "PostProcessingViewController.h" 2 | 3 | @interface PostProcessingViewController () 4 | 5 | @end 6 | 7 | @implementation PostProcessingViewController 8 | 9 | - (void)viewDidLoad { 10 | [super viewDidLoad]; 11 | 12 | self.title = NSLocalizedString(@"Post Processing", nil); 13 | self.view.backgroundColor = [UIColor whiteColor]; 14 | 15 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Done", nil) 16 | style:UIBarButtonItemStylePlain 17 | target:self 18 | action:@selector(dismissWasPressed)]; 19 | } 20 | 21 | - (void)dismissWasPressed { 22 | if (self.onCompletion) { 23 | self.onCompletion(); 24 | } 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Pod/Classes/WPPHAssetDataSource.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import Photos; 3 | 4 | #import "WPMediaCollectionDataSource.h" 5 | 6 | /** 7 | An implementation of the WPDataSource protocol using the Photos framework 8 | */ 9 | NS_CLASS_AVAILABLE_IOS(8_0) @interface WPPHAssetDataSource : NSObject 10 | 11 | + (instancetype)sharedInstance; 12 | 13 | @end 14 | 15 | /** 16 | An implementation of the WPMediaAsset protocol using the PHAsset class 17 | */ 18 | @interface PHAsset(WPMediaAsset) 19 | 20 | @end 21 | 22 | /** 23 | An implementation of the WPMediaGroup protocol using the PHAssetCollection class 24 | */ 25 | @interface PHAssetCollectionForWPMediaGroup : NSObject 26 | 27 | - (instancetype)initWithCollection:(PHAssetCollection *)collection mediaType:(WPMediaType)mediaType dispatchQueue:(dispatch_queue_t)queue; 28 | 29 | - (instancetype)initWithCollection:(PHAssetCollection *)collection mediaType:(WPMediaType)mediaType; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/SampleCustomHeaderView.m: -------------------------------------------------------------------------------- 1 | #import "SampleCustomHeaderView.h" 2 | 3 | @implementation SampleCustomHeaderView 4 | 5 | - (instancetype)initWithFrame:(CGRect)frame 6 | { 7 | if (self = [super initWithFrame:frame]) { 8 | [self commonInit]; 9 | } 10 | return self; 11 | } 12 | 13 | - (id)initWithCoder:(NSCoder *)aDecoder 14 | { 15 | if (self = [super initWithCoder:aDecoder]) { 16 | [self commonInit]; 17 | } 18 | return self; 19 | } 20 | 21 | - (void)commonInit 22 | { 23 | self.backgroundColor = [UIColor redColor]; 24 | 25 | UILabel *label = [UILabel new]; 26 | label.translatesAutoresizingMaskIntoConstraints = NO; 27 | label.text = NSLocalizedString(@"Custom Header", @""); 28 | [self addSubview:label]; 29 | 30 | [NSLayoutConstraint activateConstraints:@[ 31 | [label.centerXAnchor constraintEqualToAnchor:self.centerXAnchor], 32 | [label.centerYAnchor constraintEqualToAnchor:self.centerYAnchor] 33 | ]]; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaCollectionViewCell.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "WPMediaCollectionDataSource.h" 3 | #import "WPBadgeView.h" 4 | 5 | @interface WPMediaCollectionViewCell : UICollectionViewCell 6 | 7 | @property (nonatomic, strong) id asset; 8 | 9 | @property (nonatomic, assign) NSInteger position; 10 | 11 | @property (nonatomic, strong) UIColor *placeholderTintColor UI_APPEARANCE_SELECTOR; 12 | 13 | @property (nonatomic, strong) UIColor *loadingBackgroundColor UI_APPEARANCE_SELECTOR; 14 | 15 | @property (nonatomic, strong) UIColor *placeholderBackgroundColor UI_APPEARANCE_SELECTOR; 16 | 17 | @property (nonatomic, strong) UIColor *positionLabelUnselectedTintColor UI_APPEARANCE_SELECTOR; 18 | 19 | @property (nonatomic, assign) BOOL hiddenSelectionIndicator; 20 | 21 | @property (nonatomic, strong) UIView *overlayView; 22 | 23 | @property (nonatomic, strong) WPBadgeView* badgeView; 24 | 25 | @end 26 | 27 | @protocol ReusableOverlayView 28 | 29 | @optional 30 | 31 | - (void)prepareForReuse; 32 | 33 | @end 34 | 35 | -------------------------------------------------------------------------------- /Pod/Classes/WPVideoPlayerView.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import UIKit; 3 | @import AVFoundation; 4 | 5 | @class WPVideoPlayerView; 6 | 7 | @protocol WPVideoPlayerViewDelegate 8 | 9 | - (void)videoPlayerView:(WPVideoPlayerView *)playerView didFailWithError:(NSError *)error; 10 | - (void)videoPlayerViewStarted:(WPVideoPlayerView *)playerView; 11 | - (void)videoPlayerViewFinish:(WPVideoPlayerView *)playerView; 12 | 13 | @end 14 | 15 | @interface WPVideoPlayerView: UIView 16 | 17 | 18 | @property (nonatomic, assign) BOOL loop; 19 | 20 | @property (nonatomic, weak) id delegate; 21 | 22 | @property (nonatomic, strong) NSURL *videoURL; 23 | 24 | @property (nonatomic, strong) AVAsset *asset; 25 | 26 | @property (nonatomic, assign) BOOL controlToolbarHidden; 27 | 28 | @property (nonatomic, assign) BOOL shouldAutoPlay; 29 | 30 | - (void)setControlToolbarHidden:(BOOL)hidden animated:(BOOL)animated; 31 | - (void)setControlToolbarHidden:(BOOL)hidden animated:(BOOL)animated completion:(void(^)(void))completion; 32 | 33 | @property (nonatomic, readonly) UIToolbar *controlToolbar; 34 | 35 | - (void)play; 36 | 37 | - (void)pause; 38 | 39 | @end 40 | 41 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaPickerResources.m: -------------------------------------------------------------------------------- 1 | #import "WPMediaPickerResources.h" 2 | 3 | static NSString *const ResourcesBundleName = @"WPMediaPicker"; 4 | 5 | @implementation WPMediaPickerResources 6 | 7 | + (NSBundle *)resourceBundle 8 | { 9 | static NSBundle *_bundle = nil; 10 | static dispatch_once_t _onceToken; 11 | dispatch_once(&_onceToken, ^{ 12 | NSBundle * classBundle = [NSBundle bundleForClass:[self class]]; 13 | NSString * bundlePath = [classBundle pathForResource:ResourcesBundleName ofType:@"bundle"]; 14 | _bundle = [NSBundle bundleWithPath:bundlePath]; 15 | }); 16 | return _bundle; 17 | } 18 | 19 | + (UIImage *)imageNamed:(NSString *)imageName withExtension:(NSString *)extension 20 | { 21 | int scale = [[UIScreen mainScreen] scale]; 22 | NSString *scaleAdjustedImageName; 23 | UIImage *image = nil; 24 | do { 25 | if (scale > 1) { 26 | scaleAdjustedImageName = [NSString stringWithFormat:@"%@@%ix",imageName, scale]; 27 | } else { 28 | scaleAdjustedImageName = [imageName copy]; 29 | } 30 | NSString *path = [[self resourceBundle] pathForResource:scaleAdjustedImageName ofType:extension]; 31 | image = [UIImage imageWithContentsOfFile:path]; 32 | if (!image) { 33 | scale--; 34 | } 35 | } while (scale > 0 && !image); 36 | return image; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaPickerOptions.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "WPMediaPickerOptions.h" 3 | #import 4 | 5 | @implementation WPMediaPickerOptions 6 | 7 | - (instancetype)init { 8 | self = [super init]; 9 | if (self) { 10 | _allowCaptureOfMedia = YES; 11 | _preferFrontCamera = NO; 12 | _showMostRecentFirst = NO; 13 | _filter = WPMediaTypeVideo | WPMediaTypeImage; 14 | _allowMultipleSelection = YES; 15 | _scrollVertically = YES; 16 | _showSearchBar = NO; 17 | _showActionBar = YES; 18 | _badgedUTTypes = [NSSet set]; 19 | _preferredStatusBarStyle = UIStatusBarStyleDefault; 20 | } 21 | return self; 22 | } 23 | 24 | - (id)copyWithZone:(NSZone *)zone { 25 | WPMediaPickerOptions *options = [WPMediaPickerOptions new]; 26 | options.allowCaptureOfMedia = self.allowCaptureOfMedia; 27 | options.preferFrontCamera = self.preferFrontCamera; 28 | options.showMostRecentFirst = self.showMostRecentFirst; 29 | options.filter = self.filter; 30 | options.allowMultipleSelection = self.allowMultipleSelection; 31 | options.scrollVertically = self.scrollVertically; 32 | options.showSearchBar = self.showSearchBar; 33 | options.showActionBar = self.showActionBar; 34 | options.badgedUTTypes = [self.badgedUTTypes copy]; 35 | options.preferredStatusBarStyle = self.preferredStatusBarStyle; 36 | 37 | return options; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | # Nodes with values to reuse in the pipeline. 2 | common_params: 3 | plugins: &common_plugins 4 | - automattic/a8c-ci-toolkit#2.13.0 5 | # Common environment values to use with the `env` key. 6 | env: &common_env 7 | IMAGE_ID: xcode-15.0.1 8 | 9 | # This is the default pipeline – it will build and test the app 10 | steps: 11 | ################ 12 | # Build and Test 13 | ################ 14 | - label: "🧪 Build and Test" 15 | key: "test" 16 | command: | 17 | build_and_test_pod 18 | env: *common_env 19 | plugins: *common_plugins 20 | artifact_paths: ".build/logs/*.log" 21 | 22 | ################# 23 | # Validate Podspec 24 | ################# 25 | - label: "🔬 Validate Podspec" 26 | key: "validate" 27 | command: | 28 | validate_podspec 29 | env: *common_env 30 | plugins: *common_plugins 31 | artifact_paths: ".build/logs/*.log" 32 | 33 | ################# 34 | # Lint 35 | ################# 36 | - label: "🧹 Lint" 37 | key: "lint" 38 | command: | 39 | lint_pod 40 | env: *common_env 41 | plugins: *common_plugins 42 | 43 | ################# 44 | # Publish the Podspec (if we're building a tag) 45 | ################# 46 | - label: "⬆️ Publish Podspec" 47 | key: "publish" 48 | command: .buildkite/publish-pod.sh 49 | env: *common_env 50 | plugins: *common_plugins 51 | depends_on: 52 | - "test" 53 | - "validate" 54 | - "lint" 55 | if: build.tag != null 56 | -------------------------------------------------------------------------------- /WPMediaPicker.podspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Pod::Spec.new do |s| 4 | s.name = 'WPMediaPicker' 5 | s.version = '1.8.12' 6 | 7 | s.summary = 'WPMediaPicker is an iOS controller that allows capture and picking of media assets.' 8 | s.description = <<-DESC 9 | WPMediaPicker is an iOS controller that allows capture and picking of media assets. 10 | It allows: 11 | * Multiple selection of media. 12 | * Capture of new media while selecting 13 | DESC 14 | 15 | s.homepage = 'https://github.com/wordpress-mobile/MediaPicker-iOS' 16 | s.screenshots = 'https://raw.githubusercontent.com/wordpress-mobile/WPMediaPicker/trunk/screenshots_1.jpg' 17 | s.license = { type: 'GPLv2', file: 'LICENSE' } 18 | s.author = { 'The WordPress Mobile Team' => 'mobile@wordpress.org' } 19 | 20 | s.platform = :ios, '13.0' 21 | s.swift_version = '5.0' 22 | 23 | s.source = { git: 'https://github.com/wordpress-mobile/MediaPicker-iOS.git', tag: s.version.to_s } 24 | s.source_files = 'Pod/Classes' 25 | s.resource_bundles = { 26 | 'WPMediaPicker' => ['Pod/Assets/*.png'] 27 | } 28 | 29 | s.public_header_files = 'Pod/Classes/**/*.h' 30 | s.private_header_files = 'Pod/Classes/WPDateTimeHelpers.h', 'Pod/Classes/WPImageExporter.h', 31 | 'Pod/Classes/UIViewController+MediaAdditions.h' 32 | s.frameworks = 'UIKit', 'Photos', 'AVFoundation', 'ImageIO' 33 | end 34 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/OptionsViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | extern NSString const *MediaPickerOptionsShowMostRecentFirst; 4 | extern NSString const *MediaPickerOptionsShowCameraCapture; 5 | extern NSString const *MediaPickerOptionsPreferFrontCamera; 6 | extern NSString const *MediaPickerOptionsAllowMultipleSelection; 7 | extern NSString const *MediaPickerOptionsPostProcessingStep; 8 | extern NSString const *MediaPickerOptionsFilterType; 9 | extern NSString const *MediaPickerOptionsCustomPreview; 10 | extern NSString const *MediaPickerOptionsScrollInputPickerVertically; 11 | extern NSString const *MediaPickerOptionsShowSampleCellOverlays; 12 | extern NSString const *MediaPickerOptionsShowSearchBar; 13 | extern NSString const *MediaPickerOptionsShowActionBar; 14 | /// Note that a custom header cannot be displayed at the same time as the in-picker camera capture cell. 15 | /// If both are specified, the custom header will take precedence. 16 | extern NSString const *MediaPickerOptionsShowCustomHeader; 17 | 18 | @class OptionsViewController; 19 | 20 | @protocol OptionsViewControllerDelegate 21 | 22 | - (void)optionsViewController:(OptionsViewController *)optionsViewController changed:(NSDictionary *)options; 23 | 24 | - (void)cancelOptionsViewController:(OptionsViewController *)optionsViewController; 25 | 26 | @end 27 | @interface OptionsViewController : UITableViewController 28 | 29 | @property (nonatomic, weak) id delegate; 30 | @property (nonatomic, copy) NSDictionary *options; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Example/Tests/UnitTests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "0B1AE21B-1942-457C-8E85-D1D9F3281758", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "environmentVariableEntries" : [ 13 | { 14 | "key" : "BUILDKITE_ANALYTICS_TOKEN", 15 | "value" : "$(BUILDKITE_ANALYTICS_TOKEN)" 16 | }, 17 | { 18 | "key" : "BUILDKITE_BRANCH", 19 | "value" : "$(BUILDKITE_BRANCH)" 20 | }, 21 | { 22 | "key" : "BUILDKITE_BUILD_ID", 23 | "value" : "$(BUILDKITE_BUILD_ID)" 24 | }, 25 | { 26 | "key" : "BUILDKITE_BUILD_NUMBER", 27 | "value" : "$(BUILDKITE_BUILD_NUMBER)" 28 | }, 29 | { 30 | "key" : "BUILDKITE_BUILD_URL", 31 | "value" : "$(BUILDKITE_BUILD_URL)" 32 | }, 33 | { 34 | "key" : "BUILDKITE_COMMIT", 35 | "value" : "$(BUILDKITE_COMMIT)" 36 | }, 37 | { 38 | "key" : "BUILDKITE_JOB_ID", 39 | "value" : "$(BUILDKITE_JOB_ID)" 40 | }, 41 | { 42 | "key" : "BUILDKITE_MESSAGE", 43 | "value" : "$(BUILDKITE_MESSAGE)" 44 | } 45 | ], 46 | "targetForVariableExpansion" : { 47 | "containerPath" : "container:WPMediaPicker.xcodeproj", 48 | "identifier" : "6003F589195388D20070C39A", 49 | "name" : "WPMediaPicker" 50 | }, 51 | "testTimeoutsEnabled" : true 52 | }, 53 | "testTargets" : [ 54 | { 55 | "target" : { 56 | "containerPath" : "container:WPMediaPicker.xcodeproj", 57 | "identifier" : "6003F5AD195388D20070C39A", 58 | "name" : "Tests" 59 | } 60 | } 61 | ], 62 | "version" : 1 63 | } 64 | -------------------------------------------------------------------------------- /Pod/Classes/WPInputMediaPickerViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "WPMediaPickerViewController.h" 3 | 4 | 5 | /** 6 | A class to be used as an input view for an UITextView or UITextField. 7 | 8 | The mediaToolbar property provides a toolbar that can be used as the inputAccessoryView for this inputView. 9 | */ 10 | @interface WPInputMediaPickerViewController : UIViewController 11 | 12 | /** 13 | Init a WPInputMediaPickerViewController with the selection options 14 | 15 | @param options an WPMediaPickerOption object 16 | @return an initiated WPInputMediaPickerViewController with the designated options 17 | */ 18 | - (instancetype _Nonnull )initWithOptions:(nonnull WPMediaPickerOptions *)options; 19 | 20 | /** 21 | The delegate for the WPMediaPickerViewController events 22 | */ 23 | @property (nonatomic, weak, nullable) id mediaPickerDelegate; 24 | 25 | /** 26 | The object that acts as the data source of the media picker. 27 | 28 | @Discussion 29 | If no object is defined before the picker is show then the picker will use a shared data source that access the user media library. 30 | */ 31 | @property (nonatomic, weak, nullable) id dataSource; 32 | 33 | /** 34 | The internal WPMediaPickerViewController that is used to display the media. 35 | */ 36 | @property (nonatomic, readonly, nonnull) WPMediaPickerViewController *mediaPicker; 37 | 38 | /** 39 | A toolbar that can be used as the inputAccessoryView for this inputView. 40 | */ 41 | @property (nonatomic, readonly, nonnull) UIToolbar *mediaToolbar; 42 | 43 | /** 44 | * Presents the system image / video capture view controller, presented from `viewControllerToUseToPresent`. 45 | */ 46 | - (void)showCapture; 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Pod/Classes/WPCarouselAssetsViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "WPMediaCollectionDataSource.h" 3 | #import "WPAssetViewController.h" 4 | 5 | NS_ASSUME_NONNULL_BEGIN 6 | 7 | @class WPCarouselAssetsViewController; 8 | 9 | 10 | /** 11 | A protocol that has to be implemented when the carousel controller needs to present a custom external view controller 12 | to show an specific asset. 13 | */ 14 | @protocol WPCarouselAssetsViewControllerDelegate 15 | /** 16 | Asks the delegate for a view controller to be presented, showing the given asset. 17 | 18 | @return The view controller to show, or nil to use the default internal WPAssetViewController. 19 | */ 20 | - (nullable UIViewController *)carouselController:(WPCarouselAssetsViewController *)controller viewControllerForAsset:(id)asset; 21 | 22 | /** 23 | Asks the delegate for the asset object related to the given view controller. 24 | */ 25 | - (id)carouselController:(WPCarouselAssetsViewController *)controller assetForViewController:(UIViewController *)viewController; 26 | @end 27 | 28 | 29 | @interface WPCarouselAssetsViewController : UIPageViewController 30 | 31 | @property (nonatomic, weak, nullable) id assetViewDelegate; 32 | 33 | @property (nonatomic, weak, nullable) id carouselDelegate; 34 | 35 | /** 36 | Init a WPCarouselAssetsViewController with the list of assets to preview. 37 | 38 | @param assets An array of assets to show in the carousel preview. 39 | @return an initiated WPCarouselAssetsViewController. 40 | */ 41 | - (instancetype)initWithAssets:(NSArray> *)assets; 42 | 43 | 44 | /** 45 | Set a new asset as the presenting asset in the carousel preview 46 | 47 | @param index Index of the asset to present 48 | @param animated Should the change be animated? 49 | */ 50 | - (void)setPreviewingAssetAtIndex:(NSInteger)index animated:(BOOL)animated; 51 | 52 | @end 53 | 54 | NS_ASSUME_NONNULL_END 55 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaPickerOptions.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "WPMediaCollectionDataSource.h" 3 | 4 | @interface WPMediaPickerOptions: NSObject 5 | 6 | /** 7 | If YES the picker will show a cell that allows capture of new media, that can be used immediatelly 8 | */ 9 | @property (nonatomic, assign) BOOL allowCaptureOfMedia; 10 | 11 | /** 12 | If YES and the media picker allows media capturing, it will use the front camera by default when possible 13 | */ 14 | @property (nonatomic, assign) BOOL preferFrontCamera; 15 | 16 | /** 17 | If YES the picker will show the most recent items on the top left. If not set it will show on the bottom right. Either way it will always scroll to the most recent item when showing the picker. 18 | */ 19 | @property (nonatomic, assign) BOOL showMostRecentFirst; 20 | 21 | /** 22 | * Sets what kind of elements the picker show: allAssets, allPhotos, allVideos 23 | */ 24 | @property (nonatomic, assign) WPMediaType filter; 25 | 26 | /** 27 | If YES the picker will allow the selection of multiple items. By default this value is YES. 28 | */ 29 | @property (nonatomic, assign) BOOL allowMultipleSelection; 30 | 31 | /** 32 | If YES the picker will scroll media vertically. Defaults to YES (vertical). 33 | */ 34 | @property (nonatomic, assign) BOOL scrollVertically; 35 | 36 | /** 37 | If YES the picker will show a search bar on top. 38 | */ 39 | @property (nonatomic, assign) BOOL showSearchBar; 40 | 41 | /** 42 | If YES, the picker will use a bottom action bar instead of the top right action button for multiple selection. By default the value is YES. 43 | */ 44 | @property (nonatomic, assign) BOOL showActionBar; 45 | 46 | /** 47 | A list of UTTypes where the picker cell should show a badge showing the file type. (i.e. UTTypeGif) 48 | */ 49 | @property (nonatomic, strong, nonnull) NSSet *badgedUTTypes; 50 | 51 | /** 52 | The status bar style to use for the media picker. 53 | */ 54 | @property (nonatomic, assign) UIStatusBarStyle preferredStatusBarStyle; 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaGroupPickerViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "WPMediaPickerViewController.h" 3 | 4 | @protocol WPMediaGroupPickerViewControllerDelegate; 5 | 6 | /** 7 | The WPMediaGroupPickerViewController class creates a controller object that allows the user to view and select a ALAssetGroup ina table view. 8 | */ 9 | @interface WPMediaGroupPickerViewController : UITableViewController 10 | 11 | @property (nonatomic, weak) id delegate; 12 | 13 | /** 14 | The WPMediaCollectionDataSource that is being used to display the assets and groups. If not set the picker will create a new one. 15 | */ 16 | @property (nonatomic, weak) id dataSource; 17 | 18 | @end 19 | 20 | @protocol WPMediaGroupPickerViewControllerDelegate 21 | 22 | /** 23 | * @name Closing the Picker 24 | */ 25 | 26 | /** 27 | * Tells the delegate that the user finish picking photos or videos. 28 | * 29 | * @param picker The controller object managing the assets picker interface. 30 | * @param group the user selected WPMediaGroup 31 | * 32 | */ 33 | - (void)mediaGroupPickerViewController:(WPMediaGroupPickerViewController *)picker didPickGroup:(id)group; 34 | 35 | @optional 36 | 37 | /** 38 | * Tells the delegate that the user cancelled the pick operation. 39 | * 40 | * @param picker The controller object managing the assets group picker interface. 41 | * 42 | */ 43 | - (void)mediaGroupPickerViewControllerDidCancel:(WPMediaGroupPickerViewController *)picker; 44 | 45 | /** Asks the delegate to handle an error found by the picker 46 | * If the method is not implemented or it returns NO the media picker will default to showing an alert with the an generic error message 47 | * 48 | * @param picker The controller object managing the assets picker interface. 49 | * @param error The error to show 50 | * @return YES if the error was handled by the delegate 51 | */ 52 | - (BOOL)mediaGroupPickerViewController:(WPMediaGroupPickerViewController *)picker handleError:(NSError *)error; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/CustomPreviewViewController.m: -------------------------------------------------------------------------------- 1 | #import "CustomPreviewViewController.h" 2 | 3 | @interface CustomPreviewViewController () 4 | @property (nonatomic, strong) UIImageView *imageView; 5 | @end 6 | 7 | @implementation CustomPreviewViewController 8 | 9 | - (instancetype)initWithAsset:(id)asset 10 | { 11 | if (self = [super initWithNibName:nil bundle:nil]) { 12 | _asset = asset; 13 | } 14 | 15 | return self; 16 | } 17 | 18 | - (void)viewDidLoad 19 | { 20 | [super viewDidLoad]; 21 | 22 | self.title = NSLocalizedString(@"Preview", @""); 23 | 24 | self.view.backgroundColor = [UIColor greenColor]; 25 | 26 | [self addImageView]; 27 | [self loadImage]; 28 | } 29 | 30 | - (void)addImageView 31 | { 32 | UIImageView *imageView = [UIImageView new]; 33 | imageView.contentMode = UIViewContentModeScaleAspectFill; 34 | imageView.translatesAutoresizingMaskIntoConstraints = NO; 35 | [self.view addSubview:imageView]; 36 | 37 | [NSLayoutConstraint activateConstraints:@[ 38 | [imageView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], 39 | [imageView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor], 40 | [imageView.widthAnchor constraintEqualToConstant:200], 41 | [imageView.heightAnchor constraintEqualToConstant:200], 42 | ]]; 43 | 44 | self.imageView = imageView; 45 | } 46 | 47 | - (void)loadImage 48 | { 49 | if ([self.asset assetType] == WPMediaTypeImage) { 50 | __weak __typeof__(self) weakSelf = self; 51 | [self.asset imageWithSize:CGSizeMake(200, 200) completionHandler:^(UIImage *result, NSError *error) { 52 | __typeof__(self) strongSelf = weakSelf; 53 | if (!strongSelf) { 54 | return; 55 | } 56 | dispatch_async(dispatch_get_main_queue(), ^{ 57 | strongSelf.imageView.image = result; 58 | }); 59 | }]; 60 | } 61 | } 62 | 63 | - (CGSize)preferredContentSize 64 | { 65 | return CGSizeMake(200, 200); 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/WPMediaPicker-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | NSCameraUsageDescription 28 | To take photos or videos to use in your posts. 29 | NSMicrophoneUsageDescription 30 | For your videos to have sound on them. 31 | NSPhotoLibraryUsageDescription 32 | In order to use photos from your library on posts. 33 | UILaunchStoryboardName 34 | Launch Screen 35 | UIRequiredDeviceCapabilities 36 | 37 | armv7 38 | 39 | UIStatusBarHidden 40 | 41 | UIStatusBarStyle 42 | UIStatusBarStyleLightContent 43 | UISupportedInterfaceOrientations 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UISupportedInterfaceOrientations~ipad 50 | 51 | UIInterfaceOrientationPortrait 52 | UIInterfaceOrientationPortraitUpsideDown 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UIViewControllerBasedStatusBarAppearance 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | First off, thank you for contributing! We're excited to collaborate with you! 🎉 4 | 5 | The following is a set of guidelines for the many ways you can join our collective effort. 6 | 7 | Before anything else, please take a moment to read our [Code of Conduct](CODE_OF_CONDUCT.md). We expect all participants, from full-timers to occasional tinkerers, to uphold it. 8 | 9 | ## Reporting Bugs, Asking Questions, and Suggesting Features 10 | 11 | Have a suggestion or feedback? Please go to [Issues](https://github.com/wordpress-mobile/MediaPicker-iOS/issues) and [open a new issue](https://github.com/wordpress-mobile/MediaPicker-iOS/issues/new). Prefix the title with a category like _"Bug:"_, _"Question:"_, or _"Feature Request:"_. Screenshots help us resolve issues and answer questions faster, so thanks for including some if you can. 12 | 13 | ## Submitting Code Changes 14 | 15 | If you're just getting started and want to familiarize yourself with the app’s code, we suggest looking at [these issues](https://github.com/wordpress-mobile/MediaPicker-iOS/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) with the **good first issue** label. But if you’d like to tackle something different, you're more than welcome to visit the [Issues](https://github.com/wordpress-mobile/MediaPicker-iOS/issues) page and pick an item that interests you. 16 | 17 | We always try to avoid duplicating efforts, so if you decide to work on an issue, leave a comment to state your intent. If you choose to focus on a new feature or the change you’re proposing is significant, we recommend waiting for a response before proceeding. The issue may no longer align with project goals. 18 | 19 | If the change is trivial, feel free to send a pull request without notifying us. 20 | 21 | ### Pull Requests and Code Reviews 22 | 23 | All code contributions pass through pull requests. If you haven't created a pull request before, we recommend this free video series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 24 | 25 | The core team monitors and reviews all pull requests. Depending on the changes, we will either approve them or close them with an explanation. We might also work with you to improve a pull request before approval. 26 | 27 | We do our best to respond quickly to all pull requests. If you don't get a response from us after a week, feel free to reach out to us via Slack. 28 | 29 | ## Getting in Touch 30 | 31 | If you have questions or just want to say hi, join the [WordPress Slack](https://make.wordpress.org/chat/) and drop a message on the `#mobile` channel. -------------------------------------------------------------------------------- /Pod/Classes/WPMediaPickerAlertHelper.m: -------------------------------------------------------------------------------- 1 | #import "WPMediaPickerAlertHelper.h" 2 | #import "WPMediaCollectionDataSource.h" 3 | 4 | @implementation WPMediaPickerAlertHelper 5 | 6 | + (nonnull UIAlertController *)buildAlertControllerWithError:(NSError * _Nullable)error 7 | okActionHandler:(void (^ __nullable)(UIAlertAction * _Nullable action))handler { 8 | NSString *title = NSLocalizedString(@"Media Library", @"Title for alert when a generic error happened when loading media"); 9 | NSString *message = NSLocalizedString(@"There was a problem when trying to access your media. Please try again later.", @"Explaining to the user there was an generic error accesing media."); 10 | NSString *cancelText = NSLocalizedString(@"OK", ""); 11 | NSString *otherButtonTitle = nil; 12 | if (error.domain == WPMediaPickerErrorDomain) { 13 | title = NSLocalizedString(@"Media Library", @"Title for alert when access to the media library is not granted by the user"); 14 | if (error.code == WPMediaPickerErrorCodePermissionDenied) { 15 | otherButtonTitle = NSLocalizedString(@"Open Settings", @"Go to the settings app"); 16 | message = NSLocalizedString(@"This app needs permission to access your device media library in order to add photos and/or video to your posts. Please change the privacy settings if you wish to allow this.", 17 | @"Explaining to the user why the app needs access to the device media library."); 18 | } else if (error.code == WPMediaPickerErrorCodeRestricted) { 19 | message = NSLocalizedString(@"Your app is not authorized to access media library due to active restrictions such as parental controls. Please check your parental control settings in this device.", 20 | @"Explaining to the user why the app needs access to the device media library."); 21 | } 22 | } 23 | 24 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title 25 | message:message 26 | preferredStyle:UIAlertControllerStyleAlert]; 27 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:cancelText 28 | style:UIAlertActionStyleCancel 29 | handler:handler]; 30 | [alertController addAction:okAction]; 31 | 32 | if (otherButtonTitle) { 33 | UIAlertAction *otherAction = [UIAlertAction actionWithTitle:otherButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 34 | NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; 35 | [[UIApplication sharedApplication] openURL:settingsURL options:@{} completionHandler:nil]; 36 | }]; 37 | [alertController addAction:otherAction]; 38 | } 39 | 40 | return alertController; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /Pod/Classes/WPNavigationMediaPickerViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "WPMediaCollectionDataSource.h" 3 | #import "WPMediaPickerOptions.h" 4 | 5 | @class WPMediaPickerViewController; 6 | @protocol WPMediaPickerViewControllerDelegate; 7 | 8 | @interface WPNavigationMediaPickerViewController : UIViewController 9 | 10 | /** 11 | Init a WPNavigationMediaPickerViewController with the selection options 12 | 13 | @param options an WPMediaPickerOption object 14 | @return an initiated WPNavigationMediaPickerViewController with the designated options 15 | */ 16 | - (nonnull instancetype)initWithOptions:(nonnull WPMediaPickerOptions *)options; 17 | 18 | @property (nonatomic, weak, nullable) id delegate; 19 | 20 | /** 21 | The internal WPMediaPickerViewController that is used to display the media. 22 | */ 23 | @property (nonatomic, readonly, nonnull) WPMediaPickerViewController *mediaPicker; 24 | 25 | /** 26 | The object that acts as the data source of the media picker. 27 | 28 | @Discussion 29 | If no object is defined before the picker is show then the picker will use a shared data source that access the user media library. 30 | */ 31 | @property (nonatomic, weak, nullable) id dataSource; 32 | 33 | /** 34 | Pushes a given ViewController into the internal UINavigationController. Useful for post-processing steps. 35 | */ 36 | - (void)showAfterViewController:(nonnull UIViewController *)viewController; 37 | 38 | @property (nonatomic, strong, readonly) UICollectionViewFlowLayout * _Nonnull layout; 39 | 40 | /** 41 | A localized string that reflect the action that will be done when the picker is selected. 42 | This string can contain a a placeholder for a numeric value that will indicate the number of media items selected. 43 | If this is nil the default value will be used. The default value is 'Select %@' 44 | */ 45 | @property (nonatomic, copy, nullable) NSString *selectionActionTitle; 46 | 47 | /** 48 | A localized string that reflect the action that will be done when the user chooses to preview the selected assets. 49 | This string can contain a a placeholder for a numeric value that will indicate the number of media items selected. 50 | If this is nil the default value will be used. The default value is 'Preview %@' 51 | */ 52 | @property (nonatomic, copy, nullable) NSString *previewActionTitle; 53 | 54 | /** 55 | A localized string with the title for the cancel button 56 | If this is nil the default value will be used. The default value is 'Cancel' 57 | */ 58 | @property (nonatomic, copy, nullable) NSString *cancelButtonTitle; 59 | 60 | /** 61 | If this property is set to NO the picker will not show the interface to display groups. The default value is YES. 62 | */ 63 | @property (nonatomic, assign) BOOL showGroupSelector; 64 | 65 | /** 66 | If this property is set the navigation start on the group selector otherwise it start directly on the default active group of the data source. The default value is YES. 67 | */ 68 | @property (nonatomic, assign) BOOL startOnGroupSelector; 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /Example/Tests/WPDateTimeHelpersTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WPDateTimeHelpers : NSObject 4 | 5 | + (NSString *)userFriendlyStringDateFromDate:(NSDate *)date; 6 | 7 | + (NSString *)userFriendlyStringTimeFromDate:(NSDate *)date; 8 | 9 | + (NSString *)stringFromTimeInterval:(NSTimeInterval)timeInterval; 10 | 11 | + (void)setForcedLocaleIdentifier:(NSString *)localeIdentifier; 12 | 13 | @end 14 | 15 | @interface WPDateTimeHelpersTest : XCTestCase 16 | 17 | @end 18 | 19 | @implementation WPDateTimeHelpersTest 20 | 21 | - (void)tearDown { 22 | [WPDateTimeHelpers setForcedLocaleIdentifier:nil]; 23 | } 24 | 25 | - (void)testStringFromTimeInterval 26 | { 27 | NSTimeInterval timeInterval = 120; 28 | NSString * result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 29 | XCTAssertEqualObjects(@"02:00", result); 30 | 31 | timeInterval = 119.4; 32 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 33 | XCTAssertEqualObjects(@"02:00", result); 34 | 35 | timeInterval = 119.5; 36 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 37 | XCTAssertEqualObjects(@"02:00", result); 38 | 39 | timeInterval = 0.1; 40 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 41 | XCTAssertEqualObjects(@"00:01", result); 42 | 43 | timeInterval = 30; 44 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 45 | XCTAssertEqualObjects(@"00:30", result); 46 | 47 | timeInterval = 60; 48 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 49 | XCTAssertEqualObjects(@"01:00", result); 50 | 51 | timeInterval = 65; 52 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 53 | XCTAssertEqualObjects(@"01:05", result); 54 | 55 | timeInterval = 3600; 56 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 57 | XCTAssertEqualObjects(@"01:00:00", result); 58 | 59 | timeInterval = 3605; 60 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 61 | XCTAssertEqualObjects(@"01:00:05", result); 62 | 63 | timeInterval = 3667; 64 | result = [WPDateTimeHelpers stringFromTimeInterval:timeInterval]; 65 | XCTAssertEqualObjects(@"01:01:07", result); 66 | } 67 | 68 | - (void)testUserFriendlyStringDateFromDate { 69 | 70 | XCTAssertThrows([WPDateTimeHelpers userFriendlyStringDateFromDate:nil]); 71 | 72 | [WPDateTimeHelpers setForcedLocaleIdentifier:@"en_us"]; 73 | NSDate *now = [NSDate new]; 74 | XCTAssertEqualObjects([WPDateTimeHelpers userFriendlyStringDateFromDate:now], @"Today"); 75 | 76 | NSCalendar *calendar = [NSCalendar currentCalendar]; 77 | NSDate *yesterday = [calendar dateByAddingUnit:NSCalendarUnitDay value:-1 toDate:now options:0]; 78 | XCTAssertEqualObjects([WPDateTimeHelpers userFriendlyStringDateFromDate:yesterday], @"Yesterday"); 79 | 80 | [WPDateTimeHelpers setForcedLocaleIdentifier:@"pt_pt"]; 81 | XCTAssertEqualObjects([WPDateTimeHelpers userFriendlyStringDateFromDate:now], @"Hoje"); 82 | XCTAssertEqualObjects([WPDateTimeHelpers userFriendlyStringDateFromDate:yesterday], @"Ontem"); 83 | } 84 | 85 | @end 86 | 87 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/SampleCellOverlayView.m: -------------------------------------------------------------------------------- 1 | #import "SampleCellOverlayView.h" 2 | 3 | @interface SampleCellOverlayView () 4 | @property (nonatomic, strong) UIView *labelBackgroundView; 5 | @property (nonatomic, strong) UILabel *label; 6 | @end 7 | 8 | @implementation SampleCellOverlayView 9 | 10 | - (instancetype)init 11 | { 12 | if (self = [super init]) { 13 | [self addAlphaView]; 14 | [self addBackgroundView]; 15 | [self addLabel]; 16 | } 17 | 18 | return self; 19 | } 20 | 21 | - (void)addAlphaView 22 | { 23 | UIView *alphaView = [UIView new]; 24 | alphaView.backgroundColor = [UIColor darkGrayColor]; 25 | alphaView.alpha = 0.5; 26 | alphaView.translatesAutoresizingMaskIntoConstraints = NO; 27 | 28 | [self addSubview:alphaView]; 29 | [NSLayoutConstraint activateConstraints:@[ 30 | [alphaView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor], 31 | [alphaView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor], 32 | [alphaView.topAnchor constraintEqualToAnchor:self.topAnchor], 33 | [alphaView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] 34 | ]]; 35 | 36 | } 37 | 38 | - (void)addBackgroundView 39 | { 40 | UIView *labelBackgroundView = [UIView new]; 41 | labelBackgroundView.backgroundColor = [UIColor darkGrayColor]; 42 | labelBackgroundView.translatesAutoresizingMaskIntoConstraints = NO; 43 | 44 | [self addSubview:labelBackgroundView]; 45 | [NSLayoutConstraint activateConstraints:@[ 46 | [labelBackgroundView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor], 47 | [labelBackgroundView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor], 48 | [labelBackgroundView.topAnchor constraintEqualToAnchor:self.topAnchor], 49 | [labelBackgroundView.heightAnchor constraintEqualToConstant:20.0], 50 | ]]; 51 | 52 | self.labelBackgroundView = labelBackgroundView; 53 | } 54 | 55 | - (void)addLabel 56 | { 57 | UILabel *label = [UILabel new]; 58 | label.textAlignment = NSTextAlignmentCenter; 59 | label.font = [UIFont boldSystemFontOfSize:12.0]; 60 | label.textColor = [UIColor whiteColor]; 61 | label.translatesAutoresizingMaskIntoConstraints = NO; 62 | 63 | [self.labelBackgroundView addSubview:label]; 64 | [NSLayoutConstraint activateConstraints:@[ 65 | [label.centerXAnchor constraintEqualToAnchor:self.labelBackgroundView.centerXAnchor], 66 | [label.centerYAnchor constraintEqualToAnchor:self.labelBackgroundView.centerYAnchor] 67 | ]]; 68 | 69 | self.label = label; 70 | } 71 | 72 | - (void)setLabelText:(NSString *)labelText 73 | { 74 | self.label.text = labelText; 75 | } 76 | 77 | - (NSString *)labelText 78 | { 79 | return self.label.text; 80 | } 81 | 82 | - (void)prepareForReuse 83 | { 84 | self.label.text = nil; 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /Pod/Classes/WPImageExporter.m: -------------------------------------------------------------------------------- 1 | #import "WPImageExporter.h" 2 | 3 | @import MobileCoreServices; 4 | @import ImageIO; 5 | 6 | @implementation WPImageExporter 7 | 8 | + (NSURL *)temporaryFileURLWithExtension:(NSString *)fileExtension 9 | { 10 | NSAssert(fileExtension.length > 0, @"file Extension cannot be empty"); 11 | NSString *fileName = [NSString stringWithFormat:@"%@_file.%@", NSProcessInfo.processInfo.globallyUniqueString, fileExtension]; 12 | NSURL * fileURL = [[NSURL fileURLWithPath: NSTemporaryDirectory()] URLByAppendingPathComponent:fileName]; 13 | return fileURL; 14 | } 15 | 16 | + (BOOL)writeImage:(UIImage *)image withMetadata:(NSDictionary *)metadata toURL:(NSURL *)fileURL; 17 | { 18 | NSMutableDictionary *properties = [[NSMutableDictionary alloc] initWithDictionary:@{ (NSString *)kCGImageDestinationLossyCompressionQuality: @(0.9) }]; 19 | 20 | NSMutableDictionary *adjustedMetadata = [[NSMutableDictionary alloc] initWithDictionary:metadata]; 21 | NSNumber *adjustedOrientation = @([self CGImagePropertyOrientationForUIImageOrientation: image.imageOrientation]); 22 | adjustedMetadata[(NSString *)kCGImagePropertyOrientation] = adjustedOrientation; 23 | 24 | if (adjustedMetadata[(NSString *)kCGImagePropertyTIFFDictionary] != nil) { 25 | NSMutableDictionary *adjustedTIFF = [[NSMutableDictionary alloc] initWithDictionary:adjustedMetadata[(NSString *)kCGImagePropertyTIFFDictionary]]; 26 | adjustedTIFF[(NSString *)kCGImagePropertyTIFFOrientation] = adjustedOrientation; 27 | adjustedMetadata[(NSString *)kCGImagePropertyTIFFDictionary] = adjustedTIFF; 28 | } 29 | 30 | if (adjustedMetadata[(NSString *)kCGImagePropertyIPTCDictionary] != nil) { 31 | NSMutableDictionary *adjustedIPTC = [[NSMutableDictionary alloc] initWithDictionary:adjustedMetadata[(NSString *)kCGImagePropertyIPTCDictionary]]; 32 | adjustedIPTC[(NSString *)kCGImagePropertyIPTCImageOrientation] = adjustedOrientation; 33 | adjustedMetadata[(NSString *)kCGImagePropertyIPTCDictionary] = adjustedIPTC; 34 | } 35 | 36 | CGImageDestinationRef destination = CGImageDestinationCreateWithURL((CFURLRef)fileURL, kUTTypeJPEG, 1, nil); 37 | if (destination == NULL) { 38 | return NO; 39 | } 40 | CGImageRef imageRef = image.CGImage; 41 | 42 | CGImageDestinationSetProperties(destination, (CFDictionaryRef)properties); 43 | CGImageDestinationAddImage(destination, imageRef, (CFDictionaryRef)adjustedMetadata); 44 | 45 | BOOL result = CGImageDestinationFinalize(destination); 46 | 47 | CFRelease(destination); 48 | return result; 49 | } 50 | 51 | + (CGImagePropertyOrientation) CGImagePropertyOrientationForUIImageOrientation:(UIImageOrientation) uiOrientation { 52 | switch (uiOrientation) { 53 | case UIImageOrientationUp: return kCGImagePropertyOrientationUp; 54 | case UIImageOrientationDown: return kCGImagePropertyOrientationDown; 55 | case UIImageOrientationLeft: return kCGImagePropertyOrientationLeft; 56 | case UIImageOrientationRight: return kCGImagePropertyOrientationRight; 57 | case UIImageOrientationUpMirrored: return kCGImagePropertyOrientationUpMirrored; 58 | case UIImageOrientationDownMirrored: return kCGImagePropertyOrientationDownMirrored; 59 | case UIImageOrientationLeftMirrored: return kCGImagePropertyOrientationLeftMirrored; 60 | case UIImageOrientationRightMirrored: return kCGImagePropertyOrientationRightMirrored; 61 | } 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at https://make.wordpress.org/community/contact/. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaGroupTableViewCell.m: -------------------------------------------------------------------------------- 1 | 2 | #import "WPMediaGroupTableViewCell.h" 3 | 4 | static CGFloat const WPMediaGroupTableViewCellImagePadding = 8.0; 5 | static CGFloat const WPMediaGroupTableViewCellImageMargin = 8.0; 6 | static CGFloat const WPMediaGroupTableViewCellLabelMargin = 15.0; 7 | static CGFloat const WPMediaGroupTableViewCellCountLabelMargin = 2.0; 8 | 9 | @implementation WPMediaGroupTableViewCell 10 | 11 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 12 | { 13 | self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:NSStringFromClass([WPMediaGroupTableViewCell class])]; 14 | if (!self) { 15 | return nil; 16 | } 17 | _posterBackgroundColor = UIColor.lightGrayColor; 18 | 19 | _imagePosterView = [[UIImageView alloc] initWithFrame:CGRectZero]; 20 | _imagePosterView.contentMode = UIViewContentModeScaleAspectFill; 21 | _imagePosterView.clipsToBounds = YES; 22 | _imagePosterView.translatesAutoresizingMaskIntoConstraints = NO; 23 | _imagePosterView.backgroundColor = _posterBackgroundColor; 24 | _imagePosterView.accessibilityIgnoresInvertColors = YES; 25 | 26 | [self.contentView addSubview:_imagePosterView]; 27 | 28 | _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 29 | _titleLabel.font = [UIFont systemFontOfSize:[UIFont systemFontSize]]; 30 | _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; 31 | [self.contentView addSubview:_titleLabel]; 32 | 33 | _countLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 34 | _countLabel.font = [UIFont systemFontOfSize:[UIFont smallSystemFontSize]]; 35 | _countLabel.translatesAutoresizingMaskIntoConstraints = NO; 36 | _countLabel.textColor = [UIColor lightGrayColor]; 37 | [self.contentView addSubview:_countLabel]; 38 | 39 | [_imagePosterView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:WPMediaGroupTableViewCellImageMargin].active = YES; 40 | [_imagePosterView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:WPMediaGroupTableViewCellImagePadding].active = YES; 41 | [_imagePosterView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-WPMediaGroupTableViewCellImagePadding].active = YES; 42 | [_imagePosterView.widthAnchor constraintEqualToAnchor:_imagePosterView.heightAnchor].active = YES; 43 | [_titleLabel.leadingAnchor constraintEqualToAnchor:_imagePosterView.trailingAnchor constant:WPMediaGroupTableViewCellLabelMargin].active = YES; 44 | [_titleLabel.trailingAnchor constraintEqualToAnchor:self.contentView.layoutMarginsGuide.trailingAnchor].active = YES; 45 | [_titleLabel.bottomAnchor constraintEqualToAnchor:self.contentView.centerYAnchor].active = YES; 46 | [_countLabel.leadingAnchor constraintEqualToAnchor:_imagePosterView.trailingAnchor constant:WPMediaGroupTableViewCellLabelMargin].active = YES; 47 | [_countLabel.trailingAnchor constraintEqualToAnchor:self.contentView.layoutMarginsGuide.trailingAnchor].active = YES; 48 | [_countLabel.topAnchor constraintEqualToAnchor:self.contentView.centerYAnchor constant:WPMediaGroupTableViewCellCountLabelMargin].active = YES; 49 | 50 | return self; 51 | } 52 | 53 | - (void)setPosterBackgroundColor:(UIColor *)posterBackgroundColor { 54 | _posterBackgroundColor = posterBackgroundColor; 55 | _imagePosterView.backgroundColor = posterBackgroundColor; 56 | } 57 | 58 | - (void)prepareForReuse { 59 | [super prepareForReuse]; 60 | self.imagePosterView.image = nil; 61 | self.titleLabel.text = nil; 62 | self.countLabel.text = nil; 63 | self.groupIdentifier = nil; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "DemoViewController.h" 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 10 | self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[DemoViewController alloc] init]]; 11 | [self.window makeKeyAndVisible]; 12 | 13 | // Customize appearance 14 | 15 | //Configure bottom action bar line color 16 | [[WPActionBar appearance] setLineColor:[UIColor colorWithRed:0.91 green:0.94 blue:0.95 alpha:1.0]]; 17 | 18 | //Configure navigation bar background color 19 | UIColor *wordPressBlue = [UIColor colorWithRed:0/255.0f green:135/255.0f blue:190/255.0f alpha:1.0f]; 20 | [[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[WPMediaPickerViewController class]]] setBarTintColor: wordPressBlue]; 21 | //Configure navigation bar items text color 22 | [[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[WPMediaPickerViewController class]]] setTintColor:[UIColor whiteColor]]; 23 | //Configure navigation bar title text color 24 | [[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[WPMediaPickerViewController class]]] setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor whiteColor]} ]; 25 | //Configure background color for media scroll view 26 | [[UICollectionView appearanceWhenContainedInInstancesOfClasses:@[[WPMediaPickerViewController class]]] setBackgroundColor:[UIColor whiteColor]]; 27 | [[UITableView appearanceWhenContainedInInstancesOfClasses:@[[WPNavigationMediaPickerViewController class]]] setBackgroundColor:[UIColor whiteColor]]; 28 | //Configure background color for media cell while loading image. 29 | UIColor *cellBackgroundColor = [UIColor colorWithRed:243/255.0f green:246/255.0f blue:248/255.0f alpha:1.0f]; 30 | [[WPMediaCollectionViewCell appearanceWhenContainedInInstancesOfClasses:@[[WPMediaPickerViewController class]]] setLoadingBackgroundColor:cellBackgroundColor]; 31 | [[WPMediaCollectionViewCell appearanceWhenContainedInInstancesOfClasses:@[[WPInputMediaPickerViewController class]]] setLoadingBackgroundColor:cellBackgroundColor]; 32 | [[WPMediaGroupTableViewCell appearance] setPosterBackgroundColor:cellBackgroundColor]; 33 | 34 | //Configure placeholder background color for media cell. 35 | UIColor *placeholderCellBackgroundColor = [UIColor lightGrayColor]; 36 | [[WPMediaCollectionViewCell appearanceWhenContainedInInstancesOfClasses:@[[WPMediaPickerViewController class]]] setPlaceholderBackgroundColor:placeholderCellBackgroundColor]; 37 | [[WPMediaCollectionViewCell appearanceWhenContainedInInstancesOfClasses:@[[WPInputMediaPickerViewController class]]] setPlaceholderBackgroundColor:placeholderCellBackgroundColor]; 38 | 39 | //Configure color for activity indicator while loading media collection 40 | [[UIActivityIndicatorView appearanceWhenContainedInInstancesOfClasses:@[[WPMediaPickerViewController class]]] setColor:[UIColor grayColor]]; 41 | 42 | //Configure background color for media cell while loading image. 43 | 44 | UIColor * lightGray = [UIColor colorWithRed:198.0/255.0 green:198.0/255.0 blue:198.0/255.0 alpha:0.7]; 45 | 46 | [[WPMediaCollectionViewCell appearance] setTintColor:wordPressBlue]; 47 | [[WPMediaCollectionViewCell appearance] setPositionLabelUnselectedTintColor:lightGray]; 48 | [[WPMediaCollectionViewCell appearanceWhenContainedInInstancesOfClasses:@[[WPInputMediaPickerViewController class]]] setPositionLabelUnselectedTintColor:lightGray]; 49 | 50 | return YES; 51 | } 52 | 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Pod/Classes/WPDateTimeHelpers.m: -------------------------------------------------------------------------------- 1 | #import "WPDateTimeHelpers.h" 2 | 3 | @implementation WPDateTimeHelpers 4 | 5 | + (NSString *)userFriendlyStringDateFromDate:(NSDate *)date { 6 | NSAssert(date != nil, @"Date cannot be nil"); 7 | NSDate *now = [NSDate date]; 8 | NSString *dateString = [[[self class] sharedDateFormatter] stringFromDate:date]; 9 | 10 | NSCalendar *calendar = [NSCalendar currentCalendar]; 11 | NSDate *oneWeekAgo = [calendar dateByAddingUnit:NSCalendarUnitWeekOfYear value:-1 toDate:now options:0]; 12 | if ((![calendar isDateInToday:date] && ![calendar isDateInYesterday:date]) && [date compare:oneWeekAgo] == NSOrderedDescending) { 13 | dateString = [[[[self class] sharedDateWeekFormatter] stringFromDate:date] capitalizedStringWithLocale:nil]; 14 | } 15 | return dateString; 16 | } 17 | 18 | + (NSString *)userFriendlyStringTimeFromDate:(NSDate *)date { 19 | NSAssert(date != nil, @"Date cannot be nil"); 20 | return [[[self class] sharedTimeFormatter] stringFromDate:date]; 21 | } 22 | 23 | + (NSDateFormatter *)sharedDateFormatter { 24 | static NSDateFormatter * _sharedDateFormatter; 25 | static dispatch_once_t onceToken; 26 | dispatch_once(&onceToken, ^{ 27 | _sharedDateFormatter = [[NSDateFormatter alloc] init]; 28 | _sharedDateFormatter.dateStyle = NSDateFormatterLongStyle; 29 | _sharedDateFormatter.timeStyle = NSDateFormatterNoStyle; 30 | _sharedDateFormatter.doesRelativeDateFormatting = YES; 31 | }); 32 | return _sharedDateFormatter; 33 | } 34 | 35 | + (NSDateFormatter *)sharedTimeFormatter { 36 | static NSDateFormatter * _sharedTimeFormatter; 37 | static dispatch_once_t onceToken; 38 | dispatch_once(&onceToken, ^{ 39 | _sharedTimeFormatter = [[NSDateFormatter alloc] init]; 40 | _sharedTimeFormatter.dateStyle = NSDateFormatterNoStyle; 41 | _sharedTimeFormatter.timeStyle = NSDateFormatterShortStyle; 42 | }); 43 | return _sharedTimeFormatter; 44 | } 45 | 46 | + (NSDateFormatter *)sharedDateWeekFormatter { 47 | static NSDateFormatter * _sharedDateWeekFormatter; 48 | static dispatch_once_t onceToken; 49 | dispatch_once(&onceToken, ^{ 50 | _sharedDateWeekFormatter = [[NSDateFormatter alloc] init]; 51 | _sharedDateWeekFormatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"cccc" options:0 locale:nil]; 52 | }); 53 | return _sharedDateWeekFormatter; 54 | } 55 | 56 | + (NSDateComponentsFormatter *)sharedDateComponentsFormatter { 57 | static NSDateComponentsFormatter *_sharedDateComponentsFormatter; 58 | static dispatch_once_t onceToken; 59 | dispatch_once(&onceToken, ^{ 60 | _sharedDateComponentsFormatter = [NSDateComponentsFormatter new]; 61 | _sharedDateComponentsFormatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad; 62 | _sharedDateComponentsFormatter.allowedUnits = (NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond); 63 | }); 64 | 65 | return _sharedDateComponentsFormatter; 66 | } 67 | 68 | + (NSString *)stringFromTimeInterval:(NSTimeInterval)timeInterval 69 | { 70 | NSTimeInterval interval = ceil(timeInterval); 71 | NSInteger hours = (interval / 3600); 72 | 73 | if (hours > 0) { 74 | [[self class] sharedDateComponentsFormatter].allowedUnits = (NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond); 75 | } else { 76 | [[self class] sharedDateComponentsFormatter].allowedUnits = (NSCalendarUnitMinute | NSCalendarUnitSecond); 77 | } 78 | 79 | return [[[self class] sharedDateComponentsFormatter] stringFromTimeInterval:interval]; 80 | } 81 | 82 | + (void)setForcedLocaleIdentifier:(NSString *)forcedLocaleIdentifier { 83 | if (forcedLocaleIdentifier) { 84 | NSLocale *forcedLocale = [[NSLocale alloc] initWithLocaleIdentifier:forcedLocaleIdentifier]; 85 | self.sharedDateFormatter.locale = forcedLocale; 86 | } else { 87 | self.sharedDateFormatter.locale = NSLocale.currentLocale; 88 | } 89 | } 90 | @end 91 | -------------------------------------------------------------------------------- /Pod/Classes/WPActionBar.m: -------------------------------------------------------------------------------- 1 | #import "WPActionBar.h" 2 | 3 | static const CGFloat SeparatorLineHeight = 2.f; 4 | static const CGFloat BarMinimumHeight = 44.f; 5 | static const UIEdgeInsets ButtonsBarEdgeInsets = {0, 20, 0, 20}; //top, left, bottom, right 6 | 7 | @interface WPActionBar() 8 | @property (nonatomic, strong) UIStackView *stackView; 9 | @property (nonatomic, strong) UIView *lineView; 10 | @end 11 | 12 | @implementation WPActionBar 13 | 14 | - (instancetype)init 15 | { 16 | if (self = [super initWithFrame:CGRectZero]) { 17 | [self setup]; 18 | } 19 | return self; 20 | } 21 | 22 | - (void)setup 23 | { 24 | [self setupView]; 25 | [self setupStackView]; 26 | [self setupConstraints]; 27 | } 28 | 29 | - (void)setupView 30 | { 31 | [self setBackgroundColor:[UIColor whiteColor]]; 32 | [self setAutoresizingMask:UIViewAutoresizingFlexibleHeight]; 33 | [self addSubview:[self lineView]]; 34 | } 35 | 36 | - (void)setupStackView 37 | { 38 | [self.stackView setAxis:UILayoutConstraintAxisHorizontal]; 39 | [self.stackView setAlignment:UIStackViewAlignmentFill]; 40 | [self.stackView setDistribution:UIStackViewDistributionFill]; 41 | [self.stackView setTranslatesAutoresizingMaskIntoConstraints:NO]; 42 | 43 | [self.stackView addArrangedSubview:[self separatorView]]; 44 | [self.stackView setLayoutMargins:ButtonsBarEdgeInsets]; 45 | [self.stackView setLayoutMarginsRelativeArrangement:YES]; 46 | 47 | [self addSubview:self.stackView]; 48 | } 49 | 50 | - (UIView *)separatorView 51 | { 52 | UIView *view = [UIView new]; 53 | [view setTranslatesAutoresizingMaskIntoConstraints:NO]; 54 | [[view.heightAnchor constraintEqualToConstant:BarMinimumHeight] setActive:YES]; 55 | return view; 56 | } 57 | 58 | - (UIView *)lineView 59 | { 60 | if (_lineView) { 61 | return _lineView; 62 | } 63 | _lineView = [UIView new]; 64 | [_lineView setTranslatesAutoresizingMaskIntoConstraints:NO]; 65 | [[_lineView.heightAnchor constraintEqualToConstant:SeparatorLineHeight] setActive:YES]; 66 | [_lineView setBackgroundColor:[UIColor whiteColor]]; 67 | return _lineView; 68 | } 69 | 70 | - (CGSize)intrinsicContentSize 71 | { 72 | // Necessary to make the iPhone X bottom inset work. 73 | return CGSizeZero; 74 | } 75 | 76 | - (UIStackView *)stackView 77 | { 78 | if (!_stackView) { 79 | _stackView = [UIStackView new]; 80 | } 81 | return _stackView; 82 | } 83 | 84 | #pragma mark - public methods 85 | 86 | - (UIColor *)barBackgroundColor 87 | { 88 | return self.backgroundColor; 89 | } 90 | 91 | - (void)setBarBackgroundColor:(UIColor *)barBackgroundColor 92 | { 93 | self.backgroundColor = barBackgroundColor; 94 | } 95 | 96 | - (UIColor *)lineColor 97 | { 98 | return self.lineView.backgroundColor; 99 | } 100 | 101 | - (void)setLineColor:(UIColor *)lineColor 102 | { 103 | [self.lineView setBackgroundColor:lineColor]; 104 | } 105 | 106 | - (void)addLeftButton:(UIButton *)button 107 | { 108 | [self.stackView insertArrangedSubview:button atIndex:0]; 109 | } 110 | 111 | - (void)addRightButton:(UIButton *)button 112 | { 113 | [self.stackView addArrangedSubview:button]; 114 | } 115 | 116 | #pragma mark - Layout 117 | 118 | - (void)setupConstraints 119 | { 120 | [NSLayoutConstraint activateConstraints: 121 | @[ 122 | [self.lineView.topAnchor constraintEqualToAnchor:self.topAnchor], 123 | [self.lineView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor], 124 | [self.lineView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] 125 | ]]; 126 | 127 | [NSLayoutConstraint activateConstraints: 128 | @[ 129 | [self.stackView.topAnchor constraintEqualToAnchor:self.lineView.bottomAnchor], 130 | [self.stackView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor], 131 | [self.stackView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor], 132 | [self.stackView.bottomAnchor constraintEqualToAnchor:self.layoutMarginsGuide.bottomAnchor] 133 | ]]; 134 | } 135 | 136 | @end 137 | -------------------------------------------------------------------------------- /Pod/Classes/WPBadgeView.m: -------------------------------------------------------------------------------- 1 | 2 | #import "WPBadgeView.h" 3 | 4 | static const CGFloat kDefaultCornerRadius = 6.f; 5 | static const UIEdgeInsets kDefaultEdgeInsets = {3.f, 6.f, 3.f, 6.f}; 6 | 7 | @interface WPBadgeView() 8 | @property (nonatomic, strong) NSLayoutConstraint *topConstraint; 9 | @property (nonatomic, strong) NSLayoutConstraint *bottomConstraint; 10 | @property (nonatomic, strong) NSLayoutConstraint* leadingConstraint; 11 | @property (nonatomic, strong) NSLayoutConstraint* trailingConstraint; 12 | 13 | @property (nonatomic, strong) UIView *contentView; 14 | @property (nonatomic, strong) UIVisualEffectView *blurEffectView; 15 | @end 16 | 17 | @implementation WPBadgeView 18 | 19 | - (instancetype)initWithFrame:(CGRect)frame 20 | { 21 | self = [super initWithFrame:frame]; 22 | if (self) { 23 | [self commonInit]; 24 | } 25 | return self; 26 | } 27 | 28 | - (instancetype)init 29 | { 30 | self = [super initWithFrame:(CGRectZero)]; 31 | if (self) { 32 | [self commonInit]; 33 | } 34 | return self; 35 | } 36 | 37 | - (void)commonInit 38 | { 39 | [self setupBlur]; 40 | [self layoutLabel]; 41 | [self setupStyle]; 42 | } 43 | 44 | - (void)layoutLabel 45 | { 46 | self.label.translatesAutoresizingMaskIntoConstraints = NO; 47 | [self.contentView addSubview:self.label]; 48 | 49 | self.topConstraint = [self.label.topAnchor constraintEqualToAnchor:self.topAnchor]; 50 | self.bottomConstraint = [self.label.bottomAnchor constraintEqualToAnchor:self.bottomAnchor]; 51 | self.leadingConstraint = [self.label.leadingAnchor constraintEqualToAnchor:self.leadingAnchor]; 52 | self.trailingConstraint = [self.label.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]; 53 | 54 | [NSLayoutConstraint activateConstraints: @[ 55 | self.topConstraint, 56 | self.bottomConstraint, 57 | self.leadingConstraint, 58 | self.trailingConstraint 59 | ]]; 60 | } 61 | 62 | #pragma mark - Getters / setters 63 | 64 | - (UILabel *)label 65 | { 66 | if (_label == nil) { 67 | _label = [UILabel new]; 68 | } 69 | return _label; 70 | } 71 | 72 | - (void)setInsets:(UIEdgeInsets)insets 73 | { 74 | _insets = insets; 75 | self.topConstraint.constant = insets.top; 76 | self.bottomConstraint.constant = -insets.bottom; 77 | self.leadingConstraint.constant = insets.left; 78 | self.trailingConstraint.constant = -insets.right; 79 | [self setNeedsLayout]; 80 | } 81 | 82 | - (void)setCornerRadius:(CGFloat)cornerRadius 83 | { 84 | _cornerRadius = cornerRadius; 85 | self.blurEffectView.layer.cornerRadius = cornerRadius; 86 | self.blurEffectView.layer.masksToBounds = YES; 87 | } 88 | 89 | #pragma mark - Helpers 90 | 91 | - (void)setupStyle 92 | { 93 | self.label.font = [UIFont systemFontOfSize:14.f weight:UIFontWeightSemibold]; 94 | self.label.textColor = UIColor.whiteColor; 95 | self.insets = kDefaultEdgeInsets; 96 | self.cornerRadius = kDefaultCornerRadius; 97 | } 98 | 99 | - (void)setupBlur 100 | { 101 | self.backgroundColor = [UIColor clearColor]; 102 | 103 | UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 104 | UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; 105 | 106 | _contentView = blurEffectView.contentView; 107 | _blurEffectView = blurEffectView; 108 | 109 | [self addSubview:blurEffectView]; 110 | [self constraintEffectView:blurEffectView]; 111 | } 112 | 113 | - (void)constraintEffectView:(UIView *)view 114 | { 115 | view.translatesAutoresizingMaskIntoConstraints = NO; 116 | [NSLayoutConstraint activateConstraints:@[ 117 | [view.heightAnchor constraintEqualToAnchor:self.heightAnchor], 118 | [view.widthAnchor constraintEqualToAnchor:self.widthAnchor], 119 | [view.centerXAnchor constraintEqualToAnchor:self.centerXAnchor], 120 | [view.centerYAnchor constraintEqualToAnchor:self.centerYAnchor] 121 | ]]; 122 | } 123 | 124 | @end 125 | -------------------------------------------------------------------------------- /Example/WPMediaPicker.xcodeproj/xcshareddata/xcschemes/WPMediaPicker-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 70 | 76 | 77 | 78 | 79 | 85 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "size" : "29x29", 15 | "idiom" : "iphone", 16 | "filename" : "Icon-App-29x29@1x.png", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "size" : "29x29", 21 | "idiom" : "iphone", 22 | "filename" : "Icon-App-29x29@2x.png", 23 | "scale" : "2x" 24 | }, 25 | { 26 | "size" : "29x29", 27 | "idiom" : "iphone", 28 | "filename" : "Icon-App-29x29@3x.png", 29 | "scale" : "3x" 30 | }, 31 | { 32 | "size" : "40x40", 33 | "idiom" : "iphone", 34 | "filename" : "Icon-App-40x40@2x.png", 35 | "scale" : "2x" 36 | }, 37 | { 38 | "size" : "40x40", 39 | "idiom" : "iphone", 40 | "filename" : "Icon-App-40x40@3x.png", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "iphone", 45 | "size" : "57x57", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "iphone", 50 | "size" : "57x57", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "size" : "60x60", 55 | "idiom" : "iphone", 56 | "filename" : "Icon-App-60x60@2x.png", 57 | "scale" : "2x" 58 | }, 59 | { 60 | "size" : "60x60", 61 | "idiom" : "iphone", 62 | "filename" : "Icon-App-60x60@3x.png", 63 | "scale" : "3x" 64 | }, 65 | { 66 | "idiom" : "ipad", 67 | "size" : "20x20", 68 | "scale" : "1x" 69 | }, 70 | { 71 | "idiom" : "ipad", 72 | "size" : "20x20", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "idiom" : "ipad", 77 | "size" : "29x29", 78 | "scale" : "1x" 79 | }, 80 | { 81 | "idiom" : "ipad", 82 | "size" : "29x29", 83 | "scale" : "2x" 84 | }, 85 | { 86 | "idiom" : "ipad", 87 | "size" : "40x40", 88 | "scale" : "1x" 89 | }, 90 | { 91 | "idiom" : "ipad", 92 | "size" : "40x40", 93 | "scale" : "2x" 94 | }, 95 | { 96 | "idiom" : "ipad", 97 | "size" : "50x50", 98 | "scale" : "1x" 99 | }, 100 | { 101 | "idiom" : "ipad", 102 | "size" : "50x50", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "idiom" : "ipad", 107 | "size" : "72x72", 108 | "scale" : "1x" 109 | }, 110 | { 111 | "idiom" : "ipad", 112 | "size" : "72x72", 113 | "scale" : "2x" 114 | }, 115 | { 116 | "size" : "76x76", 117 | "idiom" : "ipad", 118 | "filename" : "Icon-App-76x76@1x.png", 119 | "scale" : "1x" 120 | }, 121 | { 122 | "size" : "76x76", 123 | "idiom" : "ipad", 124 | "filename" : "Icon-App-76x76@2x.png", 125 | "scale" : "2x" 126 | }, 127 | { 128 | "size" : "83.5x83.5", 129 | "idiom" : "ipad", 130 | "filename" : "Icon-App-83.5x83.5@2x.png", 131 | "scale" : "2x" 132 | }, 133 | { 134 | "size" : "1024x1024", 135 | "idiom" : "ios-marketing", 136 | "filename" : "Icon-App-1024x1024-1.png", 137 | "scale" : "1x" 138 | }, 139 | { 140 | "size" : "24x24", 141 | "idiom" : "watch", 142 | "scale" : "2x", 143 | "role" : "notificationCenter", 144 | "subtype" : "38mm" 145 | }, 146 | { 147 | "size" : "27.5x27.5", 148 | "idiom" : "watch", 149 | "scale" : "2x", 150 | "role" : "notificationCenter", 151 | "subtype" : "42mm" 152 | }, 153 | { 154 | "size" : "29x29", 155 | "idiom" : "watch", 156 | "role" : "companionSettings", 157 | "scale" : "2x" 158 | }, 159 | { 160 | "size" : "29x29", 161 | "idiom" : "watch", 162 | "role" : "companionSettings", 163 | "scale" : "3x" 164 | }, 165 | { 166 | "size" : "40x40", 167 | "idiom" : "watch", 168 | "scale" : "2x", 169 | "role" : "appLauncher", 170 | "subtype" : "38mm" 171 | }, 172 | { 173 | "size" : "44x44", 174 | "idiom" : "watch", 175 | "scale" : "2x", 176 | "role" : "appLauncher", 177 | "subtype" : "40mm" 178 | }, 179 | { 180 | "size" : "50x50", 181 | "idiom" : "watch", 182 | "scale" : "2x", 183 | "role" : "appLauncher", 184 | "subtype" : "44mm" 185 | }, 186 | { 187 | "size" : "86x86", 188 | "idiom" : "watch", 189 | "scale" : "2x", 190 | "role" : "quickLook", 191 | "subtype" : "38mm" 192 | }, 193 | { 194 | "size" : "98x98", 195 | "idiom" : "watch", 196 | "scale" : "2x", 197 | "role" : "quickLook", 198 | "subtype" : "42mm" 199 | }, 200 | { 201 | "size" : "108x108", 202 | "idiom" : "watch", 203 | "scale" : "2x", 204 | "role" : "quickLook", 205 | "subtype" : "44mm" 206 | }, 207 | { 208 | "size" : "1024x1024", 209 | "idiom" : "watch-marketing", 210 | "filename" : "Icon-App-1024x1024.png", 211 | "scale" : "1x" 212 | } 213 | ], 214 | "info" : { 215 | "version" : 1, 216 | "author" : "xcode" 217 | } 218 | } -------------------------------------------------------------------------------- /Example/WPMediaPicker/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 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 | -------------------------------------------------------------------------------- /Pod/Classes/WPInputMediaPickerViewController.m: -------------------------------------------------------------------------------- 1 | #import "WPInputMediaPickerViewController.h" 2 | #import "WPPHAssetDataSource.h" 3 | 4 | @interface WPInputMediaPickerViewController() 5 | 6 | @property (nonatomic, strong) WPMediaPickerViewController *mediaPicker; 7 | @property (nonatomic, strong) UIToolbar *mediaToolbar; 8 | @property (nonatomic, strong) id privateDataSource; 9 | 10 | @end 11 | 12 | @implementation WPInputMediaPickerViewController 13 | 14 | - (instancetype _Nonnull )initWithOptions:(WPMediaPickerOptions *_Nonnull)options { 15 | self = [super initWithNibName:nil bundle:nil]; 16 | if (self) { 17 | _mediaPicker = [[WPMediaPickerViewController alloc] initWithOptions:[options copy]]; 18 | } 19 | return self; 20 | } 21 | 22 | - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 23 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 24 | if (self) { 25 | _mediaPicker = [[WPMediaPickerViewController alloc] initWithOptions:[WPMediaPickerOptions new]]; 26 | } 27 | return self; 28 | } 29 | 30 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 31 | self = [super initWithCoder:aDecoder]; 32 | if (self) { 33 | _mediaPicker = [[WPMediaPickerViewController alloc] initWithOptions:[WPMediaPickerOptions new]]; 34 | } 35 | return self; 36 | } 37 | 38 | - (void)viewDidLoad 39 | { 40 | [super viewDidLoad]; 41 | [self setupMediaPickerViewController]; 42 | } 43 | 44 | - (void)setupMediaPickerViewController { 45 | self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 46 | 47 | self.privateDataSource = [[WPPHAssetDataSource alloc] init]; 48 | self.mediaPicker.dataSource = self.privateDataSource; 49 | 50 | [self addChildViewController:self.mediaPicker]; 51 | [self overridePickerTraits]; 52 | 53 | self.mediaPicker.view.frame = self.view.bounds; 54 | self.mediaPicker.view.translatesAutoresizingMaskIntoConstraints = NO; 55 | [self.view addSubview:self.mediaPicker.view]; 56 | 57 | NSLayoutAnchor *leadingAnchor = self.view.safeAreaLayoutGuide.leadingAnchor; 58 | NSLayoutAnchor *trailingAnchor = self.view.safeAreaLayoutGuide.trailingAnchor; 59 | 60 | [NSLayoutConstraint activateConstraints: 61 | @[ 62 | [self.mediaPicker.view.leadingAnchor constraintEqualToAnchor:leadingAnchor], 63 | [self.mediaPicker.view.trailingAnchor constraintEqualToAnchor:trailingAnchor], 64 | [self.mediaPicker.view.topAnchor constraintEqualToAnchor:self.view.topAnchor], 65 | [self.mediaPicker.view.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], 66 | ] 67 | ]; 68 | 69 | [self.mediaPicker didMoveToParentViewController:self]; 70 | self.view.backgroundColor = [UIColor whiteColor]; 71 | self.mediaToolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)]; 72 | self.mediaToolbar.items = @[ 73 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(mediaCanceled:)], 74 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], 75 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(mediaSelected:)] 76 | ]; 77 | } 78 | 79 | - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection 80 | { 81 | [super traitCollectionDidChange:previousTraitCollection]; 82 | [self overridePickerTraits]; 83 | } 84 | 85 | - (void)overridePickerTraits 86 | { 87 | // Due to an inputView being displayed in its own window, the force touch peek transition 88 | // doesn't display correctly. Because of this, we'll disable it for the input picker thus forcing 89 | // long touch to be used instead. 90 | UITraitCollection *traits = [UITraitCollection traitCollectionWithForceTouchCapability:UIForceTouchCapabilityUnavailable]; 91 | [self setOverrideTraitCollection:[UITraitCollection traitCollectionWithTraitsFromCollections:@[self.traitCollection, traits]] forChildViewController:self.mediaPicker]; 92 | } 93 | 94 | #pragma mark - WPMediaCollectionDataSource 95 | 96 | - (void)setDataSource:(id)dataSource { 97 | self.mediaPicker.dataSource = dataSource; 98 | } 99 | 100 | - (id)dataSource { 101 | return self.mediaPicker.dataSource; 102 | } 103 | 104 | #pragma mark - WPMediaPickerViewControllerDelegate 105 | 106 | - (void)setMediaPickerDelegate:(id)mediaPickerDelegate { 107 | self.mediaPicker.mediaPickerDelegate = mediaPickerDelegate; 108 | } 109 | 110 | - (id)mediaPickerDelegate { 111 | return self.mediaPicker.mediaPickerDelegate; 112 | } 113 | 114 | - (void)mediaSelected:(UIBarButtonItem *)sender { 115 | if ([self.mediaPickerDelegate respondsToSelector:@selector(mediaPickerController:didFinishPickingAssets:)]) { 116 | [self.mediaPickerDelegate mediaPickerController:self.mediaPicker didFinishPickingAssets:self.mediaPicker.selectedAssets]; 117 | [self.mediaPicker resetState:NO]; 118 | } 119 | 120 | } 121 | 122 | - (void)mediaCanceled:(UIBarButtonItem *)sender { 123 | if ([self.mediaPickerDelegate respondsToSelector:@selector(mediaPickerControllerDidCancel:)]) { 124 | [self.mediaPickerDelegate mediaPickerControllerDidCancel:self.mediaPicker]; 125 | [self.mediaPicker resetState:NO]; 126 | } 127 | } 128 | 129 | - (void)showCapture 130 | { 131 | [self.mediaPicker showCapture]; 132 | } 133 | 134 | @end 135 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaCapturePresenter.m: -------------------------------------------------------------------------------- 1 | #import "WPMediaCapturePresenter.h" 2 | #import "WPMediaCollectionDataSource.h" 3 | 4 | @import MobileCoreServices; 5 | @import AVFoundation; 6 | 7 | @interface WPMediaCapturePresenter () 8 | @property (nonatomic, strong, nullable) UIViewController *presentingViewController; 9 | @end 10 | 11 | @implementation WPMediaCapturePresenter 12 | 13 | + (BOOL)isCaptureAvailable 14 | { 15 | return [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]; 16 | } 17 | 18 | - (instancetype)initWithPresentingViewController:(UIViewController *)viewController 19 | { 20 | if (self = [super init]) { 21 | _presentingViewController = viewController; 22 | } 23 | 24 | return self; 25 | } 26 | 27 | - (void)presentCapture 28 | { 29 | NSString *avMediaType = AVMediaTypeVideo; 30 | AVAuthorizationStatus authorizationStatus = [AVCaptureDevice authorizationStatusForMediaType:avMediaType]; 31 | if (authorizationStatus == AVAuthorizationStatusAuthorized) { 32 | [self presentCaptureViewController]; 33 | return; 34 | } 35 | 36 | if (authorizationStatus == AVAuthorizationStatusNotDetermined) { 37 | [AVCaptureDevice requestAccessForMediaType:avMediaType completionHandler:^(BOOL granted) { 38 | dispatch_async(dispatch_get_main_queue(), ^{ 39 | if (!granted) 40 | { 41 | [self presentPermissionAlert]; 42 | return; 43 | } 44 | [self presentCaptureViewController]; 45 | }); 46 | }]; 47 | return; 48 | } 49 | 50 | dispatch_async(dispatch_get_main_queue(), ^{ 51 | [self presentPermissionAlert]; 52 | }); 53 | } 54 | 55 | - (void)presentPermissionAlert 56 | { 57 | NSString *title = NSLocalizedString(@"Media Capture", @"Title for alert when access to media capture is not granted"); 58 | NSString *message =NSLocalizedString(@"This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this.", @""); 59 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; 60 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", "Confirmation of action") style:UIAlertActionStyleCancel handler:nil]; 61 | [alertController addAction:okAction]; 62 | 63 | NSString *otherButtonTitle = NSLocalizedString(@"Open Settings", @"Go to the settings app"); 64 | UIAlertAction *otherAction = [UIAlertAction actionWithTitle:otherButtonTitle 65 | style:UIAlertActionStyleDefault 66 | handler:^(UIAlertAction *action) { 67 | NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; 68 | [[UIApplication sharedApplication] openURL:settingsURL options:@{} completionHandler:nil]; 69 | }]; 70 | [alertController addAction:otherAction]; 71 | 72 | [self.presentingViewController presentViewController:alertController animated:YES completion:nil]; 73 | } 74 | 75 | - (void)presentCaptureViewController 76 | { 77 | UIImagePickerController *imagePickerController = [[UIImagePickerController alloc] init]; 78 | NSMutableSet *mediaTypes = [NSMutableSet setWithArray:[UIImagePickerController availableMediaTypesForSourceType: 79 | UIImagePickerControllerSourceTypeCamera]]; 80 | NSMutableSet *mediaDesired = [NSMutableSet new]; 81 | if (self.mediaType & WPMediaTypeImage) { 82 | [mediaDesired addObject:(__bridge NSString *)kUTTypeImage]; 83 | } 84 | if (self.mediaType & WPMediaTypeVideo) { 85 | [mediaDesired addObject:(__bridge NSString *)kUTTypeMovie]; 86 | 87 | } 88 | if (mediaDesired.count > 0){ 89 | [mediaTypes intersectSet:mediaDesired]; 90 | } 91 | 92 | imagePickerController.mediaTypes = [mediaTypes allObjects]; 93 | imagePickerController.delegate = self; 94 | imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera; 95 | imagePickerController.cameraDevice = [self cameraDevice]; 96 | imagePickerController.modalTransitionStyle = UIModalTransitionStyleCoverVertical; 97 | imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; 98 | [self.presentingViewController presentViewController:imagePickerController animated:YES completion:nil]; 99 | } 100 | 101 | - (UIImagePickerControllerCameraDevice)cameraDevice 102 | { 103 | if (self.preferFrontCamera && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) { 104 | return UIImagePickerControllerCameraDeviceFront; 105 | } else { 106 | return UIImagePickerControllerCameraDeviceRear; 107 | } 108 | } 109 | 110 | #pragma mark - UIImagePickerControllerDelegate 111 | 112 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info 113 | { 114 | [picker dismissViewControllerAnimated:YES completion:^{ 115 | if (self.completionBlock) { 116 | self.completionBlock(info); 117 | } 118 | }]; 119 | } 120 | 121 | - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker 122 | { 123 | [picker dismissViewControllerAnimated:YES completion:^{ 124 | if (self.completionBlock) { 125 | self.completionBlock(nil); 126 | } 127 | }]; 128 | } 129 | 130 | @end 131 | -------------------------------------------------------------------------------- /Pod/Classes/WPCarouselAssetsViewController.m: -------------------------------------------------------------------------------- 1 | #import "WPCarouselAssetsViewController.h" 2 | 3 | @interface WPCarouselAssetsViewController () 4 | @property (nonatomic, strong) NSArray> *assets; 5 | @property (assign, nonatomic) NSInteger index; 6 | @property (assign, nonatomic) NSInteger nextIndex; 7 | @end 8 | 9 | @implementation WPCarouselAssetsViewController 10 | 11 | - (instancetype)initWithAssets:(NSArray> *)assets 12 | { 13 | NSDictionary *options = @{UIPageViewControllerOptionInterPageSpacingKey : @20}; 14 | self = [super initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll 15 | navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal 16 | options:options]; 17 | if (self) { 18 | _assets = assets; 19 | } 20 | 21 | return self; 22 | } 23 | 24 | - (void)viewDidLoad 25 | { 26 | [super viewDidLoad]; 27 | [self initialSetup]; 28 | [self updateTitle]; 29 | } 30 | 31 | - (void)setPreviewingAssetAtIndex:(NSInteger)index animated:(BOOL)animated 32 | { 33 | self.index = index; 34 | if (self.isViewLoaded) { 35 | UIViewController *newViewController = [self viewControllerAtIndex:index]; 36 | [self setViewController:newViewController animated:animated]; 37 | } 38 | } 39 | 40 | #pragma mark - UIPageViewControllerDelegate 41 | 42 | - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController 43 | { 44 | NSInteger index = [self indexForViewController:viewController]; 45 | if (index == 0) { 46 | return nil; 47 | } 48 | return [self viewControllerAtIndex:index - 1]; 49 | } 50 | 51 | - (nullable UIViewController *)pageViewController:(nonnull UIPageViewController *)pageViewController viewControllerAfterViewController:(nonnull UIViewController *)viewController 52 | { 53 | NSInteger index = [self indexForViewController:viewController]; 54 | if (index == self.assets.count - 1) { 55 | return nil; 56 | } 57 | return [self viewControllerAtIndex:index + 1]; 58 | } 59 | 60 | #pragma mark - UIPageViewControllerDelegate 61 | 62 | - (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed 63 | { 64 | if (completed) { 65 | self.index = self.nextIndex; 66 | [self updateTitle]; 67 | } 68 | } 69 | 70 | - (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers 71 | { 72 | UIViewController *nextViewController = pendingViewControllers.firstObject; 73 | self.nextIndex = [self indexForViewController:nextViewController]; 74 | } 75 | 76 | #pragma mark - WPAssetViewControllerDelegate 77 | 78 | - (void)assetViewController:(nonnull WPAssetViewController *)assetPreviewVC failedWithError:(nonnull NSError *)error { 79 | if (self.assetViewDelegate) { 80 | [self.assetViewDelegate assetViewController:assetPreviewVC failedWithError:error]; 81 | } 82 | } 83 | 84 | - (void)assetViewController:(nonnull WPAssetViewController *)assetPreviewVC selectionChanged:(BOOL)selected { 85 | if (self.assetViewDelegate) { 86 | [self.assetViewDelegate assetViewController:assetPreviewVC selectionChanged:selected]; 87 | } 88 | } 89 | 90 | #pragma mark - Helpers 91 | 92 | - (void)initialSetup 93 | { 94 | self.view.backgroundColor = [UIColor blackColor]; 95 | self.dataSource = self; 96 | self.delegate = self; 97 | UIViewController *initialVC = [self viewControllerAtIndex:self.index]; 98 | [self setViewController:initialVC animated:NO]; 99 | } 100 | 101 | - (void)updateTitle 102 | { 103 | NSString *separator = NSLocalizedString(@"of", @"Word separating the current index from the total amount. I.e.: 7 of 9"); 104 | long showingCount = self.index + 1; 105 | self.title = [NSString stringWithFormat:@"%ld %@ %ld", showingCount, separator, (long)self.assets.count]; 106 | } 107 | 108 | - (NSInteger)indexForViewController:(UIViewController *)viewController 109 | { 110 | id asset; 111 | if ([viewController isKindOfClass:[WPAssetViewController class]]) { 112 | WPAssetViewController *assetController = (WPAssetViewController *)viewController; 113 | asset = assetController.asset; 114 | } else if ([self.carouselDelegate respondsToSelector:@selector(carouselController:assetForViewController:)]) { 115 | asset = [self.carouselDelegate carouselController:self assetForViewController:viewController]; 116 | } else { 117 | NSAssert(NO, @"No asset found"); 118 | } 119 | 120 | return [self.assets indexOfObject:asset]; 121 | } 122 | 123 | - (UIViewController *)viewControllerAtIndex:(NSInteger)index 124 | { 125 | id asset = [self.assets objectAtIndex:index]; 126 | 127 | if ([self.carouselDelegate respondsToSelector:@selector(carouselController:viewControllerForAsset:)]) { 128 | UIViewController *viewController = [self.carouselDelegate carouselController:self viewControllerForAsset:asset]; 129 | if (viewController) { 130 | return viewController; 131 | } 132 | } 133 | 134 | WPAssetViewController *fullScreenImageVC = [[WPAssetViewController alloc] init]; 135 | fullScreenImageVC.asset = asset; 136 | fullScreenImageVC.delegate = self; 137 | return fullScreenImageVC; 138 | } 139 | 140 | - (void)setViewController:(UIViewController *)viewController animated:(BOOL)animated 141 | { 142 | NSArray *viewControllers = [NSArray arrayWithObject:viewController]; 143 | [self setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:animated completion:nil]; 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WPMediaPicker 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/WPMediaPicker.svg?style=flat)](http://cocoadocs.org/docsets/WPMediaPicker) 4 | [![License](https://img.shields.io/cocoapods/l/WPMediaPicker.svg?style=flat)](http://cocoadocs.org/docsets/WPMediaPicker) 5 | [![Platform](https://img.shields.io/cocoapods/p/WPMediaPicker.svg?style=flat)](http://cocoadocs.org/docsets/WPMediaPicker) 6 | 7 | ⚠️ **The WPMediaFramework was decommissioned on Nov 27, 2023, and is no longer maintained.** 8 | 9 | WPMediaPicker is an iOS controller that allows capture and picking of media assets. 10 | It allows: 11 | * Allows selection of multiple media objects in one go. 12 | * Capture of new media while inside the picker. 13 | * Use different data sources for the media library. 14 | * Switch between different albums. 15 | * Filtering by media types. 16 | * Preview of media (images and video) in full screen. 17 | * Show the media picker inside as a keyboard input view. 18 | * Super quick and memory optimized. 19 | * Allows horizontal and vertical scroll of assets. 20 | * Allows custom searching/filtering of assets. 21 | 22 | ![Screenshot](screenshots_1.jpg "Screenshot") 23 | 24 | ## Installation 25 | 26 | WPMediaPicker is available through [CocoaPods](http://cocoapods.org). To install 27 | it, simply add the following line to your Podfile: 28 | ``` 29 | pod "WPMediaPicker" 30 | ``` 31 | ## Usage 32 | 33 | To use the picker do the following: 34 | 35 | ### Import header 36 | 37 | ```` objective-c 38 | #import 39 | ```` 40 | 41 | ### Create and present the picker in modal mode 42 | 43 | ```` objective-c 44 | WPNavigationMediaPickerViewController * mediaPicker = [[WPNavigationMediaPickerViewController alloc] init]; 45 | mediaPicker.delegate = self; 46 | [self presentViewController:mediaPicker animated:YES completion:nil]; 47 | ```` 48 | 49 | ### Implement didFinishPickingAssets delegate 50 | 51 | The delegate is responsible for dismissing the picker when the operation completes. To dismiss the picker, call the [dismissViewControllerAnimated:completion:](https://developer.apple.com/library/ios/documentation/uikit/reference/UIViewController_Class/index.html#//apple_ref/occ/instm/UIViewController/dismissViewControllerAnimated:completion:) method of the presenting controller responsible for displaying the `WPNavigationMediaPickerController` object. Please refer to the demo app. 52 | 53 | ```` objective-c 54 | - (void)mediaPickerController:(WPMediaPickerViewController *)picker didFinishPickingAssets:(NSArray *)assets 55 | { 56 | [self dismissViewControllerAnimated:YES completion:nil]; 57 | } 58 | ```` 59 | 60 | ### Other methods to display the picker 61 | 62 | The example above shows the recommended way to show the picker in a modal. There are currently three available controllers to show the picker depending on your application needs: 63 | 64 | * [WPMediaPickerViewController](Pod/Classes/WPMediaPickerViewController.h), this is the base collection view controller that displays the media. It can be used inside other view controllers using containment. 65 | * [WPInputMediaPickerViewController](Pod/Classes/WPInputMediaPickerViewController.h), a wrapper of the WPMediaPickerController to be used as an inputView of an UIControl. 66 | * [WPNavigationMediaPickerViewController](Pod/Classes/WPNavigationMediaPickerViewController.h), a convenience wrapper of the `WPMediaPickerViewController` inside a UINavigationController to show in a modal context. 67 | 68 | ### How to configure the appearance of the picker 69 | 70 | Just use the standard appearance methods from UIKit. Here is an example how to configure the main components 71 | 72 | ```` objective-c 73 | //Configure navigation bar background color 74 | [[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[WPNavigationMediaPickerViewController class]]] setBarTintColor:[UIColor colorWithRed:0/255.0f green:135/255.0f blue:190/255.0f alpha:1.0f]]; 75 | //Configure navigation bar items text color 76 | [[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[WPNavigationMediaPickerViewController class]]] setTintColor:[UIColor whiteColor]]; 77 | //Configure navigation bar title text color 78 | [[UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[[WPNavigationMediaPickerViewController class]]] setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor whiteColor]} ]; 79 | //Configure background color for media scroll view 80 | [[UICollectionView appearanceWhenContainedInInstancesOfClasses:@[[WPMediaCollectionViewController class]]] setBackgroundColor:[UIColor colorWithRed:233/255.0f green:239/255.0f blue:243/255.0f alpha:1.0f]]; 81 | //Configure background color for media cell while loading image. 82 | [[WPMediaCollectionViewCell appearanceWhenContainedInInstancesOfClasses:@[[WPMediaCollectionViewController class]]] setBackgroundColor:[UIColor colorWithRed:243/255.0f green:246/255.0f blue:248/255.0f alpha:1.0f]]; 83 | //Configure color for activity indicator while loading media collection 84 | [[UIActivityIndicatorView appearanceWhenContainedInInstancesOfClasses:@[[WPMediaCollectionViewController class]]] setColor:[UIColor grayColor]]; 85 | ```` 86 | 87 | ### How to use a custom data source for the picker 88 | 89 | If you have a custom database of media and you want to display it using the WPMediaPicker you need to implement the following protocols around your data: 90 | 91 | * [WPMediaCollectionDataSource](Pod/Classes/WPMediaCollectionDataSource.h) 92 | * [WPMediaGroup](Pod/Classes/WPMediaCollectionDataSource.h) 93 | * [WPMediaAsset](Pod/Classes/WPMediaCollectionDataSource.h) 94 | 95 | You can view the protocols documentation for more implementation details. 96 | After you have implemented it you can use it by simple doing the following: 97 | 98 | ```` objective-c 99 | self.customDataSource = [[WPCustomAssetDataSource alloc] init]; 100 | mediaPicker.dataSource = self.customDataSource; 101 | ```` 102 | 103 | ### Sample Project 104 | 105 | To run the example project, clone the repo, and run `pod install` from the `Example` directory first. 106 | 107 | ## Requirements 108 | 109 | * ARC 110 | * Photos, AVFoundation, ImageIO 111 | * XCode 10 or above 112 | * iOS 11 or above 113 | 114 | ## Contributing 115 | 116 | Read our [Contributing Guide](CONTRIBUTING.md) to learn about reporting issues, contributing code, and more ways to contribute. 117 | 118 | ## Getting in Touch 119 | 120 | If you have questions about getting setup or just want to say hi, join the [WordPress Slack](https://chat.wordpress.org) and drop a message on the `#mobile` channel. 121 | 122 | ## Author 123 | 124 | WordPress, mobile@automattic.com 125 | 126 | ## License 127 | 128 | WPMediaPicker is available under the GPL license. See the [LICENSE file](./LICENSE) for more info. 129 | 130 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaCapturePreviewCollectionView.m: -------------------------------------------------------------------------------- 1 | #import "WPMediaCapturePreviewCollectionView.h" 2 | #import "WPMediaPickerResources.h" 3 | @import AVFoundation; 4 | 5 | @interface WPMediaCapturePreviewCollectionView () 6 | 7 | @property (nonatomic, strong) AVCaptureSession *session; 8 | @property (nonatomic, strong) dispatch_queue_t sessionQueue; 9 | @property (nonatomic, strong) UIView *previewView; 10 | @property (nonatomic, strong) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer; 11 | 12 | @end 13 | 14 | @implementation WPMediaCapturePreviewCollectionView 15 | 16 | - (instancetype)initWithFrame:(CGRect)frame 17 | { 18 | self = [super initWithFrame:frame]; 19 | if (self) { 20 | [self commonInit]; 21 | } 22 | return self; 23 | } 24 | 25 | - (void)commonInit 26 | { 27 | [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; 28 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; 29 | self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 30 | self.backgroundColor = [UIColor blackColor]; 31 | _sessionQueue = dispatch_queue_create("org.wordpress.WPMediaCapturePreviewCollectionView", DISPATCH_QUEUE_SERIAL); 32 | _previewView = [[UIView alloc] initWithFrame:self.bounds]; 33 | _previewView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 34 | [self addSubview:_previewView]; 35 | 36 | UIImage *cameraImage = [[WPMediaPickerResources imageNamed:@"gridicons-camera-large" withExtension:@"png"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 37 | UIImageView *imageView = [[UIImageView alloc] initWithImage:cameraImage]; 38 | imageView.tintColor = [UIColor whiteColor]; 39 | imageView.center = CGPointMake(CGRectGetWidth(self.frame) / 2.0, CGRectGetHeight(self.frame) / 2.0); 40 | imageView.contentMode = UIViewContentModeScaleAspectFill; 41 | imageView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; 42 | [self addSubview:imageView]; 43 | } 44 | 45 | - (void)dealloc 46 | { 47 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 48 | } 49 | 50 | - (void)layoutSubviews { 51 | [super layoutSubviews]; 52 | self.previewView.frame = self.bounds; 53 | self.captureVideoPreviewLayer.frame = self.previewView.bounds; 54 | } 55 | 56 | - (void)stopCaptureOnCompletion:(void (^)(void))block 57 | { 58 | if (!self.session) { 59 | if (block) { 60 | dispatch_async(dispatch_get_main_queue(), block); 61 | } 62 | return; 63 | } 64 | 65 | dispatch_async(self.sessionQueue, ^{ 66 | if ([self.session isRunning]){ 67 | [self.session stopRunning]; 68 | self.session = nil; 69 | [self.captureVideoPreviewLayer removeFromSuperlayer]; 70 | self.captureVideoPreviewLayer = nil; 71 | } 72 | if (block) { 73 | dispatch_async(dispatch_get_main_queue(), block); 74 | } 75 | }); 76 | } 77 | 78 | - (void)startCapture 79 | { 80 | dispatch_async(self.sessionQueue, ^{ 81 | AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; 82 | if ( status != AVAuthorizationStatusAuthorized && 83 | status != AVAuthorizationStatusNotDetermined) 84 | { 85 | return; 86 | } 87 | 88 | if (!self.session){ 89 | self.session = [[AVCaptureSession alloc] init]; 90 | self.session.sessionPreset = AVCaptureSessionPresetHigh; 91 | 92 | AVCaptureDevice *device = [self captureDevice]; 93 | 94 | NSError *error = nil; 95 | AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; 96 | if (input) { 97 | [self.session addInput:input]; 98 | } else { 99 | NSLog(@"Error: %@", error); 100 | return; 101 | } 102 | } 103 | if (!self.session.isRunning || !self.captureVideoPreviewLayer.connection.enabled){ 104 | [self.session startRunning]; 105 | if (!self.captureVideoPreviewLayer || !self.captureVideoPreviewLayer.connection.enabled) { 106 | AVCaptureVideoPreviewLayer * newLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session]; 107 | dispatch_async(dispatch_get_main_queue(), ^{ 108 | [self.captureVideoPreviewLayer removeFromSuperlayer]; 109 | self.captureVideoPreviewLayer = newLayer; 110 | CALayer *viewLayer = self.previewView.layer; 111 | self.captureVideoPreviewLayer.frame = viewLayer.bounds; 112 | self.captureVideoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; 113 | UIWindowScene *currentScene = [[[[UIApplication sharedApplication] windows] lastObject] windowScene]; 114 | self.captureVideoPreviewLayer.connection.videoOrientation = [self videoOrientationForInterfaceOrientation:[currentScene interfaceOrientation]]; 115 | [viewLayer addSublayer:self.captureVideoPreviewLayer]; 116 | }); 117 | } 118 | } 119 | }); 120 | } 121 | 122 | - (void)deviceOrientationDidChange:(NSNotification *)notification 123 | { 124 | if (self.captureVideoPreviewLayer.connection.supportsVideoOrientation) { 125 | UIWindowScene *currentScene = [[[[UIApplication sharedApplication] windows] lastObject] windowScene]; 126 | self.captureVideoPreviewLayer.connection.videoOrientation = [self videoOrientationForInterfaceOrientation:[currentScene interfaceOrientation]]; 127 | } 128 | } 129 | 130 | - (AVCaptureVideoOrientation)videoOrientationForInterfaceOrientation:(UIInterfaceOrientation)orientation 131 | { 132 | switch (orientation) { 133 | case UIInterfaceOrientationPortrait: 134 | return AVCaptureVideoOrientationPortrait; 135 | case UIInterfaceOrientationPortraitUpsideDown: 136 | return AVCaptureVideoOrientationPortraitUpsideDown; 137 | case UIInterfaceOrientationLandscapeLeft: 138 | return AVCaptureVideoOrientationLandscapeLeft; 139 | case UIInterfaceOrientationLandscapeRight: 140 | return AVCaptureVideoOrientationLandscapeRight; 141 | default:return AVCaptureVideoOrientationPortrait; 142 | } 143 | } 144 | 145 | - (BOOL)isAccessibilityElement 146 | { 147 | return YES; 148 | } 149 | 150 | - (NSString *)accessibilityLabel 151 | { 152 | return NSLocalizedString(@"Camera", @"Accessibility label for the camera tile in the collection view"); 153 | } 154 | 155 | - (AVCaptureDevice *)captureDevice 156 | { 157 | if (self.preferFrontCamera) { 158 | AVCaptureDevice *device = [[AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] 159 | mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront].devices firstObject]; 160 | if (device) { 161 | return device; 162 | } 163 | } 164 | return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; 165 | } 166 | 167 | @end 168 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaGroupPickerViewController.m: -------------------------------------------------------------------------------- 1 | #import "WPMediaGroupPickerViewController.h" 2 | #import "WPMediaGroupTableViewCell.h" 3 | #import "UIViewController+MediaAdditions.h" 4 | 5 | static CGFloat const WPMediaGroupCellHeight = 86.0f; 6 | 7 | @interface WPMediaGroupPickerViewController () 8 | 9 | @property (nonatomic, strong) NSObject *changesObserver; 10 | 11 | @end 12 | 13 | @implementation WPMediaGroupPickerViewController 14 | 15 | - (instancetype)init 16 | { 17 | self = [super initWithStyle:UITableViewStylePlain]; 18 | if (self) { 19 | self.title = NSLocalizedString(@"Albums", @"Description of albums in the photo libraries"); 20 | } 21 | return self; 22 | } 23 | 24 | - (void)dealloc 25 | { 26 | [self unregisterDataSourceObservers]; 27 | } 28 | 29 | - (void)viewDidLoad 30 | { 31 | [super viewDidLoad]; 32 | 33 | // configure table view 34 | self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 35 | if ([self respondsToSelector:@selector(popoverPresentationController)] 36 | && self.popoverPresentationController) { 37 | self.tableView.backgroundColor = [UIColor clearColor]; 38 | } 39 | [self.tableView registerClass:[WPMediaGroupTableViewCell class] forCellReuseIdentifier:NSStringFromClass([WPMediaGroupTableViewCell class])]; 40 | self.tableView.rowHeight = WPMediaGroupCellHeight; 41 | self.tableView.accessibilityIdentifier = @"AlbumTable"; 42 | 43 | //Setup navigation 44 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelPicker:)]; 45 | 46 | [self loadData]; 47 | } 48 | 49 | - (void)setDataSource:(id)dataSource { 50 | [self unregisterDataSourceObservers]; 51 | _dataSource = dataSource; 52 | [self registerDataSourceObservers]; 53 | } 54 | 55 | - (void)registerDataSourceObservers { 56 | __weak __typeof__(self) weakSelf = self; 57 | self.changesObserver = [self.dataSource registerGroupChangeObserverBlock:^() { 58 | if (weakSelf.isViewLoaded) { 59 | [weakSelf loadData]; 60 | } 61 | }]; 62 | } 63 | 64 | - (void)unregisterDataSourceObservers { 65 | if (_changesObserver) { 66 | [_dataSource unregisterGroupChangeObserver:_changesObserver]; 67 | _changesObserver = nil; 68 | } 69 | } 70 | 71 | - (void)loadData 72 | { 73 | [self.dataSource loadDataWithOptions:WPMediaLoadOptionsGroups success:^{ 74 | dispatch_async(dispatch_get_main_queue(), ^{ 75 | [self.tableView reloadData]; 76 | }); 77 | } failure:^(NSError *error) { 78 | dispatch_async(dispatch_get_main_queue(), ^{ 79 | [self showError:error]; 80 | }); 81 | }]; 82 | } 83 | 84 | - (void)showError:(NSError *)error { 85 | [self.refreshControl endRefreshing]; 86 | [self.tableView reloadData]; 87 | if ([self.delegate respondsToSelector:@selector(mediaGroupPickerViewController:handleError:)]) { 88 | if ([self.delegate mediaGroupPickerViewController:self handleError:error]) { 89 | return; 90 | } 91 | } 92 | [self wpm_showAlertWithError:error okActionHandler:^(UIAlertAction * _Nonnull action) { 93 | if ([self.delegate respondsToSelector:@selector(mediaGroupPickerViewControllerDidCancel:)]) { 94 | [self.delegate mediaGroupPickerViewControllerDidCancel:self]; 95 | } 96 | }]; 97 | } 98 | 99 | #pragma mark - UITableViewDataSource methods 100 | 101 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 102 | { 103 | return 1; 104 | } 105 | 106 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 107 | { 108 | return [self.dataSource numberOfGroups]; 109 | } 110 | 111 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 112 | { 113 | WPMediaGroupTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:NSStringFromClass([WPMediaGroupTableViewCell class]) forIndexPath:indexPath]; 114 | 115 | id group = [self.dataSource groupAtIndex:indexPath.row]; 116 | 117 | cell.imagePosterView.image = nil; 118 | NSString *groupID = group.identifier; 119 | cell.groupIdentifier = groupID; 120 | CGFloat scale = [[UIScreen mainScreen] scale]; 121 | CGSize requestSize = CGSizeApplyAffineTransform(CGSizeMake(WPMediaGroupCellHeight, WPMediaGroupCellHeight), CGAffineTransformMakeScale(scale, scale)); 122 | [group imageWithSize:requestSize 123 | completionHandler:^(UIImage *result, NSError *error) 124 | { 125 | if (error) { 126 | return; 127 | } 128 | if ([cell.groupIdentifier isEqualToString:groupID]){ 129 | dispatch_async(dispatch_get_main_queue(), ^{ 130 | cell.imagePosterView.image = result; 131 | }); 132 | } 133 | }]; 134 | cell.titleLabel.text = [group name]; 135 | NSInteger numberOfAssets = [group numberOfAssetsOfType:[self.dataSource mediaTypeFilter] completionHandler:^(NSInteger result, NSError *error) { 136 | if ([cell.groupIdentifier isEqualToString:groupID]){ 137 | dispatch_async(dispatch_get_main_queue(), ^{ 138 | cell.countLabel.text = [NSString stringWithFormat:@"%ld", (long)result]; 139 | }); 140 | } 141 | }]; 142 | if (numberOfAssets != NSNotFound) { 143 | cell.countLabel.text = [NSString stringWithFormat:@"%ld", (long)numberOfAssets]; 144 | } else { 145 | cell.countLabel.text = NSLocalizedString(@"Counting media items...", @"Message to show while media data source is finding the number of items available."); 146 | } 147 | cell.backgroundColor = [UIColor clearColor]; 148 | cell.textLabel.backgroundColor = [UIColor clearColor]; 149 | cell.detailTextLabel.backgroundColor = [UIColor clearColor]; 150 | cell.selectionStyle = UITableViewCellSelectionStyleDefault; 151 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 152 | 153 | return cell; 154 | } 155 | 156 | - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath 157 | { 158 | NSIndexPath *selectedPath = [self.tableView indexPathForSelectedRow]; 159 | if (selectedPath) { 160 | UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:selectedPath]; 161 | cell.accessoryType = UITableViewCellAccessoryNone; 162 | } 163 | return indexPath; 164 | } 165 | 166 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 167 | { 168 | [self notifySelectionOfGroup]; 169 | } 170 | 171 | #pragma mark - Callback methods 172 | 173 | - (void)cancelPicker:(UIBarButtonItem *)sender 174 | { 175 | if ([self.delegate respondsToSelector:@selector(mediaGroupPickerViewControllerDidCancel:)]) { 176 | [self.delegate mediaGroupPickerViewControllerDidCancel:self]; 177 | } 178 | } 179 | 180 | - (void)notifySelectionOfGroup 181 | { 182 | if (!self.tableView.indexPathForSelectedRow) { 183 | return; 184 | } 185 | if ([self.delegate respondsToSelector:@selector(mediaGroupPickerViewController:didPickGroup:)]) { 186 | NSInteger selectedRow = self.tableView.indexPathForSelectedRow.row; 187 | id group = [self.dataSource groupAtIndex:selectedRow]; 188 | [self.delegate mediaGroupPickerViewController:self didPickGroup:group]; 189 | } 190 | } 191 | 192 | @end 193 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | activesupport (7.1.1) 7 | base64 8 | bigdecimal 9 | concurrent-ruby (~> 1.0, >= 1.0.2) 10 | connection_pool (>= 2.2.5) 11 | drb 12 | i18n (>= 1.6, < 2) 13 | minitest (>= 5.1) 14 | mutex_m 15 | tzinfo (~> 2.0) 16 | addressable (2.8.5) 17 | public_suffix (>= 2.0.2, < 6.0) 18 | algoliasearch (1.27.5) 19 | httpclient (~> 2.8, >= 2.8.3) 20 | json (>= 1.5.1) 21 | artifactory (3.0.15) 22 | ast (2.4.2) 23 | atomos (0.1.3) 24 | aws-eventstream (1.1.1) 25 | aws-partitions (1.486.0) 26 | aws-sdk-core (3.119.0) 27 | aws-eventstream (~> 1, >= 1.0.2) 28 | aws-partitions (~> 1, >= 1.239.0) 29 | aws-sigv4 (~> 1.1) 30 | jmespath (~> 1.0) 31 | aws-sdk-kms (1.46.0) 32 | aws-sdk-core (~> 3, >= 3.119.0) 33 | aws-sigv4 (~> 1.1) 34 | aws-sdk-s3 (1.98.0) 35 | aws-sdk-core (~> 3, >= 3.119.0) 36 | aws-sdk-kms (~> 1) 37 | aws-sigv4 (~> 1.1) 38 | aws-sigv4 (1.2.4) 39 | aws-eventstream (~> 1, >= 1.0.2) 40 | babosa (1.0.4) 41 | base64 (0.1.1) 42 | bigdecimal (3.1.4) 43 | claide (1.1.0) 44 | cocoapods (1.14.2) 45 | addressable (~> 2.8) 46 | claide (>= 1.0.2, < 2.0) 47 | cocoapods-core (= 1.14.2) 48 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 49 | cocoapods-downloader (>= 2.0) 50 | cocoapods-plugins (>= 1.0.0, < 2.0) 51 | cocoapods-search (>= 1.0.0, < 2.0) 52 | cocoapods-trunk (>= 1.6.0, < 2.0) 53 | cocoapods-try (>= 1.1.0, < 2.0) 54 | colored2 (~> 3.1) 55 | escape (~> 0.0.4) 56 | fourflusher (>= 2.3.0, < 3.0) 57 | gh_inspector (~> 1.0) 58 | molinillo (~> 0.8.0) 59 | nap (~> 1.0) 60 | ruby-macho (>= 2.3.0, < 3.0) 61 | xcodeproj (>= 1.23.0, < 2.0) 62 | cocoapods-core (1.14.2) 63 | activesupport (>= 5.0, < 8) 64 | addressable (~> 2.8) 65 | algoliasearch (~> 1.0) 66 | concurrent-ruby (~> 1.1) 67 | fuzzy_match (~> 2.0.4) 68 | nap (~> 1.0) 69 | netrc (~> 0.11) 70 | public_suffix (~> 4.0) 71 | typhoeus (~> 1.0) 72 | cocoapods-deintegrate (1.0.5) 73 | cocoapods-downloader (2.0) 74 | cocoapods-plugins (1.0.0) 75 | nap 76 | cocoapods-search (1.0.1) 77 | cocoapods-trunk (1.6.0) 78 | nap (>= 0.8, < 2.0) 79 | netrc (~> 0.11) 80 | cocoapods-try (1.2.0) 81 | colored (1.2) 82 | colored2 (3.1.2) 83 | commander (4.6.0) 84 | highline (~> 2.0.0) 85 | concurrent-ruby (1.2.2) 86 | connection_pool (2.4.1) 87 | declarative (0.0.20) 88 | digest-crc (0.6.4) 89 | rake (>= 12.0.0, < 14.0.0) 90 | domain_name (0.5.20190701) 91 | unf (>= 0.0.5, < 1.0.0) 92 | dotenv (2.7.6) 93 | drb (2.1.1) 94 | ruby2_keywords 95 | emoji_regex (3.2.2) 96 | escape (0.0.4) 97 | ethon (0.16.0) 98 | ffi (>= 1.15.0) 99 | excon (0.85.0) 100 | faraday (1.7.0) 101 | faraday-em_http (~> 1.0) 102 | faraday-em_synchrony (~> 1.0) 103 | faraday-excon (~> 1.1) 104 | faraday-httpclient (~> 1.0.1) 105 | faraday-net_http (~> 1.0) 106 | faraday-net_http_persistent (~> 1.1) 107 | faraday-patron (~> 1.0) 108 | faraday-rack (~> 1.0) 109 | multipart-post (>= 1.2, < 3) 110 | ruby2_keywords (>= 0.0.4) 111 | faraday-cookie_jar (0.0.7) 112 | faraday (>= 0.8.0) 113 | http-cookie (~> 1.0.0) 114 | faraday-em_http (1.0.0) 115 | faraday-em_synchrony (1.0.0) 116 | faraday-excon (1.1.0) 117 | faraday-httpclient (1.0.1) 118 | faraday-net_http (1.0.1) 119 | faraday-net_http_persistent (1.2.0) 120 | faraday-patron (1.0.0) 121 | faraday-rack (1.0.0) 122 | faraday_middleware (1.1.0) 123 | faraday (~> 1.0) 124 | fastimage (2.2.5) 125 | fastlane (2.191.0) 126 | CFPropertyList (>= 2.3, < 4.0.0) 127 | addressable (>= 2.8, < 3.0.0) 128 | artifactory (~> 3.0) 129 | aws-sdk-s3 (~> 1.0) 130 | babosa (>= 1.0.3, < 2.0.0) 131 | bundler (>= 1.12.0, < 3.0.0) 132 | colored 133 | commander (~> 4.6) 134 | dotenv (>= 2.1.1, < 3.0.0) 135 | emoji_regex (>= 0.1, < 4.0) 136 | excon (>= 0.71.0, < 1.0.0) 137 | faraday (~> 1.0) 138 | faraday-cookie_jar (~> 0.0.6) 139 | faraday_middleware (~> 1.0) 140 | fastimage (>= 2.1.0, < 3.0.0) 141 | gh_inspector (>= 1.1.2, < 2.0.0) 142 | google-apis-androidpublisher_v3 (~> 0.3) 143 | google-apis-playcustomapp_v1 (~> 0.1) 144 | google-cloud-storage (~> 1.31) 145 | highline (~> 2.0) 146 | json (< 3.0.0) 147 | jwt (>= 2.1.0, < 3) 148 | mini_magick (>= 4.9.4, < 5.0.0) 149 | multipart-post (~> 2.0.0) 150 | naturally (~> 2.2) 151 | plist (>= 3.1.0, < 4.0.0) 152 | rubyzip (>= 2.0.0, < 3.0.0) 153 | security (= 0.1.3) 154 | simctl (~> 1.6.3) 155 | terminal-notifier (>= 2.0.0, < 3.0.0) 156 | terminal-table (>= 1.4.5, < 2.0.0) 157 | tty-screen (>= 0.6.3, < 1.0.0) 158 | tty-spinner (>= 0.8.0, < 1.0.0) 159 | word_wrap (~> 1.0.0) 160 | xcodeproj (>= 1.13.0, < 2.0.0) 161 | xcpretty (~> 0.3.0) 162 | xcpretty-travis-formatter (>= 0.0.3) 163 | ffi (1.16.3) 164 | fourflusher (2.3.1) 165 | fuzzy_match (2.0.4) 166 | gh_inspector (1.1.3) 167 | google-apis-androidpublisher_v3 (0.10.0) 168 | google-apis-core (>= 0.4, < 2.a) 169 | google-apis-core (0.4.1) 170 | addressable (~> 2.5, >= 2.5.1) 171 | googleauth (>= 0.16.2, < 2.a) 172 | httpclient (>= 2.8.1, < 3.a) 173 | mini_mime (~> 1.0) 174 | representable (~> 3.0) 175 | retriable (>= 2.0, < 4.a) 176 | rexml 177 | webrick 178 | google-apis-iamcredentials_v1 (0.6.0) 179 | google-apis-core (>= 0.4, < 2.a) 180 | google-apis-playcustomapp_v1 (0.5.0) 181 | google-apis-core (>= 0.4, < 2.a) 182 | google-apis-storage_v1 (0.6.0) 183 | google-apis-core (>= 0.4, < 2.a) 184 | google-cloud-core (1.6.0) 185 | google-cloud-env (~> 1.0) 186 | google-cloud-errors (~> 1.0) 187 | google-cloud-env (1.5.0) 188 | faraday (>= 0.17.3, < 2.0) 189 | google-cloud-errors (1.1.0) 190 | google-cloud-storage (1.34.1) 191 | addressable (~> 2.5) 192 | digest-crc (~> 0.4) 193 | google-apis-iamcredentials_v1 (~> 0.1) 194 | google-apis-storage_v1 (~> 0.1) 195 | google-cloud-core (~> 1.6) 196 | googleauth (>= 0.16.2, < 2.a) 197 | mini_mime (~> 1.0) 198 | googleauth (0.17.0) 199 | faraday (>= 0.17.3, < 2.0) 200 | jwt (>= 1.4, < 3.0) 201 | memoist (~> 0.16) 202 | multi_json (~> 1.11) 203 | os (>= 0.9, < 2.0) 204 | signet (~> 0.14) 205 | highline (2.0.3) 206 | http-cookie (1.0.4) 207 | domain_name (~> 0.5) 208 | httpclient (2.8.3) 209 | i18n (1.14.1) 210 | concurrent-ruby (~> 1.0) 211 | jmespath (1.4.0) 212 | json (2.6.3) 213 | jwt (2.2.3) 214 | memoist (0.16.2) 215 | mini_magick (4.11.0) 216 | mini_mime (1.1.0) 217 | minitest (5.20.0) 218 | molinillo (0.8.0) 219 | multi_json (1.15.0) 220 | multipart-post (2.0.0) 221 | mutex_m (0.1.2) 222 | nanaimo (0.3.0) 223 | nap (1.1.0) 224 | naturally (2.2.1) 225 | netrc (0.11.0) 226 | os (1.1.1) 227 | parallel (1.20.1) 228 | parser (3.0.2.0) 229 | ast (~> 2.4.1) 230 | plist (3.6.0) 231 | public_suffix (4.0.7) 232 | rainbow (3.0.0) 233 | rake (13.0.6) 234 | regexp_parser (2.1.1) 235 | representable (3.1.1) 236 | declarative (< 0.1.0) 237 | trailblazer-option (>= 0.1.1, < 0.2.0) 238 | uber (< 0.2.0) 239 | retriable (3.1.2) 240 | rexml (3.2.6) 241 | rouge (2.0.7) 242 | rubocop (1.18.4) 243 | parallel (~> 1.10) 244 | parser (>= 3.0.0.0) 245 | rainbow (>= 2.2.2, < 4.0) 246 | regexp_parser (>= 1.8, < 3.0) 247 | rexml 248 | rubocop-ast (>= 1.8.0, < 2.0) 249 | ruby-progressbar (~> 1.7) 250 | unicode-display_width (>= 1.4.0, < 3.0) 251 | rubocop-ast (1.9.1) 252 | parser (>= 3.0.1.1) 253 | ruby-macho (2.5.1) 254 | ruby-progressbar (1.11.0) 255 | ruby2_keywords (0.0.5) 256 | rubyzip (2.3.2) 257 | security (0.1.3) 258 | signet (0.15.0) 259 | addressable (~> 2.3) 260 | faraday (>= 0.17.3, < 2.0) 261 | jwt (>= 1.5, < 3.0) 262 | multi_json (~> 1.10) 263 | simctl (1.6.8) 264 | CFPropertyList 265 | naturally 266 | terminal-notifier (2.0.0) 267 | terminal-table (1.8.0) 268 | unicode-display_width (~> 1.1, >= 1.1.1) 269 | trailblazer-option (0.1.1) 270 | tty-cursor (0.7.1) 271 | tty-screen (0.8.1) 272 | tty-spinner (0.9.3) 273 | tty-cursor (~> 0.7) 274 | typhoeus (1.4.0) 275 | ethon (>= 0.9.0) 276 | tzinfo (2.0.6) 277 | concurrent-ruby (~> 1.0) 278 | uber (0.1.0) 279 | unf (0.1.4) 280 | unf_ext 281 | unf_ext (0.0.7.7) 282 | unicode-display_width (1.7.0) 283 | webrick (1.7.0) 284 | word_wrap (1.0.0) 285 | xcodeproj (1.23.0) 286 | CFPropertyList (>= 2.3.3, < 4.0) 287 | atomos (~> 0.1.3) 288 | claide (>= 1.0.2, < 2.0) 289 | colored2 (~> 3.1) 290 | nanaimo (~> 0.3.0) 291 | rexml (~> 3.2.4) 292 | xcpretty (0.3.0) 293 | rouge (~> 2.0.7) 294 | xcpretty-travis-formatter (1.0.1) 295 | xcpretty (~> 0.2, >= 0.0.7) 296 | 297 | PLATFORMS 298 | ruby 299 | 300 | DEPENDENCIES 301 | cocoapods (~> 1.11) 302 | fastlane (~> 2.189) 303 | rubocop (~> 1.18) 304 | 305 | BUNDLED WITH 306 | 2.3.23 307 | -------------------------------------------------------------------------------- /Pod/Classes/WPAssetViewController.m: -------------------------------------------------------------------------------- 1 | #import "WPAssetViewController.h" 2 | 3 | @import AVFoundation; 4 | @import AVKit; 5 | 6 | #import "WPVideoPlayerView.h" 7 | #import "WPDateTimeHelpers.h" 8 | 9 | @interface WPAssetViewController () 10 | 11 | @property (nonatomic, strong) UIImageView *imageView; 12 | @property (nonatomic, strong) WPVideoPlayerView *videoView; 13 | 14 | @property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; 15 | 16 | @end 17 | 18 | @implementation WPAssetViewController 19 | 20 | - (instancetype)initWithAsset:(id)asset 21 | { 22 | if (self = [super initWithNibName:nil bundle:nil]) { 23 | _asset = asset; 24 | } 25 | 26 | return self; 27 | } 28 | 29 | - (void)viewDidLoad 30 | { 31 | [super viewDidLoad]; 32 | 33 | self.view.backgroundColor = [UIColor blackColor]; 34 | 35 | [self.view addSubview:self.imageView]; 36 | self.imageView.translatesAutoresizingMaskIntoConstraints = NO; 37 | [self.imageView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES; 38 | [self.imageView.widthAnchor constraintEqualToAnchor:self.view.widthAnchor].active = YES; 39 | [self.imageView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor].active = YES; 40 | [self.imageView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor].active = YES; 41 | 42 | [self.view addSubview:self.videoView]; 43 | self.videoView.translatesAutoresizingMaskIntoConstraints = NO; 44 | [self.videoView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES; 45 | [self.videoView.widthAnchor constraintEqualToAnchor:self.view.widthAnchor].active = YES; 46 | [self.videoView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor].active = YES; 47 | [self.videoView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor].active = YES; 48 | self.videoView.delegate = self; 49 | 50 | [self.view addSubview:self.activityIndicatorView]; 51 | self.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = NO; 52 | [self.activityIndicatorView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES; 53 | [self.activityIndicatorView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES; 54 | 55 | NSString *actionTitle = NSLocalizedString(@"Add", @"Remove asset from media picker list"); 56 | if (self.selected) { 57 | actionTitle = NSLocalizedString(@"Remove", @"Add asset to media picker list"); 58 | } 59 | 60 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:actionTitle style:UIBarButtonItemStylePlain target:self action:@selector(selectAction:)]; 61 | 62 | [self showAsset]; 63 | } 64 | 65 | - (void)viewDidAppear:(BOOL)animated { 66 | [super viewDidAppear:animated]; 67 | [self updateNavigationTitle]; 68 | [self.videoView play]; 69 | } 70 | 71 | - (void)viewWillDisappear:(BOOL)animated { 72 | [super viewWillDisappear:animated]; 73 | [self.videoView pause]; 74 | } 75 | 76 | - (void)updateNavigationTitle { 77 | if (self.asset.date == nil || self.navigationController == nil) { 78 | return; 79 | } 80 | UILabel *titleLabel = [[UILabel alloc] init]; 81 | titleLabel.textColor = self.navigationController.navigationBar.tintColor; 82 | if (self.asset.date != nil) { 83 | NSString *dateString = [WPDateTimeHelpers userFriendlyStringDateFromDate:self.asset.date]; 84 | NSString *timeString = [WPDateTimeHelpers userFriendlyStringTimeFromDate:self.asset.date]; 85 | 86 | NSAttributedString *dateAttributedString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n", dateString] attributes:@{NSFontAttributeName: titleLabel.font}]; 87 | NSAttributedString *timeAttributedString = [[NSAttributedString alloc] initWithString:timeString attributes:@{NSFontAttributeName: [titleLabel.font fontWithSize:floorf(titleLabel.font.pointSize * 0.75)]}]; 88 | 89 | NSMutableAttributedString *titleAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:dateAttributedString]; 90 | [titleAttributedString appendAttributedString:timeAttributedString]; 91 | titleLabel.attributedText = titleAttributedString; 92 | } else { 93 | titleLabel.text = @""; 94 | } 95 | titleLabel.numberOfLines = 2; 96 | 97 | titleLabel.textAlignment = NSTextAlignmentCenter; 98 | [titleLabel sizeToFit]; 99 | self.navigationItem.titleView = titleLabel; 100 | } 101 | 102 | - (UIImageView *)imageView 103 | { 104 | if (_imageView) { 105 | return _imageView; 106 | } 107 | _imageView = [[UIImageView alloc] init]; 108 | _imageView.contentMode = UIViewContentModeScaleAspectFit; 109 | _imageView.backgroundColor = [UIColor blackColor]; 110 | _imageView.userInteractionEnabled = YES; 111 | [_imageView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapOnAsset:)]]; 112 | _imageView.accessibilityIgnoresInvertColors = YES; 113 | 114 | return _imageView; 115 | } 116 | 117 | - (WPVideoPlayerView *)videoView 118 | { 119 | if (_videoView) { 120 | return _videoView; 121 | } 122 | _videoView = [[WPVideoPlayerView alloc] init]; 123 | UITapGestureRecognizer *videoTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapOnAsset:)]; 124 | videoTapRecognizer.delegate = self; 125 | [_videoView addGestureRecognizer:videoTapRecognizer]; 126 | _videoView.controlToolbarHidden = YES; 127 | _videoView.shouldAutoPlay = YES; 128 | return _videoView; 129 | } 130 | 131 | 132 | - (UIActivityIndicatorView *)activityIndicatorView 133 | { 134 | if (_activityIndicatorView) { 135 | return _activityIndicatorView; 136 | } 137 | 138 | _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; 139 | 140 | return _activityIndicatorView; 141 | } 142 | 143 | - (void)showAsset 144 | { 145 | self.imageView.hidden = YES; 146 | self.videoView.hidden = YES; 147 | if (self.asset == nil) { 148 | self.imageView.image = nil; 149 | self.videoView.videoURL = nil; 150 | return; 151 | } 152 | switch ([self.asset assetType]) { 153 | case WPMediaTypeImage: 154 | [self showImageAsset]; 155 | break; 156 | case WPMediaTypeVideo: 157 | [self showVideoAsset]; 158 | break; 159 | default: 160 | return; 161 | } 162 | } 163 | 164 | - (void)showImageAsset 165 | { 166 | self.imageView.hidden = NO; 167 | [self.activityIndicatorView startAnimating]; 168 | __weak __typeof__(self) weakSelf = self; 169 | [self.asset imageWithSize:CGSizeZero completionHandler:^(UIImage *result, NSError *error) { 170 | __typeof__(self) strongSelf = weakSelf; 171 | if (!strongSelf) { 172 | return; 173 | } 174 | dispatch_async(dispatch_get_main_queue(), ^{ 175 | [strongSelf.activityIndicatorView stopAnimating]; 176 | if (error) { 177 | [strongSelf showError:error]; 178 | return; 179 | } 180 | strongSelf.imageView.image = result; 181 | }); 182 | }]; 183 | } 184 | 185 | - (void)showVideoAsset 186 | { 187 | self.videoView.hidden = NO; 188 | [self.activityIndicatorView startAnimating]; 189 | __weak __typeof__(self) weakSelf = self; 190 | [self.asset videoAssetWithCompletionHandler:^(AVAsset *asset, NSError *error) { 191 | __typeof__(self) strongSelf = weakSelf; 192 | if (!strongSelf) { 193 | return; 194 | } 195 | dispatch_async(dispatch_get_main_queue(), ^{ 196 | if (error) { 197 | [strongSelf showError:error]; 198 | return; 199 | } 200 | strongSelf.videoView.asset = asset; 201 | }); 202 | }]; 203 | } 204 | 205 | - (void)showError:(NSError *)error { 206 | [self.activityIndicatorView stopAnimating]; 207 | if (self.delegate) { 208 | [self.delegate assetViewController:self failedWithError:error]; 209 | } 210 | } 211 | 212 | - (BOOL)prefersStatusBarHidden 213 | { 214 | return self.videoView.controlToolbarHidden; 215 | } 216 | 217 | - (BOOL)prefersHomeIndicatorAutoHidden 218 | { 219 | return self.videoView.controlToolbarHidden; 220 | } 221 | 222 | - (void)handleTapOnAsset:(UIGestureRecognizer *)gestureRecognizer 223 | { 224 | if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { 225 | BOOL hidden = !self.videoView.controlToolbarHidden; 226 | [self.navigationController setNavigationBarHidden:hidden animated:YES]; 227 | __weak __typeof(self) weakSelf = self; 228 | [self.videoView setControlToolbarHidden:hidden animated:YES completion:^{ 229 | [weakSelf setNeedsStatusBarAppearanceUpdate]; 230 | [weakSelf setNeedsUpdateOfHomeIndicatorAutoHidden]; 231 | }]; 232 | } 233 | } 234 | 235 | - (void)selectAction:(UIBarButtonItem *)button 236 | { 237 | self.selected = !self.selected; 238 | if (self.delegate) { 239 | [self.delegate assetViewController:self selectionChanged:self.selected]; 240 | } 241 | } 242 | 243 | - (CGSize)preferredContentSize 244 | { 245 | CGSize size = self.view.bounds.size; 246 | 247 | // Scale the preferred content size to be the same aspect 248 | // ratio as the asset we're displaying. 249 | CGSize pixelSize = [self.asset pixelSize]; 250 | 251 | CGFloat scaleFactor = 1.0; 252 | if (!CGSizeEqualToSize(pixelSize, CGSizeZero)) { 253 | scaleFactor = pixelSize.height / pixelSize.width; 254 | } 255 | 256 | return CGSizeMake(size.width, size.width * scaleFactor); 257 | } 258 | 259 | #pragma mark - WPVideoPlayerViewDelegate 260 | 261 | - (void)videoPlayerViewStarted:(WPVideoPlayerView *)playerView { 262 | dispatch_async(dispatch_get_main_queue(), ^{ 263 | [self.activityIndicatorView stopAnimating]; 264 | }); 265 | } 266 | 267 | - (void)videoPlayerViewFinish:(WPVideoPlayerView *)playerView { 268 | 269 | } 270 | 271 | - (void)videoPlayerView:(WPVideoPlayerView *)playerView didFailWithError:(NSError *)error { 272 | dispatch_async(dispatch_get_main_queue(), ^{ 273 | [self.activityIndicatorView stopAnimating]; 274 | if (error) { 275 | [self showError:error]; 276 | return; 277 | } 278 | }); 279 | } 280 | 281 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch 282 | { 283 | if ([touch.view isDescendantOfView: self.videoView.controlToolbar]) { 284 | return NO; 285 | } 286 | return YES; 287 | } 288 | 289 | @end 290 | -------------------------------------------------------------------------------- /Pod/Classes/WPVideoPlayerView.m: -------------------------------------------------------------------------------- 1 | #import "WPVideoPlayerView.h" 2 | 3 | @import AVFoundation; 4 | #import "WPDateTimeHelpers.h" 5 | 6 | static NSString *playerItemContext = @"ItemStatusContext"; 7 | 8 | 9 | @interface WPVideoPlayerView() 10 | 11 | @property (nonatomic, strong) AVPlayer *player; 12 | @property (nonatomic, strong) AVPlayerLayer *playerLayer; 13 | @property (nonatomic, strong) AVPlayerItem *playerItem; 14 | @property (nonatomic, strong) UIToolbar *controlToolbar; 15 | @property (nonatomic, strong) UIBarButtonItem *videoDurationButton; 16 | @property (nonatomic, strong) UILabel *videoDurationLabel; 17 | @property (nonatomic, strong) id timeObserver; 18 | 19 | @end 20 | 21 | @implementation WPVideoPlayerView 22 | 23 | static NSString *tracksKey = @"tracks"; 24 | static NSString *timeFormatString = @"%@ / %@"; 25 | static CGFloat toolbarHeight = 44; 26 | 27 | - (instancetype)initWithFrame:(CGRect)frame 28 | { 29 | self = [super initWithFrame:frame]; 30 | if (!self) { 31 | return nil; 32 | } 33 | [self commonInit]; 34 | return self; 35 | } 36 | 37 | - (instancetype)initWithCoder:(NSCoder *)coder 38 | { 39 | self = [super initWithCoder:coder]; 40 | if (!self) { 41 | return nil; 42 | } 43 | [self commonInit]; 44 | return self; 45 | } 46 | 47 | - (void)commonInit { 48 | self.player = [[AVPlayer alloc] init]; 49 | self.playerLayer = [AVPlayerLayer playerLayerWithPlayer: self.player]; 50 | [self.layer addSublayer: self.playerLayer]; 51 | [self addSubview:self.controlToolbar]; 52 | 53 | __weak __typeof__(self) weakSelf = self; 54 | self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, NSEC_PER_SEC) queue:nil usingBlock:^(CMTime time) { 55 | [weakSelf updateVideoDuration]; 56 | }]; 57 | 58 | self.accessibilityIgnoresInvertColors = YES; 59 | } 60 | 61 | - (void)dealloc { 62 | [_playerItem removeObserver:self forKeyPath: @"status"]; 63 | [[NSNotificationCenter defaultCenter] removeObserver: self]; 64 | [_player removeTimeObserver:self.timeObserver]; 65 | [_player pause]; 66 | _asset = nil; 67 | _player = nil; 68 | } 69 | 70 | - (void)layoutSubviews { 71 | [super layoutSubviews]; 72 | self.playerLayer.frame = self.bounds; 73 | 74 | [self updateToolbarPosition:self.controlToolbarHidden]; 75 | } 76 | 77 | - (UIToolbar *)controlToolbar { 78 | if (_controlToolbar) { 79 | return _controlToolbar; 80 | } 81 | _controlToolbar = [[UIToolbar alloc] init]; 82 | _controlToolbar.hidden = YES; 83 | _controlToolbar.tintColor = [UIColor whiteColor]; 84 | _controlToolbar.barStyle = UIBarStyleBlack; 85 | _controlToolbar.translucent = YES; 86 | [self updateControlToolbar]; 87 | return _controlToolbar; 88 | } 89 | 90 | - (UIBarButtonItem *)videoDurationButton { 91 | if (_videoDurationButton) { 92 | return _videoDurationButton; 93 | } 94 | _videoDurationButton = [[UIBarButtonItem alloc] initWithCustomView:self.videoDurationLabel]; 95 | _videoDurationButton.enabled = NO; 96 | return _videoDurationButton; 97 | } 98 | 99 | - (UILabel *)videoDurationLabel { 100 | if (_videoDurationLabel) { 101 | return _videoDurationLabel; 102 | } 103 | 104 | _videoDurationLabel = [UILabel new]; 105 | _videoDurationLabel.textColor = [UIColor whiteColor]; 106 | _videoDurationLabel.font = [UIFont monospacedDigitSystemFontOfSize:14.0 weight: UIFontWeightBold]; 107 | _videoDurationLabel.adjustsFontSizeToFitWidth = NO; 108 | _videoDurationLabel.textAlignment = NSTextAlignmentRight; 109 | _videoDurationLabel.lineBreakMode = NSLineBreakByTruncatingTail; 110 | 111 | // Fix the label to the widest size we want to show, so it doesn't 112 | // resize itself and move around as we update the content 113 | _videoDurationLabel.text = [NSString stringWithFormat:timeFormatString, @"0:00:00", @"0:00:00"]; 114 | [_videoDurationLabel sizeToFit]; 115 | 116 | return _videoDurationLabel; 117 | } 118 | 119 | - (void)setVideoURL:(NSURL *)videoURL { 120 | _videoURL = videoURL; 121 | AVURLAsset *asset = [AVURLAsset assetWithURL:videoURL]; 122 | self.asset = asset; 123 | } 124 | 125 | - (void)setAsset:(AVAsset *)asset { 126 | [self.playerItem removeObserver:self forKeyPath: @"status"]; 127 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 128 | 129 | _asset = asset; 130 | self.playerItem = [[AVPlayerItem alloc] initWithAsset: _asset]; 131 | 132 | [self.playerItem addObserver:self 133 | forKeyPath: @"status" 134 | options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 135 | context:&playerItemContext]; 136 | [[NSNotificationCenter defaultCenter] addObserver:self 137 | selector:@selector(playerItemDidReachEnd:) 138 | name:AVPlayerItemDidPlayToEndTimeNotification 139 | object:self.playerItem]; 140 | [self.player replaceCurrentItemWithPlayerItem: self.playerItem]; 141 | if (self.shouldAutoPlay) { 142 | [self play]; 143 | } 144 | } 145 | 146 | 147 | - (void)playerItemDidReachEnd:(AVPlayerItem *)playerItem { 148 | if (self.loop) { 149 | [self.player seekToTime:kCMTimeZero]; 150 | [self.player play]; 151 | } 152 | if (self.delegate) { 153 | [self.delegate videoPlayerViewFinish:self]; 154 | } 155 | [self updateControlToolbarVideoEnded:!self.loop]; 156 | } 157 | 158 | - (void)play { 159 | [self.player play]; 160 | [self updateControlToolbar]; 161 | [self updateVideoDuration]; 162 | } 163 | 164 | - (void)pause { 165 | [self.player pause]; 166 | [self updateControlToolbar]; 167 | } 168 | 169 | - (void)togglePlayPause { 170 | if ([self.player timeControlStatus] == AVPlayerTimeControlStatusPaused) { 171 | if (CMTimeCompare(self.player.currentItem.currentTime, self.player.currentItem.duration) == 0) { 172 | [self.player seekToTime:kCMTimeZero]; 173 | } 174 | [self play]; 175 | } else { 176 | [self pause]; 177 | } 178 | } 179 | 180 | - (void)setControlToolbarHidden:(BOOL)hidden animated:(BOOL)animated { 181 | [self setControlToolbarHidden:hidden 182 | animated:animated 183 | completion:nil]; 184 | } 185 | 186 | - (void)setControlToolbarHidden:(BOOL)hidden animated:(BOOL)animated completion:(void(^)(void))completion 187 | { 188 | CGFloat animationDuration = animated ? UINavigationControllerHideShowBarDuration : 0; 189 | 190 | __weak __typeof(self) weakSelf = self; 191 | 192 | void (^updateBlock)(void) = ^{ 193 | [weakSelf updateToolbarPosition:hidden]; 194 | }; 195 | 196 | void (^completionBlock)(void) = ^{ 197 | weakSelf.controlToolbar.hidden = hidden; 198 | if (completion) { 199 | completion(); 200 | } 201 | }; 202 | 203 | if (!animated) { 204 | updateBlock(); 205 | completionBlock(); 206 | return; 207 | } 208 | 209 | if (!hidden) { 210 | // Unhide before animating appearance 211 | self.controlToolbar.hidden = hidden; 212 | } 213 | 214 | [UIView animateWithDuration:animationDuration 215 | animations:updateBlock 216 | completion:^(BOOL finished) { 217 | completionBlock(); 218 | }]; 219 | } 220 | 221 | - (void)updateToolbarPosition:(BOOL)hidden 222 | { 223 | CGFloat height = toolbarHeight; 224 | height += self.safeAreaInsets.bottom; 225 | 226 | CGFloat position = hidden ? 0 : height; 227 | self.controlToolbar.frame = CGRectMake(0, self.frame.size.height - position, self.frame.size.width, toolbarHeight); 228 | } 229 | 230 | - (void)setControlToolbarHidden:(BOOL)hidden { 231 | [self setControlToolbarHidden:hidden animated:NO]; 232 | } 233 | 234 | - (BOOL)controlToolbarHidden { 235 | return self.controlToolbar.hidden; 236 | } 237 | 238 | - (void)updateControlToolbar { 239 | [self updateControlToolbarVideoEnded:NO]; 240 | } 241 | 242 | - (void)updateControlToolbarVideoEnded:(BOOL)videoEnded{ 243 | UIBarButtonSystemItem playPauseButton = [self.player timeControlStatus] == AVPlayerTimeControlStatusPaused || videoEnded ? UIBarButtonSystemItemPlay : UIBarButtonSystemItemPause; 244 | 245 | self.controlToolbar.items = @[ 246 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil], 247 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem:playPauseButton target:self action:@selector(togglePlayPause)], 248 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], 249 | self.videoDurationButton, 250 | ]; 251 | } 252 | 253 | - (void)updateVideoDuration { 254 | AVPlayerItem *playerItem = self.player.currentItem; 255 | if (!playerItem || playerItem.status != AVPlayerItemStatusReadyToPlay) { 256 | return; 257 | } 258 | double totalSeconds = CMTimeGetSeconds(playerItem.duration); 259 | double currentSeconds = CMTimeGetSeconds(playerItem.currentTime); 260 | NSString *totalDuration = [WPDateTimeHelpers stringFromTimeInterval:totalSeconds]; 261 | NSString *currentDuration = [WPDateTimeHelpers stringFromTimeInterval:currentSeconds]; 262 | self.videoDurationLabel.text = [NSString stringWithFormat:timeFormatString, currentDuration, totalDuration]; 263 | } 264 | 265 | - (void)observeValueForKeyPath:(NSString *)keyPath 266 | ofObject:(id)object 267 | change:(NSDictionary *)change 268 | context:(void *)context 269 | { 270 | // Only handle observations for the playerItemContext 271 | if (context != &playerItemContext) { 272 | [super observeValueForKeyPath: keyPath 273 | ofObject: object 274 | change: change 275 | context: context]; 276 | return; 277 | } 278 | 279 | if ( [keyPath isEqualToString:@"status"]) { 280 | AVPlayerItemStatus status; 281 | NSNumber *statusNumber = change[NSKeyValueChangeNewKey]; 282 | // Get the status change from the change dictionary 283 | if (statusNumber != nil) { 284 | status = (AVPlayerItemStatus)[statusNumber intValue]; 285 | } else { 286 | status = AVPlayerItemStatusUnknown; 287 | } 288 | 289 | // Switch over the status 290 | switch (status) { 291 | case AVPlayerItemStatusReadyToPlay:{ 292 | // Player item is ready to play. 293 | if (self.delegate) { 294 | [self.delegate videoPlayerViewStarted:self]; 295 | dispatch_async(dispatch_get_main_queue(), ^{ 296 | [self updateVideoDuration]; 297 | }); 298 | } 299 | } 300 | break; 301 | case AVPlayerItemStatusFailed: { 302 | // Player item failed. See error. 303 | NSError *error = [self.playerItem error]; 304 | if (self.delegate) { 305 | [self.delegate videoPlayerView:self didFailWithError: error]; 306 | } 307 | } 308 | break; 309 | case AVPlayerItemStatusUnknown: 310 | // Player item is not yet ready. 311 | return; 312 | break; 313 | } 314 | } 315 | } 316 | 317 | @end 318 | 319 | -------------------------------------------------------------------------------- /Example/WPMediaPicker/OptionsViewController.m: -------------------------------------------------------------------------------- 1 | #import "OptionsViewController.h" 2 | #import "WPMediaCollectionDataSource.h" 3 | 4 | NSString const *MediaPickerOptionsShowMostRecentFirst = @"MediaPickerOptionsShowMostRecentFirst"; 5 | NSString const *MediaPickerOptionsUsePhotosLibrary = @"MediaPickerOptionsUsePhotosLibrary"; 6 | NSString const *MediaPickerOptionsShowCameraCapture = @"MediaPickerOptionsShowCameraCapture"; 7 | NSString const *MediaPickerOptionsPreferFrontCamera = @"MediaPickerOptionsPreferFrontCamera"; 8 | NSString const *MediaPickerOptionsAllowMultipleSelection = @"MediaPickerOptionsAllowMultipleSelection"; 9 | NSString const *MediaPickerOptionsPostProcessingStep = @"MediaPickerOptionsPostProcessingStep"; 10 | NSString const *MediaPickerOptionsFilterType = @"MediaPickerOptionsFilterType"; 11 | NSString const *MediaPickerOptionsCustomPreview = @"MediaPickerOptionsCustomPreview"; 12 | NSString const *MediaPickerOptionsScrollInputPickerVertically = @"MediaPickerOptionsScrollInputPickerVertically"; 13 | NSString const *MediaPickerOptionsShowSampleCellOverlays = @"MediaPickerOptionsShowSampleCellOverlays"; 14 | NSString const *MediaPickerOptionsShowSearchBar = @"MediaPickerOptionsShowSearchBar"; 15 | NSString const *MediaPickerOptionsShowActionBar = @"MediaPickerOptionsShowActionBar"; 16 | NSString const *MediaPickerOptionsShowCustomHeader = @"MediaPickerOptionsShowCustomHeader"; 17 | 18 | 19 | typedef NS_ENUM(NSInteger, OptionsViewControllerCell){ 20 | OptionsViewControllerCellShowMostRecentFirst, 21 | OptionsViewControllerCellShowCameraCapture, 22 | OptionsViewControllerCellPreferFrontCamera, 23 | OptionsViewControllerCellAllowMultipleSelection, 24 | OptionsViewControllerCellPostProcessingStep, 25 | OptionsViewControllerCellMediaType, 26 | OptionsViewControllerCellCustomPreview, 27 | OptionsViewControllerCellInputPickerScroll, 28 | OptionsViewControllerCellShowSampleCellOverlays, 29 | OptionsViewControllerCellShowSearchBar, 30 | OptionsViewControllerCellShowActionBar, 31 | OptionsViewControllerCellShowCustomHeader, 32 | OptionsViewControllerCellTotal 33 | }; 34 | 35 | @interface OptionsViewController () 36 | 37 | @property (nonatomic, strong) UITableViewCell *showMostRecentFirstCell; 38 | @property (nonatomic, strong) UITableViewCell *showCameraCaptureCell; 39 | @property (nonatomic, strong) UITableViewCell *preferFrontCameraCell; 40 | @property (nonatomic, strong) UITableViewCell *allowMultipleSelectionCell; 41 | @property (nonatomic, strong) UITableViewCell *postProcessingStepCell; 42 | @property (nonatomic, strong) UITableViewCell *filterMediaCell; 43 | @property (nonatomic, strong) UITableViewCell *customPreviewCell; 44 | @property (nonatomic, strong) UITableViewCell *scrollInputPickerCell; 45 | @property (nonatomic, strong) UITableViewCell *cellOverlaysCell; 46 | @property (nonatomic, strong) UITableViewCell *showSearchBarCell; 47 | @property (nonatomic, strong) UITableViewCell *showActionBarCell; 48 | @property (nonatomic, strong) UITableViewCell *showCustomHeaderCell; 49 | 50 | @end 51 | 52 | @implementation OptionsViewController 53 | 54 | - (void)viewDidLoad 55 | { 56 | [super viewDidLoad]; 57 | 58 | self.tableView.allowsSelection = NO; 59 | self.tableView.allowsMultipleSelection = NO; 60 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(done:)]; 61 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)]; 62 | 63 | self.showMostRecentFirstCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 64 | self.showMostRecentFirstCell.accessoryView = [[UISwitch alloc] init]; 65 | ((UISwitch *)self.showMostRecentFirstCell.accessoryView).on = [self.options[MediaPickerOptionsShowMostRecentFirst] boolValue]; 66 | self.showMostRecentFirstCell.textLabel.text = NSLocalizedString(@"Show Most Recent First", @""); 67 | 68 | self.showCameraCaptureCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 69 | self.showCameraCaptureCell.accessoryView = [[UISwitch alloc] init]; 70 | ((UISwitch *)self.showCameraCaptureCell.accessoryView).on = [self.options[MediaPickerOptionsShowCameraCapture] boolValue]; 71 | self.showCameraCaptureCell.textLabel.text = NSLocalizedString(@"Show Capture Cell", @""); 72 | 73 | self.preferFrontCameraCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 74 | self.preferFrontCameraCell.accessoryView = [[UISwitch alloc] init]; 75 | ((UISwitch *)self.preferFrontCameraCell.accessoryView).on = [self.options[MediaPickerOptionsPreferFrontCamera] boolValue]; 76 | self.preferFrontCameraCell.textLabel.text = NSLocalizedString(@"Prefer Front Camera", @""); 77 | 78 | self.allowMultipleSelectionCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 79 | self.allowMultipleSelectionCell.accessoryView = [[UISwitch alloc] init]; 80 | ((UISwitch *)self.allowMultipleSelectionCell.accessoryView).on = [self.options[MediaPickerOptionsAllowMultipleSelection] boolValue]; 81 | self.allowMultipleSelectionCell.textLabel.text = NSLocalizedString(@"Allow Multiple Selection", @""); 82 | 83 | self.postProcessingStepCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 84 | self.postProcessingStepCell.accessoryView = [[UISwitch alloc] init]; 85 | ((UISwitch *)self.postProcessingStepCell.accessoryView).on = [self.options[MediaPickerOptionsPostProcessingStep] boolValue]; 86 | self.postProcessingStepCell.textLabel.text = NSLocalizedString(@"Shows Post Processing Step", @""); 87 | 88 | self.filterMediaCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 89 | UISegmentedControl *segment = [[UISegmentedControl alloc] initWithItems:@[@"Photos", @"Videos", @"Photos & Videos"]]; 90 | self.filterMediaCell.accessoryView = segment; 91 | NSInteger filterOption = [self.options[MediaPickerOptionsFilterType] intValue]; 92 | if ((filterOption & WPMediaTypeImage) && (filterOption & WPMediaTypeVideo)) { 93 | segment.selectedSegmentIndex = 2; 94 | } else if (filterOption & WPMediaTypeImage) { 95 | segment.selectedSegmentIndex = 0; 96 | } else { 97 | segment.selectedSegmentIndex = 1; 98 | } 99 | self.filterMediaCell.textLabel.text = NSLocalizedString(@"Media Type", @""); 100 | 101 | self.customPreviewCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 102 | self.customPreviewCell.accessoryView = [[UISwitch alloc] init]; 103 | ((UISwitch *)self.customPreviewCell.accessoryView).on = [self.options[MediaPickerOptionsCustomPreview] boolValue]; 104 | self.customPreviewCell.textLabel.text = NSLocalizedString(@"Use Custom Preview Controller", @""); 105 | 106 | self.scrollInputPickerCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 107 | self.scrollInputPickerCell.accessoryView = [[UISwitch alloc] init]; 108 | ((UISwitch *)self.scrollInputPickerCell.accessoryView).on = [self.options[MediaPickerOptionsScrollInputPickerVertically] boolValue]; 109 | self.scrollInputPickerCell.textLabel.text = NSLocalizedString(@"Scroll Input Picker Vertically", @""); 110 | 111 | self.cellOverlaysCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 112 | self.cellOverlaysCell.accessoryView = [[UISwitch alloc] init]; 113 | ((UISwitch *)self.cellOverlaysCell.accessoryView).on = [self.options[MediaPickerOptionsShowSampleCellOverlays] boolValue]; 114 | self.cellOverlaysCell.textLabel.text = NSLocalizedString(@"Show Sample Cell Overlays", @""); 115 | 116 | self.showSearchBarCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 117 | self.showSearchBarCell.accessoryView = [[UISwitch alloc] init]; 118 | ((UISwitch *)self.showSearchBarCell.accessoryView).on = [self.options[MediaPickerOptionsShowSearchBar] boolValue]; 119 | self.showSearchBarCell.textLabel.text = NSLocalizedString(@"Show Search Bar", @""); 120 | 121 | self.showActionBarCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 122 | self.showActionBarCell.accessoryView = [[UISwitch alloc] init]; 123 | ((UISwitch *)self.showActionBarCell.accessoryView).on = [self.options[MediaPickerOptionsShowActionBar] boolValue]; 124 | self.showActionBarCell.textLabel.text = NSLocalizedString(@"Show Action Bar", @""); 125 | 126 | self.showCustomHeaderCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil]; 127 | self.showCustomHeaderCell.accessoryView = [[UISwitch alloc] init]; 128 | ((UISwitch *)self.showCustomHeaderCell.accessoryView).on = [self.options[MediaPickerOptionsShowCustomHeader] boolValue]; 129 | self.showCustomHeaderCell.textLabel.text = NSLocalizedString(@"Show Custom Header", @""); 130 | self.showCustomHeaderCell.detailTextLabel.text = NSLocalizedString(@"If custom header and capture cell are enabled, custom header takes precedence.", @""); 131 | self.showCustomHeaderCell.detailTextLabel.numberOfLines = 2; 132 | } 133 | 134 | #pragma mark - Table view data source 135 | 136 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 137 | { 138 | return 1; 139 | } 140 | 141 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 142 | { 143 | return OptionsViewControllerCellTotal; 144 | } 145 | 146 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 147 | { 148 | switch (indexPath.row) { 149 | case OptionsViewControllerCellShowMostRecentFirst: 150 | return self.showMostRecentFirstCell; 151 | break; 152 | case OptionsViewControllerCellShowCameraCapture: 153 | return self.showCameraCaptureCell; 154 | break; 155 | case OptionsViewControllerCellPreferFrontCamera: 156 | return self.preferFrontCameraCell; 157 | break; 158 | case OptionsViewControllerCellAllowMultipleSelection: 159 | return self.allowMultipleSelectionCell; 160 | break; 161 | case OptionsViewControllerCellPostProcessingStep: 162 | return self.postProcessingStepCell; 163 | break; 164 | case OptionsViewControllerCellMediaType: 165 | return self.filterMediaCell; 166 | break; 167 | case OptionsViewControllerCellCustomPreview: 168 | return self.customPreviewCell; 169 | break; 170 | case OptionsViewControllerCellInputPickerScroll: 171 | return self.scrollInputPickerCell; 172 | break; 173 | case OptionsViewControllerCellShowSampleCellOverlays: 174 | return self.cellOverlaysCell; 175 | case OptionsViewControllerCellShowSearchBar: 176 | return self.showSearchBarCell; 177 | case OptionsViewControllerCellShowActionBar: 178 | return self.showActionBarCell; 179 | case OptionsViewControllerCellShowCustomHeader: 180 | return self.showCustomHeaderCell; 181 | default: 182 | break; 183 | } 184 | return [UITableViewCell new]; 185 | } 186 | 187 | - (void)done:(id) sender 188 | { 189 | NSInteger selectedFilterOption = ((UISegmentedControl *)self.filterMediaCell.accessoryView).selectedSegmentIndex; 190 | NSInteger filterType = WPMediaTypeImage; 191 | if (selectedFilterOption == 1) { 192 | filterType = WPMediaTypeVideo; 193 | } else if (selectedFilterOption == 2) { 194 | filterType = WPMediaTypeImage | WPMediaTypeVideo; 195 | } 196 | 197 | if ([self.delegate respondsToSelector:@selector(optionsViewController:changed:)]){ 198 | id delegate = self.delegate; 199 | NSDictionary *newOptions = @{ 200 | MediaPickerOptionsShowMostRecentFirst:@(((UISwitch *)self.showMostRecentFirstCell.accessoryView).on), 201 | MediaPickerOptionsShowCameraCapture:@(((UISwitch *)self.showCameraCaptureCell.accessoryView).on), 202 | MediaPickerOptionsPreferFrontCamera:@(((UISwitch *)self.preferFrontCameraCell.accessoryView).on), 203 | MediaPickerOptionsAllowMultipleSelection:@(((UISwitch *)self.allowMultipleSelectionCell.accessoryView).on), 204 | MediaPickerOptionsPostProcessingStep:@(((UISwitch *)self.postProcessingStepCell.accessoryView).on), 205 | MediaPickerOptionsFilterType:@(filterType), 206 | MediaPickerOptionsCustomPreview:@(((UISwitch *)self.customPreviewCell.accessoryView).on), 207 | MediaPickerOptionsScrollInputPickerVertically:@(((UISwitch *)self.scrollInputPickerCell.accessoryView).on), 208 | MediaPickerOptionsShowSampleCellOverlays:@(((UISwitch *)self.cellOverlaysCell.accessoryView).on), 209 | MediaPickerOptionsShowSearchBar:@(((UISwitch *)self.showSearchBarCell.accessoryView).on), 210 | MediaPickerOptionsShowActionBar:@(((UISwitch *)self.showActionBarCell.accessoryView).on), 211 | MediaPickerOptionsShowCustomHeader:@(((UISwitch *)self.showCustomHeaderCell.accessoryView).on) 212 | }; 213 | 214 | [delegate optionsViewController:self changed:newOptions]; 215 | } 216 | } 217 | 218 | - (void)cancel:(id) sender 219 | { 220 | if ([self.delegate respondsToSelector:@selector(cancelOptionsViewController:)]){ 221 | id delegate = self.delegate; 222 | [delegate cancelOptionsViewController:self]; 223 | } 224 | } 225 | 226 | @end 227 | -------------------------------------------------------------------------------- /Pod/Classes/WPMediaCollectionDataSource.h: -------------------------------------------------------------------------------- 1 | @import AVFoundation; 2 | 3 | typedef NS_OPTIONS(NSInteger, WPMediaType){ 4 | WPMediaTypeImage = 1, 5 | WPMediaTypeVideo = 1 << 1, 6 | WPMediaTypeAudio = 1 << 2, 7 | WPMediaTypeOther = 1 << 3, 8 | WPMediaTypeAll= 0XFF 9 | }; 10 | 11 | static NSString * _Nonnull const WPMediaPickerErrorDomain = @"WPMediaPickerErrorDomain"; 12 | 13 | typedef NS_ENUM(NSInteger, WPMediaPickerErrorCode){ 14 | WPMediaPickerErrorCodePermissionDenied, 15 | WPMediaPickerErrorCodeRestricted, 16 | WPMediaPickerErrorCodeUnknown, 17 | WPMediaPickerErrorCodeVideoURLNotAvailable 18 | }; 19 | 20 | @protocol WPMediaMove 21 | - (NSUInteger)from; 22 | - (NSUInteger)to; 23 | @end 24 | 25 | typedef NS_ENUM(NSInteger, WPMediaLoadOptions){ 26 | WPMediaLoadOptionsGroups, 27 | WPMediaLoadOptionsAssets, 28 | WPMediaLoadOptionsGroupsAndAssets 29 | }; 30 | 31 | 32 | @protocol WPMediaAsset; 33 | 34 | typedef void (^WPMediaGroupChangesBlock)(void); 35 | typedef void (^WPMediaChangesBlock)(BOOL incrementalChanges, NSIndexSet * _Nonnull removed, NSIndexSet * _Nonnull inserted, NSIndexSet * _Nonnull changed, NSArray> * _Nonnull moves); 36 | typedef void (^WPMediaSuccessBlock)(void); 37 | typedef void (^WPMediaFailureBlock)(NSError * _Nullable error); 38 | typedef void (^WPMediaAddedBlock)(_Nullable id media, NSError * _Nullable error); 39 | typedef void (^WPMediaImageBlock)(UIImage * _Nullable result, NSError * _Nullable error); 40 | typedef void (^WPMediaCountBlock)(NSInteger result, NSError * _Nullable error); 41 | typedef void (^WPMediaAssetBlock)(AVAsset * _Nullable asset, NSError * _Nullable error); 42 | typedef int32_t WPMediaRequestID; 43 | 44 | 45 | /** 46 | * The WPMediaGroup protocol is adopted by an object that mediates between a media collection and it's representation on 47 | * an visualization like WPMediaGroupPickerViewController. 48 | */ 49 | @protocol WPMediaGroup 50 | 51 | - (NSString *_Nonnull)name; 52 | 53 | /** 54 | * Asynchronously fetches an image that represents the group 55 | * 56 | * @param size the target size for the image, this may not be respected if the requested size is not available 57 | * 58 | * @param completionHandler a block that is invoked when the image is available or when an error occurs. 59 | * 60 | * @return an unique ID of the fetch operation 61 | */ 62 | - (WPMediaRequestID)imageWithSize:(CGSize)size completionHandler:(nonnull WPMediaImageBlock)completionHandler; 63 | 64 | - (void)cancelImageRequest:(WPMediaRequestID)requestID; 65 | 66 | /** 67 | * The original object that represents a group on the underlying media implementation 68 | * 69 | * @return a object from the underlying media implementation 70 | */ 71 | - (id _Nonnull )baseGroup; 72 | 73 | /** 74 | * An unique identifer for the media group 75 | * 76 | * @return a string that uniquely identifies the group 77 | */ 78 | - (nonnull NSString *)identifier; 79 | 80 | /** 81 | The numbers of assets that exist in the group of a certain mediaType 82 | 83 | @param mediaType the asset type to count 84 | @param completionHandler a block that is executed when the real number of assets is know. 85 | @return return an estimation of the current number of assets, if no estimate is known return NSNotFound 86 | */ 87 | - (NSInteger)numberOfAssetsOfType:(WPMediaType)mediaType completionHandler:(nullable WPMediaCountBlock)completionHandler; 88 | 89 | @end 90 | 91 | /** 92 | * The WPMediaAsset protocol is adopted by an object that mediates between a concrete media asset and it's representation on 93 | * a WPMediaCollectionViewCell. 94 | */ 95 | @protocol WPMediaAsset 96 | 97 | /** 98 | Asynchronously fetches an image that represents the asset with the requested size 99 | 100 | @param size the target size for the image, this may not be respected if the requested size is not available. If the size request is zero the maximum available 101 | @param completionHandler a block that is invoked when the image is available or when an error occurs. 102 | @return an unique ID of the fetch operation that can be used to cancel it. 103 | */ 104 | - (WPMediaRequestID)imageWithSize:(CGSize)size completionHandler:(nonnull WPMediaImageBlock)completionHandler; 105 | 106 | /** 107 | * Cancels a previous ongoing request for an asset image 108 | * 109 | * @param requestID an identifier returned by the imageWithSize:completionHandler: method. 110 | */ 111 | - (void)cancelImageRequest:(WPMediaRequestID)requestID; 112 | 113 | /** 114 | Asynchronously fetches an AVAsset that represents the media object. 115 | 116 | @param completionHandler a block that is invoked when the asset is available or when an error occurs. 117 | 118 | @return an unique ID of the fetch operation that can be used to cancel it. 119 | */ 120 | - (WPMediaRequestID)videoAssetWithCompletionHandler:(nonnull WPMediaAssetBlock)completionHandler; 121 | 122 | /** 123 | * The media type of the asset. This could be an image, video, or another unknow type. 124 | * 125 | * @return a WPMEdiaType object. 126 | */ 127 | - (WPMediaType)assetType; 128 | 129 | /** 130 | * The duration of a video media asset. The is only available on video assets. 131 | * 132 | * @return The duration of a video asset. Always zero if the asset is not a video. 133 | */ 134 | - (NSTimeInterval)duration; 135 | 136 | /** 137 | * The original object that represents an asset on the underlying media implementation 138 | * 139 | * @return a object from the underlying media implementation 140 | */ 141 | - (nonnull id)baseAsset; 142 | 143 | /** 144 | * A unique identifier for the media asset 145 | * 146 | * @return a string that uniquely identifies the media asset 147 | */ 148 | - (nonnull NSString *)identifier; 149 | 150 | /** 151 | * The date when the asset was created. 152 | * 153 | * @return a NSDate object that represents the creation date of the asset. 154 | */ 155 | - (nonnull NSDate *)date; 156 | 157 | /** 158 | * The size, in pixels, of the asset’s image or video data. 159 | * 160 | * @return The size, in pixels, of the asset’s image or video data. 161 | */ 162 | - (CGSize)pixelSize; 163 | 164 | @optional 165 | 166 | /** 167 | * @return The filename of this asset. Optional. 168 | */ 169 | - (nullable NSString *)filename; 170 | 171 | /** 172 | * @return The file extension of this asset (PDF, doc, etc). Optional. 173 | */ 174 | - (nullable NSString *)fileExtension; 175 | 176 | /** 177 | * @return The uniform type identifier for this asset. Optional. 178 | */ 179 | - (nullable NSString *)UTTypeIdentifier; 180 | 181 | @end 182 | 183 | /** 184 | * The WPMediaCollectionDataSource protocol is adopted by an object that mediates between a media library implementation 185 | * and a WPMediaPickerViewController / WPMediaCollectionViewController. The data source provides information about the media groups 186 | * that exist and the media assets inside. It also provides methods to add new media assets to the library and observe changes that 187 | * happen outside it's interface. 188 | */ 189 | @protocol WPMediaCollectionDataSource 190 | 191 | /** 192 | * Asks the data source for the number of groups existing on the media library. 193 | * 194 | * @return the number of groups existing on the media library. 195 | */ 196 | - (NSInteger)numberOfGroups; 197 | 198 | /** 199 | * Asks the data source for the group at a selected index. 200 | * 201 | * @param index index location the group requested. 202 | * 203 | * @return an object implementing WPMediaGroup protocol. 204 | */ 205 | - (nonnull id)groupAtIndex:(NSInteger)index; 206 | 207 | /** 208 | * Ask the data source for the current active group of the library 209 | * 210 | * @return an object implementing WPMediaGroup protocol. 211 | */ 212 | - (nullable id)selectedGroup; 213 | 214 | /** 215 | * Ask the data source to select a specific group and update it's assets for that group. 216 | * 217 | * @param group object implementing the WPMediaGroup protocol 218 | */ 219 | - (void)setSelectedGroup:(nonnull id)group; 220 | 221 | /** 222 | * Asks the data source for the number of assets existing on the currect selected group 223 | * 224 | * @return the number of assets existing on the current selected group. 225 | */ 226 | - (NSInteger)numberOfAssets; 227 | 228 | /** 229 | * Asks the data source for the asset at the selected index. 230 | * 231 | * @param index index location of the asset requested. 232 | * 233 | * @return an object implementing the WPMediaAsset protocol 234 | */ 235 | - (nonnull id)mediaAtIndex:(NSInteger)index; 236 | 237 | /** 238 | * Returns the object with the matching identifier if it exists on the datasource 239 | * 240 | * @param identifier a unique identifier for the media 241 | * 242 | * @return the media object if it exists or nil if it's not found. 243 | */ 244 | - (nullable id)mediaWithIdentifier:(nonnull NSString *)identifier; 245 | 246 | /** 247 | * Asks the data source to be notify about changes on the media library using the given callback block. 248 | * 249 | * @discussion the callback object is retained by the data source so it needs to 250 | * be unregistered on the end to avoid leaks or retain cycles. 251 | * 252 | * @param callback a WPMediaChangesBlock that is invoked every time a change is detected. 253 | * 254 | * @return an opaque object that identifies the callback register. This should be used to later unregister the block 255 | */ 256 | - (nonnull id)registerChangeObserverBlock:(nonnull WPMediaChangesBlock)callback; 257 | 258 | /** 259 | * Asks the data source to be notify about changes on the media library group/albums using the given callback block. 260 | * 261 | * @discussion the callback object is retained by the data source so it needs to 262 | * be unregistered on the end to avoid leaks or retain cycles. 263 | * 264 | * @param callback a WPMediaGroupChangessBlock that is invoked every time a change is detected. 265 | * 266 | * @return an opaque object that identifies the callback register. This should be used to later unregister the block 267 | */ 268 | - (nonnull id)registerGroupChangeObserverBlock:(nonnull WPMediaGroupChangesBlock)callback; 269 | 270 | /** 271 | * Asks the data source to unregister the block that is identified by the block key. 272 | * 273 | * @param blockKey the unique identifier of the block. This must have been obtained 274 | * by a call to registerChangeObserverBlock 275 | */ 276 | - (void)unregisterChangeObserver:(nonnull id)blockKey; 277 | 278 | /** 279 | * Asks the data source to unregister the group observer block that is identified by the block key. 280 | * 281 | * @param blockKey the unique identifier of the block. This must have been obtained 282 | * by a call to registerGroupChangesObserverBlock 283 | */ 284 | - (void)unregisterGroupChangeObserver:(nonnull id)blockKey; 285 | 286 | /** 287 | * Asks the data source to reload the data available of the media library. This should be invoked after changing the 288 | * current active group or if a change is detected. 289 | * 290 | * @param options specifiy what type of data to load 291 | * @param successBlock a block that is invoked when the data is loaded with success. 292 | * @param failureBlock a block that is invoked when the are is any kind of error when loading the data. 293 | */ 294 | - (void)loadDataWithOptions:(WPMediaLoadOptions)options 295 | success:(nullable WPMediaSuccessBlock)successBlock 296 | failure:(nullable WPMediaFailureBlock)failureBlock; 297 | 298 | /** 299 | * Requests to the data source to add an image to the library. 300 | * 301 | * @param image an UIImage object with the asset to add 302 | * @param metadata the metadata information of the image to add. 303 | * @param completionBlock a block that is invoked when the image is added. 304 | * On success the media parameter is returned with a new object implemeting the WPMedia protocol 305 | * If an error occurs the media is nil and the error parameter contains a value 306 | */ 307 | - (void)addImage:(nonnull UIImage *)image metadata:(nullable NSDictionary *)metadata completionBlock:(nullable WPMediaAddedBlock)completionBlock; 308 | 309 | /** 310 | * Requests to the data source to add a video to the library. 311 | * 312 | * @param url an url pointing to a file that contains the video to be added to the library. 313 | * @param completionBlock a block that is invoked when the image is added. 314 | * On success the media parameter is returned with a new object implemeting the WPMedia protocol 315 | * If an error occurs the media is nil and the error parameter contains a value 316 | */ 317 | - (void)addVideoFromURL:(nonnull NSURL *)url completionBlock:(nullable WPMediaAddedBlock)completionBlock; 318 | 319 | /** 320 | * Filter the assets acording to their media type. 321 | * 322 | * @param filter the WPMediaType to filter objects to. The default value is WPMediaTypeVideoOrImage 323 | */ 324 | - (void)setMediaTypeFilter:(WPMediaType)filter; 325 | 326 | /** 327 | * 328 | * 329 | * @return The media type filter that is being used. 330 | */ 331 | - (WPMediaType)mediaTypeFilter; 332 | 333 | /** 334 | * Sets the sorting order the assets are show based on creationDate 335 | * 336 | * @param ascending the order wich assets are retrieved, based on the creationDate. The default value is YES 337 | */ 338 | - (void)setAscendingOrdering:(BOOL)ascending; 339 | 340 | /** 341 | * The sorting order on wich the assets are returned 342 | * 343 | * @return if the assets are return in ascending order 344 | */ 345 | - (BOOL)ascendingOrdering; 346 | 347 | @optional 348 | 349 | /** 350 | * Tells the Data Source that the search string has been changed 351 | * 352 | * @param searchText the new search text 353 | */ 354 | - (void)searchFor:(nullable NSString *)searchText; 355 | 356 | /** 357 | * Tells the Data Source that the search was cancelled by the user 358 | */ 359 | - (void)searchCancelled; 360 | 361 | @end 362 | 363 | --------------------------------------------------------------------------------