4

The app I'm working on uses collection view cells to display data to the user. I want the user to be able to share the data that's contained in the cells, but there are usually too many cells to try to re-size and fit onto a single iPhone-screen-sized window and get a screenshot.

So the problem I'm having is trying to get an image of all the cells in a collection view, both on-screen and off-screen. I'm aware that off-screen cells don't actually exist, but I'd be interested in a way to kind of fake an image and draw in the data (if that's possible in swift).

In short, is there a way to programmatically create an image from a collection view and the cells it contains, both on and off screen with Swift?

R Menke
  • 8,183
  • 4
  • 35
  • 63
The Beanstalk
  • 798
  • 1
  • 5
  • 20
  • 2
    Check out [this answer](http://stackoverflow.com/questions/2214957/how-do-i-take-a-screen-shot-of-a-uiview). Not certain, but I think you could set the `size` parameter of `UIGraphicsBeginImageContext()` to the bounds of your collection view. – Naftali Beder Sep 24 '15 at 23:50
  • If the collection view is large, you might have memory problems trying to create an image from it. – Abizern Sep 28 '15 at 17:55
  • To clarify, are you trying to get a snapshot of the collection view's entire content area, even when part of that content area is offscreen our outside the collection view's bounds? – algal Sep 29 '15 at 16:04
  • Yes he is, stated that in a comment in the answer below. ;) – R Menke Sep 29 '15 at 16:05
  • Yeah, I will reword the above – The Beanstalk Sep 29 '15 at 16:54

3 Answers3

3

Update

If memory is not a concern :

mutating func screenshot(scale: CGFloat) -> UIImage {
    let currentSize = frame.size
    let currentOffset = contentOffset // temp store current offset

    frame.size = contentSize
    setContentOffset(CGPointZero, animated: false)        

    // it might need a delay here to allow loading data.

    let rect = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
    UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
    self.drawViewHierarchyInRect(rect, afterScreenUpdates: true)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    frame.size = currentSize
    setContentOffset(currentOffset, animated: false)

    return resizeUIImage(image, scale: scale)        
}

This works for me:

github link -> contains up to date code

getScreenshotRects creates the offsets to which to scroll and the frames to capture. (naming is not perfect)

takeScreenshotAtPoint scrolls to the point, sets a delay to allow a redraw, takes the screenshot and returns this via completion handler.

stitchImages creates a rect with the same size as the content and draws all images in them.

makeScreenshots uses the didSet on a nested array of UIImage and a counter to create all images while also waiting for completion. When this is done it fires it own completion handler.

Basic parts :

  • scroll collectionview -> works
  • take screenshot with delay for a redraw -> works
  • crop images that are overlapping -> apparently not needed
  • stitch all images -> works
  • basic math -> works
  • maybe freeze screen or hide when all this is happening (this is not in my answer)

Code :

protocol ScrollViewImager {

    var bounds : CGRect { get }

    var contentSize : CGSize { get }

    var contentOffset : CGPoint { get }

    func setContentOffset(contentOffset: CGPoint, animated: Bool)

    func drawViewHierarchyInRect(rect: CGRect, afterScreenUpdates: Bool) -> Bool
}

extension ScrollViewImager {

    func screenshot(completion: (screenshot: UIImage) -> Void) {

        let pointsAndFrames = getScreenshotRects()
        let points = pointsAndFrames.points
        let frames = pointsAndFrames.frames

        makeScreenshots(points, frames: frames) { (screenshots) -> Void in
            let stitched = self.stitchImages(images: screenshots, finalSize: self.contentSize)
            completion(screenshot: stitched!)
        }

    }

    private func makeScreenshots(points:[[CGPoint]], frames : [[CGRect]],completion: (screenshots: [[UIImage]]) -> Void) {

        var counter : Int = 0

        var images : [[UIImage]] = [] {
            didSet {
                if counter < points.count {
                    makeScreenshotRow(points[counter], frames : frames[counter]) { (screenshot) -> Void in
                        counter += 1
                        images.append(screenshot)
                    }
                } else {
                    completion(screenshots: images)
                }
            }
        }

        makeScreenshotRow(points[counter], frames : frames[counter]) { (screenshot) -> Void in
            counter += 1
            images.append(screenshot)
        }

    }

    private func makeScreenshotRow(points:[CGPoint], frames : [CGRect],completion: (screenshots: [UIImage]) -> Void) {

        var counter : Int = 0

        var images : [UIImage] = [] {
            didSet {
                if counter < points.count {
                    takeScreenshotAtPoint(point: points[counter]) { (screenshot) -> Void in
                        counter += 1
                        images.append(screenshot)
                    }
                } else {
                    completion(screenshots: images)
                }
            }
        }

        takeScreenshotAtPoint(point: points[counter]) { (screenshot) -> Void in
            counter += 1
            images.append(screenshot)
        }

    }

    private func getScreenshotRects() -> (points:[[CGPoint]], frames:[[CGRect]]) {

        let vanillaBounds = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)

        let xPartial = contentSize.width % bounds.size.width
        let yPartial = contentSize.height % bounds.size.height

        let xSlices = Int((contentSize.width - xPartial) / bounds.size.width)
        let ySlices = Int((contentSize.height - yPartial) / bounds.size.height)

        var currentOffset = CGPoint(x: 0, y: 0)

        var offsets : [[CGPoint]] = []
        var rects : [[CGRect]] = []

        var xSlicesWithPartial : Int = xSlices

        if xPartial > 0 {
            xSlicesWithPartial += 1
        }

        var ySlicesWithPartial : Int = ySlices

        if yPartial > 0 {
            ySlicesWithPartial += 1
        }

        for y in 0..<ySlicesWithPartial {

            var offsetRow : [CGPoint] = []
            var rectRow : [CGRect] = []
            currentOffset.x = 0

            for x in 0..<xSlicesWithPartial {

                if y == ySlices && x == xSlices {
                    let rect = CGRect(x: bounds.width - xPartial, y: bounds.height - yPartial, width: xPartial, height: yPartial)
                    rectRow.append(rect)

                } else if y == ySlices {
                    let rect = CGRect(x: 0, y: bounds.height - yPartial, width: bounds.width, height: yPartial)
                    rectRow.append(rect)

                } else if x == xSlices {
                    let rect = CGRect(x: bounds.width - xPartial, y: 0, width: xPartial, height: bounds.height)
                    rectRow.append(rect)

                } else {
                    rectRow.append(vanillaBounds)
                }

                offsetRow.append(currentOffset)

                if x == xSlices {
                    currentOffset.x = contentSize.width - bounds.size.width
                } else {
                    currentOffset.x = currentOffset.x + bounds.size.width
                }
            }
            if y == ySlices {
                currentOffset.y = contentSize.height - bounds.size.height
            } else {
                currentOffset.y = currentOffset.y + bounds.size.height
            }

            offsets.append(offsetRow)
            rects.append(rectRow)

        }

        return (points:offsets, frames:rects)

    }

    private func takeScreenshotAtPoint(point point_I: CGPoint, completion: (screenshot: UIImage) -> Void) {
        let rect = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
        let currentOffset = contentOffset
        setContentOffset(point_I, animated: false)

        delay(0.001) {

            UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
            self.drawViewHierarchyInRect(rect, afterScreenUpdates: true)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()


            self.setContentOffset(currentOffset, animated: false)
            completion(screenshot: image)
        }
    }

    private func delay(delay:Double, closure:()->()) {
        dispatch_after(
            dispatch_time(
                DISPATCH_TIME_NOW,
                Int64(delay * Double(NSEC_PER_SEC))
            ),
            dispatch_get_main_queue(), closure)
    }


    private func crop(image image_I:UIImage, toRect rect:CGRect) -> UIImage? {

        guard let imageRef: CGImageRef = CGImageCreateWithImageInRect(image_I.CGImage, rect) else {
            return nil
        }
        return UIImage(CGImage:imageRef)
    }

    private func stitchImages(images images_I: [[UIImage]], finalSize : CGSize) -> UIImage? {

        let finalRect = CGRect(x: 0, y: 0, width: finalSize.width, height: finalSize.height)

        guard images_I.count > 0 else {
            return nil
        }

        UIGraphicsBeginImageContext(finalRect.size)

        var offsetY : CGFloat = 0

        for imageRow in images_I {

            var offsetX : CGFloat = 0

            for image in imageRow {

                let width = image.size.width
                let height = image.size.height


                let rect = CGRect(x: offsetX, y: offsetY, width: width, height: height)
                image.drawInRect(rect)

                offsetX += width

            }

            offsetX = 0

            if let firstimage = imageRow.first {
                offsetY += firstimage.size.height
            } // maybe add error handling here
        }

        let stitchedImages = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return stitchedImages
    }
}

