How to capture UIView to UIImage without loss of quality on retina display
Asked Answered
F

18

313

My code works fine for normal devices but creates blurry images on retina devices.

Does anybody know a solution for my issue?

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

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}
Faiyum answered 2/12, 2010 at 11:2 Comment(3)
blurry. It seems to me the right scale got lost...Faiyum
me too. met the same issue.Nash
Where are you displaying the result image? As others have explained, I believe you are rendering the view onto an image with scale 1, while your device has scale 2 or 3. So you are downscaling to a lower resolution. This is a loss of quality but should not blur. But when you are looking at this image again (on the same screen?) on a device with scale 2 it will convert from the low resolution to higher, using 2 pixels per pixel in the screenshot. This upscaling uses interpolation most likely (default on iOS), averaging color values for the extra pixels.Fagan
E
679

Switch from use of UIGraphicsBeginImageContext to UIGraphicsBeginImageContextWithOptions (as documented on this page). Pass 0.0 for scale (the third argument) and you'll get a context with a scale factor equal to that of the screen.

UIGraphicsBeginImageContext uses a fixed scale factor of 1.0, so you're actually getting exactly the same image on an iPhone 4 as on the other iPhones. I'll bet either the iPhone 4 is applying a filter when you implicitly scale it up or just your brain is picking up on it being less sharp than everything around it.

So, I guess:

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

And in 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
}
Erinn answered 2/12, 2010 at 12:20 Comment(16)
Tommy answer is fine , but you still need to import #import <QuartzCore/QuartzCore.h> to remove renderInContext: warning .Cedric
Worked for me as well (sort of), I had to flip the image about the X axis to get it to come out right on a PDF context.Stalactite
Thanks for the solution. For 320*480 iDevice, is it possible to implement some codes so that we can get the same good-quality images as if we were capturing it from iPhone 4?Micromho
@WayneLiu you could specify a scale factor of 2.0 explicitly but you probably wouldn't get exactly an iPhone 4 quality image because any bitmaps that had been loaded would be the standard versions rather than the @2x versions. I don't think there's a solution to that as there's no way to instruct everyone that is currently holding on to a UIImage to reload but force the high resolution version (and you'd likely run up against problems due to the lesser RAM available in pre-retina devices anyway).Erinn
this still result in a scaled down image, i.e 320*480 instead of a 640*960 image.Modality
Instead of using the 0.0f for the scale parameter, is it more acceptable to use [[UIScreen mainScreen] scale], it works too.Insulation
I was having trouble when doing this with blurry UIImages being returned on iPhone 3GS. I managed to solve it by forcing the scale to 2.0, it keeps the images nice and crisp. In case that helps anyone.Liver
@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. It's explicitly documented, so 0.0f is simpler and better in my opinion.Shandy
I have a large white frame around my resulting UIImage. Any idea?Nepos
This answer is out of date for iOS 7. See my answer for the new "best" method to do this.Hellenic
This works great for exporting images from a UIViewController. Is there an adaptation that would capture within the image all the rows of a table that has some rows offscreen?Hagler
@Hagler You wouldn't be able to do that as a closed box easily because one of the main functions of a tableview is to ensure that most of the information that is off screen doesn't currently have any footprint within the UIView hierarchy. If you were to attempt just to set contentOffset and repeatedly grab a table view you also might not get exactly what you expect as setNeedsLayout queue the layout to occur before the next view composition rather than immediately. You'd need to call layoutIfNeeded on each cell manually (probably just run through subviews) before grabbing a shot.Erinn
@Erinn - Thanks! I took a shortcut, scrolling each successive table row into view (using scrollToRowAtIndexPath). This lets the OS handle all the layout housekeeping. I also scroll the destination UIImage (using CGContextTranslateCTM). The approach appears to handle everything including multi-section tables with headers/footers and such. It might become slow if tables became huge, but it works fine for my moderately sized tables. Thanks for the encouragement and advice! (I could post the method, but it won't fit in a comment, and it's a not an answer to this question.)Hagler
What's the equivalent of this solution when using CGBitmapContextCreate()?Arm
@Erinn After implement above code in my application, but i face memory issues. so it's not perfect.Alroi
@LucaTorella you edited the code in this answer to not compile. Please don't do that.Costume
H
240

The currently accepted answer is now out of date, at least if you are supporting iOS 7.

Here is what you should be using if you are only supporting 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()
}

