Last active
January 4, 2019 20:52
-
-
Save ajjames/8dc0feaa9e54380eea8b to your computer and use it in GitHub Desktop.
A simple drop-in single image viewer with async image loading
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ImageViewerViewController.swift | |
| // PhotoViewer | |
| // | |
| // Created by Andrew James on 2/26/16. | |
| // Copyright © 2016 aj. All rights reserved. | |
| // | |
| import UIKit | |
| protocol ImageViewerViewControllerDelegate: class { | |
| func getImage(completion: (UIImage) -> Void) | |
| } | |
| class ImageViewerViewController: UIViewController { | |
| weak var delegate: ImageViewerViewControllerDelegate? | |
| var imageUrl: URL? | |
| var defaultImage: UIImage? | |
| private var scrollView: UIScrollView? | |
| private var doubleTapGestureRecognizer: UITapGestureRecognizer! | |
| private var imageView: UIImageView! | |
| private var initialImageSize: CGSize! | |
| private var initialZoomScale: CGFloat! | |
| private var activityIndicator: UIActivityIndicatorView! | |
| private var scrollDistanceToDismiss: CGFloat = 100 | |
| private var dragScrollViewToDismiss = true | |
| private var dragScrollViewToDismissIsReady = false | |
| private var scrollViewInitialOffset: CGFloat = 0 | |
| private var scrollViewLoaded = false | |
| override func viewDidLoad() { | |
| super.viewDidLoad() | |
| view.backgroundColor = .black | |
| navigationController?.hidesBarsOnTap = true | |
| scrollView?.contentInsetAdjustmentBehavior = .automatic | |
| setupActivityIndicator() | |
| loadImage() | |
| } | |
| override func viewWillDisappear(_ animated: Bool) { | |
| navigationController?.hidesBarsOnTap = false | |
| super.viewWillDisappear(animated) | |
| } | |
| override var prefersStatusBarHidden: Bool { | |
| return true | |
| } | |
| override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { | |
| if scrollView?.zoomScale == initialZoomScale { | |
| configureZoomScales(size) | |
| coordinator.animate(alongsideTransition: { _ in | |
| self.scrollView?.zoomScale = self.initialZoomScale | |
| }, completion: nil) | |
| } else { | |
| configureZoomScales(size) | |
| } | |
| super.viewWillTransition(to: size, with: coordinator) | |
| } | |
| override func viewDidLayoutSubviews() { | |
| activityIndicator.center = view.center | |
| } | |
| // MARK: - Actions | |
| @objc | |
| func didDoubleTap(_ recognizer: UITapGestureRecognizer) { | |
| if initialZoomScale == 1.0 { return } | |
| if scrollView?.zoomScale == initialZoomScale { | |
| scrollView?.zoom(to: CGRect(origin: recognizer.location(in: imageView), size: .zero), animated: true) | |
| } else { | |
| scrollView?.setZoomScale(initialZoomScale, animated: true) | |
| } | |
| } | |
| // MARK: - private | |
| private func setupActivityIndicator() { | |
| activityIndicator = UIActivityIndicatorView(style: .whiteLarge) | |
| activityIndicator.hidesWhenStopped = true | |
| view.addSubview(activityIndicator) | |
| } | |
| private func loadImage() { | |
| if let aDelegate = delegate { | |
| activityIndicator.startAnimating() | |
| aDelegate.getImage { image in | |
| self.imageView = UIImageView(image: image) | |
| self.finishSetup() | |
| } | |
| } else if let url = imageUrl { | |
| activityIndicator.startAnimating() | |
| UIApplication.shared.isNetworkActivityIndicatorVisible = true | |
| let urlSession = URLSession(configuration: .default) | |
| urlSession.dataTask(with: url, completionHandler: { (data, _, _) in | |
| DispatchQueue.main.async { | |
| UIApplication.shared.isNetworkActivityIndicatorVisible = false | |
| } | |
| if let imageData = data, | |
| let image = UIImage(data: imageData) { | |
| DispatchQueue.main.async { [unowned self] in | |
| self.imageView = UIImageView(image: image) | |
| self.finishSetup() | |
| } | |
| } else { | |
| DispatchQueue.main.async { [unowned self] in | |
| self.imageView = UIImageView(image: self.defaultImage) | |
| self.finishSetup() | |
| } | |
| } | |
| }).resume() | |
| } else { | |
| imageView = UIImageView(image: defaultImage) | |
| finishSetup() | |
| } | |
| } | |
| private func finishSetup() { | |
| setupScrollView() | |
| configureZoomScales(scrollView!.frame.size) | |
| centerScrollViewOn(scrollView!.frame.size) | |
| scrollView?.delegate = self | |
| scrollView?.setZoomScale(initialZoomScale, animated: false) | |
| activityIndicator.stopAnimating() | |
| scrollView?.alwaysBounceHorizontal = true | |
| scrollView?.alwaysBounceVertical = true | |
| } | |
| private func setupScrollView() { | |
| scrollView = UIScrollView(frame: view.frame) | |
| scrollView!.autoresizingMask = [.flexibleWidth, .flexibleHeight] | |
| scrollView!.backgroundColor = .black | |
| view.insertSubview(scrollView!, belowSubview: activityIndicator) | |
| doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(_:))) | |
| doubleTapGestureRecognizer.numberOfTapsRequired = 2 | |
| if let barHideOnTapGestureRecognizer = navigationController?.barHideOnTapGestureRecognizer { | |
| barHideOnTapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) | |
| } | |
| scrollView!.addGestureRecognizer(doubleTapGestureRecognizer) | |
| initialImageSize = imageView.frame.size | |
| imageView.contentMode = .center | |
| scrollView!.addSubview(imageView) | |
| scrollView!.contentSize = imageView.frame.size | |
| } | |
| private func configureZoomScales(_ size: CGSize) { | |
| let scaleWidth = size.width / initialImageSize.width | |
| let scaleHeight = size.height / initialImageSize.height | |
| let minScale = min(1.0, min(scaleWidth, scaleHeight) ) | |
| scrollView?.minimumZoomScale = minScale | |
| scrollView?.maximumZoomScale = 1.0 | |
| initialZoomScale = minScale | |
| } | |
| private func centerScrollViewOn(_ size: CGSize) { | |
| let imageFrame = imageView.frame | |
| var newOrigin = imageFrame.origin | |
| newOrigin.x = (imageFrame.size.width < size.width) ? (size.width / 2.0) - (imageFrame.size.width / 2.0) : 0 | |
| newOrigin.y = (imageFrame.size.height < size.height) ? (size.height / 2.0) - (imageFrame.size.height / 2.0) : 0 | |
| imageView.frame.origin = newOrigin | |
| } | |
| } | |
| extension ImageViewerViewController: UIScrollViewDelegate { | |
| func viewForZooming(in scrollView: UIScrollView) -> UIView? { | |
| return imageView | |
| } | |
| func scrollViewDidZoom(_ scrollView: UIScrollView) { | |
| centerScrollViewOn(scrollView.bounds.size) | |
| } | |
| func scrollViewDidScroll(_ scrollView: UIScrollView) { | |
| // store initial content offset of scroll view | |
| if !scrollViewLoaded { | |
| scrollViewLoaded = true | |
| scrollViewInitialOffset = scrollView.contentOffset.y | |
| } | |
| if dragScrollViewToDismiss { | |
| // if scrolling up, cancel dismiss | |
| if scrollView.contentOffset.y - scrollViewInitialOffset > -scrollView.contentInset.top { | |
| dragScrollViewToDismiss = false | |
| dragScrollViewToDismissIsReady = false | |
| } | |
| // if scrolling down, dismiss view controller | |
| else if scrollView.contentOffset.y - scrollViewInitialOffset <= -scrollView.contentInset.top - scrollDistanceToDismiss { | |
| done() | |
| } | |
| } | |
| } | |
| private func done() { | |
| dismiss(animated: true, completion: nil) | |
| } | |
| func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { | |
| if scrollView.contentOffset.y <= -scrollView.contentInset.top { | |
| dragScrollViewToDismissIsReady = true | |
| } | |
| } | |
| func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { | |
| if scrollView.contentOffset.y <= -scrollView.contentInset.top { | |
| dragScrollViewToDismissIsReady = true | |
| } | |
| } | |
| func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { | |
| dragScrollViewToDismissIsReady = true | |
| } | |
| func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { | |
| if dragScrollViewToDismissIsReady { | |
| dragScrollViewToDismiss = true | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Now Swift 4.2, and adds a simple drag-down to dismiss.