Getting a screenshot of a UIScrollView, including offscreen parts - ios

I have a UIScrollView decendent that implements a takeScreenshot method that looks like this:
-(void)takeScreenshot {
CGRect contextRect = CGRectMake(0, 0, 768, 1004);
UIGraphicsBeginImageContext(contextRect.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// do something with the viewImage here.
}
This basically moves to the top of the scroll view, and takes a screenshot of the visible area. It works fine when the iPad is oriented portrait, but when it's in landscape the bottom of the image is cut off (as the height of the visible area is only 748, not 1004).
Is it possible to get a snapshot of the UIScrollView, including areas not on screen? Or do I need to scroll the view down, take a second photo and stitch them together?

Here is code that works ...
- (IBAction) renderScrollViewToImage
{
UIImage* image = nil;
UIGraphicsBeginImageContext(_scrollView.contentSize);
{
CGPoint savedContentOffset = _scrollView.contentOffset;
CGRect savedFrame = _scrollView.frame;
_scrollView.contentOffset = CGPointZero;
_scrollView.frame = CGRectMake(0, 0, _scrollView.contentSize.width, _scrollView.contentSize.height);
[_scrollView.layer renderInContext: UIGraphicsGetCurrentContext()];
image = UIGraphicsGetImageFromCurrentImageContext();
_scrollView.contentOffset = savedContentOffset;
_scrollView.frame = savedFrame;
}
UIGraphicsEndImageContext();
if (image != nil) {
[UIImagePNGRepresentation(image) writeToFile: #"/tmp/test.png" atomically: YES];
system("open /tmp/test.png");
}
}
The last few lines simply write the image to /tmp/test.png and then opens it in Preview.app. This obviously only works on in the Simulator :-)
Complete project in the ScrollViewScreenShot Github Repository

For me, the currently accepted answer from Stefan Arentz didn't work.
I had to implement this on iOS 8 and above, and tested on the iPhone. The accepted answer just renders the visible part of a scroll view, while the rest of image remains blank.
I tried fixing this using drawViewHierarchyInRect - no luck. Depending on afterScreenUpdates being true or false I got stretched part of image or only part of the contents.
The only way I've found to achieve correct snapshotting of a UIScrollView's entire contents is to add it to another temporary view and then render it.
Sample code is below (scrollview is outlet in my VC)
func getImageOfScrollView() -> UIImage {
var image = UIImage()
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.mainScreen().scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset
let savedFrame = self.scrollView.frame
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPointZero
// set frame to content size
self.scrollView.frame = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clearColor()
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.renderInContext(UIGraphicsGetCurrentContext())
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset
self.scrollView.frame = savedFrame
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext()
return image
}

Working Example of UIView Extension with handling for UIScrollView:
extension UIView {
func screenshot() -> UIImage {
if(self is UIScrollView) {
let scrollView = self as! UIScrollView
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
UIGraphicsBeginImageContext(scrollView.contentSize)
scrollView.contentOffset = .zero
self.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
return image!
}
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}

I took this solution from #Roopesh Mittal's answer and made it safer/cleaner.
Swift 5 compatible
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
let savedContentOffset = contentOffset
let savedFrame = frame
UIGraphicsBeginImageContext(contentSize)
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
contentOffset = savedContentOffset
frame = savedFrame
return image
}
}

A refined Swift 4.x/5.0 version, based on #RyanG 's answer:
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
// begin image context
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
// save the orginal offset & frame
let savedContentOffset = contentOffset
let savedFrame = frame
// end ctx, restore offset & frame before returning
defer {
UIGraphicsEndImageContext()
contentOffset = savedContentOffset
frame = savedFrame
}
// change the offset & frame so as to include all content
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
}

In iOS 13 I have ran into issue that this line won't work
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
to fix the issue, I am removing scrollview from parent and then attaching in after taking the screenshot.
Full Code:
func screenshot() -> UIImage? {
let savedContentOffset = contentOffset
let savedFrame = frame
defer {
contentOffset = savedContentOffset
frame = savedFrame
}
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
let image = UIGraphicsImageRenderer(bounds: CGRect(origin: .zero, size: contentSize)).image { renderer in
let context = renderer.cgContext
layer.render(in: context)
}
return image
}
Get Screenshot:
func getScreenshot() -> UIImage? {
scrollView.removeFromSuperview()
let image = scrollView.screenshot()
addScrollView()
return image
}

SWIFT 3 version:
func snapshot() -> UIImage?
{
UIGraphicsBeginImageContext(scrollView.contentSize)
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
scrollView.contentOffset = CGPoint.zero
scrollView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
scrollView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
This worked for me

As many have pointed, current solution doesn't work, and others solutions suggest removing scrollview from superview, which leads into loosing all the constraints.
Here I'm temporarily disabling all constraints related to scroll view and turn them on after screenshot is taken:
extension UIScrollView {
func screenshot() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
// save the orginal offset, take a ref to all constraints related to the view
let savedContentOffset = contentOffset
let actualConstraints = relatedConstraints()
// deactivate non needed constraints so they won't stop us from resiging scroll view
NSLayoutConstraint.deactivate(actualConstraints)
// enable auth generated constraints based on the frame
translatesAutoresizingMaskIntoConstraints = true
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
contentOffset = .zero
defer {
UIGraphicsEndImageContext()
// reset original constraints
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(actualConstraints)
// layout superview needed before resetting content offset
superview?.layoutIfNeeded()
contentOffset = savedContentOffset
}
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
}
extension UIView {
func relatedConstraints() -> [NSLayoutConstraint] {
var constraints = self.constraints
var parent = superview
while parent != nil {
constraints.append(contentsOf: parent!.constraints.filter { $0.firstItem === self || $0.secondItem === self })
parent = parent!.superview
}
return constraints
}
}

SWIFT 3 version thanks to #gleb vodovozov:
func getImageOfScrollView()->UIImage{
var image = UIImage();
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.main.scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset;
let savedFrame = self.scrollView.frame;
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPoint.zero;
// set frame to content size
self.scrollView.frame = CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clear
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.render(in: UIGraphicsGetCurrentContext()!)
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()!;
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset;
self.scrollView.frame = savedFrame;
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext();
return image
}

Here's another way of doing it, which takes the zoom level into account. I have a scrollview with 4 different UIImageView layers in it, and I want to take a screenshot of their current state:
float theScale = 1.0f / theScrollView.zoomScale;
// The viewing rectangle in absolute coordinates
CGRect visibleArea = CGRectMake((int)(theScrollView.contentOffset.x * theScale), (int)(theScrollView.contentOffset.y * theScale),
(int)(theScrollView.bounds.size.width * theScale), (int)(theScrollView.bounds.size.height * theScale));
NSArray *layers = [NSArray arrayWithObjects:imageLayer1, imageLayer2, imageLayer3, imageLayer4, nil];
UIGraphicsBeginImageContext(visibleArea.size);
for (UIImageView *layer in layers) {
CALayer *coreLayer = layer.layer;
coreLayer.bounds = CGRectMake(layer.frame.origin.x - visibleArea.origin.x, layer.frame.origin.y - visibleArea.origin.y, layer.frame.size.width, layer.frame.size.height);
[coreLayer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
This takes the screenshot in absolute coordinates. That is, if you have a 2048*2048 image in the scrollview and you can see about a quarter of it, then regardless of the resolution of your screen it would take a screenshot of 512*512. If you want to take a screenshot at your screen resolution (say, 320*480) then you have to adjust the image as follows, directly after the above code:
UIGraphicsBeginImageContext(theScrollView.frame.size);
[screenshot drawInRect:CGRectMake(0, 0, theScrollView.frame.size.width, theScrollView.frame.size.height)];
UIImage *smallScreenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

If you don't want to expand your scroll view beyond the entire screen (and it won't work with autolayout anyway) there's a better way.
You can use core graphics transforms in conjunction with the contentOffset of the scroll view to accomplish the same thing.
//
// ScrollViewSnapshotter.swift
// ScrollViewSnapshotter
//
// Created by Moshe Berman on 4/10/16.
// Copyright © 2016 Moshe Berman. All rights reserved.
//
import UIKit
class ScrollViewSnapshotter: NSObject {
func PDFWithScrollView(scrollview: UIScrollView) -> NSData {
/**
* Step 1: The first thing we need is the default origin and size of our pages.
* Since bounds always start at (0, 0) and the scroll view's bounds give us
* the correct size for the visible area, we can just use that.
*
* In the United States, a standard printed page is 8.5 inches by 11 inches,
* but when generating a PDF it's simpler to keep the page size matching the
* visible area of the scroll view. We can let our printer software (such
* as the Preview app on OS X or the Printer app on iOS) do the scaling.
*
* If we wanted to scale ourselves, we could multiply each of those
* numbers by 72, to get the number of points for each dimension.
* We would have to change how we generated the the pages below, so
* for simplicity, we're going to stick to one page per screenful of content.
*/
let pageDimensions = scrollview.bounds
/**
* Step 2: Now we need to know how many pages we will need to fit our content.
* To get this, we divide our scroll views dimensions by the size
* of each page, in either direction.
* We also need to round up, so that the pages don't get clipped.
*/
let pageSize = pageDimensions.size
let totalSize = scrollview.contentSize
let numberOfPagesThatFitHorizontally = Int(ceil(totalSize.width / pageSize.width))
let numberOfPagesThatFitVertically = Int(ceil(totalSize.height / pageSize.height))
/**
* Step 3: Set up a Core Graphics PDF context.
*
* First we create a backing store for the PDF data, then
* pass it and the page dimensions to Core Graphics.
*
* We could pass in some document information here, which mostly cover PDF metadata,
* including author name, creator name (our software) and a password to
* require when viewing the PDF file.
*
* Also note that we can use UIGraphicsBeginPDFContextToFile() instead,
* which writes the PDF to a specified path. I haven't played with it, so
* I don't know if the data is written all at once, or as each page is closed.
*/
let outputData = NSMutableData()
UIGraphicsBeginPDFContextToData(outputData, pageDimensions, nil)
/**
* Step 4: Remember some state for later.
* Then we need to clear the content insets, so that our
* core graphics layer and our content offset match up.
* We don't need to reset the content offset, because that
* happens implicitly, in the loop below.
*/
let savedContentOffset = scrollview.contentOffset
let savedContentInset = scrollview.contentInset
scrollview.contentInset = UIEdgeInsetsZero
/**
* Step 6: Now we loop through the pages and generate the data for each page.
*/
if let context = UIGraphicsGetCurrentContext()
{
for indexHorizontal in 0 ..< numberOfPagesThatFitHorizontally
{
for indexVertical in 0 ..< numberOfPagesThatFitVertically
{
/**
* Step 6a: Start a new page.
*
* This automatically closes the previous page.
* There's a similar method UIGraphicsBeginPDFPageWithInfo,
* which allows you to configure the rectangle of the page and
* other metadata.
*/
UIGraphicsBeginPDFPage()
/**
* Step 6b:The trick here is to move the visible portion of the
* scroll view *and* adjust the core graphics context
* appropriately.
*
* Consider that the viewport of the core graphics context
* is attached to the top of the scroll view's content view
* and we need to push it in the opposite direction as we scroll.
* Further, anything not inside of the visible area of the scroll
* view is clipped, so scrolling will move the core graphics viewport
* out of the rendered area, producing empty pages.
*
* To counter this, we scroll the next screenful into view, and adjust
* the core graphics context. Note that core graphics uses a coordinate
* system which has the y coordinate decreasing as we go from top to bottom.
* This is the opposite of UIKit (although it matches AppKit on OS X.)
*/
let offsetHorizontal = CGFloat(indexHorizontal) * pageSize.width
let offsetVertical = CGFloat(indexVertical) * pageSize.height
scrollview.contentOffset = CGPointMake(offsetHorizontal, offsetVertical)
CGContextTranslateCTM(context, -offsetHorizontal, -offsetVertical) // NOTE: Negative offsets
/**
* Step 6c: Now we are ready to render the page.
*
* There are faster ways to snapshot a view, but this
* is the most straightforward way to render a layer
* into a context.
*/
scrollview.layer.renderInContext(context)
}
}
}
/**
* Step 7: End the document context.
*/
UIGraphicsEndPDFContext()
/**
* Step 8: Restore the scroll view.
*/
scrollview.contentInset = savedContentInset
scrollview.contentOffset = savedContentOffset
/**
* Step 9: Return the data.
* You can write it to a file, or display it the user,
* or even pass it to iOS for sharing.
*/
return outputData
}
}
Here's a blog post I wrote explaining the process.
The process for generating a PDF is very similar to snapshotting an image, except instead of pages, you'd need to make one large canvas that matches the size of the scroll view and then grab the contents in chunks.

I have found below code and its working for me. try this ..
extension UIView {
func capture() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
drawHierarchy(in: self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}}

Normally I wouldn't recommend using a library but... Use the SnapshotKit. It works like a charm and The code looks alright too. Using it is straight forward:
Objective-C:
UIImage *tableViewScreenShot = [yourTableView takeSnapshotOfFullContent];
Swift:
let tableViewScreenShot: UIImage = yourTableView.takeSnapshotOfFullContent()

Swift 5 Version
extension UIScrollView {
func takeScrollViewScreenShot() -> UIImage? {
UIGraphicsBeginImageContext(self.contentSize)
let savedContentOffset = self.contentOffset
let savedFrame = self.frame
self.contentOffset = CGPoint.zero
self.layer.frame = CGRect(x: 0, y: 0, width: self.contentSize.width, height: self.contentSize.height)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
self.contentOffset = savedContentOffset
self.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
}

It seems to me that the accepted solution can be fixed by updating scrollView.layer.frame rather than scrollView.frame, as pointed out here. I am not sure I actually understand why this works, though!

I don't know much but I can guess that if we set the size of the contextRect like this for landscape, it may work well:
CGRect contextRect = CGRectMake(0, 0, 1004, 768*2);
Because this contextRect will determine the size of the UIGraphicsBeginImageContext so I hope that double the height can solve your problem

Related

Capture a screenshot of UITableView/ScrollView including offscreen parts is no longer works with iOS 13

Thanks to gleb vodovozov's idea , I can capture the image of entire my TableView including offscreen parts but the superview doesn't restore my TableView as its "before changes" position after using it to render image in a tempView.
I've checked his code carefully and I didn't see any issues. Maybe just because of iOS 13 "changes" so the code that works with previous iOS versions is malfunctioning now?
Below is his code
Any help is strongly appreciated!
var image = UIImage()
UIGraphicsBeginImageContextWithOptions(self.myTable.contentSize, false, UIScreen.main.scale)
// save initial values
let savedContentOffset = self.myTable.contentOffset
let savedFrame = self.myTable.frame
let savedBackgroundColor = self.myTable.backgroundColor
// reset offset to top left point
self.myTable.contentOffset = CGPoint(x: 0, y: 0)
// set frame to content size
self.myTable.frame = CGRect(x: 0, y: 0, width: self.myTable.contentSize.width, height: self.myTable.contentSize.height)
// remove background
self.myTable.backgroundColor = UIColor.clear
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRect(x: 0, y: 0, width: self.myTable.contentSize.width, height: self.myTable.contentSize.height))
// save superview
let tempSuperView = self.myTable.superview
// remove scrollView from old superview
self.myTable.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.myTable)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.render(in: UIGraphicsGetCurrentContext()!)
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()!
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.myTable)
// restore saved settings
self.myTable.contentOffset = savedContentOffset
self.myTable.frame = savedFrame
self.myTable.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext()
return image
// remove scrollView from old superview
self.myTable.removeFromSuperview()
From documentation:
func removeFromSuperview(): Calling this method removes any constraints that refer to the view you are removing, or that refer to any view in the subtree of the view you are removing.
I know that you restore the frame, but you may need to save constraints in array before removing it from super view, then add them again after adding it again to super view.
// Save constraints
guard let superView = myTable.superView else {
return
}
var oldConstraints: [NSLayoutConstraint] = []
for constraint in superView.constraints {
if constraint.firstItem == myTable || constraint.secondItem == myTable {
oldConstraints.append(constraint)
}
}
self.myTable.removeFromSuperview()
// ...
tempSuperView?.addSubview(self.myTable)
// Restore old constraints
NSLayoutConstraint.activate(oldConstraints)
I've added tableview's old constraints to this function.
func screenshot() -> UIImage{
var image = UIImage();
UIGraphicsBeginImageContextWithOptions(self.tableView.contentSize, false, UIScreen.main.scale)
// save initial values
let savedContentOffset = self.tableView.contentOffset;
let savedFrame = self.tableView.frame;
let savedBackgroundColor = self.tableView.backgroundColor
// reset offset to top left point
self.tableView.contentOffset = CGPoint(x: 0, y: 0);
// set frame to content size
self.tableView.frame = CGRect(x: 0, y: 0, width: self.tableView.contentSize.width, height: self.tableView.contentSize.height);
// remove background
self.tableView.backgroundColor = self.tableView.backgroundColor;
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRect(x: 0, y: 0, width: self.tableView.contentSize.width, height: self.tableView.contentSize.height));
// save superview
let tempSuperView = self.tableView.superview
// Save constraints
guard let superView = self.tableView.superview else {
return UIImage();
}
//get old constraints from table
var oldConstraints: [NSLayoutConstraint] = []
for constraint in superView.constraints {
if constraint.firstItem as? NSObject == self.tableView || constraint.secondItem as? NSObject == self.tableView{
oldConstraints.append(constraint)
}
}
// remove scrollView from old superview
self.tableView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.tableView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.render(in: UIGraphicsGetCurrentContext()!)
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()!;
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.tableView)
//activate table's old constraints
NSLayoutConstraint.activate(oldConstraints)
// restore saved settings
self.tableView.contentOffset = savedContentOffset;
self.tableView.frame = savedFrame;
self.tableView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext();
return image
}

