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;
}
Daniel
fonte
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
}
Tommy
fonte
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

Dima
fonte
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)
Heberti Almeida
fonte
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
    }
}
Alexander Belyavskiy
fonte
@ 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
Glogo
fonte
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
    }
}
Mehdi Hosseinzadeh
fonte
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()
        }
    }
}
Leslie Godwin
fonte
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)

 }
AG
fonte
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
    }
}
imike
fonte
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!
    }
}
Marco Antonio Uzcategui Pescoz
fonte
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
   }
}
Danfordham
fonte
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)
        }
    }
}
Andrey M.
fonte
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.

pkamb
fonte
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;
}
PJRadadiya
fonte
-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;
}
Atanu Mondal
fonte
-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;
}
Shafraz Buhary
fonte
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