Getting a screenshot of a UIScrollView, including offscreen parts

Viewed 33789

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?

16 Answers

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
    }
}

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!

Related