diff --git a/Agrume/Agrume.h b/Agrume/Agrume.h index 4d43028..4296fca 100644 --- a/Agrume/Agrume.h +++ b/Agrume/Agrume.h @@ -2,9 +2,6 @@ // Agrume.h // Agrume // -// Created by Jan Gorman on 23/05/15. -// Copyright (c) 2015 Schnaub. All rights reserved. -// #import diff --git a/Agrume/Agrume.swift b/Agrume/Agrume.swift index 668bea9..fc5e5f4 100644 --- a/Agrume/Agrume.swift +++ b/Agrume/Agrume.swift @@ -2,9 +2,6 @@ // Agrume.swift // Agrume // -// Created by Jan Gorman on 23/05/15. -// Copyright (c) 2015 Schnaub. All rights reserved. -// import UIKit @@ -14,15 +11,7 @@ public class Agrume: UIViewController { private static let MaxScalingForExpandingOffscreen: CGFloat = 1.25 private static let ReuseIdentifier = "ReuseIdentifier" - - private var backgroundSnapshot: UIImage! - private var backgroundImageView: UIImageView! - private var blurView: UIVisualEffectView! - - private var collectionView: UICollectionView! - private var spinner: UIActivityIndicatorView! - private var downloadTask: NSURLSessionDataTask? - + private var images: [UIImage]! private var imageURLs: [NSURL]! private var startIndex: Int? @@ -30,33 +19,11 @@ public class Agrume: UIViewController { public var didDismiss: (() -> Void)? public var didScroll: ((index: Int) -> Void)? - - private init( - image: UIImage? = nil, - imageURL: NSURL? = nil, - images: [UIImage]? = nil, - imageURLs: [NSURL]? = nil, - startIndex: Int? = nil, - backgroundBlurStyle: UIBlurEffectStyle? = .Dark) { - self.images = images - if let image = image { - self.images = [image] - } - self.imageURLs = imageURLs - if let imageURL = imageURL { - self.imageURLs = [imageURL] - } - - self.startIndex = startIndex - self.backgroundBlurStyle = backgroundBlurStyle! - super.init(nibName: nil, bundle: nil) - } - public convenience init(image: UIImage, backgroundBlurStyle: UIBlurEffectStyle? = .Dark) { self.init(image: image, imageURL: nil, backgroundBlurStyle: backgroundBlurStyle) } - + public convenience init(imageURL: NSURL, backgroundBlurStyle: UIBlurEffectStyle? = .Dark) { self.init(image: nil, imageURL: imageURL, backgroundBlurStyle: backgroundBlurStyle) } @@ -69,45 +36,51 @@ public class Agrume: UIViewController { self.init(image: nil, imageURLs: imageURLs, startIndex: startIndex, backgroundBlurStyle: backgroundBlurStyle) } + private init(image: UIImage? = nil, imageURL: NSURL? = nil, images: [UIImage]? = nil, imageURLs: [NSURL]? = nil, + startIndex: Int? = nil, backgroundBlurStyle: UIBlurEffectStyle? = .Dark) { + self.images = images + if let image = image { + self.images = [image] + } + self.imageURLs = imageURLs + if let imageURL = imageURL { + self.imageURLs = [imageURL] + } + + self.startIndex = startIndex + self.backgroundBlurStyle = backgroundBlurStyle! + super.init(nibName: nil, bundle: nil) + + NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("orientationDidChange"), + name: UIDeviceOrientationDidChangeNotification, object: nil) + } + deinit { downloadTask?.cancel() - } - - func downloadImage(url: NSURL, completion: (image: UIImage?) -> Void) { - downloadTask = ImageDownloader.downloadImage(url) { - [weak self] image in - if let downloadedImage = image { - completion(image: downloadedImage) - self?.spinner.alpha = 0 - } - } + NSNotificationCenter.defaultCenter().removeObserver(self) } required public init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.autoresizingMask = .FlexibleHeight | .FlexibleWidth - - backgroundImageView = UIImageView(frame: view.bounds) - backgroundImageView.image = backgroundSnapshot - view.addSubview(backgroundImageView) - - blurView = UIVisualEffectView(effect: UIBlurEffect(style: backgroundBlurStyle)) - blurView.frame = view.bounds + + private var backgroundSnapshot: UIImage! + private var backgroundImageView: UIImageView! + private lazy var blurView: UIVisualEffectView = { + let blurView = UIVisualEffectView(effect: UIBlurEffect(style: self.backgroundBlurStyle)) + blurView.frame = self.view.bounds blurView.autoresizingMask = .FlexibleWidth | .FlexibleHeight - view.addSubview(blurView) - + return blurView + }() + + private lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 0 layout.minimumLineSpacing = 0 layout.scrollDirection = .Horizontal - layout.itemSize = view.bounds.size + layout.itemSize = self.view.bounds.size - collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout) collectionView.registerClass(AgrumeCell.self, forCellWithReuseIdentifier: Agrume.ReuseIdentifier) collectionView.dataSource = self collectionView.pagingEnabled = true @@ -115,24 +88,48 @@ public class Agrume: UIViewController { collectionView.delaysContentTouches = false collectionView.showsHorizontalScrollIndicator = false collectionView.delegate = self + return collectionView + }() + private lazy var spinner: UIActivityIndicatorView = { + let activityIndicatorStyle: UIActivityIndicatorViewStyle = self.backgroundBlurStyle == .Dark ? .WhiteLarge : .Gray + let spinner = UIActivityIndicatorView(activityIndicatorStyle: activityIndicatorStyle) + spinner.center = self.view.center + spinner.startAnimating() + spinner.alpha = 0 + return spinner + }() + private var downloadTask: NSURLSessionDataTask? + + override public func viewDidLoad() { + super.viewDidLoad() + + view.autoresizingMask = .FlexibleHeight | .FlexibleWidth + backgroundImageView = UIImageView(frame: view.bounds) + backgroundImageView.image = backgroundSnapshot + view.addSubview(backgroundImageView) + view.addSubview(blurView) view.addSubview(collectionView) if let index = startIndex { - collectionView.scrollToItemAtIndexPath(NSIndexPath(forRow: index, inSection: 0), atScrollPosition: .allZeros, animated: false) + collectionView.scrollToItemAtIndexPath(NSIndexPath(forRow: index, inSection: 0), atScrollPosition: .allZeros, + animated: false) } - - let activityIndicatorStyle: UIActivityIndicatorViewStyle = backgroundBlurStyle == .Dark ? .WhiteLarge : .Gray - spinner = UIActivityIndicatorView(activityIndicatorStyle: activityIndicatorStyle) - spinner.center = view.center - spinner.startAnimating() - spinner.alpha = 0 view.addSubview(spinner) } + private var lastUsedOrientation: UIInterfaceOrientation! + + public override func viewWillAppear(animated: Bool) { + lastUsedOrientation = UIApplication.sharedApplication().statusBarOrientation + } + + private var initialOrientation: UIInterfaceOrientation! + public func showFrom(viewController: UIViewController) { backgroundSnapshot = viewController.view.snapshot() view.userInteractionEnabled = false + initialOrientation = UIApplication.sharedApplication().statusBarOrientation viewController.presentViewController(self, animated: false) { self.collectionView.alpha = 0 @@ -160,6 +157,107 @@ public class Agrume: UIViewController { } +extension Agrume { + + // MARK: Rotation + + func orientationDidChange() { + let orientation = UIDevice.currentDevice().orientation + let landscapeToLandscape = UIDeviceOrientationIsLandscape(orientation) && UIInterfaceOrientationIsLandscape(lastUsedOrientation) + let portraitToPortrait = UIDeviceOrientationIsPortrait(orientation) && UIInterfaceOrientationIsLandscape(lastUsedOrientation) + if landscapeToLandscape || portraitToPortrait { + let newOrientation = UIInterfaceOrientation(rawValue: orientation.rawValue) + if newOrientation == lastUsedOrientation { + return + } + lastUsedOrientation = newOrientation! + UIView.animateWithDuration(0.6) { + [weak self] in + self?.updateLayoutsForCurrentOrientation() + } + } + } + + public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) { + coordinator.animateAlongsideTransition({ + [weak self] _ in + self?.updateLayoutsForCurrentOrientation() + }) { + [weak self] _ in + self?.lastUsedOrientation = UIApplication.sharedApplication().statusBarOrientation + } + } + + func updateLayoutsForCurrentOrientation() { + var transform = CGAffineTransformIdentity + if initialOrientation == .Portrait { + switch(UIApplication.sharedApplication().statusBarOrientation) { + case .LandscapeLeft: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2)) + case .LandscapeRight: + transform = CGAffineTransformMakeRotation(CGFloat(-M_PI_2)) + case .PortraitUpsideDown: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI)) + default: + break + } + } else if initialOrientation == .PortraitUpsideDown { + switch(UIApplication.sharedApplication().statusBarOrientation) { + case .LandscapeLeft: + transform = CGAffineTransformMakeRotation(CGFloat(-M_PI_2)) + case .LandscapeRight: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2)) + case .Portrait: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI)) + default: + break + } + } else if initialOrientation == .LandscapeLeft { + switch(UIApplication.sharedApplication().statusBarOrientation) { + case .LandscapeRight: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI)) + case .Portrait: + transform = CGAffineTransformMakeRotation(CGFloat(-M_PI_2)) + case .PortraitUpsideDown: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2)) + default: + break + } + } else if initialOrientation == .LandscapeRight { + switch(UIApplication.sharedApplication().statusBarOrientation) { + case .LandscapeLeft: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI)) + case .Portrait: + transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2)) + case .PortraitUpsideDown: + transform = CGAffineTransformMakeRotation(CGFloat(-M_PI_2)) + default: + break + } + } + + backgroundImageView.center = view.center + backgroundImageView.transform = CGAffineTransformConcat(transform, CGAffineTransformMakeScale(1, 1)) + + spinner.center = view.center + collectionView.frame = view.bounds + + let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout + layout.itemSize = view.bounds.size + layout.invalidateLayout() + // Apply update two runloops into the future + dispatch_async(dispatch_get_main_queue()) { + dispatch_async(dispatch_get_main_queue()) { + [unowned self] in + for visibleCell in self.collectionView.visibleCells() as! [AgrumeCell] { + visibleCell.updateScrollViewAndImageViewForCurrentMetrics() + } + } + } + } + +} + extension Agrume: UICollectionViewDataSource { // MARK: UICollectionViewDataSource @@ -189,6 +287,16 @@ extension Agrume: UICollectionViewDataSource { return cell } + func downloadImage(url: NSURL, completion: (image: UIImage?) -> Void) { + downloadTask = ImageDownloader.downloadImage(url) { + [weak self] image in + if let downloadedImage = image { + completion(image: downloadedImage) + self?.spinner.alpha = 0 + } + } + } + func dismissAfterFlick() -> (() -> Void) { return { [weak self] in diff --git a/Agrume/AgrumeCell.swift b/Agrume/AgrumeCell.swift index 5d0c259..c7477aa 100644 --- a/Agrume/AgrumeCell.swift +++ b/Agrume/AgrumeCell.swift @@ -2,9 +2,6 @@ // AgrumeCell.swift // Agrume // -// Created by Jan Gorman on 30/05/15. -// Copyright (c) 2015 Schnaub. All rights reserved. -// import UIKit @@ -14,8 +11,25 @@ class AgrumeCell: UICollectionViewCell { private static let MinFlickDismissalVelocity: CGFloat = 800 private static let HighScrollVelocity: CGFloat = 1600 - private var scrollView: UIScrollView! - private var imageView: UIImageView! + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView(frame: self.contentView.bounds) + scrollView.delegate = self + scrollView.zoomScale = 1 + scrollView.maximumZoomScale = 8 + scrollView.scrollEnabled = false + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + return scrollView + }() + private lazy var imageView: UIImageView = { + let imageView = UIImageView(frame: self.contentView.bounds) + imageView.contentMode = .ScaleAspectFit + imageView.userInteractionEnabled = true + imageView.clipsToBounds = true + imageView.layer.allowsEdgeAntialiasing = true + return imageView + }() + private var animator: UIDynamicAnimator! var image: UIImage? { didSet { @@ -30,22 +44,8 @@ class AgrumeCell: UICollectionViewCell { super.init(frame: frame) backgroundColor = UIColor.clearColor() - - scrollView = UIScrollView(frame: contentView.bounds) - scrollView.delegate = self - scrollView.zoomScale = 1 - scrollView.maximumZoomScale = 8 - scrollView.scrollEnabled = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.showsVerticalScrollIndicator = false + contentView.addSubview(scrollView) - - imageView = UIImageView(frame: contentView.bounds) - imageView.contentMode = .ScaleAspectFit - imageView.userInteractionEnabled = true - imageView.clipsToBounds = true - imageView.layer.allowsEdgeAntialiasing = true - imageView.image = image scrollView.addSubview(imageView) setupGestureRecognizers() @@ -62,39 +62,41 @@ class AgrumeCell: UICollectionViewCell { updateScrollViewAndImageViewForCurrentMetrics() } - private var singleTapGesture: UITapGestureRecognizer! - private var doubleTapGesture: UITapGestureRecognizer! - private var panGesture: UIPanGestureRecognizer! - var swipeGesture: UISwipeGestureRecognizer! + private lazy var singleTapGesture: UITapGestureRecognizer = { + let singleTapGesture = UITapGestureRecognizer(target: self, action: Selector("singleTap:")) + singleTapGesture.requireGestureRecognizerToFail(self.doubleTapGesture) + singleTapGesture.delegate = self + return singleTapGesture + }() + private lazy var doubleTapGesture: UITapGestureRecognizer = { + let doubleTapGesture = UITapGestureRecognizer(target: self, action: Selector("doubleTap:")) + doubleTapGesture.numberOfTapsRequired = 2 + return doubleTapGesture + }() + private lazy var panGesture: UIPanGestureRecognizer = { + let panGesture = UIPanGestureRecognizer(target: self, action: Selector("dismissPan:")) + panGesture.maximumNumberOfTouches = 1 + panGesture.delegate = self + return panGesture + }() + lazy var swipeGesture: UISwipeGestureRecognizer = { + let swipeGesture = UISwipeGestureRecognizer(target: self, action: nil) + swipeGesture.direction = .Left | .Right + swipeGesture.delegate = self + return swipeGesture + }() private var flickedToDismiss: Bool = false private var isDraggingImage: Bool = false private var imageDragStartingPoint: CGPoint! private var imageDragOffsetFromActualTranslation: UIOffset! private var imageDragOffsetFromImageCenter: UIOffset! - private var animator: UIDynamicAnimator! private var attachmentBehavior: UIAttachmentBehavior? private func setupGestureRecognizers() { - doubleTapGesture = UITapGestureRecognizer(target: self, action: Selector("doubleTap:")) - doubleTapGesture.numberOfTapsRequired = 2 - contentView.addGestureRecognizer(doubleTapGesture) - - singleTapGesture = UITapGestureRecognizer(target: self, action: Selector("singleTap:")) - singleTapGesture.requireGestureRecognizerToFail(doubleTapGesture) - singleTapGesture.delegate = self - contentView.addGestureRecognizer(singleTapGesture) - - panGesture = UIPanGestureRecognizer(target: self, action: Selector("dismissPan:")) - panGesture.maximumNumberOfTouches = 1 - panGesture.delegate = self + contentView.addGestureRecognizer(doubleTapGesture) scrollView.addGestureRecognizer(panGesture) - - swipeGesture = UISwipeGestureRecognizer(target: self, action: nil) - swipeGesture.direction = .Left | .Right - swipeGesture.delegate = self - contentView.addGestureRecognizer(swipeGesture) } @@ -104,13 +106,17 @@ extension AgrumeCell: UIGestureRecognizerDelegate { // MARK: UIGestureRecognizerDelegate + func notZoomed() -> Bool { + return scrollView.zoomScale == 1 + } + override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { - if let pan = gestureRecognizer as? UIPanGestureRecognizer where scrollView.zoomScale == 1 { + if let pan = gestureRecognizer as? UIPanGestureRecognizer where notZoomed() { let velocity = pan.velocityInView(scrollView) return abs(velocity.y) > abs(velocity.x) - } else if let swipe = gestureRecognizer as? UISwipeGestureRecognizer where scrollView.zoomScale == 1 { + } else if let swipe = gestureRecognizer as? UISwipeGestureRecognizer where notZoomed() { return false - } else if let tap = gestureRecognizer as? UITapGestureRecognizer where tap == singleTapGesture && scrollView.zoomScale != 1 { + } else if let tap = gestureRecognizer as? UITapGestureRecognizer where tap == singleTapGesture && !notZoomed() { return false } return true @@ -118,18 +124,16 @@ extension AgrumeCell: UIGestureRecognizerDelegate { func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool { if let pan = gestureRecognizer as? UIPanGestureRecognizer { - return scrollView.zoomScale == 1 + return notZoomed() } return true } func doubleTap(sender: UITapGestureRecognizer) { - let rawLocation = sender.locationInView(sender.view) - let point = scrollView.convertPoint(rawLocation, fromView: sender.view) - + let point = scrollView.convertPoint(sender.locationInView(sender.view), fromView: sender.view) let targetZoom: CGRect let targetInsets: UIEdgeInsets - if scrollView.zoomScale == 1 { + if notZoomed() { let zoomWidth = CGRectGetWidth(contentView.bounds) / AgrumeCell.TargetZoomForDoubleTap let zoomHeight = CGRectGetHeight(contentView.bounds) / AgrumeCell.TargetZoomForDoubleTap targetZoom = CGRect(x: point.x - zoomWidth / 2, y: point.y / zoomWidth / 2, width: zoomWidth, height: zoomHeight) @@ -294,15 +298,13 @@ extension AgrumeCell: UIGestureRecognizerDelegate { } } - private func updateScrollViewAndImageViewForCurrentMetrics() { - let supressAdjustments = false - if !supressAdjustments { - if let image = self.imageView.image { - imageView.frame = resizedFrameForSize(image.size) - } - scrollView.contentSize = imageView.frame.size - scrollView.contentInset = contentInsetForScrollView(atScale: scrollView.zoomScale) + func updateScrollViewAndImageViewForCurrentMetrics() { + scrollView.frame = contentView.bounds + if let image = self.imageView.image { + imageView.frame = resizedFrameForSize(image.size) } + scrollView.contentSize = imageView.frame.size + scrollView.contentInset = contentInsetForScrollView(atScale: scrollView.zoomScale) } private func resizedFrameForSize(imageSize: CGSize) -> CGRect { @@ -363,6 +365,7 @@ extension AgrumeCell: UIGestureRecognizerDelegate { private func appropriateValue(#defaultValue: CGFloat) -> CGFloat { let screenWidth = CGRectGetWidth(UIScreen.mainScreen().bounds) let screenHeight = CGRectGetHeight(UIScreen.mainScreen().bounds) + // Default value that works well for the screenSize adjusted for the actual size of the device return defaultValue * ((320 * 480) / (screenWidth * screenHeight)) } @@ -398,7 +401,7 @@ extension AgrumeCell: UIScrollViewDelegate { func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) { let highVelocity = AgrumeCell.HighScrollVelocity let velocity = scrollView.panGestureRecognizer.velocityInView(scrollView.panGestureRecognizer.view) - if scrollView.zoomScale == 1 && (fabs(velocity.x) > highVelocity || fabs(velocity.y) > highVelocity) { + if notZoomed() && (fabs(velocity.x) > highVelocity || fabs(velocity.y) > highVelocity) { dismiss() } } diff --git a/Agrume/ImageDownloader.swift b/Agrume/ImageDownloader.swift index fb8d051..5596586 100644 --- a/Agrume/ImageDownloader.swift +++ b/Agrume/ImageDownloader.swift @@ -2,9 +2,6 @@ // ImageDownloader.swift // Agrume // -// Created by Jan Gorman on 28/05/15. -// Copyright (c) 2015 Schnaub. All rights reserved. -// import Foundation diff --git a/Agrume/UIViewExtensions.swift b/Agrume/UIViewExtensions.swift index f75dabb..842134f 100644 --- a/Agrume/UIViewExtensions.swift +++ b/Agrume/UIViewExtensions.swift @@ -2,9 +2,6 @@ // UIViewExtensions.swift // Agrume // -// Created by Jan Gorman on 28/05/15. -// Copyright (c) 2015 Schnaub. All rights reserved. -// import UIKit diff --git a/Example/Agrume Example/AppDelegate.swift b/Example/Agrume Example/AppDelegate.swift index 174ff54..fcc60ad 100644 --- a/Example/Agrume Example/AppDelegate.swift +++ b/Example/Agrume Example/AppDelegate.swift @@ -2,9 +2,6 @@ // AppDelegate.swift // Agrume Example // -// Created by Jan Gorman on 25/05/15. -// Copyright (c) 2015 Schnaub. All rights reserved. -// import UIKit @@ -13,34 +10,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - // Override point for customization after application launch. return true } - func applicationWillResignActive(application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(application: UIApplication) { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - }