Getting a screenshot of a UIScrollView, including offscreen parts

前端 未结 12 2122
遥遥无期
遥遥无期 2020-11-22 17:31

I have a UIScrollView decendent that implements a takeScreenshot method that looks like this:

-(void)takeScreenshot {  
  CGRect contextRect  =          


        
相关标签:
12条回答
  • 2020-11-22 17:57

    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.

    0 讨论(0)
  • 2020-11-22 17:59

    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
    }
    
    0 讨论(0)
  • 2020-11-22 18:00

    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
        }
    }
    
    0 讨论(0)
  • 2020-11-22 18:01

    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!
    
            }
    }
    
    0 讨论(0)
  • 2020-11-22 18:06

    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
    }}
    
    0 讨论(0)
  • 2020-11-22 18:06

    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

    0 讨论(0)
提交回复
热议问题