extension UIScrollView : ScrollViewImager {

}
R Menke
  • 8,183
  • 4
  • 35
  • 63
  • 1
    This works beautifully, and it also works on horizontally scrolling views -- thanks for your help – The Beanstalk Sep 30 '15 at 15:42
  • @TheBeanstalk added a method to simply resize the the scrollview and take a screenshot. – R Menke Oct 06 '15 at 15:06
  • 2
    Thank you very much for your code. I forked it and updated for Swift 4.2, if anybody is interested: https://github.com/ibakurov/ScrollViewImager – Illya Bakurov Feb 22 '19 at 20:08
  • The solution is working. But the picture quality is poor. To fix this, wherever there is a scale parameter, set UIScreen.main.scale Example: func screenShotFullContent(scale: CGFloat = UIScreen.main.scale, completion: @escaping (_ screenshot: UIImage?) -> Void) {} – Sosin Vitalii Sep 09 '22 at 23:03
0

Draw the bitmap data of your UICollectionView into a UIImage using UIKit graphics functions. Then you'll have a UIImage that you could save to disk or do whatever you need with it. Something like this should work:

// your collection view
@IBOutlet weak var myCollectionView: UICollectionView!

//...

let image: UIImage!

// draw your UICollectionView into a UIImage
UIGraphicsBeginImageContext(myCollectionView.frame.size) 

