Comment capturer UIView en UIImage sans perte de qualité sur l'écran rétine


303

Mon code fonctionne bien pour les appareils normaux, mais crée des images floues sur les appareils rétiniens.

Quelqu'un connaît-il une solution à mon problème?

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

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}

floue. Il me semble que la bonne échelle s'est perdue ...
Daniel

moi aussi. rencontré le même problème.
RainCast

Où affichez-vous l'image du résultat? Comme d'autres l'ont expliqué, je pense que vous effectuez le rendu de la vue sur une image à l'échelle 1, tandis que votre appareil a une échelle 2 ou 3. Vous réduisez donc à une résolution inférieure. Il s'agit d'une perte de qualité mais ne doit pas être floue. Mais lorsque vous regardez à nouveau cette image (sur le même écran?) Sur un appareil avec une échelle 2, elle passera de la basse résolution à la plus haute, en utilisant 2 pixels par pixel dans la capture d'écran. Cette mise à l'échelle utilise l'interpolation le plus probable (par défaut sur iOS), en faisant la moyenne des valeurs de couleur pour les pixels supplémentaires.
Hans Terje Bakke

Réponses:


667

Passez de l'utilisation de UIGraphicsBeginImageContextà UIGraphicsBeginImageContextWithOptions(comme indiqué sur cette page ). Passez 0,0 pour l'échelle (le troisième argument) et vous obtiendrez un contexte avec un facteur d'échelle égal à celui de l'écran.

UIGraphicsBeginImageContextutilise un facteur d'échelle fixe de 1,0, vous obtenez donc exactement la même image sur un iPhone 4 que sur les autres iPhones. Je parie que l'iPhone 4 applique un filtre lorsque vous le modifiez implicitement ou que votre cerveau le détecte moins net que tout ce qui l'entoure.

Donc je suppose:

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

Et dans 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
La réponse de Tommy est correcte, mais vous devez toujours importer #import <QuartzCore/QuartzCore.h>pour supprimer l' renderInContext:avertissement.
gwdp

2
@WayneLiu, vous pouvez spécifier explicitement un facteur d'échelle de 2,0, mais vous n'obtiendrez probablement pas exactement une image de qualité iPhone 4, car les bitmaps qui ont été chargés seraient les versions standard plutôt que les versions @ 2x. Je ne pense pas qu'il y ait de solution à cela car il n'y a aucun moyen de demander à tous ceux qui s'accrochent actuellement à un UIImagede recharger mais de forcer la version haute résolution (et vous vous heurteriez probablement à des problèmes en raison de la moindre RAM disponible en pré -retina quand même).
Tommy

6
Au lieu d'utiliser le 0.0fpour le paramètre d'échelle, est-il plus acceptable d'utiliser [[UIScreen mainScreen] scale], cela fonctionne aussi.
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.C'est explicitement documenté, donc 0.0f est plus simple et meilleur à mon avis.
cprcrack

5
Cette réponse est obsolète pour iOS 7. Voir ma réponse pour la nouvelle "meilleure" méthode pour ce faire.
Dima

224

La réponse actuellement acceptée est désormais obsolète, du moins si vous prenez en charge iOS 7.

Voici ce que vous devriez utiliser si vous ne supportez que 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()
}

Selon cet article , vous pouvez voir que la nouvelle méthode iOS7 drawViewHierarchyInRect:afterScreenUpdates:est beaucoup plus rapide que renderInContext:.référence


5
Il ne fait aucun doute que drawViewHierarchyInRect:afterScreenUpdates:c'est beaucoup plus rapide. Je viens de lancer un Time profiler dans Instruments. Ma génération d'images est passée de 138 ms à 27 ms.
ThomasCle

9
Pour une raison quelconque, cela n'a pas fonctionné pour moi lorsque j'essayais de créer des icônes personnalisées pour les marqueurs Google Maps; je n'ai eu que des rectangles noirs :(
JakeP

6
@CarlosP avez - vous essayé de placer afterScreenUpdates:à YES?
Dima

7
définir afterScreenUpdates sur YES a résolu le problème du rectangle noir pour moi
hyouuu

4
Je recevais également des images noires. Il s'est avéré que si j'appelle le code viewDidLoadou que viewWillAppear:les images sont noires; Je devais le faire viewDidAppear:. Je suis donc finalement revenu à renderInContext:.
manicaesar

32

J'ai créé une extension Swift basée sur la solution @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 version améliorée

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

Usage:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))  
let image = UIImage.imageWithView(view)

2
Merci pour l'idée! En passant, vous pouvez également reporter {UIGraphicsEndImageContext ()} immédiatement après le début du contexte et éviter d'avoir à introduire la variable locale img;)
Dennis L

