Obtener una captura de pantalla de un UIScrollView, incluidas las piezas fuera de pantalla

Tengo un descendente UIScrollView que implementa un método takeScreenshot que se ve así:

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

Esto básicamente se mueve a la parte superior de la vista de desplazamiento, y toma una captura de pantalla del área visible. Funciona bien cuando el iPad está orientado verticalmente, pero cuando está en el paisaje, la parte inferior de la imagen se corta (ya que la altura del área visible es solo 748, no 1004).

¿Es posible obtener una instantánea de UIScrollView , incluidas las áreas que no están en la pantalla? ¿O necesito desplazar la vista hacia abajo, tomar una segunda foto y unirlos?

Aquí hay un código que funciona …

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

Las últimas líneas simplemente escriben la imagen en /tmp/test.png y luego la abren en Preview.app. Obviamente, esto solo funciona en el simulador 🙂

Proyecto completo en ScrollViewScreenShot Github Repository

para mí, la respuesta https://stackoverflow.com/a/3539944/4164443 no funcionó. Tuve la tarea de implementar esto en iOS 8.

En realidad, este método funciona para iPhone, pero iPad (tanto simu como dispositivo real) es otro caso. Simplemente representa la parte visible y el rest de la imagen está en blanco.

Intenté con drawViewHierarchyInRect – sin suerte. Dependiendo de que afterScreenUpdates sea true o false me estiraron parte de la imagen o solo una parte de la misma.

Por lo tanto, la única forma que encontré para lograr una captura de pantalla correcta es agregar scrollview a otra vista temporal y renderizarla.

El código de muestra está debajo (scrollview es outlet en mi 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) // restre saved settings self.scrollView.contentOffset = savedContentOffset; self.scrollView.frame = savedFrame; self.scrollView.backgroundColor = savedBackgroundColor UIGraphicsEndImageContext(); return image } 

Ejemplo de trabajo de la extensión UIView con manejo para 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! } } 

Tomé la solución anterior de @Roopesh Mittal y la hice más segura / limpia.

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

Si no desea expandir su vista de desplazamiento más allá de toda la pantalla (y de todos modos no funcionará con el diseño automático) hay una mejor manera.

Puede usar transformaciones de gráficos centrales junto con contentOffset de la vista de desplazamiento para lograr lo mismo.

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

Aquí hay una publicación de blog que escribí explicando el proceso.

El proceso para generar un PDF es muy similar a tomar una instantánea de una imagen, excepto que en lugar de páginas, debe hacer un canvas grande que coincida con el tamaño de la vista de desplazamiento y luego tomar el contenido en fragmentos.

Versión de 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 } 

Esto funcionó para mí

No sé mucho, pero puedo adivinar que si establecemos el tamaño de contextRect como este para el paisaje, puede funcionar bien:

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

Porque este contextRect determinará el tamaño de UIGraphicsBeginImageContext así que espero que el doble de altura pueda resolver su problema

Aquí hay otra forma de hacerlo, que tiene en cuenta el nivel de zoom. Tengo una vista de desplazamiento con 4 capas UIImageView diferentes en ella, y quiero hacer una captura de pantalla de su estado actual:

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

Esto toma la captura de pantalla en coordenadas absolutas. Es decir, si tiene una imagen de 2048 * 2048 en la vista de desplazamiento y puede ver aproximadamente una cuarta parte de ella, independientemente de la resolución de su pantalla, tomaría una captura de pantalla de 512 * 512. Si desea tomar una captura de pantalla con la resolución de su pantalla (por ejemplo, 320 * 480), debe ajustar la imagen de la siguiente manera, directamente después del código anterior:

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

Versión de SWIFT 3 gracias a @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) // restre saved settings self.scrollView.contentOffset = savedContentOffset; self.scrollView.frame = savedFrame; self.scrollView.backgroundColor = savedBackgroundColor UIGraphicsEndImageContext(); return image } 

Una versión refinada de Swift 4.1, basada en la respuesta de @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, restre 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 } }