Cómo capturar UIView a UIImage sin pérdida de calidad en la pantalla de retina

303

Mi código funciona bien para dispositivos normales pero crea imágenes borrosas en dispositivos de retina.

¿Alguien sabe una solución para mi problema?

+ (UIImage *) imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContext(view.bounds.size);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}
Daniel
fuente
borroso. Me parece que la escala correcta se perdió ...
Daniel
yo también. Se encontró con el mismo problema.
RainCast
¿Dónde estás mostrando la imagen del resultado? Como otros han explicado, creo que está renderizando la vista en una imagen con escala 1, mientras que su dispositivo tiene escala 2 o 3. Por lo tanto, está reduciendo la escala a una resolución más baja. Esta es una pérdida de calidad, pero no debe difuminarse. Pero cuando mira esta imagen nuevamente (¿en la misma pantalla?) En un dispositivo con escala 2, se convertirá de baja resolución a mayor, utilizando 2 píxeles por píxel en la captura de pantalla. Este escalado utiliza la interpolación más probable (por defecto en iOS), promediando los valores de color para los píxeles adicionales.
Hans Terje Bakke

Respuestas:

667

Cambiar del uso de UIGraphicsBeginImageContexta UIGraphicsBeginImageContextWithOptions(como se documenta en esta página ). Pase 0.0 para la escala (el tercer argumento) y obtendrá un contexto con un factor de escala igual al de la pantalla.

UIGraphicsBeginImageContextusa un factor de escala fijo de 1.0, por lo que en realidad obtienes exactamente la misma imagen en un iPhone 4 que en los otros iPhones. Apuesto a que el iPhone 4 está aplicando un filtro cuando lo escalas implícitamente o simplemente tu cerebro está notando que es menos agudo que todo lo que lo rodea.

Entonces, supongo:

#import <QuartzCore/QuartzCore.h>

+ (UIImage *)imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}

Y en Swift 4:

func image(with view: UIView) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
    defer { UIGraphicsEndImageContext() }
    if let context = UIGraphicsGetCurrentContext() {
        view.layer.render(in: context)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        return image
    }
    return nil
}
Tommy
fuente
66
La respuesta de Tommy está bien, pero aún necesita importar #import <QuartzCore/QuartzCore.h>para eliminar la renderInContext:advertencia.
gwdp
2
@WayneLiu podría especificar un factor de escala de 2.0 explícitamente, pero probablemente no obtendría exactamente una imagen de calidad de iPhone 4 porque los mapas de bits que se hayan cargado serían las versiones estándar en lugar de las versiones @ 2x. No creo que haya una solución para eso, ya que no hay forma de instruir a todos los que actualmente se aferran a una UIImagerecarga pero forzar la versión de alta resolución (y es probable que encuentres problemas debido a la menor RAM disponible en pre -redes dispositivos de todos modos).
Tommy
66
En lugar de usar el 0.0fparámetro para la escala, si es más aceptable de usar [[UIScreen mainScreen] scale], también funciona.
Adam Carter
20
@ Adam Carter scale: The scale factor to apply to the bitmap. If you specify a value of 0.0, the scale factor is set to the scale factor of the device’s main screen.Está explícitamente documentado, por lo que 0.0f es más simple y mejor en mi opinión.
cprcrack
55
Esta respuesta no está actualizada para iOS 7. Vea mi respuesta para el nuevo "mejor" método para hacer esto.
Dima
224

La respuesta actualmente aceptada está desactualizada, al menos si es compatible con iOS 7.

Esto es lo que debe usar si solo es compatible con iOS7 +:

+ (UIImage *) imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0f);
    [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
    UIImage * snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return snapshotImage;
}

Swift 4:

func imageWithView(view: UIView) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
    defer { UIGraphicsEndImageContext() }
    view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
    return UIGraphicsGetImageFromCurrentImageContext()
}

Según este artículo , puede ver que el nuevo método iOS7 drawViewHierarchyInRect:afterScreenUpdates:es muchas veces más rápido que renderInContext:.punto de referencia

Dima
fuente
55
No hay duda, eso drawViewHierarchyInRect:afterScreenUpdates:es mucho más rápido. Acabo de ejecutar un generador de perfiles de tiempo en instrumentos. Mi generación de imágenes pasó de 138ms a 27ms.
ThomasCle
99
Por alguna razón, esto no funcionó para mí cuando intentaba crear íconos personalizados para los marcadores de Google Maps; todo lo que obtuve fueron rectángulos negros :(
JakeP
66
@CarlosP has establecer afterScreenUpdates:a YES?
Dima
77
set afterScreenUpdates a YES solucionó el problema del rectángulo negro para mí
hyouuu
44
También estaba recibiendo imágenes en negro. Resultó que si llamo al código viewDidLoado viewWillAppear:las imágenes son negras; Tuve que hacerlo adentro viewDidAppear:. Así que finalmente volví a renderInContext:.
manicaesar
32

He creado una extensión Swift basada en la solución @Dima:

extension UIImage {
    class func imageWithView(view: UIView) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0)
        view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img
    }
}