As per this article, you can see that the new iOS7 method drawViewHierarchyInRect:afterScreenUpdates: is many times faster than renderInContext:. benchmark

Hellenic answered 19/3, 2014 at 2:9 Comment(13)
There is no doubt, that drawViewHierarchyInRect:afterScreenUpdates: is a lot faster. I just ran a Time profiler in Instruments. My image generation went from 138ms to 27ms.Deceased
yup, quicker than before.Georgie
For some reason this didn't work for me when I was trying to create custom icons for Google Maps markers; all I got was black rectangles :(Piccaninny
I'm also just getting a black rectangleGorgonzola
Same here re black rectangles; I'm attempting to use this for Google Maps markers too.Pandybat
@CarlosP have you tried setting afterScreenUpdates: to YES?Hellenic
I reverted to renderInContext in the end and it solved the issue.Pandybat
set afterScreenUpdates to YES fixed the black rectangle issue for meIlium
Documentation states: Use this method when you want to apply a graphical effect, such as a blur, to a view snapshot. This method is not as fast as the snapshotViewAfterScreenUpdates: method.Picky
I was also receiving black images. It turned out that if I call the code in viewDidLoad or viewWillAppear: the images are black; I had to do it in viewDidAppear:. So I also finally reverted to renderInContext:.Ostend
maybe this solution is faster but it flickers when i make a snapShot and not always make a good image in my case i used @Erinn answerCanicular
What if I need to use drawViewHierarchyInRect:afterScreenUpdates: with a larger scale? For example: a 300x300 view, with an 3000x3000 imageView inside. If I render this view in a CGContext with scale 10, my final image will be just a 300x300 upscaled. How can I solve this?Vermifuge
I was also getting 'black square'. I set the 'isOpaque' property to false in my code, then it worked for me.Cryptography
D
32

I have created a Swift extension based on @Dima solution:

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 improved version

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)
Discipline answered 17/8, 2015 at 3:39 Comment(4)
Thanks for the idea! Just as an aside, you can also defer { UIGraphicsEndImageContext() } immediately after beginning the context and avoid having to introduce the local variable img ;)Convection
Thanks so much for the well-detailed explanation and usage!Vicarage
Why is this a class function that takes a view?Larose
This works like a static function but as there is no need for overriding you can simply change it to a static func instead of class.Discipline
A
31

iOS Swift

Using modern UIGraphicsImageRenderer

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
    }
}
Ascertain answered 1/12, 2016 at 9:52 Comment(2)
@masaldana2 maybe not yet, but as soon as they start deprecating stuff or adding hardware accelerated rendering or improvements you better be on giants shoulders (AKA using last Apple APIs)Rebuff
Is anyone aware of what is the performance comparing this renderer to the old drawHierarchy..?Lattice
L
24

To improve answers by @Tommy and @Dima, use the following category to render UIView into UIImage with transparent background and without loss of quality. Working on iOS7. (Or just reuse that method in implementation, replacing self reference with your 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
Lucarne answered 30/8, 2014 at 19:22 Comment(5)
If I use drawViewHierarchyInRect with afterScreenUpdates:YES my image aspect ratio has changed and the image get distorted.Soundboard
Does this happen when your uiview content is changed? If yes then try to re-generate this UIImage again. Sorry can't test this myself because I'm on phoneLucarne
I have the same problem and seems like no one has noticed this, did you find any solution to this problem? @confile ?Judson
This is just what I need but it didn't work. I tried making @interface and @implementation both (RenderViewToImage) and adding #import "UIView+RenderViewToImage.h" to the header in ViewController.h so is this the correct usage ? e.g. UIImageView *test = [[UIImageView alloc] initWithImage:[self imageByRenderingView]]; [self addSubview:test]; ?Disqualification
and this method in ViewController.m was the view I tried to render - (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; }Disqualification
C
15

Swift 3

The Swift 3 solution (based on Dima's answer) with UIView extension should be like this:

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
    }
}
Careycarfare answered 11/7, 2016 at 20:5 Comment(0)
I
9

For Swift 5.1 you can use this extension:

extension UIView {

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