How to wait for subview layout settle down after the initial viewDidLoad cycle?

I have a UIViewController, I made an zoomableImageView by embedding a UIImageView inside a UIScrollView.
class ZoomableImageView: UIScrollView {
// public so that delegate can access
public let imageView: UIImageView = {
let _imageView = UIImageView()
_imageView.translatesAutoresizingMaskIntoConstraints = false
return _imageView
} ()
// this method will be called multiple times to display different images
public func setImage(image: UIImage) {
imageView.image = image
imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
self.contentSize = image.size
// gw: not working here, too early
setZoomScale()
}
func setZoomScale() {
let imageViewSize = imageView.bounds.size
let scrollViewSize = self.bounds.size
let widthScale = scrollViewSize.width / imageViewSize.width
let heightScale = scrollViewSize.height / imageViewSize.height
print("gw: imageViewSize: \(imageViewSize), scrollViewSize: \(scrollViewSize)")
self.minimumZoomScale = min(widthScale, heightScale)
self.maximumZoomScale = 1.2 // allow maxmum 120% of original image size
// set initial zoom to fit the longer side (longer side ==> smaller scale)
zoomScale = minimumZoomScale
}
}
Each time I change the UIImage of the image view, I want to wait for the UIImageView's bound size to settle down, before I can use it to calculate a scale factor for zooming in UIScrollView.
Question: What is the appropriate place to put setZoomScale()? I put it right before exiting the setImage method, but the imageView.bounds.size is not correct in my print statement. Note that it needs to be triggered each time the image changes, not just the initial view loading stage.
I also tried to put setZoomScale in ViewController's viewWillLayoutSubviews, but I have addtional question here: is viewWillLayoutSubviews only called once at view initialization stage? Can I force trigger it using setNeedsLayout? (which I tried, but not re-triggering viewWillLayoutSubviews)
As any change to UI elements dispatch the operation to the main dispatch queue, you can put your code in DispathQueue.main.async{} to make sure it run after the UI change is done.
public func setImage(image: UIImage) {
imageView.image = image
imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
self.contentSize = image.size
// gw: not working here, too early
DispatchQueue.main.async{
self.setZoomScale()
}
}

