MWPhotoBrowser.m 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658
  1. //
  2. // MWPhotoBrowser.m
  3. // MWPhotoBrowser
  4. //
  5. // Created by Michael Waterfall on 14/10/2010.
  6. // Copyright 2010 d3i. All rights reserved.
  7. //
  8. #import <QuartzCore/QuartzCore.h>
  9. #import "MWCommon.h"
  10. #import "MWPhotoBrowser.h"
  11. #import "MWPhotoBrowserPrivate.h"
  12. #import "UIImage+MWPhotoBrowser.h"
  13. #define PADDING 10
  14. #pragma clang diagnostic ignored "-Wundeclared-selector"
  15. static void * MWVideoPlayerObservation = &MWVideoPlayerObservation;
  16. @implementation MWPhotoBrowser
  17. #pragma mark - Init
  18. - (id)init {
  19. if ((self = [super init])) {
  20. [self _initialisation];
  21. }
  22. return self;
  23. }
  24. - (id)initWithDelegate:(id <MWPhotoBrowserDelegate>)delegate {
  25. if ((self = [self init])) {
  26. _delegate = delegate;
  27. }
  28. return self;
  29. }
  30. - (id)initWithPhotos:(NSArray *)photosArray {
  31. if ((self = [self init])) {
  32. _fixedPhotosArray = photosArray;
  33. }
  34. return self;
  35. }
  36. - (id)initWithCoder:(NSCoder *)decoder {
  37. if ((self = [super initWithCoder:decoder])) {
  38. [self _initialisation];
  39. }
  40. return self;
  41. }
  42. - (void)_initialisation {
  43. // Defaults
  44. NSNumber *isVCBasedStatusBarAppearanceNum = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
  45. if (isVCBasedStatusBarAppearanceNum) {
  46. _isVCBasedStatusBarAppearance = isVCBasedStatusBarAppearanceNum.boolValue;
  47. } else {
  48. _isVCBasedStatusBarAppearance = YES; // default
  49. }
  50. self.hidesBottomBarWhenPushed = YES;
  51. _hasBelongedToViewController = NO;
  52. _photoCount = NSNotFound;
  53. _previousLayoutBounds = CGRectZero;
  54. _currentPageIndex = 0;
  55. _previousPageIndex = NSUIntegerMax;
  56. _displayActionButton = YES;
  57. _displayShareButton = YES;
  58. _displayDeleteButton = YES;
  59. _displayNavArrows = NO;
  60. _zoomPhotosToFill = YES;
  61. _performingLayout = NO; // Reset on view did appear
  62. _rotating = NO;
  63. _viewIsActive = NO;
  64. _enableGrid = YES;
  65. _startOnGrid = NO;
  66. _enableSwipeToDismiss = YES;
  67. _delayToHideElements = 5;
  68. _visiblePages = [[NSMutableSet alloc] init];
  69. _recycledPages = [[NSMutableSet alloc] init];
  70. _photos = [[NSMutableArray alloc] init];
  71. _thumbPhotos = [[NSMutableArray alloc] init];
  72. _currentGridContentOffset = CGPointMake(0, CGFLOAT_MAX);
  73. _didSavePreviousStateOfNavBar = NO;
  74. self.automaticallyAdjustsScrollViewInsets = NO;
  75. // Listen for MWPhoto notifications
  76. [[NSNotificationCenter defaultCenter] addObserver:self
  77. selector:@selector(handleMWPhotoLoadingDidEndNotification:)
  78. name:MWPHOTO_LOADING_DID_END_NOTIFICATION
  79. object:nil];
  80. }
  81. - (void)dealloc {
  82. [self clearCurrentVideo];
  83. _pagingScrollView.delegate = nil;
  84. [[NSNotificationCenter defaultCenter] removeObserver:self];
  85. [self releaseAllUnderlyingPhotos:NO];
  86. }
  87. - (void)releaseAllUnderlyingPhotos:(BOOL)preserveCurrent {
  88. // Create a copy in case this array is modified while we are looping through
  89. // Release photos
  90. NSArray *copy = [_photos copy];
  91. for (id p in copy) {
  92. if (p != [NSNull null]) {
  93. if (preserveCurrent && p == [self photoAtIndex:self.currentIndex]) {
  94. continue; // skip current
  95. }
  96. [p unloadUnderlyingImage];
  97. }
  98. }
  99. // Release thumbs
  100. copy = [_thumbPhotos copy];
  101. for (id p in copy) {
  102. if (p != [NSNull null]) {
  103. [p unloadUnderlyingImage];
  104. }
  105. }
  106. }
  107. - (void)didReceiveMemoryWarning {
  108. // Release any cached data, images, etc that aren't in use.
  109. [self releaseAllUnderlyingPhotos:YES];
  110. [_recycledPages removeAllObjects];
  111. // Releases the view if it doesn't have a superview.
  112. [super didReceiveMemoryWarning];
  113. }
  114. #pragma mark - View Loading
  115. // Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
  116. - (void)viewDidLoad {
  117. // Validate grid settings
  118. if (_startOnGrid) _enableGrid = YES;
  119. if (_enableGrid) {
  120. _enableGrid = [_delegate respondsToSelector:@selector(photoBrowser:thumbPhotoAtIndex:)];
  121. }
  122. if (!_enableGrid) _startOnGrid = NO;
  123. // View
  124. self.view.backgroundColor = [UIColor whiteColor]; //TWS
  125. self.view.clipsToBounds = YES;
  126. // Setup paging scrolling view
  127. CGRect pagingScrollViewFrame = [self frameForPagingScrollView];
  128. _pagingScrollView = [[UIScrollView alloc] initWithFrame:pagingScrollViewFrame];
  129. _pagingScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  130. _pagingScrollView.pagingEnabled = YES;
  131. _pagingScrollView.delegate = self;
  132. _pagingScrollView.showsHorizontalScrollIndicator = NO;
  133. _pagingScrollView.showsVerticalScrollIndicator = NO;
  134. _pagingScrollView.backgroundColor = [UIColor whiteColor]; //TWS
  135. _pagingScrollView.contentSize = [self contentSizeForPagingScrollView];
  136. [self.view addSubview:_pagingScrollView];
  137. // Toolbar
  138. _toolbar = [[UIToolbar alloc] initWithFrame:[self frameForToolbarAtOrientation:[[UIApplication sharedApplication] statusBarOrientation]]];
  139. _toolbar.tintColor = COLOR_TABBAR_TEXT; //TWS
  140. _toolbar.barTintColor = COLOR_TABBAR;
  141. [_toolbar setBackgroundImage:nil forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];
  142. [_toolbar setBackgroundImage:nil forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsCompact];
  143. _toolbar.barStyle = UIBarStyleDefault; //TWS
  144. _toolbar.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
  145. // Toolbar Items
  146. if (self.displayNavArrows) {
  147. NSString *arrowPathFormat = @"UIBarButtonItemArrow%@";
  148. UIImage *previousButtonImage = [UIImage imageForResourcePath:[NSString stringWithFormat:arrowPathFormat, @"Left"] ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]];
  149. UIImage *nextButtonImage = [UIImage imageForResourcePath:[NSString stringWithFormat:arrowPathFormat, @"Right"] ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]];
  150. _previousButton = [[UIBarButtonItem alloc] initWithImage:previousButtonImage style:UIBarButtonItemStylePlain target:self action:@selector(gotoPreviousPage)];
  151. _nextButton = [[UIBarButtonItem alloc] initWithImage:nextButtonImage style:UIBarButtonItemStylePlain target:self action:@selector(gotoNextPage)];
  152. }
  153. // TWS
  154. if (self.displayDeleteButton) {
  155. _deleteButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(deleteButtonPressed:)];
  156. }
  157. if (self.displayActionButton) {
  158. _actionButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(actionButtonPressed:)];
  159. }
  160. if (self.displayShareButton) {
  161. _shareButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"actionSheetShare"] style:UIBarButtonItemStylePlain target:self action:@selector(shareButtonPressed:)];
  162. }
  163. // Update
  164. [self reloadData];
  165. // Swipe to dismiss
  166. if (_enableSwipeToDismiss) {
  167. UISwipeGestureRecognizer *swipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonPressed:)];
  168. swipeGesture.direction = UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp;
  169. [self.view addGestureRecognizer:swipeGesture];
  170. }
  171. // Super
  172. [super viewDidLoad];
  173. }
  174. - (void)performLayout {
  175. // Setup
  176. _performingLayout = YES;
  177. NSUInteger numberOfPhotos = [self numberOfPhotos];
  178. // Setup pages
  179. [_visiblePages removeAllObjects];
  180. [_recycledPages removeAllObjects];
  181. // Navigation buttons
  182. if ([self.navigationController.viewControllers objectAtIndex:0] == self) {
  183. // We're first on stack so show done button
  184. _doneButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Done", nil) style:UIBarButtonItemStylePlain target:self action:@selector(doneButtonPressed:)];
  185. // Set appearance
  186. [_doneButton setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
  187. [_doneButton setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsCompact];
  188. [_doneButton setBackgroundImage:nil forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
  189. [_doneButton setBackgroundImage:nil forState:UIControlStateHighlighted barMetrics:UIBarMetricsCompact];
  190. [_doneButton setTitleTextAttributes:[NSDictionary dictionary] forState:UIControlStateNormal];
  191. [_doneButton setTitleTextAttributes:[NSDictionary dictionary] forState:UIControlStateHighlighted];
  192. self.navigationItem.rightBarButtonItem = _doneButton;
  193. }
  194. // color
  195. self.navigationController.navigationBar.barTintColor = COLOR_NAVIGATIONBAR;
  196. self.navigationController.navigationBar.tintColor = COLOR_NAVIGATIONBAR_TEXT;
  197. [self.navigationController.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName : COLOR_NAVIGATIONBAR_TEXT}];
  198. // Toolbar items
  199. BOOL hasItems = NO;
  200. UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:self action:nil];
  201. fixedSpace.width = 32; // To balance action button
  202. UIBarButtonItem *fixedSpaceMini = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:self action:nil];
  203. fixedSpaceMini.width = 25; // To balance action button
  204. UIBarButtonItem *flexSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:self action:nil];
  205. NSMutableArray *items = [[NSMutableArray alloc] init];
  206. // Left button - Grid
  207. if (_enableGrid) {
  208. hasItems = YES;
  209. [items addObject:[[UIBarButtonItem alloc] initWithImage:[UIImage imageForResourcePath:@"UIBarButtonItemGrid" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] style:UIBarButtonItemStylePlain target:self action:@selector(showGridAnimated)]];
  210. } else {
  211. [items addObject:fixedSpace];
  212. }
  213. // Middle - Nav
  214. if (_previousButton && _nextButton && numberOfPhotos > 1) {
  215. hasItems = YES;
  216. [items addObject:flexSpace];
  217. [items addObject:_previousButton];
  218. [items addObject:flexSpace];
  219. [items addObject:_nextButton];
  220. [items addObject:flexSpace];
  221. } else {
  222. [items addObject:flexSpace];
  223. }
  224. // Right - Action
  225. if (_actionButton && !(!hasItems && !self.navigationItem.rightBarButtonItem)) {
  226. if (_deleteButton) {
  227. [items addObject:_deleteButton];
  228. [items addObject:fixedSpaceMini];
  229. }
  230. if (_shareButton) {
  231. [items addObject:_shareButton];
  232. [items addObject:fixedSpaceMini];
  233. }
  234. [items addObject:_actionButton];
  235. } else {
  236. // We're not showing the toolbar so try and show in top right
  237. if (_actionButton)
  238. self.navigationItem.rightBarButtonItem = _actionButton;
  239. [items addObject:fixedSpace];
  240. }
  241. // Toolbar visibility
  242. [_toolbar setItems:items];
  243. BOOL hideToolbar = YES;
  244. for (UIBarButtonItem* item in _toolbar.items) {
  245. if (item != fixedSpace && item != flexSpace) {
  246. hideToolbar = NO;
  247. break;
  248. }
  249. }
  250. if (hideToolbar) {
  251. [_toolbar removeFromSuperview];
  252. } else {
  253. [self.view addSubview:_toolbar];
  254. }
  255. // Update nav
  256. [self updateNavigation];
  257. // Content offset
  258. _pagingScrollView.contentOffset = [self contentOffsetForPageAtIndex:_currentPageIndex];
  259. [self tilePages];
  260. _performingLayout = NO;
  261. }
  262. // Release any retained subviews of the main view.
  263. - (void)viewDidUnload {
  264. _currentPageIndex = 0;
  265. _pagingScrollView = nil;
  266. _visiblePages = nil;
  267. _recycledPages = nil;
  268. _toolbar = nil;
  269. _previousButton = nil;
  270. _nextButton = nil;
  271. _progressHUD = nil;
  272. [super viewDidUnload];
  273. }
  274. - (BOOL)presentingViewControllerPrefersStatusBarHidden {
  275. UIViewController *presenting = self.presentingViewController;
  276. if (presenting) {
  277. if ([presenting isKindOfClass:[UINavigationController class]]) {
  278. presenting = [(UINavigationController *)presenting topViewController];
  279. }
  280. } else {
  281. // We're in a navigation controller so get previous one!
  282. if (self.navigationController && self.navigationController.viewControllers.count > 1) {
  283. presenting = [self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2];
  284. }
  285. }
  286. if (presenting) {
  287. return [presenting prefersStatusBarHidden];
  288. } else {
  289. return NO;
  290. }
  291. }
  292. #pragma mark - Appearance
  293. - (void)viewWillAppear:(BOOL)animated {
  294. // Super
  295. [super viewWillAppear:animated];
  296. // Status bar
  297. if (!_viewHasAppearedInitially) {
  298. _leaveStatusBarAlone = [self presentingViewControllerPrefersStatusBarHidden];
  299. // Check if status bar is hidden on first appear, and if so then ignore it
  300. if (CGRectEqualToRect([[UIApplication sharedApplication] statusBarFrame], CGRectZero)) {
  301. _leaveStatusBarAlone = YES;
  302. }
  303. }
  304. // Nav Bar Appearance iPAD
  305. if (self.traitCollection.horizontalSizeClass != UIUserInterfaceSizeClassCompact) {
  306. // ----- TWS ----- //
  307. self.navigationItem.hidesBackButton = YES;
  308. self.navigationController.topViewController.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
  309. self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
  310. }
  311. // Update UI
  312. [self hideControlsAfterDelay];
  313. // Initial appearance
  314. if (!_viewHasAppearedInitially) {
  315. if (_startOnGrid) {
  316. [self showGrid:NO];
  317. }
  318. }
  319. // If rotation occured while we're presenting a modal
  320. // and the index changed, make sure we show the right one now
  321. if (_currentPageIndex != _pageIndexBeforeRotation) {
  322. [self jumpToPageAtIndex:_pageIndexBeforeRotation animated:NO];
  323. }
  324. // Layaout
  325. [self.view setNeedsLayout];
  326. }
  327. - (void)viewDidAppear:(BOOL)animated {
  328. [super viewDidAppear:animated];
  329. _viewIsActive = YES;
  330. // Autoplay if first is video
  331. if (!_viewHasAppearedInitially) {
  332. if (_autoPlayOnAppear) {
  333. MWPhoto *photo = [self photoAtIndex:_currentPageIndex];
  334. if ([photo respondsToSelector:@selector(isVideo)] && photo.isVideo) {
  335. [self playVideoAtIndex:_currentPageIndex];
  336. }
  337. }
  338. }
  339. _viewHasAppearedInitially = YES;
  340. }
  341. - (void)viewWillDisappear:(BOOL)animated {
  342. // Detect if rotation occurs while we're presenting a modal
  343. _pageIndexBeforeRotation = _currentPageIndex;
  344. // Check that we're being popped for good
  345. if ([self.navigationController.viewControllers objectAtIndex:0] != self &&
  346. ![self.navigationController.viewControllers containsObject:self]) {
  347. // State
  348. _viewIsActive = NO;
  349. }
  350. // Controls
  351. [self.navigationController.navigationBar.layer removeAllAnimations]; // Stop all animations on nav bar
  352. [NSObject cancelPreviousPerformRequestsWithTarget:self]; // Cancel any pending toggles from taps
  353. [self setControlsHidden:NO animated:NO permanent:YES];
  354. // Super
  355. [super viewWillDisappear:animated];
  356. }
  357. - (void)willMoveToParentViewController:(UIViewController *)parent {
  358. if (parent && _hasBelongedToViewController) {
  359. [NSException raise:@"MWPhotoBrowser Instance Reuse" format:@"MWPhotoBrowser instances cannot be reused."];
  360. }
  361. }
  362. - (void)didMoveToParentViewController:(UIViewController *)parent {
  363. if (!parent) _hasBelongedToViewController = YES;
  364. }
  365. #pragma mark - Layout
  366. - (void)viewWillLayoutSubviews {
  367. [super viewWillLayoutSubviews];
  368. [self layoutVisiblePages];
  369. }
  370. - (void)layoutVisiblePages {
  371. // Flag
  372. _performingLayout = YES;
  373. // Toolbar
  374. _toolbar.frame = [self frameForToolbarAtOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
  375. // Remember index
  376. NSUInteger indexPriorToLayout = _currentPageIndex;
  377. // Get paging scroll view frame to determine if anything needs changing
  378. CGRect pagingScrollViewFrame = [self frameForPagingScrollView];
  379. // Frame needs changing
  380. if (!_skipNextPagingScrollViewPositioning) {
  381. _pagingScrollView.frame = pagingScrollViewFrame;
  382. }
  383. _skipNextPagingScrollViewPositioning = NO;
  384. // Recalculate contentSize based on current orientation
  385. _pagingScrollView.contentSize = [self contentSizeForPagingScrollView];
  386. // Adjust frames and configuration of each visible page
  387. for (MWZoomingScrollView *page in _visiblePages) {
  388. NSUInteger index = page.index;
  389. page.frame = [self frameForPageAtIndex:index];
  390. if (page.captionView) {
  391. page.captionView.frame = [self frameForCaptionView:page.captionView atIndex:index];
  392. }
  393. if (page.selectedButton) {
  394. page.selectedButton.frame = [self frameForSelectedButton:page.selectedButton atIndex:index];
  395. }
  396. if (page.playButton) {
  397. page.playButton.frame = [self frameForPlayButton:page.playButton atIndex:index];
  398. }
  399. // Adjust scales if bounds has changed since last time
  400. if (!CGRectEqualToRect(_previousLayoutBounds, self.view.bounds)) {
  401. // Update zooms for new bounds
  402. [page setMaxMinZoomScalesForCurrentBounds];
  403. _previousLayoutBounds = self.view.bounds;
  404. }
  405. }
  406. // Adjust video loading indicator if it's visible
  407. [self positionVideoLoadingIndicator];
  408. // Adjust contentOffset to preserve page location based on values collected prior to location
  409. _pagingScrollView.contentOffset = [self contentOffsetForPageAtIndex:indexPriorToLayout];
  410. [self didStartViewingPageAtIndex:_currentPageIndex]; // initial
  411. // Reset
  412. _currentPageIndex = indexPriorToLayout;
  413. _performingLayout = NO;
  414. }
  415. #pragma mark - Rotation
  416. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
  417. return YES;
  418. }
  419. #if __IPHONE_OS_VERSION_MAX_ALLOWED < 90000
  420. - (NSUInteger)supportedInterfaceOrientations
  421. #else
  422. - (UIInterfaceOrientationMask)supportedInterfaceOrientations
  423. #endif
  424. {
  425. return UIInterfaceOrientationMaskPortrait;
  426. }
  427. - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
  428. // Remember page index before rotation
  429. _pageIndexBeforeRotation = _currentPageIndex;
  430. _rotating = YES;
  431. // In iOS 7 the nav bar gets shown after rotation, but might as well do this for everything!
  432. if ([self areControlsHidden]) {
  433. // Force hidden
  434. self.navigationController.navigationBarHidden = YES;
  435. }
  436. }
  437. - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
  438. // Perform layout
  439. _currentPageIndex = _pageIndexBeforeRotation;
  440. // Delay control holding
  441. [self hideControlsAfterDelay];
  442. // Layout
  443. [self layoutVisiblePages];
  444. }
  445. - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
  446. _rotating = NO;
  447. // Ensure nav bar isn't re-displayed
  448. if ([self areControlsHidden]) {
  449. self.navigationController.navigationBarHidden = NO;
  450. self.navigationController.navigationBar.alpha = 0;
  451. }
  452. }
  453. #pragma mark - Data
  454. - (NSUInteger)currentIndex {
  455. return _currentPageIndex;
  456. }
  457. - (void)reloadData {
  458. // Reset
  459. _photoCount = NSNotFound;
  460. // Get data
  461. NSUInteger numberOfPhotos = [self numberOfPhotos];
  462. [self releaseAllUnderlyingPhotos:YES];
  463. [_photos removeAllObjects];
  464. [_thumbPhotos removeAllObjects];
  465. for (int i = 0; i < numberOfPhotos; i++) {
  466. [_photos addObject:[NSNull null]];
  467. [_thumbPhotos addObject:[NSNull null]];
  468. }
  469. // Update current page index
  470. if (numberOfPhotos > 0) {
  471. _currentPageIndex = MAX(0, MIN(_currentPageIndex, numberOfPhotos - 1));
  472. } else {
  473. _currentPageIndex = 0;
  474. }
  475. // Update layout
  476. if ([self isViewLoaded]) {
  477. while (_pagingScrollView.subviews.count) {
  478. [[_pagingScrollView.subviews lastObject] removeFromSuperview];
  479. }
  480. [self performLayout];
  481. [self.view setNeedsLayout];
  482. }
  483. _toolbar.hidden = (_gridController ? true : false);
  484. _toolbar.alpha = (_gridController ? 0 : 1);
  485. //TWS
  486. if (!_gridController) {
  487. self.navigationController.navigationBar.hidden = (_gridController ? true : false);
  488. self.navigationController.navigationBar.alpha = (_gridController ? 0 : 1);
  489. }
  490. //TWS Update grid if it's presented
  491. [_gridController.collectionView reloadData];
  492. }
  493. - (void)reloadDataGridAtIndex:(NSArray *)indexPaths
  494. {
  495. [_gridController.collectionView reloadItemsAtIndexPaths:indexPaths];
  496. }
  497. // TWS
  498. - (BOOL)isGridReload:(NSUInteger)index
  499. {
  500. if (_gridController) {
  501. return [_gridController visibleGridIndexPath:index];
  502. } else return NO;
  503. }
  504. - (NSUInteger)numberOfPhotos {
  505. if (_photoCount == NSNotFound) {
  506. if ([_delegate respondsToSelector:@selector(numberOfPhotosInPhotoBrowser:)]) {
  507. _photoCount = [_delegate numberOfPhotosInPhotoBrowser:self];
  508. } else if (_fixedPhotosArray) {
  509. _photoCount = _fixedPhotosArray.count;
  510. }
  511. }
  512. if (_photoCount == NSNotFound) _photoCount = 0;
  513. return _photoCount;
  514. }
  515. - (id<MWPhoto>)photoAtIndex:(NSUInteger)index {
  516. id <MWPhoto> photo = nil;
  517. if (index < _photos.count) {
  518. if ([_photos objectAtIndex:index] == [NSNull null]) {
  519. if ([_delegate respondsToSelector:@selector(photoBrowser:photoAtIndex:)]) {
  520. photo = [_delegate photoBrowser:self photoAtIndex:index];
  521. } else if (_fixedPhotosArray && index < _fixedPhotosArray.count) {
  522. photo = [_fixedPhotosArray objectAtIndex:index];
  523. }
  524. if (photo) [_photos replaceObjectAtIndex:index withObject:photo];
  525. } else {
  526. photo = [_photos objectAtIndex:index];
  527. }
  528. }
  529. return photo;
  530. }
  531. - (id<MWPhoto>)thumbPhotoAtIndex:(NSUInteger)index {
  532. id <MWPhoto> photo = nil;
  533. if (index < _thumbPhotos.count) {
  534. if ([_thumbPhotos objectAtIndex:index] == [NSNull null]) {
  535. if ([_delegate respondsToSelector:@selector(photoBrowser:thumbPhotoAtIndex:)]) {
  536. photo = [_delegate photoBrowser:self thumbPhotoAtIndex:index];
  537. }
  538. if (photo) [_thumbPhotos replaceObjectAtIndex:index withObject:photo];
  539. } else {
  540. photo = [_thumbPhotos objectAtIndex:index];
  541. }
  542. }
  543. return photo;
  544. }
  545. - (MWCaptionView *)captionViewForPhotoAtIndex:(NSUInteger)index {
  546. MWCaptionView *captionView = nil;
  547. if ([_delegate respondsToSelector:@selector(photoBrowser:captionViewForPhotoAtIndex:)]) {
  548. captionView = [_delegate photoBrowser:self captionViewForPhotoAtIndex:index];
  549. } else {
  550. id <MWPhoto> photo = [self photoAtIndex:index];
  551. if ([photo respondsToSelector:@selector(caption)]) {
  552. if ([photo caption]) captionView = [[MWCaptionView alloc] initWithPhoto:photo];
  553. }
  554. }
  555. captionView.alpha = [self areControlsHidden] ? 0 : 1; // Initial alpha
  556. return captionView;
  557. }
  558. - (BOOL)photoIsSelectedAtIndex:(NSUInteger)index {
  559. BOOL value = NO;
  560. if (_displaySelectionButtons) {
  561. if ([self.delegate respondsToSelector:@selector(photoBrowser:isPhotoSelectedAtIndex:)]) {
  562. value = [self.delegate photoBrowser:self isPhotoSelectedAtIndex:index];
  563. }
  564. }
  565. return value;
  566. }
  567. - (void)setPhotoSelected:(BOOL)selected atIndex:(NSUInteger)index {
  568. if (_displaySelectionButtons) {
  569. if ([self.delegate respondsToSelector:@selector(photoBrowser:photoAtIndex:selectedChanged:)]) {
  570. [self.delegate photoBrowser:self photoAtIndex:index selectedChanged:selected];
  571. }
  572. }
  573. }
  574. - (UIImage *)imageForPhoto:(id<MWPhoto>)photo {
  575. if (photo) {
  576. // Get image or obtain in background
  577. if ([photo underlyingImage]) {
  578. return [photo underlyingImage];
  579. } else {
  580. [photo loadUnderlyingImageAndNotify];
  581. }
  582. }
  583. return nil;
  584. }
  585. - (void)loadAdjacentPhotosIfNecessary:(id<MWPhoto>)photo
  586. {
  587. MWZoomingScrollView *page = [self pageDisplayingPhoto:photo];
  588. if (page) {
  589. // If page is current page then initiate loading of previous and next pages
  590. NSUInteger pageIndex = page.index;
  591. if (_currentPageIndex == pageIndex) {
  592. if (pageIndex > 0) {
  593. // Preload index - 1
  594. id <MWPhoto> photo = [self photoAtIndex:pageIndex-1];
  595. if (![photo underlyingImage]) {
  596. [photo loadUnderlyingImageAndNotify];
  597. MWLog(@"Pre-loading image at index %lu", (unsigned long)pageIndex-1);
  598. }
  599. }
  600. if (pageIndex < [self numberOfPhotos] - 1) {
  601. // Preload index + 1
  602. id <MWPhoto> photo = [self photoAtIndex:pageIndex+1];
  603. if (![photo underlyingImage]) {
  604. [photo loadUnderlyingImageAndNotify];
  605. MWLog(@"Pre-loading image at index %lu", (unsigned long)pageIndex+1);
  606. }
  607. }
  608. }
  609. }
  610. }
  611. #pragma mark - MWPhoto Loading Notification
  612. - (void)handleMWPhotoLoadingDidEndNotification:(NSNotification *)notification {
  613. id <MWPhoto> photo = [notification object];
  614. MWZoomingScrollView *page = [self pageDisplayingPhoto:photo];
  615. if (page) {
  616. if ([photo underlyingImage]) {
  617. // Successful load
  618. [page displayImage];
  619. [self loadAdjacentPhotosIfNecessary:photo];
  620. } else {
  621. // Failed to load
  622. [page displayImageFailure];
  623. }
  624. // Update nav
  625. [self updateNavigation];
  626. }
  627. }
  628. #pragma mark - Paging
  629. - (void)tilePages {
  630. // Calculate which pages should be visible
  631. // Ignore padding as paging bounces encroach on that
  632. // and lead to false page loads
  633. CGRect visibleBounds = _pagingScrollView.bounds;
  634. NSInteger iFirstIndex = (NSInteger)floorf((CGRectGetMinX(visibleBounds)+PADDING*2) / CGRectGetWidth(visibleBounds));
  635. NSInteger iLastIndex = (NSInteger)floorf((CGRectGetMaxX(visibleBounds)-PADDING*2-1) / CGRectGetWidth(visibleBounds));
  636. if (iFirstIndex < 0) iFirstIndex = 0;
  637. if (iFirstIndex > [self numberOfPhotos] - 1) iFirstIndex = [self numberOfPhotos] - 1;
  638. if (iLastIndex < 0) iLastIndex = 0;
  639. if (iLastIndex > [self numberOfPhotos] - 1) iLastIndex = [self numberOfPhotos] - 1;
  640. // Recycle no longer needed pages
  641. NSInteger pageIndex;
  642. for (MWZoomingScrollView *page in _visiblePages) {
  643. pageIndex = page.index;
  644. if (pageIndex < (NSUInteger)iFirstIndex || pageIndex > (NSUInteger)iLastIndex) {
  645. [_recycledPages addObject:page];
  646. [page.captionView removeFromSuperview];
  647. [page.selectedButton removeFromSuperview];
  648. [page.playButton removeFromSuperview];
  649. [page prepareForReuse];
  650. [page removeFromSuperview];
  651. MWLog(@"Removed page at index %lu", (unsigned long)pageIndex);
  652. }
  653. }
  654. [_visiblePages minusSet:_recycledPages];
  655. while (_recycledPages.count > 2) // Only keep 2 recycled pages
  656. [_recycledPages removeObject:[_recycledPages anyObject]];
  657. // Add missing pages
  658. for (NSUInteger index = (NSUInteger)iFirstIndex; index <= (NSUInteger)iLastIndex; index++) {
  659. if (![self isDisplayingPageForIndex:index]) {
  660. // Add new page
  661. MWZoomingScrollView *page = [self dequeueRecycledPage];
  662. if (!page) {
  663. page = [[MWZoomingScrollView alloc] initWithPhotoBrowser:self];
  664. }
  665. [_visiblePages addObject:page];
  666. [self configurePage:page forIndex:index];
  667. [_pagingScrollView addSubview:page];
  668. MWLog(@"Added page at index %lu", (unsigned long)index);
  669. // Add caption
  670. MWCaptionView *captionView = [self captionViewForPhotoAtIndex:index];
  671. if (captionView) {
  672. captionView.frame = [self frameForCaptionView:captionView atIndex:index];
  673. [_pagingScrollView addSubview:captionView];
  674. page.captionView = captionView;
  675. }
  676. // Add play button if needed
  677. if (page.displayingVideo) {
  678. UIButton *playButton = [UIButton buttonWithType:UIButtonTypeCustom];
  679. [playButton setImage:[UIImage imageForResourcePath:@"PlayButtonOverlayLarge" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] forState:UIControlStateNormal];
  680. [playButton setImage:[UIImage imageForResourcePath:@"PlayButtonOverlayLargeTap" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] forState:UIControlStateHighlighted];
  681. [playButton addTarget:self action:@selector(playButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
  682. [playButton sizeToFit];
  683. playButton.frame = [self frameForPlayButton:playButton atIndex:index];
  684. [_pagingScrollView addSubview:playButton];
  685. page.playButton = playButton;
  686. }
  687. // Add selected button
  688. if (self.displaySelectionButtons) {
  689. UIButton *selectedButton = [UIButton buttonWithType:UIButtonTypeCustom];
  690. [selectedButton setImage:[UIImage imageForResourcePath:@"ImageSelectedOff" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] forState:UIControlStateNormal];
  691. UIImage *selectedOnImage;
  692. if (self.customImageSelectedIconName) {
  693. selectedOnImage = [UIImage imageNamed:self.customImageSelectedIconName];
  694. } else {
  695. selectedOnImage = [UIImage imageForResourcePath:@"ImageSelectedOn" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]];
  696. }
  697. [selectedButton setImage:selectedOnImage forState:UIControlStateSelected];
  698. [selectedButton sizeToFit];
  699. selectedButton.adjustsImageWhenHighlighted = NO;
  700. [selectedButton addTarget:self action:@selector(selectedButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
  701. selectedButton.frame = [self frameForSelectedButton:selectedButton atIndex:index];
  702. [_pagingScrollView addSubview:selectedButton];
  703. page.selectedButton = selectedButton;
  704. selectedButton.selected = [self photoIsSelectedAtIndex:index];
  705. }
  706. }
  707. }
  708. }
  709. - (void)updateVisiblePageStates {
  710. NSSet *copy = [_visiblePages copy];
  711. for (MWZoomingScrollView *page in copy) {
  712. // Update selection
  713. page.selectedButton.selected = [self photoIsSelectedAtIndex:page.index];
  714. }
  715. }
  716. - (BOOL)isDisplayingPageForIndex:(NSUInteger)index {
  717. for (MWZoomingScrollView *page in _visiblePages)
  718. if (page.index == index) return YES;
  719. return NO;
  720. }
  721. - (MWZoomingScrollView *)pageDisplayedAtIndex:(NSUInteger)index {
  722. MWZoomingScrollView *thePage = nil;
  723. for (MWZoomingScrollView *page in _visiblePages) {
  724. if (page.index == index) {
  725. thePage = page; break;
  726. }
  727. }
  728. return thePage;
  729. }
  730. - (MWZoomingScrollView *)pageDisplayingPhoto:(id<MWPhoto>)photo {
  731. MWZoomingScrollView *thePage = nil;
  732. for (MWZoomingScrollView *page in _visiblePages) {
  733. if (page.photo == photo) {
  734. thePage = page; break;
  735. }
  736. }
  737. return thePage;
  738. }
  739. - (void)configurePage:(MWZoomingScrollView *)page forIndex:(NSUInteger)index {
  740. page.frame = [self frameForPageAtIndex:index];
  741. page.index = index;
  742. page.photo = [self photoAtIndex:index];
  743. }
  744. - (MWZoomingScrollView *)dequeueRecycledPage {
  745. MWZoomingScrollView *page = [_recycledPages anyObject];
  746. if (page) {
  747. [_recycledPages removeObject:page];
  748. }
  749. return page;
  750. }
  751. // Handle page changes
  752. - (void)didStartViewingPageAtIndex:(NSUInteger)index {
  753. // Handle 0 photos
  754. if (![self numberOfPhotos]) {
  755. // Show controls
  756. [self setControlsHidden:NO animated:YES permanent:YES];
  757. return;
  758. }
  759. // Handle video on page change
  760. if (!_rotating || index != _currentVideoIndex) {
  761. [self clearCurrentVideo];
  762. }
  763. // Release images further away than +/-1
  764. NSUInteger i;
  765. if (index > 0) {
  766. // Release anything < index - 1
  767. for (i = 0; i < index-1; i++) {
  768. id photo = [_photos objectAtIndex:i];
  769. if (photo != [NSNull null]) {
  770. [photo unloadUnderlyingImage];
  771. [_photos replaceObjectAtIndex:i withObject:[NSNull null]];
  772. MWLog(@"Released underlying image at index %lu", (unsigned long)i);
  773. }
  774. }
  775. }
  776. if (index < [self numberOfPhotos] - 1) {
  777. // Release anything > index + 1
  778. for (i = index + 2; i < _photos.count; i++) {
  779. id photo = [_photos objectAtIndex:i];
  780. if (photo != [NSNull null]) {
  781. [photo unloadUnderlyingImage];
  782. [_photos replaceObjectAtIndex:i withObject:[NSNull null]];
  783. MWLog(@"Released underlying image at index %lu", (unsigned long)i);
  784. }
  785. }
  786. }
  787. // Load adjacent images if needed and the photo is already
  788. // loaded. Also called after photo has been loaded in background
  789. id <MWPhoto> currentPhoto = [self photoAtIndex:index];
  790. if ([currentPhoto underlyingImage]) {
  791. // photo loaded so load ajacent now
  792. [self loadAdjacentPhotosIfNecessary:currentPhoto];
  793. }
  794. // Notify delegate
  795. if (index != _previousPageIndex) {
  796. if ([_delegate respondsToSelector:@selector(photoBrowser:didDisplayPhotoAtIndex:)])
  797. [_delegate photoBrowser:self didDisplayPhotoAtIndex:index];
  798. _previousPageIndex = index;
  799. } else {
  800. if ([_delegate respondsToSelector:@selector(photoBrowser:didDisplayPhotoAtIndex:)])
  801. [_delegate photoBrowser:self didDisplayPhotoAtIndex:index];
  802. }
  803. // Update nav
  804. [self updateNavigation];
  805. }
  806. #pragma mark - Frame Calculations
  807. - (CGRect)frameForPagingScrollView {
  808. CGRect frame = self.view.bounds;// [[UIScreen mainScreen] bounds];
  809. frame.origin.x -= PADDING;
  810. frame.size.width += (2 * PADDING);
  811. return CGRectIntegral(frame);
  812. }
  813. - (CGRect)frameForPageAtIndex:(NSUInteger)index {
  814. // We have to use our paging scroll view's bounds, not frame, to calculate the page placement. When the device is in
  815. // landscape orientation, the frame will still be in portrait because the pagingScrollView is the root view controller's
  816. // view, so its frame is in window coordinate space, which is never rotated. Its bounds, however, will be in landscape
  817. // because it has a rotation transform applied.
  818. CGRect bounds = _pagingScrollView.bounds;
  819. CGRect pageFrame = bounds;
  820. pageFrame.size.width -= (2 * PADDING);
  821. pageFrame.origin.x = (bounds.size.width * index) + PADDING;
  822. return CGRectIntegral(pageFrame);
  823. }
  824. - (CGSize)contentSizeForPagingScrollView {
  825. // We have to use the paging scroll view's bounds to calculate the contentSize, for the same reason outlined above.
  826. CGRect bounds = _pagingScrollView.bounds;
  827. return CGSizeMake(bounds.size.width * [self numberOfPhotos], bounds.size.height);
  828. }
  829. - (CGPoint)contentOffsetForPageAtIndex:(NSUInteger)index {
  830. CGFloat pageWidth = _pagingScrollView.bounds.size.width;
  831. CGFloat newOffset = index * pageWidth;
  832. return CGPointMake(newOffset, 0);
  833. }
  834. - (CGRect)frameForToolbarAtOrientation:(UIInterfaceOrientation)orientation {
  835. CGFloat height = 49; //TWS
  836. if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone &&
  837. UIInterfaceOrientationIsLandscape(orientation)) height = 49; //32
  838. return CGRectIntegral(CGRectMake(0, self.view.bounds.size.height - height, self.view.bounds.size.width, height));
  839. }
  840. - (CGRect)frameForCaptionView:(MWCaptionView *)captionView atIndex:(NSUInteger)index {
  841. CGRect pageFrame = [self frameForPageAtIndex:index];
  842. CGSize captionSize = [captionView sizeThatFits:CGSizeMake(pageFrame.size.width, 0)];
  843. CGRect captionFrame = CGRectMake(pageFrame.origin.x,
  844. pageFrame.size.height - captionSize.height - (_toolbar.superview?_toolbar.frame.size.height:0),
  845. pageFrame.size.width,
  846. captionSize.height);
  847. return CGRectIntegral(captionFrame);
  848. }
  849. - (CGRect)frameForSelectedButton:(UIButton *)selectedButton atIndex:(NSUInteger)index {
  850. CGRect pageFrame = [self frameForPageAtIndex:index];
  851. CGFloat padding = 20;
  852. CGFloat yOffset = 0;
  853. if (![self areControlsHidden]) {
  854. UINavigationBar *navBar = self.navigationController.navigationBar;
  855. yOffset = navBar.frame.origin.y + navBar.frame.size.height;
  856. }
  857. CGRect selectedButtonFrame = CGRectMake(pageFrame.origin.x + pageFrame.size.width - selectedButton.frame.size.width - padding,
  858. padding + yOffset,
  859. selectedButton.frame.size.width,
  860. selectedButton.frame.size.height);
  861. return CGRectIntegral(selectedButtonFrame);
  862. }
  863. - (CGRect)frameForPlayButton:(UIButton *)playButton atIndex:(NSUInteger)index {
  864. CGRect pageFrame = [self frameForPageAtIndex:index];
  865. return CGRectMake(floorf(CGRectGetMidX(pageFrame) - playButton.frame.size.width / 2),
  866. floorf(CGRectGetMidY(pageFrame) - playButton.frame.size.height / 2),
  867. playButton.frame.size.width,
  868. playButton.frame.size.height);
  869. }
  870. #pragma mark - UIScrollView Delegate
  871. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  872. // Checks
  873. if (!_viewIsActive || _performingLayout || _rotating) return;
  874. // Tile pages
  875. [self tilePages];
  876. // Calculate current page
  877. CGRect visibleBounds = _pagingScrollView.bounds;
  878. NSInteger index = (NSInteger)(floorf(CGRectGetMidX(visibleBounds) / CGRectGetWidth(visibleBounds)));
  879. if (index < 0) index = 0;
  880. if (index > [self numberOfPhotos] - 1) index = [self numberOfPhotos] - 1;
  881. NSUInteger previousCurrentPage = _currentPageIndex;
  882. _currentPageIndex = index;
  883. if (_currentPageIndex != previousCurrentPage) {
  884. [self didStartViewingPageAtIndex:index];
  885. }
  886. }
  887. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  888. // Hide controls when dragging begins
  889. [self setControlsHidden:YES animated:YES permanent:NO];
  890. }
  891. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  892. // Update nav when page changes
  893. [self updateNavigation];
  894. }
  895. #pragma mark - Navigation
  896. - (void)updateNavigation {
  897. // Title
  898. NSUInteger numberOfPhotos = [self numberOfPhotos];
  899. if (_gridController) {
  900. if (_gridController.selectionMode) {
  901. self.title = NSLocalizedString(@"Select Photos", nil);
  902. } else {
  903. NSString *photosText;
  904. if (numberOfPhotos == 1) {
  905. photosText = NSLocalizedString(@"photo", @"Used in the context: '1 photo'");
  906. } else {
  907. photosText = NSLocalizedString(@"photos", @"Used in the context: '3 photos'");
  908. }
  909. self.title = [NSString stringWithFormat:@"%lu %@", (unsigned long)numberOfPhotos, photosText];
  910. }
  911. } else if (numberOfPhotos > 1) {
  912. if ([_delegate respondsToSelector:@selector(photoBrowser:titleForPhotoAtIndex:)]) {
  913. self.title = [_delegate photoBrowser:self titleForPhotoAtIndex:_currentPageIndex];
  914. } else {
  915. self.title = [NSString stringWithFormat:@"%lu %@ %lu", (unsigned long)(_currentPageIndex+1), NSLocalizedString(@"of", @"Used in the context: 'Showing 1 of 3 items'"), (unsigned long)numberOfPhotos];
  916. }
  917. } else {
  918. self.title = nil;
  919. }
  920. // Buttons
  921. _previousButton.enabled = (_currentPageIndex > 0);
  922. _nextButton.enabled = (_currentPageIndex < numberOfPhotos - 1);
  923. //TWS Disable action button if there is no image or it's a video
  924. /*
  925. MWPhoto *photo = [self photoAtIndex:_currentPageIndex];
  926. if ([photo underlyingImage] == nil || ([photo respondsToSelector:@selector(isVideo)] && photo.isVideo)) {
  927. _actionButton.enabled = NO;
  928. _actionButton.tintColor = [UIColor clearColor]; // Tint to hide button
  929. } else {
  930. _actionButton.enabled = YES;
  931. _actionButton.tintColor = nil;
  932. }
  933. */
  934. }
  935. - (void)jumpToPageAtIndex:(NSUInteger)index animated:(BOOL)animated {
  936. // Change page
  937. if (index < [self numberOfPhotos]) {
  938. CGRect pageFrame = [self frameForPageAtIndex:index];
  939. [_pagingScrollView setContentOffset:CGPointMake(pageFrame.origin.x - PADDING, 0) animated:animated];
  940. [self updateNavigation];
  941. }
  942. // Update timer to give more time
  943. [self hideControlsAfterDelay];
  944. }
  945. - (void)gotoPreviousPage {
  946. [self showPreviousPhotoAnimated:NO];
  947. }
  948. - (void)gotoNextPage {
  949. [self showNextPhotoAnimated:NO];
  950. }
  951. - (void)showPreviousPhotoAnimated:(BOOL)animated {
  952. [self jumpToPageAtIndex:_currentPageIndex-1 animated:animated];
  953. }
  954. - (void)showNextPhotoAnimated:(BOOL)animated {
  955. [self jumpToPageAtIndex:_currentPageIndex+1 animated:animated];
  956. }
  957. #pragma mark - Interactions
  958. - (void)selectedButtonTapped:(id)sender {
  959. UIButton *selectedButton = (UIButton *)sender;
  960. selectedButton.selected = !selectedButton.selected;
  961. NSUInteger index = NSUIntegerMax;
  962. for (MWZoomingScrollView *page in _visiblePages) {
  963. if (page.selectedButton == selectedButton) {
  964. index = page.index;
  965. break;
  966. }
  967. }
  968. if (index != NSUIntegerMax) {
  969. [self setPhotoSelected:selectedButton.selected atIndex:index];
  970. }
  971. }
  972. - (void)playButtonTapped:(id)sender {
  973. UIButton *playButton = (UIButton *)sender;
  974. NSUInteger index = NSUIntegerMax;
  975. for (MWZoomingScrollView *page in _visiblePages) {
  976. if (page.playButton == playButton) {
  977. index = page.index;
  978. break;
  979. }
  980. }
  981. if (index != NSUIntegerMax) {
  982. if (!_currentVideoPlayerViewController) {
  983. [self playVideoAtIndex:index];
  984. }
  985. }
  986. }
  987. #pragma mark - Video
  988. - (void)playVideoAtIndex:(NSUInteger)index {
  989. id photo = [self photoAtIndex:index];
  990. if ([photo respondsToSelector:@selector(getVideoURL:)]) {
  991. // Valid for playing
  992. _currentVideoIndex = index;
  993. [self clearCurrentVideo];
  994. [self setVideoLoadingIndicatorVisible:YES atPageIndex:index];
  995. // Get video and play
  996. [photo getVideoURL:^(NSURL *url) {
  997. if (url) {
  998. dispatch_async(dispatch_get_main_queue(), ^{
  999. [self _playVideo:url atPhotoIndex:index];
  1000. });
  1001. } else {
  1002. [self setVideoLoadingIndicatorVisible:NO atPageIndex:index];
  1003. }
  1004. }];
  1005. }
  1006. }
  1007. - (void)_playVideo:(NSURL *)videoURL atPhotoIndex:(NSUInteger)index {
  1008. // Setup player
  1009. _currentVideoPlayerViewController = [[MPMoviePlayerViewController alloc] initWithContentURL:videoURL];
  1010. [_currentVideoPlayerViewController.moviePlayer prepareToPlay];
  1011. _currentVideoPlayerViewController.moviePlayer.shouldAutoplay = YES;
  1012. _currentVideoPlayerViewController.moviePlayer.scalingMode = MPMovieScalingModeAspectFit;
  1013. _currentVideoPlayerViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
  1014. // Remove the movie player view controller from the "playback did finish" notification observers
  1015. // Observe ourselves so we can get it to use the crossfade transition
  1016. [[NSNotificationCenter defaultCenter] removeObserver:_currentVideoPlayerViewController
  1017. name:MPMoviePlayerPlaybackDidFinishNotification
  1018. object:_currentVideoPlayerViewController.moviePlayer];
  1019. [[NSNotificationCenter defaultCenter] addObserver:self
  1020. selector:@selector(videoFinishedCallback:)
  1021. name:MPMoviePlayerPlaybackDidFinishNotification
  1022. object:_currentVideoPlayerViewController.moviePlayer];
  1023. // Show
  1024. [self presentViewController:_currentVideoPlayerViewController animated:YES completion:nil];
  1025. }
  1026. - (void)videoFinishedCallback:(NSNotification*)notification {
  1027. // Remove observer
  1028. [[NSNotificationCenter defaultCenter] removeObserver:self
  1029. name:MPMoviePlayerPlaybackDidFinishNotification
  1030. object:_currentVideoPlayerViewController.moviePlayer];
  1031. // Clear up
  1032. [self clearCurrentVideo];
  1033. //TWS
  1034. [[NSNotificationCenter defaultCenter] postNotificationName:@"closePhotoBrowser" object:nil];
  1035. // Dismiss
  1036. BOOL error = [[[notification userInfo] objectForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] intValue] == MPMovieFinishReasonPlaybackError;
  1037. if (error) {
  1038. // Error occured so dismiss with a delay incase error was immediate and we need to wait to dismiss the VC
  1039. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  1040. [self dismissViewControllerAnimated:YES completion:nil];
  1041. });
  1042. } else {
  1043. [self dismissViewControllerAnimated:YES completion:nil];
  1044. }
  1045. }
  1046. - (void)clearCurrentVideo {
  1047. if (!_currentVideoPlayerViewController) return;
  1048. [_currentVideoLoadingIndicator removeFromSuperview];
  1049. _currentVideoPlayerViewController = nil;
  1050. _currentVideoLoadingIndicator = nil;
  1051. _currentVideoIndex = NSUIntegerMax;
  1052. }
  1053. - (void)setVideoLoadingIndicatorVisible:(BOOL)visible atPageIndex:(NSUInteger)pageIndex {
  1054. if (_currentVideoLoadingIndicator && !visible) {
  1055. [_currentVideoLoadingIndicator removeFromSuperview];
  1056. _currentVideoLoadingIndicator = nil;
  1057. } else if (!_currentVideoLoadingIndicator && visible) {
  1058. _currentVideoLoadingIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectZero];
  1059. [_currentVideoLoadingIndicator sizeToFit];
  1060. [_currentVideoLoadingIndicator startAnimating];
  1061. [_pagingScrollView addSubview:_currentVideoLoadingIndicator];
  1062. [self positionVideoLoadingIndicator];
  1063. }
  1064. }
  1065. - (void)positionVideoLoadingIndicator {
  1066. if (_currentVideoLoadingIndicator && _currentVideoIndex != NSUIntegerMax) {
  1067. CGRect frame = [self frameForPageAtIndex:_currentVideoIndex];
  1068. _currentVideoLoadingIndicator.center = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
  1069. }
  1070. }
  1071. #pragma mark - Grid
  1072. - (BOOL)isGridController {
  1073. return (_gridController ? true : false);
  1074. }
  1075. - (void)showGridAnimated {
  1076. [self showGrid:YES];
  1077. }
  1078. - (void)showGrid:(BOOL)animated {
  1079. if (_gridController) return;
  1080. // Init grid controller
  1081. _gridController = [[MWGridViewController alloc] init];
  1082. _gridController.initialContentOffset = _currentGridContentOffset;
  1083. _gridController.browser = self;
  1084. _gridController.selectionMode = _displaySelectionButtons;
  1085. _gridController.view.frame = self.view.bounds;
  1086. _gridController.view.frame = CGRectOffset(_gridController.view.frame, 0, (self.startOnGrid ? -1 : 1) * self.view.bounds.size.height);
  1087. // Stop specific layout being triggered
  1088. _skipNextPagingScrollViewPositioning = YES;
  1089. // Add as a child view controller
  1090. [self addChildViewController:_gridController];
  1091. [self.view addSubview:_gridController.view];
  1092. // Perform any adjustments
  1093. [_gridController.view layoutIfNeeded];
  1094. [_gridController adjustOffsetsAsRequired];
  1095. // Hide action button on nav bar if it exists
  1096. if (self.navigationItem.rightBarButtonItem == _actionButton) {
  1097. _gridPreviousRightNavItem = _actionButton;
  1098. [self.navigationItem setRightBarButtonItem:nil animated:YES];
  1099. } else {
  1100. _gridPreviousRightNavItem = nil;
  1101. }
  1102. // Update
  1103. [self updateNavigation];
  1104. [self setControlsHidden:NO animated:YES permanent:YES];
  1105. // Animate grid in and photo scroller out
  1106. [_gridController willMoveToParentViewController:self];
  1107. [UIView animateWithDuration:animated ? 0.3 : 0 animations:^(void) {
  1108. _gridController.view.frame = self.view.bounds;
  1109. CGRect newPagingFrame = [self frameForPagingScrollView];
  1110. newPagingFrame = CGRectOffset(newPagingFrame, 0, (self.startOnGrid ? 1 : -1) * newPagingFrame.size.height);
  1111. _pagingScrollView.frame = newPagingFrame;
  1112. } completion:^(BOOL finished) {
  1113. [_gridController didMoveToParentViewController:self];
  1114. }];
  1115. //TWS
  1116. if ([_delegate respondsToSelector:@selector(gridWillAppear:)]) {
  1117. // Call delegate method and let them dismiss us
  1118. [_delegate gridWillAppear:self];
  1119. }
  1120. }
  1121. - (void)hideGrid {
  1122. if (!_gridController) return;
  1123. // Remember previous content offset
  1124. _currentGridContentOffset = _gridController.collectionView.contentOffset;
  1125. // Restore action button if it was removed
  1126. if (_gridPreviousRightNavItem == _actionButton && _actionButton) {
  1127. [self.navigationItem setRightBarButtonItem:_gridPreviousRightNavItem animated:YES];
  1128. }
  1129. // Position prior to hide animation
  1130. CGRect newPagingFrame = [self frameForPagingScrollView];
  1131. newPagingFrame = CGRectOffset(newPagingFrame, 0, (self.startOnGrid ? 1 : -1) * newPagingFrame.size.height);
  1132. _pagingScrollView.frame = newPagingFrame;
  1133. // Remember and remove controller now so things can detect a nil grid controller
  1134. MWGridViewController *tmpGridController = _gridController;
  1135. _gridController = nil;
  1136. // Update
  1137. [self updateNavigation];
  1138. [self updateVisiblePageStates];
  1139. // Animate, hide grid and show paging scroll view
  1140. [UIView animateWithDuration:0.3 animations:^{
  1141. tmpGridController.view.frame = CGRectOffset(self.view.bounds, 0, (self.startOnGrid ? -1 : 1) * self.view.bounds.size.height);
  1142. _pagingScrollView.frame = [self frameForPagingScrollView];
  1143. } completion:^(BOOL finished) {
  1144. [tmpGridController willMoveToParentViewController:nil];
  1145. [tmpGridController.view removeFromSuperview];
  1146. [tmpGridController removeFromParentViewController];
  1147. [self setControlsHidden:NO animated:YES permanent:NO]; // retrigger timer
  1148. }];
  1149. //TWS
  1150. if ([_delegate respondsToSelector:@selector(gridWillDisappear:)]) {
  1151. // Call delegate method and let them dismiss us
  1152. [_delegate gridWillDisappear:self];
  1153. }
  1154. }
  1155. #pragma mark - Control Hiding / Showing
  1156. // If permanent then we don't set timers to hide again
  1157. // Fades all controls on iOS 5 & 6, and iOS 7 controls slide and fade
  1158. - (void)setControlsHidden:(BOOL)hidden animated:(BOOL)animated permanent:(BOOL)permanent {
  1159. // Force visible
  1160. if (![self numberOfPhotos] || _gridController || _alwaysShowControls)
  1161. hidden = NO;
  1162. // Cancel any timers
  1163. [self cancelControlHiding];
  1164. // Animations & positions
  1165. CGFloat animatonOffset = 20;
  1166. CGFloat animationDuration = (animated ? 0.35 : 0);
  1167. // Status bar
  1168. if (!_leaveStatusBarAlone) {
  1169. // Hide status bar
  1170. if (!_isVCBasedStatusBarAppearance) {
  1171. //TWS Non-view controller based
  1172. //[[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:animated ? UIStatusBarAnimationSlide : UIStatusBarAnimationNone];
  1173. } else {
  1174. // View controller based so animate away
  1175. _statusBarShouldBeHidden = hidden;
  1176. //TWS
  1177. //[UIView animateWithDuration:animationDuration animations:^(void) {
  1178. // [self setNeedsStatusBarAppearanceUpdate];
  1179. //} completion:^(BOOL finished) {}];
  1180. }
  1181. }
  1182. // Toolbar, nav bar and captions
  1183. // Pre-appear animation positions for sliding
  1184. if ([self areControlsHidden] && !hidden && animated) {
  1185. // Toolbar
  1186. _toolbar.frame = CGRectOffset([self frameForToolbarAtOrientation:[[UIApplication sharedApplication] statusBarOrientation]], 0, animatonOffset);
  1187. // Captions
  1188. for (MWZoomingScrollView *page in _visiblePages) {
  1189. if (page.captionView) {
  1190. MWCaptionView *v = page.captionView;
  1191. // TWS
  1192. id <MWPhoto> photo = [self photoAtIndex:self.currentIndex];
  1193. if (photo.caption) {
  1194. if ([photo caption]) v.label.text = photo.caption;
  1195. }
  1196. // Pass any index, all we're interested in is the Y
  1197. CGRect captionFrame = [self frameForCaptionView:v atIndex:0];
  1198. captionFrame.origin.x = v.frame.origin.x; // Reset X
  1199. v.frame = CGRectOffset(captionFrame, 0, animatonOffset);
  1200. }
  1201. }
  1202. }
  1203. if ([_delegate respondsToSelector:@selector(setControlsHidden:animated:permanent:)]) {
  1204. [_delegate setControlsHidden:hidden animated:animated permanent:permanent];
  1205. }
  1206. [UIView animateWithDuration:animationDuration animations:^(void) {
  1207. CGFloat alpha = hidden ? 0 : 1;
  1208. // Nav bar slides up on it's own on iOS 7+
  1209. [self.navigationController.navigationBar setAlpha:alpha];
  1210. // Toolbar
  1211. _toolbar.frame = [self frameForToolbarAtOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
  1212. if (hidden) _toolbar.frame = CGRectOffset(_toolbar.frame, 0, animatonOffset);
  1213. _toolbar.alpha = alpha;
  1214. // Captions
  1215. for (MWZoomingScrollView *page in _visiblePages) {
  1216. if (page.captionView) {
  1217. MWCaptionView *v = page.captionView;
  1218. // Pass any index, all we're interested in is the Y
  1219. CGRect captionFrame = [self frameForCaptionView:v atIndex:0];
  1220. captionFrame.origin.x = v.frame.origin.x; // Reset X
  1221. if (hidden) captionFrame = CGRectOffset(captionFrame, 0, animatonOffset);
  1222. v.frame = captionFrame;
  1223. v.alpha = alpha;
  1224. }
  1225. }
  1226. // Selected buttons
  1227. for (MWZoomingScrollView *page in _visiblePages) {
  1228. if (page.selectedButton) {
  1229. UIButton *v = page.selectedButton;
  1230. CGRect newFrame = [self frameForSelectedButton:v atIndex:0];
  1231. newFrame.origin.x = v.frame.origin.x;
  1232. v.frame = newFrame;
  1233. }
  1234. }
  1235. } completion:^(BOOL finished) {}];
  1236. // Control hiding timer
  1237. // Will cancel existing timer but only begin hiding if
  1238. // they are visible
  1239. if (!permanent) [self hideControlsAfterDelay];
  1240. }
  1241. - (BOOL)prefersStatusBarHidden {
  1242. if (!_leaveStatusBarAlone) {
  1243. return _statusBarShouldBeHidden;
  1244. } else {
  1245. return [self presentingViewControllerPrefersStatusBarHidden];
  1246. }
  1247. }
  1248. - (UIStatusBarStyle)preferredStatusBarStyle {
  1249. return UIStatusBarStyleLightContent;
  1250. }
  1251. - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
  1252. return UIStatusBarAnimationSlide;
  1253. }
  1254. - (void)cancelControlHiding {
  1255. // If a timer exists then cancel and release
  1256. if (_controlVisibilityTimer) {
  1257. [_controlVisibilityTimer invalidate];
  1258. _controlVisibilityTimer = nil;
  1259. }
  1260. }
  1261. // Enable/disable control visiblity timer
  1262. - (void)hideControlsAfterDelay {
  1263. if (![self areControlsHidden]) {
  1264. [self cancelControlHiding];
  1265. _controlVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:self.delayToHideElements target:self selector:@selector(hideControls) userInfo:nil repeats:NO];
  1266. }
  1267. }
  1268. - (BOOL)areControlsHidden {
  1269. return (_toolbar.alpha == 0);
  1270. }
  1271. - (void)hideControls { [self setControlsHidden:YES animated:YES permanent:NO]; }
  1272. - (void)showControls { [self setControlsHidden:NO animated:YES permanent:NO]; }
  1273. - (void)toggleControls { [self setControlsHidden:![self areControlsHidden] animated:YES permanent:NO]; }
  1274. #pragma mark - Properties
  1275. - (void)setCurrentPhotoIndex:(NSUInteger)index {
  1276. // Validate
  1277. NSUInteger photoCount = [self numberOfPhotos];
  1278. if (photoCount == 0) {
  1279. index = 0;
  1280. } else {
  1281. if (index >= photoCount)
  1282. index = [self numberOfPhotos]-1;
  1283. }
  1284. _currentPageIndex = index;
  1285. if ([self isViewLoaded]) {
  1286. [self jumpToPageAtIndex:index animated:NO];
  1287. if (!_viewIsActive)
  1288. [self tilePages]; // Force tiling if view is not visible
  1289. }
  1290. }
  1291. #pragma mark - Misc
  1292. - (void)doneButtonPressed:(id)sender {
  1293. // Only if we're modal and there's a done button
  1294. if (_doneButton) {
  1295. // See if we actually just want to show/hide grid
  1296. if (self.enableGrid) {
  1297. if (self.startOnGrid && !_gridController) {
  1298. [self showGrid:YES];
  1299. return;
  1300. } else if (!self.startOnGrid && _gridController) {
  1301. [self hideGrid];
  1302. return;
  1303. }
  1304. }
  1305. // Dismiss view controller
  1306. if ([_delegate respondsToSelector:@selector(photoBrowserDidFinishModalPresentation:)]) {
  1307. // Call delegate method and let them dismiss us
  1308. [_delegate photoBrowserDidFinishModalPresentation:self];
  1309. } else {
  1310. [self dismissViewControllerAnimated:YES completion:nil];
  1311. }
  1312. }
  1313. }
  1314. #pragma mark - Delete
  1315. - (void)deleteButtonPressed:(id)sender {
  1316. if ([self.delegate respondsToSelector:@selector(photoBrowser:deleteButtonPressedForPhotoAtIndex:deleteButton:)])
  1317. [self.delegate photoBrowser:self deleteButtonPressedForPhotoAtIndex:_currentPageIndex deleteButton:self.deleteButton];
  1318. }
  1319. #pragma mark - Share
  1320. - (void)shareButtonPressed:(id)sender {
  1321. if ([self.delegate respondsToSelector:@selector(photoBrowser:shareButtonPressedForPhotoAtIndex:)])
  1322. [self.delegate photoBrowser:self shareButtonPressedForPhotoAtIndex:_currentPageIndex];
  1323. }
  1324. #pragma mark - Actions
  1325. - (void)actionButtonPressed:(id)sender {
  1326. // Only react when image has loaded
  1327. id <MWPhoto> photo = [self photoAtIndex:_currentPageIndex];
  1328. if ([self numberOfPhotos] > 0 && [photo underlyingImage]) {
  1329. // If they have defined a delegate method then just message them
  1330. if ([self.delegate respondsToSelector:@selector(photoBrowser:actionButtonPressedForPhotoAtIndex:)]) {
  1331. // Let delegate handle things
  1332. [self.delegate photoBrowser:self actionButtonPressedForPhotoAtIndex:_currentPageIndex];
  1333. } else {
  1334. // Show activity view controller
  1335. NSMutableArray *items = [NSMutableArray arrayWithObject:[photo underlyingImage]];
  1336. if (photo.caption) {
  1337. [items addObject:photo.caption];
  1338. }
  1339. self.activityViewController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil];
  1340. // Show
  1341. typeof(self) __weak weakSelf = self;
  1342. [self.activityViewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
  1343. weakSelf.activityViewController = nil;
  1344. [weakSelf hideControlsAfterDelay];
  1345. }];
  1346. // iOS 8 - Set the Anchor Point for the popover
  1347. if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8")) {
  1348. self.activityViewController.popoverPresentationController.barButtonItem = _actionButton;
  1349. }
  1350. [self presentViewController:self.activityViewController animated:YES completion:nil];
  1351. }
  1352. // Keep controls hidden
  1353. [self setControlsHidden:NO animated:YES permanent:YES];
  1354. }
  1355. }
  1356. @end