I have solved this using a UIScrollView and a UICollectionViewLayout subclass.
1) place a UIScrollView on top of the UICollectionView with the same frame.
 self.view.addSubview(scrollView)
 scrollView.addSubview(dummyViewForZooming)
 scrollView.frame = collectionView.frame
 scrollView.bouncesZoom = false
 scrollView.minimumZoomScale = 0.5
 scrollView.maximumZoomScale = 3.0
2) Set the contentSize of the UIScrollView and zoomingView to be the same as the UICollectionView  
  override func viewDidLayoutSubviews() {
    super.viewWillLayoutSubviews()
    scrollView.contentSize = layout.collectionViewContentSize
    dummyViewForZooming.frame = CGRect(origin: .zero, size: layout.collectionViewContentSize)
    scrollView.frame = collectionView.frame
  }
3) Remove all gesture recognizers from the UICollectionView and add a delegate for the UIScrollView. Add a tap gesture recognizer to the UIScrollview
    collectionView.gestureRecognizers?.forEach {
        collectionView.removeGestureRecognizer($0)
    }
    let tap = UITapGestureRecognizer.init(target: self, action: #selector(scrollViewWasTapped(sender:)))
    tap.numberOfTapsRequired = 1
    scrollView.addGestureRecognizer(tap)
    scrollView.delegate = self
4) When the ScrollView scrolls or zooms, set the contentOffset of the UICollectionView to be the same as the ScrollView contentOffset, set the layoutScale of your UICollectionViewLayout as the zoomscale and invalidate the layout. 
func scrollViewDidZoom(_ scrollView: UIScrollView) {
    if let layout = self.layout, layout.getScale() != scrollView.zoomScale {
        layout.layoutScale = scrollView.zoomScale
        self.layout.invalidateLayout()
        collectionView.contentOffset = scrollView.contentOffset
    }
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
    return dummyViewForZooming
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    collectionView.contentOffset = scrollView.contentOffset
}
5) override the prepare method in the UICollectionViewLayout, scan through all your layoutAttributes and set a transform:
    attribute.transformedFrame = attribute.originalFrame.scale(layoutScale)
    let ts = CGAffineTransform(scaleX: layoutScale, y: layoutScale)
        attribute.transform = ts
    let xDifference = attribute.frame.origin.x - attribute.transformedFrame.origin.x
    let yDifference = attribute.frame.origin.y - attribute.transformedFrame.origin.y
    let t1 = CGAffineTransform(translationX: -xDifference, y: -yDifference)
    let t = ts.concatenating(t1)
        attribute.transform = t
6) ensure you scale the collectionView content size:
override var collectionViewContentSize: CGSize  {
        return CGSize(width: width * layoutScale, height: height * layoutScale)
    } 
7) Intercept taps from the tap gesture recognizer and convert the location in view to a point in the collection view, you can then get the indexPath of that cell using indexPathForItem(point:) and select the cell or pass on events to the underlying views of the cell etc..  
hope this helps