├── .gitmodules ├── Example ├── MUKMediaGallery Example │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── Resources │ │ ├── Images │ │ │ ├── palms.jpg │ │ │ ├── palms-thumbnail.jpg │ │ │ └── sea-movie-thumbnail.jpg │ │ ├── Movies │ │ │ └── sea.mp4 │ │ ├── Default-568h@2x.png │ │ └── Audio │ │ │ └── SexForModerns-StopTheClock_64kb.mp3 │ ├── CarouselViewController.h │ ├── ThumbnailsViewController.h │ ├── RootViewController.h │ ├── AppDelegate.h │ ├── main.m │ ├── MediaAsset.h │ ├── MediaAsset.m │ ├── MUKMediaGallery Example-Prefix.pch │ ├── RootViewController.m │ ├── MUKMediaGallery Example-Info.plist │ ├── AppDelegate.m │ ├── CarouselViewController.m │ ├── RootViewController.xib │ └── ThumbnailsViewController.m ├── MUKMediaGallery ExampleTests │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── MUKMediaGallery_ExampleTests.m │ └── MUKMediaGallery ExampleTests-Info.plist ├── Podfile ├── MUKMediaGallery Example.xcodeproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── Podfile.lock ├── MUKMediaGallery ├── Private │ ├── Views │ │ ├── MUKMediaGalleryToolbar.h │ │ ├── MUKMediaGallerySlider.h │ │ ├── MUKMediaGalleryToolbar.m │ │ ├── MUKMediaCarouselPlayerControlsView.h │ │ ├── MUKMediaGallerySlider.m │ │ ├── Cells │ │ │ ├── MUKMediaThumbnailCell.h │ │ │ └── MUKMediaThumbnailCell.m │ │ └── MUKMediaCarouselPlayerControlsView.m │ ├── MUKMediaCarouselFlowLayout.h │ ├── MUKMediaGalleryUtils.h │ ├── Model │ │ ├── MUKMediaAttributesCache.h │ │ ├── MUKMediaModelCache.h │ │ ├── MUKMediaAttributesCache.m │ │ └── MUKMediaModelCache.m │ ├── MUKMediaGalleryImageResizeOperation.h │ ├── View Controllers │ │ └── Carousel │ │ │ ├── Video │ │ │ ├── MUKMediaCarouselYouTubePlayerViewController.h │ │ │ ├── MUKMediaCarouselPlayerViewController.h │ │ │ ├── MUKMediaCarouselYouTubePlayerViewController.m │ │ │ └── MUKMediaCarouselPlayerViewController.m │ │ │ ├── Image │ │ │ ├── MUKMediaCarouselFullImageViewController.h │ │ │ └── MUKMediaCarouselFullImageViewController.m │ │ │ ├── MUKMediaCarouselItemViewController.h │ │ │ └── MUKMediaCarouselItemViewController.m │ ├── MUKMediaGalleryUtils.m │ ├── MUKMediaGalleryImageResizeOperation.m │ └── MUKMediaCarouselFlowLayout.m ├── Resources │ └── Images │ │ ├── audio_small.png │ │ ├── video_small.png │ │ ├── audio_small@2x.png │ │ ├── mediaPlayer_play.png │ │ ├── video_small@2x.png │ │ ├── mediaPlayer_pause.png │ │ ├── audio_big_transparent.png │ │ ├── mediaPlayer_pause@2x.png │ │ ├── mediaPlayer_play@2x.png │ │ ├── video_big_transparent.png │ │ ├── mediaPlayer_sliderThumb.png │ │ ├── audio_big_transparent@2x.png │ │ ├── mediaPlayer_sliderThumb@2x.png │ │ └── video_big_transparent@2x.png ├── MUKMediaGalleryResources.bundle │ ├── Info.plist │ ├── audio_small.png │ ├── video_small.png │ ├── audio_small@2x.png │ ├── mediaPlayer_play.png │ ├── video_small@2x.png │ ├── mediaPlayer_pause.png │ ├── audio_big_transparent.png │ ├── mediaPlayer_pause@2x.png │ ├── mediaPlayer_play@2x.png │ ├── video_big_transparent.png │ ├── mediaPlayer_scaleToFill.png │ ├── mediaPlayer_scaleToFit.png │ ├── mediaPlayer_sliderThumb.png │ ├── audio_big_transparent@2x.png │ ├── mediaPlayer_scaleToFill@2x.png │ ├── mediaPlayer_scaleToFit@2x.png │ ├── mediaPlayer_sliderThumb@2x.png │ └── video_big_transparent@2x.png ├── MUKMediaImageKind.h ├── MUKMediaGallery.h ├── MUKMediaAttributes.m ├── MUKMediaAttributes.h ├── MUKMediaImageScrollView.h ├── MUKMediaCarouselViewController.h ├── MUKMediaThumbnailsViewController.h ├── MUKMediaImageScrollView.m └── MUKMediaThumbnailsViewController.m ├── .gitignore ├── MUKMediaGallery.podspec ├── README.md ├── LICENSE └── MUKMediaGallery.xcworkspace └── contents.xcworkspacedata /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery ExampleTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/MUKMediaGalleryToolbar.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MUKMediaGalleryToolbar : UIToolbar 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/audio_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/audio_small.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/video_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/video_small.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/audio_small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/audio_small@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/mediaPlayer_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/mediaPlayer_play.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/video_small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/video_small@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/mediaPlayer_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/mediaPlayer_pause.png -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/Resources/Images/palms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/Example/MUKMediaGallery Example/Resources/Images/palms.jpg -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/Resources/Movies/sea.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/Example/MUKMediaGallery Example/Resources/Movies/sea.mp4 -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/Info.plist -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/audio_big_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/audio_big_transparent.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/mediaPlayer_pause@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/mediaPlayer_pause@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/mediaPlayer_play@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/mediaPlayer_play@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/video_big_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/video_big_transparent.png -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '6.0' 2 | workspace '../MUKMediaGallery' 3 | pod 'MUKMediaGallery', :path => '..' 4 | pod 'MUKToolkit', '~> 1.1' 5 | pod 'AFNetworking', '~> 1.3' -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/mediaPlayer_sliderThumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/mediaPlayer_sliderThumb.png -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/Resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/Example/MUKMediaGallery Example/Resources/Default-568h@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_small.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/video_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/video_small.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/audio_big_transparent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/audio_big_transparent@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/mediaPlayer_sliderThumb@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/mediaPlayer_sliderThumb@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/Resources/Images/video_big_transparent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/Resources/Images/video_big_transparent@2x.png -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/Resources/Images/palms-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/Example/MUKMediaGallery Example/Resources/Images/palms-thumbnail.jpg -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_small@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_play.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/video_small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/video_small@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/MUKMediaGallerySlider.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MUKMediaGallerySlider : UISlider 4 | @property (nonatomic) CGSize thumbOffset; 5 | @end 6 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_pause.png -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/Resources/Images/sea-movie-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/Example/MUKMediaGallery Example/Resources/Images/sea-movie-thumbnail.jpg -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_big_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_big_transparent.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_pause@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_pause@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_play@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_play@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/video_big_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/video_big_transparent.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaImageKind.h: -------------------------------------------------------------------------------- 1 | typedef NS_ENUM(NSInteger, MUKMediaImageKind) { 2 | MUKMediaImageKindNone = 0, 3 | MUKMediaImageKindThumbnail, 4 | MUKMediaImageKindFullSize 5 | }; 6 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFill.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFit.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_sliderThumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_sliderThumb.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGallery.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_big_transparent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/audio_big_transparent@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFill@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFill@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFit@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_scaleToFit@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_sliderThumb@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/mediaPlayer_sliderThumb@2x.png -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaGalleryResources.bundle/video_big_transparent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/MUKMediaGallery/MUKMediaGalleryResources.bundle/video_big_transparent@2x.png -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/Resources/Audio/SexForModerns-StopTheClock_64kb.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muccy/MUKMediaGallery/HEAD/Example/MUKMediaGallery Example/Resources/Audio/SexForModerns-StopTheClock_64kb.mp3 -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/CarouselViewController.h: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselViewController.h" 2 | 3 | @interface CarouselViewController : MUKMediaCarouselViewController 4 | @property (nonatomic) NSArray *mediaAssets; 5 | @end 6 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/ThumbnailsViewController.h: -------------------------------------------------------------------------------- 1 | #import "MUKMediaThumbnailsViewController.h" 2 | 3 | @interface ThumbnailsViewController : MUKMediaThumbnailsViewController 4 | @property (nonatomic) NSArray *mediaAssets; 5 | @end 6 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/MUKMediaCarouselFlowLayout.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MUKMediaCarouselFlowLayout : UICollectionViewFlowLayout 4 | 5 | + (CGFloat)fullPageWidthForFrame:(CGRect)frame spacing:(CGFloat)spacing; 6 | @end 7 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/MUKMediaGalleryToolbar.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaGalleryToolbar.h" 2 | 3 | @implementation MUKMediaGalleryToolbar 4 | 5 | - (CGSize)intrinsicContentSize { 6 | return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric); 7 | } 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/MUKMediaCarouselPlayerControlsView.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface MUKMediaCarouselPlayerControlsView : UIView 5 | - (instancetype)initWithMoviePlayerController:(MPMoviePlayerController *)moviePlayerController; 6 | @end 7 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/RootViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.h 3 | // MUKMediaGallery Example 4 | // 5 | // Created by Marco on 03/07/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RootViewController : UIViewController 12 | - (IBAction)goToGridButtonPressed:(id)sender; 13 | @end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcodeproj/*.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | *.xccheckout 20 | 21 | # Cocoapods 22 | Pods 23 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // MUKMediaGallery Example 4 | // 5 | // Created by Marco on 23/06/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/MUKMediaGallerySlider.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaGallerySlider.h" 2 | 3 | @implementation MUKMediaGallerySlider 4 | 5 | - (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value 6 | { 7 | CGRect thumbRect = [super thumbRectForBounds:bounds trackRect:rect value:value]; 8 | return CGRectOffset(thumbRect, self.thumbOffset.width, self.thumbOffset.height); 9 | } 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/MUKMediaGalleryUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef NS_ENUM(NSInteger, MUKMediaGalleryUIParadigm) { 4 | MUKMediaGalleryUIParadigmLayered = 0, 5 | MUKMediaGalleryUIParadigmGlossy 6 | }; 7 | 8 | @interface MUKMediaGalleryUtils : NSObject 9 | 10 | + (NSBundle *)resourcesBundle; 11 | + (UIImage *)imageNamed:(NSString *)name; 12 | 13 | + (MUKMediaGalleryUIParadigm)defaultUIParadigm; 14 | 15 | @end -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MUKMediaGallery Example 4 | // 5 | // Created by Marco on 23/06/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "AppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Model/MUKMediaAttributesCache.h: -------------------------------------------------------------------------------- 1 | #import "MUKMediaModelCache.h" 2 | #import "MUKMediaAttributes.h" 3 | 4 | @interface MUKMediaAttributesCache : MUKMediaModelCache 5 | 6 | // Search for attributes in cache. If attributes can not be found, it invokes 7 | // loadingHandler and caches 8 | - (MUKMediaAttributes *)mediaAttributesAtIndex:(NSInteger)index cacheIfNeeded:(BOOL)cache loadingHandler:(MUKMediaAttributes *(^)(void))loadingHandler; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/Cells/MUKMediaThumbnailCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MUKMediaThumbnailCell : UICollectionViewCell 4 | @property (nonatomic, weak, readonly) UIImageView *imageView; 5 | @property (nonatomic, weak, readonly) UIView *bottomView; 6 | @property (nonatomic, weak, readonly) UIImageView *bottomIconImageView; 7 | @property (nonatomic, weak, readonly) UILabel *captionLabel; 8 | 9 | + (void)drawBorderInsideRect:(CGRect)rect context:(CGContextRef)ctx; 10 | @end 11 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/MediaAsset.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaAsset.h 3 | // MUKMediaGallery Example 4 | // 5 | // Created by Marco on 26/06/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MediaAsset : NSObject 12 | @property (nonatomic) MUKMediaKind kind; 13 | @property (nonatomic) NSURL *thumbnailURL, *URL; 14 | @property (nonatomic) NSTimeInterval duration; 15 | @property (nonatomic) NSString *caption; 16 | 17 | - (instancetype)initWithKind:(MUKMediaKind)kind; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/MediaAsset.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaAsset.m 3 | // MUKMediaGallery Example 4 | // 5 | // Created by Marco on 26/06/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import "MediaAsset.h" 10 | 11 | @implementation MediaAsset 12 | 13 | - (id)init { 14 | return [self initWithKind:MUKMediaKindImage]; 15 | } 16 | 17 | - (instancetype)initWithKind:(MUKMediaKind)kind { 18 | self = [super init]; 19 | if (self) { 20 | _kind = kind; 21 | } 22 | 23 | return self; 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaAttributes.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaAttributes.h" 2 | #import 3 | 4 | @implementation MUKMediaAttributes 5 | 6 | - (id)init { 7 | return [self initWithKind:MUKMediaKindImage]; 8 | } 9 | 10 | - (instancetype)initWithKind:(MUKMediaKind)kind { 11 | self = [super init]; 12 | if (self) { 13 | _kind = kind; 14 | } 15 | 16 | return self; 17 | } 18 | 19 | - (void)setCaptionWithTimeInterval:(NSTimeInterval)interval { 20 | self.caption = [MUK stringRepresentationOfTimeInterval:interval]; 21 | } 22 | 23 | @end -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/MUKMediaGallery Example-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_3_0 10 | #warning "This project uses features only available in iOS SDK 3.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #import 17 | #import 18 | #import 19 | #endif 20 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Model/MUKMediaModelCache.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | extern NSUInteger const MUKMediaModelCacheDefaultCostLimit; // 10,000,000 4 | 5 | @interface MUKMediaModelCache : NSObject 6 | @property (nonatomic, readonly) NSCache *cache; 7 | @property (nonatomic, readonly) BOOL cacheNulls; 8 | 9 | // Cache is created with given count limit, plus a cost limit of MUKMediaModelCacheDefaultCostLimit 10 | - (instancetype)initWithCountLimit:(NSInteger)countLimit cacheNulls:(BOOL)cacheNulls; 11 | 12 | // With you set images it automatically sets a cost of width * height 13 | - (void)setObject:(id)object atIndex:(NSInteger)index; 14 | - (id)objectAtIndex:(NSInteger)index isNull:(BOOL *)isNull; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery ExampleTests/MUKMediaGallery_ExampleTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // MUKMediaGallery_ExampleTests.m 3 | // MUKMediaGallery ExampleTests 4 | // 5 | // Created by Marco on 23/06/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MUKMediaGallery_ExampleTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation MUKMediaGallery_ExampleTests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | 21 | // Set-up code here. 22 | } 23 | 24 | - (void)tearDown 25 | { 26 | // Tear-down code here. 27 | 28 | [super tearDown]; 29 | } 30 | 31 | - (void)testExample 32 | { 33 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/MUKMediaGalleryImageResizeOperation.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class MUKMediaGalleryImageResizeOperation; 4 | @protocol MUKMediaGalleryImageResizeOperationDrawingDelegate 5 | @required 6 | - (void)imageResizeOperation:(MUKMediaGalleryImageResizeOperation *)op drawOverResizedImageInContext:(CGContextRef)ctx; 7 | @end 8 | 9 | @interface MUKMediaGalleryImageResizeOperation : NSOperation 10 | // Input 11 | @property (nonatomic) UIImage *sourceImage; 12 | @property (nonatomic) CGSize boundingSize; 13 | @property (nonatomic) id userInfo; 14 | @property (nonatomic, weak) id drawingDelegate; 15 | 16 | // Output 17 | @property (nonatomic, readonly) UIImage *resizedImage; 18 | @end 19 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (1.3.4) 3 | - MUKMediaGallery (2.1.4): 4 | - MUKMediaGallery/ImageScrollView 5 | - MUKToolkit (~> 1.1) 6 | - XCDYouTubeKit (~> 2.0) 7 | - MUKMediaGallery/ImageScrollView (2.1.4): 8 | - MUKToolkit (~> 1.1) 9 | - XCDYouTubeKit (~> 2.0) 10 | - MUKToolkit (1.1.4) 11 | - XCDYouTubeKit (2.0.2) 12 | 13 | DEPENDENCIES: 14 | - AFNetworking (~> 1.3) 15 | - MUKMediaGallery (from `..`) 16 | - MUKToolkit (~> 1.1) 17 | 18 | EXTERNAL SOURCES: 19 | MUKMediaGallery: 20 | :path: .. 21 | 22 | SPEC CHECKSUMS: 23 | AFNetworking: 80c4e0652b08eb34e25b9c0ff3c82556fe5967b4 24 | MUKMediaGallery: 24756eefcc37167561e8aacfef5cc970fac886a0 25 | MUKToolkit: c780b0b11b1b1d7fa3df28f3cf20ae127858f749 26 | XCDYouTubeKit: 65d5f5b9fe4e731327ab61ecc253b8e70e5d5a4d 27 | 28 | COCOAPODS: 0.34.1 29 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery ExampleTests/MUKMediaGallery ExampleTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | it.melive.mukit.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Model/MUKMediaAttributesCache.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaAttributesCache.h" 2 | 3 | @implementation MUKMediaAttributesCache 4 | 5 | - (MUKMediaAttributes *)mediaAttributesAtIndex:(NSInteger)index cacheIfNeeded:(BOOL)cache loadingHandler:(MUKMediaAttributes *(^)(void))loadingHandler 6 | { 7 | BOOL nullAttributes = NO; 8 | MUKMediaAttributes *attributes = [self objectAtIndex:index isNull:&nullAttributes]; 9 | 10 | // User has chosen for this index 11 | if (attributes || nullAttributes) { 12 | return attributes; 13 | } 14 | 15 | // At this point attributes == nil for sure 16 | // Load from handler! 17 | if (loadingHandler) { 18 | attributes = loadingHandler(); 19 | } 20 | 21 | // Should cache it? 22 | if (cache) { 23 | [self setObject:attributes atIndex:index]; 24 | } 25 | 26 | return attributes; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/Video/MUKMediaCarouselYouTubePlayerViewController.h: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselPlayerViewController.h" 2 | 3 | @class MUKMediaCarouselYouTubePlayerViewController; 4 | @protocol MUKMediaCarouselYouTubePlayerViewControllerDelegate 5 | - (void)carouselYouTubePlayerViewController:(MUKMediaCarouselYouTubePlayerViewController *)viewController webView:(UIWebView *)webView didReceiveTapWithGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer; 6 | - (void)carouselYouTubePlayerViewController:(MUKMediaCarouselYouTubePlayerViewController *)viewController didFinishLoadingWebView:(UIWebView *)webView error:(NSError *)error; 7 | @end 8 | 9 | 10 | @interface MUKMediaCarouselYouTubePlayerViewController : MUKMediaCarouselPlayerViewController 11 | @property (nonatomic, weak) id delegate; 12 | 13 | - (void)setYouTubeURL:(NSURL *)youTubeURL; 14 | @end 15 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/Image/MUKMediaCarouselFullImageViewController.h: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselItemViewController.h" 2 | #import "MUKMediaImageKind.h" 3 | 4 | @class MUKMediaImageScrollView; 5 | @class MUKMediaCarouselFullImageViewController; 6 | @protocol MUKMediaCarouselFullImageViewControllerDelegate 7 | - (void)carouselFullImageViewController:(MUKMediaCarouselFullImageViewController *)viewController imageScrollViewDidReceiveTapWithGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer; 8 | @end 9 | 10 | 11 | @interface MUKMediaCarouselFullImageViewController : MUKMediaCarouselItemViewController 12 | @property (nonatomic, weak) iddelegate; 13 | 14 | @property (nonatomic, weak, readonly) MUKMediaImageScrollView *imageScrollView; 15 | @property (nonatomic, readonly) MUKMediaImageKind imageKind; 16 | 17 | - (void)setImage:(UIImage *)image ofKind:(MUKMediaImageKind)kind; 18 | @end 19 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/Video/MUKMediaCarouselPlayerViewController.h: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselItemViewController.h" 2 | #import 3 | 4 | @class MUKMediaCarouselPlayerViewController; 5 | @protocol MUKMediaCarouselPlayerViewControllerDelegate 6 | - (void)carouselPlayerViewControllerDidChangeNowPlayingMovie:(MUKMediaCarouselPlayerViewController *)viewController; 7 | - (void)carouselPlayerViewControllerDidChangePlaybackState:(MUKMediaCarouselPlayerViewController *)viewController; 8 | @end 9 | 10 | 11 | @interface MUKMediaCarouselPlayerViewController : MUKMediaCarouselItemViewController 12 | @property (nonatomic, weak) id delegate; 13 | @property (nonatomic, readonly) MPMoviePlayerController *moviePlayerController; 14 | 15 | - (void)setMediaURL:(NSURL *)mediaURL; 16 | - (void)setPlayerControlsHidden:(BOOL)hidden animated:(BOOL)animated completion:(void (^)(BOOL finished))completionHandler; 17 | @end 18 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaAttributes.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef NS_ENUM(NSInteger, MUKMediaKind) { 4 | MUKMediaKindImage = 0, 5 | MUKMediaKindAudio, 6 | MUKMediaKindVideo, 7 | MUKMediaKindYouTubeVideo 8 | }; 9 | 10 | /** 11 | Additional infos about a media item. 12 | */ 13 | @interface MUKMediaAttributes : NSObject 14 | /** 15 | Kind of media. 16 | 17 | It could be an image (default), an audio, a video or a YouTube video (see MUKMediaKind 18 | enumeration). 19 | */ 20 | @property (nonatomic) MUKMediaKind kind; 21 | /** 22 | Caption to show with a media item. 23 | 24 | In MUKMediaThumbnailsViewController caption should be short, like video duration. 25 | In MUKMediaCarouselViewController caption could be longer, like description of 26 | an image. 27 | */ 28 | @property (nonatomic, copy) NSString *caption; 29 | 30 | /** 31 | Default initializer. 32 | 33 | @param kind Kind of media to initialize. 34 | @return A new instance. 35 | */ 36 | - (instancetype)initWithKind:(MUKMediaKind)kind; 37 | 38 | /** 39 | Utility method to set caption formatting a time inteval. 40 | 41 | @param interval Time interval to format in `HH:mm:ss`. 42 | */ 43 | - (void)setCaptionWithTimeInterval:(NSTimeInterval)interval; 44 | 45 | @end -------------------------------------------------------------------------------- /MUKMediaGallery/Private/MUKMediaGalleryUtils.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaGalleryUtils.h" 2 | 3 | static NSString *const kResourcesBundleName = @"MUKMediaGalleryResources"; 4 | 5 | @implementation MUKMediaGalleryUtils 6 | 7 | + (NSBundle *)resourcesBundle { 8 | static NSBundle *bundle = nil; 9 | static dispatch_once_t onceToken; 10 | dispatch_once(&onceToken, ^{ 11 | NSURL *url = [[NSBundle mainBundle] URLForResource:kResourcesBundleName withExtension:@"bundle"]; 12 | bundle = [NSBundle bundleWithURL:url]; 13 | }); 14 | 15 | return bundle; 16 | } 17 | 18 | + (UIImage *)imageNamed:(NSString *)name { 19 | return [UIImage imageNamed:[kResourcesBundleName stringByAppendingFormat:@".bundle/%@", name]]; 20 | } 21 | 22 | + (MUKMediaGalleryUIParadigm)defaultUIParadigm { 23 | static MUKMediaGalleryUIParadigm paradigm; 24 | 25 | static dispatch_once_t onceToken; 26 | dispatch_once(&onceToken, ^{ 27 | NSInteger deviceSystemMajorVersion = [[[[UIDevice currentDevice] systemVersion] componentsSeparatedByString:@"."][0] integerValue]; 28 | paradigm = (deviceSystemMajorVersion < 7 ? MUKMediaGalleryUIParadigmGlossy : MUKMediaGalleryUIParadigmLayered); 29 | }); 30 | 31 | return paradigm; 32 | } 33 | 34 | @end -------------------------------------------------------------------------------- /MUKMediaGallery.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'MUKMediaGallery' 3 | s.version = '2.1.6' 4 | s.summary = 'A simple iOS library built to provide you a component which replicates Photos app functionalities.' 5 | s.license = { :type => 'BSD 3-Clause', :file => 'LICENSE' } 6 | s.platform = :ios, '6.0' 7 | s.homepage = 'https://github.com/muccy/MUKMediaGallery' 8 | s.author = { 9 | 'Marco Muccinelli' => 'muccymac@gmail.com' 10 | } 11 | s.source = { 12 | :git => 'https://github.com/muccy/MUKMediaGallery.git', 13 | :tag => s.version.to_s 14 | } 15 | s.compiler_flags = '-Wdocumentation' 16 | s.source_files = 'MUKMediaGallery/**/*.{h,m}' 17 | s.public_header_files = 'MUKMediaGallery/*.h' 18 | s.requires_arc = true 19 | s.frameworks = 'QuartzCore', 'MediaPlayer' 20 | s.resource_bundle = { 'MUKMediaGalleryResources' => 'MUKMediaGallery/Resources/Images/**' } 21 | 22 | s.dependency 'MUKToolkit', '~> 1.1' 23 | s.dependency 'XCDYouTubeKit', '~> 2.0' 24 | 25 | s.subspec "ImageScrollView" do |sp| 26 | sp.source_files = 'MUKMediaGallery/MUKMediaImageScrollView.{h,m}' 27 | sp.public_header_files = 'MUKMediaGallery/MUKMediaImageScrollView.h' 28 | sp.requires_arc = true 29 | end 30 | end -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/RootViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.m 3 | // MUKMediaGallery Example 4 | // 5 | // Created by Marco on 03/07/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import "RootViewController.h" 10 | #import "ThumbnailsViewController.h" 11 | 12 | @interface RootViewController () 13 | @property (nonatomic) BOOL pushedThumbnailsGridViewController; 14 | @end 15 | 16 | @implementation RootViewController 17 | 18 | - (void)viewWillAppear:(BOOL)animated { 19 | [super viewWillAppear:animated]; 20 | 21 | if (!self.pushedThumbnailsGridViewController) { 22 | self.pushedThumbnailsGridViewController = YES; 23 | 24 | double delayInSeconds = 1.0; 25 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 26 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 27 | ThumbnailsViewController *thumbnailsViewController = [[ThumbnailsViewController alloc] init]; 28 | [self.navigationController pushViewController:thumbnailsViewController animated:YES]; 29 | }); 30 | } 31 | } 32 | 33 | - (void)goToGridButtonPressed:(id)sender { 34 | ThumbnailsViewController *thumbnailsViewController = [[ThumbnailsViewController alloc] init]; 35 | [self.navigationController pushViewController:thumbnailsViewController animated:YES]; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/MUKMediaCarouselItemViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class MUKMediaCarouselItemViewController; 4 | @protocol MUKMediaCarouselItemViewControllerDelegate 5 | - (void)carouselItemViewControllerDidReceiveTap:(MUKMediaCarouselItemViewController *)viewController; 6 | @end 7 | 8 | // A page of carousel 9 | @interface MUKMediaCarouselItemViewController : UIViewController 10 | @property (nonatomic, weak) id delegate; 11 | @property (nonatomic, readonly) NSInteger mediaIndex; 12 | @property (nonatomic, weak, readonly) UIView *overlayView; 13 | @property (nonatomic, weak, readonly) UIActivityIndicatorView *activityIndicatorView; 14 | @property (nonatomic, weak, readonly) UILabel *captionLabel; 15 | @property (nonatomic, weak, readonly) UIView *captionBackgroundView; 16 | @property (nonatomic, weak, readonly) UIImageView *thumbnailImageView; 17 | 18 | - (instancetype)initWithMediaIndex:(NSInteger)idx; 19 | @end 20 | 21 | 22 | @interface MUKMediaCarouselItemViewController (Caption) 23 | - (BOOL)isCaptionHidden; 24 | - (void)setCaptionHidden:(BOOL)hidden animated:(BOOL)animated completion:(void (^)(BOOL finished))completionHandler; 25 | @end 26 | 27 | 28 | @interface MUKMediaCarouselItemViewController (Thumbnail) 29 | - (void)createThumbnailImageViewIfNeededInSuperview:(UIView *)superview belowSubview:(UIView *)subview; 30 | @end 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MUKMediaGallery 2 | =============== 3 | MUKMediaGallery is a simple iOS 6+ library built to provide you a component which replicates Photos app functionalities. Classes provided by this project give you a fast path to show medias (photos, videos, audios) in you iOS app. 4 | This version is 2.0, a complete rewrite from previous version, which now can take benefit of `UICollectionView` and `UIPageViewController`. 5 | 6 | ![Thumbnails Grid](http://i.imgur.com/GgCG4at.jpg "Thumbnails Grid") ![Carousel](http://i.imgur.com/uOX7n48.jpg "Carousel") 7 | ![Video Playback](http://i.imgur.com/dnNsCQW.jpg "Video Playback") 8 | 9 | Requirements 10 | ------------ 11 | * ARC enabled compiler 12 | * Deployment target: iOS 6 or greater 13 | * Base SDK: iOS 7 or greater 14 | * Xcode 5 or greater 15 | 16 | Usage 17 | ----- 18 | See sample project to see usage. 19 | 20 | This framework basically contains two classes: 21 | 22 | * `MUKMediaThumbnailsViewController`, a view controller displaying a grid of thumbnails. 23 | * `MUKMediaCarouselViewController`, a view controller displaying a paginated list of photos, videos and audios. 24 | 25 | Installation 26 | ------------ 27 | Use Cocoapods. Really. 28 | 29 | pod 'MUKMediaGallery', '~> 2.0' 30 | 31 | Otherwise you need to: 32 | 33 | 1. add `MUKMediaGallery` folder to your project 34 | 2. add `MUKMediaGalleryResources.bundle` 35 | 3. link against `QuartzCore` and `MediaPlayer` frameworks 36 | 4. install `MUKToolkit` and `LBYouTubeView` libraries 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Marco Muccinelli 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the MeLive nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL MARCO MUCCINELLI BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MUKMediaGallery/Private/MUKMediaGalleryImageResizeOperation.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaGalleryImageResizeOperation.h" 2 | #import 3 | 4 | @interface MUKMediaGalleryImageResizeOperation () 5 | @property (nonatomic, readwrite) UIImage *resizedImage; 6 | @end 7 | 8 | @implementation MUKMediaGalleryImageResizeOperation 9 | 10 | - (void)main { 11 | if ([self isCancelled] || self.sourceImage == nil || CGSizeEqualToSize(self.boundingSize, CGSizeZero)) 12 | { 13 | return; 14 | } 15 | 16 | // Calculate max size 17 | CGRect imageRect = CGRectZero; 18 | imageRect.size = self.sourceImage.size; 19 | 20 | CGRect boundingRect = CGRectZero; 21 | boundingRect.size = self.boundingSize; 22 | 23 | imageRect = [MUK rect:imageRect transform:MUKGeometryTransformScaleAspectFill respectToRect:boundingRect]; 24 | 25 | if ([self isCancelled]) { 26 | return; 27 | } 28 | 29 | UIGraphicsBeginImageContextWithOptions(self.boundingSize, NO, 0.0f); 30 | 31 | // Draw image 32 | [self.sourceImage drawInRect:imageRect]; 33 | 34 | // Delegate could draw over image 35 | if (![self isCancelled] && [self.drawingDelegate respondsToSelector:@selector(imageResizeOperation:drawOverResizedImageInContext:)]) 36 | { 37 | CGContextRef ctx = UIGraphicsGetCurrentContext(); 38 | [self.drawingDelegate imageResizeOperation:self drawOverResizedImageInContext:ctx]; 39 | } 40 | 41 | // Capture composed image 42 | self.resizedImage = UIGraphicsGetImageFromCurrentImageContext(); 43 | 44 | UIGraphicsEndImageContext(); 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/MUKMediaGallery Example-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Media Gallery 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | it.melive.mukit.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | UIInterfaceOrientationPortraitUpsideDown 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaImageScrollView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class MUKMediaImageScrollView; 4 | /** 5 | A set of methods used by MUKMediaImageScrollView to communicate with its delegate 6 | instance. 7 | */ 8 | @protocol MUKMediaImageScrollViewDelegate 9 | @optional 10 | /** 11 | Notifies taps on image. This method is optional. 12 | 13 | @param imageScrollView The image scroll view which sends this info. 14 | @param tapCount Number of taps (it could be 1 or 2). 15 | @param gestureRecognizer The tap gesture recognizer which has intercepted the gesture. 16 | */ 17 | - (void)imageScrollView:(MUKMediaImageScrollView *)imageScrollView didReceiveTaps:(NSInteger)tapCount withGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer; 18 | @end 19 | 20 | 21 | 22 | /** 23 | A scroll view configured to host a zoomable image. 24 | 25 | This class sets itself as delegate implementing only 26 | - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView 27 | */ 28 | @interface MUKMediaImageScrollView : UIScrollView 29 | 30 | /** 31 | The object that acts as the delegate of the receiving image scroll view. 32 | */ 33 | @property (nonatomic, weak) id imageDelegate; 34 | 35 | /** 36 | Maximum magnifing factor applicable to unzoomed image. Defaults to 3.0f. 37 | */ 38 | @property (nonatomic) float maximumZoomFactor; 39 | 40 | /** 41 | Zoom applied to unzoomed image when a double tap gesture is detected. Defaults to 2.0f. 42 | */ 43 | @property (nonatomic) float doubleTapZoomFactor; 44 | 45 | /** 46 | The image currently displayed. 47 | */ 48 | @property (nonatomic, readonly) UIImage *image; 49 | 50 | /** 51 | Shows image unzoomed. 52 | 53 | @param image Image to display. 54 | */ 55 | - (void)displayImage:(UIImage *)image; 56 | @end 57 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Model/MUKMediaModelCache.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaModelCache.h" 2 | 3 | NSUInteger const MUKMediaModelCacheDefaultCostLimit = 10000000; 4 | 5 | @implementation MUKMediaModelCache 6 | 7 | - (instancetype)initWithCountLimit:(NSInteger)countLimit cacheNulls:(BOOL)cacheNulls 8 | { 9 | self = [super init]; 10 | if (self) { 11 | _cache = [[NSCache alloc] init]; 12 | _cache.countLimit = countLimit; 13 | _cache.totalCostLimit = MUKMediaModelCacheDefaultCostLimit; 14 | 15 | _cacheNulls = cacheNulls; 16 | 17 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 18 | [nc addObserver:self selector:@selector(didReceiveMemoryWarningNotification:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 19 | } 20 | 21 | return self; 22 | } 23 | 24 | - (id)init { 25 | return [self initWithCountLimit:0 cacheNulls:NO]; 26 | } 27 | 28 | - (void)dealloc { 29 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 30 | [nc removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 31 | } 32 | 33 | #pragma mark - Attributes 34 | 35 | - (void)setObject:(id)object atIndex:(NSInteger)index { 36 | if (object == nil) { 37 | if (self.cacheNulls) { 38 | object = [NSNull null]; 39 | } 40 | else { 41 | return; // Abort 42 | } 43 | } 44 | 45 | NSUInteger cost = 0; 46 | if ([object isKindOfClass:[UIImage class]]) { 47 | UIImage *image = object; 48 | cost = image.size.width * image.size.height; 49 | } 50 | 51 | [self.cache setObject:object forKey:@(index) cost:cost]; 52 | } 53 | 54 | - (id)objectAtIndex:(NSInteger)index isNull:(BOOL *)isNull { 55 | id object = [self.cache objectForKey:@(index)]; 56 | 57 | if (self.cacheNulls && object == [NSNull null]) { 58 | if (isNull != NULL) { 59 | *isNull = YES; 60 | } 61 | 62 | object = nil; 63 | } 64 | else { 65 | if (isNull != NULL) { 66 | *isNull = NO; 67 | } 68 | } 69 | 70 | return object; 71 | } 72 | 73 | #pragma mark - Private 74 | 75 | - (void)didReceiveMemoryWarningNotification:(NSNotification *)notification 76 | { 77 | [self.cache removeAllObjects]; // Just to be sure... 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/MUKMediaCarouselFlowLayout.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselFlowLayout.h" 2 | 3 | @implementation MUKMediaCarouselFlowLayout 4 | 5 | - (id)init { 6 | self = [super init]; 7 | if (self) { 8 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal; 9 | } 10 | 11 | return self; 12 | } 13 | 14 | + (CGFloat)fullPageWidthForFrame:(CGRect)frame spacing:(CGFloat)spacing { 15 | return frame.size.width + spacing; 16 | } 17 | 18 | #pragma mark - Overrides 19 | 20 | // I need this method because standard UIScrollView pagination is buggy: when 21 | // a photo is zoomed, sometimes it snaps to a wrong location (bouncing back). 22 | // Setting target manually both resolves this problem and simplify the process 23 | // of putting gaps between pages 24 | - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity 25 | { 26 | CGPoint targetOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset withScrollingVelocity:velocity]; 27 | 28 | CGFloat pageWidth = [[self class] fullPageWidthForFrame:self.collectionView.frame spacing:self.minimumLineSpacing]; 29 | NSInteger minPage = self.collectionView.contentOffset.x/pageWidth; 30 | 31 | NSInteger targetPage; 32 | 33 | // Slow dragging not lifting finger 34 | if (velocity.x == 0.0f) { 35 | CGFloat normalizedCurrentXOffset = fmodf(self.collectionView.contentOffset.x, pageWidth); 36 | 37 | if (normalizedCurrentXOffset > pageWidth/2.0f) { 38 | // Go to next page 39 | targetPage = minPage + 1; 40 | } 41 | else { 42 | // Stay to current page 43 | targetPage = minPage; 44 | } 45 | } 46 | 47 | // Fast dragging 48 | else { 49 | // Going back 50 | if (velocity.x < 0.0f) { 51 | targetPage = minPage; 52 | } 53 | 54 | // Going forward 55 | else { 56 | targetPage = minPage + 1; 57 | } 58 | } 59 | 60 | CGFloat targetX = pageWidth * targetPage; 61 | if (targetX < 0.0f) { 62 | targetX = 0.0f; 63 | } 64 | else if (targetX > self.collectionViewContentSize.width) 65 | { 66 | targetX = self.collectionViewContentSize.width - self.collectionView.frame.size.width; 67 | } 68 | 69 | targetOffset.x = targetX; 70 | return targetOffset; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // MUKMediaGallery Example 4 | // 5 | // Created by Marco on 23/06/13. 6 | // Copyright (c) 2013 MeLive. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "RootViewController.h" 11 | 12 | @implementation AppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | [[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES]; 17 | 18 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 19 | 20 | RootViewController *viewController = [[RootViewController alloc] init]; 21 | UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController]; 22 | self.window.rootViewController = navController; 23 | 24 | self.window.backgroundColor = [UIColor whiteColor]; 25 | [self.window makeKeyAndVisible]; 26 | return YES; 27 | } 28 | 29 | - (void)applicationWillResignActive:(UIApplication *)application 30 | { 31 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 32 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 33 | } 34 | 35 | - (void)applicationDidEnterBackground:(UIApplication *)application 36 | { 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | } 40 | 41 | - (void)applicationWillEnterForeground:(UIApplication *)application 42 | { 43 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 44 | } 45 | 46 | - (void)applicationDidBecomeActive:(UIApplication *)application 47 | { 48 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 49 | } 50 | 51 | - (void)applicationWillTerminate:(UIApplication *)application 52 | { 53 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/Image/MUKMediaCarouselFullImageViewController.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselFullImageViewController.h" 2 | #import "MUKMediaImageScrollView.h" 3 | 4 | @interface MUKMediaCarouselFullImageViewController () 5 | @property (nonatomic, weak, readwrite) MUKMediaImageScrollView *imageScrollView; 6 | @property (nonatomic, readwrite) MUKMediaImageKind imageKind; 7 | @end 8 | 9 | @implementation MUKMediaCarouselFullImageViewController 10 | 11 | - (instancetype)initWithMediaIndex:(NSInteger)idx { 12 | self = [super initWithMediaIndex:idx]; 13 | if (self) { 14 | _imageKind = MUKMediaImageKindNone; 15 | } 16 | 17 | return self; 18 | } 19 | 20 | #pragma mark - Methods 21 | 22 | - (void)setImage:(UIImage *)image ofKind:(MUKMediaImageKind)kind { 23 | switch (kind) { 24 | case MUKMediaImageKindFullSize: { 25 | // Create scroll view if needed 26 | if (self.imageScrollView == nil) { 27 | MUKMediaImageScrollView *imageScrollView = [[MUKMediaImageScrollView alloc] initWithFrame:self.view.bounds]; 28 | imageScrollView.imageDelegate = self; 29 | imageScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 30 | [self.view insertSubview:imageScrollView belowSubview:self.overlayView]; 31 | self.imageScrollView = imageScrollView; 32 | } 33 | 34 | // Remove thumbnail 35 | self.thumbnailImageView.image = nil; 36 | 37 | // Display image 38 | [self.imageScrollView displayImage:image]; 39 | 40 | break; 41 | } 42 | 43 | case MUKMediaImageKindThumbnail: { 44 | // Remove full image 45 | [self.imageScrollView removeFromSuperview]; 46 | 47 | // Display image 48 | [self createThumbnailImageViewIfNeededInSuperview:self.view belowSubview:self.overlayView]; 49 | self.thumbnailImageView.image = image; 50 | 51 | break; 52 | } 53 | 54 | default: { 55 | // Remove all 56 | [self.imageScrollView removeFromSuperview]; 57 | self.thumbnailImageView.image = nil; 58 | break; 59 | } 60 | } 61 | 62 | self.imageKind = kind; 63 | } 64 | 65 | #pragma mark - 66 | 67 | - (void)imageScrollView:(MUKMediaImageScrollView *)imageScrollView didReceiveTaps:(NSInteger)tapCount withGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer 68 | { 69 | if (tapCount == 1) { 70 | [self.delegate carouselFullImageViewController:self imageScrollViewDidReceiveTapWithGestureRecognizer:gestureRecognizer]; 71 | } 72 | } 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/CarouselViewController.m: -------------------------------------------------------------------------------- 1 | #import "CarouselViewController.h" 2 | #import "MediaAsset.h" 3 | 4 | @interface CarouselViewController () 5 | @property (nonatomic) NSOperationQueue *networkQueue; 6 | @end 7 | 8 | @implementation CarouselViewController 9 | 10 | - (id)init { 11 | self = [super init]; 12 | if (self) { 13 | self.title = @"Media Carousel"; 14 | self.carouselDelegate = self; 15 | _networkQueue = [[NSOperationQueue alloc] init]; 16 | _networkQueue.maxConcurrentOperationCount = 2; 17 | } 18 | 19 | return self; 20 | } 21 | 22 | - (void)viewDidLoad 23 | { 24 | [super viewDidLoad]; 25 | // Do any additional setup after loading the view. 26 | } 27 | 28 | - (void)didReceiveMemoryWarning 29 | { 30 | [super didReceiveMemoryWarning]; 31 | // Dispose of any resources that can be recreated. 32 | } 33 | 34 | #pragma mark - 35 | 36 | - (NSInteger)numberOfItemsInCarouselViewController:(MUKMediaCarouselViewController *)viewController 37 | { 38 | return [self.mediaAssets count]; 39 | } 40 | 41 | - (MUKMediaAttributes *)carouselViewController:(MUKMediaCarouselViewController *)viewController attributesForItemAtIndex:(NSInteger)idx 42 | { 43 | MediaAsset *asset = self.mediaAssets[idx]; 44 | MUKMediaAttributes *attributes = [[MUKMediaAttributes alloc] initWithKind:asset.kind]; 45 | attributes.caption = asset.caption; 46 | return attributes; 47 | } 48 | 49 | - (void)carouselViewController:(MUKMediaCarouselViewController *)viewController loadImageOfKind:(MUKMediaImageKind)imageKind forItemAtIndex:(NSInteger)idx completionHandler:(void (^)(UIImage *image))completionHandler 50 | { 51 | MediaAsset *asset = self.mediaAssets[idx]; 52 | NSURL *URL = (imageKind == MUKMediaImageKindFullSize ? asset.URL : asset.thumbnailURL); 53 | 54 | if (!URL) { 55 | completionHandler(nil); 56 | return; 57 | } 58 | 59 | NSURLRequest *request = [NSURLRequest requestWithURL:URL]; 60 | AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 61 | op.userInfo = @{ @"index" : @(idx), @"imageKind" : @(imageKind) }; 62 | 63 | [op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 64 | UIImage *image = nil; 65 | 66 | if ([responseObject length]) { 67 | image = [UIImage imageWithData:responseObject]; 68 | } 69 | 70 | completionHandler(image); 71 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 72 | completionHandler(nil); 73 | }]; 74 | 75 | [self.networkQueue addOperation:op]; 76 | } 77 | 78 | - (void)carouselViewController:(MUKMediaCarouselViewController *)viewController cancelLoadingForImageOfKind:(MUKMediaImageKind)imageKind atIndex:(NSInteger)idx 79 | { 80 | for (AFHTTPRequestOperation *op in self.networkQueue.operations) { 81 | if ([op.userInfo[@"index"] integerValue] == idx && 82 | [op.userInfo[@"imageKind"] integerValue] == imageKind) 83 | { 84 | [op cancel]; 85 | break; 86 | } 87 | } 88 | } 89 | 90 | - (NSURL *)carouselViewController:(MUKMediaCarouselViewController *)viewController mediaURLForItemAtIndex:(NSInteger)idx 91 | { 92 | MediaAsset *asset = self.mediaAssets[idx]; 93 | return asset.URL; 94 | } 95 | 96 | @end 97 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/RootViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaCarouselViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @class MUKMediaCarouselViewController; 5 | @class MUKMediaAttributes; 6 | /** 7 | A set of methods used by MUKMediaCarouselViewController to present media items 8 | properly. 9 | */ 10 | @protocol MUKMediaCarouselViewControllerDelegate 11 | 12 | @required 13 | /** 14 | Requests how many items should be presented. This method is required. 15 | 16 | @param viewController The carousel view controller which requests this info. 17 | @return Total number of media items which carousel view controller will present. 18 | */ 19 | - (NSInteger)numberOfItemsInCarouselViewController:(MUKMediaCarouselViewController *)viewController; 20 | 21 | /** 22 | Requests to load an image. This method is required. It is also required to call 23 | completionHandler when image is loaded. 24 | This method is called also of audios or videos (only with MUKMediaImageKindThumbnail). 25 | 26 | @param viewController The carousel view controller which requests this info. 27 | @param imageKind Kind of requested image. It could be a thumbnail or a full image. 28 | @param idx Media item index in carousel. 29 | @param completionHandler A block (which takes a UIImage parameter) which has to be called 30 | as image is loaded. 31 | */ 32 | - (void)carouselViewController:(MUKMediaCarouselViewController *)viewController loadImageOfKind:(MUKMediaImageKind)imageKind forItemAtIndex:(NSInteger)idx completionHandler:(void (^)(UIImage *image))completionHandler; 33 | 34 | /** 35 | Requests media URL for an audio or video item. This method is required. 36 | 37 | @param viewController The carousel view controller which requests this info. 38 | @param idx Media item index in carousel. 39 | @return Media item URL. This will be used to load video/audio internally. 40 | */ 41 | - (NSURL *)carouselViewController:(MUKMediaCarouselViewController *)viewController mediaURLForItemAtIndex:(NSInteger)idx; 42 | 43 | @optional 44 | /** 45 | Requests attributes for a media item. This method is optional. 46 | 47 | @param viewController The carousel view controller which requests this info. 48 | @param idx Media item index in carousel. 49 | @return Attributes for requested media item. If this method is not implemented or 50 | if it returns nil, it assumes the item is an image with no caption. 51 | */ 52 | - (MUKMediaAttributes *)carouselViewController:(MUKMediaCarouselViewController *)viewController attributesForItemAtIndex:(NSInteger)idx; 53 | 54 | /** 55 | Requests to cancel image loading. This method is optional, but you should implement 56 | it in order to optimize network usage. 57 | 58 | @param viewController The carousel view controller which sends this info. 59 | @param imageKind Kind of requested image. It could be a thumbnail or a full image. 60 | @param idx Media item index in carousel. 61 | */ 62 | - (void)carouselViewController:(MUKMediaCarouselViewController *)viewController cancelLoadingForImageOfKind:(MUKMediaImageKind)imageKind atIndex:(NSInteger)idx; 63 | 64 | @end 65 | 66 | 67 | 68 | /** 69 | A view controller which presents a paginated list of media items. 70 | */ 71 | @interface MUKMediaCarouselViewController : UIPageViewController 72 | 73 | /** 74 | The object that acts as the delegate of the receiving carousel view controller. 75 | */ 76 | @property (nonatomic, weak) id carouselDelegate; 77 | 78 | /** 79 | Scrolls the carousel to a media item at given index. 80 | 81 | @param index Media item index to reveal. 82 | @param animated If `YES` transition is animated. Otherwise, scroll is immediate. 83 | */ 84 | - (void)scrollToItemAtIndex:(NSInteger)index animated:(BOOL)animated completion:(void (^)(BOOL finished))completionHandler; 85 | @end 86 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/Cells/MUKMediaThumbnailCell.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaThumbnailCell.h" 2 | #import 3 | 4 | @interface MUKMediaThumbnailCell () 5 | @end 6 | 7 | @implementation MUKMediaThumbnailCell 8 | 9 | - (id)initWithFrame:(CGRect)frame { 10 | self = [super initWithFrame:frame]; 11 | if (self) { 12 | CGRect rect = self.contentView.bounds; 13 | UIView *backgroundView = [[UIView alloc] initWithFrame:rect]; 14 | self.backgroundView = backgroundView; 15 | 16 | UIView *selectionOverlayView = [[UIView alloc] initWithFrame:rect]; 17 | selectionOverlayView.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.4f]; 18 | self.selectedBackgroundView = selectionOverlayView; 19 | 20 | // Put everything in background view, so selected background view will 21 | // overlap contents when cell is tapped. Content view will remain empty 22 | 23 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:rect]; 24 | imageView.clipsToBounds = YES; 25 | imageView.contentMode = UIViewContentModeTopLeft; 26 | imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 27 | imageView.opaque = YES; 28 | [self.backgroundView addSubview:imageView]; 29 | _imageView = imageView; 30 | 31 | CGFloat const kBottomViewHeight = 17.0f; 32 | rect.origin.y = rect.size.height - kBottomViewHeight; 33 | rect.size.height = kBottomViewHeight; 34 | UIView *bottomView = [[UIView alloc] initWithFrame:rect]; 35 | bottomView.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.5f]; 36 | bottomView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleWidth; 37 | bottomView.userInteractionEnabled = NO; 38 | [self.backgroundView addSubview:bottomView]; 39 | _bottomView = bottomView; 40 | 41 | rect = bottomView.bounds; 42 | rect.origin.x = 6.0f; 43 | rect.size.width = 13.0f; 44 | UIImageView *bottomIconImageView = [[UIImageView alloc] initWithFrame:rect]; 45 | bottomIconImageView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleRightMargin|UIViewAutoresizingFlexibleBottomMargin; 46 | bottomIconImageView.contentMode = UIViewContentModeLeft; 47 | bottomIconImageView.backgroundColor = [UIColor clearColor]; 48 | [bottomView addSubview:bottomIconImageView]; 49 | _bottomIconImageView = bottomIconImageView; 50 | 51 | rect = bottomView.bounds; 52 | rect.origin.x = CGRectGetMaxX(bottomIconImageView.frame) + 4.0f; 53 | rect.size.width -= rect.origin.x + 4.0f; 54 | UILabel *captionLabel = [[UILabel alloc] initWithFrame:rect]; 55 | captionLabel.backgroundColor = [UIColor clearColor]; 56 | captionLabel.textColor = [UIColor whiteColor]; 57 | captionLabel.numberOfLines = 1; 58 | captionLabel.textAlignment = NSTextAlignmentRight; 59 | captionLabel.font = [UIFont boldSystemFontOfSize:11.0f]; 60 | captionLabel.autoresizingMask = UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleBottomMargin|UIViewAutoresizingFlexibleWidth; 61 | [bottomView addSubview:captionLabel]; 62 | _captionLabel = captionLabel; 63 | } 64 | 65 | return self; 66 | } 67 | 68 | - (void)setBackgroundColor:(UIColor *)backgroundColor { 69 | [super setBackgroundColor:backgroundColor]; 70 | self.imageView.backgroundColor = backgroundColor; 71 | } 72 | 73 | - (void)drawRect:(CGRect)rect { 74 | // Draw border for empty cells 75 | // Borders for filled cells are drawn over resized thumbnails as a huge 76 | // optimization 77 | 78 | if (!self.imageView.image) { 79 | [[self class] drawBorderInsideRect:self.bounds context:UIGraphicsGetCurrentContext()]; 80 | } 81 | } 82 | 83 | + (void)drawBorderInsideRect:(CGRect)rect context:(CGContextRef)ctx { 84 | rect = CGRectInset(rect, 1.0f, 1.0f); 85 | 86 | CGContextSaveGState(ctx); 87 | CGContextSetStrokeColorWithColor(ctx, [UIColor colorWithWhite:0.0f alpha:0.05f].CGColor); 88 | CGContextStrokeRect(ctx, rect); 89 | CGContextRestoreGState(ctx); 90 | } 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /MUKMediaGallery.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 33 | 34 | 36 | 37 | 40 | 43 | 45 | 46 | 48 | 49 | 51 | 52 | 54 | 55 | 56 | 58 | 59 | 61 | 62 | 64 | 65 | 67 | 68 | 70 | 71 | 73 | 74 | 77 | 80 | 82 | 83 | 85 | 86 | 89 | 91 | 92 | 94 | 95 | 96 | 99 | 101 | 102 | 104 | 105 | 107 | 108 | 110 | 111 | 112 | 113 | 114 | 117 | 120 | 122 | 123 | 125 | 126 | 127 | 129 | 130 | 132 | 133 | 135 | 136 | 138 | 139 | 141 | 142 | 144 | 145 | 146 | 147 | 149 | 150 | 151 | 153 | 154 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/Video/MUKMediaCarouselYouTubePlayerViewController.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselYouTubePlayerViewController.h" 2 | 3 | @interface MUKMediaCarouselYouTubePlayerViewController () 4 | @property (nonatomic, weak, readwrite) UIWebView *webView; 5 | @property (nonatomic) CGRect lastWebViewBounds; 6 | @end 7 | 8 | @implementation MUKMediaCarouselYouTubePlayerViewController 9 | 10 | - (void)dealloc { 11 | [self disposeWebView]; 12 | } 13 | 14 | - (instancetype)initWithMediaIndex:(NSInteger)idx { 15 | self = [super initWithMediaIndex:idx]; 16 | if (self) { 17 | _lastWebViewBounds = CGRectNull; 18 | } 19 | 20 | return self; 21 | } 22 | 23 | - (void)viewDidLoad { 24 | [super viewDidLoad]; 25 | [self createThumbnailImageViewIfNeededInSuperview:self.view belowSubview:self.overlayView]; 26 | } 27 | 28 | - (void)viewDidLayoutSubviews { 29 | [super viewDidLayoutSubviews]; 30 | 31 | // Do the best to resize web view embed 32 | if (self.webView.superview != nil) { 33 | if (!CGRectIsNull(self.lastWebViewBounds)) { 34 | if (!CGSizeEqualToSize(self.lastWebViewBounds.size, self.webView.bounds.size)) 35 | { 36 | [self updateYouTubeEmbedInWebView:self.webView toSize:self.webView.bounds.size]; 37 | } 38 | } 39 | 40 | self.lastWebViewBounds = self.webView.bounds; 41 | } 42 | } 43 | 44 | #pragma mark - Methods 45 | 46 | - (void)setYouTubeURL:(NSURL *)youTubeURL { 47 | if (youTubeURL == nil) { 48 | [self disposeWebView]; 49 | return; 50 | } 51 | 52 | // Create web view if needed 53 | if (self.webView == nil) { 54 | UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds]; 55 | webView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 56 | webView.allowsInlineMediaPlayback = YES; 57 | webView.mediaPlaybackRequiresUserAction = YES; 58 | webView.opaque = NO; 59 | webView.backgroundColor = [UIColor clearColor]; 60 | webView.multipleTouchEnabled = NO; 61 | webView.scrollView.scrollEnabled = NO; 62 | webView.delegate = self; 63 | 64 | UIView *relativeView; 65 | if ([self.thumbnailImageView.superview isEqual:self.view]) { 66 | relativeView = self.thumbnailImageView; 67 | } 68 | else { 69 | relativeView = self.overlayView; 70 | } 71 | 72 | [self.view insertSubview:webView belowSubview:relativeView]; 73 | self.webView = webView; 74 | 75 | UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleWebViewTap:)]; 76 | tapGestureRecognizer.delegate = self; 77 | [webView addGestureRecognizer:tapGestureRecognizer]; 78 | } 79 | 80 | self.lastWebViewBounds = self.webView.bounds; 81 | 82 | // Load HTML embed 83 | NSString *html = [self youTubeEmbedForURL:youTubeURL size:self.view.bounds.size]; 84 | [self.webView loadHTMLString:html baseURL:nil]; 85 | } 86 | 87 | #pragma mark - Overrides 88 | 89 | - (void)setMediaURL:(NSURL *)mediaURL { 90 | // Keep a strong reference to current thumbnail 91 | UIImage *thumbnail = self.thumbnailImageView.image; 92 | 93 | // This recreates thumnail image view in correct position 94 | [super setMediaURL:mediaURL]; 95 | 96 | // Restore past thumbnail 97 | self.thumbnailImageView.image = thumbnail; 98 | 99 | // Remove web view 100 | [self disposeWebView]; 101 | } 102 | 103 | #pragma mark - Private 104 | 105 | - (void)disposeWebView { 106 | [self.webView loadHTMLString:@"" baseURL:nil]; 107 | self.webView.delegate = nil; 108 | [self.webView removeFromSuperview]; 109 | self.webView = nil; 110 | } 111 | 112 | - (NSString *)youTubeEmbedForURL:(NSURL *)url size:(CGSize)size { 113 | static NSString *const kEmbedHTMLMask = @" \ 115 | \ 116 | "; 118 | 119 | NSString *URLString = [[url absoluteString] stringByReplacingOccurrencesOfString:@"watch?v=" withString:@"v/"]; 120 | return [NSString stringWithFormat:kEmbedHTMLMask, URLString, size.width, size.height]; 121 | } 122 | 123 | - (void)updateYouTubeEmbedInWebView:(UIWebView *)webView toSize:(CGSize)size { 124 | static NSString *const kJSCommandMask = @"\ 125 | (function (width, height) {\ 126 | var embeds = document.getElementsByTagName('embed');\ 127 | if (embeds.length == 0) return;\ 128 | var embed = embeds[0];\ 129 | embed.width = width;\ 130 | embed.height = height;\ 131 | })(%.0f, %.0f);\ 132 | "; 133 | 134 | NSString *command = [[NSString alloc] initWithFormat:kJSCommandMask, size.width, size.height]; 135 | [webView stringByEvaluatingJavaScriptFromString:command]; 136 | } 137 | 138 | #pragma mark - Private — Gesture Recognizers 139 | 140 | - (void)handleWebViewTap:(UITapGestureRecognizer *)recognizer { 141 | if (recognizer.state == UIGestureRecognizerStateEnded) { 142 | [self.delegate carouselYouTubePlayerViewController:self webView:self.webView didReceiveTapWithGestureRecognizer:recognizer]; 143 | } 144 | } 145 | 146 | #pragma mark - 147 | 148 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 149 | { 150 | return YES; 151 | } 152 | 153 | #pragma mark - 154 | 155 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 156 | [self.delegate carouselYouTubePlayerViewController:self didFinishLoadingWebView:webView error:nil]; 157 | } 158 | 159 | - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error 160 | { 161 | [self.delegate carouselYouTubePlayerViewController:self didFinishLoadingWebView:webView error:error]; 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaThumbnailsViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef NS_ENUM(NSInteger, MUKMediaThumbnailsViewControllerToCarouselTransition) 4 | { 5 | MUKMediaThumbnailsViewControllerToCarouselTransitionPush, 6 | MUKMediaThumbnailsViewControllerToCarouselTransitionCoverVertical, 7 | MUKMediaThumbnailsViewControllerToCarouselTransitionCrossDissolve 8 | }; 9 | 10 | @class MUKMediaThumbnailsViewController; 11 | @class MUKMediaCarouselViewController; 12 | @class MUKMediaAttributes; 13 | /** 14 | A set of methods used by MUKMediaThumbnailsViewController to present media items 15 | properly. 16 | */ 17 | @protocol MUKMediaThumbnailsViewControllerDelegate 18 | 19 | @required 20 | /** 21 | Requests how many items should be presented. This method is required. 22 | 23 | @param viewController The thumbnails view controller which requests this info. 24 | @return Total number of media items which thumbnails view controller will present. 25 | */ 26 | - (NSInteger)numberOfItemsInThumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController; 27 | 28 | /** 29 | Requests to load an image. This method is required. It is also required to call 30 | completionHandler when image is loaded. 31 | 32 | @param viewController The thumbnails view controller which requests this info. 33 | @param idx Media item index in grid. 34 | @param completionHandler A block (which takes a UIImage parameter) which has to be called 35 | as image is loaded. 36 | */ 37 | - (void)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController loadImageForItemAtIndex:(NSInteger)idx completionHandler:(void (^)(UIImage *image))completionHandler; 38 | 39 | @optional 40 | /** 41 | Requests to cancel image loading. This method is optional, but you should implement 42 | it in order to optimize network usage. 43 | 44 | @param viewController The thumbnails view controller which sends this info. 45 | @param idx Media item index in grid. 46 | */ 47 | - (void)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController cancelLoadingForImageAtIndex:(NSInteger)idx; 48 | 49 | /** 50 | Requests attributes for a media item. This method is optional. 51 | 52 | @param viewController The thumbnails view controller which requests this info. 53 | @param idx Media item index in carousel. 54 | @return Attributes for requested media item. If this method is not implemented or 55 | if it returns nil, it assumes the item is an image with no caption. 56 | */ 57 | - (MUKMediaAttributes *)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController attributesForItemAtIndex:(NSInteger)idx; 58 | 59 | /** 60 | Requests a carousel to display bigger selected image. This method is optional. 61 | 62 | @param viewController The thumbnails view controller which requests this info. 63 | @param idx Media item index in carousel. 64 | @return An initialized carousel view controller. If this method is not implemented 65 | (or it returns nil), no carousel will be presented. 66 | -[MUKMediaCarouselViewController scrollToItemAtIndex:animated:] is automatically 67 | called on returned instance. 68 | */ 69 | - (MUKMediaCarouselViewController *)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController carouselToPresentAfterSelectingItemAtIndex:(NSInteger)idx; 70 | 71 | /** 72 | How do you want to present carousel? You can choose here. 73 | If not implemented, MUKMediaThumbnailsViewControllerToCarouselTransitionPush 74 | is default. 75 | 76 | @param viewController The thumbnails view controller which requests this info. 77 | @param carouselViewController The carousel view controller which 78 | will be presented. 79 | @param idx Media item index in carousel. 80 | @return Transition style. 81 | */ 82 | - (MUKMediaThumbnailsViewControllerToCarouselTransition)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController transitionToPresentCarouselViewController:(MUKMediaCarouselViewController *)carouselViewController forItemAtIndex:(NSInteger)idx; 83 | 84 | /** 85 | Where transition is applied. 86 | 87 | @param viewController The thumbnails view controller which requests this info. 88 | @param carouselViewController The carousel view controller which 89 | will be presented. 90 | @param idx Media item index in carousel. 91 | @return A navigation controller if you have chosen a push style. 92 | A generic view controller if you have chosen a cover vertical style. 93 | */ 94 | - (UIViewController *)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController presentationViewControllerForCarouselViewController:(MUKMediaCarouselViewController *)carouselViewController forItemAtIndex:(NSInteger)idx; 95 | 96 | /** 97 | The view controller which is pushed/presented. 98 | 99 | @param viewController The thumbnails view controller which requests this info. 100 | @param proposedViewController The view controller the thumbnails view controller 101 | tries to present naturally. 102 | @param carouselViewController The carousel view controller which 103 | will be presented. 104 | @param idx Media item index in carousel. 105 | @return A proper view controller to be pushed/presented. You can use proposedViewController 106 | to perform some kind of containment, if needed. If you return nil, proposedViewController 107 | is used as default (the same behaviour you get if you don't implement this method). 108 | */ 109 | - (UIViewController *)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController viewControllerToPresent:(UIViewController *)proposedViewController toShowCarouselViewController:(MUKMediaCarouselViewController *)carouselViewController forItemAtIndex:(NSInteger)idx; 110 | 111 | @end 112 | 113 | 114 | 115 | /** 116 | A view controller which presents a grid of media item thumbnails. 117 | */ 118 | @interface MUKMediaThumbnailsViewController : UICollectionViewController 119 | 120 | /** 121 | The object that acts as the delegate of the receiving thumbnails view controller. 122 | */ 123 | @property (nonatomic, weak) id delegate; 124 | 125 | /** 126 | Size of thumbnail cell. 127 | Default: 75x75 for phones, 104x104 for pads. 128 | */ 129 | @property (nonatomic) CGSize thumbnailCellSize; 130 | 131 | /** 132 | Spacing between thumbnail cells. 133 | Default: 4.0 for phones, 5.0 for pads. 134 | */ 135 | @property (nonatomic) CGFloat thumbnailCellSpacing; 136 | 137 | /** 138 | Empties caches and reloads underlying collection view, requesting every info 139 | from scratch. 140 | Remember to cancel your image loadings before to call this method. 141 | */ 142 | - (void)reloadData; 143 | @end 144 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/Video/MUKMediaCarouselPlayerViewController.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselPlayerViewController.h" 2 | #import "MUKMediaCarouselPlayerControlsView.h" 3 | 4 | @interface MUKMediaCarouselPlayerViewController () 5 | @property (nonatomic, readwrite) MPMoviePlayerController *moviePlayerController; 6 | @property (nonatomic, weak) MUKMediaCarouselPlayerControlsView *playerControlsView; 7 | @property (nonatomic, getter = isRegisteredToMoviePlayerControllerNotifications) BOOL registeredToMoviePlayerControllerNotifications; 8 | @property (nonatomic) NSTimer *playerControlsHideTimer; 9 | @end 10 | 11 | @implementation MUKMediaCarouselPlayerViewController 12 | 13 | - (void)dealloc { 14 | [self cancelPlayerControlsHideTimer]; 15 | [self unregisterFromMoviePlayerControllerNotifications]; 16 | } 17 | 18 | #pragma mark - Methods 19 | 20 | - (void)setMediaURL:(NSURL *)mediaURL { 21 | if (mediaURL == nil) { 22 | [self.moviePlayerController stop]; 23 | [self.moviePlayerController.view removeFromSuperview]; 24 | self.moviePlayerController = nil; 25 | return; 26 | } 27 | 28 | if (self.moviePlayerController == nil) { 29 | self.moviePlayerController = [[MPMoviePlayerController alloc] initWithContentURL:mediaURL]; 30 | self.moviePlayerController.shouldAutoplay = NO; 31 | self.moviePlayerController.controlStyle = MPMovieControlStyleNone; 32 | 33 | self.moviePlayerController.view.frame = self.view.bounds; 34 | self.moviePlayerController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 35 | [self.view insertSubview:self.moviePlayerController.view belowSubview:self.overlayView]; 36 | 37 | // This disables two finger gesture to enter fullscreen 38 | UIView *coverView = [[UIView alloc] initWithFrame:self.moviePlayerController.view.bounds]; 39 | coverView.backgroundColor = [UIColor clearColor]; 40 | coverView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 41 | [self.moviePlayerController.view addSubview:coverView]; 42 | 43 | // Create custom controls 44 | MUKMediaCarouselPlayerControlsView *controlsView = [[MUKMediaCarouselPlayerControlsView alloc] initWithMoviePlayerController:self.moviePlayerController]; 45 | self.playerControlsView = controlsView; 46 | [self.moviePlayerController.view addSubview:controlsView]; 47 | 48 | NSDictionary *viewsDict = @{ 49 | @"controls" : controlsView, 50 | @"captionBackground" : self.captionBackgroundView 51 | }; 52 | 53 | NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-(0)-[controls]-(0)-|" options:0 metrics:nil views:viewsDict]; 54 | [self.moviePlayerController.view addConstraints:constraints]; 55 | 56 | constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[controls]-(0)-[captionBackground]" options:0 metrics:nil views:viewsDict]; 57 | [self.view addConstraints:constraints]; 58 | 59 | // Create room for thumbnail 60 | [self createThumbnailImageViewIfNeededInSuperview:self.moviePlayerController.view belowSubview:self.playerControlsView]; 61 | 62 | // Register to notifications 63 | [self registerToMoviePlayerControllerNotifications:self.moviePlayerController]; 64 | } 65 | else { 66 | self.moviePlayerController.contentURL = mediaURL; 67 | } 68 | 69 | // Show 70 | [self setPlayerControlsHidden:NO animated:NO completion:nil]; 71 | } 72 | 73 | - (void)setPlayerControlsHidden:(BOOL)hidden animated:(BOOL)animated completion:(void (^)(BOOL finished))completionHandler 74 | { 75 | // Requested action invalidates automatic one 76 | [self cancelPlayerControlsHideTimer]; 77 | 78 | NSTimeInterval const kDuration = animated ? UINavigationControllerHideShowBarDuration : 0.0; 79 | 80 | [UIView animateWithDuration:kDuration animations:^{ 81 | self.playerControlsView.alpha = (hidden ? 0.0f : 1.0f); 82 | } completion:completionHandler]; 83 | } 84 | 85 | #pragma mark - Private — Player Controls 86 | 87 | - (void)startPlayerControlsHideTimer { 88 | [self cancelPlayerControlsHideTimer]; 89 | self.playerControlsHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(playerControlsHideTimerFired:) userInfo:nil repeats:NO]; 90 | } 91 | 92 | - (void)cancelPlayerControlsHideTimer { 93 | if ([self.playerControlsHideTimer isValid]) { 94 | [self.playerControlsHideTimer invalidate]; 95 | self.playerControlsHideTimer = nil; 96 | } 97 | } 98 | 99 | - (void)playerControlsHideTimerFired:(NSTimer *)timer { 100 | [self setPlayerControlsHidden:YES animated:YES completion:nil]; 101 | } 102 | 103 | #pragma mark - Private — Movie Player Controller Notifications 104 | 105 | - (void)registerToMoviePlayerControllerNotifications:(MPMoviePlayerController *)moviePlayerController 106 | { 107 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 108 | [nc addObserver:self selector:@selector(moviePlayerControllerNowPlayingMovieChangedNotification:) name:MPMoviePlayerNowPlayingMovieDidChangeNotification object:moviePlayerController]; 109 | [nc addObserver:self selector:@selector(moviePlayerControllerPlaybackStateDidChangeNotification:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:moviePlayerController]; 110 | 111 | self.registeredToMoviePlayerControllerNotifications = YES; 112 | } 113 | 114 | - (void)unregisterFromMoviePlayerControllerNotifications { 115 | if (self.isRegisteredToMoviePlayerControllerNotifications) { 116 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 117 | [nc removeObserver:self name:MPMoviePlayerNowPlayingMovieDidChangeNotification object:nil]; 118 | [nc removeObserver:self name:MPMoviePlayerPlaybackStateDidChangeNotification object:nil]; 119 | 120 | self.registeredToMoviePlayerControllerNotifications = NO; 121 | } 122 | } 123 | 124 | - (void)moviePlayerControllerNowPlayingMovieChangedNotification:(NSNotification *)notification 125 | { 126 | [self.delegate carouselPlayerViewControllerDidChangeNowPlayingMovie:self]; 127 | } 128 | 129 | - (void)moviePlayerControllerPlaybackStateDidChangeNotification:(NSNotification *)notifcation 130 | { 131 | [self.delegate carouselPlayerViewControllerDidChangePlaybackState:self]; 132 | 133 | // When media starts playing, hide controls after a while 134 | if (self.moviePlayerController.playbackState == MPMoviePlaybackStatePlaying) 135 | { 136 | [self startPlayerControlsHideTimer]; 137 | } 138 | else { 139 | [self cancelPlayerControlsHideTimer]; 140 | } 141 | } 142 | 143 | @end 144 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaImageScrollView.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaImageScrollView.h" 2 | 3 | @interface MUKMediaImageScrollView () 4 | @property (nonatomic, readwrite) UITapGestureRecognizer *tapGestureRecognizer, *doubleTapGestureRecognizer; 5 | 6 | @property (nonatomic) UIImageView *zoomView; 7 | @property (nonatomic) CGSize imageSize; 8 | @property (nonatomic) CGPoint pointToCenterAfterResize; 9 | @property (nonatomic) CGFloat scaleToRestoreAfterResize; 10 | @end 11 | 12 | @implementation MUKMediaImageScrollView 13 | @dynamic image; 14 | 15 | - (id)initWithFrame:(CGRect)frame { 16 | self = [super initWithFrame:frame]; 17 | if (self) { 18 | CommonInitialization(self); 19 | } 20 | 21 | return self; 22 | } 23 | 24 | - (id)initWithCoder:(NSCoder *)aDecoder { 25 | self = [super initWithCoder:aDecoder]; 26 | if (self) { 27 | CommonInitialization(self); 28 | } 29 | 30 | return self; 31 | } 32 | 33 | #pragma mark - Overrides 34 | 35 | - (void)layoutSubviews { 36 | [super layoutSubviews]; 37 | 38 | // center the zoom view as it becomes smaller than the size of the screen 39 | CGSize boundsSize = self.bounds.size; 40 | CGRect frameToCenter = self.zoomView.frame; 41 | 42 | // center horizontally 43 | if (frameToCenter.size.width < boundsSize.width) { 44 | frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2; 45 | } 46 | else { 47 | frameToCenter.origin.x = 0.0f; 48 | } 49 | 50 | // center vertically 51 | if (frameToCenter.size.height < boundsSize.height) { 52 | frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2; 53 | } 54 | else { 55 | frameToCenter.origin.y = 0.0f; 56 | } 57 | 58 | self.zoomView.frame = frameToCenter; 59 | } 60 | 61 | - (void)setFrame:(CGRect)frame { 62 | BOOL sizeChanging = !CGSizeEqualToSize(frame.size, self.frame.size); 63 | 64 | if (sizeChanging) { 65 | [self prepareToResize]; 66 | } 67 | 68 | [super setFrame:frame]; 69 | 70 | if (sizeChanging) { 71 | [self recoverFromResizing]; 72 | } 73 | } 74 | 75 | #pragma mark - Accessors 76 | 77 | - (void)setMaximumZoomFactor:(float)maximumZoomFactor { 78 | if (maximumZoomFactor != _maximumZoomFactor) { 79 | _maximumZoomFactor = maximumZoomFactor; 80 | [self setMaxMinZoomScalesForCurrentBounds]; 81 | } 82 | } 83 | 84 | #pragma mark - Methods 85 | 86 | - (void)displayImage:(UIImage *)image { 87 | // clear the previous image 88 | [self.zoomView removeFromSuperview]; 89 | 90 | // reset our zoomScale to 1.0 before doing any further calculations 91 | self.zoomScale = 1.0; 92 | 93 | // make a new UIImageView for the new image 94 | self.zoomView = [[UIImageView alloc] initWithImage:image]; 95 | [self addSubview:self.zoomView]; 96 | 97 | [self configureForImageSize:image.size]; 98 | } 99 | 100 | - (UIImage *)image { 101 | return self.zoomView.image; 102 | } 103 | 104 | #pragma mark - Private 105 | 106 | static void CommonInitialization(MUKMediaImageScrollView *view) { 107 | view.maximumZoomFactor = 3.0f; 108 | view.doubleTapZoomFactor = 2.0f; 109 | 110 | view.showsVerticalScrollIndicator = NO; 111 | view.showsHorizontalScrollIndicator = NO; 112 | view.bouncesZoom = YES; 113 | view.decelerationRate = UIScrollViewDecelerationRateFast; 114 | view.delegate = view; 115 | 116 | [view setupGestureRecognizers]; 117 | } 118 | 119 | #pragma mark - Private — Layout 120 | 121 | - (BOOL)hasImageWithPositiveSize { 122 | return self.imageSize.width > 0.0f && self.imageSize.height > 0.0f; 123 | } 124 | 125 | - (void)configureForImageSize:(CGSize)imageSize { 126 | self.imageSize = imageSize; 127 | self.contentSize = imageSize; 128 | [self setMaxMinZoomScalesForCurrentBounds]; 129 | 130 | if ([self hasImageWithPositiveSize]) { 131 | self.zoomScale = self.minimumZoomScale; 132 | } 133 | } 134 | 135 | - (void)setMaxMinZoomScalesForCurrentBounds { 136 | CGSize boundsSize = self.bounds.size; 137 | 138 | CGFloat minScale, maxScale; 139 | 140 | if ([self hasImageWithPositiveSize]) { 141 | // calculate min/max zoomscale 142 | CGFloat xScale = boundsSize.width / self.imageSize.width; // the scale needed to perfectly fit the image width-wise 143 | CGFloat yScale = boundsSize.height / self.imageSize.height; // the scale needed to perfectly fit the image height-wise 144 | 145 | minScale = MIN(xScale, yScale); 146 | maxScale = minScale * self.maximumZoomFactor; 147 | 148 | // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) 149 | if (minScale > maxScale) { 150 | minScale = maxScale; 151 | } 152 | } 153 | else { 154 | minScale = maxScale = 1.0f; 155 | } 156 | 157 | self.minimumZoomScale = minScale; 158 | self.maximumZoomScale = maxScale; 159 | } 160 | 161 | #pragma mark - Private: Resizing Support 162 | 163 | - (void)prepareToResize { 164 | CGPoint boundsCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 165 | self.pointToCenterAfterResize = [self convertPoint:boundsCenter toView:self.zoomView]; 166 | 167 | self.scaleToRestoreAfterResize = self.zoomScale; 168 | 169 | // If we're at the minimum zoom scale, preserve that by returning 0, which 170 | // will be converted to the minimum allowable scale when the scale is restored. 171 | if (self.scaleToRestoreAfterResize <= self.minimumZoomScale + FLT_EPSILON) { 172 | self.scaleToRestoreAfterResize = 0.0f; 173 | } 174 | } 175 | 176 | - (void)recoverFromResizing { 177 | [self setMaxMinZoomScalesForCurrentBounds]; 178 | 179 | // Step 1: restore zoom scale, first making sure it is within the allowable range. 180 | CGFloat maxZoomScale = MAX(self.minimumZoomScale, self.scaleToRestoreAfterResize); 181 | self.zoomScale = MIN(self.maximumZoomScale, maxZoomScale); 182 | 183 | // Step 2: restore center point, first making sure it is within the allowable range. 184 | 185 | // 2a: convert our desired center point back to our own coordinate space 186 | CGPoint boundsCenter = [self convertPoint:self.pointToCenterAfterResize fromView:self.zoomView]; 187 | 188 | // 2b: calculate the content offset that would yield that center point 189 | CGPoint offset = CGPointMake(boundsCenter.x - self.bounds.size.width / 2.0f, 190 | boundsCenter.y - self.bounds.size.height / 2.0f); 191 | 192 | // 2c: restore offset, adjusted to be within the allowable range 193 | CGPoint maxOffset = [self maximumContentOffset]; 194 | CGPoint minOffset = [self minimumContentOffset]; 195 | 196 | CGFloat realMaxOffset = MIN(maxOffset.x, offset.x); 197 | offset.x = MAX(minOffset.x, realMaxOffset); 198 | 199 | realMaxOffset = MIN(maxOffset.y, offset.y); 200 | offset.y = MAX(minOffset.y, realMaxOffset); 201 | 202 | self.contentOffset = offset; 203 | } 204 | 205 | - (CGPoint)maximumContentOffset { 206 | CGSize contentSize = self.contentSize; 207 | CGSize boundsSize = self.bounds.size; 208 | return CGPointMake(contentSize.width - boundsSize.width, contentSize.height - boundsSize.height); 209 | } 210 | 211 | - (CGPoint)minimumContentOffset { 212 | return CGPointZero; 213 | } 214 | 215 | #pragma mark - Private — Zoom 216 | 217 | - (void)zoomToPoint:(CGPoint)point animated:(BOOL)animated { 218 | CGFloat newZoomScale = self.zoomScale * self.doubleTapZoomFactor; 219 | newZoomScale = MIN(newZoomScale, self.maximumZoomScale); 220 | 221 | CGSize scrollViewSize = self.bounds.size; 222 | 223 | CGFloat w = scrollViewSize.width / newZoomScale; 224 | CGFloat h = scrollViewSize.height / newZoomScale; 225 | CGFloat x = point.x - (w / 2.0f); 226 | CGFloat y = point.y - (h / 2.0f); 227 | 228 | CGRect rectToZoomTo = CGRectMake(x, y, w, h); 229 | [self zoomToRect:rectToZoomTo animated:animated]; 230 | } 231 | 232 | #pragma mark - Private — Gestures 233 | 234 | - (void)setupGestureRecognizers { 235 | self.doubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)]; 236 | [self.doubleTapGestureRecognizer setNumberOfTapsRequired:2]; 237 | [self addGestureRecognizer:self.doubleTapGestureRecognizer]; 238 | 239 | self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)]; 240 | [self.tapGestureRecognizer requireGestureRecognizerToFail:self.doubleTapGestureRecognizer]; 241 | [self addGestureRecognizer:self.tapGestureRecognizer]; 242 | } 243 | 244 | - (void)handleSingleTap:(UITapGestureRecognizer *)recognizer { 245 | if (recognizer.state == UIGestureRecognizerStateEnded) { 246 | if ([self.imageDelegate respondsToSelector:@selector(imageScrollView:didReceiveTaps:withGestureRecognizer:)]) 247 | { 248 | [self.imageDelegate imageScrollView:self didReceiveTaps:1 withGestureRecognizer:recognizer]; 249 | } 250 | } 251 | } 252 | 253 | - (void)handleDoubleTap:(UITapGestureRecognizer *)recognizer { 254 | if (recognizer.state == UIGestureRecognizerStateEnded) { 255 | // Zoom to point in not already zoomed in 256 | // Otherwise zoom back 257 | if (self.zoomScale > self.minimumZoomScale) { 258 | [self setZoomScale:self.minimumZoomScale animated:YES]; 259 | } 260 | else { 261 | CGPoint pointInView = [recognizer locationInView:self.zoomView]; 262 | [self zoomToPoint:pointInView animated:YES]; 263 | } 264 | 265 | // Inform delegate 266 | if ([self.imageDelegate respondsToSelector:@selector(imageScrollView:didReceiveTaps:withGestureRecognizer:)]) 267 | { 268 | [self.imageDelegate imageScrollView:self didReceiveTaps:2 withGestureRecognizer:recognizer]; 269 | } 270 | } 271 | } 272 | 273 | #pragma mark - 274 | 275 | - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { 276 | return self.zoomView; 277 | } 278 | 279 | @end 280 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/View Controllers/Carousel/MUKMediaCarouselItemViewController.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselItemViewController.h" 2 | #import "MUKMediaGalleryToolbar.h" 3 | #import "MUKMediaGalleryUtils.h" 4 | 5 | static CGFloat const kCaptionLabelMaxHeight = 80.0f; 6 | static CGFloat const kCaptionLabelLateralPadding = 8.0f; 7 | static CGFloat const kCaptionLabelBottomPadding = 5.0f; 8 | static CGFloat const kCaptionLabelTopPadding = 3.0f; 9 | 10 | @interface MUKMediaCarouselItemViewController () 11 | @property (nonatomic, weak, readwrite) UIView *overlayView; 12 | @property (nonatomic, weak, readwrite) UIActivityIndicatorView *activityIndicatorView; 13 | @property (nonatomic, weak, readwrite) UILabel *captionLabel; 14 | @property (nonatomic, weak, readwrite) UIView *captionBackgroundView; 15 | @property (nonatomic, weak, readwrite) UIImageView *thumbnailImageView; 16 | 17 | @property (nonatomic, strong) NSLayoutConstraint *captionLabelBottomConstraint, *captionLabelTopConstraint, *captionBackgroundViewBottomConstraint, *captionBackgroundViewTopConstraint; 18 | @end 19 | 20 | @implementation MUKMediaCarouselItemViewController 21 | 22 | - (void)dealloc { 23 | [self unregisterFromContentSizeCategoryNotifications]; 24 | } 25 | 26 | - (instancetype)initWithMediaIndex:(NSInteger)idx { 27 | self = [super initWithNibName:nil bundle:nil]; 28 | if (self) { 29 | _mediaIndex = idx; 30 | } 31 | 32 | return self; 33 | } 34 | 35 | - (void)viewDidLoad { 36 | [super viewDidLoad]; 37 | 38 | UIView *overlayView = [self newOverlayViewInSuperview:self.view]; 39 | self.overlayView = overlayView; 40 | 41 | UIActivityIndicatorView *activityIndicatorView = [self newCenteredActivityIndicatorViewInSuperview:overlayView]; 42 | self.activityIndicatorView = activityIndicatorView; 43 | 44 | UILabel *captionLabel = [self newBottomAttachedCaptionLabelInSuperview:overlayView]; 45 | self.captionLabel = captionLabel; 46 | 47 | UIView *captionBackgroundView = [self newBottomAttachedBackgroundViewForCaptionLabel:captionLabel inSuperview:overlayView]; 48 | self.captionBackgroundView = captionBackgroundView; 49 | 50 | [self updateCaptionConstraintsWhenHidden:NO]; 51 | [self registerToContentSizeCategoryNotifications]; 52 | [self attachTapGestureRecognizer]; 53 | } 54 | 55 | #pragma mark - Caption 56 | 57 | - (BOOL)isCaptionHidden { 58 | return self.captionBackgroundView.alpha < 1.0f; 59 | } 60 | 61 | - (void)setCaptionHidden:(BOOL)hidden animated:(BOOL)animated completion:(void (^)(BOOL finished))completionHandler 62 | { 63 | NSTimeInterval const duration = animated ? UINavigationControllerHideShowBarDuration : 0.0; 64 | 65 | if (hidden) { 66 | [UIView animateWithDuration:duration animations:^{ 67 | self.captionLabel.alpha = 0.0f; 68 | self.captionBackgroundView.alpha = 0.0f; 69 | } completion:^(BOOL finished) { 70 | if (finished) { 71 | [self updateCaptionConstraintsWhenHidden:hidden]; 72 | 73 | // Animate constraints change too, if requested 74 | [UIView animateWithDuration:duration animations:^{ 75 | [self.view layoutIfNeeded]; 76 | } completion:nil]; 77 | } 78 | 79 | // Notify completion immediately, because caption has already 80 | // disappeared 81 | if (completionHandler) { 82 | completionHandler(finished); 83 | } 84 | }]; 85 | } 86 | else { 87 | self.captionLabel.alpha = 1.0f; 88 | self.captionBackgroundView.alpha = 1.0f; 89 | 90 | [self updateCaptionConstraintsWhenHidden:hidden]; 91 | 92 | [UIView animateWithDuration:duration animations:^{ 93 | [self.view layoutIfNeeded]; 94 | } completion:completionHandler]; 95 | } 96 | } 97 | 98 | #pragma mark - Thumbnail 99 | 100 | - (void)createThumbnailImageViewIfNeededInSuperview:(UIView *)superview belowSubview:(UIView *)subview 101 | { 102 | if (![self.thumbnailImageView.superview isEqual:superview]) { 103 | [self.thumbnailImageView removeFromSuperview]; 104 | self.thumbnailImageView = nil; 105 | } 106 | 107 | if (self.thumbnailImageView == nil) { 108 | UIImageView *thumbnailImageView = [[UIImageView alloc] initWithFrame:superview.bounds]; 109 | thumbnailImageView.contentMode = UIViewContentModeScaleAspectFit; 110 | thumbnailImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 111 | 112 | if (subview) { 113 | [superview insertSubview:thumbnailImageView belowSubview:subview]; 114 | } 115 | else { 116 | [superview addSubview:thumbnailImageView]; 117 | } 118 | 119 | self.thumbnailImageView = thumbnailImageView; 120 | } 121 | } 122 | 123 | #pragma mark - Private 124 | 125 | - (UIView *)newOverlayViewInSuperview:(UIView *)superview { 126 | UIView *overlayView = [[UIView alloc] initWithFrame:superview.bounds]; 127 | overlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 128 | overlayView.userInteractionEnabled = NO; 129 | overlayView.backgroundColor = [UIColor clearColor]; 130 | [superview addSubview:overlayView]; 131 | return overlayView; 132 | } 133 | 134 | - (UIActivityIndicatorView *)newCenteredActivityIndicatorViewInSuperview:(UIView *)superview 135 | { 136 | UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; 137 | activityIndicatorView.translatesAutoresizingMaskIntoConstraints = NO; 138 | [superview addSubview:activityIndicatorView]; 139 | 140 | NSLayoutConstraint *centerX = [NSLayoutConstraint constraintWithItem:superview attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:activityIndicatorView attribute:NSLayoutAttributeCenterX multiplier:1.0f constant:0.0f]; 141 | NSLayoutConstraint *centerY = [NSLayoutConstraint constraintWithItem:superview attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:activityIndicatorView attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]; 142 | [superview addConstraints:@[centerX, centerY]]; 143 | 144 | return activityIndicatorView; 145 | } 146 | 147 | - (UILabel *)newBottomAttachedCaptionLabelInSuperview:(UIView *)superview { 148 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 20.0f, 20.0f)]; 149 | label.userInteractionEnabled = NO; 150 | label.textColor = [UIColor whiteColor]; 151 | label.font = [[self class] defaultCaptionLabelFont]; 152 | label.numberOfLines = 0; 153 | label.backgroundColor = [UIColor clearColor]; 154 | label.translatesAutoresizingMaskIntoConstraints = NO; 155 | [superview addSubview:label]; 156 | 157 | NSDictionary *viewsDict = NSDictionaryOfVariableBindings(label); 158 | NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-(padding)-[label]-(padding)-|" options:0 metrics:@{@"padding" : @(kCaptionLabelLateralPadding)} views:viewsDict]; 159 | [superview addConstraints:constraints]; 160 | 161 | constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[label(<=maxHeight)]" options:0 metrics:@{ @"maxHeight" : @(kCaptionLabelMaxHeight) } views:viewsDict]; 162 | [superview addConstraints:constraints]; 163 | 164 | return label; 165 | } 166 | 167 | - (UIView *)newBottomAttachedBackgroundViewForCaptionLabel:(UILabel *)label inSuperview:(UIView *)superview 168 | { 169 | UIView *view; 170 | if ([MUKMediaGalleryUtils defaultUIParadigm] == MUKMediaGalleryUIParadigmLayered) 171 | { 172 | // A toolbar gives live blurry effect on iOS 7 173 | MUKMediaGalleryToolbar *toolbar = [[MUKMediaGalleryToolbar alloc] initWithFrame:label.frame]; 174 | toolbar.barStyle = UIBarStyleBlack; 175 | toolbar.delegate = self; 176 | 177 | view = toolbar; 178 | } 179 | else { 180 | view = [[UIView alloc] initWithFrame:label.frame]; 181 | view.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.5f]; 182 | } 183 | 184 | view.userInteractionEnabled = NO; 185 | view.translatesAutoresizingMaskIntoConstraints = NO; 186 | [superview insertSubview:view belowSubview:label]; 187 | 188 | NSDictionary *viewsDict = NSDictionaryOfVariableBindings(view); 189 | NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-(0)-[view]-(0)-|" options:0 metrics:nil views:viewsDict]; 190 | [superview addConstraints:constraints]; 191 | 192 | NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:label attribute:NSLayoutAttributeHeight multiplier:1.0f constant:kCaptionLabelBottomPadding + kCaptionLabelTopPadding]; 193 | [superview addConstraint:constraint]; 194 | 195 | return view; 196 | } 197 | 198 | #pragma mark - Private — Caption 199 | 200 | - (void)updateCaptionConstraintsWhenHidden:(BOOL)hidden { 201 | UIView *const superview = self.captionLabel.superview; 202 | 203 | // Create all constraints 204 | if (!self.captionLabelTopConstraint) { 205 | self.captionLabelTopConstraint = [NSLayoutConstraint constraintWithItem:self.captionLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeBottom multiplier:1.0f constant:kCaptionLabelTopPadding]; 206 | } 207 | 208 | if (!self.captionBackgroundViewTopConstraint) { 209 | self.captionBackgroundViewTopConstraint = [NSLayoutConstraint constraintWithItem:self.captionBackgroundView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeBottom multiplier:1.0f constant:kCaptionLabelTopPadding]; 210 | } 211 | 212 | if (!self.captionLabelBottomConstraint) { 213 | self.captionLabelBottomConstraint = [NSLayoutConstraint constraintWithItem:self.captionLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeBottom multiplier:1.0f constant:-kCaptionLabelBottomPadding]; 214 | } 215 | 216 | if (!self.captionBackgroundViewBottomConstraint) { 217 | self.captionBackgroundViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.captionBackgroundView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f]; 218 | } 219 | 220 | // Change constraints 221 | NSArray *unusedConstraints, *usedConstraints; 222 | 223 | if (hidden) { 224 | usedConstraints = @[ self.captionLabelTopConstraint, self.captionBackgroundViewTopConstraint ]; 225 | unusedConstraints = @[ self.captionLabelBottomConstraint, self.captionBackgroundViewBottomConstraint ]; 226 | } 227 | else { 228 | usedConstraints = @[ self.captionLabelBottomConstraint, self.captionBackgroundViewBottomConstraint ]; 229 | unusedConstraints = @[ self.captionLabelTopConstraint, self.captionBackgroundViewTopConstraint ]; 230 | } 231 | 232 | [superview removeConstraints:unusedConstraints]; 233 | [superview addConstraints:usedConstraints]; 234 | 235 | // Notify 236 | [self.view setNeedsUpdateConstraints]; 237 | } 238 | 239 | #pragma mark - Private — Notifications 240 | 241 | - (void)registerToContentSizeCategoryNotifications { 242 | if (&UIContentSizeCategoryDidChangeNotification != NULL) { 243 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 244 | [nc addObserver:self selector:@selector(contentSizeCategoryDidChangeNotification:) name:UIContentSizeCategoryDidChangeNotification object:nil]; 245 | } 246 | } 247 | 248 | - (void)unregisterFromContentSizeCategoryNotifications { 249 | if (&UIContentSizeCategoryDidChangeNotification != NULL) { 250 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 251 | [nc removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil]; 252 | } 253 | } 254 | 255 | - (void)contentSizeCategoryDidChangeNotification:(NSNotification *)notification 256 | { 257 | self.captionLabel.font = [[self class] defaultCaptionLabelFont]; 258 | } 259 | 260 | #pragma mark - Private — Fonts 261 | 262 | + (UIFont *)defaultCaptionLabelFont { 263 | UIFont *font; 264 | 265 | if ([[UIFont class] respondsToSelector:@selector(preferredFontForTextStyle:)]) 266 | { 267 | font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1]; 268 | } 269 | else { 270 | font = [UIFont systemFontOfSize:12.0f]; 271 | } 272 | 273 | return font; 274 | } 275 | 276 | #pragma mark - Private — Tap Gesture Recognizer 277 | 278 | - (void)attachTapGestureRecognizer { 279 | UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; 280 | [self.view addGestureRecognizer:gestureRecognizer]; 281 | } 282 | 283 | - (void)handleTap:(UITapGestureRecognizer *)recognizer { 284 | if (recognizer.state == UIGestureRecognizerStateEnded) { 285 | [self.delegate carouselItemViewControllerDidReceiveTap:self]; 286 | } 287 | } 288 | 289 | #pragma mark - 290 | 291 | - (UIBarPosition)positionForBar:(id)bar { 292 | return UIBarPositionBottom; 293 | } 294 | 295 | @end 296 | -------------------------------------------------------------------------------- /MUKMediaGallery/Private/Views/MUKMediaCarouselPlayerControlsView.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaCarouselPlayerControlsView.h" 2 | #import "MUKMediaGalleryToolbar.h" 3 | #import "MUKMediaGalleryUtils.h" 4 | #import "MUKMediaGallerySlider.h" 5 | #import 6 | 7 | static CGFloat const kToolbarHeight = 44.0f; 8 | 9 | @interface MUKMediaCarouselPlayerControlsView () 10 | @property (nonatomic, weak) MPMoviePlayerController *moviePlayerController; 11 | @property (nonatomic, weak) UIView *backgroundView; 12 | @property (nonatomic, weak) UIButton *playPauseButton; 13 | @property (nonatomic, weak) UISlider *slider; 14 | @property (nonatomic, weak) UILabel *timeLabel; 15 | @property (nonatomic) NSTimer *playbackProgressUpdateTimer; 16 | @property (nonatomic) BOOL isTouchingSlider; 17 | @property (nonatomic) MPMoviePlaybackState playbackStateBeforeSliderTouches; 18 | @end 19 | 20 | @implementation MUKMediaCarouselPlayerControlsView 21 | 22 | - (instancetype)initWithMoviePlayerController:(MPMoviePlayerController *)moviePlayerController 23 | { 24 | self = [super initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, kToolbarHeight)]; 25 | if (self) { 26 | self.translatesAutoresizingMaskIntoConstraints = NO; 27 | self.backgroundColor = [UIColor clearColor]; 28 | 29 | _moviePlayerController = moviePlayerController; 30 | 31 | UIView *backgroundView = [self newBackgroundViewInSuperview:self]; 32 | _backgroundView = backgroundView; 33 | 34 | UIButton *playPauseButton = [self newPlayPauseButtonInSuperview:self]; 35 | _playPauseButton = playPauseButton; 36 | [self showPauseIconForMoviePlayerController:moviePlayerController]; 37 | 38 | UISlider *slider = [self newSliderInSuperview:self afterPlayPauseButton:playPauseButton]; 39 | _slider = slider; 40 | [self showSliderProgressForMoviePlayerController:moviePlayerController]; 41 | [self managePlaybackProgressUpdateTimerForMoviePlayerController:moviePlayerController]; 42 | 43 | UILabel *timeLabel = [self newTimeLabelInSuperview:self afterSlider:slider]; 44 | _timeLabel = timeLabel; 45 | [self showPlaybackTimeAndDurationForMoviePlayerController:self.moviePlayerController]; 46 | 47 | [self registerToMediaPlayerControllerNotifications]; 48 | } 49 | 50 | return self; 51 | } 52 | 53 | - (id)initWithFrame:(CGRect)frame { 54 | return [self initWithMoviePlayerController:nil]; 55 | } 56 | 57 | - (void)dealloc { 58 | [self unregisterFromMediaPlayerControllerNotifications]; 59 | } 60 | 61 | #pragma mark - Overrides 62 | 63 | - (CGSize)intrinsicContentSize { 64 | return CGSizeMake(UIViewNoIntrinsicMetric, self.frame.size.height); 65 | } 66 | 67 | #pragma mark - Private — View Building 68 | 69 | - (UIView *)newBackgroundViewInSuperview:(UIView *)superview { 70 | UIView *view; 71 | if ([MUKMediaGalleryUtils defaultUIParadigm] == MUKMediaGalleryUIParadigmLayered) 72 | { 73 | // A toolbar gives live blurry effect on iOS 7 74 | MUKMediaGalleryToolbar *toolbar = [[MUKMediaGalleryToolbar alloc] initWithFrame:superview.bounds]; 75 | toolbar.barStyle = UIBarStyleBlack; 76 | toolbar.delegate = self; 77 | 78 | view = toolbar; 79 | } 80 | else { 81 | view = [[UIView alloc] initWithFrame:superview.bounds]; 82 | view.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.5f]; 83 | } 84 | 85 | view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 86 | [superview addSubview:view]; 87 | 88 | return view; 89 | } 90 | 91 | - (UIButton *)newPlayPauseButtonInSuperview:(UIView *)superview { 92 | UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; 93 | button.translatesAutoresizingMaskIntoConstraints = NO; 94 | button.showsTouchWhenHighlighted = YES; 95 | [button addTarget:self action:@selector(playPauseButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; 96 | [superview addSubview:button]; 97 | 98 | CGSize const kMaxButtonSize = CGSizeMake(19.0f, 23.0f); 99 | 100 | NSDictionary *const viewsDict = NSDictionaryOfVariableBindings(button); 101 | NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-(10)-[button(==w)]" options:0 metrics:@{ @"w" : @(kMaxButtonSize.width) } views:viewsDict]; 102 | [superview addConstraints:constraints]; 103 | 104 | NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]; 105 | [superview addConstraint:constraint]; 106 | 107 | constraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0f constant:kMaxButtonSize.height]; 108 | [superview addConstraint:constraint]; 109 | 110 | return button; 111 | } 112 | 113 | - (UISlider *)newSliderInSuperview:(UIView *)superview afterPlayPauseButton:(UIButton *)playPauseButton 114 | { 115 | MUKMediaGallerySlider *slider = [[MUKMediaGallerySlider alloc] init]; 116 | slider.translatesAutoresizingMaskIntoConstraints = NO; 117 | UIImage *thumbImage = [MUKMediaGalleryUtils imageNamed:@"mediaPlayer_sliderThumb"]; 118 | [slider setThumbImage:thumbImage forState:UIControlStateNormal]; 119 | slider.thumbOffset = CGSizeMake(0.0f, 2.0f); 120 | [slider setMaximumTrackTintColor:[UIColor colorWithWhite:1.0f alpha:0.5f]]; 121 | 122 | [slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; 123 | [slider addTarget:self action:@selector(sliderTouchedDown:) forControlEvents:UIControlEventTouchDown]; 124 | [slider addTarget:self action:@selector(sliderBecameUntouched:) forControlEvents:UIControlEventTouchUpInside|UIControlEventTouchUpOutside|UIControlEventTouchCancel]; 125 | 126 | [superview addSubview:slider]; 127 | 128 | NSDictionary *const viewsDict = NSDictionaryOfVariableBindings(slider, playPauseButton); 129 | NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"[playPauseButton]-(12)-[slider(>=100)]" options:0 metrics:nil views:viewsDict]; 130 | [superview addConstraints:constraints]; 131 | 132 | NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:slider attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:playPauseButton attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:-2.0f]; 133 | [superview addConstraint:constraint]; 134 | 135 | return slider; 136 | } 137 | 138 | - (UILabel *)newTimeLabelInSuperview:(UIView *)superview afterSlider:(UISlider *)slider 139 | { 140 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 50.0f, 40.0f)]; 141 | label.translatesAutoresizingMaskIntoConstraints = NO; 142 | label.backgroundColor = [UIColor clearColor]; 143 | label.textColor = [UIColor whiteColor]; 144 | label.font = [UIFont systemFontOfSize:10.0f]; 145 | [superview addSubview:label]; 146 | 147 | NSDictionary *const viewsDict = NSDictionaryOfVariableBindings(slider, label); 148 | NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"[slider]-(6)-[label(>=20)]-(6)-|" options:0 metrics:nil views:viewsDict]; 149 | [superview addConstraints:constraints]; 150 | 151 | CGFloat offset = 2.0f; 152 | if ([MUKMediaGalleryUtils defaultUIParadigm] == MUKMediaGalleryUIParadigmGlossy) 153 | { 154 | offset = 1.0f; 155 | } 156 | 157 | NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:slider attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:offset]; 158 | [superview addConstraint:constraint]; 159 | 160 | return label; 161 | } 162 | 163 | #pragma mark - Private — Play/Pause 164 | 165 | - (BOOL)isPausedMoviePlayerController:(MPMoviePlayerController *)moviePlayerController 166 | { 167 | return (moviePlayerController.playbackState == MPMoviePlaybackStatePaused || 168 | moviePlayerController.playbackState == MPMoviePlaybackStateStopped || 169 | moviePlayerController.playbackState == MPMoviePlaybackStateInterrupted); 170 | } 171 | 172 | - (void)showPauseIcon:(BOOL)showsPauseIcon { 173 | UIImage *icon; 174 | if (showsPauseIcon) { 175 | icon = [MUKMediaGalleryUtils imageNamed:@"mediaPlayer_pause"]; 176 | } 177 | else { 178 | icon = [MUKMediaGalleryUtils imageNamed:@"mediaPlayer_play"]; 179 | } 180 | 181 | [self.playPauseButton setImage:icon forState:UIControlStateNormal]; 182 | } 183 | 184 | - (void)showPauseIconForMoviePlayerController:(MPMoviePlayerController *)moviePlayerController 185 | { 186 | [self showPauseIcon:![self isPausedMoviePlayerController:moviePlayerController]]; 187 | } 188 | 189 | - (void)playPauseButtonPressed:(id)sender { 190 | if ([self isPausedMoviePlayerController:self.moviePlayerController]) 191 | { 192 | [self.moviePlayerController play]; 193 | } 194 | else { 195 | [self.moviePlayerController pause]; 196 | } 197 | } 198 | 199 | #pragma mark - Private — Time 200 | 201 | - (void)showPlaybackTime:(NSTimeInterval)playbackTime duration:(NSTimeInterval)duration 202 | { 203 | NSMutableAttributedString *fullAttributedString = [[NSMutableAttributedString alloc] init]; 204 | 205 | // First part: playback time 206 | NSString *string; 207 | if (playbackTime >= 0.0) { 208 | string = [MUK stringRepresentationOfTimeInterval:playbackTime]; 209 | } 210 | else { 211 | string = @"--:--"; 212 | } 213 | 214 | NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:@{ NSForegroundColorAttributeName : [UIColor whiteColor] }]; 215 | [fullAttributedString appendAttributedString:attributedString]; 216 | 217 | // Second part: duration 218 | if (duration > 0.0) { 219 | string = [@"/" stringByAppendingString:[MUK stringRepresentationOfTimeInterval:duration]]; 220 | attributedString = [[NSAttributedString alloc] initWithString:string attributes:@{ NSForegroundColorAttributeName : [UIColor colorWithWhite:1.0f alpha:0.5f] }]; 221 | [fullAttributedString appendAttributedString:attributedString]; 222 | } 223 | 224 | self.timeLabel.attributedText = fullAttributedString; 225 | } 226 | 227 | - (void)showPlaybackTimeAndDurationForMoviePlayerController:(MPMoviePlayerController *)moviePlayerController 228 | { 229 | [self showPlaybackTime:moviePlayerController.currentPlaybackTime duration:moviePlayerController.duration]; 230 | } 231 | 232 | #pragma mark - Private — Slider 233 | 234 | - (void)sliderTouchedDown:(id)sender { 235 | if (self.isTouchingSlider == NO) { 236 | self.isTouchingSlider = YES; 237 | [self rememberPlaybackStateBeforeSliderTouchesAndPause]; 238 | } 239 | } 240 | 241 | - (void)sliderBecameUntouched:(id)sender { 242 | self.isTouchingSlider = NO; 243 | [self attemptToRestorePlaybackStateBeforeSliderTouches]; 244 | } 245 | 246 | - (void)sliderValueChanged:(id)sender { 247 | if (self.isTouchingSlider == NO) { 248 | self.isTouchingSlider = YES; 249 | [self rememberPlaybackStateBeforeSliderTouchesAndPause]; 250 | } 251 | 252 | [self setPlaybackForSliderProgress:self.slider.value]; 253 | } 254 | 255 | - (void)rememberPlaybackStateBeforeSliderTouchesAndPause { 256 | self.playbackStateBeforeSliderTouches = self.moviePlayerController.playbackState; 257 | [self.moviePlayerController pause]; 258 | } 259 | 260 | - (void)attemptToRestorePlaybackStateBeforeSliderTouches { 261 | BOOL canRestorePastPlaybackState = YES; 262 | if (self.playbackStateBeforeSliderTouches == MPMoviePlaybackStatePlaying && 263 | self.moviePlayerController.currentPlaybackTime >= self.moviePlayerController.duration) 264 | { 265 | canRestorePastPlaybackState = NO; 266 | } 267 | 268 | if (canRestorePastPlaybackState) { 269 | if (self.playbackStateBeforeSliderTouches == MPMoviePlaybackStatePlaying) 270 | { 271 | [self.moviePlayerController play]; 272 | } 273 | else { 274 | [self.moviePlayerController pause]; 275 | } 276 | } 277 | } 278 | 279 | #pragma mark - Private — Playback Progress 280 | 281 | - (void)showSliderProgressForMoviePlayerController:(MPMoviePlayerController *)moviePlayerController 282 | { 283 | float progress = 0.0f; 284 | 285 | if (moviePlayerController.duration > 0.0){ 286 | progress = moviePlayerController.currentPlaybackTime/moviePlayerController.duration; 287 | } 288 | 289 | [self.slider setValue:progress animated:NO]; 290 | } 291 | 292 | - (void)setPlaybackForSliderProgress:(float)progress { 293 | NSTimeInterval playbackTime = 0.0; 294 | 295 | if (self.moviePlayerController.duration > 0.0) { 296 | playbackTime = self.moviePlayerController.duration * progress; 297 | } 298 | 299 | self.moviePlayerController.currentPlaybackTime = playbackTime; 300 | [self showPlaybackTimeAndDurationForMoviePlayerController:self.moviePlayerController]; 301 | } 302 | 303 | - (void)startPlackbackProgressUpdateTimer { 304 | if (![self.playbackProgressUpdateTimer isValid]) { 305 | self.playbackProgressUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(playbackProgressUpdateTimerFired:) userInfo:nil repeats:YES]; 306 | } 307 | } 308 | 309 | - (void)stopPlaybackProgressUpdateTimer { 310 | if ([self.playbackProgressUpdateTimer isValid]) { 311 | [self.playbackProgressUpdateTimer invalidate]; 312 | self.playbackProgressUpdateTimer = nil; 313 | } 314 | } 315 | 316 | - (void)managePlaybackProgressUpdateTimerForMoviePlayerController:(MPMoviePlayerController *)moviePlayerController 317 | { 318 | if ([self isPausedMoviePlayerController:moviePlayerController]) { 319 | [self stopPlaybackProgressUpdateTimer]; 320 | } 321 | else { 322 | [self startPlackbackProgressUpdateTimer]; 323 | } 324 | } 325 | 326 | - (void)playbackProgressUpdateTimerFired:(NSTimer *)timer { 327 | if (self.isTouchingSlider == NO) { 328 | [self showPlaybackTimeAndDurationForMoviePlayerController:self.moviePlayerController]; 329 | [self showSliderProgressForMoviePlayerController:self.moviePlayerController]; 330 | } 331 | } 332 | 333 | #pragma mark - Private — Notifications 334 | 335 | - (void)registerToMediaPlayerControllerNotifications { 336 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 337 | [nc addObserver:self selector:@selector(playbackStateDidChangeNotification:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:self.moviePlayerController]; 338 | [nc addObserver:self selector:@selector(durationAvailableNotification:) name:MPMovieDurationAvailableNotification object:self.moviePlayerController]; 339 | } 340 | 341 | - (void)unregisterFromMediaPlayerControllerNotifications { 342 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 343 | [nc removeObserver:self name:MPMoviePlayerPlaybackStateDidChangeNotification object:nil]; 344 | [nc removeObserver:self name:MPMovieDurationAvailableNotification object:nil]; 345 | } 346 | 347 | - (void)playbackStateDidChangeNotification:(NSNotification *)notification { 348 | if (!self.isTouchingSlider) { 349 | [self showPauseIconForMoviePlayerController:self.moviePlayerController]; 350 | [self showPlaybackTimeAndDurationForMoviePlayerController:self.moviePlayerController]; 351 | [self managePlaybackProgressUpdateTimerForMoviePlayerController:self.moviePlayerController]; 352 | } 353 | } 354 | 355 | - (void)durationAvailableNotification:(NSNotification *)notification { 356 | [self showSliderProgressForMoviePlayerController:self.moviePlayerController]; 357 | } 358 | 359 | #pragma mark - 360 | 361 | - (UIBarPosition)positionForBar:(id)bar { 362 | return UIBarPositionBottom; 363 | } 364 | 365 | @end 366 | -------------------------------------------------------------------------------- /Example/MUKMediaGallery Example/ThumbnailsViewController.m: -------------------------------------------------------------------------------- 1 | #import "ThumbnailsViewController.h" 2 | #import "MediaAsset.h" 3 | #import "CarouselViewController.h" 4 | 5 | #define DEBUG_SIMULATE_ASSETS_DOWNLOADING 0 6 | #define DEBUG_HUGE_ASSETS 0 7 | 8 | @interface ThumbnailsViewController () 9 | @property (nonatomic) NSOperationQueue *networkQueue; 10 | @end 11 | 12 | @implementation ThumbnailsViewController 13 | 14 | - (id)initWithCollectionViewLayout:(UICollectionViewLayout *)layout { 15 | self = [super initWithCollectionViewLayout:layout]; 16 | if (self) { 17 | self.title = @"Thumbnails Grid"; 18 | self.delegate = self; 19 | _networkQueue = [[NSOperationQueue alloc] init]; 20 | _networkQueue.maxConcurrentOperationCount = 3; 21 | 22 | #if !DEBUG_SIMULATE_ASSETS_DOWNLOADING 23 | _mediaAssets = [[self class] newAssets]; 24 | #endif 25 | } 26 | 27 | return self; 28 | } 29 | 30 | - (void)viewDidLoad { 31 | [super viewDidLoad]; 32 | 33 | #if DEBUG_SIMULATE_ASSETS_DOWNLOADING 34 | UIView *backgroundView = [[UIView alloc] initWithFrame:self.collectionView.bounds]; 35 | self.collectionView.backgroundView = backgroundView; 36 | 37 | UIView *wrapperView = [[UIView alloc] initWithFrame:self.collectionView.backgroundView.bounds]; 38 | wrapperView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 39 | 40 | UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; 41 | [spinner startAnimating]; 42 | [wrapperView addSubview:spinner]; 43 | [self.collectionView.backgroundView addSubview:wrapperView]; 44 | 45 | NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:spinner attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:spinner.superview attribute:NSLayoutAttributeCenterX multiplier:1.0f constant:0.0f]; 46 | NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:spinner attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:spinner.superview attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]; 47 | wrapperView.translatesAutoresizingMaskIntoConstraints = NO; 48 | [wrapperView addConstraints:@[centerXConstraint, centerYConstraint]]; 49 | 50 | double delayInSeconds = 3.0; 51 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 52 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 53 | self.mediaAssets = [[self class] newAssets]; 54 | [self reloadData]; 55 | 56 | [self.collectionView.backgroundView removeFromSuperview]; 57 | self.collectionView.backgroundView = nil; 58 | }); 59 | #endif 60 | } 61 | 62 | #pragma mark - Private 63 | 64 | + (NSArray *)newAssets { 65 | NSMutableArray *mediaAssets = [[NSMutableArray alloc] init]; 66 | 67 | #if DEBUG_HUGE_ASSETS 68 | NSArray *URLStrings = @[@"http://photopin.com/phpflickr/getphoto.php?size=original&id=4561126538&url=http%3A%2F%2Ffarm5.staticflickr.com%2F4020%2F4561126538_ff11823d70_o.jpg", 69 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=2456720470&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2095%2F2456720470_d95381244e_o.jpg", 70 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=8544074541&url=http%3A%2F%2Ffarm9.staticflickr.com%2F8391%2F8544074541_d524cbd894_o.jpg", 71 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=164963669&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F164963669_a8b04e7ac3_o.jpg", 72 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=13290875115&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3711%2F13290875115_59b6ef17c0_o.jpg", 73 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=3644223516&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3395%2F3644223516_c57fae255b_o.jpg", 74 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=3101281662&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3180%2F3101281662_2646526718_o.jpg", 75 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=292917043&url=http%3A%2F%2Ffarm1.staticflickr.com%2F113%2F292917043_32f325372f_o.jpg", 76 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=213839709&url=http%3A%2F%2Ffarm1.staticflickr.com%2F81%2F213839709_889a86f0c9_o.jpg", 77 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=157527342&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F157527342_00affe5f94_o.jpg", 78 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=97204589&url=http%3A%2F%2Ffarm1.staticflickr.com%2F23%2F97204589_f70f4f6e25_o.jpg", 79 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=5826755&url=http%3A%2F%2Ffarm1.staticflickr.com%2F6%2F5826755_8f7ca43323_o.jpg", 80 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=13227507635&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7257%2F13227507635_08beee10c6_o.jpg", 81 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=12086742175&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2835%2F12086742175_646648b0b3_o.jpg", 82 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=11934616456&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2856%2F11934616456_5fb701fc1b_o.jpg", 83 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=11608712713&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7395%2F11608712713_7527cf777b_o.jpg", 84 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=11191240794&url=http%3A%2F%2Ffarm6.staticflickr.com%2F5528%2F11191240794_360c2c0341_o.jpg", 85 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=10531577523&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3780%2F10531577523_8169627052_o.jpg", 86 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=4561126538&url=http%3A%2F%2Ffarm5.staticflickr.com%2F4020%2F4561126538_ff11823d70_o.jpg", 87 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=2456720470&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2095%2F2456720470_d95381244e_o.jpg", 88 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=8544074541&url=http%3A%2F%2Ffarm9.staticflickr.com%2F8391%2F8544074541_d524cbd894_o.jpg", 89 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=164963669&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F164963669_a8b04e7ac3_o.jpg", 90 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=13290875115&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3711%2F13290875115_59b6ef17c0_o.jpg", 91 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=3644223516&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3395%2F3644223516_c57fae255b_o.jpg", 92 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=3101281662&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3180%2F3101281662_2646526718_o.jpg", 93 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=292917043&url=http%3A%2F%2Ffarm1.staticflickr.com%2F113%2F292917043_32f325372f_o.jpg", 94 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=213839709&url=http%3A%2F%2Ffarm1.staticflickr.com%2F81%2F213839709_889a86f0c9_o.jpg", 95 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=157527342&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F157527342_00affe5f94_o.jpg", 96 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=97204589&url=http%3A%2F%2Ffarm1.staticflickr.com%2F23%2F97204589_f70f4f6e25_o.jpg", 97 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=5826755&url=http%3A%2F%2Ffarm1.staticflickr.com%2F6%2F5826755_8f7ca43323_o.jpg", 98 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=13227507635&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7257%2F13227507635_08beee10c6_o.jpg", 99 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=12086742175&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2835%2F12086742175_646648b0b3_o.jpg", 100 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=11934616456&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2856%2F11934616456_5fb701fc1b_o.jpg", 101 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=11608712713&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7395%2F11608712713_7527cf777b_o.jpg", 102 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=11191240794&url=http%3A%2F%2Ffarm6.staticflickr.com%2F5528%2F11191240794_360c2c0341_o.jpg", 103 | @"http://photopin.com/phpflickr/getphoto.php?size=original&id=10531577523&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3780%2F10531577523_8169627052_o.jpg"]; 104 | NSArray *thumbnailURLStrings = @[@"http://photopin.com/phpflickr/getphoto.php?size=small&id=4561126538&url=http%3A%2F%2Ffarm5.staticflickr.com%2F4020%2F4561126538_374d62dfa2_m.jpg", 105 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=2456720470&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2095%2F2456720470_94fb3d246d_m.jpg", 106 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=8544074541&url=http%3A%2F%2Ffarm9.staticflickr.com%2F8391%2F8544074541_29a2c7a292_m.jpg", 107 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=164963669&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F164963669_a8b04e7ac3_m.jpg", 108 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=13290875115&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3711%2F13290875115_bed624a74c_m.jpg", 109 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=3644223516&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3395%2F3644223516_a23eaafb99_m.jpg", 110 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=3101281662&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3180%2F3101281662_d6c9f0e7f9_m.jpg", 111 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=292917043&url=http%3A%2F%2Ffarm1.staticflickr.com%2F113%2F292917043_32f325372f_m.jpg", 112 | @"http://photopin.com/phpflickr/getphoto.php?size=medium&id=213839709&url=http%3A%2F%2Ffarm1.staticflickr.com%2F81%2F213839709_889a86f0c9.jpg", 113 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=157527342&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F157527342_00affe5f94_m.jpg", 114 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=97204589&url=http%3A%2F%2Ffarm1.staticflickr.com%2F23%2F97204589_f70f4f6e25_m.jpg", 115 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=5826755&url=http%3A%2F%2Ffarm1.staticflickr.com%2F6%2F5826755_8f7ca43323_m.jpg", 116 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=13227507635&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7257%2F13227507635_c2a72a5d92_m.jpg", 117 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=12086742175&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2835%2F12086742175_cbd381f749_m.jpg", 118 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=11934616456&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2856%2F11934616456_8c2d52e95b_m.jpg", 119 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=11608712713&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7395%2F11608712713_c68a777d03_m.jpg", 120 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=11191240794&url=http%3A%2F%2Ffarm6.staticflickr.com%2F5528%2F11191240794_e79d6399f8_m.jpg", 121 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=10531577523&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3780%2F10531577523_0e077a4791_m.jpg", 122 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=4561126538&url=http%3A%2F%2Ffarm5.staticflickr.com%2F4020%2F4561126538_374d62dfa2_m.jpg", 123 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=2456720470&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2095%2F2456720470_94fb3d246d_m.jpg", 124 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=8544074541&url=http%3A%2F%2Ffarm9.staticflickr.com%2F8391%2F8544074541_29a2c7a292_m.jpg", 125 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=164963669&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F164963669_a8b04e7ac3_m.jpg", 126 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=13290875115&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3711%2F13290875115_bed624a74c_m.jpg", 127 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=3644223516&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3395%2F3644223516_a23eaafb99_m.jpg", 128 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=3101281662&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3180%2F3101281662_d6c9f0e7f9_m.jpg", 129 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=292917043&url=http%3A%2F%2Ffarm1.staticflickr.com%2F113%2F292917043_32f325372f_m.jpg", 130 | @"http://photopin.com/phpflickr/getphoto.php?size=medium&id=213839709&url=http%3A%2F%2Ffarm1.staticflickr.com%2F81%2F213839709_889a86f0c9.jpg", 131 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=157527342&url=http%3A%2F%2Ffarm1.staticflickr.com%2F48%2F157527342_00affe5f94_m.jpg", 132 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=97204589&url=http%3A%2F%2Ffarm1.staticflickr.com%2F23%2F97204589_f70f4f6e25_m.jpg", 133 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=5826755&url=http%3A%2F%2Ffarm1.staticflickr.com%2F6%2F5826755_8f7ca43323_m.jpg", 134 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=13227507635&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7257%2F13227507635_c2a72a5d92_m.jpg", 135 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=12086742175&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2835%2F12086742175_cbd381f749_m.jpg", 136 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=11934616456&url=http%3A%2F%2Ffarm3.staticflickr.com%2F2856%2F11934616456_8c2d52e95b_m.jpg", 137 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=11608712713&url=http%3A%2F%2Ffarm8.staticflickr.com%2F7395%2F11608712713_c68a777d03_m.jpg", 138 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=11191240794&url=http%3A%2F%2Ffarm6.staticflickr.com%2F5528%2F11191240794_e79d6399f8_m.jpg", 139 | @"http://photopin.com/phpflickr/getphoto.php?size=small&id=10531577523&url=http%3A%2F%2Ffarm4.staticflickr.com%2F3780%2F10531577523_0e077a4791_m.jpg"]; 140 | 141 | for (NSInteger i=0; i < [URLStrings count]; i++) { 142 | NSString *URLString = URLStrings[i]; 143 | NSString *thumbURLString = thumbnailURLStrings[i]; 144 | 145 | MediaAsset *asset = [[MediaAsset alloc] initWithKind:MUKMediaKindImage]; 146 | asset.URL = [NSURL URLWithString:URLString]; 147 | asset.thumbnailURL = [NSURL URLWithString:thumbURLString]; 148 | 149 | [mediaAssets addObject:asset]; 150 | } 151 | #else 152 | for (NSInteger i=0; i<100; i++) { 153 | MediaAsset *youTubeVideoAsset = [[MediaAsset alloc] initWithKind:MUKMediaKindYouTubeVideo]; 154 | youTubeVideoAsset.duration = 906; // 15:06 155 | youTubeVideoAsset.thumbnailURL = [NSURL URLWithString:@"http://i2.ytimg.com/vi/UF8uR6Z6KLc/default.jpg"]; 156 | youTubeVideoAsset.URL = [NSURL URLWithString:@"http://www.youtube.com/watch?v=UF8uR6Z6KLc"]; 157 | youTubeVideoAsset.caption = @"Steve Jobs at Stanford"; 158 | [mediaAssets addObject:youTubeVideoAsset]; 159 | 160 | MediaAsset *localVideoAsset = [[MediaAsset alloc] initWithKind:MUKMediaKindVideo]; 161 | localVideoAsset.thumbnailURL = [MUK URLForImageFileNamed:@"sea-movie-thumbnail.jpg" bundle:nil]; 162 | localVideoAsset.URL = [[NSBundle mainBundle] URLForResource:@"sea" withExtension:@"mp4"]; 163 | localVideoAsset.caption = @"A local video asset downloaded from Flickr which has been recorded to be relaxing and entertaining"; 164 | [mediaAssets addObject:localVideoAsset]; 165 | 166 | MediaAsset *remoteVideoAsset = [[MediaAsset alloc] initWithKind:MUKMediaKindVideo]; 167 | remoteVideoAsset.URL = [NSURL URLWithString:@"http://mirrors.creativecommons.org/movingimages/Building_on_the_Past.mp4"]; 168 | [mediaAssets addObject:remoteVideoAsset]; 169 | 170 | // http://www.flickr.com/photos/26895569@N07/9134939367/sizes/l/in/explore-2013-06-25/ 171 | MediaAsset *remoteImageAsset = [[MediaAsset alloc] initWithKind:MUKMediaKindImage]; 172 | remoteImageAsset.thumbnailURL = [NSURL URLWithString:@"http://farm3.staticflickr.com/2867/9134939367_228c3d310d_m.jpg"]; 173 | remoteImageAsset.URL = [NSURL URLWithString:@"http://farm3.staticflickr.com/2867/9134939367_228c3d310d_b.jpg"]; // [NSURL URLWithString:@"http://farm3.staticflickr.com/2867/9134939367_5083834193_o.jpg"]; 174 | remoteImageAsset.caption = @"Castilla entera se desangra, y nadie cierra la herida ni recoge su roja sangre derramada. Quien puede atender tamaña brecha no siente la sangre por sus venas, y deja a su albur sangre y herida. Los cielos que saben de olvidos acercan su luz a esos colores que gritan quedo su agonía. Quien puede encauzar esas arterias, que ponga el color dentro, en las venas, y fluya roja y encendida haciendo de la vida un paraíso."; 175 | [mediaAssets addObject:remoteImageAsset]; 176 | 177 | MediaAsset *localImageAsset = [[MediaAsset alloc] initWithKind:MUKMediaKindImage]; 178 | localImageAsset.thumbnailURL = [MUK URLForImageFileNamed:@"palms-thumbnail.jpg" bundle:nil]; 179 | localImageAsset.URL = [MUK URLForImageFileNamed:@"palms.jpg" bundle:nil]; 180 | localImageAsset.caption = @"A local photo which represents a palm. Palms are a popular tree in many North African countries which is used in many ways."; 181 | [mediaAssets addObject:localImageAsset]; 182 | 183 | MediaAsset *localAudioAsset = [[MediaAsset alloc] initWithKind:MUKMediaKindAudio]; 184 | localAudioAsset.thumbnailURL = [NSURL URLWithString:@"http://farm6.staticflickr.com/5178/5500963965_2776bf6a98_t.jpg"]; 185 | localAudioAsset.URL = [[NSBundle mainBundle] URLForResource:@"SexForModerns-StopTheClock_64kb" withExtension:@"mp3"]; 186 | [mediaAssets addObject:localAudioAsset]; 187 | 188 | MediaAsset *remoteAudioAsset = [[MediaAsset alloc] initWithKind:MUKMediaKindAudio]; 189 | remoteAudioAsset.duration = 332.0; // 05:32 190 | remoteAudioAsset.URL = [NSURL URLWithString:@"http://ia600201.us.archive.org/21/items/SexForModerns-PenetratingLoveRay/SexForModerns-PenetratingLoveRay.mp3"]; 191 | [mediaAssets addObject:remoteAudioAsset]; 192 | } 193 | #endif 194 | 195 | return mediaAssets; 196 | } 197 | 198 | #pragma mark - 199 | 200 | - (NSInteger)numberOfItemsInThumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController 201 | { 202 | return [self.mediaAssets count]; 203 | } 204 | 205 | - (void)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController loadImageForItemAtIndex:(NSInteger)idx completionHandler:(void (^)(UIImage *))completionHandler 206 | { 207 | MediaAsset *asset = self.mediaAssets[idx]; 208 | 209 | if (!asset.thumbnailURL) { 210 | completionHandler(nil); 211 | return; 212 | } 213 | 214 | NSURLRequest *request = [NSURLRequest requestWithURL:asset.thumbnailURL]; 215 | AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 216 | op.userInfo = @{ @"index" : @(idx) }; 217 | 218 | [op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 219 | UIImage *image = nil; 220 | 221 | if ([responseObject length]) { 222 | image = [UIImage imageWithData:responseObject]; 223 | } 224 | 225 | completionHandler(image); 226 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 227 | completionHandler(nil); 228 | }]; 229 | 230 | [self.networkQueue addOperation:op]; 231 | } 232 | 233 | - (void)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController cancelLoadingForImageAtIndex:(NSInteger)idx 234 | { 235 | for (AFHTTPRequestOperation *op in self.networkQueue.operations) { 236 | if ([op.userInfo[@"index"] integerValue] == idx) { 237 | [op cancel]; 238 | break; 239 | } 240 | } 241 | } 242 | 243 | - (MUKMediaAttributes *)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController attributesForItemAtIndex:(NSInteger)idx 244 | { 245 | MediaAsset *asset = self.mediaAssets[idx]; 246 | MUKMediaAttributes *attributes = [[MUKMediaAttributes alloc] initWithKind:asset.kind]; 247 | 248 | if (asset.duration > 0.0) { 249 | [attributes setCaptionWithTimeInterval:asset.duration]; 250 | } 251 | 252 | return attributes; 253 | } 254 | 255 | - (MUKMediaCarouselViewController *)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController carouselToPresentAfterSelectingItemAtIndex:(NSInteger)idx 256 | { 257 | CarouselViewController *carouselViewController = [[CarouselViewController alloc] init]; 258 | carouselViewController.mediaAssets = self.mediaAssets; 259 | return carouselViewController; 260 | } 261 | 262 | /* 263 | - (MUKMediaThumbnailsViewControllerToCarouselTransition)thumbnailsViewController:(MUKMediaThumbnailsViewController *)viewController transitionToPresentCarouselViewController:(MUKMediaCarouselViewController *)carouselViewController forItemAtIndex:(NSInteger)idx 264 | { 265 | return MUKMediaThumbnailsViewControllerToCarouselTransitionCrossDissolve; 266 | } 267 | */ 268 | 269 | @end 270 | -------------------------------------------------------------------------------- /MUKMediaGallery/MUKMediaThumbnailsViewController.m: -------------------------------------------------------------------------------- 1 | #import "MUKMediaThumbnailsViewController.h" 2 | #import "MUKMediaThumbnailCell.h" 3 | #import "MUKMediaAttributesCache.h" 4 | #import "MUKMediaGalleryUtils.h" 5 | #import "MUKMediaGalleryImageResizeOperation.h" 6 | #import "MUKMediaCarouselViewController.h" 7 | 8 | static NSString *const kCellIdentifier = @"MUKMediaThumbnailCell"; 9 | 10 | static NSString *const kNavigationBarBoundsKVOIdentifier = @"NavigationBarFrameKVOIdentifier"; 11 | 12 | @interface MUKMediaThumbnailsViewController () 13 | @property (nonatomic) MUKMediaModelCache *imagesCache; 14 | @property (nonatomic) MUKMediaAttributesCache *mediaAttributesCache; 15 | @property (nonatomic) NSMutableIndexSet *loadingImageIndexes; 16 | @property (nonatomic) CGRect lastCollectionSuperviewBounds, lastCollectionViewBounds; 17 | @property (nonatomic) NSOperationQueue *thumbnailResizeQueue; 18 | 19 | @property (nonatomic) UIBarStyle previousNavigationBarStyle; 20 | @property (nonatomic) UIStatusBarStyle previousStatusBarStyle; 21 | @property (nonatomic) BOOL isTransitioningWithCarouselViewController; 22 | @property (nonatomic) UINavigationBar *observedNavigationBar; 23 | @property (nonatomic, weak) UIViewController *carouselPresentationViewController; 24 | @property (nonatomic) BOOL shouldReloadDataInViewWillAppear, shouldChangeCollectionViewLayoutAtViewDidLayoutSubviews, shouldChangeCollectionViewContentOffsetAtViewDidLayoutSubviews; 25 | @property (nonatomic) CGPoint collectionViewContentOffsetToSetAtViewDidLayoutSubviews; 26 | @end 27 | 28 | @implementation MUKMediaThumbnailsViewController 29 | 30 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 31 | { 32 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 33 | if (self) { 34 | CommonInitialization(self, [[UICollectionViewFlowLayout alloc] init]); 35 | } 36 | return self; 37 | } 38 | 39 | - (id)initWithCollectionViewLayout:(UICollectionViewLayout *)layout { 40 | layout = [[UICollectionViewFlowLayout alloc] init]; 41 | self = [super initWithCollectionViewLayout:layout]; 42 | if (self) { 43 | CommonInitialization(self, nil); 44 | } 45 | 46 | return self; 47 | } 48 | 49 | - (id)init { 50 | return [self initWithCollectionViewLayout:nil]; 51 | } 52 | 53 | - (void)dealloc { 54 | [self stopObservingChangesToAdjustTopPadding]; 55 | } 56 | 57 | - (void)viewDidLoad { 58 | [super viewDidLoad]; 59 | 60 | self.collectionView.backgroundColor = [UIColor whiteColor]; 61 | [self.collectionView registerClass:[MUKMediaThumbnailCell class] forCellWithReuseIdentifier:kCellIdentifier]; 62 | 63 | if (![self automaticallyAdjustsTopPadding]) { 64 | [self adjustTopPadding]; 65 | [self beginObservingChangesToAdjustTopPadding]; 66 | } 67 | } 68 | 69 | - (void)viewWillAppear:(BOOL)animated { 70 | [super viewWillAppear:animated]; 71 | 72 | if (!self.isTransitioningWithCarouselViewController) { 73 | // if not coming from carousel, save past bar styles 74 | self.previousNavigationBarStyle = self.navigationController.navigationBar.barStyle; 75 | self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle]; 76 | } 77 | else { 78 | // if coming from carousel, say is not coming from carousel anymore 79 | self.isTransitioningWithCarouselViewController = NO; 80 | } 81 | 82 | if ([MUKMediaGalleryUtils defaultUIParadigm] == MUKMediaGalleryUIParadigmGlossy) 83 | { 84 | #pragma clang diagnostic push 85 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 86 | self.wantsFullScreenLayout = YES; 87 | self.navigationController.navigationBar.barStyle = UIBarStyleBlackTranslucent; 88 | [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackTranslucent animated:animated]; 89 | #pragma clang diagnostic pop 90 | } 91 | else { 92 | self.navigationController.navigationBar.barStyle = UIBarStyleDefault; 93 | } 94 | 95 | if (self.shouldReloadDataInViewWillAppear) { 96 | self.shouldReloadDataInViewWillAppear = NO; 97 | [self.collectionView reloadData]; 98 | } 99 | } 100 | 101 | - (void)viewWillDisappear:(BOOL)animated { 102 | [super viewWillDisappear:animated]; 103 | 104 | if (!self.isTransitioningWithCarouselViewController) { 105 | // If not transitioning to carousel, reset past bar values 106 | 107 | if ([MUKMediaGalleryUtils defaultUIParadigm] == MUKMediaGalleryUIParadigmGlossy) 108 | { 109 | self.navigationController.navigationBar.barStyle = self.previousNavigationBarStyle; 110 | [[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle animated:animated]; 111 | } 112 | else { 113 | self.navigationController.navigationBar.barStyle = self.previousNavigationBarStyle; 114 | } 115 | } 116 | } 117 | 118 | - (void)viewDidDisappear:(BOOL)animated { 119 | [super viewDidDisappear:animated]; 120 | 121 | for (NSIndexPath *indexPath in [self.collectionView indexPathsForVisibleItems]) 122 | { 123 | [self requestLoadingCancellationForImageAtIndexPath:indexPath]; 124 | self.shouldReloadDataInViewWillAppear = YES; 125 | } 126 | } 127 | 128 | - (void)viewWillLayoutSubviews { 129 | [super viewWillLayoutSubviews]; 130 | 131 | if (!CGRectIsNull(self.lastCollectionSuperviewBounds)) { 132 | if (self.lastCollectionSuperviewBounds.size.width != self.collectionView.superview.bounds.size.width) 133 | { 134 | // Maintain scrolling ratio 135 | CGFloat const ratio = self.collectionView.superview.bounds.size.height/self.lastCollectionSuperviewBounds.size.height; 136 | 137 | CGPoint offset; 138 | if ([MUKMediaGalleryUtils defaultUIParadigm] == MUKMediaGalleryUIParadigmLayered) 139 | { 140 | // Value stored in self.collectionView.contentOffset is already 141 | // changed at this moment: use bounds instead (on iOS 7 and over) 142 | offset = self.lastCollectionViewBounds.origin; 143 | } 144 | else { 145 | offset = self.collectionView.contentOffset; 146 | } 147 | 148 | // Don't include insets into calculations 149 | offset.y = ((offset.y + self.collectionView.contentInset.top) * ratio) - self.collectionView.contentInset.top; 150 | 151 | self.shouldChangeCollectionViewContentOffsetAtViewDidLayoutSubviews = YES; 152 | self.collectionViewContentOffsetToSetAtViewDidLayoutSubviews = offset; 153 | 154 | // Update layout at -viewDidLayoutSubviews 155 | self.shouldChangeCollectionViewLayoutAtViewDidLayoutSubviews = YES; 156 | 157 | // Hide transition 158 | [UIView animateWithDuration:0.25 animations:^{ 159 | self.collectionView.alpha = 0.0f; 160 | } completion:nil]; 161 | } 162 | } 163 | 164 | self.lastCollectionViewBounds = self.collectionView.bounds; 165 | self.lastCollectionSuperviewBounds = self.collectionView.superview.bounds; 166 | } 167 | 168 | - (void)viewDidLayoutSubviews { 169 | [super viewDidLayoutSubviews]; 170 | 171 | if (self.shouldChangeCollectionViewLayoutAtViewDidLayoutSubviews) { 172 | self.shouldChangeCollectionViewLayoutAtViewDidLayoutSubviews = NO; 173 | 174 | // Invalidate layout 175 | __weak MUKMediaThumbnailsViewController *weakSelf = self; 176 | [self.collectionView performBatchUpdates:nil completion:^(BOOL finished) 177 | { 178 | MUKMediaThumbnailsViewController *strongSelf = weakSelf; 179 | 180 | // Sometimes collection view goes out of bounds 181 | strongSelf.collectionView.frame = strongSelf.collectionView.superview.bounds; 182 | 183 | // Maintain scrolling ratio 184 | if (strongSelf.shouldChangeCollectionViewContentOffsetAtViewDidLayoutSubviews) 185 | { 186 | strongSelf.shouldChangeCollectionViewContentOffsetAtViewDidLayoutSubviews = NO; 187 | 188 | CGPoint const offset = strongSelf.collectionViewContentOffsetToSetAtViewDidLayoutSubviews; 189 | if (!CGPointEqualToPoint(offset, strongSelf.collectionView.contentOffset)) 190 | { 191 | [strongSelf.collectionView setContentOffset:offset animated:NO]; 192 | } 193 | } 194 | 195 | // Restore visibility 196 | [UIView animateWithDuration:0.1 animations:^{ 197 | strongSelf.collectionView.alpha = 1.0f; 198 | } completion:nil]; 199 | }]; 200 | } 201 | 202 | // Maintain scrolling ratio also when layout has not been invalidated 203 | else if (self.shouldChangeCollectionViewContentOffsetAtViewDidLayoutSubviews) 204 | { 205 | self.shouldChangeCollectionViewContentOffsetAtViewDidLayoutSubviews = NO; 206 | 207 | CGPoint const offset = self.collectionViewContentOffsetToSetAtViewDidLayoutSubviews; 208 | if (!CGPointEqualToPoint(offset, self.collectionView.contentOffset)) 209 | { 210 | [self.collectionView setContentOffset:offset animated:NO]; 211 | } 212 | } 213 | } 214 | 215 | #pragma mark - Methods 216 | 217 | - (void)reloadData { 218 | // Empty caches 219 | [self.imagesCache.cache removeAllObjects]; 220 | [self.mediaAttributesCache.cache removeAllObjects]; 221 | 222 | // Mark every image as not loaded 223 | [self.loadingImageIndexes removeAllIndexes]; 224 | 225 | // Cancel every resize in progress 226 | [self.thumbnailResizeQueue cancelAllOperations]; 227 | 228 | // Reset initial values 229 | self.lastCollectionViewBounds = CGRectNull; 230 | self.lastCollectionSuperviewBounds = CGRectNull; 231 | 232 | // Reload collection view 233 | [self.collectionView reloadData]; 234 | } 235 | 236 | #pragma mark - Private 237 | 238 | static void CommonInitialization(MUKMediaThumbnailsViewController *viewController, UICollectionViewLayout *layout) 239 | { 240 | if (UIUserInterfaceIdiomPad == [[UIDevice currentDevice] userInterfaceIdiom]) 241 | { 242 | viewController->_thumbnailCellSize = CGSizeMake(104.0f, 104.0f); 243 | viewController->_thumbnailCellSpacing = 5.0f; 244 | } 245 | else { 246 | viewController->_thumbnailCellSize = CGSizeMake(75.0f, 75.0f); 247 | viewController->_thumbnailCellSpacing = 4.0f; 248 | } 249 | 250 | // One screen contains ~28 thumbnails on phones: cache more than 5 screens 251 | // One screen contains ~70 thumbnails on pads: cache more than 3 screens 252 | NSInteger countLimit; 253 | if (UIUserInterfaceIdiomPad == [[UIDevice currentDevice] userInterfaceIdiom]) 254 | { 255 | countLimit = 250; 256 | } 257 | else { 258 | countLimit = 150; 259 | } 260 | 261 | viewController.imagesCache = [[MUKMediaModelCache alloc] initWithCountLimit:countLimit cacheNulls:NO]; 262 | viewController.mediaAttributesCache = [[MUKMediaAttributesCache alloc] initWithCountLimit:countLimit cacheNulls:YES]; 263 | 264 | viewController.loadingImageIndexes = [[NSMutableIndexSet alloc] init]; 265 | 266 | viewController.thumbnailResizeQueue = [[NSOperationQueue alloc] init]; 267 | viewController.thumbnailResizeQueue.name = @"it.melive.MUKit.MUKMediaGallery.MUKMediaThumbnailsViewController.ThumbnailResizeQueue"; 268 | viewController.thumbnailResizeQueue.maxConcurrentOperationCount = 1; 269 | 270 | viewController.lastCollectionViewBounds = CGRectNull; 271 | viewController.lastCollectionSuperviewBounds = CGRectNull; 272 | 273 | if (layout) { 274 | viewController.collectionView.collectionViewLayout = layout; 275 | } 276 | } 277 | 278 | - (void)requestLoadingCancellationForImageAtIndexPath:(NSIndexPath *)indexPath { 279 | if ([self.delegate respondsToSelector:@selector(thumbnailsViewController:cancelLoadingForImageAtIndex:)]) 280 | { 281 | NSInteger const kImageIndex = indexPath.item; 282 | 283 | if ([self isLoadingImageAtIndex:kImageIndex]) { 284 | [self.delegate thumbnailsViewController:self cancelLoadingForImageAtIndex:kImageIndex]; 285 | 286 | // Mark as not loading 287 | [self setLoading:NO imageAtIndex:kImageIndex]; 288 | 289 | // Cancel image resizing 290 | [self cancelImageResizingForItemAtIndexPath:indexPath]; 291 | 292 | } // if -isLoadingImageAtIndex: 293 | } // if delegate responds 294 | } 295 | 296 | #pragma mark - Private — Layout 297 | 298 | - (BOOL)automaticallyAdjustsTopPadding { 299 | if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) 300 | { 301 | return self.automaticallyAdjustsScrollViewInsets; 302 | } 303 | 304 | return NO; 305 | } 306 | 307 | - (CGFloat)topPadding { 308 | CGFloat statusBarHeight; 309 | if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) 310 | { 311 | statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height; 312 | } 313 | else { 314 | statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.width; 315 | } 316 | 317 | CGFloat topPadding = statusBarHeight + self.navigationController.navigationBar.bounds.size.height; 318 | return topPadding; 319 | } 320 | 321 | - (void)adjustTopPadding { 322 | CGFloat topPadding = [self topPadding]; 323 | CGFloat diff = topPadding - self.collectionView.contentInset.top; 324 | 325 | if (ABS(diff) > 0.0f) { 326 | UIEdgeInsets insets = UIEdgeInsetsMake(topPadding, 0.0f, 0.0f, 0.0f); 327 | self.collectionView.contentInset = insets; 328 | self.collectionView.scrollIndicatorInsets = insets; 329 | 330 | CGPoint offset = self.collectionView.contentOffset; 331 | offset.y -= diff; 332 | self.collectionView.contentOffset = offset; 333 | } 334 | } 335 | 336 | #pragma mark - Private — KVO 337 | 338 | - (void)beginObservingChangesToAdjustTopPadding { 339 | self.observedNavigationBar = self.navigationController.navigationBar; 340 | [self.observedNavigationBar addObserver:self forKeyPath:@"bounds" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:(__bridge void *)kNavigationBarBoundsKVOIdentifier]; 341 | } 342 | 343 | - (void)stopObservingChangesToAdjustTopPadding { 344 | [self.observedNavigationBar removeObserver:self forKeyPath:@"bounds"]; 345 | self.observedNavigationBar = nil; 346 | } 347 | 348 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 349 | { 350 | if (context == (__bridge void *)kNavigationBarBoundsKVOIdentifier) { 351 | CGRect old = [change[NSKeyValueChangeOldKey] CGRectValue]; 352 | CGRect new = [change[NSKeyValueChangeNewKey] CGRectValue]; 353 | 354 | if (!CGSizeEqualToSize(old.size, new.size)) { 355 | [self adjustTopPadding]; 356 | } 357 | } 358 | } 359 | 360 | #pragma mark - Private — Images 361 | 362 | - (BOOL)isLoadingImageAtIndex:(NSInteger)index { 363 | return [self.loadingImageIndexes containsIndex:index]; 364 | } 365 | 366 | - (void)setLoading:(BOOL)loading imageAtIndex:(NSInteger)index { 367 | if (loading) { 368 | [self.loadingImageIndexes addIndex:index]; 369 | } 370 | else { 371 | [self.loadingImageIndexes removeIndex:index]; 372 | } 373 | } 374 | 375 | - (UIImage *)cachedImageOrRequestLoadingForItemAtIndexPath:(NSIndexPath *)indexPath 376 | { 377 | NSInteger const kImageIndex = indexPath.item; 378 | 379 | // Try to load from cache 380 | UIImage *image = [self.imagesCache objectAtIndex:kImageIndex isNull:NULL]; 381 | 382 | // If no image is cached, check if it's loading 383 | if (image == nil) { 384 | // If it's not loading, request an image 385 | if ([self isLoadingImageAtIndex:kImageIndex] == NO) { 386 | // Mark as loading 387 | [self setLoading:YES imageAtIndex:kImageIndex]; 388 | 389 | // This block is called by delegate which can give back an image 390 | // asynchronously 391 | __weak MUKMediaThumbnailsViewController *weakSelf = self; 392 | void (^completionHandler)(UIImage *) = ^(UIImage *image) { 393 | MUKMediaThumbnailsViewController *strongSelf = weakSelf; 394 | if (!strongSelf) { 395 | return; 396 | } 397 | 398 | // If it is still loading... 399 | if ([strongSelf isLoadingImageAtIndex:kImageIndex]) { 400 | // Resize image in a detached queue 401 | [strongSelf beginResizingImage:image toThumbnailSize:strongSelf.thumbnailCellSize forItemAtIndexPath:indexPath]; 402 | } 403 | }; 404 | 405 | [self.delegate thumbnailsViewController:self loadImageForItemAtIndex:kImageIndex completionHandler:completionHandler]; 406 | } 407 | } 408 | 409 | return image; 410 | } 411 | 412 | - (void)beginResizingImage:(UIImage *)image toThumbnailSize:(CGSize)size forItemAtIndexPath:(NSIndexPath *)indexPath 413 | { 414 | if (!image) { 415 | [self didFinishResizingImage:nil forItemAtIndexPath:indexPath cancelled:NO]; 416 | return; 417 | } 418 | 419 | MUKMediaGalleryImageResizeOperation *op = [[MUKMediaGalleryImageResizeOperation alloc] init]; 420 | op.boundingSize = size; 421 | op.sourceImage = image; 422 | op.userInfo = indexPath; 423 | op.drawingDelegate = self; 424 | 425 | __weak MUKMediaGalleryImageResizeOperation *weakOp = op; 426 | __weak MUKMediaThumbnailsViewController *weakSelf = self; 427 | op.completionBlock = ^{ 428 | MUKMediaGalleryImageResizeOperation *strongOp = weakOp; 429 | MUKMediaThumbnailsViewController *strongSelf = weakSelf; 430 | 431 | dispatch_async(dispatch_get_main_queue(), ^{ 432 | [strongSelf didFinishResizingImage:strongOp.resizedImage forItemAtIndexPath:indexPath cancelled:[strongOp isCancelled]]; 433 | }); 434 | }; 435 | 436 | [self.thumbnailResizeQueue addOperation:op]; 437 | } 438 | 439 | - (void)cancelImageResizingForItemAtIndexPath:(NSIndexPath *)indexPath { 440 | for (MUKMediaGalleryImageResizeOperation *op in self.thumbnailResizeQueue.operations) 441 | { 442 | if ([(NSIndexPath *)op.userInfo isEqual:indexPath]) { 443 | [op cancel]; 444 | break; 445 | } 446 | } 447 | } 448 | 449 | - (void)didFinishResizingImage:(UIImage *)resizedImage forItemAtIndexPath:(NSIndexPath *)indexPath cancelled:(BOOL)cancelled 450 | { 451 | NSInteger const kImageIndex = indexPath.item; 452 | 453 | // Set image as not loading 454 | [self setLoading:NO imageAtIndex:kImageIndex]; 455 | 456 | if (!cancelled) { 457 | // Cache resized image 458 | [self.imagesCache setObject:resizedImage atIndex:kImageIndex]; 459 | 460 | // Take actual cell and set image 461 | MUKMediaThumbnailCell *actualCell = (MUKMediaThumbnailCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; 462 | [self setImage:resizedImage inThumbnailCell:actualCell]; 463 | } 464 | } 465 | 466 | #pragma mark - Private — Cell 467 | 468 | - (void)configureThumbnailCell:(MUKMediaThumbnailCell *)cell atIndexPath:(NSIndexPath *)indexPath 469 | { 470 | cell.backgroundColor = [UIColor colorWithWhite:0.9f alpha:1.0f]; 471 | 472 | // Get cached image or request it to delegate 473 | UIImage *image = [self cachedImageOrRequestLoadingForItemAtIndexPath:indexPath]; 474 | 475 | // Anyway set the image, also if it's nil 476 | [self setImage:image inThumbnailCell:cell]; 477 | 478 | // Configure bottom view of cell 479 | [self configureBottomViewInThumbnailCell:cell atIndexPath:indexPath]; 480 | } 481 | 482 | - (void)setImage:(UIImage *)image inThumbnailCell:(MUKMediaThumbnailCell *)cell { 483 | cell.imageView.image = image; 484 | } 485 | 486 | - (void)configureBottomViewInThumbnailCell:(MUKMediaThumbnailCell *)cell atIndexPath:(NSIndexPath *)indexPath 487 | { 488 | MUKMediaAttributes *attributes = [self.mediaAttributesCache mediaAttributesAtIndex:indexPath.item cacheIfNeeded:YES loadingHandler:^MUKMediaAttributes * 489 | { 490 | if ([self.delegate respondsToSelector:@selector(thumbnailsViewController:attributesForItemAtIndex:)]) 491 | { 492 | return [self.delegate thumbnailsViewController:self attributesForItemAtIndex:indexPath.item]; 493 | } 494 | 495 | return nil; 496 | }]; 497 | 498 | cell.bottomView.hidden = (attributes == nil || (attributes.kind == MUKMediaKindImage && [attributes.caption length] == 0)); 499 | cell.bottomIconImageView.image = [self thumbnailCellBottomViewIconForMediaAttributes:attributes]; 500 | cell.captionLabel.text = attributes.caption; 501 | } 502 | 503 | - (UIImage *)thumbnailCellBottomViewIconForMediaAttributes:(MUKMediaAttributes *)attributes 504 | { 505 | if (attributes == nil) { 506 | return nil; 507 | } 508 | 509 | UIImage *icon; 510 | 511 | switch (attributes.kind) { 512 | case MUKMediaKindAudio: 513 | icon = [MUKMediaGalleryUtils imageNamed:@"audio_small"]; 514 | break; 515 | 516 | case MUKMediaKindVideo: 517 | case MUKMediaKindYouTubeVideo: 518 | icon = [MUKMediaGalleryUtils imageNamed:@"video_small"]; 519 | break; 520 | 521 | default: 522 | icon = nil; 523 | break; 524 | } 525 | 526 | return icon; 527 | } 528 | 529 | #pragma mark - Private — Carousel 530 | 531 | - (void)carouselViewControllerDoneBarButtonItemPressed:(id)sender { 532 | [self.carouselPresentationViewController dismissViewControllerAnimated:YES completion:nil]; 533 | self.carouselPresentationViewController = nil; 534 | } 535 | 536 | #pragma mark - 537 | 538 | - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath 539 | { 540 | return self.thumbnailCellSize; 541 | } 542 | 543 | - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section 544 | { 545 | return self.thumbnailCellSpacing/2.0f; 546 | } 547 | 548 | - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section 549 | { 550 | return self.thumbnailCellSpacing; 551 | } 552 | 553 | - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section 554 | { 555 | CGFloat const availableWidth = CGRectGetWidth(collectionView.superview.frame); 556 | NSUInteger const cellsPerRow = floorf(availableWidth/self.thumbnailCellSize.width); 557 | 558 | CGFloat const usedSpace = (cellsPerRow * self.thumbnailCellSize.width) + ((cellsPerRow - 1) * self.thumbnailCellSpacing); 559 | CGFloat const margin = (availableWidth - usedSpace) / 2.0; 560 | 561 | return UIEdgeInsetsMake(self.thumbnailCellSpacing, margin, self.thumbnailCellSpacing, margin); 562 | } 563 | 564 | #pragma mark - 565 | 566 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 567 | { 568 | return [self.delegate numberOfItemsInThumbnailsViewController:self]; 569 | } 570 | 571 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 572 | { 573 | MUKMediaThumbnailCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath]; 574 | [self configureThumbnailCell:cell atIndexPath:indexPath]; 575 | return cell; 576 | } 577 | 578 | #pragma mark - 579 | 580 | - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath 581 | { 582 | [self requestLoadingCancellationForImageAtIndexPath:indexPath]; 583 | } 584 | 585 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath 586 | { 587 | if ([self.delegate respondsToSelector:@selector(thumbnailsViewController:carouselToPresentAfterSelectingItemAtIndex:)]) 588 | { 589 | MUKMediaCarouselViewController *carouselViewController = [self.delegate thumbnailsViewController:self carouselToPresentAfterSelectingItemAtIndex:indexPath.item]; 590 | 591 | if (carouselViewController) { 592 | [carouselViewController scrollToItemAtIndex:indexPath.item animated:NO completion:nil]; 593 | 594 | self.isTransitioningWithCarouselViewController = YES; 595 | 596 | MUKMediaThumbnailsViewControllerToCarouselTransition transition = MUKMediaThumbnailsViewControllerToCarouselTransitionPush; 597 | if ([self.delegate respondsToSelector:@selector(thumbnailsViewController:transitionToPresentCarouselViewController:forItemAtIndex:)]) 598 | { 599 | transition = [self.delegate thumbnailsViewController:self transitionToPresentCarouselViewController:carouselViewController forItemAtIndex:indexPath.item]; 600 | } 601 | 602 | UIViewController *presentationViewController = nil; 603 | if ([self.delegate respondsToSelector:@selector(thumbnailsViewController:presentationViewControllerForCarouselViewController:forItemAtIndex:)]) 604 | { 605 | presentationViewController = [self.delegate thumbnailsViewController:self presentationViewControllerForCarouselViewController:carouselViewController forItemAtIndex:indexPath.item]; 606 | } 607 | 608 | // Define a local block to choose what to present 609 | UIViewController *(^viewControllerToPresentBlock)(UIViewController *defaultViewController); 610 | viewControllerToPresentBlock = ^(UIViewController *defaultViewController){ 611 | // Choose what to present 612 | UIViewController *viewController = nil; 613 | if ([self.delegate respondsToSelector:@selector(thumbnailsViewController:viewControllerToPresent:toShowCarouselViewController:forItemAtIndex:)]) 614 | { 615 | viewController = [self.delegate thumbnailsViewController:self viewControllerToPresent:defaultViewController toShowCarouselViewController:carouselViewController forItemAtIndex:indexPath.item]; 616 | } 617 | 618 | if (![viewController isKindOfClass:[UIViewController class]]) { 619 | viewController = defaultViewController; 620 | } 621 | 622 | return viewController; 623 | }; 624 | 625 | switch (transition) { 626 | case MUKMediaThumbnailsViewControllerToCarouselTransitionPush: 627 | { 628 | // Choose presenter 629 | UINavigationController *navController = nil; 630 | if ([presentationViewController isKindOfClass:[UINavigationController class]]) 631 | { 632 | navController = (UINavigationController *)presentationViewController; 633 | } 634 | else { 635 | navController = self.navigationController; 636 | } 637 | 638 | // Choose what to present 639 | UIViewController *viewController = viewControllerToPresentBlock(carouselViewController); 640 | 641 | // Present it! 642 | [navController pushViewController:viewController animated:YES]; 643 | 644 | break; 645 | } 646 | 647 | case MUKMediaThumbnailsViewControllerToCarouselTransitionCoverVertical: 648 | case MUKMediaThumbnailsViewControllerToCarouselTransitionCrossDissolve: 649 | { 650 | if (![presentationViewController respondsToSelector:@selector(presentViewController:animated:completion:)]) 651 | { 652 | presentationViewController = self; 653 | } 654 | 655 | // Create a button to close modal presentation 656 | UIBarButtonItem *doneBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(carouselViewControllerDoneBarButtonItemPressed:)]; 657 | carouselViewController.navigationItem.leftBarButtonItem = doneBarButtonItem; 658 | 659 | // Embed in navigation controller 660 | UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:carouselViewController]; 661 | navController.navigationBar.barStyle = UIBarStyleBlackTranslucent; 662 | 663 | // Choose what to present 664 | UIViewController *viewController = viewControllerToPresentBlock(navController); 665 | 666 | // Choose transition style 667 | if (transition == MUKMediaThumbnailsViewControllerToCarouselTransitionCrossDissolve) 668 | { 669 | viewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; 670 | } 671 | 672 | // Present it! 673 | self.carouselPresentationViewController = presentationViewController; 674 | [presentationViewController presentViewController:viewController animated:YES completion:nil]; 675 | 676 | break; 677 | } 678 | 679 | default: 680 | break; 681 | } // switch 682 | } 683 | } 684 | } 685 | 686 | #pragma mark - 687 | 688 | - (void)imageResizeOperation:(MUKMediaGalleryImageResizeOperation *)op drawOverResizedImageInContext:(CGContextRef)ctx 689 | { 690 | // Draw a border over the image 691 | CGRect rect = CGRectZero; 692 | rect.size = op.boundingSize; 693 | [MUKMediaThumbnailCell drawBorderInsideRect:rect context:ctx]; 694 | } 695 | 696 | @end 697 | --------------------------------------------------------------------------------