14

How do I take a 1:1 screenshot of UIScrollView visible area? The content may be larger or smaller than UIScrollView bounds as well as half-hidden (I've implemented custom scrolling for smaller content, so it's not in the top-left corner). I've achieved desired result on simulator, but not on device itself:

-(UIImage *)imageFromCombinedContext:(UIView *)background {
      UIImage *image;
      CGRect vis = background.bounds;
      CGSize size = vis.size;
      UIGraphicsBeginImageContext(size);
      [background.layer affineTransform];
      [background.layer renderInontext:UIGraphicsGetCurrentContext()];
      image = UIGraphicsGetImageFromCurrentImageContext();
      UIGraphicsEndImageContext();
      CGImageRef imref = CGImageCreateWithImageInRect([image CGImage], vis);
      image = [UIImage imageWithCGImage:imref];
      CGImageRelease(imref);
      return image;
}
Concuror
  • 408
  • 3
  • 9

9 Answers9

33

Another approach would be to use the contentOffset to adjust the layer's visible area and capture only the currently visible area of UIScrollView.

UIScrollView *contentScrollView;....//scrollview instance

UIGraphicsBeginImageContextWithOptions(contentScrollView.bounds.size, 
                                       YES, 
                                       [UIScreen mainScreen].scale);

//this is the key
CGPoint offset=contentScrollView.contentOffset;
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -offset.x, -offset.y); 

[contentScrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *visibleScrollViewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Cheers :)

Abduliam Rehmanius
  • 928
  • 12
  • 23
  • 4
    this is a lot simpler and memory optimized as there is no need to crop a bigger image. – Gianluca P. Oct 21 '13 at 12:40
  • 1
    This answer is the only one that worked for me. Thanks. – iOS Dev May 23 '14 at 08:19
  • Anyone have luck with this on Swift? I have a "zoomable" scrollview, calling this extension function and getting an empty image: func screenshot() -> UIImage { let offset = self.contentOffset UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.mainScreen().scale) CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -offset.x, -offset.y) self.layer.renderInContext(UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } – Ryan Nov 06 '15 at 01:07
  • Worked for me in zoomable scrollview in swift. Thanks. – Kiran Jasvanee Feb 10 '16 at 05:42
6

Swift version of Abduliam Rehmanius answer.

func screenshot() -> UIImage {

        UIGraphicsBeginImageContextWithOptions(self.scrollCrop.bounds.size, true, UIScreen.mainScreen().scale);
        //this is the key
        let offset:CGPoint = self.scrollCrop.contentOffset;
        CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -offset.x, -offset.y);
        self.scrollCrop.layer.renderInContext(UIGraphicsGetCurrentContext()!);
        let visibleScrollViewImage: UIImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return visibleScrollViewImage;
    }

Swift 4 version:

func screenshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(self.scrollCrop.bounds.size, false, UIScreen.main.scale)
        let offset = self.scrollCrop.contentOffset
        let thisContext = UIGraphicsGetCurrentContext()
        thisContext?.translateBy(x: -offset.x, y: -offset.y)
        self.scrollCrop.layer.render(in: thisContext!)
        let visibleScrollViewImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
return visibleScrollViewImage
}
Abhishek
  • 1,261
  • 1
  • 13
  • 30
Deepak Thakur
  • 3,453
  • 2
  • 37
  • 65
4

I've found a solution myself - I took screenshot of the whole view and then crop it to the size and position of UIScrollView frame.

-(UIImage *)imageFromCombinedContext:(UIView *)background 
{
      UIImage *image;
      CGSize size = self.view.frame.size;
      UIGraphicsBeginImageContext(size);
      [background.layer affineTransform];
      [self.view.layer.layer renderInContext:UIGraphicsGetCurrentContext()];
      image = UIGraphicsGetImageFromCurrentImageContext();
      UIGraphicsEndImageContext();
      CGImageRef imgRef = CGImageCreateWithImageInRect([image CGImage],background.frame);
      image = [UIImage imageWithCGImage:imref];
      CGImageRelease(imref);
      return image;
}
iOS Dev
  • 4,143
  • 5
  • 30
  • 58
Concuror
  • 408
  • 3
  • 9
4

Swift 4 version of Abduliam Rehmanius answer as UIScrollView extension with translation, no slow cropping

extension UIScrollView {

    var snapshotVisibleArea: UIImage? {
        UIGraphicsBeginImageContext(bounds.size)
        UIGraphicsGetCurrentContext()?.translateBy(x: -contentOffset.x, y: -contentOffset.y)
        layer.render(in: UIGraphicsGetCurrentContext()!)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
}
Stoyan
  • 1,265
  • 11
  • 20
3

Jeffery Sun has the right answer. Just put your scroll view inside another view. Get container view to render in context. done.

In the code below, cropView contains the scroll view to be captured. The solution is really just that simple.

As I understand the question and why I found this page, the whole content of the scroll view isn't wanted - just the visible portion.

func captureCrop() -> UIImage {
    UIGraphicsBeginImageContextWithOptions(self.cropView.frame.size, true, 0.0)
    self.cropView.layer.renderInContext(UIGraphicsGetCurrentContext())
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image;
}
AutomatonTec
  • 666
  • 6
  • 15
0

@Abduliam Rehmanius's answer has poor performance, since if the UIScrollView contains a large content area, we will draw that entire content area (even outside the visible bounds). @Concuror's answer has the issue that it will also draw anything that is on top of the UIScrollView.

My solution was to put the UIScrollView inside a UIView called containerView with the same bounds and then render containerView:

containerView.renderInContext(context)
Jeffrey Sun
  • 7,789
  • 1
  • 24
  • 17
0

Swift 3.0 :

 func captureScreen() -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(self.yourScrollViewName.bounds.size, true, UIScreen.main.scale)
    let offset:CGPoint = self.yourScrollViewName.contentOffset;
    UIGraphicsGetCurrentContext()!.translateBy(x: -offset.x, y: -offset.y);
    self.yourScrollViewName.layer.render(in: UIGraphicsGetCurrentContext()!)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
}

and use it as : let Image = captureScreen()

Sri Hari YS
  • 146
  • 2
  • 1
0

Update swift 3+, 4 on @Concuror code

func getImage(fromCombinedContext background: UIView) -> UIImage {
        var image: UIImage?
        let size: CGSize = view.frame.size
        UIGraphicsBeginImageContext(size)
        background.layer.affineTransform()
        view.layer.render(in: UIGraphicsGetCurrentContext()!)
        image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        let imgRef = image?.cgImage?.cropping(to: background.frame)
        image = UIImage(cgImage: imgRef!)
        // CGImageRelease(imgRef!) // Removing on Swift - 'CGImageRelease' is unavailable: Core Foundation objects are automatically memory managed
        return image ?? UIImage()
    }
Tuan Huynh
  • 566
  • 4
  • 13
0

A lot of the answers use UIGraphicsBeginImageContext (pre iOS 10.0) to create an image, this creates an image missing the P3 colour gamut - reference https://stackoverflow.com/a/41288197/2481602

extension UIScrollView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
        
            inputView?.layer.render(in: rendererContext.cgContext)
            layer.render(in: rendererContext.cgContext)
        }
    }
}

The above will result in a better quality image being produced.

The second image is clearer, and showing more of the colours - this was done with the UIGraphicsImageRenderer rather than the UIGraphicsBeginImageContext (first Image)

UIGraphicsBeginImageContext method

UIGraphicsImageRenderer method

MindBlower3
  • 485
  • 4
  • 20