Skip to content

Instantly share code, notes, and snippets.

@ajjames
Last active January 4, 2019 20:52
Show Gist options
  • Select an option

  • Save ajjames/8dc0feaa9e54380eea8b to your computer and use it in GitHub Desktop.

Select an option

Save ajjames/8dc0feaa9e54380eea8b to your computer and use it in GitHub Desktop.
A simple drop-in single image viewer with async image loading
//
// 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
}
}
}
@ajjames

ajjames commented Jan 4, 2019

Copy link
Copy Markdown
Author

Now Swift 4.2, and adds a simple drag-down to dismiss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment