Search code examples
swiftuiimageviewuitapgesturerecognizer

How to make ImageView TapGesture smooth in swift


In my project I have one profile with profilepic(imageview).. here if i tap on imageview the image showing in separate view very sharply and coming back to its original position as well sharply

here i want the tapped image should open and close smoothly.. how to do that

here is my total code:

class ViewController: UIViewController {

@IBOutlet weak var sampImage: UIImageView!
override func viewDidLoad() {
    super.viewDidLoad()
    
    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped(tapGestureRecognizer:)))
    sampImage.isUserInteractionEnabled = true
    sampImage.addGestureRecognizer(tapGestureRecognizer)
    }


@objc func imageTapped(tapGestureRecognizer: UITapGestureRecognizer)
{
let tappedImage = tapGestureRecognizer.view as! UIImageView
let newImageView = UIImageView(image: tappedImage.image)
newImageView.frame = UIScreen.main.bounds
newImageView.backgroundColor = .black
newImageView.contentMode = .scaleAspectFit
newImageView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
newImageView.addGestureRecognizer(tap)
self.view.addSubview(newImageView)
self.navigationController?.isNavigationBarHidden = true
self.tabBarController?.tabBar.isHidden = true
}

@objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
self.navigationController?.isNavigationBarHidden = false
self.tabBarController?.tabBar.isHidden = false
sender.view?.removeFromSuperview()
}
}

how to make tapped image open and close smoothly.