EDITAR: Swift 4 versión mejorada

extension UIImage {
    class func imageWithView(_ view: UIView) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0)
        defer { UIGraphicsEndImageContext() }
        view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
        return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
    }
}

Uso:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))  
let image = UIImage.imageWithView(view)
Heberti Almeida
fuente
2
Gracias por la idea! Además, también puede diferir {UIGraphicsEndImageContext ()} inmediatamente después de comenzar el contexto y evitar tener que introducir la variable local img;)
Dennis L
¡Muchas gracias por la explicación y el uso bien detallados!
Zeus
¿Por qué es esta una classfunción que tiene una vista?
Rudolf Adamkovič
Esto funciona como una staticfunción, pero como no hay necesidad de anularlo, simplemente puede cambiarlo a una staticfunción en lugar de hacerlo class.
Heberti Almeida
25

iOS Rápido

Usando UIGraphicsImageRenderer moderno

public extension UIView {
    @available(iOS 10.0, *)
    public func renderToImage(afterScreenUpdates: Bool = false) -> UIImage {
        let rendererFormat = UIGraphicsImageRendererFormat.default()
        rendererFormat.opaque = isOpaque
        let renderer = UIGraphicsImageRenderer(size: bounds.size, format: rendererFormat)

        let snapshotImage = renderer.image { _ in
            drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates)
        }
        return snapshotImage
    }
}
Alexander Belyavskiy
fuente
@ masaldana2 tal vez aún no, pero tan pronto como comiencen a dejar de usar cosas o agreguen renderizaciones o mejoras aceleradas por hardware, es mejor que estén en los hombros de los gigantes (también conocido como API de Apple)
Juan Boero
¿Alguien sabe cuál es el rendimiento al comparar esto renderercon el viejo drawHierarchy...?
Vive el
24

Para mejorar las respuestas de @Tommy y @Dima, use la siguiente categoría para representar UIView en UIImage con fondo transparente y sin pérdida de calidad. Trabajando en iOS7. (O simplemente reutilice ese método en la implementación, reemplazandoself referencia con su imagen)

UIView + RenderViewToImage.h

#import <UIKit/UIKit.h>

@interface UIView (RenderToImage)

- (UIImage *)imageByRenderingView;

@end

UIView + RenderViewToImage.m

#import "UIView+RenderViewToImage.h"

@implementation UIView (RenderViewToImage)

- (UIImage *)imageByRenderingView
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
    UIImage * snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return snapshotImage;
}

@end
Glogo
fuente
1
Si uso drawViewHierarchyInRect con afterScreenUpdates: SÍ, la relación de aspecto de mi imagen ha cambiado y la imagen se distorsiona.
Confile
¿Esto sucede cuando se cambia su contenido uiview? En caso afirmativo, intente volver a generar este UIImage nuevamente. Lo siento, no puedo probar esto yo mismo porque estoy en el teléfono
Glogo
Tengo el mismo problema y parece que nadie se ha dado cuenta de esto, ¿encontró alguna solución a este problema? @confile?
Reza.Ab
Esto es justo lo que necesito pero no funcionó. Intenté hacer @interfacey @implementationambos (RenderViewToImage)y agregar #import "UIView+RenderViewToImage.h"al encabezado, ¿ ViewController.hes este el uso correcto? por ejemplo UIImageView *test = [[UIImageView alloc] initWithImage:[self imageByRenderingView]]; [self addSubview:test];?
Greg
y este método ViewController.mfue la vista que traté de representar- (UIView *)testView { UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 50, 50)]; v1.backgroundColor = [UIColor redColor]; UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 150, 150)]; v2.backgroundColor = [UIColor blueColor]; [v2 addSubview:v1]; return v2; }
Greg
15

Swift 3

La solución Swift 3 (basada en la respuesta de Dima ) con la extensión UIView debería ser así:

extension UIView {
    public func getSnapshotImage() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0)
        self.drawHierarchy(in: self.bounds, afterScreenUpdates: false)
        let snapshotImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return snapshotImage
    }
}
Mehdi Hosseinzadeh
fuente
6

Extensión Swift 3.0 incorporada que admite la nueva API iOS 10.0 y el método anterior.

Nota:

  • Verificación de la versión de iOS
  • Tenga en cuenta el uso de diferir para simplificar la limpieza de contexto.
  • También aplicará la opacidad y la escala actual de la vista.
  • Nada simplemente se desenvuelve usando lo !que podría causar un bloqueo.

extension UIView
{
    public func renderToImage(afterScreenUpdates: Bool = false) -> UIImage?
    {
        if #available(iOS 10.0, *)
        {
            let rendererFormat = UIGraphicsImageRendererFormat.default()
            rendererFormat.scale = self.layer.contentsScale
            rendererFormat.opaque = self.isOpaque
            let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: rendererFormat)

            return
                renderer.image
                {
                    _ in

                    self.drawHierarchy(in: self.bounds, afterScreenUpdates: afterScreenUpdates)
                }
        }
        else
        {
            UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, self.layer.contentsScale)
            defer
            {
                UIGraphicsEndImageContext()
            }

