diff --git a/Classes/MWCaptionView.h b/Classes/MWCaptionView.h new file mode 100644 index 0000000..3b26635 --- /dev/null +++ b/Classes/MWCaptionView.h @@ -0,0 +1,33 @@ +// +// MWCaptionView.h +// MWPhotoBrowser +// +// Created by Michael Waterfall on 30/12/2011. +// Copyright (c) 2011 __MyCompanyName__. All rights reserved. +// + +#import + +@class MWPhoto; + +@interface MWCaptionView : UIView { + MWPhoto *_photo; + UILabel *_label; +} + +// Init +- (id)initWithPhoto:(MWPhoto *)photo; + +// To create your own custom caption view, subclass this view +// and override the following two methods: + +// Override -setupCaption so setup your subviews and customise the appearance +// of your custom caption +- (void)setupCaption; + +// Override -sizeThatFits: and return a CGSize specifying the height of your +// custom caption view. With width property is ignored and the caption is displayed +// the full width of the screen +- (CGSize)sizeThatFits:(CGSize)size; + +@end diff --git a/Classes/MWCaptionView.m b/Classes/MWCaptionView.m new file mode 100644 index 0000000..b4944aa --- /dev/null +++ b/Classes/MWCaptionView.m @@ -0,0 +1,60 @@ +// +// MWCaptionView.m +// MWPhotoBrowser +// +// Created by Michael Waterfall on 30/12/2011. +// Copyright (c) 2011 __MyCompanyName__. All rights reserved. +// + +#import "MWCaptionView.h" +#import "MWPhoto.h" + +static const CGFloat labelPadding = 10; + +@implementation MWCaptionView + +- (id)initWithPhoto:(MWPhoto *)photo { + self = [super initWithFrame:CGRectMake(0, 0, 320, 44)]; // Random initial frame + if (self) { + _photo = [photo retain]; + self.opaque = NO; + self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6]; + self.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleLeftMargin|UIViewAutoresizingFlexibleRightMargin; + [self setupCaption]; + } + return self; +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGFloat maxHeight = 9999; + if (_label.numberOfLines > 0) maxHeight = _label.font.leading*_label.numberOfLines; + CGSize textSize = [_label.text sizeWithFont:_label.font + constrainedToSize:CGSizeMake(size.width - labelPadding*2, maxHeight) + lineBreakMode:_label.lineBreakMode]; + return CGSizeMake(size.width, textSize.height + labelPadding * 2); +} + +- (void)setupCaption { + _label = [[UILabel alloc] initWithFrame:CGRectMake(labelPadding, 0, + self.bounds.size.width-labelPadding*2, + self.bounds.size.height)]; + _label.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + _label.opaque = NO; + _label.text = _photo.caption ? _photo.caption : @"[No Title]"; + _label.backgroundColor = [UIColor clearColor]; + _label.textAlignment = UITextAlignmentCenter; + _label.lineBreakMode = UILineBreakModeWordWrap; + _label.numberOfLines = 3; + _label.textColor = [UIColor whiteColor]; + _label.shadowColor = [UIColor blackColor]; + _label.shadowOffset = CGSizeMake(1, 1); + [self addSubview:_label]; +} + +- (void)dealloc { + [_label release]; + [_photo release]; + [super dealloc]; +} + +@end diff --git a/Classes/MWPhoto.h b/Classes/MWPhoto.h index 4570f57..12f74fc 100644 --- a/Classes/MWPhoto.h +++ b/Classes/MWPhoto.h @@ -26,11 +26,17 @@ // Image UIImage *_underlyingImage; + // Other + NSString *_caption; + // Delegate id _photoLoadingDelegate; } +// Properties +@property (nonatomic, retain) NSString *caption; + // Class + (MWPhoto *)photoWithImage:(UIImage *)image; + (MWPhoto *)photoWithFilePath:(NSString *)path; diff --git a/Classes/MWPhoto.m b/Classes/MWPhoto.m index 3239724..c86f9e2 100644 --- a/Classes/MWPhoto.m +++ b/Classes/MWPhoto.m @@ -21,7 +21,8 @@ // Properties @synthesize underlyingImage = _underlyingImage, -photoLoadingDelegate = _photoLoadingDelegate; +photoLoadingDelegate = _photoLoadingDelegate, +caption = _caption; #pragma mark Class Methods @@ -61,6 +62,7 @@ photoLoadingDelegate = _photoLoadingDelegate; } - (void)dealloc { + [_caption release]; [[SDWebImageManager sharedManager] cancelForDelegate:self]; [_photoPath release]; [_photoURL release]; diff --git a/Classes/MWPhotoBrowser.h b/Classes/MWPhotoBrowser.h index af9b53a..772aa37 100644 --- a/Classes/MWPhotoBrowser.h +++ b/Classes/MWPhotoBrowser.h @@ -8,6 +8,7 @@ #import #import "MWPhoto.h" +#import "MWCaptionView.h" // Debug Logging #if 0 // Set to 1 to enable debug logging @@ -75,4 +76,5 @@ - (NSUInteger)numberOfPhotosInPhotoBrowser:(MWPhotoBrowser *)photoBrowser; - (MWPhoto *)photoBrowser:(MWPhotoBrowser *)photoBrowser photoAtIndex:(NSUInteger)index; @optional +- (MWCaptionView *)photoBrowser:(MWPhotoBrowser *)photoBrowser captionViewForPhotoAtIndex:(NSUInteger)index; @end diff --git a/Classes/MWPhotoBrowser.m b/Classes/MWPhotoBrowser.m index 63e173e..80e617c 100644 --- a/Classes/MWPhotoBrowser.m +++ b/Classes/MWPhotoBrowser.m @@ -46,6 +46,7 @@ - (CGSize)contentSizeForPagingScrollView; - (CGPoint)contentOffsetForPageAtIndex:(NSUInteger)index; - (CGRect)frameForToolbarAtOrientation:(UIInterfaceOrientation)orientation; +- (CGRect)frameForCaptionView:(MWCaptionView *)captionView atIndex:(NSUInteger)index; // Navigation - (void)updateNavigation; @@ -58,6 +59,7 @@ - (void)hideControlsAfterDelay; - (void)setControlsHidden:(BOOL)hidden animated:(BOOL)animated; - (void)toggleControls; +- (BOOL)areControlsHidden; // Data - (NSUInteger)numberOfPhotos; @@ -342,7 +344,9 @@ navigationBarBackgroundImageLandscapePhone = _navigationBarBackgroundImageLandsc // Adjust frames and configuration of each visible page for (MWZoomingScrollView *page in _visiblePages) { - page.frame = [self frameForPageAtIndex:PAGE_INDEX(page)]; + NSUInteger index = PAGE_INDEX(page); + page.frame = [self frameForPageAtIndex:index]; + page.captionView.frame = [self frameForCaptionView:page.captionView atIndex:index]; [page setMaxMinZoomScalesForCurrentBounds]; } @@ -439,6 +443,18 @@ navigationBarBackgroundImageLandscapePhone = _navigationBarBackgroundImageLandsc return photo; } +- (MWCaptionView *)captionViewForPhotoAtIndex:(NSUInteger)index { + MWCaptionView *captionView = nil; + if ([_delegate respondsToSelector:@selector(photoBrowser:captionViewForPhotoAtIndex:)]) { + captionView = [_delegate photoBrowser:self captionViewForPhotoAtIndex:index]; + } else { + MWPhoto *photo = [self photoAtIndex:index]; + if (photo.caption) captionView = [[[MWCaptionView alloc] initWithPhoto:photo] autorelease]; + } + captionView.alpha = [self areControlsHidden] ? 0 : 1; // Initial alpha + return captionView; +} + - (UIImage *)imageForPhoto:(MWPhoto *)photo { if (photo) { // Get image or obtain in background @@ -521,18 +537,20 @@ navigationBarBackgroundImageLandscapePhone = _navigationBarBackgroundImageLandsc pageIndex = PAGE_INDEX(page); if (pageIndex < (NSUInteger)iFirstIndex || pageIndex > (NSUInteger)iLastIndex) { [_recycledPages addObject:page]; - [page prepareForReuse]; // Cleanup - MWLog(@"Removed page at index %i", PAGE_INDEX(page)); + [page prepareForReuse]; [page removeFromSuperview]; + MWLog(@"Removed page at index %i", PAGE_INDEX(page)); } } [_visiblePages minusSet:_recycledPages]; - // Only keep 2 recycled pages - while (_recycledPages.count > 2) [_recycledPages removeObject:[_recycledPages anyObject]]; + while (_recycledPages.count > 2) // Only keep 2 recycled pages + [_recycledPages removeObject:[_recycledPages anyObject]]; // Add missing pages for (NSUInteger index = (NSUInteger)iFirstIndex; index <= (NSUInteger)iLastIndex; index++) { if (![self isDisplayingPageForIndex:index]) { + + // Add new page MWZoomingScrollView *page = [self dequeueRecycledPage]; if (!page) { page = [[[MWZoomingScrollView alloc] initWithPhotoBrowser:self] autorelease]; @@ -540,7 +558,14 @@ navigationBarBackgroundImageLandscapePhone = _navigationBarBackgroundImageLandsc [self configurePage:page forIndex:index]; [_visiblePages addObject:page]; [_pagingScrollView addSubview:page]; - MWLog(@"Added page at index %i", PAGE_INDEX(page)); + MWLog(@"Added page at index %i", index); + + // Add caption + MWCaptionView *captionView = [self captionViewForPhotoAtIndex:index]; + captionView.frame = [self frameForCaptionView:captionView atIndex:index]; + [_pagingScrollView addSubview:captionView]; + page.captionView = captionView; + } } @@ -668,6 +693,14 @@ navigationBarBackgroundImageLandscapePhone = _navigationBarBackgroundImageLandsc return CGRectMake(0, self.view.bounds.size.height - height, self.view.bounds.size.width, height); } +- (CGRect)frameForCaptionView:(MWCaptionView *)captionView atIndex:(NSUInteger)index { + CGRect pageFrame = [self frameForPageAtIndex:index]; + captionView.frame = CGRectMake(0, 0, pageFrame.size.width, 44); // set initial frame + CGSize captionSize = [captionView sizeThatFits:CGSizeMake(pageFrame.size.width, 0)]; + CGRect captionFrame = CGRectMake(pageFrame.origin.x, pageFrame.size.height - captionSize.height - (_toolbar.superview?_toolbar.frame.size.height:0), pageFrame.size.width, captionSize.height); + return captionFrame; +} + #pragma mark - UIScrollView Delegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { @@ -766,14 +799,22 @@ navigationBarBackgroundImageLandscapePhone = _navigationBarBackgroundImageLandsc CGRect navBarFrame = self.navigationController.navigationBar.frame; navBarFrame.origin.y = statusBarHeight; self.navigationController.navigationBar.frame = navBarFrame; + + // Captions + NSMutableSet *captionViews = [[[NSMutableSet alloc] initWithCapacity:_visiblePages.count] autorelease]; + for (MWZoomingScrollView *page in _visiblePages) { + if (page.captionView) [captionViews addObject:page.captionView]; + } - // Bars + // Animate if (animated) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.35]; } - [self.navigationController.navigationBar setAlpha:hidden ? 0 : 1]; - [_toolbar setAlpha:hidden ? 0 : 1]; + CGFloat alpha = hidden ? 0 : 1; + [self.navigationController.navigationBar setAlpha:alpha]; + [_toolbar setAlpha:alpha]; + for (UIView *v in captionViews) v.alpha = alpha; if (animated) [UIView commitAnimations]; // Control hiding timer @@ -794,14 +835,15 @@ navigationBarBackgroundImageLandscapePhone = _navigationBarBackgroundImageLandsc // Enable/disable control visiblity timer - (void)hideControlsAfterDelay { - if (!_disappearing && ![UIApplication sharedApplication].isStatusBarHidden) { + if (!_disappearing && ![self areControlsHidden]) { [self cancelControlHiding]; _controlVisibilityTimer = [[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(hideControls) userInfo:nil repeats:NO] retain]; } } +- (BOOL)areControlsHidden { return [UIApplication sharedApplication].isStatusBarHidden; } - (void)hideControls { [self setControlsHidden:YES animated:YES]; } -- (void)toggleControls { [self setControlsHidden:![UIApplication sharedApplication].isStatusBarHidden animated:YES]; } +- (void)toggleControls { [self setControlsHidden:![self areControlsHidden] animated:YES]; } #pragma mark - Properties diff --git a/Classes/MWZoomingScrollView.h b/Classes/MWZoomingScrollView.h index f4f378f..4881c9c 100644 --- a/Classes/MWZoomingScrollView.h +++ b/Classes/MWZoomingScrollView.h @@ -10,19 +10,24 @@ #import "UIImageViewTap.h" #import "UIViewTap.h" -@class MWPhotoBrowser, MWPhoto; +@class MWPhotoBrowser, MWPhoto, MWCaptionView; @interface MWZoomingScrollView : UIScrollView { MWPhotoBrowser *_photoBrowser; MWPhoto *_photo; + // This view references the related caption view for simplified + // handling in photo browser + MWCaptionView *_captionView; + UIViewTap *_tapView; // for background taps UIImageViewTap *_photoImageView; UIActivityIndicatorView *_spinner; } +@property (nonatomic, retain) MWCaptionView *captionView; @property (nonatomic, retain) MWPhoto *photo; - (id)initWithPhotoBrowser:(MWPhotoBrowser *)browser; diff --git a/Classes/MWZoomingScrollView.m b/Classes/MWZoomingScrollView.m index 1e3ef27..84de491 100644 --- a/Classes/MWZoomingScrollView.m +++ b/Classes/MWZoomingScrollView.m @@ -26,7 +26,7 @@ @implementation MWZoomingScrollView -@synthesize photoBrowser = _photoBrowser, photo = _photo; +@synthesize photoBrowser = _photoBrowser, photo = _photo, captionView = _captionView; - (id)initWithPhotoBrowser:(MWPhotoBrowser *)browser { if ((self = [super init])) { @@ -87,6 +87,8 @@ - (void)prepareForReuse { [_photo releasePhoto]; self.photo = nil; + [_captionView removeFromSuperview]; + self.captionView = nil; } #pragma mark - Image diff --git a/Classes/Menu.m b/Classes/Menu.m index 7ca45dd..ad6a64d 100644 --- a/Classes/Menu.m +++ b/Classes/Menu.m @@ -87,16 +87,28 @@ // Browser NSMutableArray *photos = [[NSMutableArray alloc] init]; + MWPhoto *photo; switch (indexPath.row) { case 0: - [photos addObject:[MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo2l" ofType:@"jpg"]]]; + photo = [MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo2l" ofType:@"jpg"]]; + photo.caption = @"The London Eye is a giant Ferris wheel situated on the banks of the River Thames, in London, England."; + [photos addObject:photo]; break; - case 1: - [photos addObject:[MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo1l" ofType:@"jpg"]]]; - [photos addObject:[MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo2l" ofType:@"jpg"]]]; - [photos addObject:[MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo3l" ofType:@"jpg"]]]; - [photos addObject:[MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo4l" ofType:@"jpg"]]]; + case 1: { + photo = [MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo1l" ofType:@"jpg"]]; + photo.caption = @"Grotto of the Madonna"; + [photos addObject:photo]; + photo = [MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo2l" ofType:@"jpg"]]; + photo.caption = @"The London Eye is a giant Ferris wheel situated on the banks of the River Thames, in London, England."; + [photos addObject:photo]; + photo = [MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo3l" ofType:@"jpg"]]; + photo.caption = @"York Floods"; + [photos addObject:photo]; + photo = [MWPhoto photoWithFilePath:[[NSBundle mainBundle] pathForResource:@"photo4l" ofType:@"jpg"]]; + photo.caption = @"Campervan"; + [photos addObject:photo]; break; + } case 2: [photos addObject:[MWPhoto photoWithURL:[NSURL URLWithString:@"http://farm4.static.flickr.com/3567/3523321514_371d9ac42f.jpg"]]]; [photos addObject:[MWPhoto photoWithURL:[NSURL URLWithString:@"http://farm4.static.flickr.com/3629/3339128908_7aecabc34b.jpg"]]]; @@ -146,5 +158,12 @@ return nil; } +//- (MWCaptionView *)photoBrowser:(MWPhotoBrowser *)photoBrowser captionViewForPhotoAtIndex:(NSUInteger)index { +// MWCaptionView *captionView = nil; +// MWPhoto *photo = [_photos objectAtIndex:index]; +// captionView = [[[MWCaptionView alloc] initWithPhoto:photo] autorelease]; +// return captionView; +//} + @end diff --git a/MWPhotoBrowser.xcodeproj/project.pbxproj b/MWPhotoBrowser.xcodeproj/project.pbxproj index c1ac128..9daeb77 100755 --- a/MWPhotoBrowser.xcodeproj/project.pbxproj +++ b/MWPhotoBrowser.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 4C15BF4914ACDD64004F9CA6 /* SDWebImagePrefetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C15BF4014ACDD64004F9CA6 /* SDWebImagePrefetcher.m */; }; 4C15BF4A14ACDD64004F9CA6 /* UIButton+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C15BF4214ACDD64004F9CA6 /* UIButton+WebCache.m */; }; 4C15BF4B14ACDD64004F9CA6 /* UIImageView+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C15BF4414ACDD64004F9CA6 /* UIImageView+WebCache.m */; }; + 4C15BF5214AE300F004F9CA6 /* MWCaptionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C15BF5114AE300F004F9CA6 /* MWCaptionView.m */; }; 4C4156C61277301E00C96767 /* MainWindow-iPad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C4156C51277301E00C96767 /* MainWindow-iPad.xib */; }; 4C48EFD2126F90B100F85546 /* UIViewTap.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C48EFD1126F90B100F85546 /* UIViewTap.m */; }; 4C667B2D1270AFCE0008B630 /* Menu.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C667B2C1270AFCE0008B630 /* Menu.m */; }; @@ -71,6 +72,8 @@ 4C15BF4214ACDD64004F9CA6 /* UIButton+WebCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIButton+WebCache.m"; path = "SDWebImage/UIButton+WebCache.m"; sourceTree = SOURCE_ROOT; }; 4C15BF4314ACDD64004F9CA6 /* UIImageView+WebCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImageView+WebCache.h"; path = "SDWebImage/UIImageView+WebCache.h"; sourceTree = SOURCE_ROOT; }; 4C15BF4414ACDD64004F9CA6 /* UIImageView+WebCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImageView+WebCache.m"; path = "SDWebImage/UIImageView+WebCache.m"; sourceTree = SOURCE_ROOT; }; + 4C15BF5014AE300F004F9CA6 /* MWCaptionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWCaptionView.h; sourceTree = ""; }; + 4C15BF5114AE300F004F9CA6 /* MWCaptionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MWCaptionView.m; sourceTree = ""; }; 4C4156C51277301E00C96767 /* MainWindow-iPad.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "MainWindow-iPad.xib"; path = "Resources-iPad/MainWindow-iPad.xib"; sourceTree = ""; }; 4C48EFD0126F90B100F85546 /* UIViewTap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIViewTap.h; sourceTree = ""; }; 4C48EFD1126F90B100F85546 /* UIViewTap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIViewTap.m; sourceTree = ""; }; @@ -237,6 +240,8 @@ 4C75D46E126B659A004D0ECF /* MWPhoto.m */, 4CC82BE41266804500C05633 /* MWZoomingScrollView.h */, 4CC82BE51266804500C05633 /* MWZoomingScrollView.m */, + 4C15BF5014AE300F004F9CA6 /* MWCaptionView.h */, + 4C15BF5114AE300F004F9CA6 /* MWCaptionView.m */, ); name = MWPhotoBrowser; sourceTree = ""; @@ -343,6 +348,7 @@ 4C15BF4914ACDD64004F9CA6 /* SDWebImagePrefetcher.m in Sources */, 4C15BF4A14ACDD64004F9CA6 /* UIButton+WebCache.m in Sources */, 4C15BF4B14ACDD64004F9CA6 /* UIImageView+WebCache.m in Sources */, + 4C15BF5214AE300F004F9CA6 /* MWCaptionView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/README.markdown b/README.markdown index 600e5da..867ff11 100644 --- a/README.markdown +++ b/README.markdown @@ -39,10 +39,11 @@ If desired, you can choose which photo is displayed first: ## Outstanding issues and improvements -- Photo captions. +- Action button +- Modify README to describe new features. +- Test with larger images 3-4MB and check performance. - Check memory usage and for leaks. - Create static library which can be added to projects easier. -- Test with larger images 3-4MB and check performance. ## Licence