        return renderer.image { layer.render(in: $0.cgContext) }
    }
}
Involucre answered 2/4, 2020 at 16:54 Comment(0)
R
6

Drop-in Swift 3.0 extension that supports the new iOS 10.0 API & the previous method.

Note:

  • iOS version check
  • Note the use of defer to simplify the context cleanup.
  • Will also apply the opacity & current scale of the view.
  • Nothing is just unwrapped using ! which could cause a 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()
        }
    }
}
Recalesce answered 26/5, 2017 at 7:16 Comment(0)
A
5

Swift 2.0:

Using extension method:

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)

 }
Amhara answered 9/3, 2016 at 5:42 Comment(0)
C
3

Swift 3.0 implementation

extension UIView {
    func getSnapshotImage() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
        drawHierarchy(in: bounds, afterScreenUpdates: false)
        let snapshotImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return snapshotImage
    }
}
Cumbrous answered 7/10, 2016 at 21:38 Comment(0)
A
3

All Swift 3 answers did not worked for me so I have translated the most accepted answer:

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!
    }
}
Ability answered 9/5, 2017 at 12:6 Comment(0)
C
3

Here's a Swift 4 UIView extension based on the answer from @Dima.

extension UIView {
   func snapshotImage() -> UIImage? {
       UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
       drawHierarchy(in: bounds, afterScreenUpdates: false)
       let image = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()
       return image
   }
}
Catamount answered 6/6, 2018 at 20:2 Comment(0)
A
1

UIGraphicsImageRenderer is a relatively new API, introduced in iOS 10. You construct a UIGraphicsImageRenderer by specifying a point size. The image method takes a closure argument and returns a bitmap that results from executing the passed closure. In this case, the result is the original image scaled down to draw within the specified bounds.

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

So be sure the size you are passing into UIGraphicsImageRenderer is points, not pixels.

If your images are larger than you are expecting, you need to divide your size by the scale factor.

Andi answered 25/5, 2019 at 1:39 Comment(1)
I'm curious, could you help me with this please? #61248077 I'm using UIGraphicsImageRenderer, problem is that I want the exported image to be bigger size than the actual view on device. But with desired size subviews (labels) aren't sharp enough – so there is loss of quality.Bridgettebridgewater
H
0

Some times drawRect Method makes problem so I got these answers more appropriate. You too may have a look on it Capture UIImage of UIView stuck in DrawRect method

Humankind answered 12/2, 2013 at 8:24 Comment(0)
P
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;
}
Paltry answered 1/3, 2016 at 6:5 Comment(0)
T
0

Something must have changed recently as I was not able to get a retina display image following all the above answers.

What I ended up doing:

    static func takeScreenshot(of viewController: UIViewController) -> UIImage? {
        guard let view = viewController.view else {
            return nil
        }

        let scale = UIScreen.main.scale
        let size = CGSize(width: view.bounds.size.width * scale, height: view.bounds.size.height * scale)

        UIGraphicsBeginImageContextWithOptions(size, view.isOpaque, scale)

        defer { UIGraphicsEndImageContext() }

        if let context = UIGraphicsGetCurrentContext() {
            context.scaleBy(x: scale, y: scale) // Set the scale of the graphics context
            
            view.layer.render(in: context)
            
            let screenshot = UIGraphicsGetImageFromCurrentImageContext()
            
            return screenshot
        }

        return nil

    }
Thiazole answered 16/5, 2023 at 15:37 Comment(0)
A
-2

In this method just pass a view object and it will returns a UIImage object.

-(UIImage*)getUIImageFromView:(UIView*)yourView
{
 UIGraphicsBeginImageContext(yourView.bounds.size);
 [yourView.layer renderInContext:UIGraphicsGetCurrentContext()];
 UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 return image;
}
Armitage answered 12/4, 2016 at 12:29 Comment(0)
P
-6

Add this to method to UIView Category

- (UIImage*) capture {
    UIGraphicsBeginImageContext(self.bounds.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.layer renderInContext:context];
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
}
Plaice answered 30/1, 2013 at 6:44 Comment(1)
Hiya there, while this may well answer the question, please be aware that other users might not be as knowledgeable as you. Why don't you add a little explanation as to why this code works? Thanks!Zuniga

© 2022 - 2024 — McMap. All rights reserved.