Como capturar UIView para UIImage sem perda de qualidade na exibição da retina


303

Meu código funciona bem para dispositivos normais, mas cria imagens borradas em dispositivos retina.

Alguém conhece uma solução para o meu problema?

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

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}

embaçado. Parece-me a escala certa se perdeu ...
Daniel

eu também. encontrou o mesmo problema.
RainCast

Onde você está exibindo a imagem resultante? Como outros explicaram, acredito que você está renderizando a exibição em uma imagem com escala 1, enquanto o dispositivo tem escala 2 ou 3. Portanto, você está diminuindo o tamanho para uma resolução mais baixa. Isso é uma perda de qualidade, mas não deve ficar desfocada. Porém, quando você estiver visualizando a imagem novamente (na mesma tela?) Em um dispositivo com escala 2, ela será convertida da baixa resolução para a mais alta, usando 2 pixels por pixel na captura de tela. Esse upscaling usa interpolação provavelmente (padrão no iOS), calculando a média dos valores de cores para os pixels extras.
Hans Terje Bakke

Respostas:


667

Alterne do uso de UIGraphicsBeginImageContextpara UIGraphicsBeginImageContextWithOptions(conforme documentado nesta página ). Passe 0,0 para a escala (o terceiro argumento) e você obterá um contexto com um fator de escala igual ao da tela.

UIGraphicsBeginImageContextusa um fator de escala fixa de 1,0, então você está obtendo exatamente a mesma imagem em um iPhone 4 que nos outros iPhones. Aposto que o iPhone 4 está aplicando um filtro quando você o escala implicitamente ou apenas seu cérebro percebe que é menos nítido do que tudo ao seu redor.

Então eu acho:

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

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

6
A resposta do Tommy está correta, mas você ainda precisa importar #import <QuartzCore/QuartzCore.h>para remover o renderInContext:aviso.
Gwdp

2
@WayneLiu, você poderia especificar um fator de escala de 2.0 explicitamente, mas provavelmente não obteria exatamente uma imagem de qualidade do iPhone 4, porque os bitmaps carregados seriam as versões padrão e não as versões @ 2x. Eu não acho que exista uma solução para isso, pois não há como instruir todos os que estão atualmente no momento UIImagede recarregar, mas forçar a versão de alta resolução (e você provavelmente enfrentaria problemas devido à menor RAM disponível na pré-instalação). dispositivos -retina de qualquer maneira).
Tommy

6
Em vez de usar o 0.0fparâmetro for the scale, é mais aceitável usar [[UIScreen mainScreen] scale], também 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.É explicitamente documentado, então 0.0f é mais simples e melhor na minha opinião.
Cprcrack #

5
Esta resposta está desatualizada para o iOS 7. Veja minha resposta para o novo "melhor" método para fazer isso.
Dima

224

A resposta atualmente aceita está desatualizada, pelo menos se você estiver suportando o iOS 7.

Aqui está o que você deve usar se estiver suportando apenas o 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()
}

De acordo com este artigo , você pode ver que o novo método iOS7 drawViewHierarchyInRect:afterScreenUpdates:é muitas vezes mais rápido que renderInContext:.referência


5
Não há dúvida, isso drawViewHierarchyInRect:afterScreenUpdates:é muito mais rápido. Acabei de executar um criador de perfil Time em Instruments. Minha geração de imagens passou de 138ms para 27ms.
ThomasCle

9
Por algum motivo, isso não funcionou para mim quando eu estava tentando criar ícones personalizados para marcadores do Google Maps; tudo o que tenho foi rectângulos pretos :(
JakeP

6
@CarlosP você já tentou configurar afterScreenUpdates:a YES?
Dima

7
conjunto afterScreenUpdates para SIM corrigido o problema retângulo preto para mim
hyouuu

4
Eu também estava recebendo imagens em preto. Aconteceu que, se eu ligar para o código viewDidLoadou viewWillAppear:as imagens forem pretas; Eu tive que fazer isso viewDidAppear:. Então, eu também finalmente voltei renderInContext:.
Manicaesar

32

Eu criei uma extensão Swift com base na solução @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
    }
}

EDIT: Swift 4 versão melhorada

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)

2
Obrigado pela ideia! Apenas como um aparte, você também pode adiar {UIGraphicsEndImageContext ()} imediatamente após iniciar o contexto e evitar a necessidade de introduzir a variável local img;) #
1113 Dennis L

Muito obrigado pela explicação e uso bem detalhados!
Zeus

