web-dev-qa-db-ja.com

オフスクリーンパーツを含むUIScrollViewのスクリーンショットの取得

次のようなtakeScreenshotメソッドを実装するUIScrollView decendentがあります。

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

これは基本的にスクロールビューの上部に移動し、表示領域のスクリーンショットを撮ります。 iPadが縦向きの場合は正常に機能しますが、横向きの場合は画像の下部が切り取られます(可視領域の高さは1004ではなく748にすぎないため)。

画面上にない領域を含むUIScrollViewのスナップショットを取得することはできますか?または、ビューを下にスクロールし、2枚目の写真を撮り、それらをつなぎ合わせる必要がありますか?

53
Tim Sullivan

動作するコードは次のとおりです...

- (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");
    }
}

最後の数行は、イメージを/tmp/test.pngに書き込むだけで、Preview.appで開きます。これは明らかにシミュレータでのみ動作します:-)

ScrollViewScreenShot Githubリポジトリのプロジェクトを完了します

109
Stefan Arentz

私にとって、 現在ステファン・アレンツからの回答を受け入れた は機能しませんでした。

IOS 8以降でこれを実装し、iPhoneでテストする必要がありました。受け入れられた回答は、スクロールビューの可視部分をレンダリングするだけで、残りの画像は空白のままです。

私はdrawViewHierarchyInRectを使用してこれを修正しようとしました-運はありません。 afterScreenUpdatestrueまたはfalseであることに応じて、画像の一部またはコンテンツの一部のみが引き伸ばされました。

UIScrollViewのコンテンツ全体の正しいスナップショットを実現するために私が見つけた唯一の方法は、それを別の一時ビューに追加してからレンダリングすることです。

サンプルコードは次のとおりです(scrollviewは私の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
}
17
gleb vodovozov

UIScrollViewを処理するUIView拡張機能の動作例:

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!

        }
}
10
Roopesh Mittal

@ Roopesh Mittal's answer からこのソリューションを取得し、より安全/クリーンにした。

Swift 4互換

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

洗練されたSwift 4.x/5.0バージョン、 @ RyanGの答え に基づいて:

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

Swift 3バージョン:

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
}

これは私のために働いた

3
Krishna Kirana

画面全体にスクロールビューを拡大したくない場合(とにかく自動レイアウトでは機能しません)、より良い方法があります。

同じことを達成するために、スクロールビューのcontentOffsetと組み合わせてコアグラフィック変換を使用できます。

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

ここに ブログ投稿 プロセスの説明を書きました。

PDFを生成するプロセスは、ページの代わりに、スクロールビューのサイズに一致する1つの大きなキャンバスを作成してからコンテンツを取得する必要があることを除いて、画像のスナップショットに非常に似ていますチャンクで。

2
Moshe

ズームレベルを考慮した別の方法があります。 4つの異なるUIImageViewレイヤーを含むスクロールビューがあり、現在の状態のスクリーンショットを撮りたいと思います。

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();

これにより、スクリーンショットが絶対座標で取得されます。つまり、スクロールビューに2048 * 2048の画像があり、その約4分の1を見ることができる場合、画面の解像度に関係なく、512 * 512のスクリーンショットが撮られます。スクリーン解像度(320 * 480など)でスクリーンショットを撮りたい場合は、上記のコードの直後に、次のように画像を調整する必要があります。

UIGraphicsBeginImageContext(theScrollView.frame.size);
[screenshot drawInRect:CGRectMake(0, 0, theScrollView.frame.size.width, theScrollView.frame.size.height)];
UIImage *smallScreenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
2
Adam

@gleb vodovozovのおかげでSwift 3バージョン:

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

私はあまり知りませんが、contextRectのサイズをランドスケープ用にこのように設定すると、うまくいくかもしれません:

  CGRect contextRect  = CGRectMake(0, 0, 1004, 768*2);

このcontextRectはUIGraphicsBeginImageContextのサイズを決定するので、高さを2倍にすると問題を解決できることを願っています

0
vodkhang