Merci beaucoup pour l'explication et l'utilisation bien détaillées!
Zeus

Pourquoi est-ce une classfonction qui prend une vue?
Rudolf Adamkovič

Cela fonctionne comme une staticfonction mais comme il n'est pas nécessaire de la remplacer, vous pouvez simplement la changer en staticfunc au lieu de class.
Heberti Almeida

25

iOS Rapide

Utilisation de UIGraphicsImageRenderer moderne

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 peut-être pas encore, mais dès qu'ils commencent à déprécier des trucs ou à ajouter du rendu accéléré par le matériel ou des améliorations, vous feriez mieux d'être sur des épaules géantes (AKA utilisant les dernières API Apple)
Juan Boero

Quelqu'un sait-il quelle est la performance comparant cela rendererà l'ancien drawHierarchy..?
Vive le

24

Pour améliorer les réponses de @Tommy et @Dima, utilisez la catégorie suivante pour rendre UIView en UIImage avec un fond transparent et sans perte de qualité. Travailler sur iOS7. (Ou réutilisez simplement cette méthode dans l'implémentation, en remplaçant la selfréférence par votre image)

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
Si j'utilise drawViewHierarchyInRect avec afterScreenUpdates: OUI mon format d'image a changé et l'image est déformée.
confile le

Cela se produit-il lorsque votre contenu uiview est modifié? Si oui, essayez à nouveau de recréer ce UIImage. Désolé, je ne peux pas tester cela moi-même parce que je suis au téléphone
Glogo

J'ai le même problème et il semble que personne ne l'ait remarqué, avez-vous trouvé une solution à ce problème? @confile?
Reza.Ab

C'est juste ce dont j'ai besoin mais cela n'a pas fonctionné. J'ai essayé de faire @interfaceet les @implementationdeux (RenderViewToImage)et d'ajouter #import "UIView+RenderViewToImage.h"à l'en-tête ViewController.hdonc est-ce la bonne utilisation? par exemple UIImageView *test = [[UIImageView alloc] initWithImage:[self imageByRenderingView]]; [self addSubview:test];?
Greg

et cette méthode ViewController.métait la vue que j'ai essayé de rendre- (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 solution Swift 3 (basée sur la réponse de Dima ) avec l'extension UIView devrait ressembler à ceci:

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

Extension Drop-in Swift 3.0 qui prend en charge la nouvelle API iOS 10.0 et la méthode précédente.

Remarque:

  • Vérification de la version iOS
  • Notez l'utilisation de différer pour simplifier le nettoyage du contexte.
  • Appliquera également l'opacité et l'échelle actuelle de la vue.
  • Rien n'est simplement déballé en utilisant !ce qui pourrait provoquer un crash.

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:

En utilisant la méthode d'extension:

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
   }

}

Usage:

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

Implémentation de 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

Toutes les réponses de Swift 3 n'ont pas fonctionné pour moi, j'ai donc traduit la réponse la plus acceptée:

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

Voici une extension Swift 4 UIView basée sur la réponse de @Dima.

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

2

Pour Swift 5.1, vous pouvez utiliser cette extension:

extension UIView {

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

        return renderer.image {
            rendererContext in

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

1

UIGraphicsImageRendererest une API relativement nouvelle, introduite dans iOS 10. Vous construisez un UIGraphicsImageRenderer en spécifiant une taille en points . La méthode image prend un argument de fermeture et renvoie une image bitmap résultant de l'exécution de la fermeture passée. Dans ce cas, le résultat est l'image originale réduite pour dessiner dans les limites spécifiées.

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

Assurez-vous donc que la taille dans laquelle vous passez est en UIGraphicsImageRendererpoints, pas en pixels.

Si vos images sont plus grandes que prévu, vous devez diviser votre taille par le facteur d'échelle.


Je suis curieux, pourriez-vous m'aider avec ça s'il vous plait? stackoverflow.com/questions/61247577/… J'utilise UIGraphicsImageRenderer, le problème est que je veux que l'image exportée soit plus grande que la vue réelle sur l'appareil. Mais avec la taille souhaitée, les sous-vues (étiquettes) ne sont pas assez nettes - il y a donc une perte de qualité.
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;
}

-2

Dans cette méthode, passez simplement un objet de vue et il retournera un objet UIImage.

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

-6

Ajoutez cette méthode à la catégorie UIView

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

2
Hiya, bien que cela puisse répondre à la question, sachez que les autres utilisateurs ne sont peut-être pas aussi bien informés que vous. Pourquoi n'ajoutez-vous pas une petite explication sur la raison pour laquelle ce code fonctionne? Merci!
Vogel612
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.