Por que essa é uma classfunção que tem uma visão?
Rudolf Adamkovič

Isso funciona como uma staticfunção, mas como não há necessidade de substituir, você pode simplesmente alterá-lo para uma staticfunção em vez de class.
Heberti Almeida 19/02

25

iOS Rápido

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

@ masaldana2 talvez ainda não, mas assim que eles começam depreciativo coisas ou adicionar hardware acelerado renderização ou melhorias é melhor você estar sobre os ombros gigantes (AKA usando últimos APIs da Apple)
Juan Boero

Alguém está ciente de qual é o desempenho comparando isso renderercom o antigo drawHierarchy..?
Vive

24

Para melhorar as respostas de @Tommy e @Dima, use a categoria a seguir para renderizar o UIView no UIImage com fundo transparente e sem perda de qualidade. Trabalhando no iOS7. (Ou apenas reutilize esse método na implementação, substituindo a selfreferência pela sua imagem)

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

1
Se eu usar o drawViewHierarchyInRect com afterScreenUpdates: SIM, minha proporção de imagem foi alterada e a imagem fica distorcida.
Confile

Isso acontece quando o conteúdo do uiview é alterado? Se sim, tente gerar novamente esta UIImage novamente. Desculpe não posso testar isso sozinho, porque eu estou no telefone
Glogo 21/04

Eu tenho o mesmo problema e parece que ninguém percebeu isso, você encontrou alguma solução para esse problema? @confile?
precisa saber é o seguinte

É exatamente isso que eu preciso, mas não funcionou. Eu tentei fazer @interfacee @implementationambos (RenderViewToImage)e adicionando #import "UIView+RenderViewToImage.h"ao cabeçalho no ViewController.hentão é este o uso correto? por exemplo UIImageView *test = [[UIImageView alloc] initWithImage:[self imageByRenderingView]]; [self addSubview:test];?
Greg

e este método em ViewController.mera a visão Tentei tornar- (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

A solução Swift 3 (com base na resposta de Dima ) com extensão UIView deve ser assim:

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

6

Extensão do Drop-in Swift 3.0 que suporta a nova API do iOS 10.0 e o método anterior.

Nota:

  • Verificação de versão do iOS
  • Observe o uso de adiar para simplificar a limpeza do contexto.
  • Também aplicará a opacidade e a escala atual da exibição.
  • Nada é apenas desembrulhado usando o !que poderia causar um acidente.

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

5

Swift 2.0:

Usando o método de extensão:

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)

 }

3

Implementação do Swift 3.0

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

3

Todas as respostas do Swift 3 não funcionaram para mim, por isso traduzi a resposta mais aceita:

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

2

Aqui está uma extensão Swift 4 UIView com base na resposta do @Dima.

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

2

Para o Swift 5.1, você pode usar esta extensão:

extension UIView {

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

        return renderer.image {
            rendererContext in

            layer.render(in: rendererContext.cgContext)
        }
    }
}

1

UIGraphicsImageRendereré uma API relativamente nova, introduzida no iOS 10. Você constrói um UIGraphicsImageRenderer especificando um tamanho de ponto . O método de imagem usa um argumento de fechamento e retorna um bitmap resultante da execução do fechamento passado. Nesse caso, o resultado é a imagem original reduzida para desenhar dentro dos limites especificados.

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

Portanto, verifique se o tamanho que você está passando UIGraphicsImageRendereré de pontos, não de pixels.

Se suas imagens forem maiores do que o esperado, você precisará dividir seu tamanho pelo fator de escala.


Estou curioso, você poderia me ajudar com isso, por favor? stackoverflow.com/questions/61247577/… Estou usando o UIGraphicsImageRenderer, o problema é que desejo que a imagem exportada tenha um tamanho maior que a visualização real no dispositivo. Porém, com as subvisões de tamanho desejado (rótulos) não são nítidas o suficiente - portanto, há perda de qualidade.
Ondřej Korol 19/04


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

-2

Neste método, basta passar um objeto de exibição e ele retornará um objeto UIImage.

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

-6

Adicione isso ao método na Categoria UIView

- (UIImage*) capture {
    UIGraphicsBeginImageContext(self.bounds.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.layer renderInContext:context];
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
}

2
Olá, embora isso possa responder à pergunta, lembre-se de que outros usuários podem não ter tanto conhecimento quanto você. Por que você não adiciona uma pequena explicação sobre por que esse código funciona? Obrigado!
precisa saber é o seguinte
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.