Как захватить UIView в UIImage без потери качества на дисплее сетчатки

303

Мой код работает нормально для обычных устройств, но создает размытые изображения на устройствах Retina.

Кто-нибудь знает решение моей проблемы?

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

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}
Даниил
источник
размыто. Мне кажется, правильная шкала потерялась ...
Даниэль
я тоже. встретил ту же проблему.
RainCast
Где вы отображаете результат изображения? Как объяснили другие, я полагаю, что вы визуализируете вид на изображение с масштабом 1, а ваше устройство с масштабом 2 или 3. Таким образом, вы уменьшаете масштаб до более низкого разрешения. Это потеря качества, но она не должна стираться. Но когда вы снова смотрите на это изображение (на том же экране?) На устройстве с масштабом 2, оно преобразуется из низкого разрешения в более высокое, используя 2 пикселя на пиксель на снимке экрана. При таком повышении масштаба вероятнее всего используется интерполяция (по умолчанию на iOS), усреднение значений цвета для дополнительных пикселей.
Ганс Терье Бакке

Ответы:

667

Переключитесь с использования UIGraphicsBeginImageContextна UIGraphicsBeginImageContextWithOptions(как описано на этой странице ). Передайте 0.0 для масштаба (третий аргумент), и вы получите контекст с масштабным коэффициентом, равным коэффициенту экрана.

UIGraphicsBeginImageContextиспользует фиксированный масштабный коэффициент 1,0, поэтому на iPhone 4 вы получаете точно такое же изображение, что и на других iPhone. Могу поспорить, что либо iPhone 4 применяет фильтр, когда вы неявно увеличиваете его, либо ваш мозг замечает, что он менее резкий, чем все вокруг.

Так что я думаю:

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

И в 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
Томми ответ хорошо, но вам все равно нужно импортировать, #import <QuartzCore/QuartzCore.h>чтобы удалить renderInContext:предупреждение.
GWDP
2
@WayneLiu Вы можете указать масштабный коэффициент 2,0 явно, но вы, вероятно, не получите точно изображение с качеством iPhone 4, потому что любые загруженные растровые изображения будут стандартными версиями, а не версиями @ 2x. Я не думаю, что есть решение этой проблемы, так как нет способа проинструктировать всех, кто в данный момент держится за a, UIImageперезагрузить, но заставить версию с высоким разрешением (и вы, вероятно, столкнетесь с проблемами из-за меньшего объема оперативной памяти, доступной в pre -ретина устройства в любом случае).
Томми
6
Вместо того, чтобы использовать параметр 0.0fдля масштаба, это более приемлемо для использования [[UIScreen mainScreen] scale], это тоже работает.
Адам Картер
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.Это явно задокументировано, поэтому, на мой взгляд, 0.0f проще и лучше.
cprcrack
5
Этот ответ устарел для iOS 7. См. Мой ответ о новом «лучшем» способе сделать это.
Дима
224

В настоящее время принятый ответ устарел, по крайней мере, если вы поддерживаете iOS 7.

Вот что вы должны использовать, если вы поддерживаете только 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;
}

Свифт 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()
}

В этой статье вы можете увидеть, что новый метод iOS7 drawViewHierarchyInRect:afterScreenUpdates:во много раз быстрее, чем renderInContext:.эталонный тест

Дима
источник
5
Нет сомнений, что drawViewHierarchyInRect:afterScreenUpdates:это намного быстрее. Я только что запустил профилировщик времени в инструментах. Генерация моего изображения увеличилась с 138 до 27 мс.
ThomasCle
9
По какой-то причине это не сработало для меня, когда я пытался создать собственные значки для маркеров Карт Google; все, что я получил, было черными прямоугольниками :(
JakeP
6
@CarlosP ты пробовал установить afterScreenUpdates:на YES?
Дима
7
установите afterScreenUpdates на YES, исправил проблему с черным прямоугольником для меня
hyouuu
4
Я также получал черные изображения. Оказалось, что если я назову код viewDidLoadили viewWillAppear:изображения будут черными; Я должен был сделать это в viewDidAppear:. Так что я, наконец, вернулся к renderInContext:.
маникесар
32

Я создал расширение Swift на основе решения @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
    }
}

РЕДАКТИРОВАТЬ: Swift 4 улучшенная версия

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

Использование:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))  
let image = UIImage.imageWithView(view)
Хеберти Алмейда
источник
2
Спасибо за идею! Помимо этого, вы также можете отложить {UIGraphicsEndImageContext ()} сразу после начала контекста и избежать необходимости вводить локальную переменную img;)
Деннис L
Большое спасибо за подробное объяснение и использование!
Зевс
Почему это classфункция, которая принимает вид?
Рудольф Адамкович
Это работает как staticфункция, но поскольку нет необходимости переопределять, вы можете просто изменить его на staticfunc вместо class.
Хеберти Алмейда
25