myCollectionView.layer.renderInContext(UIGraphicsGetCurrentContext()!)

image = UIGraphicsGetImageFromCurrentImageContext()  

UIGraphicsEndImageContext()
eric
  • 4,863
  • 11
  • 41
  • 55
  • Tried this, but I get a blank image. Also, I'm looking to include the off-screen cells. – The Beanstalk Sep 28 '15 at 23:38
  • 1
    UICollectionView doesn't load all of your cells into memory at once so many of the 'off screen' cells don't even really exist until a user pans at which point they are loaded and drawn on demand. So if you wanted to make an image of all of the cells need to create an adapter that can read @TheBeanstalk in a cell's data, create and draw a view for the cell, and output an image. Then, when you need to build your composite image, you would iterate over your collection, run each data item through the adapter which generates an image for it, and then stitch all the images together into one large im – eric Sep 29 '15 at 01:34
0

For swift 4 to make screenshot of UICollectionView

func makeScreenShotToShare()-> UIImage{
        UIGraphicsBeginImageContextWithOptions(CGSize.init(width: self.colHistory.contentSize.width, height: self.colHistory.contentSize.height + 84.0), false, 0)
        colHistory.scrollToItem(at: IndexPath.init(row: 0, section: 0), at: .top, animated: false)
        colHistory.layer.render(in: UIGraphicsGetCurrentContext()!)
        let row = colHistory.numberOfItems(inSection: 0)
        let numberofRowthatShowinscreen = self.colHistory.size.height / (self.arrHistoryData.count == 1 ? 130 : 220)
        let scrollCount = row / Int(numberofRowthatShowinscreen)

        for  i in 0..<scrollCount {
            colHistory.scrollToItem(at: IndexPath.init(row: (i+1)*Int(numberofRowthatShowinscreen), section: 0), at: .top, animated: false)
            colHistory.layer.render(in: UIGraphicsGetCurrentContext()!)
        }

        let image:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext();
        return image
    }
Hardik Thakkar
  • 15,269
  • 2
  • 94
  • 81