Browse Source

clear code

Marino Faggiana 6 years ago
parent
commit
c3abe4c21a
100 changed files with 6394 additions and 0 deletions
  1. 50 0
      Libraries external/CTAssetsPickerController/CTAssetCheckmark.h
  2. 165 0
      Libraries external/CTAssetsPickerController/CTAssetCheckmark.m
  3. 54 0
      Libraries external/CTAssetsPickerController/CTAssetCollectionViewCell.h
  4. 319 0
      Libraries external/CTAssetsPickerController/CTAssetCollectionViewCell.m
  5. 33 0
      Libraries external/CTAssetsPickerController/CTAssetCollectionViewController.h
  6. 564 0
      Libraries external/CTAssetsPickerController/CTAssetCollectionViewController.m
  7. 45 0
      Libraries external/CTAssetsPickerController/CTAssetItemViewController.h
  8. 422 0
      Libraries external/CTAssetsPickerController/CTAssetItemViewController.m
  9. 31 0
      Libraries external/CTAssetsPickerController/CTAssetPlayButton.h
  10. 180 0
      Libraries external/CTAssetsPickerController/CTAssetPlayButton.m
  11. 67 0
      Libraries external/CTAssetsPickerController/CTAssetScrollView.h
  12. 713 0
      Libraries external/CTAssetsPickerController/CTAssetScrollView.m
  13. 31 0
      Libraries external/CTAssetsPickerController/CTAssetSelectionButton.h
  14. 122 0
      Libraries external/CTAssetsPickerController/CTAssetSelectionButton.m
  15. 99 0
      Libraries external/CTAssetsPickerController/CTAssetSelectionLabel.h
  16. 219 0
      Libraries external/CTAssetsPickerController/CTAssetSelectionLabel.m
  17. 36 0
      Libraries external/CTAssetsPickerController/CTAssetThumbnailOverlay.h
  18. 156 0
      Libraries external/CTAssetsPickerController/CTAssetThumbnailOverlay.m
  19. 39 0
      Libraries external/CTAssetsPickerController/CTAssetThumbnailStacks.h
  20. 141 0
      Libraries external/CTAssetsPickerController/CTAssetThumbnailStacks.m
  21. 38 0
      Libraries external/CTAssetsPickerController/CTAssetThumbnailView.h
  22. 196 0
      Libraries external/CTAssetsPickerController/CTAssetThumbnailView.m
  23. 41 0
      Libraries external/CTAssetsPickerController/CTAssetsGridSelectedView.h
  24. 142 0
      Libraries external/CTAssetsPickerController/CTAssetsGridSelectedView.m
  25. 33 0
      Libraries external/CTAssetsPickerController/CTAssetsGridView.h
  26. 86 0
      Libraries external/CTAssetsPickerController/CTAssetsGridView.m
  27. 44 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewCell.h
  28. 204 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewCell.m
  29. 50 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewController.h
  30. 802 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewController.m
  31. 38 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewFooter.h
  32. 146 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewFooter.m
  33. 33 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewLayout.h
  34. 109 0
      Libraries external/CTAssetsPickerController/CTAssetsGridViewLayout.m
  35. 31 0
      Libraries external/CTAssetsPickerController/CTAssetsNavigationController.h
  36. 119 0
      Libraries external/CTAssetsPickerController/CTAssetsNavigationController.m
  37. 37 0
      Libraries external/CTAssetsPickerController/CTAssetsPageView.h
  38. 111 0
      Libraries external/CTAssetsPickerController/CTAssetsPageView.m
  39. 69 0
      Libraries external/CTAssetsPickerController/CTAssetsPageViewController.h
  40. 422 0
      Libraries external/CTAssetsPickerController/CTAssetsPageViewController.m
  41. 31 0
      Libraries external/CTAssetsPickerController/CTAssetsPickerAccessDeniedView.h
  42. 126 0
      Libraries external/CTAssetsPickerController/CTAssetsPickerAccessDeniedView.m
  43. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock.png
  44. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock@2x.png
  45. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock@3x.png
  46. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeAllPhotos@2x.png
  47. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeAllPhotos@3x.png
  48. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeBurst@2x.png
  49. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeBurst@3x.png
  50. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeFavorites@2x.png
  51. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeFavorites@3x.png
  52. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeLastImport@2x.png
  53. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeLastImport@3x.png
  54. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgePanorama@2x.png
  55. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgePanorama@3x.png
  56. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeRecentlyDeleted@2x.png
  57. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeRecentlyDeleted@3x.png
  58. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeScreenshots@2x.png
  59. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeScreenshots@3x.png
  60. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSelfPortraits@2x.png
  61. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSelfPortraits@3x.png
  62. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSloMoSmall.png
  63. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSloMoSmall@2x.png
  64. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSloMoSmall@3x.png
  65. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSlomo@2x.png
  66. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSlomo@3x.png
  67. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapse@2x.png
  68. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapse@3x.png
  69. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapseSmall.png
  70. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapseSmall@2x.png
  71. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapseSmall@3x.png
  72. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideo@2x.png
  73. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideo@3x.png
  74. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideoSmall.png
  75. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideoSmall@2x.png
  76. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideoSmall@3x.png
  77. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/Checkmark.png
  78. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/Checkmark@2x.png
  79. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/Checkmark@3x.png
  80. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkShadow.png
  81. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkShadow@2x.png
  82. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkShadow@3x.png
  83. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkUnselected.png
  84. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkUnselected@2x.png
  85. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkUnselected@3x.png
  86. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/DisclosureArrow.png
  87. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/DisclosureArrow@2x.png
  88. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/DisclosureArrow@3x.png
  89. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridDisabledAsset.png
  90. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridDisabledAsset@2x.png
  91. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridDisabledAsset@3x.png
  92. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbum.png
  93. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbum@2x.png
  94. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbum@3x.png
  95. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbumShared.png
  96. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbumShared@2x.png
  97. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbumShared@3x.png
  98. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyCameraRoll.png
  99. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyCameraRoll@2x.png
  100. BIN
      Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyCameraRoll@3x.png

+ 50 - 0
Libraries external/CTAssetsPickerController/CTAssetCheckmark.h

@@ -0,0 +1,50 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+
+/**
+ *  The check mark to show selected asset.
+ */
+@interface CTAssetCheckmark : UIView
+
+#pragma mark Customizing Appearance
+
+/**
+ *  @name Customizing Appearance
+ */
+
+/**
+ *  To set margin of the check mark from specific edges.
+ *
+ *  @param margin The margin from the edges.
+ *  @param edgeX  The layout attribute respresents vertical edge that the check mark pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
+ *  @param edgeY  The layout attribute respresents horizontal edge that the check mark pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
+ */
+- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY UI_APPEARANCE_SELECTOR;
+
+@end

+ 165 - 0
Libraries external/CTAssetsPickerController/CTAssetCheckmark.m

@@ -0,0 +1,165 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetCheckmark.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+
+
+/**
+ *  The check mark to show selected asset.
+ */
+@interface CTAssetCheckmark ()
+
+
+#pragma mark Managing Subviews
+
+/**
+ *  The image view of the check mark shadow.
+ */
+@property (nonatomic, strong) UIImageView *shadowImageView;
+
+/**
+ *  The image view of the check mark.
+ */
+@property (nonatomic, strong) UIImageView *checkmarkImageView;
+
+
+#pragma mark Managing Auto Layout
+
+/**
+ *  The constraint for pinning the check mark to vertical edge.
+ */
+@property (nonatomic, strong) NSLayoutConstraint *verticalConstraint;
+
+/**
+ *  The constraint for pinning the check mark to horizontal edge.
+ */
+@property (nonatomic, strong) NSLayoutConstraint *horizontalConstraint;
+
+/**
+ *  Determines whether or not the constraints have been set up.
+ */
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+@implementation CTAssetCheckmark
+
+#pragma mark Initializing a Check Mark Object
+
+/**
+ *  Designated Initializer
+ *
+ *  @return an initialized check mark object
+ */
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    
+    if (self)
+    {
+        self.userInteractionEnabled = NO;
+        self.isAccessibilityElement = NO;
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+
+#pragma mark Setting up Subviews
+
+/**
+ *  To setup subviews.
+ */
+- (void)setupViews
+{
+    UIImage *shadowImage = [UIImage ctassetsPickerImageNamed:@"CheckmarkShadow"];
+    UIImageView *shadowImageView = [[UIImageView alloc] initWithImage:shadowImage];
+    shadowImageView.userInteractionEnabled = NO;
+    self.shadowImageView = shadowImageView;
+    [self addSubview:self.shadowImageView];
+    
+    UIImage *checkmarkImage = [UIImage ctassetsPickerImageNamed:@"Checkmark"];
+    checkmarkImage = [checkmarkImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+    UIImageView *checkmarkImageView = [[UIImageView alloc] initWithImage:checkmarkImage];
+    checkmarkImageView.userInteractionEnabled = NO;
+    self.checkmarkImageView = checkmarkImageView;
+    [self addSubview:self.checkmarkImageView];
+}
+
+
+#pragma mark Customizing Appearance
+
+/**
+ *  To set margin of the check mark from specific edges.
+ *
+ *  @param margin The margin from the edges.
+ *  @param edgeX  The layout attribute respresents vertical edge that the check mark pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
+ *  @param edgeY  The layout attribute respresents horizontal edge that the check mark pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
+ */
+- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY
+{
+    NSAssert(edgeX == NSLayoutAttributeLeft || edgeX == NSLayoutAttributeRight, @"Vertical edge must be NSLayoutAttributeLeft or NSLayoutAttributeRight");
+    NSAssert(edgeY == NSLayoutAttributeTop || edgeY == NSLayoutAttributeBottom, @"Horizontal edge must be NSLayoutAttributeTop or NSLayoutAttributeBottom");
+    
+    [self.superview removeConstraints:@[self.verticalConstraint, self.horizontalConstraint]];
+    self.verticalConstraint   = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeX withInset:margin];
+    self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeY withInset:margin];
+}
+
+
+#pragma mark Triggering Auto Layout
+
+/**
+ *  Updates constraints of the check mark.
+ */
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        CGSize size = [UIImage ctassetsPickerImageNamed:@"CheckmarkShadow"].size;
+        
+        [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+            [self autoSetDimensionsToSize:size];
+        }];
+        
+        [self.shadowImageView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        [self.checkmarkImageView autoCenterInSuperview];
+        
+        self.verticalConstraint   = [self autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0];
+        self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:0];
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+@end

+ 54 - 0
Libraries external/CTAssetsPickerController/CTAssetCollectionViewCell.h

@@ -0,0 +1,54 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+#import "CTAssetThumbnailStacks.h"
+
+
+
+@interface CTAssetCollectionViewCell : UITableViewCell
+
+@property (nonatomic, strong, readonly, nonnull) CTAssetThumbnailStacks *thumbnailStacks;
+
+@property (nonatomic, weak, nullable) UIFont *titleFont UI_APPEARANCE_SELECTOR;
+@property (nonatomic, strong, nullable) UIColor *titleTextColor UI_APPEARANCE_SELECTOR;
+@property (nonatomic, strong, nullable) UIColor *selectedTitleTextColor UI_APPEARANCE_SELECTOR;
+
+@property (nonatomic, weak, nullable) UIFont *countFont UI_APPEARANCE_SELECTOR;
+@property (nonatomic, strong, nullable) UIColor *countTextColor UI_APPEARANCE_SELECTOR;
+@property (nonatomic, strong, nullable) UIColor *selectedCountTextColor UI_APPEARANCE_SELECTOR;
+
+@property (nonatomic, strong, nullable) UIColor *accessoryColor UI_APPEARANCE_SELECTOR;
+@property (nonatomic, strong, nullable) UIColor *selectedAccessoryColor UI_APPEARANCE_SELECTOR;
+
+@property (nonatomic, weak, nullable) UIColor *selectedBackgroundColor UI_APPEARANCE_SELECTOR;
+
+
+- (instancetype)initWithThumbnailSize:(CGSize)size reuseIdentifier:(nullable NSString *)reuseIdentifier;
+- (void)bind:(nonnull PHAssetCollection *)collection count:(NSUInteger)count;
+
+@end

+ 319 - 0
Libraries external/CTAssetsPickerController/CTAssetCollectionViewCell.m

@@ -0,0 +1,319 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetCollectionViewCell.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+#import "NSNumberFormatter+CTAssetsPickerController.h"
+
+
+
+@interface CTAssetCollectionViewCell ()
+
+@property (nonatomic, assign) CGSize thumbnailSize;
+
+@property (nonatomic, strong) CTAssetThumbnailStacks *thumbnailStacks;
+@property (nonatomic, strong) UIView *labelsView;
+@property (nonatomic, strong) UILabel *titleLabel;
+@property (nonatomic, strong) UILabel *countLabel;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@property (nonatomic, strong) PHAssetCollection *collection;
+@property (nonatomic, assign) NSUInteger count;
+
+@end
+
+
+
+
+
+@implementation CTAssetCollectionViewCell
+
+- (instancetype)initWithThumbnailSize:(CGSize)size reuseIdentifier:(NSString *)reuseIdentifier;
+{
+    if (self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier])
+    {
+        _thumbnailSize = size;
+        
+        _titleTextColor         = CTAssetCollectionViewCellTitleTextColor;
+        _selectedTitleTextColor = CTAssetCollectionViewCellTitleTextColor;
+        _countTextColor         = CTAssetCollectionViewCellCountTextColor;
+        _selectedCountTextColor = CTAssetCollectionViewCellCountTextColor;
+
+        _accessoryColor         = CTAssetCollectionViewCellAccessoryColor;
+        _selectedAccessoryColor = CTAssetCollectionViewCellAccessoryColor;
+
+        self.opaque                             = YES;
+        self.isAccessibilityElement             = YES;
+        self.textLabel.backgroundColor          = self.backgroundColor;
+        self.detailTextLabel.backgroundColor    = self.backgroundColor;
+        self.accessoryType                      = UITableViewCellAccessoryNone;
+        
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    CTAssetThumbnailStacks *thumbnailStacks = [CTAssetThumbnailStacks newAutoLayoutView];
+    thumbnailStacks.thumbnailSize = self.thumbnailSize;
+    self.thumbnailStacks = thumbnailStacks;
+    
+    UILabel *titleLabel = [UILabel newAutoLayoutView];
+    titleLabel.font = CTAssetCollectionViewCellTitleFont;
+    titleLabel.textColor = self.titleTextColor;
+    self.titleLabel = titleLabel;
+    
+    UILabel *countLabel = [UILabel newAutoLayoutView];
+    countLabel.font = CTAssetCollectionViewCellCountFont;
+    countLabel.textColor = self.countTextColor;
+    self.countLabel = countLabel;
+    
+    UIView *labelsView = [UIView newAutoLayoutView];
+    [labelsView addSubview:self.titleLabel];
+    [labelsView addSubview:self.countLabel];
+    self.labelsView = labelsView;
+    
+    [self.contentView addSubview:self.thumbnailStacks];
+    [self.contentView addSubview:self.labelsView];
+    
+    UIImage *accessory = [UIImage ctassetsPickerImageNamed:@"DisclosureArrow"];
+    accessory = [accessory imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+    UIImageView *accessoryView = [[UIImageView alloc] initWithImage:accessory];
+    accessoryView.tintColor = self.accessoryColor;
+    self.accessoryView = accessoryView;
+}
+
+- (void)setupPlaceholderImage
+{
+    NSString *imageName = [self placeHolderImageNameOfCollectionSubtype:self.collection.assetCollectionSubtype];
+    UIImage *image = [UIImage ctassetsPickerImageNamed:imageName];
+    image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+    
+    for (CTAssetThumbnailView *thumbnailView in self.thumbnailStacks.thumbnailViews)
+    {
+        [thumbnailView bind:nil assetCollection:nil];
+        thumbnailView.backgroundImage = image;
+    }
+}
+
+- (NSString *)placeHolderImageNameOfCollectionSubtype:(PHAssetCollectionSubtype)subtype
+{
+    if (subtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary)
+        return @"GridEmptyCameraRoll";
+    
+    else if (subtype == PHAssetCollectionSubtypeSmartAlbumAllHidden)
+        return @"GridHiddenAlbum";
+    
+    else if (subtype == PHAssetCollectionSubtypeAlbumCloudShared)
+        return @"GridEmptyAlbumShared";
+    
+    else
+        return @"GridEmptyAlbum";
+}
+
+
+#pragma mark - Apperance
+
+- (UIFont *)titleFont
+{
+    return self.titleLabel.font;
+}
+
+- (void)setTitleFont:(UIFont *)titleFont
+{
+    UIFont *font = (titleFont) ? titleFont : CTAssetCollectionViewCellTitleFont;
+    self.titleLabel.font = font;
+}
+
+- (void)setTitleTextColor:(UIColor *)titleTextColor
+{
+    UIColor *color = (titleTextColor) ? titleTextColor : CTAssetCollectionViewCellTitleTextColor;
+    _titleTextColor = color;
+}
+
+- (void)setSelectedTitleTextColor:(UIColor *)titleTextColor
+{
+    UIColor *color = (titleTextColor) ? titleTextColor : CTAssetCollectionViewCellTitleTextColor;
+    _selectedTitleTextColor = color;
+}
+
+- (UIFont *)countFont
+{
+    return self.countLabel.font;
+}
+
+- (void)setCountFont:(UIFont *)countFont
+{
+    UIFont *font = (countFont) ? countFont : CTAssetCollectionViewCellCountFont;
+    self.countLabel.font = font;
+}
+
+- (void)setCountTextColor:(UIColor *)countTextColor
+{
+    UIColor *color = (countTextColor) ? countTextColor : CTAssetCollectionViewCellCountTextColor;
+    _countTextColor = color;
+}
+
+- (void)setSelectedCountTextColor:(UIColor *)countTextColor
+{
+    UIColor *color = (countTextColor) ? countTextColor : CTAssetCollectionViewCellCountTextColor;
+    _selectedCountTextColor = color;
+}
+
+- (void)setAccessoryColor:(UIColor *)accessoryColor
+{
+    UIColor *color = (accessoryColor) ? accessoryColor : CTAssetCollectionViewCellAccessoryColor;
+    _accessoryColor = color;
+}
+
+- (void)setSelectedAccessoryColor:(UIColor *)accessoryColor
+{
+    UIColor *color = (accessoryColor) ? accessoryColor : CTAssetCollectionViewCellAccessoryColor;
+    _selectedAccessoryColor = color;
+}
+
+- (UIColor *)selectedBackgroundColor
+{
+    return self.selectedBackgroundView.backgroundColor;
+}
+
+- (void)setSelectedBackgroundColor:(UIColor *)selectedBackgroundColor
+{
+    if (!selectedBackgroundColor)
+        self.selectedBackgroundView = nil;
+    else
+    {
+        UIView *view = [UIView new];
+        view.backgroundColor = selectedBackgroundColor;
+        self.selectedBackgroundView = view;
+    }
+}
+
+
+#pragma mark - Override highlighted / selected
+
+- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
+{
+    [super setHighlighted:highlighted animated:animated];
+    [self.thumbnailStacks setHighlighted:highlighted];
+    
+    self.titleLabel.textColor = (highlighted) ? self.selectedTitleTextColor : self.titleTextColor;
+    self.countLabel.textColor = (highlighted) ? self.selectedCountTextColor : self.countTextColor;
+    self.accessoryView.tintColor = (highlighted) ? self.selectedAccessoryColor : self.accessoryColor;
+}
+
+- (void)setSelected:(BOOL)selected animated:(BOOL)animated
+{
+    [super setSelected:selected animated:animated];
+    [self.thumbnailStacks setHighlighted:selected];
+    
+    self.titleLabel.textColor = (selected) ? self.selectedTitleTextColor : self.titleTextColor;
+    self.countLabel.textColor = (selected) ? self.selectedCountTextColor : self.countTextColor;
+    self.accessoryView.tintColor = (selected) ? self.selectedAccessoryColor : self.accessoryColor;
+}
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        CGSize size = self.thumbnailSize;
+        CGFloat top = self.thumbnailStacks.edgeInsets.top;
+        size.height += top;
+        
+        [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+            [self.thumbnailStacks autoSetDimensionsToSize:size];
+        }];
+                
+        [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
+            [self.thumbnailStacks autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeTrailing];
+        }];
+        
+        [self.labelsView autoAlignAxisToSuperviewAxis:ALAxisHorizontal];
+        [self.labelsView autoPinEdge:ALEdgeLeading
+                              toEdge:ALEdgeTrailing
+                              ofView:self.thumbnailStacks
+                          withOffset:self.labelsView.layoutMargins.left
+                            relation:NSLayoutRelationGreaterThanOrEqual];
+        
+        [self.titleLabel autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeBottom];
+        [self.countLabel autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeTop];
+        [self.countLabel autoPinEdge:ALEdgeTop
+                              toEdge:ALEdgeBottom
+                              ofView:self.titleLabel
+                          withOffset:self.countLabel.layoutMargins.top
+                            relation:NSLayoutRelationGreaterThanOrEqual];
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+
+#pragma mark - Bind asset collection
+
+- (void)bind:(PHAssetCollection *)collection count:(NSUInteger)count
+{
+    self.collection = collection;
+    self.count      = count;
+    
+    [self setupPlaceholderImage];
+
+    self.titleLabel.text = collection.localizedTitle;
+    
+    if (count != NSNotFound)
+    {
+        NSNumberFormatter *nf = [NSNumberFormatter new];
+        self.countLabel.text = [nf ctassetsPickerStringFromAssetsCount:count];
+    }
+    
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];
+}
+
+
+#pragma mark - Accessibility label
+
+- (NSString *)accessibilityLabel
+{
+    NSString *title = self.titleLabel.text;
+    NSString *count = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Photos", nil), self.countLabel.text];
+    
+    NSArray *labels = @[title, count];
+    return [labels componentsJoinedByString:@","];
+}
+
+@end

+ 33 - 0
Libraries external/CTAssetsPickerController/CTAssetCollectionViewController.h

@@ -0,0 +1,33 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetCollectionViewController : UITableViewController
+
+- (void)reloadUserInterface;
+
+@end

+ 564 - 0
Libraries external/CTAssetsPickerController/CTAssetCollectionViewController.m

@@ -0,0 +1,564 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsPickerController.h"
+#import "CTAssetsPickerController+Internal.h"
+#import "CTAssetCollectionViewController.h"
+#import "CTAssetCollectionViewCell.h"
+#import "CTAssetsGridViewController.h"
+#import "PHAssetCollection+CTAssetsPickerController.h"
+#import "PHAsset+CTAssetsPickerController.h"
+#import "PHImageManager+CTAssetsPickerController.h"
+#import "NSBundle+CTAssetsPickerController.h"
+
+
+
+
+
+@interface CTAssetCollectionViewController()
+<PHPhotoLibraryChangeObserver, CTAssetsGridViewControllerDelegate>
+
+@property (nonatomic, weak) CTAssetsPickerController *picker;
+
+@property (nonatomic, strong) UIBarButtonItem *cancelButton;
+@property (nonatomic, strong) UIBarButtonItem *doneButton;
+
+@property (nonatomic, copy) NSArray *fetchResults;
+@property (nonatomic, copy) NSArray *assetCollections;
+@property (nonatomic, strong) PHCachingImageManager *imageManager;
+
+@property (nonatomic, strong) PHAssetCollection *defaultAssetCollection;
+@property (nonatomic, assign) BOOL didShowDefaultAssetCollection;
+@property (nonatomic, assign) BOOL didSelectDefaultAssetCollection;
+
+@end
+
+
+
+
+
+@implementation CTAssetCollectionViewController
+
+- (instancetype)init
+{
+    if (self = [super initWithStyle:UITableViewStylePlain])
+    {
+        _imageManager = [PHCachingImageManager new];
+        [self addNotificationObserver];
+    }
+    
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    [self setupViews];
+    [self localize];
+    [self setupDefaultAssetCollection];
+    [self setupFetchResults];
+    [self registerChangeObserver];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    [super viewWillAppear:animated];
+    [self setupButtons];
+    [self updateTitle:self.picker.selectedAssets];
+    [self updateButton:self.picker.selectedAssets];
+    [self selectDefaultAssetCollection];
+}
+
+- (void)dealloc
+{
+    [self unregisterChangeObserver];
+    [self removeNotificationObserver];
+}
+
+
+#pragma mark - Reload user interface
+
+- (void)reloadUserInterface
+{
+    [self setupViews];
+    [self setupButtons];
+    [self localize];
+    [self setupDefaultAssetCollection];
+    [self setupFetchResults];
+}
+
+
+#pragma mark - Accessors
+
+- (CTAssetsPickerController *)picker
+{
+    return (CTAssetsPickerController *)self.splitViewController.parentViewController;
+}
+
+- (NSIndexPath *)indexPathForAssetCollection:(PHAssetCollection *)assetCollection
+{
+    NSInteger row = [self.assetCollections indexOfObject:assetCollection];
+
+    if (row != NSNotFound)
+        return [NSIndexPath indexPathForRow:row inSection:0];
+    else
+        return nil;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    self.tableView.rowHeight = UITableViewAutomaticDimension;
+    
+    self.tableView.estimatedRowHeight =
+    self.picker.assetCollectionThumbnailSize.height +
+    self.tableView.layoutMargins.top +
+    self.tableView.layoutMargins.bottom;
+
+    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
+}
+
+- (void)setupButtons
+{
+    if (self.doneButton == nil)
+    {
+        NSString *title = (self.picker.doneButtonTitle) ?
+        self.picker.doneButtonTitle : CTAssetsPickerLocalizedString(@"Done", nil);
+        
+        self.doneButton =
+        [[UIBarButtonItem alloc] initWithTitle:title
+                                         style:UIBarButtonItemStyleDone
+                                        target:self.picker
+                                        action:@selector(finishPickingAssets:)];
+    }
+    
+    if (self.cancelButton == nil)
+    {
+        self.cancelButton =
+        [[UIBarButtonItem alloc] initWithTitle:CTAssetsPickerLocalizedString(@"Cancel", nil)
+                                         style:UIBarButtonItemStylePlain
+                                        target:self.picker
+                                        action:@selector(dismiss:)];
+    }
+}
+
+- (void)localize
+{
+    [self resetTitle];
+}
+
+- (void)setupFetchResults
+{
+    NSMutableArray *fetchResults = [NSMutableArray new];
+
+    for (NSNumber *subtypeNumber in self.picker.assetCollectionSubtypes)
+    {
+        PHAssetCollectionType type       = [PHAssetCollection ctassetPickerAssetCollectionTypeOfSubtype:subtypeNumber.integerValue];
+        PHAssetCollectionSubtype subtype = subtypeNumber.integerValue;
+        
+        PHFetchResult *fetchResult =
+        [PHAssetCollection fetchAssetCollectionsWithType:type
+                                                 subtype:subtype
+                                                 options:self.picker.assetCollectionFetchOptions];
+        
+        [fetchResults addObject:fetchResult];
+    }
+    
+    self.fetchResults = [NSMutableArray arrayWithArray:fetchResults];
+    
+    [self updateAssetCollections];
+    [self reloadData];
+    [self showDefaultAssetCollection];
+}
+
+- (void)updateAssetCollections
+{
+    NSMutableArray *assetCollections = [NSMutableArray new];
+
+    for (PHFetchResult *fetchResult in self.fetchResults)
+    {
+        for (PHAssetCollection *assetCollection in fetchResult)
+        {
+            BOOL showsAssetCollection = YES;
+            
+            if (!self.picker.showsEmptyAlbums)
+            {
+                PHFetchOptions *options = [PHFetchOptions new];
+                options.predicate = self.picker.assetsFetchOptions.predicate;
+                
+                if ([options respondsToSelector:@selector(setFetchLimit:)])
+                    options.fetchLimit = 1;
+                
+                NSInteger count = [assetCollection ctassetPikcerCountOfAssetsFetchedWithOptions:options];
+                
+                showsAssetCollection = (count > 0);
+            }
+            
+            if (showsAssetCollection)
+                [assetCollections addObject:assetCollection];
+        }
+    }
+
+    self.assetCollections = [NSMutableArray arrayWithArray:assetCollections];
+}
+
+- (void)setupDefaultAssetCollection
+{
+    if (!self.picker || self.picker.defaultAssetCollection == PHAssetCollectionSubtypeAny) {
+        self.defaultAssetCollection = nil;
+        return;
+    }
+    
+    PHAssetCollectionType type = [PHAssetCollection ctassetPickerAssetCollectionTypeOfSubtype:self.picker.defaultAssetCollection];
+    PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:type subtype:self.picker.defaultAssetCollection options:self.picker.assetCollectionFetchOptions];
+    
+    self.defaultAssetCollection = fetchResult.firstObject;
+}
+
+
+#pragma mark - Rotation
+
+- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
+{
+    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
+    
+    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
+        [self updateTitle:self.picker.selectedAssets];
+        [self updateButton:self.picker.selectedAssets];
+    } completion:nil];
+}
+
+#pragma mark - Notifications
+
+- (void)addNotificationObserver
+{
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+    
+    [center addObserver:self
+               selector:@selector(selectedAssetsChanged:)
+                   name:CTAssetsPickerSelectedAssetsDidChangeNotification
+                 object:nil];
+    
+    [center addObserver:self
+               selector:@selector(contentSizeCategoryChanged:)
+                   name:UIContentSizeCategoryDidChangeNotification
+                 object:nil];
+}
+
+- (void)removeNotificationObserver
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:CTAssetsPickerSelectedAssetsDidChangeNotification object:nil];
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
+}
+
+
+#pragma mark - Photo library change observer
+
+- (void)registerChangeObserver
+{
+    [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
+}
+
+- (void)unregisterChangeObserver
+{
+    [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
+}
+
+
+#pragma mark - Photo library changed
+
+- (void)photoLibraryDidChange:(PHChange *)changeInstance
+{
+    // Call might come on any background queue. Re-dispatch to the main queue to handle it.
+    dispatch_async(dispatch_get_main_queue(), ^{
+        
+        NSMutableArray *updatedFetchResults = nil;
+        
+        for (PHFetchResult *fetchResult in self.fetchResults)
+        {
+            PHFetchResultChangeDetails *changeDetails = [changeInstance changeDetailsForFetchResult:fetchResult];
+            
+            if (changeDetails)
+            {
+                if (!updatedFetchResults)
+                    updatedFetchResults = [self.fetchResults mutableCopy];
+                
+                updatedFetchResults[[self.fetchResults indexOfObject:fetchResult]] = changeDetails.fetchResultAfterChanges;
+            }
+        }
+        
+        if (updatedFetchResults)
+        {
+            self.fetchResults = updatedFetchResults;
+            [self updateAssetCollections];
+            [self reloadData];
+        }
+        
+    });
+}
+
+
+#pragma mark - Selected assets changed
+
+- (void)selectedAssetsChanged:(NSNotification *)notification
+{
+    NSArray *selectedAssets = (NSArray *)notification.object;
+    [self updateTitle:selectedAssets];
+    [self updateButton:selectedAssets];
+}
+
+- (void)updateTitle:(NSArray *)selectedAssets
+{
+    if ([self isTopViewController] && selectedAssets.count > 0)
+        self.title = self.picker.selectedAssetsString;
+    else
+        [self resetTitle];
+}
+
+- (void)updateButton:(NSArray *)selectedAssets
+{
+    self.navigationItem.leftBarButtonItem = (self.picker.showsCancelButton) ? self.cancelButton : nil;
+    self.navigationItem.rightBarButtonItem = [self isTopViewController] ? self.doneButton : nil;
+    
+    if (self.picker.alwaysEnableDoneButton)
+        self.navigationItem.rightBarButtonItem.enabled = YES;
+    else
+        self.navigationItem.rightBarButtonItem.enabled = (self.picker.selectedAssets.count > 0);
+}
+
+- (BOOL)isTopViewController
+{
+    UIViewController *vc = self.splitViewController.viewControllers.lastObject;
+    
+    if ([vc isMemberOfClass:[UINavigationController class]])
+        return (self == ((UINavigationController *)vc).topViewController);
+    else
+        return NO;
+}
+
+- (void)resetTitle
+{
+    if (!self.picker.title)
+        self.title = CTAssetsPickerLocalizedString(@"Photos", nil);
+    else
+        self.title = self.picker.title;
+}
+
+
+#pragma mark - Content size category changed
+
+- (void)contentSizeCategoryChanged:(NSNotification *)notification
+{
+    [self reloadData];
+}
+
+
+#pragma mark - Reload data
+
+- (void)reloadData
+{
+    if (self.assetCollections.count > 0)
+        [self.tableView reloadData];
+    else
+        [self.picker showNoAssets];
+}
+
+
+#pragma mark - Table view data source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return self.assetCollections.count;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAssetCollection *collection = self.assetCollections[indexPath.row];
+    NSUInteger count;
+    
+    if (self.picker.showsNumberOfAssets)
+        count = [collection ctassetPikcerCountOfAssetsFetchedWithOptions:self.picker.assetsFetchOptions];
+    else
+        count = NSNotFound;
+    
+    static NSString *cellIdentifier = @"CellIdentifier";
+    
+    CTAssetCollectionViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
+    
+    if (cell == nil)
+        cell = [[CTAssetCollectionViewCell alloc] initWithThumbnailSize:self.picker.assetCollectionThumbnailSize
+                                                            reuseIdentifier:cellIdentifier];
+    
+    [cell bind:collection count:count];
+    [self requestThumbnailsForCell:cell assetCollection:collection];
+    
+    return cell;
+}
+
+- (void)requestThumbnailsForCell:(CTAssetCollectionViewCell *)cell assetCollection:(PHAssetCollection *)collection
+{
+    NSUInteger count    = cell.thumbnailStacks.thumbnailViews.count;
+    NSArray *assets     = [self posterAssetsFromAssetCollection:collection count:count];
+    CGSize targetSize   = [self.picker imageSizeForContainerSize:self.picker.assetCollectionThumbnailSize];
+    
+    for (NSUInteger index = 0; index < count; index++)
+    {
+        CTAssetThumbnailView *thumbnailView = [cell.thumbnailStacks thumbnailAtIndex:index];
+        thumbnailView.hidden = (assets.count > 0) ? YES : NO;
+        
+        if (index < assets.count)
+        {
+            PHAsset *asset = assets[index];
+            [self.imageManager ctassetsPickerRequestImageForAsset:asset
+                                         targetSize:targetSize
+                                        contentMode:PHImageContentModeAspectFill
+                                            options:self.picker.thumbnailRequestOptions
+                                      resultHandler:^(UIImage *image, NSDictionary *info){
+                                          [thumbnailView setHidden:NO];
+                                          [thumbnailView bind:image assetCollection:collection];
+                                      }];
+        }
+    }
+}
+
+- (NSArray *)posterAssetsFromAssetCollection:(PHAssetCollection *)collection count:(NSUInteger)count;
+{
+    PHFetchOptions *options = [PHFetchOptions new];
+    options.predicate       = self.picker.assetsFetchOptions.predicate; // aligned specified predicate
+    options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
+    
+    PHFetchResult *result = [PHAsset fetchKeyAssetsInAssetCollection:collection options:options];
+    
+    NSUInteger location = 0;
+    NSUInteger length   = (result.count < count) ? result.count : count;
+    NSArray *assets     = [self itemsFromFetchResult:result range:NSMakeRange(location, length)];
+    
+    return assets;
+}
+
+- (NSArray *)itemsFromFetchResult:(PHFetchResult *)result range:(NSRange)range
+{
+    if (result.count == 0)
+        return nil;
+    
+    NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
+    NSArray *array = [result objectsAtIndexes:indexSet];
+    
+    return array;
+}
+
+
+#pragma mark - Table view delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAssetCollection *collection = self.assetCollections[indexPath.row];
+    
+    CTAssetsGridViewController *vc = [CTAssetsGridViewController new];
+    vc.title = self.picker.selectedAssetsString ? : collection.localizedTitle;
+    vc.assetCollection = collection;
+    vc.delegate = self;
+    
+    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
+    nav.delegate = (id<UINavigationControllerDelegate>)self.picker;
+    
+    [self.picker setShouldCollapseDetailViewController:NO];
+    [self.splitViewController showDetailViewController:nav sender:nil];
+}
+
+
+#pragma mark - Show / select default asset collection
+
+- (void)showDefaultAssetCollection
+{
+    if (self.defaultAssetCollection && !self.didShowDefaultAssetCollection)
+    {
+        CTAssetsGridViewController *vc = [CTAssetsGridViewController new];
+        vc.title = self.picker.selectedAssetsString ? : self.defaultAssetCollection.localizedTitle;
+        vc.assetCollection = self.defaultAssetCollection;
+        vc.delegate = self;
+        
+        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
+        nav.delegate = (id<UINavigationControllerDelegate>)self.picker;
+        
+        [self.picker setShouldCollapseDetailViewController:(self.picker.modalPresentationStyle == UIModalPresentationFormSheet)];
+        [self.splitViewController showDetailViewController:nav sender:nil];
+
+        NSIndexPath *indexPath = [self indexPathForAssetCollection:self.defaultAssetCollection];
+        [self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
+        
+        self.didShowDefaultAssetCollection = YES;
+    }
+}
+
+- (void)selectDefaultAssetCollection
+{
+    if (self.defaultAssetCollection && !self.didSelectDefaultAssetCollection)
+    {
+        NSIndexPath *indexPath = [self indexPathForAssetCollection:self.defaultAssetCollection];
+        
+        if (indexPath)
+        {
+            [UIView animateWithDuration:0.0f
+                             animations:^{
+                                 [self.tableView selectRowAtIndexPath:indexPath
+                                                             animated:(!self.splitViewController.collapsed)
+                                                       scrollPosition:UITableViewScrollPositionTop];
+                         }
+                         completion:^(BOOL finished){
+                             // mimic clearsSelectionOnViewWillAppear
+                             if (finished && self.splitViewController.collapsed)
+                                 [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
+                         }];
+        }
+        
+        self.didSelectDefaultAssetCollection = YES;
+    }
+}
+
+
+#pragma mark - Grid view controller delegate
+
+- (void)assetsGridViewController:(CTAssetsGridViewController *)picker photoLibraryDidChangeForAssetCollection:(PHAssetCollection *)assetCollection
+{
+    NSIndexPath *indexPath = [self indexPathForAssetCollection:assetCollection];
+    
+    if (indexPath)
+    {
+        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
+        [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
+    }
+}
+
+@end

+ 45 - 0
Libraries external/CTAssetsPickerController/CTAssetItemViewController.h

@@ -0,0 +1,45 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface CTAssetItemViewController : UIViewController
+
+@property (nonatomic, assign) BOOL allowsSelection;
+@property (nonatomic, strong, readonly) PHAsset *asset;
+@property (nonatomic, strong, readonly, nullable) UIImage *image;
+
++ (CTAssetItemViewController *)assetItemViewControllerForAsset:(PHAsset *)asset;
+
+- (void)playAsset:(nullable id)sender;
+- (void)pauseAsset:(nullable id)sender;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 422 - 0
Libraries external/CTAssetsPickerController/CTAssetItemViewController.m

@@ -0,0 +1,422 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerController.h"
+#import "CTAssetItemViewController.h"
+#import "CTAssetScrollView.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "PHAsset+CTAssetsPickerController.h"
+#import "PHImageManager+CTAssetsPickerController.h"
+
+
+
+
+@interface CTAssetItemViewController ()
+
+@property (nonatomic, weak) CTAssetsPickerController *picker;
+
+@property (nonatomic, strong) PHAsset *asset;
+@property (nonatomic, strong) UIImage *image;
+
+@property (nonatomic, strong) PHImageManager *imageManager;
+@property (nonatomic, assign) PHImageRequestID imageRequestID;
+@property (nonatomic, assign) PHImageRequestID playerItemRequestID;
+@property (nonatomic, strong) CTAssetScrollView *scrollView;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+
+
+@implementation CTAssetItemViewController
+
++ (CTAssetItemViewController *)assetItemViewControllerForAsset:(PHAsset *)asset
+{
+    return [[self alloc] initWithAsset:asset];
+}
+
+- (instancetype)initWithAsset:(PHAsset *)asset
+{
+    if (self = [super init])
+    {
+        _imageManager = [PHImageManager defaultManager];
+        self.asset = asset;
+        self.allowsSelection = NO;
+    }
+    
+    return self;
+}
+
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    [self setupViews];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    [super viewWillAppear:animated];
+    [self setupScrollViewButtons];
+    [self requestAssetImage];
+}
+
+- (void)viewWillDisappear:(BOOL)animated
+{
+    [super viewWillDisappear:animated];
+    [self pauseAsset:self.view];
+    [self cancelRequestAsset];
+}
+
+- (void)viewWillLayoutSubviews
+{
+    [super viewWillLayoutSubviews];
+    
+    [self.scrollView setNeedsUpdateConstraints];
+    [self.scrollView updateConstraintsIfNeeded];
+}
+
+- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
+{
+    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
+
+    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
+        [self.scrollView updateZoomScalesAndZoom:YES];
+    } completion:nil];
+}
+
+
+#pragma mark - Accessors
+
+- (CTAssetsPickerController *)picker
+{
+    return (CTAssetsPickerController *)self.splitViewController.parentViewController;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    CTAssetScrollView *scrollView = [CTAssetScrollView newAutoLayoutView];
+    scrollView.allowsSelection = self.allowsSelection;
+    
+    self.scrollView = scrollView;
+    [self.view addSubview:self.scrollView];
+    [self.view layoutIfNeeded];
+}
+
+- (void)setupScrollViewButtons
+{
+    CTAssetPlayButton *playButton = self.scrollView.playButton;
+    [playButton addTarget:self action:@selector(playAsset:) forControlEvents:UIControlEventTouchUpInside];
+    
+    CTAssetSelectionButton *selectionButton = self.scrollView.selectionButton;
+
+    selectionButton.enabled  = [self assetScrollView:self.scrollView shouldEnableAsset:self.asset];
+    selectionButton.selected = [self.picker.selectedAssets containsObject:self.asset];
+
+    [selectionButton addTarget:self action:@selector(selectionButtonTouchDown:) forControlEvents:UIControlEventTouchDown];
+    [selectionButton addTarget:self action:@selector(selectionButtonTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
+}
+
+
+#pragma mark - Cancel request
+
+- (void)cancelRequestAsset
+{
+    [self cancelRequestImage];
+    [self cancelRequestPlayerItem];
+}
+
+- (void)cancelRequestImage
+{
+    if (self.imageRequestID)
+    {
+        [self.scrollView setProgress:1];
+        [self.imageManager cancelImageRequest:self.imageRequestID];
+    }
+}
+
+- (void)cancelRequestPlayerItem
+{
+    if (self.playerItemRequestID)
+    {
+        [self.scrollView stopActivityAnimating];
+        [self.imageManager cancelImageRequest:self.playerItemRequestID];
+    }
+}
+
+
+#pragma mark - Request image
+
+- (void)requestAssetImage
+{
+    [self.scrollView setProgress:0];
+    
+    CGSize targetSize = [self targetImageSize];
+    PHImageRequestOptions *options = [self imageRequestOptions];
+    
+    self.imageRequestID =
+    [self.imageManager ctassetsPickerRequestImageForAsset:self.asset
+                                 targetSize:targetSize
+                                contentMode:PHImageContentModeAspectFit
+                                    options:options
+                              resultHandler:^(UIImage *image, NSDictionary *info) {
+
+                                  // this image is set for transition animation
+                                  self.image = image;
+                                  
+                                  dispatch_async(dispatch_get_main_queue(), ^{
+                                  
+                                      NSError *error = info[PHImageErrorKey];
+                                      
+                                      if (error)
+                                          [self showRequestImageError:error title:nil];
+                                      else
+                                          [self.scrollView bind:self.asset image:image requestInfo:info];
+                                  });
+                              }];
+}
+
+- (CGSize)targetImageSize
+{
+    UIScreen *screen    = UIScreen.mainScreen;
+    CGFloat scale       = screen.scale;
+    return CGSizeMake(CGRectGetWidth(screen.bounds) * scale, CGRectGetHeight(screen.bounds) * scale);
+}
+
+- (PHImageRequestOptions *)imageRequestOptions
+{
+    PHImageRequestOptions *options  = [PHImageRequestOptions new];
+    options.networkAccessAllowed    = YES;
+    options.progressHandler         = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self.scrollView setProgress:progress];
+        });
+    };
+    
+    return options;
+}
+
+
+#pragma mark - Request player item
+
+- (void)requestAssetPlayerItem:(id)sender
+{
+    [self.scrollView startActivityAnimating];
+    
+    PHVideoRequestOptions *options = [self videoRequestOptions];
+    
+    self.playerItemRequestID =
+    [self.imageManager requestPlayerItemForVideo:self.asset
+                                         options:options
+                                   resultHandler:^(AVPlayerItem *playerItem, NSDictionary *info) {
+                                       dispatch_async(dispatch_get_main_queue(), ^{
+                                           
+                                           NSError *error   = info[PHImageErrorKey];
+                                           NSString * title = CTAssetsPickerLocalizedString(@"Cannot Play Stream Video", nil);
+                                           
+                                           if (error)
+                                               [self showRequestVideoError:error title:title];
+                                           else
+                                               [self.scrollView bind:playerItem requestInfo:info];
+                                       });
+                                   }];
+}
+
+- (PHVideoRequestOptions *)videoRequestOptions
+{
+    PHVideoRequestOptions *options  = [PHVideoRequestOptions new];
+    options.networkAccessAllowed    = YES;
+    options.progressHandler         = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            //do nothing
+        });
+    };
+    
+    return options;
+}
+
+
+#pragma mark - Request error
+
+- (void)showRequestImageError:(NSError *)error title:(NSString *)title
+{
+    [self.scrollView setProgress:1];
+    [self showRequestError:error title:title];
+}
+
+- (void)showRequestVideoError:(NSError *)error title:(NSString *)title
+{
+    [self.scrollView stopActivityAnimating];
+    [self showRequestError:error title:title];
+}
+
+- (void)showRequestError:(NSError *)error title:(NSString *)title
+{
+    UIAlertController *alert =
+    [UIAlertController alertControllerWithTitle:title
+                                        message:error.localizedDescription
+                                 preferredStyle:UIAlertControllerStyleAlert];
+    
+    UIAlertAction *action =
+    [UIAlertAction actionWithTitle:CTAssetsPickerLocalizedString(@"OK", nil)
+                             style:UIAlertActionStyleDefault
+                           handler:nil];
+    
+    [alert addAction:action];
+    
+    [self presentViewController:alert animated:YES completion:nil];
+}
+
+
+#pragma mark - Playback
+
+- (void)playAsset:(id)sender
+{
+    if (!self.scrollView.player)
+        [self requestAssetPlayerItem:sender];
+    else
+        [self.scrollView playVideo];
+}
+
+- (void)pauseAsset:(id)sender
+{
+    if (!self.scrollView.player)
+        [self cancelRequestPlayerItem];
+    else
+        [self.scrollView pauseVideo];
+}
+
+
+#pragma mark - Selection
+
+- (void)selectionButtonTouchDown:(id)sender
+{
+    PHAsset *asset = self.asset;
+    CTAssetScrollView *scrollView = self.scrollView;
+    
+    if ([self assetScrollView:scrollView shouldHighlightAsset:asset])
+        [self assetScrollView:scrollView didHighlightAsset:asset];
+}
+
+- (void)selectionButtonTouchUpInside:(id)sender
+{
+    PHAsset *asset = self.asset;
+    CTAssetScrollView *scrollView = self.scrollView;
+    CTAssetSelectionButton *selectionButton = scrollView.selectionButton;
+    
+    
+    if (!selectionButton.selected)
+    {
+        if ([self assetScrollView:scrollView shouldSelectAsset:asset])
+        {
+            [self.picker selectAsset:asset];
+            [selectionButton setSelected:YES];
+            [self assetScrollView:scrollView didSelectAsset:asset];
+        }
+    }
+    
+    else
+    {
+        if ([self assetScrollView:scrollView shouldDeselectAsset:asset])
+        {
+            [self.picker deselectAsset:asset];
+            [selectionButton setSelected:NO];
+            [self assetScrollView:scrollView didDeselectAsset:asset];
+        }
+    }
+    
+    [self assetScrollView:self.scrollView didUnhighlightAsset:self.asset];
+}
+
+
+#pragma mark - Asset scrollView delegate
+
+- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldEnableAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldEnableAsset:)])
+        return [self.picker.delegate assetsPickerController:self.picker shouldEnableAsset:asset];
+    else
+        return YES;
+}
+
+- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldSelectAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldSelectAsset:)])
+        return [self.picker.delegate assetsPickerController:self.picker shouldSelectAsset:asset];
+    else
+        return YES;
+}
+
+- (void)assetScrollView:(CTAssetScrollView *)scrollView didSelectAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didSelectAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didSelectAsset:asset];
+}
+
+- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldDeselectAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldDeselectAsset:)])
+        return [self.picker.delegate assetsPickerController:self.picker shouldDeselectAsset:asset];
+    else
+        return YES;
+}
+
+- (void)assetScrollView:(CTAssetScrollView *)scrollView didDeselectAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didDeselectAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didDeselectAsset:asset];
+}
+
+- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldHighlightAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldHighlightAsset:)])
+        return [self.picker.delegate assetsPickerController:self.picker shouldHighlightAsset:asset];
+    else
+        return YES;
+}
+
+- (void)assetScrollView:(CTAssetScrollView *)scrollView didHighlightAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didHighlightAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didHighlightAsset:asset];
+}
+
+- (void)assetScrollView:(CTAssetScrollView *)scrollView didUnhighlightAsset:(PHAsset *)asset
+{
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didUnhighlightAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didUnhighlightAsset:asset];
+}
+
+
+@end

