diff --git a/CHANGELOG.md b/CHANGELOG.md index b586e812..17bf98fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ Changes for users of the library currently on `develop`: -_This space intentionally left blank._ +- Expose a data-source-oriented API for PhotosViewController ([#226](https://github.com/NYTimes/NYTPhotoViewer/pull/226)) + - A data source no longer has to handle out-of-bounds indexes ([#227](https://github.com/NYTimes/NYTPhotoViewer/pull/227)) + - The data source is not retained ([#227](https://github.com/NYTimes/NYTPhotoViewer/pull/227)) +- Respect safe areas for iOS 11 support ## [1.2.0](https://github.com/NYTimes/NYTPhotoViewer/releases/tag/1.2.0) diff --git a/Example-Swift/AppDelegate.swift b/Example-Swift/AppDelegate.swift index 8be6e6aa..ee78a3f5 100755 --- a/Example-Swift/AppDelegate.swift +++ b/Example-Swift/AppDelegate.swift @@ -9,12 +9,11 @@ import UIKit @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { +final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { return true } } - diff --git a/Example-Swift/ExamplePhoto.swift b/Example-Swift/ExamplePhoto.swift deleted file mode 100755 index 6df2b0cb..00000000 --- a/Example-Swift/ExamplePhoto.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ExamplePhoto.swift -// NYTPhotoViewer -// -// Created by Mark Keefe on 3/20/15. -// Copyright (c) 2015 The New York Times. All rights reserved. -// - -import UIKit -import NYTPhotoViewer - -class ExamplePhoto: NSObject, NYTPhoto { - - var image: UIImage? - var imageData: NSData? - var placeholderImage: UIImage? - let attributedCaptionTitle: NSAttributedString? - let attributedCaptionSummary: NSAttributedString? = NSAttributedString(string: "summary string", attributes: [NSForegroundColorAttributeName: UIColor.grayColor()]) - let attributedCaptionCredit: NSAttributedString? = NSAttributedString(string: "credit", attributes: [NSForegroundColorAttributeName: UIColor.darkGrayColor()]) - - init(image: UIImage? = nil, imageData: NSData? = nil, attributedCaptionTitle: NSAttributedString) { - self.image = image - self.imageData = imageData - self.attributedCaptionTitle = attributedCaptionTitle - super.init() - } - -} diff --git a/Example-Swift/Photo.swift b/Example-Swift/Photo.swift new file mode 100644 index 00000000..630b1025 --- /dev/null +++ b/Example-Swift/Photo.swift @@ -0,0 +1,19 @@ +// +// Photo.swift +// NYTPhotoViewer +// +// Created by Chris Dzombak on 2/2/17. +// Copyright © 2017 NYTimes. All rights reserved. +// + +import UIKit + +/// A photo may often be represented in a Swift application as a value type. +struct Photo { + // This would usually be a URL, but for this demo we load images from the bundle. + let name: String + let summary: String + let credit: String + + let identifier: Int +} diff --git a/Example-Swift/PhotoBox.swift b/Example-Swift/PhotoBox.swift new file mode 100644 index 00000000..48c9d46c --- /dev/null +++ b/Example-Swift/PhotoBox.swift @@ -0,0 +1,50 @@ +// +// PhotoBox.swift +// NYTPhotoViewer +// +// Created by Chris Dzombak on 2/2/17. +// Copyright © 2017 NYTimes. All rights reserved. +// + +import UIKit +import NYTPhotoViewer + +/// A box allowing NYTPhotoViewer to consume Swift value types from our codebase. +final class NYTPhotoBox: NSObject, NYTPhoto { + + let value: Photo + + init(_ photo: Photo) { + value = photo + } + + // MARK: NYTPhoto + + var image: UIImage? + var imageData: Data? + var placeholderImage: UIImage? + + var attributedCaptionTitle: NSAttributedString? + + var attributedCaptionSummary: NSAttributedString? { + let attributes = [NSForegroundColorAttributeName: UIColor.white, + NSFontAttributeName: UIFont.preferredFont(forTextStyle: .body)] + return NSAttributedString(string: value.summary, attributes: attributes) + } + + var attributedCaptionCredit: NSAttributedString? { + let attributes = [NSForegroundColorAttributeName: UIColor.gray, + NSFontAttributeName: UIFont.preferredFont(forTextStyle: .footnote)] + return NSAttributedString(string: value.credit, attributes: attributes) + } +} + +// MARK: NSObject Equality + +extension NYTPhotoBox { + @objc + override func isEqual(_ object: Any?) -> Bool { + guard let otherPhoto = object as? NYTPhotoBox else { return false } + return value.identifier == otherPhoto.value.identifier + } +} diff --git a/Example-Swift/PhotoViewerCoordinator.swift b/Example-Swift/PhotoViewerCoordinator.swift new file mode 100644 index 00000000..ae25b91a --- /dev/null +++ b/Example-Swift/PhotoViewerCoordinator.swift @@ -0,0 +1,53 @@ +// +// PhotoViewerCoordinator.swift +// NYTPhotoViewer +// +// Created by Chris Dzombak on 2/2/17. +// Copyright © 2017 NYTimes. All rights reserved. +// + +import NYTPhotoViewer + +/// Coordinates interaction between the application's data layer and the photo viewer component. +final class PhotoViewerCoordinator: NYTPhotoViewerDataSource { + let slideshow: [NYTPhotoBox] + let provider: PhotosProvider + + lazy var photoViewer: NYTPhotosViewController = { + return NYTPhotosViewController(dataSource: self) + }() + + init(provider: PhotosProvider) { + self.provider = provider + self.slideshow = provider.fetchDemoSlideshow().map { NYTPhotoBox($0) } + self.fetchPhotos() + } + + func fetchPhotos() { + for box in slideshow { + provider.fetchPhoto(named: box.value.name, then: { [weak self] (result) in + box.image = result + self?.photoViewer.update(box) + }) + } + } + + // MARK: NYTPhotoViewerDataSource + + @objc + var numberOfPhotos: NSNumber? { + return NSNumber(integerLiteral: slideshow.count) + } + + @objc + func index(of photo: NYTPhoto) -> Int { + guard let box = photo as? NYTPhotoBox else { return NSNotFound } + return slideshow.index(where: { $0.value.identifier == box.value.identifier }) ?? NSNotFound + } + + @objc + func photo(at index: Int) -> NYTPhoto? { + guard index < slideshow.count else { return nil } + return slideshow[index] + } +} diff --git a/Example-Swift/PhotosProvider.swift b/Example-Swift/PhotosProvider.swift index 23488004..e5dc437b 100755 --- a/Example-Swift/PhotosProvider.swift +++ b/Example-Swift/PhotosProvider.swift @@ -7,39 +7,43 @@ // import UIKit -import NYTPhotoViewer -/** -* In Swift 1.2, the following file level constants can be moved inside the class for better encapsulation -*/ -let CustomEverythingPhotoIndex = 1, DefaultLoadingSpinnerPhotoIndex = 3, NoReferenceViewPhotoIndex = 4 -let PrimaryImageName = "NYTimesBuilding" -let PlaceholderImageName = "NYTimesBuildingPlaceholder" +/// A component of your data layer, which might load photos from the cache or network. +final class PhotosProvider { + typealias Slideshow = [Photo] -class PhotosProvider: NSObject { + /// Simulate a synchronous fetch of a slideshow, perhaps from a local database. + func fetchDemoSlideshow() -> Slideshow { + return (0...4).map { demoPhoto(identifier: $0) } + } - let photos: [ExamplePhoto] = { - - var mutablePhotos: [ExamplePhoto] = [] - var image = UIImage(named: PrimaryImageName) - let NumberOfPhotos = 5 - - func shouldSetImageOnIndex(photoIndex: Int) -> Bool { - return photoIndex != CustomEverythingPhotoIndex && photoIndex != DefaultLoadingSpinnerPhotoIndex + /// Simulate fetching a photo from the network. + /// For simplicity in this demo, errors are not simulated, and the callback is invoked on the main queue. + func fetchPhoto(named name: String, then completionHandler: @escaping (UIImage) -> Void) { + let delay = Double(arc4random_uniform(3) + 2) + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + guard let result = UIImage(named: name) else { fatalError("Image '\(name)' could not be loaded from the bundle.") } + completionHandler(result) } - - for photoIndex in 0 ..< NumberOfPhotos { - let title = NSAttributedString(string: "\(photoIndex + 1)", attributes: [NSForegroundColorAttributeName: UIColor.whiteColor()]) - - let photo = shouldSetImageOnIndex(photoIndex) ? ExamplePhoto(image: image, attributedCaptionTitle: title) : ExamplePhoto(attributedCaptionTitle: title) - - if photoIndex == CustomEverythingPhotoIndex { - photo.placeholderImage = UIImage(named: PlaceholderImageName) - } - - mutablePhotos.append(photo) + } +} + +extension PhotosProvider { + fileprivate func demoPhoto(identifier: Int) -> Photo { + let photoName: String + let photoSummary: String + let photoCredit: String + + if (arc4random_uniform(2) == 0 && identifier != 0) { + photoName = "Chess" + photoSummary = "Children gather around a game of chess during the Ann Arbor Summer Festival. (Photo ID \(identifier))" + photoCredit = "Photo: Chris Dzombak" + } else { + photoName = "NYTimesBuilding" + photoSummary = "The New York Times office in Manhattan. (Photo ID \(identifier))" + photoCredit = "Photo: Nic Lehoux" } - - return mutablePhotos - }() + + return Photo(name: photoName, summary: photoSummary, credit: photoCredit, identifier: identifier) + } } diff --git a/Example-Swift/ViewController.swift b/Example-Swift/ViewController.swift index 5e341be2..85a8f1cf 100755 --- a/Example-Swift/ViewController.swift +++ b/Example-Swift/ViewController.swift @@ -9,102 +9,63 @@ import UIKit import NYTPhotoViewer +// The Swift demo project doesn't aim to exactly replicate the ObjC demo, which comprehensively demonstrates the photo viewer. +// Rather, the Swift demo aims to show how to interact with the photo viewer in Swift, and how an application might coordinate the photo viewer with a more complex data layer. -class ViewController: UIViewController, NYTPhotosViewControllerDelegate { +final class ViewController: UIViewController { + + let ReferencePhotoName = "NYTimesBuilding" + + var photoViewerCoordinator: PhotoViewerCoordinator? @IBOutlet weak var imageButton : UIButton? - private let photos = PhotosProvider().photos - @IBAction func buttonTapped(sender: UIButton) { - let photosViewController = NYTPhotosViewController(photos: self.photos) + @IBAction func buttonTapped(_ sender: UIButton) { + let coordinator = PhotoViewerCoordinator(provider: PhotosProvider()) + photoViewerCoordinator = coordinator + + let photosViewController = coordinator.photoViewer photosViewController.delegate = self - presentViewController(photosViewController, animated: true, completion: nil) - - updateImagesOnPhotosViewController(photosViewController, afterDelayWithPhotos: photos) + present(photosViewController, animated: true, completion: nil) } - - func updateImagesOnPhotosViewController(photosViewController: NYTPhotosViewController, afterDelayWithPhotos: [ExamplePhoto]) { - - let delayTime = dispatch_time(DISPATCH_TIME_NOW, 5 * Int64(NSEC_PER_SEC)) - - dispatch_after(delayTime, dispatch_get_main_queue()) { - for photo in self.photos { - if photo.image == nil { - photo.image = UIImage(named: PrimaryImageName) - photosViewController.updateImageForPhoto(photo) - } - } - } - } - + override func viewDidLoad() { super.viewDidLoad() - let buttonImage = UIImage(named: PrimaryImageName) - imageButton?.setBackgroundImage(buttonImage, forState: .Normal) - } - - // MARK: - NYTPhotosViewControllerDelegate - - func photosViewController(photosViewController: NYTPhotosViewController, handleActionButtonTappedForPhoto photo: NYTPhoto) -> Bool { - if UIDevice.currentDevice().userInterfaceIdiom == .Pad { - - guard let photoImage = photo.image else { return false } - - let shareActivityViewController = UIActivityViewController(activityItems: [photoImage], applicationActivities: nil) - - shareActivityViewController.completionWithItemsHandler = {(activityType: String?, completed: Bool, items: [AnyObject]?, error: NSError?) in - if completed { - photosViewController.delegate?.photosViewController!(photosViewController, actionCompletedWithActivityType: activityType!) - } - } + let buttonImage = UIImage(named: ReferencePhotoName) + imageButton?.setBackgroundImage(buttonImage, for: UIControlState()) + } +} - shareActivityViewController.popoverPresentationController?.barButtonItem = photosViewController.rightBarButtonItem - photosViewController.presentViewController(shareActivityViewController, animated: true, completion: nil) +// MARK: NYTPhotosViewControllerDelegate - return true - } - - return false - } +extension ViewController: NYTPhotosViewControllerDelegate { - func photosViewController(photosViewController: NYTPhotosViewController, referenceViewForPhoto photo: NYTPhoto) -> UIView? { - if photo as? ExamplePhoto == photos[NoReferenceViewPhotoIndex] { - return nil + func photosViewController(_ photosViewController: NYTPhotosViewController, handleActionButtonTappedFor photo: NYTPhoto) -> Bool { + guard UIDevice.current.userInterfaceIdiom == .pad, let photoImage = photo.image else { + return false } - return imageButton - } - - func photosViewController(photosViewController: NYTPhotosViewController, loadingViewForPhoto photo: NYTPhoto) -> UIView? { - if photo as! ExamplePhoto == photos[CustomEverythingPhotoIndex] { - let label = UILabel() - label.text = "Custom Loading..." - label.textColor = UIColor.greenColor() - return label - } - return nil - } - - func photosViewController(photosViewController: NYTPhotosViewController, captionViewForPhoto photo: NYTPhoto) -> UIView? { - if photo as! ExamplePhoto == photos[CustomEverythingPhotoIndex] { - let label = UILabel() - label.text = "Custom Caption View" - label.textColor = UIColor.whiteColor() - label.backgroundColor = UIColor.redColor() - return label + + let shareActivityViewController = UIActivityViewController(activityItems: [photoImage], applicationActivities: nil) + shareActivityViewController.completionWithItemsHandler = {(activityType: UIActivityType?, completed: Bool, items: [Any]?, error: Error?) in + if completed { + photosViewController.delegate?.photosViewController!(photosViewController, actionCompletedWithActivityType: activityType?.rawValue) + } } - return nil - } - - func photosViewController(photosViewController: NYTPhotosViewController, didNavigateToPhoto photo: NYTPhoto, atIndex photoIndex: UInt) { - print("Did Navigate To Photo: \(photo) identifier: \(photoIndex)") + + shareActivityViewController.popoverPresentationController?.barButtonItem = photosViewController.rightBarButtonItem + photosViewController.present(shareActivityViewController, animated: true, completion: nil) + + return true } - func photosViewController(photosViewController: NYTPhotosViewController, actionCompletedWithActivityType activityType: String?) { - print("Action Completed With Activity Type: \(activityType)") + func photosViewController(_ photosViewController: NYTPhotosViewController, referenceViewFor photo: NYTPhoto) -> UIView? { + guard let box = photo as? NYTPhotoBox else { return nil } + + return box.value.name == ReferencePhotoName ? imageButton : nil } - func photosViewControllerDidDismiss(photosViewController: NYTPhotosViewController) { - print("Did dismiss Photo Viewer: \(photosViewController)") + func photosViewControllerDidDismiss(_ photosViewController: NYTPhotosViewController) { + photoViewerCoordinator = nil } } diff --git a/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Assets.xcassets/AppIcon.appiconset/Contents.json index 118c98f7..b8236c65 100644 --- a/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", diff --git a/Example/Assets.xcassets/Chess.imageset/Contents.json b/Example/Assets.xcassets/Chess.imageset/Contents.json new file mode 100644 index 00000000..9107d8f1 --- /dev/null +++ b/Example/Assets.xcassets/Chess.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "dzombak-chess.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "dzombak-chess-1.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "dzombak-chess-2.jpg", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/Chess.imageset/dzombak-chess-1.jpg b/Example/Assets.xcassets/Chess.imageset/dzombak-chess-1.jpg new file mode 100644 index 00000000..0f750472 Binary files /dev/null and b/Example/Assets.xcassets/Chess.imageset/dzombak-chess-1.jpg differ diff --git a/Example/Assets.xcassets/Chess.imageset/dzombak-chess-2.jpg b/Example/Assets.xcassets/Chess.imageset/dzombak-chess-2.jpg new file mode 100644 index 00000000..0f750472 Binary files /dev/null and b/Example/Assets.xcassets/Chess.imageset/dzombak-chess-2.jpg differ diff --git a/Example/Assets.xcassets/Chess.imageset/dzombak-chess.jpg b/Example/Assets.xcassets/Chess.imageset/dzombak-chess.jpg new file mode 100644 index 00000000..0f750472 Binary files /dev/null and b/Example/Assets.xcassets/Chess.imageset/dzombak-chess.jpg differ diff --git a/Example/NYTViewController.m b/Example/NYTViewController.m index 28823f09..6a504027 100644 --- a/Example/NYTViewController.m +++ b/Example/NYTViewController.m @@ -8,6 +8,7 @@ #import "NYTViewController.h" #import +#import #import "NYTExamplePhoto.h" typedef NS_ENUM(NSUInteger, NYTViewControllerPhotoIndex) { @@ -23,93 +24,63 @@ typedef NS_ENUM(NSUInteger, NYTViewControllerPhotoIndex) { @interface NYTViewController () @property (weak, nonatomic) IBOutlet UIButton *imageButton; -@property (nonatomic) NSArray *photos; +@property (nonatomic) NYTPhotoViewerArrayDataSource *dataSource; @end @implementation NYTViewController - (IBAction)imageButtonTapped:(id)sender { - self.photos = [[self class] newTestPhotos]; - - NYTPhotosViewController *photosViewController = [[NYTPhotosViewController alloc] initWithPhotos:self.photos initialPhoto:nil delegate:self]; + self.dataSource = [self.class newTimesBuildingDataSource]; + + NYTPhotosViewController *photosViewController = [[NYTPhotosViewController alloc] initWithDataSource:self.dataSource initialPhoto:nil delegate:self]; [self presentViewController:photosViewController animated:YES completion:nil]; - - [self updateImagesOnPhotosViewController:photosViewController afterDelayWithPhotos:self.photos]; + + [self updateImagesOnPhotosViewController:photosViewController afterDelayWithDataSource:self.dataSource]; + BOOL demonstrateDataSourceSwitchAfterTenSeconds = NO; + if (demonstrateDataSourceSwitchAfterTenSeconds) { + [self switchDataSourceOnPhotosViewController:photosViewController afterDelayWithDataSource:self.dataSource]; + } } -// This method simulates previously blank photos loading their images after some time. -- (void)updateImagesOnPhotosViewController:(NYTPhotosViewController *)photosViewController afterDelayWithPhotos:(NSArray *)photos { +// This method simulates a previously blank photo loading its images after 5 seconds. +- (void)updateImagesOnPhotosViewController:(NYTPhotosViewController *)photosViewController afterDelayWithDataSource:(NYTPhotoViewerArrayDataSource *)dataSource { + if (dataSource != self.dataSource) { + return; + } + CGFloat updateImageDelay = 5.0; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(updateImageDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - for (NYTExamplePhoto *photo in photos) { + for (NYTExamplePhoto *photo in dataSource.photos) { if (!photo.image && !photo.imageData) { photo.image = [UIImage imageNamed:@"NYTimesBuilding"]; - [photosViewController updateImageForPhoto:photo]; + photo.attributedCaptionSummary = [[NSAttributedString alloc] initWithString:@"Photo which previously had a loading spinner (to see the spinner, reopen the photo viewer and scroll to this photo)" attributes:@{NSForegroundColorAttributeName: [UIColor whiteColor], NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}]; + [photosViewController updatePhoto:photo]; } } }); } -+ (NSArray *)newTestPhotos { - NSMutableArray *photos = [NSMutableArray array]; - - for (NSUInteger i = 0; i < NYTViewControllerPhotoCount; i++) { - NYTExamplePhoto *photo = [[NYTExamplePhoto alloc] init]; - - if (i == NYTViewControllerPhotoIndexGif) { - photo.imageData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"giphy" ofType:@"gif"]]; - } else if (i == NYTViewControllerPhotoIndexCustomEverything || i == NYTViewControllerPhotoIndexDefaultLoadingSpinner) { - // no-op, left here for clarity: - photo.image = nil; - } else { - photo.image = [UIImage imageNamed:@"NYTimesBuilding"]; - } - - if (i == NYTViewControllerPhotoIndexCustomEverything) { - photo.placeholderImage = [UIImage imageNamed:@"NYTimesBuildingPlaceholder"]; - } - - NSString *caption = @"summary"; - switch ((NYTViewControllerPhotoIndex)i) { - case NYTViewControllerPhotoIndexCustomEverything: - caption = @"photo with custom everything"; - break; - case NYTViewControllerPhotoIndexLongCaption: - caption = @"photo with long caption. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum maximus laoreet vehicula. Maecenas elit quam, pellentesque at tempor vel, tempus non sem. Vestibulum ut aliquam elit. Vivamus rhoncus sapien turpis, at feugiat augue luctus id. Nulla mi urna, viverra sed augue malesuada, bibendum bibendum massa. Cras urna nibh, lacinia vitae feugiat eu, consectetur a tellus. Morbi venenatis nunc sit amet varius pretium. Duis eget sem nec nulla lobortis finibus. Nullam pulvinar gravida est eget tristique. Curabitur faucibus nisl eu diam ullamcorper, at pharetra eros dictum. Suspendisse nibh urna, ultrices a augue a, euismod mattis felis. Ut varius tortor ac efficitur pellentesque. Mauris sit amet rhoncus dolor. Proin vel porttitor mi. Pellentesque lobortis interdum turpis, vitae tincidunt purus vestibulum vel. Phasellus tincidunt vel mi sit amet congue."; - break; - case NYTViewControllerPhotoIndexDefaultLoadingSpinner: - caption = @"photo with loading spinner"; - break; - case NYTViewControllerPhotoIndexNoReferenceView: - caption = @"photo without reference view"; - break; - case NYTViewControllerPhotoIndexCustomMaxZoomScale: - caption = @"photo with custom maximum zoom scale"; - break; - case NYTViewControllerPhotoIndexGif: - caption = @"animated GIF"; - break; - case NYTViewControllerPhotoCount: - // this case statement intentionally left blank. - break; - } - - photo.attributedCaptionTitle = [[NSAttributedString alloc] initWithString:@(i + 1).stringValue attributes:@{NSForegroundColorAttributeName: [UIColor whiteColor], NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}]; - photo.attributedCaptionSummary = [[NSAttributedString alloc] initWithString:caption attributes:@{NSForegroundColorAttributeName: [UIColor lightGrayColor], NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}]; - photo.attributedCaptionCredit = [[NSAttributedString alloc] initWithString:@"NYT Building Photo Credit: Nic Lehoux" attributes:@{NSForegroundColorAttributeName: [UIColor grayColor], NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1]}]; - - [photos addObject:photo]; +// This method simulates completely swapping out the data source, after 10 seconds. +- (void)switchDataSourceOnPhotosViewController:(NYTPhotosViewController *)photosViewController afterDelayWithDataSource:(NYTPhotoViewerArrayDataSource *)dataSource { + if (dataSource != self.dataSource) { + return; } - - return photos; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NYTExamplePhoto *photoWithLongCaption = (NYTExamplePhoto *)dataSource[NYTViewControllerPhotoIndexLongCaption]; + photosViewController.delegate = nil; // delegate methods in this VC are intended for use with the TimesBuildingDataSource + self.dataSource = [self.class newVariedDataSourceIncludingPhoto:photoWithLongCaption]; + photosViewController.dataSource = self.dataSource; + [photosViewController reloadPhotosAnimated:YES]; + }); } #pragma mark - NYTPhotosViewControllerDelegate - (UIView *)photosViewController:(NYTPhotosViewController *)photosViewController referenceViewForPhoto:(id )photo { - if ([photo isEqual:self.photos[NYTViewControllerPhotoIndexNoReferenceView]]) { + if ([photo isEqual:self.dataSource[NYTViewControllerPhotoIndexNoReferenceView]]) { return nil; } @@ -117,7 +88,7 @@ - (UIView *)photosViewController:(NYTPhotosViewController *)photosViewController } - (UIView *)photosViewController:(NYTPhotosViewController *)photosViewController loadingViewForPhoto:(id )photo { - if ([photo isEqual:self.photos[NYTViewControllerPhotoIndexCustomEverything]]) { + if ([photo isEqual:self.dataSource.photos[NYTViewControllerPhotoIndexCustomEverything]]) { UILabel *loadingLabel = [[UILabel alloc] init]; loadingLabel.text = @"Custom Loading..."; loadingLabel.textColor = [UIColor greenColor]; @@ -128,7 +99,7 @@ - (UIView *)photosViewController:(NYTPhotosViewController *)photosViewController } - (UIView *)photosViewController:(NYTPhotosViewController *)photosViewController captionViewForPhoto:(id )photo { - if ([photo isEqual:self.photos[NYTViewControllerPhotoIndexCustomEverything]]) { + if ([photo isEqual:self.dataSource.photos[NYTViewControllerPhotoIndexCustomEverything]]) { UILabel *label = [[UILabel alloc] init]; label.text = @"Custom Caption View"; label.textColor = [UIColor whiteColor]; @@ -140,24 +111,24 @@ - (UIView *)photosViewController:(NYTPhotosViewController *)photosViewController } - (CGFloat)photosViewController:(NYTPhotosViewController *)photosViewController maximumZoomScaleForPhoto:(id )photo { - if ([photo isEqual:self.photos[NYTViewControllerPhotoIndexCustomMaxZoomScale]]) { - return 10.0f; + if ([photo isEqual:self.dataSource.photos[NYTViewControllerPhotoIndexCustomMaxZoomScale]]) { + return 0.5f; } return 1.0f; } - (NSDictionary *)photosViewController:(NYTPhotosViewController *)photosViewController overlayTitleTextAttributesForPhoto:(id )photo { - if ([photo isEqual:self.photos[NYTViewControllerPhotoIndexCustomEverything]]) { + if ([photo isEqual:self.dataSource.photos[NYTViewControllerPhotoIndexCustomEverything]]) { return @{NSForegroundColorAttributeName: [UIColor grayColor]}; } return nil; } -- (NSString *)photosViewController:(NYTPhotosViewController *)photosViewController titleForPhoto:(id)photo atIndex:(NSUInteger)photoIndex totalPhotoCount:(NSUInteger)totalPhotoCount { - if ([photo isEqual:self.photos[NYTViewControllerPhotoIndexCustomEverything]]) { - return [NSString stringWithFormat:@"%lu/%lu", (unsigned long)photoIndex+1, (unsigned long)totalPhotoCount]; +- (NSString *)photosViewController:(NYTPhotosViewController *)photosViewController titleForPhoto:(id)photo atIndex:(NSInteger)photoIndex totalPhotoCount:(nullable NSNumber *)totalPhotoCount { + if ([photo isEqual:self.dataSource.photos[NYTViewControllerPhotoIndexCustomEverything]]) { + return [NSString stringWithFormat:@"%lu/%lu", (unsigned long)photoIndex+1, (unsigned long)totalPhotoCount.integerValue]; } return nil; @@ -175,4 +146,97 @@ - (void)photosViewControllerDidDismiss:(NYTPhotosViewController *)photosViewCont NSLog(@"Did Dismiss Photo Viewer: %@", photosViewController); } +#pragma mark - Sample Data Sources + ++ (NYTPhotoViewerArrayDataSource *)newTimesBuildingDataSource { + NSMutableArray *photos = [NSMutableArray array]; + + for (NSUInteger i = 0; i < NYTViewControllerPhotoCount; i++) { + NYTExamplePhoto *photo = [NYTExamplePhoto new]; + + if (i == NYTViewControllerPhotoIndexGif) { + photo.imageData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"giphy" ofType:@"gif"]]; + } else if (i == NYTViewControllerPhotoIndexCustomEverything || i == NYTViewControllerPhotoIndexDefaultLoadingSpinner) { + // no-op, left here for clarity: + photo.image = nil; + } else { + photo.image = [UIImage imageNamed:@"NYTimesBuilding"]; + } + + if (i == NYTViewControllerPhotoIndexCustomEverything) { + photo.placeholderImage = [UIImage imageNamed:@"NYTimesBuildingPlaceholder"]; + } + + NSString *caption = @""; + switch ((NYTViewControllerPhotoIndex)i) { + case NYTViewControllerPhotoIndexCustomEverything: + caption = @"Photo with custom everything"; + break; + case NYTViewControllerPhotoIndexLongCaption: + caption = @"Photo with long caption.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum maximus laoreet vehicula. Maecenas elit quam, pellentesque at tempor vel, tempus non sem. Vestibulum ut aliquam elit. Vivamus rhoncus sapien turpis, at feugiat augue luctus id. Nulla mi urna, viverra sed augue malesuada, bibendum bibendum massa. Cras urna nibh, lacinia vitae feugiat eu, consectetur a tellus. Morbi venenatis nunc sit amet varius pretium. Duis eget sem nec nulla lobortis finibus. Nullam pulvinar gravida est eget tristique. Curabitur faucibus nisl eu diam ullamcorper, at pharetra eros dictum. Suspendisse nibh urna, ultrices a augue a, euismod mattis felis. Ut varius tortor ac efficitur pellentesque. Mauris sit amet rhoncus dolor. Proin vel porttitor mi. Pellentesque lobortis interdum turpis, vitae tincidunt purus vestibulum vel. Phasellus tincidunt vel mi sit amet congue."; + break; + case NYTViewControllerPhotoIndexDefaultLoadingSpinner: + caption = @"Photo with loading spinner"; + break; + case NYTViewControllerPhotoIndexNoReferenceView: + caption = @"Photo without reference view"; + break; + case NYTViewControllerPhotoIndexCustomMaxZoomScale: + caption = @"Photo with custom maximum zoom scale"; + break; + case NYTViewControllerPhotoIndexGif: + caption = @"Animated GIF"; + break; + case NYTViewControllerPhotoCount: + // this case statement intentionally left blank. + break; + } + + photo.attributedCaptionTitle = [self attributedTitleFromString:@(i + 1).stringValue]; + photo.attributedCaptionSummary = [self attributedSummaryFromString:caption]; + + if (i != NYTViewControllerPhotoIndexGif) { + photo.attributedCaptionCredit = [self attributedCreditFromString:@"Photo: Nic Lehoux"]; + } + + [photos addObject:photo]; + } + + return [NYTPhotoViewerArrayDataSource dataSourceWithPhotos:photos]; +} + +/// A second set of test photos, to demonstrate reloading the entire data source. ++ (NYTPhotoViewerArrayDataSource *)newVariedDataSourceIncludingPhoto:(NYTExamplePhoto *)photo { + NSMutableArray *photos = [NSMutableArray array]; + + [photos addObject:({ + NYTExamplePhoto *p = [NYTExamplePhoto new]; + p.image = [UIImage imageNamed:@"Chess"]; + p.attributedCaptionTitle = [self attributedTitleFromString:@"Chess"]; + p.attributedCaptionCredit = [self attributedCreditFromString:@"Photo: Chris Dzombak"]; + p; + })]; + + [photos addObject:({ + NYTExamplePhoto *p = photo; + photo.attributedCaptionTitle = nil; + p.attributedCaptionSummary = [self attributedSummaryFromString:@"This photo’s caption has changed in the data source."]; + p; + })]; + + return [NYTPhotoViewerArrayDataSource dataSourceWithPhotos:photos]; +} + ++ (NSAttributedString *)attributedTitleFromString:(NSString *)caption { + return [[NSAttributedString alloc] initWithString:caption attributes:@{NSForegroundColorAttributeName: [UIColor whiteColor], NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}]; +} + ++ (NSAttributedString *)attributedSummaryFromString:(NSString *)summary { + return [[NSAttributedString alloc] initWithString:summary attributes:@{NSForegroundColorAttributeName: [UIColor lightGrayColor], NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}]; +} + ++ (NSAttributedString *)attributedCreditFromString:(NSString *)credit { + return [[NSAttributedString alloc] initWithString:credit attributes:@{NSForegroundColorAttributeName: [UIColor grayColor], NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1]}]; +} + @end diff --git a/LICENSE.md b/LICENSE.md index 1b8afe29..85159eb5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,11 +1,11 @@ -Copyright (c) 2015-2016 The New York Times Company - +Copyright (c) 2015-2017 The New York Times Company + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this library except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/NYTPhotoViewer.podspec b/NYTPhotoViewer.podspec index 25eb1533..6fe70c84 100644 --- a/NYTPhotoViewer.podspec +++ b/NYTPhotoViewer.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "NYTPhotoViewer" - s.version = "1.2.0" + s.version = "2.0.0" s.description = <<-DESC NYTPhotoViewer is a slideshow and image viewer that includes double tap to zoom, captions, support for multiple images, interactive flick to dismiss, animated zooming presentation, and more. diff --git a/NYTPhotoViewer.xcodeproj/project.pbxproj b/NYTPhotoViewer.xcodeproj/project.pbxproj index 95143a45..da309f1c 100644 --- a/NYTPhotoViewer.xcodeproj/project.pbxproj +++ b/NYTPhotoViewer.xcodeproj/project.pbxproj @@ -19,8 +19,8 @@ 1110844D1C87682300699670 /* NYTPhotoCaptionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084341C87682300699670 /* NYTPhotoCaptionView.m */; }; 1110844E1C87682300699670 /* NYTPhotoDismissalInteractionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084351C87682300699670 /* NYTPhotoDismissalInteractionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1110844F1C87682300699670 /* NYTPhotoDismissalInteractionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084361C87682300699670 /* NYTPhotoDismissalInteractionController.m */; }; - 111084501C87682300699670 /* NYTPhotosDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084371C87682300699670 /* NYTPhotosDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 111084511C87682300699670 /* NYTPhotosDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084381C87682300699670 /* NYTPhotosDataSource.m */; }; + 111084501C87682300699670 /* NYTPhotoViewerArrayDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084371C87682300699670 /* NYTPhotoViewerArrayDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 111084511C87682300699670 /* NYTPhotoViewerArrayDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084381C87682300699670 /* NYTPhotoViewerArrayDataSource.m */; }; 111084521C87682300699670 /* NYTPhotosOverlayView.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084391C87682300699670 /* NYTPhotosOverlayView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 111084531C87682300699670 /* NYTPhotosOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1110843A1C87682300699670 /* NYTPhotosOverlayView.m */; }; 111084541C87682300699670 /* NYTPhotosViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 1110843B1C87682300699670 /* NYTPhotosViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -36,7 +36,7 @@ 1110845E1C87682300699670 /* NYTPhoto.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084461C87682300699670 /* NYTPhoto.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1110845F1C87682300699670 /* NYTPhotoCaptionViewLayoutWidthHinting.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084471C87682300699670 /* NYTPhotoCaptionViewLayoutWidthHinting.h */; settings = {ATTRIBUTES = (Public, ); }; }; 111084601C87682300699670 /* NYTPhotoContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084481C87682300699670 /* NYTPhotoContainer.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 111084611C87682300699670 /* NYTPhotosViewControllerDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084491C87682300699670 /* NYTPhotosViewControllerDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 111084611C87682300699670 /* NYTPhotoViewerDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084491C87682300699670 /* NYTPhotoViewerDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 111084621C87682300699670 /* NSBundle+NYTPhotoViewer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1110844B1C87682300699670 /* NSBundle+NYTPhotoViewer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 111084631C87682300699670 /* NSBundle+NYTPhotoViewer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1110844C1C87682300699670 /* NSBundle+NYTPhotoViewer.m */; }; 111084651C87684D00699670 /* NYTPhotoCaptionView.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084641C87684D00699670 /* NYTPhotoCaptionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -63,8 +63,8 @@ 11FBDA8D1C87753200018169 /* NYTPhotoCaptionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084341C87682300699670 /* NYTPhotoCaptionView.m */; }; 11FBDA8E1C87753200018169 /* NYTPhotoDismissalInteractionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084351C87682300699670 /* NYTPhotoDismissalInteractionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11FBDA8F1C87753200018169 /* NYTPhotoDismissalInteractionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084361C87682300699670 /* NYTPhotoDismissalInteractionController.m */; }; - 11FBDA901C87753200018169 /* NYTPhotosDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084371C87682300699670 /* NYTPhotosDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 11FBDA911C87753200018169 /* NYTPhotosDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084381C87682300699670 /* NYTPhotosDataSource.m */; }; + 11FBDA901C87753200018169 /* NYTPhotoViewerArrayDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084371C87682300699670 /* NYTPhotoViewerArrayDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 11FBDA911C87753200018169 /* NYTPhotoViewerArrayDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 111084381C87682300699670 /* NYTPhotoViewerArrayDataSource.m */; }; 11FBDA921C87753200018169 /* NYTPhotosOverlayView.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084391C87682300699670 /* NYTPhotosOverlayView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11FBDA931C87753200018169 /* NYTPhotosOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1110843A1C87682300699670 /* NYTPhotosOverlayView.m */; }; 11FBDA941C87753200018169 /* NYTPhotosViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 1110843B1C87682300699670 /* NYTPhotosViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -80,7 +80,7 @@ 11FBDA9E1C87753200018169 /* NYTPhoto.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084461C87682300699670 /* NYTPhoto.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11FBDA9F1C87753200018169 /* NYTPhotoCaptionViewLayoutWidthHinting.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084471C87682300699670 /* NYTPhotoCaptionViewLayoutWidthHinting.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11FBDAA01C87753200018169 /* NYTPhotoContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084481C87682300699670 /* NYTPhotoContainer.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 11FBDAA11C87753200018169 /* NYTPhotosViewControllerDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084491C87682300699670 /* NYTPhotosViewControllerDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 11FBDAA11C87753200018169 /* NYTPhotoViewerDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 111084491C87682300699670 /* NYTPhotoViewerDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11FBDAA21C87753200018169 /* NSBundle+NYTPhotoViewer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1110844B1C87682300699670 /* NSBundle+NYTPhotoViewer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11FBDAA31C87753200018169 /* NSBundle+NYTPhotoViewer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1110844C1C87682300699670 /* NSBundle+NYTPhotoViewer.m */; }; 11FBDAA41C87754A00018169 /* NYTPhotoViewer.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 11FBDA791C87714600018169 /* NYTPhotoViewer.bundle */; }; @@ -88,13 +88,19 @@ 11FBDADF1C877BD600018169 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FBDADE1C877BD600018169 /* AppDelegate.swift */; }; 11FBDAE11C877BD600018169 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FBDAE01C877BD600018169 /* ViewController.swift */; }; 11FBDAE41C877BD600018169 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 11FBDAE21C877BD600018169 /* Main.storyboard */; }; - 11FBDAEF1C877C5D00018169 /* ExamplePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FBDAEE1C877C5D00018169 /* ExamplePhoto.swift */; }; 11FBDAF11C877C8B00018169 /* PhotosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FBDAF01C877C8B00018169 /* PhotosProvider.swift */; }; 11FBDAF71C877CEF00018169 /* NYTPhotoViewer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 111084061C875B5000699670 /* NYTPhotoViewer.framework */; }; 11FBDAF81C877CEF00018169 /* NYTPhotoViewer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 111084061C875B5000699670 /* NYTPhotoViewer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 11FBDAFD1C877D9A00018169 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 11FBDAFB1C877D9A00018169 /* LaunchScreen.xib */; }; 11FBDAFE1C877F5500018169 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 111084781C876A2B00699670 /* Assets.xcassets */; }; 11FBDAFF1C877F8000018169 /* giphy.gif in Resources */ = {isa = PBXBuildFile; fileRef = 111084851C876B0400699670 /* giphy.gif */; }; + 9371A73C1E438B9C00A8F2EF /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371A73B1E438B9C00A8F2EF /* Photo.swift */; }; + 9371A7441E438BCE00A8F2EF /* PhotoBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371A7431E438BCE00A8F2EF /* PhotoBox.swift */; }; + 9371A7461E43D61E00A8F2EF /* PhotoViewerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371A7451E43D61E00A8F2EF /* PhotoViewerCoordinator.swift */; }; + 93F45B181E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 93F45B161E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 93F45B191E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 93F45B161E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 93F45B1A1E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 93F45B171E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m */; }; + 93F45B1B1E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 93F45B171E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -241,8 +247,8 @@ 111084341C87682300699670 /* NYTPhotoCaptionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTPhotoCaptionView.m; sourceTree = ""; }; 111084351C87682300699670 /* NYTPhotoDismissalInteractionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotoDismissalInteractionController.h; sourceTree = ""; }; 111084361C87682300699670 /* NYTPhotoDismissalInteractionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTPhotoDismissalInteractionController.m; sourceTree = ""; }; - 111084371C87682300699670 /* NYTPhotosDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotosDataSource.h; sourceTree = ""; }; - 111084381C87682300699670 /* NYTPhotosDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTPhotosDataSource.m; sourceTree = ""; }; + 111084371C87682300699670 /* NYTPhotoViewerArrayDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotoViewerArrayDataSource.h; sourceTree = ""; }; + 111084381C87682300699670 /* NYTPhotoViewerArrayDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTPhotoViewerArrayDataSource.m; sourceTree = ""; }; 111084391C87682300699670 /* NYTPhotosOverlayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotosOverlayView.h; sourceTree = ""; }; 1110843A1C87682300699670 /* NYTPhotosOverlayView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTPhotosOverlayView.m; sourceTree = ""; }; 1110843B1C87682300699670 /* NYTPhotosViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotosViewController.h; sourceTree = ""; }; @@ -258,7 +264,7 @@ 111084461C87682300699670 /* NYTPhoto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhoto.h; sourceTree = ""; }; 111084471C87682300699670 /* NYTPhotoCaptionViewLayoutWidthHinting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotoCaptionViewLayoutWidthHinting.h; sourceTree = ""; }; 111084481C87682300699670 /* NYTPhotoContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotoContainer.h; sourceTree = ""; }; - 111084491C87682300699670 /* NYTPhotosViewControllerDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotosViewControllerDataSource.h; sourceTree = ""; }; + 111084491C87682300699670 /* NYTPhotoViewerDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotoViewerDataSource.h; sourceTree = ""; }; 1110844B1C87682300699670 /* NSBundle+NYTPhotoViewer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSBundle+NYTPhotoViewer.h"; sourceTree = ""; }; 1110844C1C87682300699670 /* NSBundle+NYTPhotoViewer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+NYTPhotoViewer.m"; sourceTree = ""; }; 111084641C87684D00699670 /* NYTPhotoCaptionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotoCaptionView.h; sourceTree = ""; }; @@ -285,9 +291,13 @@ 11FBDAE01C877BD600018169 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 11FBDAE31C877BD600018169 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 11FBDAEA1C877BD600018169 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 11FBDAEE1C877C5D00018169 /* ExamplePhoto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExamplePhoto.swift; sourceTree = ""; }; 11FBDAF01C877C8B00018169 /* PhotosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosProvider.swift; sourceTree = ""; }; 11FBDAFC1C877D9A00018169 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 9371A73B1E438B9C00A8F2EF /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; + 9371A7431E438BCE00A8F2EF /* PhotoBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoBox.swift; sourceTree = ""; }; + 9371A7451E43D61E00A8F2EF /* PhotoViewerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoViewerCoordinator.swift; sourceTree = ""; }; + 93F45B161E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYTPhotoViewerSinglePhotoDataSource.h; sourceTree = ""; }; + 93F45B171E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYTPhotoViewerSinglePhotoDataSource.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -369,8 +379,6 @@ 111084341C87682300699670 /* NYTPhotoCaptionView.m */, 111084351C87682300699670 /* NYTPhotoDismissalInteractionController.h */, 111084361C87682300699670 /* NYTPhotoDismissalInteractionController.m */, - 111084371C87682300699670 /* NYTPhotosDataSource.h */, - 111084381C87682300699670 /* NYTPhotosDataSource.m */, 111084391C87682300699670 /* NYTPhotosOverlayView.h */, 1110843A1C87682300699670 /* NYTPhotosOverlayView.m */, 1110843B1C87682300699670 /* NYTPhotosViewController.h */, @@ -381,6 +389,10 @@ 111084401C87682300699670 /* NYTPhotoTransitionController.m */, 111084411C87682300699670 /* NYTPhotoViewController.h */, 111084421C87682300699670 /* NYTPhotoViewController.m */, + 111084371C87682300699670 /* NYTPhotoViewerArrayDataSource.h */, + 111084381C87682300699670 /* NYTPhotoViewerArrayDataSource.m */, + 93F45B161E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h */, + 93F45B171E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m */, 111084431C87682300699670 /* NYTScalingImageView.h */, 111084441C87682300699670 /* NYTScalingImageView.m */, 111084451C87682300699670 /* Protocols */, @@ -411,7 +423,7 @@ 111084461C87682300699670 /* NYTPhoto.h */, 111084471C87682300699670 /* NYTPhotoCaptionViewLayoutWidthHinting.h */, 111084481C87682300699670 /* NYTPhotoContainer.h */, - 111084491C87682300699670 /* NYTPhotosViewControllerDataSource.h */, + 111084491C87682300699670 /* NYTPhotoViewerDataSource.h */, ); path = Protocols; sourceTree = ""; @@ -488,8 +500,10 @@ children = ( 11FBDADE1C877BD600018169 /* AppDelegate.swift */, 11FBDAE01C877BD600018169 /* ViewController.swift */, - 11FBDAEE1C877C5D00018169 /* ExamplePhoto.swift */, + 9371A7451E43D61E00A8F2EF /* PhotoViewerCoordinator.swift */, 11FBDAF01C877C8B00018169 /* PhotosProvider.swift */, + 9371A7431E438BCE00A8F2EF /* PhotoBox.swift */, + 9371A73B1E438B9C00A8F2EF /* Photo.swift */, 11FBDAE21C877BD600018169 /* Main.storyboard */, 11FBDAFB1C877D9A00018169 /* LaunchScreen.xib */, 11FBDAEA1C877BD600018169 /* Info.plist */, @@ -508,15 +522,16 @@ 111084541C87682300699670 /* NYTPhotosViewController.h in Headers */, 111084601C87682300699670 /* NYTPhotoContainer.h in Headers */, 1110844E1C87682300699670 /* NYTPhotoDismissalInteractionController.h in Headers */, - 111084611C87682300699670 /* NYTPhotosViewControllerDataSource.h in Headers */, + 111084611C87682300699670 /* NYTPhotoViewerDataSource.h in Headers */, 1110845A1C87682300699670 /* NYTPhotoViewController.h in Headers */, 1110845C1C87682300699670 /* NYTScalingImageView.h in Headers */, 111084521C87682300699670 /* NYTPhotosOverlayView.h in Headers */, 111084561C87682300699670 /* NYTPhotoTransitionAnimator.h in Headers */, + 93F45B181E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h in Headers */, 111084581C87682300699670 /* NYTPhotoTransitionController.h in Headers */, 1110845F1C87682300699670 /* NYTPhotoCaptionViewLayoutWidthHinting.h in Headers */, 1110845E1C87682300699670 /* NYTPhoto.h in Headers */, - 111084501C87682300699670 /* NYTPhotosDataSource.h in Headers */, + 111084501C87682300699670 /* NYTPhotoViewerArrayDataSource.h in Headers */, 1110840A1C875B5000699670 /* NYTPhotoViewer.h in Headers */, 111084651C87684D00699670 /* NYTPhotoCaptionView.h in Headers */, ); @@ -531,10 +546,11 @@ 11FBDA9C1C87753200018169 /* NYTScalingImageView.h in Headers */, 11FBDA921C87753200018169 /* NYTPhotosOverlayView.h in Headers */, 11FBDA8E1C87753200018169 /* NYTPhotoDismissalInteractionController.h in Headers */, - 11FBDAA11C87753200018169 /* NYTPhotosViewControllerDataSource.h in Headers */, - 11FBDA901C87753200018169 /* NYTPhotosDataSource.h in Headers */, + 11FBDAA11C87753200018169 /* NYTPhotoViewerDataSource.h in Headers */, + 11FBDA901C87753200018169 /* NYTPhotoViewerArrayDataSource.h in Headers */, 11FBDA9F1C87753200018169 /* NYTPhotoCaptionViewLayoutWidthHinting.h in Headers */, 11FBDA941C87753200018169 /* NYTPhotosViewController.h in Headers */, + 93F45B191E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.h in Headers */, 11FBDAA61C87768B00018169 /* NYTPhotoViewerCore.h in Headers */, 11FBDA9E1C87753200018169 /* NYTPhoto.h in Headers */, 11FBDAA21C87753200018169 /* NSBundle+NYTPhotoViewer.h in Headers */, @@ -650,7 +666,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 0820; ORGANIZATIONNAME = NYTimes; TargetAttributes = { 111084051C875B5000699670 = { @@ -667,6 +683,7 @@ }; 11FBDADB1C877BD600018169 = { CreatedOnToolsVersion = 7.2.1; + LastSwiftMigration = 0810; }; }; }; @@ -811,9 +828,10 @@ 111084591C87682300699670 /* NYTPhotoTransitionController.m in Sources */, 111084631C87682300699670 /* NSBundle+NYTPhotoViewer.m in Sources */, 111084551C87682300699670 /* NYTPhotosViewController.m in Sources */, + 93F45B1A1E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m in Sources */, 1110844D1C87682300699670 /* NYTPhotoCaptionView.m in Sources */, 1110845D1C87682300699670 /* NYTScalingImageView.m in Sources */, - 111084511C87682300699670 /* NYTPhotosDataSource.m in Sources */, + 111084511C87682300699670 /* NYTPhotoViewerArrayDataSource.m in Sources */, 111084531C87682300699670 /* NYTPhotosOverlayView.m in Sources */, 111084571C87682300699670 /* NYTPhotoTransitionAnimator.m in Sources */, 1110844F1C87682300699670 /* NYTPhotoDismissalInteractionController.m in Sources */, @@ -854,9 +872,10 @@ 11FBDA991C87753200018169 /* NYTPhotoTransitionController.m in Sources */, 11FBDAA31C87753200018169 /* NSBundle+NYTPhotoViewer.m in Sources */, 11FBDA951C87753200018169 /* NYTPhotosViewController.m in Sources */, + 93F45B1B1E3BB5610093DB93 /* NYTPhotoViewerSinglePhotoDataSource.m in Sources */, 11FBDA8D1C87753200018169 /* NYTPhotoCaptionView.m in Sources */, 11FBDA9D1C87753200018169 /* NYTScalingImageView.m in Sources */, - 11FBDA911C87753200018169 /* NYTPhotosDataSource.m in Sources */, + 11FBDA911C87753200018169 /* NYTPhotoViewerArrayDataSource.m in Sources */, 11FBDA931C87753200018169 /* NYTPhotosOverlayView.m in Sources */, 11FBDA971C87753200018169 /* NYTPhotoTransitionAnimator.m in Sources */, 11FBDA8F1C87753200018169 /* NYTPhotoDismissalInteractionController.m in Sources */, @@ -868,10 +887,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9371A7441E438BCE00A8F2EF /* PhotoBox.swift in Sources */, 11FBDAE11C877BD600018169 /* ViewController.swift in Sources */, - 11FBDAEF1C877C5D00018169 /* ExamplePhoto.swift in Sources */, 11FBDADF1C877BD600018169 /* AppDelegate.swift in Sources */, + 9371A7461E43D61E00A8F2EF /* PhotoViewerCoordinator.swift in Sources */, 11FBDAF11C877C8B00018169 /* PhotosProvider.swift in Sources */, + 9371A73C1E438B9C00A8F2EF /* Photo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -964,8 +985,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1011,8 +1034,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1032,6 +1057,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.2; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -1042,6 +1068,7 @@ 1110841B1C875B5000699670 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -1064,6 +1091,7 @@ 1110841C1C875B5000699670 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -1124,6 +1152,7 @@ 11FBDA891C8774AD00018169 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -1141,6 +1170,7 @@ 11FBDA8A1C8774AD00018169 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -1168,6 +1198,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.NYTimes.Example-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -1183,6 +1214,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.NYTimes.Example-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/NYTPhotoViewer.xcodeproj/xcshareddata/xcschemes/NYTPhotoViewer.xcscheme b/NYTPhotoViewer.xcodeproj/xcshareddata/xcschemes/NYTPhotoViewer.xcscheme index 7a1ba328..f5b83ab8 100644 --- a/NYTPhotoViewer.xcodeproj/xcshareddata/xcschemes/NYTPhotoViewer.xcscheme +++ b/NYTPhotoViewer.xcodeproj/xcshareddata/xcschemes/NYTPhotoViewer.xcscheme @@ -1,6 +1,6 @@ diff --git a/NYTPhotoViewer/NYTPhotoViewController.m b/NYTPhotoViewer/NYTPhotoViewController.m index d37b25f6..301854c9 100644 --- a/NYTPhotoViewer/NYTPhotoViewController.m +++ b/NYTPhotoViewer/NYTPhotoViewController.m @@ -80,6 +80,10 @@ - (void)viewWillLayoutSubviews { self.loadingView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds)); } +- (BOOL)prefersHomeIndicatorAutoHidden { + return YES; +} + #pragma mark - NYTPhotoViewController - (instancetype)initWithPhoto:(id )photo loadingView:(UIView *)loadingView notificationCenter:(NSNotificationCenter *)notificationCenter { diff --git a/NYTPhotoViewer/NYTPhotoViewer.h b/NYTPhotoViewer/NYTPhotoViewer.h index 7ed4c3e9..c28f4983 100644 --- a/NYTPhotoViewer/NYTPhotoViewer.h +++ b/NYTPhotoViewer/NYTPhotoViewer.h @@ -17,7 +17,8 @@ FOUNDATION_EXPORT const unsigned char NYTPhotoViewerVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import #import #import -#import +#import +#import #import #import #import @@ -29,6 +30,7 @@ FOUNDATION_EXPORT const unsigned char NYTPhotoViewerVersionString[]; #import #import #import -#import +#import +// Support #import diff --git a/NYTPhotoViewer/NYTPhotoViewerArrayDataSource.h b/NYTPhotoViewer/NYTPhotoViewerArrayDataSource.h new file mode 100644 index 00000000..f1a6baf2 --- /dev/null +++ b/NYTPhotoViewer/NYTPhotoViewerArrayDataSource.h @@ -0,0 +1,37 @@ +// +// NYTPhotoViewerArrayDataSource.h +// NYTPhotoViewer +// +// Created by Brian Capps on 2/11/15. +// Copyright (c) 2017 The New York Times Company. All rights reserved. +// + +@import Foundation; + +#import "NYTPhotoViewerDataSource.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A simple concrete implementation of `NYTPhotoViewerDataSource`, for use with an array of images. + */ +@interface NYTPhotoViewerArrayDataSource : NSObject + +@property (nonatomic, readonly) NSArray> *photos; + +/** + * The designated initializer that takes and stores an array of photos. + * + * @param photos An array of objects conforming to the `NYTPhoto` protocol. + * + * @return A fully initialized data source. + */ +- (instancetype)initWithPhotos:(nullable NSArray> *)photos NS_DESIGNATED_INITIALIZER; + ++ (instancetype)dataSourceWithPhotos:(nullable NSArray> *)photos; + +- (id)objectAtIndexedSubscript:(NSUInteger)idx; + +@end + +NS_ASSUME_NONNULL_END diff --git a/NYTPhotoViewer/NYTPhotoViewerArrayDataSource.m b/NYTPhotoViewer/NYTPhotoViewerArrayDataSource.m new file mode 100644 index 00000000..37d8ea53 --- /dev/null +++ b/NYTPhotoViewer/NYTPhotoViewerArrayDataSource.m @@ -0,0 +1,69 @@ +// +// NYTPhotoViewerArrayDataSource.m +// NYTPhotoViewer +// +// Created by Brian Capps on 2/11/15. +// Copyright (c) 2017 The New York Times Company. All rights reserved. +// + +#import "NYTPhotoViewerArrayDataSource.h" + +@implementation NYTPhotoViewerArrayDataSource + +#pragma mark - NSObject + +- (instancetype)init { + return [self initWithPhotos:nil]; +} + +#pragma mark - NYTPhotosDataSource + +- (instancetype)initWithPhotos:(nullable NSArray> *)photos { + self = [super init]; + + if (self) { + if (photos == nil) { + _photos = @[]; + } else { + _photos = [photos copy]; + } + } + + return self; +} + ++ (instancetype)dataSourceWithPhotos:(nullable NSArray> *)photos { + return [[self alloc] initWithPhotos:photos]; +} + +#pragma mark - NSFastEnumeration + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)length { + return [self.photos countByEnumeratingWithState:state objects:buffer count:length]; +} + +#pragma mark - NYTPhotosViewControllerDataSource + +- (NSNumber *)numberOfPhotos { + return @(self.photos.count); +} + +- (nullable id )photoAtIndex:(NSInteger)photoIndex { + if (photoIndex < self.photos.count) { + return self.photos[photoIndex]; + } + + return nil; +} + +- (NSInteger)indexOfPhoto:(id )photo { + return [self.photos indexOfObject:photo]; +} + +#pragma mark - Subscripting + +- (id)objectAtIndexedSubscript:(NSUInteger)idx { + return self.photos[idx]; +} + +@end diff --git a/NYTPhotoViewer/NYTPhotoViewerCore.h b/NYTPhotoViewer/NYTPhotoViewerCore.h index 4d46ea12..55dc8a38 100644 --- a/NYTPhotoViewer/NYTPhotoViewerCore.h +++ b/NYTPhotoViewer/NYTPhotoViewerCore.h @@ -1,5 +1,5 @@ // -// NYTPhotoViewer.h +// NYTPhotoViewerCore.h // NYTPhotoViewer // // Created by David Beck on 3/2/16. @@ -17,7 +17,8 @@ FOUNDATION_EXPORT const unsigned char NYTPhotoViewerCoreVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import #import #import -#import +#import +#import #import #import #import @@ -29,6 +30,7 @@ FOUNDATION_EXPORT const unsigned char NYTPhotoViewerCoreVersionString[]; #import #import #import -#import +#import +// Support #import diff --git a/NYTPhotoViewer/NYTPhotoViewerSinglePhotoDataSource.h b/NYTPhotoViewer/NYTPhotoViewerSinglePhotoDataSource.h new file mode 100644 index 00000000..65edcc56 --- /dev/null +++ b/NYTPhotoViewer/NYTPhotoViewerSinglePhotoDataSource.h @@ -0,0 +1,40 @@ +// +// NYTPhotoViewerSinglePhotoDataSource.h +// NYTPhotoViewer +// +// Created by Chris Dzombak on 1/27/17. +// Copyright © 2017 The New York Times Company. All rights reserved. +// + +@import Foundation; + +#import "NYTPhotoViewerDataSource.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A simple concrete implementation of `NYTPhotoViewerDataSource`, for use with a single image. + */ +@interface NYTPhotoViewerSinglePhotoDataSource : NSObject + +@property (nonatomic, readonly) id photo; + +/** + * The designated initializer that takes and stores a single photo. + * + * @param photos An object conforming to the `NYTPhoto` protocol. + * + * @return A fully initialized data source. + */ +- (instancetype)initWithPhoto:(id)photo NS_DESIGNATED_INITIALIZER; + ++ (instancetype)dataSourceWithPhoto:(id)photo; + +/** + * Initializing without a photo is invalid. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/NYTPhotoViewer/NYTPhotoViewerSinglePhotoDataSource.m b/NYTPhotoViewer/NYTPhotoViewerSinglePhotoDataSource.m new file mode 100644 index 00000000..3ec9104e --- /dev/null +++ b/NYTPhotoViewer/NYTPhotoViewerSinglePhotoDataSource.m @@ -0,0 +1,38 @@ +// +// NYTPhotoViewerSinglePhotoDataSource.m +// NYTPhotoViewer +// +// Created by Chris Dzombak on 1/27/17. +// Copyright © 2017 The New York Times Company. All rights reserved. +// + +#import "NYTPhotoViewerSinglePhotoDataSource.h" + +@implementation NYTPhotoViewerSinglePhotoDataSource + +- (instancetype)initWithPhoto:(id)photo { + if ((self = [super init])) { + _photo = photo; + } + return self; +} + ++ (instancetype)dataSourceWithPhoto:(id)photo { + return [[self alloc] initWithPhoto:photo]; +} + +#pragma mark NYTPhotoViewerDataSource + +- (NSNumber *)numberOfPhotos { + return @(1); +} + +- (id)photoAtIndex:(NSInteger)photoIndex { + return self.photo; +} + +- (NSInteger)indexOfPhoto:(id)photo { + return 0; +} + +@end diff --git a/NYTPhotoViewer/NYTPhotosDataSource.h b/NYTPhotoViewer/NYTPhotosDataSource.h deleted file mode 100644 index 053f30bc..00000000 --- a/NYTPhotoViewer/NYTPhotosDataSource.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// NYTPhotosDataSource.h -// NYTPhotoViewer -// -// Created by Brian Capps on 2/11/15. -// -// - -@import Foundation; - -#import "NYTPhotosViewControllerDataSource.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * A concrete implementation of the `NYTPhotosViewControllerDataSource`. - */ -@interface NYTPhotosDataSource : NSObject - -/** - * The designated initializer that takes and stores an array of photos. - * - * @param photos An array of objects conforming to the `NYTPhoto` protocol. - * - * @return A fully initialized object. - */ -- (instancetype)initWithPhotos:(nullable NSArray *)photos NS_DESIGNATED_INITIALIZER; - -@end - -NS_ASSUME_NONNULL_END diff --git a/NYTPhotoViewer/NYTPhotosDataSource.m b/NYTPhotoViewer/NYTPhotosDataSource.m deleted file mode 100644 index 98f9453d..00000000 --- a/NYTPhotoViewer/NYTPhotosDataSource.m +++ /dev/null @@ -1,69 +0,0 @@ -// -// NYTPhotosDataSource.m -// NYTPhotoViewer -// -// Created by Brian Capps on 2/11/15. -// -// - -#import "NYTPhotosDataSource.h" - -@interface NYTPhotosDataSource () - -@property (nonatomic, copy) NSArray *photos; - -@end - -@implementation NYTPhotosDataSource - -#pragma mark - NSObject - -- (instancetype)init { - return [self initWithPhotos:nil]; -} - -#pragma mark - NYTPhotosDataSource - -- (instancetype)initWithPhotos:(NSArray *)photos { - self = [super init]; - - if (self) { - _photos = photos; - } - - return self; -} - -#pragma mark - NSFastEnumeration - -- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)length { - return [self.photos countByEnumeratingWithState:state objects:buffer count:length]; -} - -#pragma mark - NYTPhotosViewControllerDataSource - -- (NSUInteger)numberOfPhotos { - return self.photos.count; -} - -- (id )photoAtIndex:(NSUInteger)photoIndex { - if (photoIndex < self.photos.count) { - return self.photos[photoIndex]; - } - - return nil; -} - -- (NSUInteger)indexOfPhoto:(id )photo { - return [self.photos indexOfObject:photo]; -} - -- (BOOL)containsPhoto:(id )photo { - return [self.photos containsObject:photo]; -} - -- (id )objectAtIndexedSubscript:(NSUInteger)photoIndex { - return [self photoAtIndex:photoIndex]; -} - -@end diff --git a/NYTPhotoViewer/NYTPhotosOverlayView.h b/NYTPhotoViewer/NYTPhotosOverlayView.h index cca8d9d4..f62c52dd 100644 --- a/NYTPhotoViewer/NYTPhotosOverlayView.h +++ b/NYTPhotoViewer/NYTPhotosOverlayView.h @@ -55,6 +55,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, nullable) UIView *captionView; +/** + * Whether the `captionView` should respect the safe area or not + */ +@property (nonatomic) BOOL captionViewRespectsSafeArea; + @end NS_ASSUME_NONNULL_END diff --git a/NYTPhotoViewer/NYTPhotosOverlayView.m b/NYTPhotoViewer/NYTPhotosOverlayView.m index fc001066..8ee2fb57 100644 --- a/NYTPhotoViewer/NYTPhotosOverlayView.m +++ b/NYTPhotoViewer/NYTPhotosOverlayView.m @@ -9,6 +9,12 @@ #import "NYTPhotosOverlayView.h" #import "NYTPhotoCaptionViewLayoutWidthHinting.h" +@interface UIView (NYTSafeArea) + +@property (nonatomic, readonly, strong) UILayoutGuide *safeAreaLayoutGuide; + +@end + @interface NYTPhotosOverlayView () @property (nonatomic) UINavigationItem *navigationItem; @@ -74,10 +80,17 @@ - (void)setupNavigationBar { [self addSubview:self.navigationBar]; - NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.navigationBar attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]; - NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:self.navigationBar attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]; - NSLayoutConstraint *horizontalPositionConstraint = [NSLayoutConstraint constraintWithItem:self.navigationBar attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]; - [self addConstraints:@[topConstraint, widthConstraint, horizontalPositionConstraint]]; + if ([self respondsToSelector:@selector(safeAreaLayoutGuide)]) { + NSLayoutConstraint *topConstraint = [self.navigationBar.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor]; + NSLayoutConstraint *leftConstraint = [self.navigationBar.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor]; + NSLayoutConstraint *rightConstraint = [self.navigationBar.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor]; + [self addConstraints:@[topConstraint, leftConstraint, rightConstraint]]; + } else { + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.navigationBar attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]; + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:self.navigationBar attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]; + NSLayoutConstraint *horizontalPositionConstraint = [NSLayoutConstraint constraintWithItem:self.navigationBar attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]; + [self addConstraints:@[topConstraint, widthConstraint, horizontalPositionConstraint]]; + } } - (void)setCaptionView:(UIView *)captionView { @@ -91,11 +104,18 @@ - (void)setCaptionView:(UIView *)captionView { self.captionView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:self.captionView]; - - NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self.captionView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]; - NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:self.captionView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]; - NSLayoutConstraint *horizontalPositionConstraint = [NSLayoutConstraint constraintWithItem:self.captionView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]; - [self addConstraints:@[bottomConstraint, widthConstraint, horizontalPositionConstraint]]; + + if ([self respondsToSelector:@selector(safeAreaLayoutGuide)] && self.captionViewRespectsSafeArea) { + NSLayoutConstraint *bottomConstraint = [self.captionView.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor]; + NSLayoutConstraint *leftConstraint = [self.captionView.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor]; + NSLayoutConstraint *rightConstraint = [self.captionView.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor]; + [self addConstraints:@[bottomConstraint, leftConstraint, rightConstraint]]; + } else { + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self.captionView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]; + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:self.captionView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]; + NSLayoutConstraint *horizontalPositionConstraint = [NSLayoutConstraint constraintWithItem:self.captionView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]; + [self addConstraints:@[bottomConstraint, widthConstraint, horizontalPositionConstraint]]; + } } - (UIBarButtonItem *)leftBarButtonItem { diff --git a/NYTPhotoViewer/NYTPhotosViewController.h b/NYTPhotoViewer/NYTPhotosViewController.h index f67a9d6a..be8a9845 100644 --- a/NYTPhotoViewer/NYTPhotosViewController.h +++ b/NYTPhotoViewer/NYTPhotosViewController.h @@ -12,6 +12,7 @@ @protocol NYTPhoto; @protocol NYTPhotosViewControllerDelegate; +@protocol NYTPhotoViewerDataSource; NS_ASSUME_NONNULL_BEGIN @@ -53,8 +54,17 @@ extern NSString * const NYTPhotosViewControllerDidDismissNotification; */ @property (nonatomic, readonly, nullable) UIPageViewController *pageViewController; +/** + * The data source underlying this PhotosViewController. + * + * After setting a new data source, you must call `-reloadPhotosAnimated:`. + */ +@property (nonatomic, weak, nullable) id dataSource; + /** * The object conforming to `NYTPhoto` that is currently being displayed by the `pageViewController`. + * + * This photo will be one of the photos from the data source. */ @property (nonatomic, readonly, nullable) id currentlyDisplayedPhoto; @@ -85,40 +95,39 @@ extern NSString * const NYTPhotosViewControllerDidDismissNotification; /** * The object that acts as the delegate of the `NYTPhotosViewController`. - * - * @warning It is recommended that you pass a delegate to the designated initializer of this class; otherwise certain delegate methods may not be called for the initial photo displayed by this view controller. */ @property (nonatomic, weak, nullable) id delegate; /** - * A convenience initializer that calls `initWithPhotos:initialPhoto:delegate:`, passing the first photo as the `initialPhoto` argument, and `nil` as the `delegate` argument. + * Initializes a `PhotosViewController` with the given data source, initially displaying the first photo in the data source. * - * @param photos An array of objects conforming to the `NYTPhoto` protocol. + * @param dataSource The data source underlying this photo viewer. * - * @return A fully initialized object. + * @return A fully initialized `PhotosViewController` instance. */ -- (instancetype)initWithPhotos:(NSArray > * _Nullable)photos; +- (instancetype)initWithDataSource:(id )dataSource; /** - * A convenience initializer that calls `initWithPhotos:initialPhoto:delegate:`, passing `nil` as the `delegate` argument. + * Initializes a `PhotosViewController` with the given data source and delegate, initially displaying the photo at the given index in the data source. * - * @param photos An array of objects conforming to the `NYTPhoto` protocol. - * @param initialPhoto The photo to display initially. Must be contained within the `photos` array. If `nil` or not within the `photos` array, the first photo within the `photos` array will be displayed. + * @param dataSource The data source underlying this photo viewer. + * @param initialPhotoIndex The photo to display initially. If outside the bounds of the data source, the first photo from the data source will be displayed. + * @param delegate The delegate for this `NYTPhotosViewController`. * - * @return A fully initialized object. + * @return A fully initialized `PhotosViewController` instance. */ -- (instancetype)initWithPhotos:(NSArray > * _Nullable)photos initialPhoto:(id _Nullable)initialPhoto; +- (instancetype)initWithDataSource:(id )dataSource initialPhotoIndex:(NSInteger)initialPhotoIndex delegate:(nullable id )delegate; /** - * The designated initializer that stores the array of objects conforming to the `NYTPhoto` protocol for display, along with specifying an initial photo for display. + * Initializes a `PhotosViewController` with the given data source and delegate, initially displaying the given photo. * - * @param photos An array of objects conforming to the `NYTPhoto` protocol. - * @param initialPhoto The photo to display initially. Must be contained within the `photos` array. If `nil` or not within the `photos` array, the first photo within the `photos` array will be displayed. - * @param delegate The delegate for this `NYTPhotosViewController`. + * @param dataSource The data source underlying this photo viewer. + * @param initialPhoto The photo to display initially. Must be a member of the data source. If `nil` or not a member of the data source, the first photo from the data source will be displayed. + * @param delegate The delegate for this `NYTPhotosViewController`. * - * @return A fully initialized object. + * @return A fully initialized `PhotosViewController` instance. */ -- (instancetype)initWithPhotos:(NSArray > * _Nullable)photos initialPhoto:(id _Nullable)initialPhoto delegate:(nullable id )delegate NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithDataSource:(id )dataSource initialPhoto:(nullable id )initialPhoto delegate:(nullable id )delegate NS_DESIGNATED_INITIALIZER; /** * Displays the specified photo. Can be called before the view controller is displayed. Calling with a photo not contained within the data source has no effect. @@ -126,19 +135,41 @@ extern NSString * const NYTPhotosViewControllerDidDismissNotification; * @param photo The photo to make the currently displayed photo. * @param animated Whether to animate the transition to the new photo. */ -- (void)displayPhoto:(id _Nullable)photo animated:(BOOL)animated; +- (void)displayPhoto:(nullable id )photo animated:(BOOL)animated; + +/** + * Informs the photo viewer that the photo in the data source at this index has changed. + * + * In response, the photo viewer will retrieve and update the overlay information and the photo itself. + * + * This method has no effect if the given index is out of bounds in the data source. + * + * @param photo The index of the photo which changed in the data source. + */ +- (void)updatePhotoAtIndex:(NSInteger)photoIndex; + +/** + * Informs the photo viewer that the given photo in the data source has changed. + * + * In response, the photo viewer will retrieve and update the overlay information and the photo itself. + * + * This method has no effect if the photo doesn't exist in the data source. + * + * @param photo The photo which changed in the data source. + */ +- (void)updatePhoto:(id)photo; /** - * Update the image displayed for the given photo object. + * Tells the photo viewer to reload all data from its data source. * - * @param photo The photo for which to display the new image. + * @param animated Whether any resulting transitions should be animated. */ -- (void)updateImageForPhoto:(id _Nullable)photo; +- (void)reloadPhotosAnimated:(BOOL)animated; @end /** - * A protocol of entirely optional methods called for configuration and lifecycle events by an `NYTPhotosViewController` instance. + * A protocol of entirely optional methods called for view-related configuration and lifecycle events by an `NYTPhotosViewController` instance. */ @protocol NYTPhotosViewControllerDelegate @@ -169,7 +200,11 @@ extern NSString * const NYTPhotosViewControllerDidDismissNotification; - (void)photosViewControllerDidDismiss:(NYTPhotosViewController *)photosViewController; /** - * Returns a view to display over a photo, full width, locked to the bottom, representing the caption for the photo. Can be any `UIView` object, but is expected to respond to `intrinsicContentSize` appropriately to calculate height. + * Returns a view to display over a photo, full width, locked to the bottom, representing the caption for the photo. + * + * Can be any `UIView` object, but the view returned is expected to respond to `intrinsicContentSize` appropriately to calculate height. + * + * @note Your implementation can get caption information from the appropriate properties on the given `NYTPhoto`. * * @param photosViewController The `NYTPhotosViewController` instance that sent the delegate message. * @param photo The photo object over which to display the caption view. @@ -178,22 +213,34 @@ extern NSString * const NYTPhotosViewControllerDidDismissNotification; */ - (UIView * _Nullable)photosViewController:(NYTPhotosViewController *)photosViewController captionViewForPhoto:(id )photo; +/** + * Returns whether the caption view should respect the safe area. + * + * @note If this method is not implemented it will default to `YES`. + * + * @param photosViewController The `NYTPhotosViewController` instance that sent the delegate message. + * @param photo The photo object over which to display the caption view. + * + * @return A `BOOL` indicating whether the caption view should respect the safe area for the given photo or not. + */ +- (BOOL)photosViewController:(NYTPhotosViewController *)photosViewController captionViewRespectsSafeAreaForPhoto:(id )photo; + /** * Returns a string to display as the title in the navigation-bar area for a photo. * - * This small area of the screen is not intended to display a caption or similar information about the photo itself. (NYTPhotoViewer is designed to provide this information in the caption view, and as such the `NYTPhoto` protocol provides properties for the title, summary, and credit for each photo.) Instead, consider using this delegate method to customize how your app displays the user's progress through a set of photos. + * This small area of the screen is not intended to display a caption or similar information about the photo itself. (NYTPhotoViewer is designed to provide this information in the caption view, and as such the `NYTPhoto` protocol provides properties for a title, summary, and credit for each photo.) Instead, consider using this delegate method to customize how your app displays the user's progress through a set of photos. * * @param photosViewController The `NYTPhotosViewController` instance that sent the delegate message. * @param photo The photo object for which to display the title. * @param photoIndex The index of the photo. - * @param totalPhotoCount The number of photos being displayed by the photo viewer. + * @param totalPhotoCount The number of photos being displayed by the photo viewer, or `nil` if the total number of photos is not known. The given number packages an `NSInteger`. * * @return The text to display as the navigation-item title for the given photo. Return `nil` to show a default title like "1 of 4" indicating progress in a slideshow, or an empty string to hide this text entirely. */ -- (NSString * _Nullable)photosViewController:(NYTPhotosViewController *)photosViewController titleForPhoto:(id )photo atIndex:(NSUInteger)photoIndex totalPhotoCount:(NSUInteger)totalPhotoCount; +- (NSString * _Nullable)photosViewController:(NYTPhotosViewController *)photosViewController titleForPhoto:(id )photo atIndex:(NSInteger)photoIndex totalPhotoCount:(nullable NSNumber *)totalPhotoCount; /** - * Returns a view to display while a photo is loading. Can be any `UIView` object, but is expected to respond to `sizeToFit` appropriately. This view will be sized and centered in the blank area, and hidden when the photo image is loaded. + * Returns a view to display while a photo is loading. Can be any `UIView` object, but is expected to respond to `sizeToFit` appropriately. This view will be sized and centered in the blank area, and hidden when the photo image or its placeholder is loaded. * * @param photosViewController The `NYTPhotosViewController` instance that sent the delegate message. * @param photo The photo object over which to display the activity view. diff --git a/NYTPhotoViewer/NYTPhotosViewController.m b/NYTPhotoViewer/NYTPhotosViewController.m index d66060cc..79e3281b 100644 --- a/NYTPhotoViewer/NYTPhotosViewController.m +++ b/NYTPhotoViewer/NYTPhotosViewController.m @@ -7,8 +7,8 @@ // #import "NYTPhotosViewController.h" -#import "NYTPhotosViewControllerDataSource.h" -#import "NYTPhotosDataSource.h" +#import "NYTPhotoViewerDataSource.h" +#import "NYTPhotoViewerArrayDataSource.h" #import "NYTPhotoViewController.h" #import "NYTPhotoTransitionController.h" #import "NYTScalingImageView.h" @@ -33,7 +33,6 @@ @interface NYTPhotosViewController () dataSource; @property (nonatomic) UIPageViewController *pageViewController; @property (nonatomic) NYTPhotoTransitionController *transitionController; @property (nonatomic) UIPopoverController *activityPopoverController; @@ -53,6 +52,8 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; @property (nonatomic, readonly) UIView *referenceViewForCurrentPhoto; @property (nonatomic, readonly) CGPoint boundsCenterPoint; +@property (nonatomic, nullable) id initialPhoto; + @end @implementation NYTPhotosViewController @@ -87,14 +88,14 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { #pragma mark - UIViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - return [self initWithPhotos:nil]; + return [self initWithDataSource:[NYTPhotoViewerArrayDataSource dataSourceWithPhotos:@[]] initialPhoto:nil delegate:nil]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { - [self commonInitWithPhotos:nil initialPhoto:nil delegate:nil]; + [self commonInitWithDataSource:[NYTPhotoViewerArrayDataSource dataSourceWithPhotos:@[]] initialPhoto:nil delegate:nil]; } return self; @@ -103,6 +104,8 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { - (void)viewDidLoad { [super viewDidLoad]; + [self configurePageViewControllerWithInitialPhoto]; + self.view.tintColor = [UIColor whiteColor]; self.view.backgroundColor = [UIColor blackColor]; self.pageViewController.view.backgroundColor = [UIColor clearColor]; @@ -145,6 +148,10 @@ - (BOOL)prefersStatusBarHidden { return YES; } +- (BOOL)prefersHomeIndicatorAutoHidden { + return YES; +} + - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation { return UIStatusBarAnimationFade; } @@ -155,27 +162,30 @@ - (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void)) #pragma mark - NYTPhotosViewController -- (instancetype)initWithPhotos:(NSArray *)photos { - return [self initWithPhotos:photos initialPhoto:photos.firstObject delegate:nil]; +- (instancetype)initWithDataSource:(id )dataSource { + return [self initWithDataSource:dataSource initialPhoto:nil delegate:nil]; } -- (instancetype)initWithPhotos:(NSArray *)photos initialPhoto:(id )initialPhoto { - return [self initWithPhotos:photos initialPhoto:initialPhoto delegate:nil]; +- (instancetype)initWithDataSource:(id )dataSource initialPhotoIndex:(NSInteger)initialPhotoIndex delegate:(nullable id )delegate { + id initialPhoto = [dataSource photoAtIndex:initialPhotoIndex]; + + return [self initWithDataSource:dataSource initialPhoto:initialPhoto delegate:delegate]; } -- (instancetype)initWithPhotos:(NSArray *)photos initialPhoto:(id )initialPhoto delegate:(id)delegate { +- (instancetype)initWithDataSource:(id )dataSource initialPhoto:(id _Nullable)initialPhoto delegate:(nullable id )delegate { self = [super initWithNibName:nil bundle:nil]; if (self) { - [self commonInitWithPhotos:photos initialPhoto:initialPhoto delegate:delegate]; + [self commonInitWithDataSource:dataSource initialPhoto:initialPhoto delegate:delegate]; } return self; } -- (void)commonInitWithPhotos:(NSArray *)photos initialPhoto:(id )initialPhoto delegate:(id)delegate { - _dataSource = [[NYTPhotosDataSource alloc] initWithPhotos:photos]; +- (void)commonInitWithDataSource:(id )dataSource initialPhoto:(id _Nullable)initialPhoto delegate:(nullable id )delegate { + _dataSource = dataSource; _delegate = delegate; + _initialPhoto = initialPhoto; _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPanWithGestureRecognizer:)]; _singleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didSingleTapWithGestureRecognizer:)]; @@ -185,31 +195,32 @@ - (void)commonInitWithPhotos:(NSArray *)photos initialPhoto:(id )initi self.transitioningDelegate = _transitionController; self.modalPresentationCapturesStatusBarAppearance = YES; - _overlayView = [[NYTPhotosOverlayView alloc] initWithFrame:CGRectZero]; - _overlayView.leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"NYTPhotoViewerCloseButtonX" inBundle:[NSBundle nyt_photoViewerResourceBundle] compatibleWithTraitCollection:nil] landscapeImagePhone:[UIImage imageNamed:@"NYTPhotoViewerCloseButtonXLandscape" inBundle:[NSBundle nyt_photoViewerResourceBundle] compatibleWithTraitCollection:nil] style:UIBarButtonItemStylePlain target:self action:@selector(doneButtonTapped:)]; - _overlayView.leftBarButtonItem.imageInsets = NYTPhotosViewControllerCloseButtonImageInsets; - _overlayView.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(actionButtonTapped:)]; + _overlayView = ({ + NYTPhotosOverlayView *v = [[NYTPhotosOverlayView alloc] initWithFrame:CGRectZero]; + v.leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"NYTPhotoViewerCloseButtonX" inBundle:[NSBundle nyt_photoViewerResourceBundle] compatibleWithTraitCollection:nil] landscapeImagePhone:[UIImage imageNamed:@"NYTPhotoViewerCloseButtonXLandscape" inBundle:[NSBundle nyt_photoViewerResourceBundle] compatibleWithTraitCollection:nil] style:UIBarButtonItemStylePlain target:self action:@selector(doneButtonTapped:)]; + v.leftBarButtonItem.imageInsets = NYTPhotosViewControllerCloseButtonImageInsets; + v.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(actionButtonTapped:)]; + v; + }); - _notificationCenter = [[NSNotificationCenter alloc] init]; + _notificationCenter = [NSNotificationCenter new]; - [self setupPageViewControllerWithInitialPhoto:initialPhoto]; -} - -- (void)setupPageViewControllerWithInitialPhoto:(id )initialPhoto { self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:@{UIPageViewControllerOptionInterPageSpacingKey: @(NYTPhotosViewControllerInterPhotoSpacing)}]; - + self.pageViewController.delegate = self; self.pageViewController.dataSource = self; - +} + +- (void)configurePageViewControllerWithInitialPhoto { NYTPhotoViewController *initialPhotoViewController; - - if ([self.dataSource containsPhoto:initialPhoto]) { - initialPhotoViewController = [self newPhotoViewControllerForPhoto:initialPhoto]; + + if (self.initialPhoto != nil && [self.dataSource indexOfPhoto:self.initialPhoto] != NSNotFound) { + initialPhotoViewController = [self newPhotoViewControllerForPhoto:self.initialPhoto]; } else { - initialPhotoViewController = [self newPhotoViewControllerForPhoto:self.dataSource[0]]; + initialPhotoViewController = [self newPhotoViewControllerForPhoto:[self.dataSource photoAtIndex:0]]; } - + [self setCurrentlyDisplayedViewController:initialPhotoViewController animated:NO]; } @@ -228,21 +239,19 @@ - (void)addOverlayView { - (void)updateOverlayInformation { NSString *overlayTitle; - NSUInteger photoIndex = [self.dataSource indexOfPhoto:self.currentlyDisplayedPhoto]; + NSInteger displayIndex = photoIndex + 1; if ([self.delegate respondsToSelector:@selector(photosViewController:titleForPhoto:atIndex:totalPhotoCount:)]) { overlayTitle = [self.delegate photosViewController:self titleForPhoto:self.currentlyDisplayedPhoto atIndex:photoIndex totalPhotoCount:self.dataSource.numberOfPhotos]; } - - if (!overlayTitle && self.dataSource.numberOfPhotos > 1) { - NSUInteger displayIndex = 1; - - if (photoIndex < self.dataSource.numberOfPhotos) { - displayIndex = photoIndex + 1; - } - overlayTitle = [NSString localizedStringWithFormat:NSLocalizedString(@"%lu of %lu", nil), (unsigned long)displayIndex, (unsigned long)self.dataSource.numberOfPhotos]; + if (!overlayTitle && self.dataSource.numberOfPhotos == nil) { + overlayTitle = [NSString localizedStringWithFormat:@"%lu", (unsigned long)displayIndex]; + } + + if (!overlayTitle && self.dataSource.numberOfPhotos.integerValue > 1) { + overlayTitle = [NSString localizedStringWithFormat:NSLocalizedString(@"%lu of %lu", nil), (unsigned long)displayIndex, (unsigned long)self.dataSource.numberOfPhotos.integerValue]; } self.overlayView.title = overlayTitle; @@ -255,7 +264,13 @@ - (void)updateOverlayInformation { if (!captionView) { captionView = [[NYTPhotoCaptionView alloc] initWithAttributedTitle:self.currentlyDisplayedPhoto.attributedCaptionTitle attributedSummary:self.currentlyDisplayedPhoto.attributedCaptionSummary attributedCredit:self.currentlyDisplayedPhoto.attributedCaptionCredit]; } - + + BOOL captionViewRespectsSafeArea = YES; + if ([self.delegate respondsToSelector:@selector(photosViewController:captionViewRespectsSafeAreaForPhoto:)]) { + captionViewRespectsSafeArea = [self.delegate photosViewController:self captionViewRespectsSafeAreaForPhoto:self.currentlyDisplayedPhoto]; + } + + self.overlayView.captionViewRespectsSafeArea = captionViewRespectsSafeArea; self.overlayView.captionView = captionView; } @@ -328,7 +343,7 @@ - (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems { } - (void)displayPhoto:(id )photo animated:(BOOL)animated { - if (![self.dataSource containsPhoto:photo]) { + if ([self.dataSource indexOfPhoto:photo] == NSNotFound) { return; } @@ -337,8 +352,41 @@ - (void)displayPhoto:(id )photo animated:(BOOL)animated { [self updateOverlayInformation]; } -- (void)updateImageForPhoto:(id )photo { +- (void)updatePhotoAtIndex:(NSInteger)photoIndex { + id photo = [self.dataSource photoAtIndex:photoIndex]; + if (!photo) { + return; + } + + [self updatePhoto:photo]; +} + +- (void)updatePhoto:(id)photo { + if ([self.dataSource indexOfPhoto:photo] == NSNotFound) { + return; + } + [self.notificationCenter postNotificationName:NYTPhotoViewControllerPhotoImageUpdatedNotification object:photo]; + + if ([self.currentlyDisplayedPhoto isEqual:photo]) { + [self updateOverlayInformation]; + } +} + +- (void)reloadPhotosAnimated:(BOOL)animated { + id newCurrentPhoto; + + if ([self.dataSource indexOfPhoto:self.currentlyDisplayedPhoto] != NSNotFound) { + newCurrentPhoto = self.currentlyDisplayedPhoto; + } else { + newCurrentPhoto = [self.dataSource photoAtIndex:0]; + } + + [self displayPhoto:newCurrentPhoto animated:animated]; + + if (self.overlayView.hidden) { + [self setOverlayViewHidden:NO animated:animated]; + } } #pragma mark - Gesture Recognizers @@ -414,8 +462,16 @@ - (void)setCurrentlyDisplayedViewController:(UIViewController *)viewController { NSUInteger photoIndex = [self.dataSource indexOfPhoto:viewController.photo]; - return [self newPhotoViewControllerForPhoto:self.dataSource[photoIndex - 1]]; + if (photoIndex == 0 || photoIndex == NSNotFound) { + return nil; + } + + return [self newPhotoViewControllerForPhoto:[self.dataSource photoAtIndex:(photoIndex - 1)]]; } - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController { NSUInteger photoIndex = [self.dataSource indexOfPhoto:viewController.photo]; - return [self newPhotoViewControllerForPhoto:self.dataSource[photoIndex + 1]]; + if (photoIndex == NSNotFound) { + return nil; + } + + return [self newPhotoViewControllerForPhoto:[self.dataSource photoAtIndex:(photoIndex + 1)]]; } #pragma mark - UIPageViewControllerDelegate diff --git a/NYTPhotoViewer/Protocols/NYTPhoto.h b/NYTPhotoViewer/Protocols/NYTPhoto.h index 0541a3d7..83395f35 100644 --- a/NYTPhotoViewer/Protocols/NYTPhoto.h +++ b/NYTPhotoViewer/Protocols/NYTPhoto.h @@ -12,6 +12,8 @@ NS_ASSUME_NONNULL_BEGIN /** * The model for photos displayed in an `NYTPhotosViewController`. + * + * Your models (or boxes, if working with Swift value types) should override `isEqual:` to provide a concept of identity for the PhotoViewer to work with. */ @protocol NYTPhoto @@ -34,10 +36,12 @@ NS_ASSUME_NONNULL_BEGIN /** * A placeholder image for display while the image is loading. * - * This property is used if and only if `-imageData` returns `nil`. + * This property is used if and only if `-imageData` and `-image` return `nil`. */ @property (nonatomic, readonly, nullable) UIImage *placeholderImage; +#pragma mark Caption + /** * An attributed string for display as the title of the caption. */ diff --git a/NYTPhotoViewer/Protocols/NYTPhotoViewerDataSource.h b/NYTPhotoViewer/Protocols/NYTPhotoViewerDataSource.h new file mode 100644 index 00000000..6f6ad5b5 --- /dev/null +++ b/NYTPhotoViewer/Protocols/NYTPhotoViewerDataSource.h @@ -0,0 +1,51 @@ +// +// NYTPhotosViewControllerDataSource.h +// NYTPhotoViewer +// +// Created by Brian Capps on 2/10/15. +// Copyright (c) 2015 NYTimes. All rights reserved. +// + +@import UIKit; + +@protocol NYTPhoto; + +NS_ASSUME_NONNULL_BEGIN + +/** + * The data source for an `NYTPhotosViewController` instance. + * + * A view controller, view model, or model in your application could conform to this protocol, depending on what makes sense in your architecture. + * + * Alternatively, `NYTPhotoViewerArrayDataSource` and `NYTPhotoViewerSinglePhotoDataSource` are concrete classes which conveniently handle the most common use cases for NYTPhotoViewer. + */ +@protocol NYTPhotoViewerDataSource + +/** + * The total number of photos in the data source, or `nil` if the number is not known. + * + * The number returned should package an `NSInteger` value. + */ +@property (nonatomic, readonly, nullable) NSNumber *numberOfPhotos; + +/** + * Returns the index of a given photo, or `NSNotFound` if the photo is not in the data source. + * + * @param photo The photo against which to look for the index. + * + * @return The index of a given photo, or `NSNotFound` if the photo is not in the data source. + */ +- (NSInteger)indexOfPhoto:(id )photo; + +/** + * Returns the photo object at a specified index, or `nil` if one does not exist at that index. + * + * @param photoIndex The index of the desired photo. + * + * @return The photo object at a specified index, or `nil` if one does not exist at that index. + */ +- (nullable id )photoAtIndex:(NSInteger)photoIndex; + +@end + +NS_ASSUME_NONNULL_END diff --git a/NYTPhotoViewer/Protocols/NYTPhotosViewControllerDataSource.h b/NYTPhotoViewer/Protocols/NYTPhotosViewControllerDataSource.h deleted file mode 100644 index cf8a007b..00000000 --- a/NYTPhotoViewer/Protocols/NYTPhotosViewControllerDataSource.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// NYTPhotosViewControllerDataSource.h -// NYTPhotoViewer -// -// Created by Brian Capps on 2/10/15. -// Copyright (c) 2015 NYTimes. All rights reserved. -// - -@import UIKit; - -@protocol NYTPhoto; - -/** - * A protocol defining methods that must exist on a data source for an `NYTPhotosViewController`. - */ -@protocol NYTPhotosViewControllerDataSource - -/** - * The total number of photos in the data source. - */ -@property (nonatomic, readonly) NSUInteger numberOfPhotos; - -/** - * Returns the photo object at a specified index, or `nil` if one does not exist at that index. - * - * @param photoIndex The index of the desired photo. - * - * @return The photo object at a specified index, or `nil` if one does not exist at that index. - */ -- (id )photoAtIndex:(NSUInteger)photoIndex; - -/** - * Returns the index of a given photo, or `NSNotFound` if the photo is ot in the data source. - * - * @param photo The photo against which to look for the index. - * - * @return The index of a given photo, or `NSNotFound` if the photo is ot in the data source. - */ -- (NSUInteger)indexOfPhoto:(id )photo; - -/** - * Returns a `BOOL` representing whether the data source contains the passed-in photo. - * - * @param photo The photo to check existence of in the data source. - * - * @return A `BOOL` representing whether the data source contains the passed-in photo. - */ -- (BOOL)containsPhoto:(id )photo; - -/** - * Subscripting support. For example, `dataSource[0]` will be a valid way to obtain the photo at index 0. - * @note Indexes outside the range of the data source are expected to return `nil` and not to crash. - * - * @param photoIndex The index of the photo. - * - * @return The photo at the index, or `nil` if there is none. - */ -- (id )objectAtIndexedSubscript:(NSUInteger)photoIndex; - -@end diff --git a/README.md b/README.md index f1087ae3..3e8a4116 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Platform](http://cocoapod-badges.herokuapp.com/p/NYTPhotoViewer/badge.png)](http://cocoadocs.org/docsets/NYTPhotoViewer) [![Version](http://cocoapod-badges.herokuapp.com/v/NYTPhotoViewer/badge.png)](http://cocoadocs.org/docsets/NYTPhotoViewer) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) NYTPhotoViewer is a slideshow and image viewer that includes double-tap to zoom, captions, support for multiple images, interactive flick to dismiss, animated zooming presentation, and more. @@ -16,6 +17,12 @@ NYTPhotosViewController *photosViewController = [[NYTPhotosViewController alloc] [self presentViewController:photosViewController animated:YES completion:nil]; ``` +## Running the Example + +The Example project uses [Carthage](https://github.com/Carthage/Carthage) to integrate its dependencies. If you don’t have Carthge installed, you can install it via [Homebrew](http://brew.sh) with `brew install carthage`. + +Then, in your checkout of the `NYTPhotoViewer` repo, run `carthage checkout --use-submodules`. + ## Installation ### Carthage @@ -35,7 +42,7 @@ If you don't want support for animated GIFs, you may instead link against only t NYTPhotoViewer is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your `Podfile`: ``` -pod 'NYTPhotoViewer', '~> 1.1.0' +pod 'NYTPhotoViewer' ``` ## Requirements @@ -52,7 +59,7 @@ Please **open pull requests against the `develop` branch**, and add a relevant n ## Swift -NYTPhotoViewer is written in Objective-C but is intended to be fully interoperable with Swift. If you experience any interoperability difficulties, please open an issue or pull request and we will work to resolve it. +NYTPhotoViewer is written in Objective-C but is fully interoperable with Swift. If you experience any interoperability difficulties, please open an issue or pull request and we will work to resolve it. ## Inspiration