Take screenshots programmatically of entire scrollable area in iOS [duplicate]

I have a UIScrollView decendent that implements a takeScreenshot method that looks like this:
-(void)takeScreenshot {
CGRect contextRect = CGRectMake(0, 0, 768, 1004);
UIGraphicsBeginImageContext(contextRect.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// do something with the viewImage here.
}
This basically moves to the top of the scroll view, and takes a screenshot of the visible area. It works fine when the iPad is oriented portrait, but when it's in landscape the bottom of the image is cut off (as the height of the visible area is only 748, not 1004).
Is it possible to get a snapshot of the UIScrollView, including areas not on screen? Or do I need to scroll the view down, take a second photo and stitch them together?
Here is code that works ...
- (IBAction) renderScrollViewToImage
{
UIImage* image = nil;
UIGraphicsBeginImageContext(_scrollView.contentSize);
{
CGPoint savedContentOffset = _scrollView.contentOffset;
CGRect savedFrame = _scrollView.frame;
_scrollView.contentOffset = CGPointZero;
_scrollView.frame = CGRectMake(0, 0, _scrollView.contentSize.width, _scrollView.contentSize.height);
[_scrollView.layer renderInContext: UIGraphicsGetCurrentContext()];
image = UIGraphicsGetImageFromCurrentImageContext();
_scrollView.contentOffset = savedContentOffset;
_scrollView.frame = savedFrame;
}
UIGraphicsEndImageContext();
if (image != nil) {
[UIImagePNGRepresentation(image) writeToFile: #"/tmp/test.png" atomically: YES];
system("open /tmp/test.png");
}
}
The last few lines simply write the image to /tmp/test.png and then opens it in Preview.app. This obviously only works on in the Simulator :-)
Complete project in the ScrollViewScreenShot Github Repository
For me, the currently accepted answer from Stefan Arentz didn't work.
I had to implement this on iOS 8 and above, and tested on the iPhone. The accepted answer just renders the visible part of a scroll view, while the rest of image remains blank.
I tried fixing this using drawViewHierarchyInRect - no luck. Depending on afterScreenUpdates being true or false I got stretched part of image or only part of the contents.
The only way I've found to achieve correct snapshotting of a UIScrollView's entire contents is to add it to another temporary view and then render it.
Sample code is below (scrollview is outlet in my VC)
func getImageOfScrollView() -> UIImage {
var image = UIImage()
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.mainScreen().scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset
let savedFrame = self.scrollView.frame
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPointZero
// set frame to content size
self.scrollView.frame = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clearColor()
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.renderInContext(UIGraphicsGetCurrentContext())
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset
self.scrollView.frame = savedFrame
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext()
return image
}
Working Example of UIView Extension with handling for UIScrollView:
extension UIView {
func screenshot() -> UIImage {
if(self is UIScrollView) {
let scrollView = self as! UIScrollView
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
UIGraphicsBeginImageContext(scrollView.contentSize)
scrollView.contentOffset = .zero
self.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
return image!
}
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
I took this solution from #Roopesh Mittal's answer and made it safer/cleaner.
Swift 5 compatible
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
let savedContentOffset = contentOffset
let savedFrame = frame
UIGraphicsBeginImageContext(contentSize)
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
contentOffset = savedContentOffset
frame = savedFrame
return image
}
}
A refined Swift 4.x/5.0 version, based on #RyanG 's answer:
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
// begin image context
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
// save the orginal offset & frame
let savedContentOffset = contentOffset
let savedFrame = frame
// end ctx, restore offset & frame before returning
defer {
UIGraphicsEndImageContext()
contentOffset = savedContentOffset
frame = savedFrame
}
// change the offset & frame so as to include all content
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
}
In iOS 13 I have ran into issue that this line won't work
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
to fix the issue, I am removing scrollview from parent and then attaching in after taking the screenshot.
Full Code:
func screenshot() -> UIImage? {
let savedContentOffset = contentOffset
let savedFrame = frame
defer {
contentOffset = savedContentOffset
frame = savedFrame
}
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
let image = UIGraphicsImageRenderer(bounds: CGRect(origin: .zero, size: contentSize)).image { renderer in
let context = renderer.cgContext
layer.render(in: context)
}
return image
}
Get Screenshot:
func getScreenshot() -> UIImage? {
scrollView.removeFromSuperview()
let image = scrollView.screenshot()
addScrollView()
return image
}
SWIFT 3 version:
func snapshot() -> UIImage?
{
UIGraphicsBeginImageContext(scrollView.contentSize)
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
scrollView.contentOffset = CGPoint.zero
scrollView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
scrollView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
This worked for me
As many have pointed, current solution doesn't work, and others solutions suggest removing scrollview from superview, which leads into loosing all the constraints.
Here I'm temporarily disabling all constraints related to scroll view and turn them on after screenshot is taken:
extension UIScrollView {
func screenshot() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
// save the orginal offset, take a ref to all constraints related to the view
let savedContentOffset = contentOffset
let actualConstraints = relatedConstraints()
// deactivate non needed constraints so they won't stop us from resiging scroll view
NSLayoutConstraint.deactivate(actualConstraints)
// enable auth generated constraints based on the frame
translatesAutoresizingMaskIntoConstraints = true
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
contentOffset = .zero
defer {
UIGraphicsEndImageContext()
// reset original constraints
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(actualConstraints)
// layout superview needed before resetting content offset
superview?.layoutIfNeeded()
contentOffset = savedContentOffset
}
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
}
extension UIView {
func relatedConstraints() -> [NSLayoutConstraint] {
var constraints = self.constraints
var parent = superview
while parent != nil {
constraints.append(contentsOf: parent!.constraints.filter { $0.firstItem === self || $0.secondItem === self })
parent = parent!.superview
}
return constraints
}
}
SWIFT 3 version thanks to #gleb vodovozov:
func getImageOfScrollView()->UIImage{
var image = UIImage();
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.main.scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset;
let savedFrame = self.scrollView.frame;
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPoint.zero;
// set frame to content size
self.scrollView.frame = CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clear
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.render(in: UIGraphicsGetCurrentContext()!)
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()!;
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset;
self.scrollView.frame = savedFrame;
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext();
return image
}
Here's another way of doing it, which takes the zoom level into account. I have a scrollview with 4 different UIImageView layers in it, and I want to take a screenshot of their current state:
float theScale = 1.0f / theScrollView.zoomScale;
// The viewing rectangle in absolute coordinates
CGRect visibleArea = CGRectMake((int)(theScrollView.contentOffset.x * theScale), (int)(theScrollView.contentOffset.y * theScale),
(int)(theScrollView.bounds.size.width * theScale), (int)(theScrollView.bounds.size.height * theScale));
NSArray *layers = [NSArray arrayWithObjects:imageLayer1, imageLayer2, imageLayer3, imageLayer4, nil];
UIGraphicsBeginImageContext(visibleArea.size);
for (UIImageView *layer in layers) {
CALayer *coreLayer = layer.layer;
coreLayer.bounds = CGRectMake(layer.frame.origin.x - visibleArea.origin.x, layer.frame.origin.y - visibleArea.origin.y, layer.frame.size.width, layer.frame.size.height);
[coreLayer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
This takes the screenshot in absolute coordinates. That is, if you have a 2048*2048 image in the scrollview and you can see about a quarter of it, then regardless of the resolution of your screen it would take a screenshot of 512*512. If you want to take a screenshot at your screen resolution (say, 320*480) then you have to adjust the image as follows, directly after the above code:
UIGraphicsBeginImageContext(theScrollView.frame.size);
[screenshot drawInRect:CGRectMake(0, 0, theScrollView.frame.size.width, theScrollView.frame.size.height)];
UIImage *smallScreenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
If you don't want to expand your scroll view beyond the entire screen (and it won't work with autolayout anyway) there's a better way.
You can use core graphics transforms in conjunction with the contentOffset of the scroll view to accomplish the same thing.
//
// ScrollViewSnapshotter.swift
// ScrollViewSnapshotter
//
// Created by Moshe Berman on 4/10/16.
// Copyright © 2016 Moshe Berman. All rights reserved.
//
import UIKit
class ScrollViewSnapshotter: NSObject {
func PDFWithScrollView(scrollview: UIScrollView) -> NSData {
/**
* Step 1: The first thing we need is the default origin and size of our pages.
* Since bounds always start at (0, 0) and the scroll view's bounds give us
* the correct size for the visible area, we can just use that.
*
* In the United States, a standard printed page is 8.5 inches by 11 inches,
* but when generating a PDF it's simpler to keep the page size matching the
* visible area of the scroll view. We can let our printer software (such
* as the Preview app on OS X or the Printer app on iOS) do the scaling.
*
* If we wanted to scale ourselves, we could multiply each of those
* numbers by 72, to get the number of points for each dimension.
* We would have to change how we generated the the pages below, so
* for simplicity, we're going to stick to one page per screenful of content.
*/
let pageDimensions = scrollview.bounds
/**
* Step 2: Now we need to know how many pages we will need to fit our content.
* To get this, we divide our scroll views dimensions by the size
* of each page, in either direction.
* We also need to round up, so that the pages don't get clipped.
*/
let pageSize = pageDimensions.size
let totalSize = scrollview.contentSize
let numberOfPagesThatFitHorizontally = Int(ceil(totalSize.width / pageSize.width))
let numberOfPagesThatFitVertically = Int(ceil(totalSize.height / pageSize.height))
/**
* Step 3: Set up a Core Graphics PDF context.
*
* First we create a backing store for the PDF data, then
* pass it and the page dimensions to Core Graphics.
*
* We could pass in some document information here, which mostly cover PDF metadata,
* including author name, creator name (our software) and a password to
* require when viewing the PDF file.
*
* Also note that we can use UIGraphicsBeginPDFContextToFile() instead,
* which writes the PDF to a specified path. I haven't played with it, so
* I don't know if the data is written all at once, or as each page is closed.
*/
let outputData = NSMutableData()
UIGraphicsBeginPDFContextToData(outputData, pageDimensions, nil)
/**
* Step 4: Remember some state for later.
* Then we need to clear the content insets, so that our
* core graphics layer and our content offset match up.
* We don't need to reset the content offset, because that
* happens implicitly, in the loop below.
*/
let savedContentOffset = scrollview.contentOffset
let savedContentInset = scrollview.contentInset
scrollview.contentInset = UIEdgeInsetsZero
/**
* Step 6: Now we loop through the pages and generate the data for each page.
*/
if let context = UIGraphicsGetCurrentContext()
{
for indexHorizontal in 0 ..< numberOfPagesThatFitHorizontally
{
for indexVertical in 0 ..< numberOfPagesThatFitVertically
{
/**
* Step 6a: Start a new page.
*
* This automatically closes the previous page.
* There's a similar method UIGraphicsBeginPDFPageWithInfo,
* which allows you to configure the rectangle of the page and
* other metadata.
*/
UIGraphicsBeginPDFPage()
/**
* Step 6b:The trick here is to move the visible portion of the
* scroll view *and* adjust the core graphics context
* appropriately.
*
* Consider that the viewport of the core graphics context
* is attached to the top of the scroll view's content view
* and we need to push it in the opposite direction as we scroll.
* Further, anything not inside of the visible area of the scroll
* view is clipped, so scrolling will move the core graphics viewport
* out of the rendered area, producing empty pages.
*
* To counter this, we scroll the next screenful into view, and adjust
* the core graphics context. Note that core graphics uses a coordinate
* system which has the y coordinate decreasing as we go from top to bottom.
* This is the opposite of UIKit (although it matches AppKit on OS X.)
*/
let offsetHorizontal = CGFloat(indexHorizontal) * pageSize.width
let offsetVertical = CGFloat(indexVertical) * pageSize.height
scrollview.contentOffset = CGPointMake(offsetHorizontal, offsetVertical)
CGContextTranslateCTM(context, -offsetHorizontal, -offsetVertical) // NOTE: Negative offsets
/**
* Step 6c: Now we are ready to render the page.
*
* There are faster ways to snapshot a view, but this
* is the most straightforward way to render a layer
* into a context.
*/
scrollview.layer.renderInContext(context)
}
}
}
/**
* Step 7: End the document context.
*/
UIGraphicsEndPDFContext()
/**
* Step 8: Restore the scroll view.
*/
scrollview.contentInset = savedContentInset
scrollview.contentOffset = savedContentOffset
/**
* Step 9: Return the data.
* You can write it to a file, or display it the user,
* or even pass it to iOS for sharing.
*/
return outputData
}
}
Here's a blog post I wrote explaining the process.
The process for generating a PDF is very similar to snapshotting an image, except instead of pages, you'd need to make one large canvas that matches the size of the scroll view and then grab the contents in chunks.
I have found below code and its working for me. try this ..
extension UIView {
func capture() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
drawHierarchy(in: self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}}
Normally I wouldn't recommend using a library but... Use the SnapshotKit. It works like a charm and The code looks alright too. Using it is straight forward:
Objective-C:
UIImage *tableViewScreenShot = [yourTableView takeSnapshotOfFullContent];
Swift:
let tableViewScreenShot: UIImage = yourTableView.takeSnapshotOfFullContent()
Swift 5 Version
extension UIScrollView {
func takeScrollViewScreenShot() -> UIImage? {
UIGraphicsBeginImageContext(self.contentSize)
let savedContentOffset = self.contentOffset
let savedFrame = self.frame
self.contentOffset = CGPoint.zero
self.layer.frame = CGRect(x: 0, y: 0, width: self.contentSize.width, height: self.contentSize.height)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
self.contentOffset = savedContentOffset
self.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
}
It seems to me that the accepted solution can be fixed by updating scrollView.layer.frame rather than scrollView.frame, as pointed out here. I am not sure I actually understand why this works, though!
I don't know much but I can guess that if we set the size of the contextRect like this for landscape, it may work well:
CGRect contextRect = CGRectMake(0, 0, 1004, 768*2);
Because this contextRect will determine the size of the UIGraphicsBeginImageContext so I hope that double the height can solve your problem

Does Core Graphics' transformation matrix work differently than iPad?

I've have a class that snapshots the contents of a scroll view by changing the contentOffset of the scroll view and translating the current transformation matrix, as it creates new pages in a PDF context. The result is a PDF that contains all of the contents of the scroll view, in PDF-page-sized chunks.
On iPad, this works in both orientations and both directions, but on iPhone, the snapshot fails (an empty PDF is generated) when the device is in landscape orientation. I was wondering if this has to do with the way iOS handles rotation on iPhone.
Is there something that I'm missing, or doing incorrectly?
Here's my code:
class ScrollViewSnapshotter: NSObject {
func PDFWithScrollView(scrollview: UIScrollView) -> NSData {
let pageDimensions = scrollview.bounds
let pageSize = pageDimensions.size
let totalSize = scrollview.contentSize
let numberOfPagesThatFitHorizontally = Int(ceil(totalSize.width / pageSize.width))
let numberOfPagesThatFitVertically = Int(ceil(totalSize.height / pageSize.height))
let outputData = NSMutableData()
UIGraphicsBeginPDFContextToData(outputData, pageDimensions, nil)
let savedContentOffset = scrollview.contentOffset
let savedContentInset = scrollview.contentInset
scrollview.contentInset = UIEdgeInsetsZero
if let context = UIGraphicsGetCurrentContext()
{
for indexHorizontal in 0 ..< numberOfPagesThatFitHorizontally
{
for indexVertical in 0 ..< numberOfPagesThatFitVertically
{
UIGraphicsBeginPDFPage()
let offsetHorizontal = CGFloat(indexHorizontal) * pageSize.width
let offsetVertical = CGFloat(indexVertical) * pageSize.height
scrollview.contentOffset = CGPointMake(offsetHorizontal, offsetVertical)
CGContextTranslateCTM(context, -offsetHorizontal, -offsetVertical) // NOTE: Negative offsets
scrollview.layer.renderInContext(context)
}
}
}
UIGraphicsEndPDFContext()
scrollview.contentInset = savedContentInset
scrollview.contentOffset = savedContentOffset
return outputData
}
}
I'm wondering if iPad doesn't do the same transforms on the current transformation matrix that the iPhone does. Is this true?
You should use CGAffineTransform. The affine transform functions operate on and return a CGAffineTransform data structure. The affine transform functions for Quartz operate on matrices, not on CTMs. You should use these ones to construct a matrix itself you then apply it to the CTM by calling a concatenation function CGContextConcatCTM.
Look at Interface Orientation article. There are some useful tips on transformation matrix.
Or you can use an extension:
extension CGAffineTransform {
public func translatedBy(x tx: CGFloat, y ty: CGFloat) -> CGAffineTransform
public func rotate(by angle: CGFloat) -> CGAffineTransform
public func scaledBy(x sx: CGFloat, y sy: CGFloat) -> CGAffineTransform
}
If you scroll manually before taking a PDF, you'll see incorrect output on iPad as well.
You have to account for scrollView.bounds.offset:
CGContextTranslateCTM(
context,
-offsetHorizontal + pageDimensions.minX,
-offsetVertical - pageDimensions.minY)
I think this should do it for you... just add one line inside your loop:
if let context = UIGraphicsGetCurrentContext()
{
for indexHorizontal in 0 ..< numberOfPagesThatFitHorizontally
{
for indexVertical in 0 ..< numberOfPagesThatFitVertically
{
UIGraphicsBeginPDFPage()
let offsetHorizontal = CGFloat(indexHorizontal) * pageSize.width
let offsetVertical = CGFloat(indexVertical) * pageSize.height
scrollview.contentOffset = CGPointMake(offsetHorizontal, offsetVertical)
CGContextTranslateCTM(context, -offsetHorizontal, -offsetVertical) // NOTE: Negative offsets
// Translate again, with the original Content Offsets
// Positive X, Negative Y
CGContextTranslateCTM(context, savedContentOffset.x, -savedContentOffset.y)
scrollview.layer.renderInContext(context)
}
}
}

How to capture the whole content of a UIScrollView? [duplicate]

I have a UIScrollView decendent that implements a takeScreenshot method that looks like this:
-(void)takeScreenshot {
CGRect contextRect = CGRectMake(0, 0, 768, 1004);
UIGraphicsBeginImageContext(contextRect.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// do something with the viewImage here.
}
This basically moves to the top of the scroll view, and takes a screenshot of the visible area. It works fine when the iPad is oriented portrait, but when it's in landscape the bottom of the image is cut off (as the height of the visible area is only 748, not 1004).
Is it possible to get a snapshot of the UIScrollView, including areas not on screen? Or do I need to scroll the view down, take a second photo and stitch them together?
Here is code that works ...
- (IBAction) renderScrollViewToImage
{
UIImage* image = nil;
UIGraphicsBeginImageContext(_scrollView.contentSize);
{
CGPoint savedContentOffset = _scrollView.contentOffset;
CGRect savedFrame = _scrollView.frame;
_scrollView.contentOffset = CGPointZero;
_scrollView.frame = CGRectMake(0, 0, _scrollView.contentSize.width, _scrollView.contentSize.height);
[_scrollView.layer renderInContext: UIGraphicsGetCurrentContext()];
image = UIGraphicsGetImageFromCurrentImageContext();
_scrollView.contentOffset = savedContentOffset;
_scrollView.frame = savedFrame;
}
UIGraphicsEndImageContext();
if (image != nil) {
[UIImagePNGRepresentation(image) writeToFile: #"/tmp/test.png" atomically: YES];
system("open /tmp/test.png");
}
}
The last few lines simply write the image to /tmp/test.png and then opens it in Preview.app. This obviously only works on in the Simulator :-)
Complete project in the ScrollViewScreenShot Github Repository
For me, the currently accepted answer from Stefan Arentz didn't work.
I had to implement this on iOS 8 and above, and tested on the iPhone. The accepted answer just renders the visible part of a scroll view, while the rest of image remains blank.
I tried fixing this using drawViewHierarchyInRect - no luck. Depending on afterScreenUpdates being true or false I got stretched part of image or only part of the contents.
The only way I've found to achieve correct snapshotting of a UIScrollView's entire contents is to add it to another temporary view and then render it.
Sample code is below (scrollview is outlet in my VC)
func getImageOfScrollView() -> UIImage {
var image = UIImage()
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.mainScreen().scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset
let savedFrame = self.scrollView.frame
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPointZero
// set frame to content size
self.scrollView.frame = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clearColor()
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.renderInContext(UIGraphicsGetCurrentContext())
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset
self.scrollView.frame = savedFrame
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext()
return image
}
Working Example of UIView Extension with handling for UIScrollView:
extension UIView {
func screenshot() -> UIImage {
if(self is UIScrollView) {
let scrollView = self as! UIScrollView
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
UIGraphicsBeginImageContext(scrollView.contentSize)
scrollView.contentOffset = .zero
self.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
return image!
}
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
I took this solution from #Roopesh Mittal's answer and made it safer/cleaner.
Swift 5 compatible
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
let savedContentOffset = contentOffset
let savedFrame = frame
UIGraphicsBeginImageContext(contentSize)
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
contentOffset = savedContentOffset
frame = savedFrame
return image
}
}
A refined Swift 4.x/5.0 version, based on #RyanG 's answer:
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
// begin image context
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
// save the orginal offset & frame
let savedContentOffset = contentOffset
let savedFrame = frame
// end ctx, restore offset & frame before returning
defer {
UIGraphicsEndImageContext()
contentOffset = savedContentOffset
frame = savedFrame
}
// change the offset & frame so as to include all content
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
}
In iOS 13 I have ran into issue that this line won't work
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
to fix the issue, I am removing scrollview from parent and then attaching in after taking the screenshot.
Full Code:
func screenshot() -> UIImage? {
let savedContentOffset = contentOffset
let savedFrame = frame
defer {
contentOffset = savedContentOffset
frame = savedFrame
}
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
let image = UIGraphicsImageRenderer(bounds: CGRect(origin: .zero, size: contentSize)).image { renderer in
let context = renderer.cgContext
layer.render(in: context)
}
return image
}
Get Screenshot:
func getScreenshot() -> UIImage? {
scrollView.removeFromSuperview()
let image = scrollView.screenshot()
addScrollView()
return image
}
SWIFT 3 version:
func snapshot() -> UIImage?
{
UIGraphicsBeginImageContext(scrollView.contentSize)
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
scrollView.contentOffset = CGPoint.zero
scrollView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
scrollView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
This worked for me
As many have pointed, current solution doesn't work, and others solutions suggest removing scrollview from superview, which leads into loosing all the constraints.
Here I'm temporarily disabling all constraints related to scroll view and turn them on after screenshot is taken:
extension UIScrollView {
func screenshot() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
// save the orginal offset, take a ref to all constraints related to the view
let savedContentOffset = contentOffset
let actualConstraints = relatedConstraints()
// deactivate non needed constraints so they won't stop us from resiging scroll view
NSLayoutConstraint.deactivate(actualConstraints)
// enable auth generated constraints based on the frame
translatesAutoresizingMaskIntoConstraints = true
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
contentOffset = .zero
defer {
UIGraphicsEndImageContext()
// reset original constraints
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(actualConstraints)
// layout superview needed before resetting content offset
superview?.layoutIfNeeded()
contentOffset = savedContentOffset
}
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
}
extension UIView {
func relatedConstraints() -> [NSLayoutConstraint] {
var constraints = self.constraints
var parent = superview
while parent != nil {
constraints.append(contentsOf: parent!.constraints.filter { $0.firstItem === self || $0.secondItem === self })
parent = parent!.superview
}
return constraints
}
}
SWIFT 3 version thanks to #gleb vodovozov:
func getImageOfScrollView()->UIImage{
var image = UIImage();
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.main.scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset;
let savedFrame = self.scrollView.frame;
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPoint.zero;
// set frame to content size
self.scrollView.frame = CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clear
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.render(in: UIGraphicsGetCurrentContext()!)
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()!;
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset;
self.scrollView.frame = savedFrame;
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext();
return image
}
Here's another way of doing it, which takes the zoom level into account. I have a scrollview with 4 different UIImageView layers in it, and I want to take a screenshot of their current state:
float theScale = 1.0f / theScrollView.zoomScale;
// The viewing rectangle in absolute coordinates
CGRect visibleArea = CGRectMake((int)(theScrollView.contentOffset.x * theScale), (int)(theScrollView.contentOffset.y * theScale),
(int)(theScrollView.bounds.size.width * theScale), (int)(theScrollView.bounds.size.height * theScale));
NSArray *layers = [NSArray arrayWithObjects:imageLayer1, imageLayer2, imageLayer3, imageLayer4, nil];
UIGraphicsBeginImageContext(visibleArea.size);
for (UIImageView *layer in layers) {
CALayer *coreLayer = layer.layer;
coreLayer.bounds = CGRectMake(layer.frame.origin.x - visibleArea.origin.x, layer.frame.origin.y - visibleArea.origin.y, layer.frame.size.width, layer.frame.size.height);
[coreLayer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
This takes the screenshot in absolute coordinates. That is, if you have a 2048*2048 image in the scrollview and you can see about a quarter of it, then regardless of the resolution of your screen it would take a screenshot of 512*512. If you want to take a screenshot at your screen resolution (say, 320*480) then you have to adjust the image as follows, directly after the above code:
UIGraphicsBeginImageContext(theScrollView.frame.size);
[screenshot drawInRect:CGRectMake(0, 0, theScrollView.frame.size.width, theScrollView.frame.size.height)];
UIImage *smallScreenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
If you don't want to expand your scroll view beyond the entire screen (and it won't work with autolayout anyway) there's a better way.
You can use core graphics transforms in conjunction with the contentOffset of the scroll view to accomplish the same thing.
//
// ScrollViewSnapshotter.swift
// ScrollViewSnapshotter
//
// Created by Moshe Berman on 4/10/16.
// Copyright © 2016 Moshe Berman. All rights reserved.
//
import UIKit
class ScrollViewSnapshotter: NSObject {
func PDFWithScrollView(scrollview: UIScrollView) -> NSData {
/**
* Step 1: The first thing we need is the default origin and size of our pages.
* Since bounds always start at (0, 0) and the scroll view's bounds give us
* the correct size for the visible area, we can just use that.
*
* In the United States, a standard printed page is 8.5 inches by 11 inches,
* but when generating a PDF it's simpler to keep the page size matching the
* visible area of the scroll view. We can let our printer software (such
* as the Preview app on OS X or the Printer app on iOS) do the scaling.
*
* If we wanted to scale ourselves, we could multiply each of those
* numbers by 72, to get the number of points for each dimension.
* We would have to change how we generated the the pages below, so
* for simplicity, we're going to stick to one page per screenful of content.
*/
let pageDimensions = scrollview.bounds
/**
* Step 2: Now we need to know how many pages we will need to fit our content.
* To get this, we divide our scroll views dimensions by the size
* of each page, in either direction.
* We also need to round up, so that the pages don't get clipped.
*/
let pageSize = pageDimensions.size
let totalSize = scrollview.contentSize
let numberOfPagesThatFitHorizontally = Int(ceil(totalSize.width / pageSize.width))
let numberOfPagesThatFitVertically = Int(ceil(totalSize.height / pageSize.height))
/**
* Step 3: Set up a Core Graphics PDF context.
*
* First we create a backing store for the PDF data, then
* pass it and the page dimensions to Core Graphics.
*
* We could pass in some document information here, which mostly cover PDF metadata,
* including author name, creator name (our software) and a password to
* require when viewing the PDF file.
*
* Also note that we can use UIGraphicsBeginPDFContextToFile() instead,
* which writes the PDF to a specified path. I haven't played with it, so
* I don't know if the data is written all at once, or as each page is closed.
*/
let outputData = NSMutableData()
UIGraphicsBeginPDFContextToData(outputData, pageDimensions, nil)
/**
* Step 4: Remember some state for later.
* Then we need to clear the content insets, so that our
* core graphics layer and our content offset match up.
* We don't need to reset the content offset, because that
* happens implicitly, in the loop below.
*/
let savedContentOffset = scrollview.contentOffset
let savedContentInset = scrollview.contentInset
scrollview.contentInset = UIEdgeInsetsZero
/**
* Step 6: Now we loop through the pages and generate the data for each page.
*/
if let context = UIGraphicsGetCurrentContext()
{
for indexHorizontal in 0 ..< numberOfPagesThatFitHorizontally
{
for indexVertical in 0 ..< numberOfPagesThatFitVertically
{
/**
* Step 6a: Start a new page.
*
* This automatically closes the previous page.
* There's a similar method UIGraphicsBeginPDFPageWithInfo,
* which allows you to configure the rectangle of the page and
* other metadata.
*/
UIGraphicsBeginPDFPage()
/**
* Step 6b:The trick here is to move the visible portion of the
* scroll view *and* adjust the core graphics context
* appropriately.
*
* Consider that the viewport of the core graphics context
* is attached to the top of the scroll view's content view
* and we need to push it in the opposite direction as we scroll.
* Further, anything not inside of the visible area of the scroll
* view is clipped, so scrolling will move the core graphics viewport
* out of the rendered area, producing empty pages.
*
* To counter this, we scroll the next screenful into view, and adjust
* the core graphics context. Note that core graphics uses a coordinate
* system which has the y coordinate decreasing as we go from top to bottom.
* This is the opposite of UIKit (although it matches AppKit on OS X.)
*/
let offsetHorizontal = CGFloat(indexHorizontal) * pageSize.width
let offsetVertical = CGFloat(indexVertical) * pageSize.height
scrollview.contentOffset = CGPointMake(offsetHorizontal, offsetVertical)
CGContextTranslateCTM(context, -offsetHorizontal, -offsetVertical) // NOTE: Negative offsets
/**
* Step 6c: Now we are ready to render the page.
*
* There are faster ways to snapshot a view, but this
* is the most straightforward way to render a layer
* into a context.
*/
scrollview.layer.renderInContext(context)
}
}
}
/**
* Step 7: End the document context.
*/
UIGraphicsEndPDFContext()
/**
* Step 8: Restore the scroll view.
*/
scrollview.contentInset = savedContentInset
scrollview.contentOffset = savedContentOffset
/**
* Step 9: Return the data.
* You can write it to a file, or display it the user,
* or even pass it to iOS for sharing.
*/
return outputData
}
}
Here's a blog post I wrote explaining the process.
The process for generating a PDF is very similar to snapshotting an image, except instead of pages, you'd need to make one large canvas that matches the size of the scroll view and then grab the contents in chunks.
I have found below code and its working for me. try this ..
extension UIView {
func capture() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
drawHierarchy(in: self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}}
Normally I wouldn't recommend using a library but... Use the SnapshotKit. It works like a charm and The code looks alright too. Using it is straight forward:
Objective-C:
UIImage *tableViewScreenShot = [yourTableView takeSnapshotOfFullContent];
Swift:
let tableViewScreenShot: UIImage = yourTableView.takeSnapshotOfFullContent()
Swift 5 Version
extension UIScrollView {
func takeScrollViewScreenShot() -> UIImage? {
UIGraphicsBeginImageContext(self.contentSize)
let savedContentOffset = self.contentOffset
let savedFrame = self.frame
self.contentOffset = CGPoint.zero
self.layer.frame = CGRect(x: 0, y: 0, width: self.contentSize.width, height: self.contentSize.height)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
self.contentOffset = savedContentOffset
self.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
}
It seems to me that the accepted solution can be fixed by updating scrollView.layer.frame rather than scrollView.frame, as pointed out here. I am not sure I actually understand why this works, though!
I don't know much but I can guess that if we set the size of the contextRect like this for landscape, it may work well:
CGRect contextRect = CGRectMake(0, 0, 1004, 768*2);
Because this contextRect will determine the size of the UIGraphicsBeginImageContext so I hope that double the height can solve your problem

Resources