+ 31 - 0
Libraries external/CTAssetsPickerController/CTAssetPlayButton.h

@@ -0,0 +1,31 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetPlayButton : UIControl
+
+@end

+ 180 - 0
Libraries external/CTAssetsPickerController/CTAssetPlayButton.m

@@ -0,0 +1,180 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetPlayButton.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+
+
+
+@interface CTAssetPlayButton ()
+
+@property (nonatomic, strong) UIVisualEffectView *blurView;
+@property (nonatomic, strong) UIVisualEffectView *vibrancyView;
+@property (nonatomic, strong) UIView *vibrancyFill;
+
+@property (nonatomic, strong) UIView *highlightedView;
+@property (nonatomic, strong) UIView *colorView;
+
+@property (nonatomic, strong) UIImageView *glyphMask;
+@property (nonatomic, strong) UIImageView *buttonMask;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+
+
+@implementation CTAssetPlayButton
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    
+    if (self)
+    {
+        self.isAccessibilityElement = YES;
+        self.accessibilityTraits = UIAccessibilityTraitButton;
+        
+        [self setupViews];
+        [self localize];
+    }
+    
+    return self;
+}
+
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    [self setupEffectViews];
+    [self setupHightlightedView];
+    [self setupColorView];
+    [self setupMaskViews];
+}
+
+- (void)setupEffectViews
+{
+    // Blur effect
+    UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
+    UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
+    blurView.userInteractionEnabled = NO;
+    self.blurView = blurView;
+    
+    // Vibrancy effect
+    UIVibrancyEffect *vibrancyEffect = [UIVibrancyEffect effectForBlurEffect:blurEffect];
+    UIVisualEffectView *vibrancyView = [[UIVisualEffectView alloc] initWithEffect:vibrancyEffect];
+    vibrancyView.userInteractionEnabled = NO;
+    self.vibrancyView = vibrancyView;
+    
+    UIView *vibrancyFill = [UIView newAutoLayoutView];
+    vibrancyFill.backgroundColor = [UIColor whiteColor];
+    vibrancyFill.userInteractionEnabled = NO;
+    self.vibrancyFill = vibrancyFill;
+    
+    // Add fill to the vibrancy view
+    [vibrancyView.contentView addSubview:self.vibrancyFill];
+    [blurView.contentView addSubview:self.vibrancyView];
+    
+    [self addSubview:blurView];
+}
+
+- (void)setupHightlightedView
+{
+    UIView *highlightedView = [UIView newAutoLayoutView];
+    highlightedView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
+    highlightedView.userInteractionEnabled = NO;
+    highlightedView.hidden = YES;
+    self.highlightedView = highlightedView;
+    
+    [self addSubview:self.highlightedView];
+}
+
+- (void)setupColorView
+{
+    UIView *colorView = [UIView newAutoLayoutView];
+    colorView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.8];
+    colorView.userInteractionEnabled = NO;
+    self.colorView = colorView;
+    
+    [self addSubview:self.colorView];
+}
+
+- (void)setupMaskViews
+{
+    UIImage *glyphMaskImage = [UIImage ctassetsPickerImageNamed:@"VideoPlayGlyphMask"];
+    UIImageView *glyphMask = [[UIImageView alloc] initWithImage:glyphMaskImage];
+    glyphMask.userInteractionEnabled = NO;
+    self.colorView.maskView = glyphMask;
+    
+    UIImage *buttonMaskImage = [UIImage ctassetsPickerImageNamed:@"VideoPlayButtonMask"];
+    UIImageView *buttonMask = [[UIImageView alloc] initWithImage:buttonMaskImage];
+    buttonMask.userInteractionEnabled = NO;
+    self.maskView = buttonMask;
+}
+
+- (void)localize
+{
+    self.accessibilityLabel = CTAssetsPickerLocalizedString(@"Play", nil);
+}
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        CGSize size = [UIImage ctassetsPickerImageNamed:@"VideoPlayButtonMask"].size;
+        
+        [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+            [self autoSetDimensionsToSize:size];
+        }];
+        
+        [self.blurView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        [self.vibrancyView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        [self.vibrancyFill autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        [self.highlightedView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        [self.colorView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        
+        self.didSetupConstraints = YES;        
+    }
+    
+    [super updateConstraints];
+}
+
+
+#pragma mark - States
+
+- (void)setHighlighted:(BOOL)highlighted
+{
+    super.highlighted = highlighted;
+    self.highlightedView.hidden = !highlighted;
+}
+
+@end

+ 67 - 0
Libraries external/CTAssetsPickerController/CTAssetScrollView.h

@@ -0,0 +1,67 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import "CTAssetItemViewController.h"
+#import "CTAssetPlayButton.h"
+#import "CTAssetSelectionButton.h"
+
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSString * const CTAssetScrollViewDidTapNotification;
+extern NSString * const CTAssetScrollViewPlayerWillPlayNotification;
+extern NSString * const CTAssetScrollViewPlayerWillPauseNotification;
+
+
+@interface CTAssetScrollView : UIScrollView
+
+@property (nonatomic, assign) BOOL allowsSelection;
+
+@property (nonatomic, strong, readonly, nullable) UIImage *image;
+@property (nonatomic, strong, readonly, nullable) AVPlayer *player;
+
+@property (nonatomic, strong, readonly) UIImageView *imageView;
+@property (nonatomic, strong, readonly) CTAssetPlayButton *playButton;
+@property (nonatomic, strong, readonly) CTAssetSelectionButton *selectionButton;
+
+
+- (void)startActivityAnimating;
+- (void)stopActivityAnimating;
+
+- (void)setProgress:(CGFloat)progress;
+
+- (void)bind:(PHAsset *)asset image:(nullable UIImage *)image requestInfo:(nullable NSDictionary<NSString*, id> *)info;
+- (void)bind:(AVPlayerItem *)playerItem requestInfo:(nullable NSDictionary *)info;
+
+- (void)updateZoomScalesAndZoom:(BOOL)zoom;
+
+- (void)playVideo;
+- (void)pauseVideo;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 713 - 0
Libraries external/CTAssetsPickerController/CTAssetScrollView.m

@@ -0,0 +1,713 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetScrollView.h"
+#import "CTAssetPlayButton.h"
+#import "PHAsset+CTAssetsPickerController.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+
+
+
+
+NSString * const CTAssetScrollViewDidTapNotification = @"CTAssetScrollViewDidTapNotification";
+NSString * const CTAssetScrollViewPlayerWillPlayNotification = @"CTAssetScrollViewPlayerWillPlayNotification";
+NSString * const CTAssetScrollViewPlayerWillPauseNotification = @"CTAssetScrollViewPlayerWillPauseNotification";
+
+
+
+
+@interface CTAssetScrollView ()
+<UIScrollViewDelegate, UIGestureRecognizerDelegate>
+
+@property (nonatomic, strong) PHAsset *asset;
+@property (nonatomic, strong) UIImage *image;
+@property (nonatomic, strong) AVPlayer *player;
+
+@property (nonatomic, assign) BOOL didLoadPlayerItem;
+
+@property (nonatomic, assign) CGFloat perspectiveZoomScale;
+
+@property (nonatomic, strong) UIImageView *imageView;
+
+@property (nonatomic, strong) UIProgressView *progressView;
+@property (nonatomic, strong) UIActivityIndicatorView *activityView;
+@property (nonatomic, strong) CTAssetPlayButton *playButton;
+@property (nonatomic, strong) CTAssetSelectionButton *selectionButton;
+
+@property (nonatomic, assign) BOOL shouldUpdateConstraints;
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+
+@end
+
+
+
+
+
+@implementation CTAssetScrollView
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    
+    if (self)
+    {
+        _shouldUpdateConstraints            = YES;
+        self.allowsSelection                = NO;
+        self.showsVerticalScrollIndicator   = NO;
+        self.showsHorizontalScrollIndicator = NO;
+        self.bouncesZoom                    = YES;
+        self.decelerationRate               = UIScrollViewDecelerationRateFast;
+        self.delegate                       = self;
+        
+        [self setupViews];
+        [self addGestureRecognizers];
+    }
+    
+    return self;
+}
+
+- (void)dealloc
+{
+    [self removePlayerNotificationObserver];
+    [self removePlayerLoadedTimeRangesObserver];
+    [self removePlayerRateObserver];
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    UIImageView *imageView = [UIImageView new];
+    imageView.isAccessibilityElement    = YES;
+    imageView.accessibilityTraits       = UIAccessibilityTraitImage;
+    self.imageView = imageView;
+    [self addSubview:self.imageView];
+    
+    UIProgressView *progressView =
+    [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
+    self.progressView = progressView;
+    [self addSubview:self.progressView];
+    
+    UIActivityIndicatorView *activityView =
+    [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
+    self.activityView = activityView;
+    [self addSubview:self.activityView];
+    
+    CTAssetPlayButton *playButton = [CTAssetPlayButton newAutoLayoutView];
+    self.playButton = playButton;
+    [self addSubview:self.playButton];
+    
+    CTAssetSelectionButton *selectionButton = [CTAssetSelectionButton newAutoLayoutView];
+    self.selectionButton = selectionButton;
+    [self addSubview:self.selectionButton];
+}
+
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [self updateSelectionButtonIfNeeded];
+        [self autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        [self updateProgressConstraints];
+        [self updateActivityConstraints];
+        [self updateButtonsConstraints];
+        
+        self.didSetupConstraints = YES;
+    }
+
+    [self updateContentFrame];
+    [super updateConstraints];
+}
+
+- (void)updateSelectionButtonIfNeeded
+{
+    if (!self.allowsSelection)
+    {
+        [self.selectionButton removeFromSuperview];
+        self.selectionButton = nil;
+    }
+}
+
+- (void)updateProgressConstraints
+{
+    [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow forConstraints:^{
+        [self.progressView autoConstrainAttribute:ALAttributeLeading toAttribute:ALAttributeLeading ofView:self.superview withMultiplier:1 relation:NSLayoutRelationEqual];
+        [self.progressView autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.superview withMultiplier:1 relation:NSLayoutRelationEqual];
+        [self.progressView autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.superview withMultiplier:1 relation:NSLayoutRelationEqual];
+    }];
+    
+    [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
+        [self.progressView autoConstrainAttribute:ALAttributeLeading toAttribute:ALAttributeLeading ofView:self.imageView withMultiplier:1 relation:NSLayoutRelationGreaterThanOrEqual];
+        [self.progressView autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.imageView withMultiplier:1 relation:NSLayoutRelationLessThanOrEqual];
+        [self.progressView autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.imageView withMultiplier:1 relation:NSLayoutRelationLessThanOrEqual];
+    }];
+}
+
+- (void)updateActivityConstraints
+{
+    [self.activityView autoAlignAxis:ALAxisVertical toSameAxisOfView:self.superview];
+    [self.activityView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.superview];
+}
+
+- (void)updateButtonsConstraints
+{
+    [self.playButton autoAlignAxis:ALAxisVertical toSameAxisOfView:self.superview];
+    [self.playButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.superview];
+    
+    [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow forConstraints:^{
+        [self.selectionButton autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.superview withOffset:-self.layoutMargins.right relation:NSLayoutRelationEqual];
+        [self.selectionButton autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.superview withOffset:-self.layoutMargins.bottom relation:NSLayoutRelationEqual];
+    }];
+    
+    [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
+        [self.selectionButton autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.imageView withOffset:-self.layoutMargins.right relation:NSLayoutRelationLessThanOrEqual];
+        [self.selectionButton autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.imageView withOffset:-self.layoutMargins.bottom relation:NSLayoutRelationLessThanOrEqual];
+    }];
+}
+
+- (void)updateContentFrame
+{
+    CGSize boundsSize = self.bounds.size;
+    
+    CGFloat w = self.zoomScale * self.asset.pixelWidth;
+    CGFloat h = self.zoomScale * self.asset.pixelHeight;
+    
+    CGFloat dx = (boundsSize.width - w) / 2.0;
+    CGFloat dy = (boundsSize.height - h) / 2.0;
+
+    self.contentOffset = CGPointZero;
+    self.imageView.frame = CGRectMake(dx, dy, w, h);
+}
+
+
+
+#pragma mark - Start/stop loading animation
+
+- (void)startActivityAnimating
+{
+    [self.playButton setHidden:YES];
+    [self.selectionButton setHidden:YES];
+    [self.activityView startAnimating];
+    [self postPlayerWillPlayNotification];
+}
+
+- (void)stopActivityAnimating
+{
+    [self.playButton setHidden:NO];
+    [self.selectionButton setHidden:NO];
+    [self.activityView stopAnimating];
+    [self postPlayerWillPauseNotification];
+}
+
+
+#pragma mark - Set progress
+
+- (void)setProgress:(CGFloat)progress
+{
+#if !defined(CT_APP_EXTENSIONS)
+    [UIApplication sharedApplication].networkActivityIndicatorVisible = progress < 1;
+#endif
+    [self.progressView setProgress:progress animated:(progress < 1)];
+    self.progressView.hidden = progress == 1;
+}
+
+// To mimic image downloading progress
+// as PHImageRequestOptions does not work as expected
+- (void)mimicProgress
+{
+    CGFloat progress = self.progressView.progress;
+
+    if (progress < 0.95)
+    {
+        int lowerbound = progress * 100 + 1;
+        int upperbound = 95;
+        
+        int random = lowerbound + arc4random() % (upperbound - lowerbound);
+        CGFloat randomProgress = random / 100.0f;
+
+        [self setProgress:randomProgress];
+        
+        NSInteger randomDelay = 1 + arc4random() % (3 - 1);
+        [self performSelector:@selector(mimicProgress) withObject:nil afterDelay:randomDelay];
+    }
+}
+
+
+#pragma mark - asset size
+
+- (CGSize)assetSize
+{
+    return CGSizeMake(self.asset.pixelWidth, self.asset.pixelHeight);
+}
+
+
+#pragma mark - Bind asset image
+
+- (void)bind:(PHAsset *)asset image:(UIImage *)image requestInfo:(NSDictionary *)info
+{
+    self.asset = asset;
+    self.imageView.accessibilityLabel = asset.accessibilityLabel;    
+    self.playButton.hidden = [asset ctassetsPickerIsPhoto];
+    
+    BOOL isDegraded = [info[PHImageResultIsDegradedKey] boolValue];
+    
+    if (self.image == nil || !isDegraded)
+    {
+        BOOL zoom = (!self.image);
+        self.image = image;
+        self.imageView.image = image;
+        
+        if (isDegraded)
+            [self mimicProgress];
+        else
+            [self setProgress:1];
+
+        [self setNeedsUpdateConstraints];
+        [self updateConstraintsIfNeeded];        
+        [self updateZoomScalesAndZoom:zoom];
+    }
+}
+
+
+#pragma mark - Bind player item
+
+- (void)bind:(AVPlayerItem *)playerItem requestInfo:(NSDictionary *)info
+{
+    [self unbindPlayerItem];
+    
+    AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
+    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
+    playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
+
+    CALayer *layer = self.imageView.layer;
+    [layer addSublayer:playerLayer];
+    playerLayer.frame = layer.bounds;
+    
+    self.player = player;
+
+    [self addPlayerNotificationObserver];
+    [self addPlayerLoadedTimeRangesObserver];
+}
+
+- (void)unbindPlayerItem
+{
+    [self removePlayerNotificationObserver];
+    [self removePlayerLoadedTimeRangesObserver];
+
+    for (CALayer *layer in self.imageView.layer.sublayers)
+        [layer removeFromSuperlayer];
+    
+    self.player = nil;
+}
+
+
+
+#pragma mark - Upate zoom scales
+
+- (void)updateZoomScalesAndZoom:(BOOL)zoom
+{
+    if (!self.asset)
+        return;
+    
+    CGSize assetSize    = [self assetSize];
+    CGSize boundsSize   = self.bounds.size;
+    
+    CGFloat xScale = boundsSize.width / assetSize.width;    //scale needed to perfectly fit the image width-wise
+    CGFloat yScale = boundsSize.height / assetSize.height;  //scale needed to perfectly fit the image height-wise
+    
+    CGFloat minScale = MIN(xScale, yScale);
+    CGFloat maxScale = 3.0 * minScale;
+    
+    if ([self.asset ctassetsPickerIsVideo])
+    {
+        self.minimumZoomScale = minScale;
+        self.maximumZoomScale = minScale;
+    }
+    
+    else
+    {
+        self.minimumZoomScale = minScale;
+        self.maximumZoomScale = maxScale;
+    }
+    
+    // update perspective zoom scale
+    self.perspectiveZoomScale = (boundsSize.width > boundsSize.height) ? xScale : yScale;
+    
+    if (zoom)
+        [self zoomToInitialScale];
+}
+
+
+
+#pragma mark - Zoom
+
+- (void)zoomToInitialScale
+{
+    if ([self canPerspectiveZoom])
+        [self zoomToPerspectiveZoomScaleAnimated:NO];
+    else
+        [self zoomToMinimumZoomScaleAnimated:NO];
+}
+
+- (void)zoomToMinimumZoomScaleAnimated:(BOOL)animated
+{
+    [self setZoomScale:self.minimumZoomScale animated:animated];
+}
+
+- (void)zoomToMaximumZoomScaleWithGestureRecognizer:(UITapGestureRecognizer *)recognizer
+{
+    CGRect zoomRect = [self zoomRectWithScale:self.maximumZoomScale withCenter:[recognizer locationInView:recognizer.view]];
+
+    self.shouldUpdateConstraints = NO;
+    
+    [UIView animateWithDuration:0.3 animations:^{
+        [self zoomToRect:zoomRect animated:NO];
+        
+        CGRect frame = self.imageView.frame;
+        frame.origin.x = 0;
+        frame.origin.y = 0;
+
+        self.imageView.frame = frame;
+    }];
+}
+
+
+#pragma mark - Perspective zoom
+
+- (BOOL)canPerspectiveZoom
+{
+    CGSize assetSize    = [self assetSize];
+    CGSize boundsSize   = self.bounds.size;
+    
+    CGFloat assetRatio  = assetSize.width / assetSize.height;
+    CGFloat boundsRatio = boundsSize.width / boundsSize.height;
+    
+    // can perform perspective zoom when the difference of aspect ratios is smaller than 20%
+    return (fabs( (assetRatio - boundsRatio) / boundsRatio ) < 0.2f);
+}
+
+- (void)zoomToPerspectiveZoomScaleAnimated:(BOOL)animated;
+{
+    CGRect zoomRect = [self zoomRectWithScale:self.perspectiveZoomScale];
+    [self zoomToRect:zoomRect animated:animated];
+}
+
+- (CGRect)zoomRectWithScale:(CGFloat)scale
+{
+    CGSize targetSize;
+    targetSize.width    = self.bounds.size.width / scale;
+    targetSize.height   = self.bounds.size.height / scale;
+    
+    CGPoint targetOrigin;
+    targetOrigin.x      = (self.asset.pixelWidth - targetSize.width) / 2.0;
+    targetOrigin.y      = (self.asset.pixelHeight - targetSize.height) / 2.0;
+    
+    CGRect zoomRect;
+    zoomRect.origin = targetOrigin;
+    zoomRect.size   = targetSize;
+
+    return zoomRect;
+}
+
+
+#pragma mark - Zoom with gesture recognizer
+
+- (void)zoomWithGestureRecognizer:(UITapGestureRecognizer *)recognizer
+{
+    if (self.minimumZoomScale == self.maximumZoomScale)
+        return;
+    
+    if ([self canPerspectiveZoom])
+    {
+        if ((self.zoomScale >= self.minimumZoomScale && self.zoomScale < self.perspectiveZoomScale) ||
+            (self.zoomScale <= self.maximumZoomScale && self.zoomScale > self.perspectiveZoomScale))
+            [self zoomToPerspectiveZoomScaleAnimated:YES];
+        else
+            [self zoomToMaximumZoomScaleWithGestureRecognizer:recognizer];
+        
+        return;
+    }
+    
+    if (self.zoomScale < self.maximumZoomScale)
+        [self zoomToMaximumZoomScaleWithGestureRecognizer:recognizer];
+    else
+        [self zoomToMinimumZoomScaleAnimated:YES];
+}
+
+- (CGRect)zoomRectWithScale:(CGFloat)scale withCenter:(CGPoint)center
+{
+    center = [self.imageView convertPoint:center fromView:self];
+    
+    CGRect zoomRect;
+    
+    zoomRect.size.height = self.imageView.frame.size.height / scale;
+    zoomRect.size.width  = self.imageView.frame.size.width  / scale;
+    
+    zoomRect.origin.x    = center.x - ((zoomRect.size.width / 2.0));
+    zoomRect.origin.y    = center.y - ((zoomRect.size.height / 2.0));
+    
+    return zoomRect;
+}
+
+
+#pragma mark - Gesture recognizers
+
+- (void)addGestureRecognizers
+{
+    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapping:)];
+    UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapping:)];
+    
+    doubleTap.numberOfTapsRequired = 2.0;
+    [singleTap requireGestureRecognizerToFail:doubleTap];
+    
+    singleTap.delegate = self;
+    doubleTap.delegate = self;
+    
+    [self addGestureRecognizer:singleTap];
+    [self addGestureRecognizer:doubleTap];
+}
+
+
+#pragma mark - Handle tappings
+
+- (void)handleTapping:(UITapGestureRecognizer *)recognizer
+{
+    [[NSNotificationCenter defaultCenter] postNotificationName:CTAssetScrollViewDidTapNotification object:recognizer];
+    
+    if (recognizer.numberOfTapsRequired == 2)
+        [self zoomWithGestureRecognizer:recognizer];
+}
+
+
+#pragma mark - Scroll view delegate
+
+- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
+{
+    return self.imageView;
+}
+
+- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
+{
+    self.shouldUpdateConstraints = YES;
+}
+
+- (void)scrollViewDidZoom:(UIScrollView *)scrollView
+{
+    self.scrollEnabled = self.zoomScale != self.perspectiveZoomScale;
+    
+    if (self.shouldUpdateConstraints)
+    {
+        [self setNeedsUpdateConstraints];
+        [self updateConstraintsIfNeeded];
+    }
+}
+
+
+#pragma mark - Gesture recognizer delegate
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
+{
+    return !([touch.view isDescendantOfView:self.playButton] || [touch.view isDescendantOfView:self.selectionButton]);
+}
+
+
+#pragma mark - Notification observer
+
+- (void)addPlayerNotificationObserver
+{
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+    
+    [center addObserver:self
+               selector:@selector(applicationWillResignActive:)
+                   name:UIApplicationWillResignActiveNotification
+                 object:nil];
+}
+
+- (void)removePlayerNotificationObserver
+{
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+
+    [center removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
+}
+
+
+
+#pragma mark - Video player item key-value observer
+
+- (void)addPlayerLoadedTimeRangesObserver
+{
+    [self.player addObserver:self
+                  forKeyPath:@"currentItem.loadedTimeRanges"
+                     options:NSKeyValueObservingOptionNew
+                     context:nil];
+}
+
+- (void)removePlayerLoadedTimeRangesObserver
+{
+    @try {
+        [self.player removeObserver:self forKeyPath:@"currentItem.loadedTimeRanges"];
+    }
+    @catch (NSException *exception) {
+        // do nothing
+    }
+}
+
+- (void)addPlayerRateObserver
+{
+    [self.player addObserver:self
+                  forKeyPath:@"rate"
+                     options:NSKeyValueObservingOptionNew
+                     context:nil];
+}
+
+- (void)removePlayerRateObserver
+{
+    @try {
+        [self.player removeObserver:self forKeyPath:@"rate"];
+    }
+    @catch (NSException *exception) {
+        // do nothing
+    }    
+}
+
+
+#pragma mark - Video playback Key-Value changed
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
+{
+    if (object == self.player && [keyPath isEqual:@"currentItem.loadedTimeRanges"])
+    {
+        NSArray *timeRanges = change[NSKeyValueChangeNewKey];
+
+        if (timeRanges && timeRanges.count)
+        {
+            CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
+            
+            if (CMTIME_COMPARE_INLINE(timeRange.duration, ==, self.player.currentItem.duration))
+                [self performSelector:@selector(playerDidLoadItem:) withObject:object];
+        }
+    }
+    
+    if (object == self.player && [keyPath isEqual:@"rate"])
+    {
+        CGFloat rate = [[change valueForKey:NSKeyValueChangeNewKey] floatValue];
+        
+        if (rate > 0)
+            [self performSelector:@selector(playerDidPlay:) withObject:object];
+        
+        if (rate == 0)
+            [self performSelector:@selector(playerDidPause:) withObject:object];
+    }
+}
+
+
+
+#pragma mark - Notifications
+
+- (void)postPlayerWillPlayNotification
+{
+    [[NSNotificationCenter defaultCenter] postNotificationName:CTAssetScrollViewPlayerWillPlayNotification object:nil];
+}
+
+- (void)postPlayerWillPauseNotification
+{
+    [[NSNotificationCenter defaultCenter] postNotificationName:CTAssetScrollViewPlayerWillPauseNotification object:nil];
+}
+
+
+#pragma mark - Playback events
+
+- (void)applicationWillResignActive:(NSNotification *)notification
+{
+    [self pauseVideo];
+}
+
+
+- (void)playerDidPlay:(id)sender
+{
+    [self setProgress:1];
+    [self.playButton setHidden:YES];
+    [self.selectionButton setHidden:YES];
+    [self.activityView stopAnimating];
+}
+
+
+- (void)playerDidPause:(id)sender
+{
+    [self.playButton setHidden:NO];
+    [self.selectionButton setHidden:NO];
+}
+
+- (void)playerDidLoadItem:(id)sender
+{
+    if (!self.didLoadPlayerItem)
+    {
+        [self setDidLoadPlayerItem:YES];
+        [self addPlayerRateObserver];
+        
+        [self.activityView stopAnimating];
+        [self playVideo];
+    }
+}
+
+
+#pragma mark - Playback
+
+- (void)playVideo
+{
+    if (self.didLoadPlayerItem)
+    {
+        if (CMTIME_COMPARE_INLINE(self.player.currentTime, == , self.player.currentItem.duration))
+            [self.player seekToTime:kCMTimeZero];
+        
+        [self postPlayerWillPlayNotification];
+        [self.player play];
+    }
+}
+
+- (void)pauseVideo
+{
+    if (self.didLoadPlayerItem)
+    {
+        [self postPlayerWillPauseNotification];
+        [self.player pause];
+    }
+    else
+    {
+        [self stopActivityAnimating];
+        [self unbindPlayerItem];
+    }
+}
+
+
+@end

+ 31 - 0
Libraries external/CTAssetsPickerController/CTAssetSelectionButton.h

@@ -0,0 +1,31 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetSelectionButton : UIControl
+
+@end

+ 122 - 0
Libraries external/CTAssetsPickerController/CTAssetSelectionButton.m

@@ -0,0 +1,122 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetSelectionButton.h"
+#import "CTAssetCheckmark.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+
+
+
+@interface CTAssetSelectionButton ()
+
+@property (nonatomic, strong) CTAssetCheckmark *checkmark;
+@property (nonatomic, strong) UIImageView *backgroundView;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+
+
+@implementation CTAssetSelectionButton
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    
+    if (self)
+    {
+        self.isAccessibilityElement = YES;
+        self.accessibilityTraits = UIAccessibilityTraitButton;
+        
+        [self setupViews];
+        [self localize];
+    }
+    
+    return self;
+}
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    // Background
+    UIImage *backgroundImage = [UIImage ctassetsPickerImageNamed:@"CheckmarkUnselected"];
+    UIImageView *backgroundView = [[UIImageView alloc] initWithImage:backgroundImage];
+    backgroundView.userInteractionEnabled = NO;
+    self.backgroundView = backgroundView;
+    
+    [self addSubview:self.backgroundView];
+    
+    // Checkmark
+    CTAssetCheckmark *checkmark = [CTAssetCheckmark newAutoLayoutView];
+    checkmark.userInteractionEnabled = NO;
+    checkmark.hidden = YES;
+    self.checkmark = checkmark;
+    
+    [self addSubview:self.checkmark];
+}
+
+- (void)localize
+{
+    self.accessibilityLabel = CTAssetsPickerLocalizedString(@"Select", nil);
+}
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        CGSize size = [UIImage ctassetsPickerImageNamed:@"CheckmarkUnselected"].size;
+        
+        [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+            [self autoSetDimensionsToSize:size];
+        }];
+        
+        [self.backgroundView autoCenterInSuperview];
+        [self.checkmark autoCenterInSuperview];
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+#pragma mark - States
+
+- (void)setSelected:(BOOL)selected
+{
+    super.selected = selected;
+    self.checkmark.hidden = !selected;
+    
+    self.accessibilityLabel = (selected) ? CTAssetsPickerLocalizedString(@"Deselect", nil) : CTAssetsPickerLocalizedString(@"Select", nil);
+}
+
+
+@end