            self.drawHierarchy(in: self.bounds, afterScreenUpdates: afterScreenUpdates)

            return UIGraphicsGetImageFromCurrentImageContext()
        }
    }
}
Leslie Godwin
fuente
5

Swift 2.0:

Usando el método de extensión:

extension UIImage{

   class func renderUIViewToImage(viewToBeRendered:UIView?) -> UIImage
   {
       UIGraphicsBeginImageContextWithOptions((viewToBeRendered?.bounds.size)!, false, 0.0)
       viewToBeRendered!.drawViewHierarchyInRect(viewToBeRendered!.bounds, afterScreenUpdates: true)
       viewToBeRendered!.layer.renderInContext(UIGraphicsGetCurrentContext()!)

       let finalImage = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()

       return finalImage
   }

}

Uso:

override func viewDidLoad() {
    super.viewDidLoad()

    //Sample View To Self.view
    let sampleView = UIView(frame: CGRectMake(100,100,200,200))
    sampleView.backgroundColor =  UIColor(patternImage: UIImage(named: "ic_120x120")!)
    self.view.addSubview(sampleView)    

    //ImageView With Image
    let sampleImageView = UIImageView(frame: CGRectMake(100,400,200,200))

    //sampleView is rendered to sampleImage
    var sampleImage = UIImage.renderUIViewToImage(sampleView)

    sampleImageView.image = sampleImage
    self.view.addSubview(sampleImageView)

 }
AG
fuente
3

Implementación Swift 3.0

extension UIView {
    func getSnapshotImage() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
        drawHierarchy(in: bounds, afterScreenUpdates: false)
        let snapshotImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return snapshotImage
    }
}
imike
fuente
3

Todas las respuestas de Swift 3 no funcionaron para mí, así que traduje la respuesta más aceptada:

extension UIImage {
    class func imageWithView(view: UIView) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
        view.layer.render(in: UIGraphicsGetCurrentContext()!)
        let img: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img!
    }
}
Marco Antonio Uzcategui Pescoz
fuente
2

Aquí hay una extensión Swift 4 UIView basada en la respuesta de @Dima.

extension UIView {
   func snapshotImage() -> UIImage? {
       UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
       drawHierarchy(in: bounds, afterScreenUpdates: false)
       let image = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()
       return image
   }
}
danfordham
fuente
2

Para Swift 5.1 puede usar esta extensión:

extension UIView {

    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)

        return renderer.image {
            rendererContext in

            layer.render(in: rendererContext.cgContext)
        }
    }
}
Andrey M.
fuente
1

UIGraphicsImageRendereres una API relativamente nueva, introducida en iOS 10. Usted construye un UIGraphicsImageRenderer especificando un tamaño de punto . El método de imagen toma un argumento de cierre y devuelve un mapa de bits que resulta de ejecutar el cierre pasado. En este caso, el resultado es la imagen original reducida para dibujar dentro de los límites especificados.

https://nshipster.com/image-resizing/

Así que asegúrese de que el tamaño al que está pasando UIGraphicsImageRendererson puntos, no píxeles.

Si sus imágenes son más grandes de lo que espera, debe dividir su tamaño por el factor de escala.

pkamb
fuente
Tengo curiosidad, ¿podrías ayudarme con esto por favor? stackoverflow.com/questions/61247577/… Estoy usando UIGraphicsImageRenderer, el problema es que quiero que la imagen exportada sea más grande que la vista real en el dispositivo. Pero con el tamaño deseado, las subvistas (etiquetas) no son lo suficientemente nítidas, por lo que hay pérdida de calidad.
Ondřej Korol
0
- (UIImage*)screenshotForView:(UIView *)view
{
    UIGraphicsBeginImageContext(view.bounds.size);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // hack, helps w/ our colors when blurring
    NSData *imageData = UIImageJPEGRepresentation(image, 1); // convert to jpeg
    image = [UIImage imageWithData:imageData];

    return image;
}
PJRadadiya
fuente
-2

En este método, simplemente pase un objeto de vista y devolverá un objeto UIImage.

-(UIImage*)getUIImageFromView:(UIView*)yourView
{
 UIGraphicsBeginImageContext(yourView.bounds.size);
 [yourView.layer renderInContext:UIGraphicsGetCurrentContext()];
 UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 return image;
}
Atanu Mondal
fuente
-6

Agregue esto al método a la categoría UIView

- (UIImage*) capture {
    UIGraphicsBeginImageContext(self.bounds.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.layer renderInContext:context];
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
}
Shafraz Buhary
fuente
2
Hola, si bien esto puede responder a la pregunta, tenga en cuenta que otros usuarios pueden no estar tan bien informados como usted. ¿Por qué no agrega una pequeña explicación de por qué funciona este código? ¡Gracias!
Vogel612