Solution

  • You may user GSImageViewController. If you dont want to install the pod, you may just copy only the GSImageViewController.swift code in your project.

    Create a New File named GSImageViewController.swift and copy-paste the below code

    //
    //  GSImageViewerController.swift
    //  GSImageViewerControllerExample
    //
    //  Created by Gesen on 15/12/22.
    //  Copyright © 2015年 Gesen. All rights reserved.
    //
    
    import UIKit
    
    public struct GSImageInfo {
        
        public enum ImageMode : Int {
            case aspectFit  = 1
            case aspectFill = 2
        }
        
        public let image     : UIImage
        public let imageMode : ImageMode
        public var imageHD   : URL?
        
        public var contentMode : UIView.ContentMode {
            return UIView.ContentMode(rawValue: imageMode.rawValue)!
        }
        
        public init(image: UIImage, imageMode: ImageMode) {
            self.image     = image
            self.imageMode = imageMode
        }
        
        public init(image: UIImage, imageMode: ImageMode, imageHD: URL?) {
            self.init(image: image, imageMode: imageMode)
            self.imageHD = imageHD
        }
        
        func calculate(rect: CGRect, origin: CGPoint? = nil, imageMode: ImageMode? = nil) -> CGRect {
            switch imageMode ?? self.imageMode {
                
            case .aspectFit:
                return rect
                
            case .aspectFill:
                let r = max(rect.size.width / image.size.width, rect.size.height / image.size.height)
                let w = image.size.width * r
                let h = image.size.height * r
                
                return CGRect(
                    x      : origin?.x ?? rect.origin.x - (w - rect.width) / 2,
                    y      : origin?.y ?? rect.origin.y - (h - rect.height) / 2,
                    width  : w,
                    height : h
                )
            }
        }
        
        func calculateMaximumZoomScale(_ size: CGSize) -> CGFloat {
            return max(2, max(
                image.size.width  / size.width,
                image.size.height / size.height
            ))
        }
        
    }
    
    open class GSTransitionInfo {
        
        open var duration: TimeInterval = 0.35
        open var canSwipe: Bool         = true
        
        public init(fromView: UIView) {
            self.fromView = fromView
        }
        
        public init(fromRect: CGRect) {
            self.convertedRect = fromRect
        }
        
        weak var fromView: UIView?
        
        fileprivate var fromRect: CGRect!
        fileprivate var convertedRect: CGRect!
        
    }
    
    open class GSImageViewerController: UIViewController {
        
        public let imageView  = UIImageView()
        public let scrollView = UIScrollView()
        
        public let imageInfo: GSImageInfo
        
        open var transitionInfo: GSTransitionInfo?
        
        open var dismissCompletion: (() -> Void)?
        
        open var backgroundColor: UIColor = .black {
            didSet {
                view.backgroundColor = backgroundColor
            }
        }
        
        open lazy var session: URLSession = {
            let configuration = URLSessionConfiguration.ephemeral
            return URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
        }()
        
        // MARK: Initialization
        
        public init(imageInfo: GSImageInfo) {
            self.imageInfo = imageInfo
            super.init(nibName: nil, bundle: nil)
        }
        
        public convenience init(imageInfo: GSImageInfo, transitionInfo: GSTransitionInfo) {
            self.init(imageInfo: imageInfo)
            self.transitionInfo = transitionInfo
            
            if let fromView = transitionInfo.fromView, let referenceView = fromView.superview {
                transitionInfo.fromRect = referenceView.convert(fromView.frame, to: nil)
                
                if fromView.contentMode != imageInfo.contentMode {
                    transitionInfo.convertedRect = imageInfo.calculate(
                        rect: transitionInfo.fromRect!,
                        imageMode: GSImageInfo.ImageMode(rawValue: fromView.contentMode.rawValue)
                    )
                } else {
                    transitionInfo.convertedRect = transitionInfo.fromRect
                }
            }
            
            if transitionInfo.convertedRect != nil {
                self.transitioningDelegate = self
                self.modalPresentationStyle = .overFullScreen
            }
        }
        
        public convenience init(image: UIImage, imageMode: UIView.ContentMode, imageHD: URL?, fromView: UIView?) {
            let imageInfo = GSImageInfo(image: image, imageMode: GSImageInfo.ImageMode(rawValue: imageMode.rawValue)!, imageHD: imageHD)
            
            if let fromView = fromView {
                self.init(imageInfo: imageInfo, transitionInfo: GSTransitionInfo(fromView: fromView))
            } else {
                self.init(imageInfo: imageInfo)
            }
        }
        
        public required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        // MARK: Override
        
        override open func viewDidLoad() {
            super.viewDidLoad()
            
            setupView()
            setupScrollView()
            setupImageView()
            setupGesture()
            setupImageHD()
            
            edgesForExtendedLayout = UIRectEdge()
            automaticallyAdjustsScrollViewInsets = false
        }
        
        override open func viewWillLayoutSubviews() {
            super.viewWillLayoutSubviews()
            
            imageView.frame = imageInfo.calculate(rect: view.bounds, origin: .zero)
            
            scrollView.frame = view.bounds
            scrollView.contentSize = imageView.bounds.size
            scrollView.maximumZoomScale = imageInfo.calculateMaximumZoomScale(scrollView.bounds.size)
        }
        
        // MARK: Setups
        
        fileprivate func setupView() {
            view.backgroundColor = backgroundColor
        }
        
        fileprivate func setupScrollView() {
            scrollView.delegate = self
            scrollView.minimumZoomScale = 1.0
            scrollView.showsHorizontalScrollIndicator = false
            scrollView.showsVerticalScrollIndicator = false
            view.addSubview(scrollView)
        }
        
        fileprivate func setupImageView() {
            imageView.image = imageInfo.image
            imageView.contentMode = .scaleAspectFit
            scrollView.addSubview(imageView)
        }
        
        fileprivate func setupGesture() {
            let single = UITapGestureRecognizer(target: self, action: #selector(singleTap))
            let double = UITapGestureRecognizer(target: self, action: #selector(doubleTap(_:)))
            double.numberOfTapsRequired = 2
            single.require(toFail: double)
            scrollView.addGestureRecognizer(single)
            scrollView.addGestureRecognizer(double)
            
            if transitionInfo?.canSwipe == true {
                let pan = UIPanGestureRecognizer(target: self, action: #selector(pan(_:)))
                pan.delegate = self
                scrollView.addGestureRecognizer(pan)
            }
        }
        
        fileprivate func setupImageHD() {
            guard let imageHD = imageInfo.imageHD else { return }
                
            let request = URLRequest(url: imageHD, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 15)
            let task = session.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
                guard let data = data else { return }
                guard let image = UIImage(data: data) else { return }
                self.imageView.image = image
                self.view.layoutIfNeeded()
            })
            task.resume()
        }
        
        // MARK: Gesture
        
        @objc fileprivate func singleTap() {
            if navigationController == nil || (presentingViewController != nil && navigationController!.viewControllers.count <= 1) {
                dismiss(animated: true, completion: dismissCompletion)
            }
        }
        
        @objc fileprivate func doubleTap(_ gesture: UITapGestureRecognizer) {
            let point = gesture.location(in: scrollView)
            
            if scrollView.zoomScale == 1.0 {
                scrollView.zoom(to: CGRect(x: point.x-40, y: point.y-40, width: 80, height: 80), animated: true)
            } else {
                scrollView.setZoomScale(1.0, animated: true)
            }
        }
        
        fileprivate var panViewOrigin : CGPoint?
        fileprivate var panViewAlpha  : CGFloat = 1
        
        @objc fileprivate func pan(_ gesture: UIPanGestureRecognizer) {
            
            func getProgress() -> CGFloat {
                let origin = panViewOrigin!
                let changeX = abs(scrollView.center.x - origin.x)
                let changeY = abs(scrollView.center.y - origin.y)
                let progressX = changeX / view.bounds.width
                let progressY = changeY / view.bounds.height
                return max(progressX, progressY)
            }
            
            func getChanged() -> CGPoint {
                let origin = scrollView.center
                let change = gesture.translation(in: view)
                return CGPoint(x: origin.x + change.x, y: origin.y + change.y)
            }
            
            func getVelocity() -> CGFloat {
                let vel = gesture.velocity(in: scrollView)
                return sqrt(vel.x*vel.x + vel.y*vel.y)
            }
            
            switch gesture.state {
    
            case .began:
                
                panViewOrigin = scrollView.center
                
            case .changed:
                
                scrollView.center = getChanged()
                panViewAlpha = 1 - getProgress()
                view.backgroundColor = backgroundColor.withAlphaComponent(panViewAlpha)
                gesture.setTranslation(CGPoint.zero, in: nil)
    
            case .ended:
                
                if getProgress() > 0.25 || getVelocity() > 1000 {
                    dismiss(animated: true, completion: dismissCompletion)
                } else {
                    fallthrough
                }
                
            default:
                
                UIView.animate(withDuration: 0.3,
                    animations: {
                        self.scrollView.center = self.panViewOrigin!
                        self.view.backgroundColor = self.backgroundColor
                    },
                    completion: { _ in
                        self.panViewOrigin = nil
                        self.panViewAlpha  = 1.0
                    }
                )
                
            }
        }
        
    }
    
    extension GSImageViewerController: UIScrollViewDelegate {
        
        public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            return imageView
        }
        
        public func scrollViewDidZoom(_ scrollView: UIScrollView) {
            imageView.frame = imageInfo.calculate(rect: CGRect(origin: .zero, size: scrollView.contentSize), origin: .zero)
        }
        
    }
    
    extension GSImageViewerController: UIViewControllerTransitioningDelegate {
        
        public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return GSImageViewerTransition(imageInfo: imageInfo, transitionInfo: transitionInfo!, transitionMode: .present)
        }
        
        public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return GSImageViewerTransition(imageInfo: imageInfo, transitionInfo: transitionInfo!, transitionMode: .dismiss)
        }
        
    }
    
    class GSImageViewerTransition: NSObject, UIViewControllerAnimatedTransitioning {
        
        let imageInfo      : GSImageInfo
        let transitionInfo : GSTransitionInfo
        var transitionMode : TransitionMode
        
        enum TransitionMode {
            case present
            case dismiss
        }
        
        init(imageInfo: GSImageInfo, transitionInfo: GSTransitionInfo, transitionMode: TransitionMode) {
            self.imageInfo = imageInfo
            self.transitionInfo = transitionInfo
            self.transitionMode = transitionMode
            super.init()
        }
        
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return transitionInfo.duration
        }
        
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            let containerView = transitionContext.containerView
            
            let tempBackground = UIView()
                tempBackground.backgroundColor = UIColor.black
            
            let tempMask = UIView()
                tempMask.backgroundColor = .black
                tempMask.layer.cornerRadius = transitionInfo.fromView?.layer.cornerRadius ?? 0
                tempMask.layer.masksToBounds = transitionInfo.fromView?.layer.masksToBounds ?? false
            
            let tempImage = UIImageView(image: imageInfo.image)
                tempImage.contentMode = imageInfo.contentMode
                tempImage.mask = tempMask
            
            containerView.addSubview(tempBackground)
            containerView.addSubview(tempImage)
            
            if transitionMode == .present {
                let imageViewer = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! GSImageViewerController
                    imageViewer.view.layoutIfNeeded()
                
                tempBackground.alpha = 0
                tempBackground.frame = imageViewer.view.bounds
                tempImage.frame = transitionInfo.convertedRect
                tempMask.frame = tempImage.convert(transitionInfo.fromRect, from: nil)
                
                transitionInfo.fromView?.alpha = 0
                
                UIView.animate(withDuration: transitionInfo.duration, animations: {
                    tempBackground.alpha  = 1
                    tempImage.frame = imageViewer.imageView.frame
                    tempMask.frame = tempImage.bounds
                }, completion: { _ in
                    tempBackground.removeFromSuperview()
                    tempImage.removeFromSuperview()
                    containerView.addSubview(imageViewer.view)
                    transitionContext.completeTransition(true)
                })
            }
            
            else if transitionMode == .dismiss {
                let imageViewer = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! GSImageViewerController
                    imageViewer.view.removeFromSuperview()
                
                tempBackground.alpha = imageViewer.panViewAlpha
                tempBackground.frame = imageViewer.view.bounds
                
                if imageViewer.scrollView.zoomScale == 1 && imageInfo.imageMode == .aspectFit {
                    tempImage.frame = imageViewer.scrollView.frame
                } else {
                    tempImage.frame = CGRect(x: imageViewer.scrollView.contentOffset.x * -1, y: imageViewer.scrollView.contentOffset.y * -1, width: imageViewer.scrollView.contentSize.width, height: imageViewer.scrollView.contentSize.height)
                }
                
                tempMask.frame = tempImage.bounds
                
                UIView.animate(withDuration: transitionInfo.duration, animations: {
                    tempBackground.alpha = 0
                    tempImage.frame = self.transitionInfo.convertedRect
                    tempMask.frame = tempImage.convert(self.transitionInfo.fromRect, from: nil)
                }, completion: { _ in
                    tempBackground.removeFromSuperview()
                    tempImage.removeFromSuperview()
                    imageViewer.view.removeFromSuperview()
                    self.transitionInfo.fromView?.alpha = 1
                    transitionContext.completeTransition(true)
                })
            }
        }
    }
    
    extension GSImageViewerController: UIGestureRecognizerDelegate {
        
        public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
            if let pan = gestureRecognizer as? UIPanGestureRecognizer {
                if scrollView.zoomScale != 1.0 {
                    return false
                }
                if imageInfo.imageMode == .aspectFill && (scrollView.contentOffset.x > 0 || pan.translation(in: view).x <= 0) {
                    return false
                }
            }
            return true
        }
        
    }
    

    Now, as the documentation says, use the below code to show the image view in full screen.

    @objc func showImage() {
        let imageInfo   = GSImageInfo(image: yourImageView.image, imageMode: .aspectFit)
        let imageViewer = GSImageViewerController(imageInfo: imageInfo)
        navigationController?.pushViewController(imageViewer, animated: true)
    }