+ 99 - 0
Libraries external/CTAssetsPickerController/CTAssetSelectionLabel.h

@@ -0,0 +1,99 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+/**
+ *  The label to show selection index.
+ */
+@interface CTAssetSelectionLabel : UILabel
+
+
+#pragma mark Customizing Appearance
+
+/**
+ *  @name Customizing Appearance
+ */
+
+/**
+*  Determines whether the label is circular or not. *Deprecated*.
+*/
+@property (nonatomic, assign, getter=isCircular) BOOL circular UI_APPEARANCE_SELECTOR DEPRECATED_MSG_ATTRIBUTE("Use setCornerRadius: instead.");
+
+/**
+ *  The width of the label's border.
+ */
+@property (nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;
+
+/**
+ *  The color of the label's border.
+ */
+@property (nonatomic, weak) UIColor *borderColor UI_APPEARANCE_SELECTOR;
+
+/**
+ *  To set the size of label.
+ *
+ *  @param size The size of the label.
+ */
+- (void)setSize:(CGSize)size UI_APPEARANCE_SELECTOR;
+
+/**
+ *  To set the corner radius of label.
+ *
+ *  @param cornerRadius The radius to use when drawing rounded corners for the label’s background.
+ */
+- (void)setCornerRadius:(CGFloat)cornerRadius UI_APPEARANCE_SELECTOR;
+
+/**
+ *  To set margin of the label from the edges.
+ *
+ *  @param margin The margin from the edges.
+ *
+ *  @see setMargin:forVerticalEdge:horizontalEdge:
+ */
+- (void)setMargin:(CGFloat)margin UI_APPEARANCE_SELECTOR;
+
+/**
+ *  To set margin of the label from specific edges.
+ *
+ *  @param margin The margin from the edges.
+ *  @param edgeX  The layout attribute respresents vertical edge that the label pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
+ *  @param edgeY  The layout attribute respresents horizontal edge that the label pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
+ *
+ *  @see setMargin:
+ */
+- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY UI_APPEARANCE_SELECTOR;
+
+/**
+ *  To set the text attributes to display the label.
+ *
+ *  Currently only supports attributes `NSFontAttributeName`, `NSForegroundColorAttributeName` and `NSBackgroundColorAttributeName`.
+ *
+ *  @param textAttributes The text attributes used to display the label.
+ */
+- (void)setTextAttributes:(NSDictionary<NSString*, id> *)textAttributes UI_APPEARANCE_SELECTOR;
+
+@end

+ 219 - 0
Libraries external/CTAssetsPickerController/CTAssetSelectionLabel.m

@@ -0,0 +1,219 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetSelectionLabel.h"
+#import "CTAssetsPickerDefines.h"
+
+
+/**
+ *  The label to show selection index.
+ */
+@interface CTAssetSelectionLabel ()
+
+#pragma mark Managing Auto Layout
+
+/**
+ *  The constraints of the size of the label.
+ */
+@property (nonatomic, strong) NSArray *sizeConstraints;
+
+/**
+ *  The constraint for pinning the label to vertical edge.
+ */
+@property (nonatomic, strong) NSLayoutConstraint *verticalConstraint;
+
+/**
+ *  The constraint for pinning the label to horizontal edge.
+ */
+@property (nonatomic, strong) NSLayoutConstraint *horizontalConstraint;
+
+/**
+ *  Determines whether or not the constraints have been set up.
+ */
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+@implementation CTAssetSelectionLabel
+
+#pragma mark Initializing a Label Object
+
+/**
+ *  Designated Initializer
+ *
+ *  @return an initialized label object
+ */
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    
+    if (self)
+    {
+        self.textAlignment = NSTextAlignmentCenter;
+        self.font = CTAssetLabelFont;
+        self.textColor = CTAssetLabelTextColor;
+        self.backgroundColor = CTAssetLabelBackgroundColor;
+        self.layer.borderColor = CTAssetLabelBorderColor.CGColor;
+        self.layer.masksToBounds = YES;
+        self.isAccessibilityElement = NO;
+    }
+    
+    return self;
+}
+
+
+#pragma mark Customizing Appearance
+
+/**
+ *  The width of the label's border
+ */
+- (CGFloat)borderWidth
+{
+    return self.layer.borderWidth;
+}
+
+- (void)setBorderWidth:(CGFloat)borderWidth
+{
+    self.layer.borderWidth = borderWidth;
+}
+
+/**
+ *  The color of the label's border
+ */
+
+- (UIColor *)borderColor
+{
+    return [UIColor colorWithCGColor:self.layer.borderColor];
+}
+
+- (void)setBorderColor:(UIColor *)borderColor
+{
+    UIColor *color = (borderColor) ? borderColor : CTAssetLabelBorderColor;
+    self.layer.borderColor = color.CGColor;
+}
+
+/**
+ *  To set the size of label.
+ *
+ *  @param size The size of the label.
+ */
+- (void)setSize:(CGSize)size
+{
+    if (CGSizeEqualToSize(size, CGSizeZero)) {
+        size = CTAssetLabelSize;
+    }
+
+    [self removeConstraints:self.sizeConstraints];
+    
+    [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+        self.sizeConstraints = [self autoSetDimensionsToSize:size];
+    }];
+}
+
+/**
+ *  To set the size of label.
+ *
+ *  @param cornerRadius The radius to use when drawing rounded corners for the label’s background.
+ */
+- (void)setCornerRadius:(CGFloat)cornerRadius
+{
+    self.layer.cornerRadius = cornerRadius;
+}
+
+/**
+ *  To set margin of the label from the edges.
+ *
+ *  @param margin The margin from the edges.
+ *
+ *  @see setMargin:forVerticalEdge:horizontalEdge:
+ */
+- (void)setMargin:(CGFloat)margin
+{
+    [self setMargin:margin forVerticalEdge:NSLayoutAttributeRight horizontalEdge:NSLayoutAttributeBottom];
+}
+
+/**
+ *  To set margin of the label from specific edges.
+ *
+ *  @param margin The margin from the edges.
+ *  @param edgeX  The layout attribute respresents vertical edge that the label pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
+ *  @param edgeY  The layout attribute respresents horizontal edge that the label pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
+ *
+ *  @see setMargin:
+ */
+- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY
+{
+    NSAssert(edgeX == NSLayoutAttributeLeft || edgeX == NSLayoutAttributeRight,
+             @"Vertical edge must be NSLayoutAttributeLeft or NSLayoutAttributeRight");
+
+    NSAssert(edgeY == NSLayoutAttributeTop || edgeY == NSLayoutAttributeBottom,
+             @"Horizontal edge must be NSLayoutAttributeTop or NSLayoutAttributeBottom");
+
+    [self.superview removeConstraints:@[self.verticalConstraint, self.horizontalConstraint]];
+    self.verticalConstraint   = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeX withInset:margin];
+    self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeY withInset:margin];
+}
+
+/**
+ *  To set the text attributes to display the label.
+ *
+ *  Currently only supports attributes `NSFontAttributeName`, `NSForegroundColorAttributeName` and `NSBackgroundColorAttributeName`.
+ *
+ *  @param textAttributes The text attributes used to display the label.
+ */
+- (void)setTextAttributes:(NSDictionary *)textAttributes
+{
+    self.font  = (textAttributes) ? textAttributes[NSFontAttributeName] : CTAssetLabelFont;
+    self.textColor = (textAttributes) ? textAttributes[NSForegroundColorAttributeName] : CTAssetLabelTextColor;
+    self.backgroundColor = (textAttributes) ? textAttributes[NSBackgroundColorAttributeName] : CTAssetLabelBackgroundColor;
+}
+
+
+#pragma mark Triggering Auto Layout
+
+/**
+ *  Updates constraints of the label.
+ */
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+            self.sizeConstraints = [self autoSetDimensionsToSize:CTAssetLabelSize];
+        }];
+
+        self.verticalConstraint   = [self autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:0];
+        self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0];
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+@end

+ 36 - 0
Libraries external/CTAssetsPickerController/CTAssetThumbnailOverlay.h

@@ -0,0 +1,36 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+
+
+@interface CTAssetThumbnailOverlay : UIView
+
+- (void)bind:(nullable PHAsset *)asset duration:(nullable NSString *)duration;
+- (void)bind:(nullable PHAssetCollection *)assetCollection;
+
+@end

+ 156 - 0
Libraries external/CTAssetsPickerController/CTAssetThumbnailOverlay.m

@@ -0,0 +1,156 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetThumbnailOverlay.h"
+#import "UIImage+CTAssetsPickerController.h"
+#import "PHAsset+CTAssetsPickerController.h"
+#import "PHAssetCollection+CTAssetsPickerController.h"
+
+
+
+@interface CTAssetThumbnailOverlay ()
+
+@property (nonatomic, strong) UIImageView *gradient;
+@property (nonatomic, strong) UIImageView *badge;
+@property (nonatomic, strong) UILabel *duration;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+@implementation CTAssetThumbnailOverlay
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        self.opaque                 = NO;
+        self.clipsToBounds          = YES;
+        self.isAccessibilityElement = NO;
+        
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+
+#pragma markt - Setup
+
+- (void)setupViews
+{
+    UIImageView *gradient = [UIImageView newAutoLayoutView];
+    gradient.image = [UIImage ctassetsPickerImageNamed:@"GridGradient"];
+    self.gradient = gradient;
+    
+    [self addSubview:self.gradient];
+    
+    UIImageView *badge = [UIImageView newAutoLayoutView];
+    badge.tintColor = [UIColor whiteColor];
+    self.badge = badge;
+    
+    [self addSubview:self.badge];
+    
+    UILabel *duration = [UILabel newAutoLayoutView];
+    duration.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2];
+    duration.textColor = [UIColor whiteColor];
+    duration.lineBreakMode = NSLineBreakByTruncatingTail;
+    duration.layoutMargins = UIEdgeInsetsMake(2.5, 2.5, 2.5, 2.5);
+    self.duration = duration;
+    
+    [self addSubview:self.duration];
+}
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [self.gradient autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero excludingEdge:ALEdgeTop];
+        [self.gradient autoSetDimension:ALDimensionHeight toSize:self.gradient.image.size.height];
+        [self.badge autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.badge.layoutMargins.left];
+        [self.badge autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.badge.layoutMargins.bottom];
+        [self.duration autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:self.duration.layoutMargins.right];
+        [self.duration autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.duration.layoutMargins.bottom];
+        
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+
+#pragma - Bind asset and duration
+
+- (void)bind:(PHAsset *)asset duration:(NSString *)duration;
+{
+    self.badge.image = [asset badgeImage];
+    self.badge.layoutMargins = [self layoutMarginsForAsset:asset];
+    self.duration.text = duration;
+    
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];
+}
+
+- (UIEdgeInsets)layoutMarginsForAsset:(PHAsset *)asset
+{
+    if (asset.ctassetsPickerIsHighFrameRateVideo)
+        return UIEdgeInsetsMake(2.5, 2.5, 2.5, 2.5);
+    
+    else if (asset.ctassetsPickerIsTimelapseVideo)
+        return UIEdgeInsetsMake(2.5, 2.5, 2.5, 2.5);
+    
+    else if (asset.ctassetsPickerIsVideo)
+        return UIEdgeInsetsMake(4.5, 4.5, 4.5, 4.5);
+    
+    else
+        return UIEdgeInsetsZero;
+}
+
+
+#pragma - Bind asset collection
+
+- (void)bind:(PHAssetCollection *)assetCollection;
+{
+    self.badge.image = [assetCollection badgeImage];
+    self.badge.layoutMargins = [self layoutMarginsForAssetCollection:assetCollection];
+    self.duration.text = nil;
+    
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];    
+}
+
+- (UIEdgeInsets)layoutMarginsForAssetCollection:(PHAssetCollection *)assetCollection
+{
+    return UIEdgeInsetsMake(4.0, 4.0, 4.0, 4.0);
+}
+
+@end

+ 39 - 0
Libraries external/CTAssetsPickerController/CTAssetThumbnailStacks.h

@@ -0,0 +1,39 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import "CTAssetThumbnailView.h"
+
+@interface CTAssetThumbnailStacks : UIView
+
+@property (nonatomic, assign) CGSize thumbnailSize;
+@property (nonatomic, copy, readonly) NSArray<CTAssetThumbnailView*> *thumbnailViews;
+@property (nonatomic, assign, readonly) UIEdgeInsets edgeInsets;
+
+- (nonnull CTAssetThumbnailView *)thumbnailAtIndex:(NSUInteger)index;
+- (void)setHighlighted:(BOOL)highlighted;
+
+@end

+ 141 - 0
Libraries external/CTAssetsPickerController/CTAssetThumbnailStacks.m

@@ -0,0 +1,141 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetThumbnailStacks.h"
+#import "CTAssetThumbnailView.h"
+
+
+@interface CTAssetThumbnailStacks ()
+
+@property (nonatomic, copy) NSArray *thumbnailViews;
+@property (nonatomic, assign) UIEdgeInsets edgeInsets;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+@implementation CTAssetThumbnailStacks
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        _edgeInsets = UIEdgeInsetsMake(4.0, 0, 0, 0);
+        
+        self.opaque                 = YES;
+        self.clipsToBounds          = YES;
+        self.isAccessibilityElement = NO;
+        
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    NSMutableArray *thumbnailViews = [NSMutableArray new];
+    
+    for (NSUInteger index = 0; index < 3; index++)
+    {
+        CTAssetThumbnailView *thumbnailView = [CTAssetThumbnailView newAutoLayoutView];
+        thumbnailView.showsDuration = NO;
+        thumbnailView.layer.borderColor = [UIColor whiteColor].CGColor;
+        thumbnailView.layer.borderWidth = 0.5f;
+        
+        [thumbnailViews addObject:thumbnailView];
+        [self insertSubview:thumbnailView atIndex:0];
+    }
+    
+    self.thumbnailViews = [NSArray arrayWithArray:thumbnailViews];
+}
+
+
+#pragma markt - Setters
+
+- (void)setThumbnailSize:(CGSize)thumbnailSize
+{
+    _thumbnailSize = thumbnailSize;
+
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];
+}
+
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        for (NSUInteger index = 0; index < self.thumbnailViews.count; index++)
+        {
+            CTAssetThumbnailView *thumbnailView = [self thumbnailAtIndex:index];
+            
+            CGFloat delta = self.edgeInsets.top / 2;
+            
+            CGSize size = self.thumbnailSize;
+            size.width  -= index * delta * 2;
+            size.height -= index * delta * 2;
+            
+            CGFloat inset = (index * delta * 3);
+
+            [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+                [thumbnailView autoSetDimensionsToSize:size];
+            }];
+            
+            [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
+                [thumbnailView autoAlignAxisToSuperviewAxis:ALAxisVertical];
+                [thumbnailView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:inset];
+            }];
+        }
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+
+- (CTAssetThumbnailView *)thumbnailAtIndex:(NSUInteger)index
+{
+    return self.thumbnailViews[index];
+}
+
+- (void)setHighlighted:(BOOL)highlighted
+{
+    for (CTAssetThumbnailView *thumbnailView in self.thumbnailViews)
+        thumbnailView.backgroundColor = CTAssetsPikcerThumbnailBackgroundColor;
+}
+
+@end

+ 38 - 0
Libraries external/CTAssetsPickerController/CTAssetThumbnailView.h

@@ -0,0 +1,38 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+
+@interface CTAssetThumbnailView : UIView
+
+@property (nonatomic, assign) BOOL showsDuration;
+@property (nonatomic, strong, nullable) UIImage *backgroundImage;
+
+- (void)bind:(nullable UIImage *)image asset:(nullable PHAsset *)asset;
+- (void)bind:(nullable UIImage *)image assetCollection:(nullable PHAssetCollection *)assetCollection;
+
+@end

+ 196 - 0
Libraries external/CTAssetsPickerController/CTAssetThumbnailView.m

@@ -0,0 +1,196 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetThumbnailView.h"
+#import "CTAssetThumbnailOverlay.h"
+#import "PHAsset+CTAssetsPickerController.h"
+#import "NSDateFormatter+CTAssetsPickerController.h"
+
+
+
+@interface CTAssetThumbnailView ()
+
+@property (nonatomic, strong) CTAssetThumbnailOverlay *overlay;
+@property (nonatomic, strong) UIImageView *imageView;
+@property (nonatomic, strong) UIImageView *backgroundView;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+@implementation CTAssetThumbnailView
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        _showsDuration              = YES;
+        self.opaque                 = YES;
+        self.clipsToBounds          = YES;
+        self.isAccessibilityElement = NO;
+        
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+#pragma markt - Setup
+
+- (void)setupViews
+{
+    self.backgroundColor = CTAssetsPikcerThumbnailBackgroundColor;
+    
+    UIImageView *backgroundView = [UIImageView new];
+    backgroundView.contentMode = UIViewContentModeCenter;
+    backgroundView.tintColor = CTAssetsPikcerThumbnailTintColor;
+    self.backgroundView = backgroundView;
+    
+    UIImageView *imageView = [UIImageView new];
+    imageView.contentMode = UIViewContentModeScaleAspectFill;
+    self.imageView = imageView;
+    
+    [self addSubview:self.backgroundView];
+    [self addSubview:self.imageView];
+}
+
+
+#pragma markt - Setters
+
+- (void)setBackgroundImage:(UIImage *)backgroundImage
+{
+    _backgroundImage = backgroundImage;
+    self.backgroundView.image = backgroundImage;
+}
+
+
+#pragma markt - Override set bounds
+
+-(void)setBounds:(CGRect)bounds
+{
+    super.bounds = bounds;
+    
+    self.overlay.frame = bounds;
+    [self.overlay setNeedsDisplay];
+}
+
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [self.backgroundView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        [self.imageView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+#pragma - Bind asset and image
+
+- (void)bind:(UIImage *)image asset:(PHAsset *)asset;
+{
+    [self setupOverlayForAsset:asset];
+    
+    self.imageView.image = image;
+    self.backgroundView.hidden = (image != nil);
+    
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];
+}
+
+- (void)setupOverlayForAsset:(PHAsset *)asset
+{
+    if (asset.ctassetsPickerIsVideo)
+    {
+        if (!self.overlay) {
+            self.overlay = [[CTAssetThumbnailOverlay alloc] initWithFrame:self.bounds];
+            [self addSubview:self.overlay];
+        }
+        
+        NSString *duration = nil;
+
+        if (self.showsDuration)
+        {
+            NSDateFormatter *df = [NSDateFormatter new];
+            duration = [df ctassetsPickerStringFromTimeInterval:asset.duration];
+        }
+    
+        [self.overlay bind:asset duration:duration];
+    }
+    
+    else
+    {
+        [self.overlay removeFromSuperview];
+        self.overlay = nil;
+    }        
+}
+
+
+#pragma - Bind asset collection and image
+
+- (void)bind:(UIImage *)image assetCollection:(PHAssetCollection *)assetCollection;
+{
+    [self setupOverlayForAssetCollection:assetCollection];
+    
+    self.imageView.image = image;
+    self.backgroundView.hidden = (image != nil);
+    
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];
+}
+
+- (void)setupOverlayForAssetCollection:(PHAssetCollection *)assetCollection
+{
+    if (assetCollection.assetCollectionType == PHAssetCollectionTypeSmartAlbum &&
+        assetCollection.assetCollectionSubtype != PHAssetCollectionSubtypeSmartAlbumAllHidden)
+    {
+        if (!self.overlay) {
+            self.overlay = [[CTAssetThumbnailOverlay alloc] initWithFrame:self.bounds];
+            [self addSubview:self.overlay];
+        }
+        
+        [self.overlay bind:assetCollection];
+    }
+    
+    else
+    {
+        [self.overlay removeFromSuperview];
+        self.overlay = nil;
+    }
+}
+
+
+
+@end

+ 41 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridSelectedView.h

@@ -0,0 +1,41 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2013 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetsGridSelectedView : UIView
+
+@property (nonatomic, assign) BOOL showsSelectionIndex;
+@property (nonatomic, assign) NSUInteger selectionIndex;
+
+@property (nonatomic, weak, nullable) UIColor *selectedBackgroundColor UI_APPEARANCE_SELECTOR;
+@property (nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;
+
+@property (nonatomic, weak, nullable) UIFont *font UI_APPEARANCE_SELECTOR DEPRECATED_MSG_ATTRIBUTE("Use setTextAttributes: of CTAssetSelectionLabel instead.");
+@property (nonatomic, weak, nullable) UIColor *textColor UI_APPEARANCE_SELECTOR DEPRECATED_MSG_ATTRIBUTE("Use setTextAttributes: of CTAssetSelectionLabel instead.");
+
+
+@end

+ 142 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridSelectedView.m

@@ -0,0 +1,142 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2013 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsGridSelectedView.h"
+#import "CTAssetCheckmark.h"
+#import "CTAssetSelectionLabel.h"
+
+
+
+
+@interface CTAssetsGridSelectedView ()
+
+@property (nonatomic, strong) CTAssetCheckmark *checkmark;
+@property (nonatomic, strong) CTAssetSelectionLabel *selectionIndexLabel;
+
+@end
+
+
+
+
+
+@implementation CTAssetsGridSelectedView
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        [self setupViews];
+        self.showsSelectionIndex = NO;
+    }
+    
+    return self;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    self.backgroundColor = CTAssetsGridSelectedViewBackgroundColor;
+    self.layer.borderColor = CTAssetsGridSelectedViewTintColor.CGColor;
+    
+    CTAssetCheckmark *checkmark = [CTAssetCheckmark newAutoLayoutView];
+    self.checkmark = checkmark;
+    [self addSubview:checkmark];
+    
+    CTAssetSelectionLabel *selectionIndexLabel = [CTAssetSelectionLabel newAutoLayoutView];
+    self.selectionIndexLabel = selectionIndexLabel;
+    
+    [self addSubview:self.selectionIndexLabel];
+}
+
+
+#pragma mark - Accessors
+
+- (void)setShowsSelectionIndex:(BOOL)showsSelectionIndex
+{
+    _showsSelectionIndex = showsSelectionIndex;
+    
+    if (showsSelectionIndex)
+    {
+        self.checkmark.hidden = YES;
+        self.selectionIndexLabel.hidden = NO;
+    }
+    else
+    {
+        self.checkmark.hidden = NO;
+        self.selectionIndexLabel.hidden = YES;
+    }
+}
+
+- (void)setSelectionIndex:(NSUInteger)selectionIndex
+{
+    _selectionIndex = selectionIndex;
+    self.selectionIndexLabel.text = [NSString stringWithFormat:@"%lu", (unsigned long)(selectionIndex + 1)];
+}
+
+
+#pragma mark - Apperance
+
+- (UIColor *)selectedBackgroundColor
+{
+    return self.backgroundColor;
+}
+
+- (void)setSelectedBackgroundColor:(UIColor *)backgroundColor
+{
+    UIColor *color = (backgroundColor) ? backgroundColor : CTAssetsGridSelectedViewBackgroundColor;
+    self.backgroundColor = color;
+}
+
+- (CGFloat)borderWidth
+{
+    return self.layer.borderWidth;
+}
+
+- (void)setBorderWidth:(CGFloat)borderWidth
+{
+    self.layer.borderWidth = borderWidth;
+}
+
+- (void)setTintColor:(UIColor *)tintColor
+{
+    UIColor *color = (tintColor) ? tintColor : CTAssetsGridSelectedViewTintColor;
+    self.layer.borderColor = color.CGColor;
+}
+
+
+#pragma mark - Accessibility Label
+
+- (NSString *)accessibilityLabel
+{
+    return self.selectionIndexLabel.text;
+}
+
+
+@end

+ 33 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridView.h

@@ -0,0 +1,33 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetsGridView : UIView
+
+@property (nonatomic, weak, nullable) UIColor *gridBackgroundColor UI_APPEARANCE_SELECTOR;
+
+@end

+ 86 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridView.m

@@ -0,0 +1,86 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsGridView.h"
+
+@interface CTAssetsGridView ()
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+@implementation CTAssetsGridView
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    self.backgroundColor = CTAssetsGridViewBackgroundColor;
+}
+
+
+#pragma mark - Apperance
+
+- (UIColor *)gridBackgroundColor
+{
+    return self.backgroundColor;
+}
+
+- (void)setGridBackgroundColor:(UIColor *)backgroundColor
+{
+    UIColor *color = (backgroundColor) ? (backgroundColor) : CTAssetsGridViewBackgroundColor;
+    self.backgroundColor = color;
+}
+
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [self autoPinEdgesToSuperviewEdges];
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+@end

+ 44 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewCell.h

@@ -0,0 +1,44 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+#import "CTAssetThumbnailView.h"
+
+
+
+@interface CTAssetsGridViewCell : UICollectionViewCell
+
+@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
+@property (nonatomic, assign) BOOL showsSelectionIndex;
+@property (nonatomic, assign) NSUInteger selectionIndex;
+
+@property (nonatomic, weak, nullable) UIColor *disabledColor UI_APPEARANCE_SELECTOR;
+@property (nonatomic, weak, nullable) UIColor *highlightedColor UI_APPEARANCE_SELECTOR;
+
+- (void)bind:(nonnull PHAsset *)asset;
+
+@end

+ 204 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewCell.m

@@ -0,0 +1,204 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsGridViewCell.h"
+#import "CTAssetsGridSelectedView.h"
+#import "PHAsset+CTAssetsPickerController.h"
+#import "NSDateFormatter+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+
+
+
+@interface CTAssetsGridViewCell ()
+
+@property (nonatomic, strong) PHAsset *asset;
+
+@property (nonatomic, strong) UIImageView *disabledImageView;
+@property (nonatomic, strong) UIView *disabledView;
+@property (nonatomic, strong) UIView *highlightedView;
+@property (nonatomic, strong) CTAssetsGridSelectedView *selectedView;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+
+
+@implementation CTAssetsGridViewCell
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        self.opaque                 = YES;
+        self.isAccessibilityElement = YES;
+        self.accessibilityTraits    = UIAccessibilityTraitImage;
+        self.enabled                = YES;
+        self.showsSelectionIndex    = NO;
+        
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    CTAssetThumbnailView *thumbnailView = [CTAssetThumbnailView newAutoLayoutView];
+    self.backgroundView = thumbnailView;
+    
+    UIImage *disabledImage = [UIImage ctassetsPickerImageNamed:@"GridDisabledAsset"];
+    disabledImage = [disabledImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+    UIImageView *disabledImageView = [[UIImageView alloc] initWithImage:disabledImage];
+    disabledImageView.tintColor = CTAssetsPikcerThumbnailTintColor;
+    self.disabledImageView = disabledImageView;
+    
+    UIView *disabledView = [UIView newAutoLayoutView];
+    disabledView.backgroundColor = CTAssetsGridViewCellDisabledColor;
+    disabledView.hidden = YES;
+    [disabledView addSubview:self.disabledImageView];
+    self.disabledView = disabledView;
+    [self addSubview:self.disabledView];
+    
+    UIView *highlightedView = [UIView newAutoLayoutView];
+    highlightedView.backgroundColor = CTAssetsGridViewCellHighlightedColor;
+    highlightedView.hidden = YES;
+    self.highlightedView = highlightedView;
+    [self addSubview:self.highlightedView];
+    
+    CTAssetsGridSelectedView *selectedView = [CTAssetsGridSelectedView newAutoLayoutView];
+    selectedView.hidden = YES;
+    self.selectedView = selectedView;
+    [self addSubview:self.selectedView];
+}
+
+#pragma mark - Apperance
+
+- (UIColor *)disabledColor
+{
+    return self.disabledView.backgroundColor;
+}
+
+- (void)setDisabledColor:(UIColor *)disabledColor
+{
+    UIColor *color = (disabledColor) ? disabledColor : CTAssetsGridViewCellDisabledColor;
+    self.disabledView.backgroundColor = color;
+}
+
+- (UIColor *)highlightedColor
+{
+    return self.highlightedView.backgroundColor;
+}
+
+- (void)setHighlightedColor:(UIColor *)highlightedColor
+{
+    UIColor *color = (highlightedColor) ? highlightedColor : CTAssetsGridViewCellHighlightedColor;
+    self.highlightedView.backgroundColor = color;
+}
+
+
+#pragma mark - Accessors
+
+- (void)setEnabled:(BOOL)enabled
+{
+    _enabled = enabled;
+    self.disabledView.hidden = enabled;
+}
+
+- (void)setHighlighted:(BOOL)highlighted
+{
+    super.highlighted = highlighted;
+    self.highlightedView.hidden = !highlighted;
+}
+
+- (void)setSelected:(BOOL)selected
+{
+    super.selected = selected;
+    self.selectedView.hidden = !selected;
+}
+
+- (void)setShowsSelectionIndex:(BOOL)showsSelectionIndex
+{
+    _showsSelectionIndex = showsSelectionIndex;
+    self.selectedView.showsSelectionIndex = showsSelectionIndex;
+}
+
+- (void)setSelectionIndex:(NSUInteger)selectionIndex
+{
+    _selectionIndex = selectionIndex;
+    self.selectedView.selectionIndex = selectionIndex;
+}
+
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
+            [self.backgroundView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+            [self.disabledView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+            [self.highlightedView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+            [self.selectedView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
+        }];
+        
+        [self.disabledImageView autoCenterInSuperview];
+
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+
+- (void)bind:(PHAsset *)asset
+{
+    self.asset = asset;
+    
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];
+}
+
+
+#pragma mark - Accessibility Label
+
+- (NSString *)accessibilityLabel
+{
+    if (self.selectedView.accessibilityLabel)
+        return [NSString stringWithFormat:@"%@, %@", self.selectedView.accessibilityLabel, self.asset.accessibilityLabel];
+    else
+        return self.asset.accessibilityLabel;
+}
+
+@end

+ 50 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewController.h

@@ -0,0 +1,50 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+
+
+@class CTAssetsGridViewController;
+
+
+
+@protocol CTAssetsGridViewControllerDelegate <NSObject>
+
+- (void)assetsGridViewController:(nonnull CTAssetsGridViewController *)picker photoLibraryDidChangeForAssetCollection:(nonnull PHAssetCollection *)assetCollection;
+
+@end
+
+
+
+@interface CTAssetsGridViewController : UICollectionViewController
+
+@property (nonatomic, weak) id<CTAssetsGridViewControllerDelegate> delegate;
+@property (nonatomic, strong, nonnull) PHAssetCollection *assetCollection;
+
+@end
+
+

+ 802 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewController.m

@@ -0,0 +1,802 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsPickerController.h"
+#import "CTAssetsPickerController+Internal.h"
+#import "CTAssetsGridViewController.h"
+#import "CTAssetsGridView.h"
+#import "CTAssetsGridViewLayout.h"
+#import "CTAssetsGridViewCell.h"
+#import "CTAssetsGridViewFooter.h"
+#import "CTAssetsPickerNoAssetsView.h"
+#import "CTAssetsPageViewController.h"
+#import "CTAssetsPageViewController+Internal.h"
+#import "CTAssetsViewControllerTransition.h"
+#import "UICollectionView+CTAssetsPickerController.h"
+#import "NSIndexSet+CTAssetsPickerController.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "PHImageManager+CTAssetsPickerController.h"
+
+
+
+
+
+NSString * const CTAssetsGridViewCellIdentifier = @"CTAssetsGridViewCellIdentifier";
+NSString * const CTAssetsGridViewFooterIdentifier = @"CTAssetsGridViewFooterIdentifier";
+
+
+@interface CTAssetsGridViewController ()
+<PHPhotoLibraryChangeObserver>
+
+@property (nonatomic, weak) CTAssetsPickerController *picker;
+@property (nonatomic, strong) PHFetchResult *fetchResult;
+@property (nonatomic, strong) PHCachingImageManager *imageManager;
+
+@property (nonatomic, assign) CGRect previousPreheatRect;
+@property (nonatomic, assign) CGRect previousBounds;
+
+@property (nonatomic, strong) CTAssetsGridViewFooter *footer;
+@property (nonatomic, strong) CTAssetsPickerNoAssetsView *noAssetsView;
+
+@property (nonatomic, assign) BOOL didLayoutSubviews;
+
+@end
+
+
+
+
+
+@implementation CTAssetsGridViewController
+
+
+- (instancetype)init
+{
+    CTAssetsGridViewLayout *layout = [CTAssetsGridViewLayout new];
+    
+    if (self = [super initWithCollectionViewLayout:layout])
+    {
+        _imageManager = [PHCachingImageManager new];
+        
+        self.extendedLayoutIncludesOpaqueBars = YES;
+        
+        self.collectionView.allowsMultipleSelection = YES;
+        
+        [self.collectionView registerClass:CTAssetsGridViewCell.class
+                forCellWithReuseIdentifier:CTAssetsGridViewCellIdentifier];
+        
+        [self.collectionView registerClass:CTAssetsGridViewFooter.class
+                forSupplementaryViewOfKind:UICollectionElementKindSectionFooter
+                       withReuseIdentifier:CTAssetsGridViewFooterIdentifier];
+        
+        [self addNotificationObserver];
+    }
+    
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    [self setupViews];
+    [self registerChangeObserver];
+    [self addGestureRecognizer];
+    [self addNotificationObserver];
+    [self resetCachedAssetImages];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    [super viewWillAppear:animated];
+    [self setupButtons];
+    [self setupAssets];
+    [self updateTitle:self.picker.selectedAssets];
+    [self updateButton:self.picker.selectedAssets];
+}
+
+- (void)viewDidAppear:(BOOL)animated
+{
+    [super viewDidAppear:animated];
+    [self updateCachedAssetImages];
+}
+
+- (void)viewWillLayoutSubviews
+{
+    [super viewWillLayoutSubviews];
+    
+    if (!CGRectEqualToRect(self.view.bounds, self.previousBounds))
+    {
+        [self updateCollectionViewLayout];
+        self.previousBounds = self.view.bounds;
+    }
+}
+
+- (void)viewDidLayoutSubviews
+{
+    [super viewDidLayoutSubviews];
+    
+    if (!self.didLayoutSubviews && self.fetchResult.count > 0)
+    {
+        [self scrollToBottomIfNeeded];
+        self.didLayoutSubviews = YES;
+    }
+}
+
+- (void)dealloc
+{
+    [self unregisterChangeObserver];
+    [self removeNotificationObserver];
+}
+
+
+#pragma mark - Accessors
+
+- (CTAssetsPickerController *)picker
+{
+    return (CTAssetsPickerController *)self.splitViewController.parentViewController;
+}
+
+- (PHAsset *)assetAtIndexPath:(NSIndexPath *)indexPath
+{
+    return (self.fetchResult.count > 0) ? self.fetchResult[indexPath.item] : nil;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    self.collectionView.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
+    CTAssetsGridView *gridView = [CTAssetsGridView new];
+    [self.view insertSubview:gridView atIndex:0];
+    [self.view setNeedsUpdateConstraints];
+}
+
+- (void)setupButtons
+{
+    if (self.navigationItem.rightBarButtonItem == nil)
+    {
+        NSString *title = (self.picker.doneButtonTitle) ?
+        self.picker.doneButtonTitle : CTAssetsPickerLocalizedString(@"Done", nil);
+        
+        self.navigationItem.rightBarButtonItem =
+        [[UIBarButtonItem alloc] initWithTitle:title
+                                         style:UIBarButtonItemStyleDone
+                                        target:self.picker
+                                        action:@selector(finishPickingAssets:)];
+    }
+}
+
+- (void)setupAssets
+{
+    PHFetchResult *fetchResult =
+    [PHAsset fetchAssetsInAssetCollection:self.assetCollection
+                                  options:self.picker.assetsFetchOptions];
+    
+    self.fetchResult = fetchResult;
+    [self reloadData];
+}
+
+
+
+
+#pragma mark - Collection view layout
+
+- (void)updateCollectionViewLayout
+{
+    UITraitCollection *trait = self.traitCollection;
+    CGSize contentSize = self.view.bounds.size;
+    UICollectionViewLayout *layout;
+
+    NSArray *attributes = [self.collectionView.collectionViewLayout layoutAttributesForElementsInRect:self.collectionView.bounds];
+    UICollectionViewLayoutAttributes *attr = (UICollectionViewLayoutAttributes*)attributes.firstObject;
+    // new content size should be at least of first item size, else ignoring
+    if (contentSize.width < attr.size.width || contentSize.height < attr.size.height) {
+        return;
+    }
+
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:collectionViewLayoutForContentSize:traitCollection:)]) {
+        layout = [self.picker.delegate assetsPickerController:self.picker collectionViewLayoutForContentSize:contentSize traitCollection:trait];
+    } else {
+        layout = [[CTAssetsGridViewLayout alloc] initWithContentSize:contentSize traitCollection:trait];
+    }
+    
+    __weak CTAssetsGridViewController *weakSelf = self;
+    
+    [self.collectionView setCollectionViewLayout:layout animated:NO completion:^(BOOL finished){
+        [weakSelf.collectionView reloadItemsAtIndexPaths:[weakSelf.collectionView indexPathsForVisibleItems]];
+    }];
+}
+
+
+
+#pragma mark - Scroll to bottom
+
+- (void)scrollToBottomIfNeeded
+{
+    BOOL shouldScrollToBottom;
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldScrollToBottomForAssetCollection:)])
+        shouldScrollToBottom = [self.picker.delegate assetsPickerController:self.picker shouldScrollToBottomForAssetCollection:self.assetCollection];
+    else
+        shouldScrollToBottom = YES;
+ 
+    if (shouldScrollToBottom)
+    {
+        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.fetchResult.count-1 inSection:0];
+        [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
+    }
+}
+
+
+
+#pragma mark - Notifications
+
+- (void)addNotificationObserver
+{
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+    
+    [center addObserver:self
+               selector:@selector(assetsPickerSelectedAssetsDidChange:)
+                   name:CTAssetsPickerSelectedAssetsDidChangeNotification
+                 object:nil];
+    
+    [center addObserver:self
+               selector:@selector(assetsPickerDidSelectAsset:)
+                   name:CTAssetsPickerDidSelectAssetNotification
+                 object:nil];
+
+    [center addObserver:self
+               selector:@selector(assetsPickerDidDeselectAsset:)
+                   name:CTAssetsPickerDidDeselectAssetNotification
+                 object:nil];
+}
+
+- (void)removeNotificationObserver
+{
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+    
+    [center removeObserver:self name:CTAssetsPickerSelectedAssetsDidChangeNotification object:nil];
+    [center removeObserver:self name:CTAssetsPickerDidSelectAssetNotification object:nil];
+    [center removeObserver:self name:CTAssetsPickerDidDeselectAssetNotification object:nil];
+}
+
+
+#pragma mark - Photo library change observer
+
+- (void)registerChangeObserver
+{
+    [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
+}
+
+- (void)unregisterChangeObserver
+{
+    [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
+}
+
+
+#pragma mark - Photo library changed
+
+- (void)photoLibraryDidChange:(PHChange *)changeInstance
+{
+    // Call might come on any background queue. Re-dispatch to the main queue to handle it.
+    dispatch_async(dispatch_get_main_queue(), ^{
+        
+        PHFetchResultChangeDetails *changeDetails = [changeInstance changeDetailsForFetchResult:self.fetchResult];
+        
+        if (changeDetails)
+        {
+            self.fetchResult = changeDetails.fetchResultAfterChanges;
+            
+            UICollectionView *collectionView = self.collectionView;
+            
+            if (!changeDetails.hasIncrementalChanges || changeDetails.hasMoves)
+            {
+                [collectionView reloadData];
+                [self resetCachedAssetImages];
+            }
+            else
+            {
+                NSArray *removedPaths;
+                NSArray *insertedPaths;
+                NSArray *changedPaths;
+                
+                NSIndexSet *removedIndexes = changeDetails.removedIndexes;
+                removedPaths = [removedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0];
+                
+                NSIndexSet *insertedIndexes = changeDetails.insertedIndexes;
+                insertedPaths = [insertedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0];
+                
+                NSIndexSet *changedIndexes = changeDetails.changedIndexes;
+                changedPaths = [changedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0];
+                
+                BOOL shouldReload = NO;
+                
+                if (changedPaths != nil && removedPaths != nil)
+                {
+                    for (NSIndexPath *changedPath in changedPaths)
+                    {
+                        if ([removedPaths containsObject:changedPath])
+                        {
+                            shouldReload = YES;
+                            break;
+                        }
+                    }
+                }
+                
+                if (removedPaths.lastObject && ((NSIndexPath *)removedPaths.lastObject).item >= self.fetchResult.count)
+                {
+                    shouldReload = YES;
+                }
+                
+                if (shouldReload)
+                {
+                    [collectionView reloadData];
+                    
+                }
+                else
+                {
+                    // if we have incremental diffs, tell the collection view to animate insertions and deletions
+                    [collectionView performBatchUpdates:^{
+                        if (removedPaths.count)
+                        {
+                            [collectionView deleteItemsAtIndexPaths:[removedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0]];
+                        }
+                        
+                        if (insertedPaths.count)
+                        {
+                            [collectionView insertItemsAtIndexPaths:[insertedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0]];
+                        }
+                        
+                        if (changedPaths.count)
+                        {
+                            [collectionView reloadItemsAtIndexPaths:[changedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0] ];
+                        }
+                    } completion:^(BOOL finished){
+                        if (finished)
+                            [self resetCachedAssetImages];
+                    }];
+                }
+            }
+            
+            [self.footer bind:self.fetchResult];
+            
+            if (self.fetchResult.count == 0)
+                [self showNoAssets];
+            else
+                [self hideNoAssets];
+        }
+        
+        if ([self.delegate respondsToSelector:@selector(assetsGridViewController:photoLibraryDidChangeForAssetCollection:)])
+            [self.delegate assetsGridViewController:self photoLibraryDidChangeForAssetCollection:self.assetCollection];
+        
+    });
+}
+
+
+#pragma mark - Selected assets changed
+
+- (void)assetsPickerSelectedAssetsDidChange:(NSNotification *)notification
+{
+    NSArray *selectedAssets = (NSArray *)notification.object;
+    [self updateTitle:selectedAssets];
+    [self updateButton:selectedAssets];
+}
+
+- (void)updateTitle:(NSArray *)selectedAssets
+{
+    if (selectedAssets.count > 0)
+        self.title = self.picker.selectedAssetsString;
+    else
+        self.title = self.assetCollection.localizedTitle;
+}
+
+- (void)updateButton:(NSArray *)selectedAssets
+{
+    if (self.picker.alwaysEnableDoneButton)
+        self.navigationItem.rightBarButtonItem.enabled = YES;
+    else
+        self.navigationItem.rightBarButtonItem.enabled = (self.picker.selectedAssets.count > 0);
+}
+
+
+#pragma mark - Did de/select asset notifications
+
+- (void)assetsPickerDidSelectAsset:(NSNotification *)notification
+{
+    PHAsset *asset = (PHAsset *)notification.object;
+    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:[self.fetchResult indexOfObject:asset] inSection:0];
+    [self.collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
+    
+    [self updateSelectionOrderLabels];
+}
+
+- (void)assetsPickerDidDeselectAsset:(NSNotification *)notification
+{
+    PHAsset *asset = (PHAsset *)notification.object;
+    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:[self.fetchResult indexOfObject:asset] inSection:0];
+    [self.collectionView deselectItemAtIndexPath:indexPath animated:NO];
+    
+    [self updateSelectionOrderLabels];
+}
+
+
+#pragma mark - Update Selection Order Labels
+
+- (void)updateSelectionOrderLabels
+{
+    for (NSIndexPath *indexPath in [self.collectionView indexPathsForSelectedItems])
+    {
+        PHAsset *asset = [self assetAtIndexPath:indexPath];
+        CTAssetsGridViewCell *cell = (CTAssetsGridViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
+        cell.selectionIndex = [self.picker.selectedAssets indexOfObject:asset];
+    }
+}
+
+
+#pragma mark - Gesture recognizer
+
+- (void)addGestureRecognizer
+{
+    UILongPressGestureRecognizer *longPress =
+    [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(pushPageViewController:)];
+    
+    [self.collectionView addGestureRecognizer:longPress];
+}
+
+
+#pragma mark - Push assets page view controller
+
+- (void)pushPageViewController:(UILongPressGestureRecognizer *)longPress
+{
+    if (longPress.state == UIGestureRecognizerStateBegan)
+    {
+        CGPoint point           = [longPress locationInView:self.collectionView];
+        NSIndexPath *indexPath  = [self.collectionView indexPathForItemAtPoint:point];
+        
+        CTAssetsPageViewController *vc = [[CTAssetsPageViewController alloc] initWithFetchResult:self.fetchResult];
+        vc.allowsSelection = YES;
+        vc.pageIndex = indexPath.item;
+        
+        [self.navigationController pushViewController:vc animated:YES];
+    }
+}
+
+
+#pragma mark - Reload data
+
+- (void)reloadData
+{
+    if (self.fetchResult.count > 0)
+    {
+        [self hideNoAssets];
+        [self.collectionView reloadData];
+    }
+    else
+    {
+        [self showNoAssets];
+    }
+}
+
+
+#pragma mark - Asset images caching
+
+- (void)resetCachedAssetImages
+{
+    [self.imageManager stopCachingImagesForAllAssets];
+    self.previousPreheatRect = CGRectZero;
+}
+
+- (void)updateCachedAssetImages
+{
+    BOOL isViewVisible = [self isViewLoaded] && self.view.window != nil;
+    
+    if (!isViewVisible)
+        return;
+    
+    // The preheat window is twice the height of the visible rect
+    CGRect preheatRect = self.collectionView.bounds;
+    preheatRect = CGRectInset(preheatRect, 0.0f, -0.5f * CGRectGetHeight(preheatRect));
+    
+    // If scrolled by a "reasonable" amount...
+    CGFloat delta = ABS(CGRectGetMidY(preheatRect) - CGRectGetMidY(self.previousPreheatRect));
+    
+    if (delta > CGRectGetHeight(self.collectionView.bounds) / 3.0f)
+    {
+        // Compute the assets to start caching and to stop caching.
+        NSMutableArray *addedIndexPaths = [NSMutableArray array];
+        NSMutableArray *removedIndexPaths = [NSMutableArray array];
+        
+        [self computeDifferenceBetweenRect:self.previousPreheatRect
+                                   andRect:preheatRect
+                            removedHandler:^(CGRect removedRect) {
+                                NSArray *indexPaths = [self.collectionView ctassetsPickerIndexPathsForElementsInRect:removedRect];
+                                [removedIndexPaths addObjectsFromArray:indexPaths];
+                            } addedHandler:^(CGRect addedRect) {
+                                NSArray *indexPaths = [self.collectionView ctassetsPickerIndexPathsForElementsInRect:addedRect];
+                                [addedIndexPaths addObjectsFromArray:indexPaths];
+                            }];
+        
+        [self startCachingThumbnailsForIndexPaths:addedIndexPaths];
+        [self stopCachingThumbnailsForIndexPaths:removedIndexPaths];
+        
+        self.previousPreheatRect = preheatRect;
+    }
+}
+
+- (void)startCachingThumbnailsForIndexPaths:(NSArray *)indexPaths
+{
+    for (NSIndexPath *indexPath in indexPaths)
+    {
+        PHAsset *asset = [self assetAtIndexPath:indexPath];
+        
+        if (!asset) break;
+        
+        UICollectionViewLayoutAttributes *attributes =
+        [self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
+        
+        CGSize targetSize = [self.picker imageSizeForContainerSize:attributes.size];
+        
+        [self.imageManager startCachingImagesForAssets:@[asset]
+                                            targetSize:targetSize
+                                           contentMode:PHImageContentModeAspectFill
+                                               options:self.picker.thumbnailRequestOptions];
+    }
+}
+
+- (void)stopCachingThumbnailsForIndexPaths:(NSArray *)indexPaths
+{
+    for (NSIndexPath *indexPath in indexPaths)
+    {
+        PHAsset *asset = [self assetAtIndexPath:indexPath];
+        
+        if (!asset) break;
+
+        UICollectionViewLayoutAttributes *attributes =
+        [self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
+        
+        CGSize targetSize = [self.picker imageSizeForContainerSize:attributes.size];
+        
+        [self.imageManager stopCachingImagesForAssets:@[asset]
+                                           targetSize:targetSize
+                                          contentMode:PHImageContentModeAspectFill
+                                              options:self.picker.thumbnailRequestOptions];
+    }
+}
+
+- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler
+{
+    if (CGRectIntersectsRect(newRect, oldRect)) {
+        CGFloat oldMaxY = CGRectGetMaxY(oldRect);
+        CGFloat oldMinY = CGRectGetMinY(oldRect);
+        CGFloat newMaxY = CGRectGetMaxY(newRect);
+        CGFloat newMinY = CGRectGetMinY(newRect);
+        if (newMaxY > oldMaxY) {
+            CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
+            addedHandler(rectToAdd);
+        }
+        if (oldMinY > newMinY) {
+            CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
+            addedHandler(rectToAdd);
+        }
+        if (newMaxY < oldMaxY) {
+            CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
+            removedHandler(rectToRemove);
+        }
+        if (oldMinY < newMinY) {
+            CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
+            removedHandler(rectToRemove);
+        }
+    } else {
+        addedHandler(newRect);
+        removedHandler(oldRect);
+    }
+}
+
+
+#pragma mark - Scroll view delegate
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView
+{
+    [self updateCachedAssetImages];
+}
+
+
+#pragma mark - No assets
+
+- (void)showNoAssets
+{
+    CTAssetsPickerNoAssetsView *view = [CTAssetsPickerNoAssetsView new];
+    [self.view addSubview:view];
+    [view setNeedsUpdateConstraints];
+    [view updateConstraintsIfNeeded];
+    
+    self.noAssetsView = view;
+}
+
+- (void)hideNoAssets
+{
+    if (self.noAssetsView)
+    {
+        [self.noAssetsView removeFromSuperview];
+        self.noAssetsView = nil;
+    }
+}
+
+
+#pragma mark - Collection view data source
+
+- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
+{
+    return 1;
+}
+
+- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
+{
+    return self.fetchResult.count;
+}
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    CTAssetsGridViewCell *cell =
+    [collectionView dequeueReusableCellWithReuseIdentifier:CTAssetsGridViewCellIdentifier
+                                              forIndexPath:indexPath];
+    
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldEnableAsset:)])
+        cell.enabled = [self.picker.delegate assetsPickerController:self.picker shouldEnableAsset:asset];
+    else
+        cell.enabled = YES;
+    
+    cell.showsSelectionIndex = self.picker.showsSelectionIndex;
+    
+    // XXX
+    // Setting `selected` property blocks further deselection.
+    // Have to call selectItemAtIndexPath too. ( ref: http://stackoverflow.com/a/17812116/1648333 )
+    if ([self.picker.selectedAssets containsObject:asset])
+    {
+        cell.selected = YES;
+        cell.selectionIndex = [self.picker.selectedAssets indexOfObject:asset];
+        [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
+    }
+    
+    [cell bind:asset];
+    
+    UICollectionViewLayoutAttributes *attributes =
+    [collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
+    
+    CGSize targetSize = [self.picker imageSizeForContainerSize:attributes.size];
+    
+    [self requestThumbnailForCell:cell targetSize:targetSize asset:asset];
+
+    return cell;
+}
+
+- (void)requestThumbnailForCell:(CTAssetsGridViewCell *)cell targetSize:(CGSize)targetSize asset:(PHAsset *)asset
+{
+    NSInteger tag = cell.tag + 1;
+    cell.tag = tag;
+
+    [self.imageManager ctassetsPickerRequestImageForAsset:asset
+                                 targetSize:targetSize
+                                contentMode:PHImageContentModeAspectFill
+                                    options:self.picker.thumbnailRequestOptions
+                              resultHandler:^(UIImage *image, NSDictionary *info){
+                                  // Only update the image if the cell tag hasn't changed. Otherwise, the cell has been re-used.
+                                  if (cell.tag == tag)
+                                      [(CTAssetThumbnailView *)cell.backgroundView bind:image asset:asset];
+                              }];
+}
+
+- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
+{
+    CTAssetsGridViewFooter *footer =
+    [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter
+                                       withReuseIdentifier:CTAssetsGridViewFooterIdentifier
+                                              forIndexPath:indexPath];
+    
+    [footer bind:self.fetchResult];
+    
+    self.footer = footer;
+    
+    return footer;
+}
+
+
+#pragma mark - Collection view delegate
+
+- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    CTAssetsGridViewCell *cell = (CTAssetsGridViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
+    
+    if (!cell.isEnabled)
+        return NO;
+    else if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldSelectAsset:)])
+        return [self.picker.delegate assetsPickerController:self.picker shouldSelectAsset:asset];
+    else
+        return YES;
+}
+
+- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    [self.picker selectAsset:asset];
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didSelectAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didSelectAsset:asset];
+}
+
+- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldDeselectAsset:)])
+        return [self.picker.delegate assetsPickerController:self.picker shouldDeselectAsset:asset];
+    else
+        return YES;
+}
+
+- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    [self.picker deselectAsset:asset];
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didDeselectAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didDeselectAsset:asset];
+}
+
+- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldHighlightAsset:)])
+        return [self.picker.delegate assetsPickerController:self.picker shouldHighlightAsset:asset];
+    else
+        return YES;
+}
+
+- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didHighlightAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didHighlightAsset:asset];
+}
+
+- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath
+{
+    PHAsset *asset = [self assetAtIndexPath:indexPath];
+    
+    if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didUnhighlightAsset:)])
+        [self.picker.delegate assetsPickerController:self.picker didUnhighlightAsset:asset];
+}
+
+@end