IOS стриж

Использование современного 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
    }
}
Александр Белявский
источник
@ masaldana2, может быть, еще нет, но как только они начнут оспаривать вещи или добавлять аппаратное ускорение рендеринга или улучшения, вам лучше быть на плечах гигантов (AKA использует последние API Apple)
Хуан Боэро,
Кто-нибудь знает о том, что производительность сравнивает это rendererсо старым drawHierarchy..?
Вив
24

Чтобы улучшить ответы @Tommy и @Dima, используйте следующую категорию для визуализации UIView в UIImage с прозрачным фоном и без потери качества. Работаю на iOS7. (Или просто используйте этот метод в реализации, заменив selfссылку на ваше изображение)

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
источник
1
Если я использую drawViewHierarchyInRect с afterScreenUpdates: YES, мое соотношение сторон изображения изменилось, и изображение исказилось.
конфил
Происходит ли это, когда изменяется содержимое вашего uiview? Если да, то попробуйте заново сгенерировать этот UIImage снова. Извините, я не могу проверить это сам, потому что я разговариваю по телефону
Глого
У меня та же проблема, и кажется, что никто не заметил этого, вы нашли какое-либо решение этой проблемы? @confile?
Reza.Ab
Это как раз то, что мне нужно, но это не сработало. Я попытался сделать @interfaceи то и @implementationдругое (RenderViewToImage)и добавить #import "UIView+RenderViewToImage.h"в заголовок, ViewController.hтак это правильное использование? например UIImageView *test = [[UIImageView alloc] initWithImage:[self imageByRenderingView]]; [self addSubview:test];?
Грег
и этот метод ViewController.mбыл в представлении, которое я пытался визуализировать- (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; }
Грег
15

Свифт 3

Решение Swift 3 (на основе ответа Димы ) с расширением UIView должно выглядеть так:

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

Встроенное расширение Swift 3.0, поддерживающее новый API iOS 10.0 и предыдущий метод.

Примечание:

  • проверка версии iOS
  • Обратите внимание на использование defer для упрощения очистки контекста.
  • Также будет применена прозрачность и текущая шкала вида.
  • Ничто не может быть просто развернуто, !что может привести к сбою.

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:

Используя метод расширения:

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
   }

}

Использование:

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
источник
3

Реализация 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
источник
3

Все ответы Swift 3 не сработали, поэтому я перевел наиболее приемлемый ответ:

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

Вот расширение Swift 4 UIView, основанное на ответе @Dima.

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

Для Swift 5.1 вы можете использовать это расширение:

extension UIView {

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

        return renderer.image {
            rendererContext in

            layer.render(in: rendererContext.cgContext)
        }
    }
}
Андрей М.
источник
1

UIGraphicsImageRendererявляется относительно новым API, представленным в iOS 10. Вы создаете UIGraphicsImageRenderer , указывая размер точки . Метод image принимает аргумент замыкания и возвращает растровое изображение, полученное в результате выполнения переданного замыкания. В этом случае результатом является исходное изображение, уменьшенное для рисования в указанных пределах.

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

Поэтому убедитесь, что размер, который вы передаете, UIGraphicsImageRendererэто точки, а не пиксели.

Если ваши изображения больше, чем вы ожидаете, вам нужно разделить ваш размер на коэффициент масштабирования.

pkamb
источник
Мне любопытно, не могли бы вы помочь мне с этим, пожалуйста? stackoverflow.com/questions/61247577/… Я использую UIGraphicsImageRenderer, проблема в том, что я хочу, чтобы экспортированное изображение было большего размера, чем фактический вид на устройстве. Но с желаемыми размерами подпредставления (ярлыки) недостаточно резкие - поэтому теряется качество.
Ондржей Король
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
источник
-2

В этом методе просто передайте объект представления, и он вернет объект UIImage.

-(UIImage*)getUIImageFromView:(UIView*)yourView
{
 UIGraphicsBeginImageContext(yourView.bounds.size);
 [yourView.layer renderInContext:UIGraphicsGetCurrentContext()];
 UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 return image;
}
Атану Мондал
источник
-6

Добавьте это к методу в категорию UIView

- (UIImage*) capture {
    UIGraphicsBeginImageContext(self.bounds.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.layer renderInContext:context];
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
}
Шафраз Бухары
источник
2
Привет, хотя это вполне может ответить на вопрос, но учтите, что другие пользователи могут быть не настолько осведомлены, как вы. Почему бы вам не добавить небольшое объяснение, почему этот код работает? Спасибо!
Vogel612