+ 38 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewFooter.h

@@ -0,0 +1,38 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+
+
+@interface CTAssetsGridViewFooter : UICollectionReusableView
+
+@property (nonatomic, weak, nullable) UIFont *font UI_APPEARANCE_SELECTOR;
+@property (nonatomic, weak, nullable) UIColor *textColor UI_APPEARANCE_SELECTOR;
+
+- (void)bind:(nonnull PHFetchResult *)result;
+
+@end

+ 146 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewFooter.m

@@ -0,0 +1,146 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsGridViewFooter.h"
+#import "NSNumberFormatter+CTAssetsPickerController.h"
+#import "NSBundle+CTAssetsPickerController.h"
+
+
+
+
+@interface CTAssetsGridViewFooter ()
+
+@property (nonatomic, strong) UILabel *label;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+
+
+
+@implementation CTAssetsGridViewFooter
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    UILabel *label = [UILabel newAutoLayoutView];
+    label.textAlignment = NSTextAlignmentCenter;
+    label.font = CTAssetsGridViewFooterFont;
+    label.textColor = CTAssetsGridViewFooterTextColor;
+    
+    self.label = label;
+    [self addSubview:self.label];
+}
+
+
+#pragma mark - Appearance
+
+- (UIFont *)font
+{
+    return self.label.font;
+}
+
+- (void)setFont:(UIFont *)font
+{
+    UIFont *labelFont = (font) ? font : CTAssetsGridViewFooterFont;
+    self.label.font = labelFont;
+}
+
+- (UIColor *)textColor
+{
+    return self.label.textColor;
+}
+
+- (void)setTextColor:(UIColor *)textColor
+{
+    UIColor *color = (textColor) ? textColor : CTAssetsGridViewFooterTextColor;
+    self.label.textColor = color;
+}
+
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [self.label autoPinEdgesToSuperviewMargins];
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+- (void)bind:(PHFetchResult *)result
+{
+    NSNumberFormatter *nf = [NSNumberFormatter new];
+    
+    NSString *numberOfVideos = @"";
+    NSString *numberOfPhotos = @"";
+    
+    NSUInteger videoCount = [result countOfAssetsWithMediaType:PHAssetMediaTypeVideo];
+    NSUInteger photoCount = [result countOfAssetsWithMediaType:PHAssetMediaTypeImage];
+    
+    if (videoCount > 0)
+        numberOfVideos = [nf ctassetsPickerStringFromAssetsCount:videoCount];
+    
+    if (photoCount > 0)
+        numberOfPhotos = [nf ctassetsPickerStringFromAssetsCount:photoCount];
+    
+    if (photoCount > 0 && videoCount > 0)
+        self.label.text = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Photos, %@ Videos", nil), numberOfPhotos, numberOfVideos];
+    else if (photoCount > 0 && videoCount <= 0)
+        self.label.text = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Photos", nil), numberOfPhotos];
+    else if (photoCount <= 0 && videoCount > 0)
+        self.label.text = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Videos", nil), numberOfVideos];
+    else
+        self.label.text = @"";
+    
+    self.hidden = (result.count == 0);
+    
+    [self setNeedsUpdateConstraints];
+    [self updateConstraintsIfNeeded];
+}
+
+@end

+ 33 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewLayout.h

@@ -0,0 +1,33 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetsGridViewLayout : UICollectionViewFlowLayout
+
+- (instancetype)initWithContentSize:(CGSize)contentSize traitCollection:(UITraitCollection *)traits;
+
+@end

+ 109 - 0
Libraries external/CTAssetsPickerController/CTAssetsGridViewLayout.m

@@ -0,0 +1,109 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import "CTAssetsGridViewLayout.h"
+
+@implementation CTAssetsGridViewLayout
+
+- (instancetype)initWithContentSize:(CGSize)contentSize traitCollection:(UITraitCollection *)traits
+{
+    if (self = [super init])
+    {
+        CGFloat scale = traits.displayScale;
+        NSInteger numberOfColumns = [self numberOfColumnsForTraitCollection:traits];
+        CGFloat onePixel = (scale == 3.0) ? (2.0 / scale) : (1.0 / scale);
+        
+        // spacing is as small as possible
+        self.minimumInteritemSpacing = onePixel;
+        self.minimumLineSpacing = onePixel;
+        
+        // total spaces between items (in pixel)
+        CGFloat spaces  = self.minimumInteritemSpacing * (numberOfColumns - 1);
+        
+        // item length (in pixel)
+        CGFloat length  = (scale * (contentSize.width - spaces)) / numberOfColumns;
+        
+        // remaining spaces (in pixel) after rounding the length to integer
+        CGFloat insets  = (length - floor(length)) * numberOfColumns;
+        
+        // round the length to integer (in pixel)
+        length = floor(length);
+        
+        // divide insets to two
+        CGFloat left = insets / 2;
+        CGFloat right = insets / 2;
+        
+        // adjust if insets is odd
+        if (fmodf(insets, 2.0) == 1.0f)
+        {
+            left -= 0.5;
+            right += 0.5;
+        }
+
+        // left / right insets (in point, 2 decimal)
+        left    = floorf(left / scale * 100) / 100;
+        right   = floorf(right / scale * 100) / 100;
+        
+        // item length (in point, 2 decimal)
+        length  = floorf(length / scale * 100) / 100;
+        
+        self.sectionInset = UIEdgeInsetsMake(0, left, 0, right);
+        self.itemSize = CGSizeMake(length, length);
+        
+        self.footerReferenceSize = CGSizeMake(contentSize.width, floor(length * 2/3));
+    }
+    
+    return self;
+}
+
+- (NSInteger)numberOfColumnsForTraitCollection:(UITraitCollection *)traits
+{
+    switch (traits.userInterfaceIdiom) {
+        case UIUserInterfaceIdiomPad:
+        {
+            return 6;
+            break;
+        }
+        case UIUserInterfaceIdiomPhone:
+        {
+            // iPhone 6+ landscape
+            if (traits.horizontalSizeClass == UIUserInterfaceSizeClassRegular)
+                return 4;
+            // iPhone landscape
+            else if (traits.verticalSizeClass == UIUserInterfaceSizeClassCompact)
+                return 6;
+            // iPhone portrait
+            else
+                return 4;
+            break;
+        }
+        default:
+            return 4;
+            break;
+    }
+}
+
+@end

+ 31 - 0
Libraries external/CTAssetsPickerController/CTAssetsNavigationController.h

@@ -0,0 +1,31 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 bawn
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetsNavigationController : UINavigationController
+
+@end

+ 119 - 0
Libraries external/CTAssetsPickerController/CTAssetsNavigationController.m

@@ -0,0 +1,119 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 bawn
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import "CTAssetsNavigationController.h"
+
+
+
+@interface CTAssetsNavigationController ()<UIGestureRecognizerDelegate, UINavigationControllerDelegate>
+
+@end
+
+@implementation CTAssetsNavigationController
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    __weak CTAssetsNavigationController<UIGestureRecognizerDelegate> *weakSelf = self;
+    
+    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
+    {
+        self.interactivePopGestureRecognizer.delegate = weakSelf;
+        
+        self.delegate = weakSelf;
+    }
+    
+}
+
+- (void)didReceiveMemoryWarning {
+    [super didReceiveMemoryWarning];
+    // Dispose of any resources that can be recreated.
+}
+
+- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
+{
+    
+    if ( [self respondsToSelector:@selector(interactivePopGestureRecognizer)] && animated == YES )
+    {
+        self.interactivePopGestureRecognizer.enabled = NO;
+    }
+    
+    [super pushViewController:viewController animated:animated];
+    
+}
+
+- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
+{
+    if ( [self respondsToSelector:@selector(interactivePopGestureRecognizer)] && animated == YES )
+    {
+        self.interactivePopGestureRecognizer.enabled = NO;
+    }
+    
+    return  [super popToRootViewControllerAnimated:animated];
+    
+}
+
+- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
+{
+    if( [self respondsToSelector:@selector(interactivePopGestureRecognizer)] )
+    {
+        self.interactivePopGestureRecognizer.enabled = NO;
+    }
+    
+    return [super popToViewController:viewController animated:animated];
+    
+}
+
+#pragma mark UINavigationControllerDelegate
+
+- (void)navigationController:(UINavigationController *)navigationController
+       didShowViewController:(UIViewController *)viewController
+                    animated:(BOOL)animate
+{
+    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
+    {
+        self.interactivePopGestureRecognizer.enabled = YES;
+    }
+}
+
+
+- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
+{
+    
+    if ( gestureRecognizer == self.interactivePopGestureRecognizer )
+    {
+        if ( self.viewControllers.count < 2 || self.visibleViewController == self.viewControllers[0] )
+        {
+            return NO;
+        }
+    }
+    
+    return YES;
+}
+
+
+@end
+

+ 37 - 0
Libraries external/CTAssetsPickerController/CTAssetsPageView.h

@@ -0,0 +1,37 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetsPageView : UIView
+
+@property (nonatomic, strong, nullable) UIColor *pageBackgroundColor UI_APPEARANCE_SELECTOR;
+@property (nonatomic, strong, nullable) UIColor *fullscreenBackgroundColor UI_APPEARANCE_SELECTOR;
+
+- (void)enterFullscreen;
+- (void)exitFullscreen;
+
+@end

+ 111 - 0
Libraries external/CTAssetsPickerController/CTAssetsPageView.m

@@ -0,0 +1,111 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsPageView.h"
+
+@interface CTAssetsPageView ()
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end
+
+
+
+@implementation CTAssetsPageView
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        _pageBackgroundColor = CTAssetsPageViewPageBackgroundColor;
+        _fullscreenBackgroundColor = CTAssetsPageViewFullscreenBackgroundColor;
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    self.backgroundColor = self.pageBackgroundColor;
+}
+
+
+#pragma mark - Apperance
+
+- (void)setPageBackgroundColor:(UIColor *)backgroundColor
+{
+    UIColor *color = (backgroundColor) ? backgroundColor : CTAssetsPageViewPageBackgroundColor;
+    _pageBackgroundColor = color;
+    self.backgroundColor = color;
+}
+
+- (void)setFullscreenBackgroundColor:(UIColor *)fullscreenBackgroundColor
+{
+    UIColor *color = (fullscreenBackgroundColor) ? fullscreenBackgroundColor : CTAssetsPageViewFullscreenBackgroundColor;
+    _fullscreenBackgroundColor = color;
+}
+
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [self autoPinEdgesToSuperviewEdges];
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+
+#pragma mark - Fading views
+
+- (void)enterFullscreen
+{
+    [UIView animateWithDuration:0.2
+                     animations:^{
+                         self.backgroundColor = self.fullscreenBackgroundColor;
+                     }];
+}
+
+- (void)exitFullscreen
+{
+    [UIView animateWithDuration:0.2
+                     animations:^{
+                         self.backgroundColor = self.pageBackgroundColor;
+                     }];
+}
+
+
+@end

+ 69 - 0
Libraries external/CTAssetsPickerController/CTAssetsPageViewController.h

@@ -0,0 +1,69 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+#import <Photos/Photos.h>
+
+
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ *  A view controller that shows selected photos and vidoes from user's photo library that let the user navigate the item page by page.
+ */
+@interface CTAssetsPageViewController : UIPageViewController
+
+/**
+ *  The index of the photo or video with the currently showing item.
+ */
+@property (nonatomic, assign) NSInteger pageIndex;
+
+
+/**
+ *  @name Creating a Assets Page View Controller
+ */
+
+/**
+ *  Initializes a newly created view controller with a fetech result.
+ *
+ *  @param fetchResult A fetch result of `PHAsset` objects.
+ *
+ *  @return An instance of `CTAssetPageViewController` initialized to show the asset items in `fetchResult`.
+ */
+- (instancetype)initWithFetchResult:(PHFetchResult *)fetchResult;
+
+/**
+ *  Initializes a newly created view controller with an array of assets.
+ *
+ *  @param assets An array of `PHAsset` objects.
+ *
+ *  @return An instance of `CTAssetPageViewController` initialized to show the asset items in `assets`.
+ */
+- (instancetype)initWithAssets:(NSArray<PHAsset*> *)assets;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 422 - 0
Libraries external/CTAssetsPickerController/CTAssetsPageViewController.m

@@ -0,0 +1,422 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import "CTAssetsPageViewController.h"
+#import "CTAssetsPageView.h"
+#import "CTAssetItemViewController.h"
+#import "CTAssetScrollView.h"
+#import "NSNumberFormatter+CTAssetsPickerController.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+#import "PHAsset+CTAssetsPickerController.h"
+
+
+
+
+
+@interface CTAssetsPageViewController ()
+<UIPageViewControllerDataSource, UIPageViewControllerDelegate>
+
+
+@property (nonatomic, assign) BOOL allowsSelection;
+
+@property (nonatomic, assign, getter = isStatusBarHidden) BOOL statusBarHidden;
+
+@property (nonatomic, copy) NSArray *assets;
+@property (nonatomic, strong, readonly) PHAsset *asset;
+
+@property (nonatomic, strong) CTAssetsPageView *pageView;
+
+@property (nonatomic, strong) UIBarButtonItem *playButton;
+@property (nonatomic, strong) UIBarButtonItem *pauseButton;
+
+@end
+
+
+
+
+
+@implementation CTAssetsPageViewController
+
+- (instancetype)initWithFetchResult:(PHFetchResult *)fetchResult
+{
+    NSMutableArray *assets = [NSMutableArray new];
+    
+    for (PHAsset *asset in fetchResult)
+        [assets addObject:asset];
+    
+    return [self initWithAssets:[NSArray arrayWithArray:assets]];
+}
+
+- (instancetype)initWithAssets:(NSArray *)assets
+{
+    self = [super initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
+                    navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
+                                  options:@{UIPageViewControllerOptionInterPageSpacingKey:@30.f}];
+    
+    if (self)
+    {
+        self.assets          = assets;
+        self.dataSource      = self;
+        self.delegate        = self;
+        self.allowsSelection = NO;
+        self.automaticallyAdjustsScrollViewInsets = NO;
+    }
+    
+    return self;
+}
+
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    [self setupViews];
+    [self addNotificationObserver];
+}
+
+- (void)dealloc
+{
+    [self removeNotificationObserver];
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    if (self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact)
+        return YES;
+    else
+        return self.isStatusBarHidden;
+}
+
+
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    self.pageView = [CTAssetsPageView new];
+    [self.view insertSubview:self.pageView atIndex:0];
+    [self.view setNeedsUpdateConstraints];
+}
+
+- (void)setupButtons
+{
+    if (!self.playButton)
+    {
+        UIImage *playImage = [UIImage ctassetsPickerImageNamed:@"PlayButton"];
+        playImage = [playImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+        
+        UIBarButtonItem *playButton =
+        [[UIBarButtonItem alloc] initWithImage:playImage style:UIBarButtonItemStyleDone target:self action:@selector(playAsset:)];
+        
+        self.playButton = playButton;
+    }
+    
+    if (!self.pauseButton)
+    {
+        UIImage *pasueImage = [UIImage ctassetsPickerImageNamed:@"PauseButton"];
+        pasueImage = [pasueImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+        
+        UIBarButtonItem *pauseButton =
+        [[UIBarButtonItem alloc] initWithImage:pasueImage style:UIBarButtonItemStylePlain target:self action:@selector(pauseAsset:)];
+        
+        self.pauseButton = pauseButton;
+    }
+}
+
+
+#pragma mark - Update title
+
+- (void)updateTitle:(NSInteger)index
+{
+    NSNumberFormatter *nf = [NSNumberFormatter new];
+
+    NSInteger count = self.assets.count;
+    self.title      = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ of %@", nil),
+                       [nf ctassetsPickerStringFromAssetsCount:index],
+                       [nf ctassetsPickerStringFromAssetsCount:count]];
+}
+
+
+#pragma mark - Update toolbar
+
+- (void)updateToolbar
+{
+    [self setupButtons];
+    
+    if ([self.asset ctassetsPickerIsVideo])
+        self.toolbarItems = @[[self toolbarSpace], self.playButton, [self toolbarSpace]];
+    else
+        self.toolbarItems = nil;
+}
+
+- (void)replaceToolbarButton:(UIBarButtonItem *)button
+{
+    if (button)
+    {
+        UIBarButtonItem *space = [self toolbarSpace];
+        self.toolbarItems = @[space, button, space];
+    }
+}
+
+- (UIBarButtonItem *)toolbarSpace
+{
+    return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
+}
+
+
+
+#pragma mark - Accessors
+
+- (NSInteger)pageIndex
+{
+    return [self.assets indexOfObject:self.asset];
+}
+
+- (void)setPageIndex:(NSInteger)pageIndex
+{
+    NSInteger count = self.assets.count;
+    
+    if (pageIndex >= 0 && pageIndex < count)
+    {
+        PHAsset *asset = self.assets[pageIndex];
+        
+        CTAssetItemViewController *page = [CTAssetItemViewController assetItemViewControllerForAsset:asset];
+        page.allowsSelection = self.allowsSelection;
+        
+        [self setViewControllers:@[page]
+                       direction:UIPageViewControllerNavigationDirectionForward
+                        animated:NO
+                      completion:NULL];
+        
+        [self updateTitle:pageIndex + 1];
+        [self updateToolbar];
+    }
+}
+
+- (PHAsset *)asset
+{
+    return ((CTAssetItemViewController *)self.viewControllers[0]).asset;
+}
+
+
+#pragma mark - Page view controller data source
+
+- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
+{
+    PHAsset *asset = ((CTAssetItemViewController *)viewController).asset;
+    NSInteger index = [self.assets indexOfObject:asset];
+    
+    if (index > 0)
+    {
+        PHAsset *beforeAsset = self.assets[(index - 1)];
+        CTAssetItemViewController *page = [CTAssetItemViewController assetItemViewControllerForAsset:beforeAsset];
+        page.allowsSelection = self.allowsSelection;
+        
+        return page;
+    }
+
+    return nil;
+}
+
+- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
+{
+    PHAsset *asset  = ((CTAssetItemViewController *)viewController).asset;
+    NSInteger index = [self.assets indexOfObject:asset];
+    NSInteger count = self.assets.count;
+    
+    if (index < count - 1)
+    {
+        PHAsset *afterAsset = self.assets[(index + 1)];
+        CTAssetItemViewController *page = [CTAssetItemViewController assetItemViewControllerForAsset:afterAsset];
+        page.allowsSelection = self.allowsSelection;
+        
+        return page;
+    }
+    
+    return nil;
+}
+
+
+#pragma mark - Page view controller delegate
+
+- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
+{
+    if (completed)
+    {
+        CTAssetItemViewController *vc = (CTAssetItemViewController *)pageViewController.viewControllers[0];
+        NSInteger index = [self.assets indexOfObject:vc.asset] + 1;
+        
+        [self updateTitle:index];
+        [self updateToolbar];
+    }
+}
+
+- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
+{
+    [self.navigationController setToolbarHidden:YES animated:YES];
+}
+
+
+#pragma mark - Notification observer
+
+- (void)addNotificationObserver
+{
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+    
+    [center addObserver:self
+               selector:@selector(assetScrollViewDidTap:)
+                   name:CTAssetScrollViewDidTapNotification
+                 object:nil];
+    
+    [center addObserver:self
+               selector:@selector(assetScrollViewPlayerDidPlayToEnd:)
+                   name:AVPlayerItemDidPlayToEndTimeNotification
+                 object:nil];    
+    
+    [center addObserver:self
+               selector:@selector(assetScrollViewPlayerWillPlay:)
+                   name:CTAssetScrollViewPlayerWillPlayNotification
+                 object:nil];
+    
+    [center addObserver:self
+               selector:@selector(assetScrollViewPlayerWillPause:)
+                   name:CTAssetScrollViewPlayerWillPauseNotification
+                 object:nil];    
+}
+
+- (void)removeNotificationObserver
+{
+    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+    
+    [center removeObserver:self name:CTAssetScrollViewDidTapNotification object:nil];
+    [center removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
+    [center removeObserver:self name:CTAssetScrollViewPlayerWillPlayNotification object:nil];
+    [center removeObserver:self name:CTAssetScrollViewPlayerWillPauseNotification object:nil];
+}
+
+
+#pragma mark - Notification events
+
+- (void)assetScrollViewDidTap:(NSNotification *)notification
+{
+    UITapGestureRecognizer *gesture = (UITapGestureRecognizer *)notification.object;
+    
+    if (gesture.numberOfTapsRequired == 1)
+        [self toggleFullscreen:gesture];
+}
+
+- (void)assetScrollViewPlayerDidPlayToEnd:(NSNotification *)notification
+{
+    [self replaceToolbarButton:self.playButton];
+    [self setFullscreen:NO];
+}
+
+- (void)assetScrollViewPlayerWillPlay:(NSNotification *)notification
+{
+    [self replaceToolbarButton:self.pauseButton];
+    [self setFullscreen:YES];
+}
+
+- (void)assetScrollViewPlayerWillPause:(NSNotification *)notification
+{
+    [self replaceToolbarButton:self.playButton];
+}
+
+
+#pragma mark - Toggle fullscreen
+
+- (void)toggleFullscreen:(id)sender
+{
+    [self setFullscreen:!self.isStatusBarHidden];
+}
+
+- (void)setFullscreen:(BOOL)fullscreen
+{
+    if (fullscreen)
+    {
+        [self.pageView enterFullscreen];
+        [self fadeAwayControls:self.navigationController];
+    }
+    else
+    {
+        [self.pageView exitFullscreen];
+        [self fadeInControls:self.navigationController];
+    }
+    
+}
+
+- (void)fadeInControls:(UINavigationController *)nav
+{
+    self.statusBarHidden = NO;
+    
+    [nav setNavigationBarHidden:NO];
+    nav.navigationBar.alpha = 0.0f;
+    
+    if ([self.asset ctassetsPickerIsVideo])
+    {
+        [nav setToolbarHidden:NO];
+        nav.toolbar.alpha = 0.0f;
+    }
+    
+    [UIView animateWithDuration:0.2
+                     animations:^{
+                         [self setNeedsStatusBarAppearanceUpdate];
+                         nav.navigationBar.alpha = 1.0f;
+                         
+                         if ([self.asset ctassetsPickerIsVideo])
+                             nav.toolbar.alpha = 1.0f;
+                     }];
+}
+
+- (void)fadeAwayControls:(UINavigationController *)nav
+{
+    self.statusBarHidden = YES;
+    
+    [UIView animateWithDuration:0.2
+                     animations:^{
+                         [self setNeedsStatusBarAppearanceUpdate];
+                         [nav setNavigationBarHidden:YES animated:NO];
+                         [nav setToolbarHidden:YES animated:NO];
+                         nav.navigationBar.alpha = 0.0f;
+                         nav.toolbar.alpha = 0.0f;
+                     }];
+}
+
+
+#pragma mark - Playback
+
+- (void)playAsset:(id)sender
+{
+    [((CTAssetItemViewController *)self.viewControllers[0]) playAsset:sender];
+}
+
+- (void)pauseAsset:(id)sender
+{
+    [((CTAssetItemViewController *)self.viewControllers[0]) pauseAsset:sender];
+}
+
+
+@end

+ 31 - 0
Libraries external/CTAssetsPickerController/CTAssetsPickerAccessDeniedView.h

@@ -0,0 +1,31 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface CTAssetsPickerAccessDeniedView : UIView
+
+@end

+ 126 - 0
Libraries external/CTAssetsPickerController/CTAssetsPickerAccessDeniedView.m

@@ -0,0 +1,126 @@
+/*
+ 
+ MIT License (MIT)
+ 
+ Copyright (c) 2015 Clement CN Tsang
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ 
+ */
+
+#import <PureLayout/PureLayout.h>
+#import "CTAssetsPickerDefines.h"
+#import "CTAssetsPickerAccessDeniedView.h"
+#import "NSBundle+CTAssetsPickerController.h"
+#import "UIImage+CTAssetsPickerController.h"
+
+
+
+@interface CTAssetsPickerAccessDeniedView ()
+
+@property (nonatomic, strong) UIImageView *padlock;
+@property (nonatomic, strong) UILabel *title;
+@property (nonatomic, strong) UILabel *message;
+
+@property (nonatomic, assign) BOOL didSetupConstraints;
+
+@end;
+
+
+@implementation CTAssetsPickerAccessDeniedView
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    if (self = [super initWithFrame:frame])
+    {
+        [self setupViews];
+    }
+    
+    return self;
+}
+
+#pragma mark - Setup
+
+- (void)setupViews
+{
+    UIImageView *padlock = [self padlockImageView];
+    self.padlock = padlock;
+    
+    UILabel *title      = [UILabel new];
+    title.textColor     = CTAssetsPikcerAccessDeniedViewTextColor;
+    title.font          = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
+    title.textAlignment = NSTextAlignmentCenter;
+    title.numberOfLines = 5;
+    title.text          = CTAssetsPickerLocalizedString(@"This app does not have access to your photos or videos.", nil);
+    self.title = title;
+    
+    UILabel *message        = [UILabel new];
+    message.textColor       = CTAssetsPikcerAccessDeniedViewTextColor;
+    message.font            = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
+    message.textAlignment   = NSTextAlignmentCenter;
+    message.numberOfLines   = 5;
+    message.text            = CTAssetsPickerLocalizedString(@"You can enable access in Privacy Settings.", nil);
+    self.message = message;
+    
+    [self addSubview:self.padlock];
+    [self addSubview:self.title];
+    [self addSubview:self.message];
+}
+
+- (UIImageView *)padlockImageView
+{
+    UIImage *image = [UIImage ctassetsPickerImageNamed:@"AccessDeniedViewLock"];
+    image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+    
+    UIImageView *padlock = [[UIImageView alloc] initWithImage:image];
+    padlock.tintColor    = CTAssetsPikcerAccessDeniedViewTextColor;
+    
+    return padlock;
+}
+
+#pragma mark - Update auto layout constraints
+
+- (void)updateConstraints
+{
+    if (!self.didSetupConstraints)
+    {
+        [self autoCenterInSuperview];
+        
+        // suggested solution for issue #176
+        [self autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:self.layoutMargins.top];
+        [self autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:self.layoutMargins.bottom];
+
+        [self.padlock autoAlignAxisToSuperviewAxis:ALAxisVertical];
+        [self.padlock autoPinEdgeToSuperviewEdge:ALEdgeTop];
+        [self.title autoAlignAxis:ALAxisVertical toSameAxisOfView:self.padlock];
+        [self.title autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.padlock withOffset:20];
+        [self.title autoPinEdgeToSuperviewEdge:ALEdgeLeading];
+        [self.title autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
+        [self.message autoAlignAxis:ALAxisVertical toSameAxisOfView:self.padlock];
+        [self.message autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.title withOffset:10];
+        [self.message autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero excludingEdge:ALEdgeTop];
+        
+        self.didSetupConstraints = YES;
+    }
+    
+    [super updateConstraints];
+}
+
+
+@end

BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeAllPhotos@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeAllPhotos@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeBurst@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeBurst@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeFavorites@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeFavorites@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeLastImport@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeLastImport@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgePanorama@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgePanorama@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeRecentlyDeleted@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeRecentlyDeleted@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeScreenshots@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeScreenshots@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSelfPortraits@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSelfPortraits@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSloMoSmall.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSloMoSmall@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSloMoSmall@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSlomo@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSlomo@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapse@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapse@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapseSmall.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapseSmall@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeTimelapseSmall@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideo@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideo@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideoSmall.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideoSmall@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeVideoSmall@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/Checkmark.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/Checkmark@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/Checkmark@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkShadow.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkShadow@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkShadow@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkUnselected.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkUnselected@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/CheckmarkUnselected@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/DisclosureArrow.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/DisclosureArrow@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/DisclosureArrow@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridDisabledAsset.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridDisabledAsset@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridDisabledAsset@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbum.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbum@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbum@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbumShared.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbumShared@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyAlbumShared@3x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyCameraRoll.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyCameraRoll@2x.png


BIN
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/GridEmptyCameraRoll@3x.png


Some files were not shown because too many files changed in this diff