Saturday, November 30, 2013

Сетевое кэширование в iOS. Введение



Сетевое кэширование в iOS. Введение

Есть еще один потенциально интересный подход: github.com/AFNetworking/AFIncrementalStore

Практически каждое мобильное приложение получает какие-либо данные из сети.
К сожалению, доступ к сети не всегда возможен и поэтому разработчику важно правильно реализовать сетевой кэш в приложении.

В связи с этим, я решил написать серию статей о том, какие существуют способы имплементации кэша и когда их применять.

Итак, введение.


Стратегии для кэширования


Есть два подхода к кэшированию: кэширование по требованию и предварительное кэширование.

Кэширование по требованию позволяет в offline режиме просматривать контент, который был просмотрен ранее. Данные, полученные от сервера, хранятся на устройстве и для каждого запроса происходит проверка их актуальности. Если данные актуальны, то они берутся с диска, если нет то идет запрос на сервер. 

Предварительное кэширование подразумевает, что все данные, которые могут потребоваться пользователю, будут получены и сохранены на диск сразу же. 

Чтобы определить какую стратегию кэширования использовать, нужно понять, может ли потребоваться пост обработка данных после их загрузки. Пост обработка подразумевает какую-либо модификацию загруженных данных. Например, изменение ссылок в HTML странице так, чтобы они указывали на картинки кэшированные локально и т.д.

Где хранить кэш


Приложения могут хранить информацию только в своей песочнице. Так как кэшируемые данные не генерируются пользователем, то они должны быть сохранены в NSCachesDirectory, а не в NSDocumentsDirectory. Хорошей практикой является создание отдельной директории, для всех кэшируемых данных.

В этом примере в папке Library/Caches создается директория MyAppCache:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);  NSString *cachesDirectory = [paths objectAtIndex:0];  cachesDirectory = [cachesDirectory stringByAppendingPathComponent:@"MyAppCache"];  

Причиной хранения кэша в папке Library/Caches является то, что iCloud (и iTunes) исключает эту директорию из бэкапа. И, следовательно, и так ограниченное в iCloud пространство (в настоящее время для бесплатного аккаунта, это около 5 GB) не тратится на хранение ненужных данных.

В случае если в приложении происходит интенсивное кэширование, рекомендуется вместо диска использовать память и выгружать данные на диск при закрытии приложения. Это связано с тем, что flash память iPhone имеет ограниченное число циклов записи/чтения и нежелательно нагружать ее лишний раз.

Как хранить кэш


На iOS существует множество различных способов хранения пользовательских данных. Для кэширования лучше всего подходят: NSKeyedArchiver, Core Data, SQLite, NSURLCache. 

NSKeyedArchiver


Кэширование модели данных реализуется с использованием класса NSKeyedArchiver. Для того, чтобы объекты модели могли быть архивированы, классы модели должны реализовать протокол NSCoding. А именно методы
- (void)encodeWithCoder:(NSCoder *)aCoder;  - (id)initWithCoder:(NSCoder *)aDecoder;  

Если класс реализует NSCoding, для архивации достаточно вызвать один из следующих методов:
  [NSKeyedArchiver archiveRootObject:objectForArchiving toFile:archiveFilePath];  

[NSKeyedArchiver archivedDataWithRootObject:objectForArchiving];  

Первый метод создаст файл с архивом по пути archiveFilePath. Второй метод вернет объект NSData. NSData обычно быстрее, так как отсутствуют дополнительные затраты на доступ к файлу, но при этом данные будут храниться в памяти приложения.

Для разархивирования модели из файла (или указателя на NSData) используется класс NSKeyedUnarchiver. Разархивировать данные можно одним из следующих методов:
  [NSKeyedUnarchiver unarchiveObjectWithData:data];  

[NSKeyedUnarchiver unarchiveObjectWithFile:archiveFilePath];  

Использование NSKeyedArchiver/NSKeyedUnarchiver требует чтобы модели удовлетворяли протоколу NSCoding. Реализация NSCoding очень проста, но если файлов много, то она может занять много времени. Поэтому для автоматизации этого процесса лучше использовать какой-либо инструмент. Например среду разработки AppCode.

Core Data


Чтобы хранить данные в Core Data, необходимо создать файл модели, который содержит описание сущностей (Entities), а также связей между ними (Relationships), и написать методы для сохранения и получения данных. Используя Core Data можно получить настоящий offline режим работы приложения, как это сделано в стандартных приложениях Mail и Calendar. 

При реализации предварительного кэширования нужно периодически удалять данные, которые не нужны. Иначе размер кэша начнет заметно расти, что вызовет потерю производительности. Синхронизация локальных изменений выполняется путем отслеживания набора изменений и отправкой их обратно на сервер. Существует много алгоритмов для отслеживания наборов изменений, но лучше использовать один из тех, что работает в Git.

Несмотря на то, что Core Data можно использовать для кэширования по требованию, лучше этого не делать. Главное преимущество Core Data заключается в предоставление доступа к свойствам модели без необходимости разархивировать все данные. Однако сложность реализации Core Data в приложении, перекрывает это преимущество.

Raw SQLite


Для работы с SQLite надо слинковать приложение с библиотекой libsqlite3, но такой подход имеет значительные недостатки.
Все sqlite3 библиотеки и механизм Object Relational Mapping (ORM) работают медленнее чем Core Data. Кроме того реализация sqlite3 в iOS не потоко-безопасна. Так что если вы не используете отдельно собранную sqlite3 библиотеку (скомпилированную с флагом thread-safe), то вы сами отвечаете за то, чтобы гарантировать потоко-безопасный доступ на чтение/запись к базе данных sqlite3. 

Так как Core Data может предложить гораздо больше возможностей (миграция данных, встроенная потоко-безопасность, ...) рекомендуется избегать использование нативной SQLite на iOS.

NSURLCache


Идеальный вариант для кэширования по запросу. Позволяет кэшировать данные возвращаемые NSURLRequest практически в автоматическом режиме и требует минимум кода.

Всего лишь пара строк и ваше приложение получит дисковый кэш для запросов.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024                                                          diskCapacity:20 * 1024 * 1024                                                              diskPath:nil];    [NSURLCache setSharedURLCache:URLCache];  }  





К сожаление пригоден только для REST сервисов и есть проблемы при работе с некоторыми HTTP заголовками.

Заключение


Для реализации кэширования по запросу лучше использовать NSURLCache или NSKeyedArchiver. Реализация полноценного offline режима требует работы c CoreData.

В следующей части я планирую детально рассмотреть работу с NSURLCache/NSKeyedArchiver и описать случаи, для которых они подходят.

Thursday, November 28, 2013

http://stackoverflow.com/questions/14902835/sdwebimage-download-image-and-store-to-cache-for-key



Hello I am using the SDWebImage framework in a project and I was wanting to download and cache some images, but I think my code is storing the image in the cache twice? Is there a way to store a UIImage in the cache by a key one time? Here is my code.

           SDWebImageManager *manager = [SDWebImageManager sharedManager];           [manager downloadWithURL:[NSURL URLWithString:url] options:0 progress:^(NSUInteger receivedSize, long long expectedSize) {              } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {                if(image){                  NSString *localKey = [NSString stringWithFormat:@"Item-%d", i];                  [[SDImageCache sharedImageCache] storeImage:image forKey:localKey];              }              }];  

Possibly there is just something I missed? Looks like doing this in my allocations instrument is pilling up a lot of memory.

asked Feb 15 at 20:29
AgnosticDev
754314
add comment

I'm surprised nobody answered this question, but I've had a similar question and came across this, so I'll answer it for people viewing this going forward (assuming you've sorted this out yourself by now).

To directly answer your question, yes, you are caching the image twice.

Download calls to SDWebImageManager automatically cache images with keys based on the absoluteString of the image's url. If you want your own key, you can use the download call on SDWebImageDownloader which as far as I can tell does NOT cache by default. From there you can call the sharedImageCache as you're already doing and cache with whatever key you want.

That aside, it is strange you're seeing allocations piling up in any case as SDWebImage likes to cache to disk and not memory generally. Maybe something else is going on at the same time?

Hope this helps,

-Brandon

answered Apr 22 at 19:04
Stakenborg
636214

Работа с изображениями IOS http://forum.i-ekb.ru/index.php?showtopic=1716

http://forum.i-ekb.ru/index.php?showtopic=1716

Работа с изображениями


Как скачать, создать и показать изображение через URL


URL на удаленное изображение
  NSURL *url = [NSURL URLWithString: @"http://darknessproduction.ru/ifullstat.png"];
Создание UIImage из NSData
  UIImage *image = [UIImage imageWithData: [NSData dataWithContentsOfURL:url]];

Поместим на наш UIView
  NSURL *url = [NSURL URLWithString:
   
@"http://darknessproduction.ru/ifullstat.png"];
UIImage *image = [UIImage imageWithData: [NSData dataWithContentsOfURL:url]];
[self.view addSubview:[[[UIImageView alloc] initWithImage:image] autorelease]];


Закругляем изображения

Добавляем
  #include <QuartzCore/QuartzCore.h>
в viewDidLoad нашего контроллера прописываем:
CALayer * ourLayer = [imageView layer]; // Будем округлять UIImageView
ourLayer
.cornerRadius = 8.0f;           // Задаем радиус для округления.
ourLayer
.masksToBounds = YES;           // Чтобы за овальной границей в углах ничего не рисовалось
ourLayer
.borderWidth = 0.0f;            // Границу рисовать не будем. Если нужна - указываем толщину
Ну и собственно всё. Очевидно, что если хотим получить овальный View, указываем cornerRadius равный половине длины короткой стороны нашего View.


Как сделать анимацию из картинок

GIF грузить не получится стандартными средствами SDK, так что если речь идет не о анимации в OpenGL, то делайте так:
1. создаете UIImageView
2. грузите серию изображений вашей анимации как массив UIImage объектов
3. проставляете массив изображений для свойства animationImages в UIImageView
4. устанавливаете продолжительность анимации в свойстве animationDuration
5. startAnimating для UIImageView
  NSArrray *theImages = [NSArray arrayWithObjects:
       
[UIImage imageNamed:@"image_1.png"],
       
[UIImage imageNamed:@"image_2.png"],
       
[UIImage imageNamed:@"image_3.png"],
       
[UIImage imageNamed:@"image_4.png"],
       
[UIImage imageNamed:@"image_5.png"], nil];
UIImageView *anImageView = ... //мембер класса, или создаете по месту, не важно
anImageView
.animationImages = theImages;
anImageView
.animationDuration = 1.0; // 1 секунда
[anImageView startAnimating];

Как отправить емейл с вложениями, используя камеру и фотоальбом

Запуск камеры iPhone
  - (void)buttonPressed:(UIButton *)button
{
 
// Создание image picker controller
 
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
   
// Устанавливаем сорс для камеры
  imagePicker
.sourceType =  UIImagePickerControllerSourceTypeCamera;
   
// Текущий Delegate
  imagePicker
.delegate = self;
   
// Вырубаем редактирование изображений
  imagePicker
.allowsImageEditing = NO;
   
// Показываем image picker
 
[self presentModalViewController:imagePicker animated:YES];  
}
Конвертируем изображение с камеры в NSData
  - (void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
 
// Берем изображение из информации словаря
 
UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
   
// Закрываем окно камеры
 
[self dismissModalViewControllerAnimated:YES];
   
// Запускаем функцию отправки изображения по почте
 
[self performSelector:@selector(emailImage:) withObject:image afterDelay:1.0];
   
// Релизим picker
 
[picker release];
}
Отправка изображения с камеры
  - (void)emailImage:(UIImage *)image
{
 
MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
  picker
.mailComposeDelegate = self;
   
// Устанавливаем тему письма
 
[picker setSubject:@"Picture from my iPhone!"];
   
// Добавляем адреса почты, куда будем отсылать      
 
[picker setToRecipients:[NSArray arrayWithObjects:@"(Гости не видят ссылок. )", @"(Гости не видят ссылок. )", nil]];
 
[picker setCcRecipients:[NSArray arrayWithObject:@"(Гости не видят ссылок. )"]];      
 
[picker setBccRecipients:[NSArray arrayWithObject:@"(Гости не видят ссылок. )"]];
   
// Устанавливаем тело сообщения
 
NSString *emailBody = @"I just took this picture, check it out.";
   
// Вырубаем XTML формат
 
[picker setMessageBody:emailBody isHTML:NO];
   
// Создаем NSData из PNG изображения с камеры
 
NSData *data = UIImagePNGRepresentation(image);
   
// Добавляем изображение к письму как вложение
 
[picker addAttachmentData:data mimeType:@"image/png" fileName:@"CameraImage"];
   
// Показываем окно отправки письма
 
[self presentModalViewController:picker animated:YES];
   
// Релизим picker
 
[picker release];
}
 
- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error
{
 
// Закрываем окно отправки письма    
 
[self dismissModalViewControllerAnimated:YES];
}


Как анимированно вращать изображение

Радианы и градусы
Переводим из градусов в радианы
  // This is defined in Math.h
#define M_PI   3.14159265358979323846264338327950288   /* pi */
#define DEGREES_TO_RADIANS(angle) ((angle / 180.0) * M_PI)
Вращаем изображение
  - (void)rotateImage:(UIImageView *)image duration:(NSTimeInterval)duration 
       curve
:(int)curve degrees:(CGFloat)degrees
{
 
// Устанавливаем анимацию
 
[UIView beginAnimations:nil context:NULL];
 
[UIView setAnimationDuration:duration];
 
[UIView setAnimationCurve:curve];
 
[UIView setAnimationBeginsFromCurrentState:YES];
 
CGAffineTransform transform =
     
CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(degrees));
  image
.transform = transform;
 
[UIView commitAnimations];
}
Вызов вращения
  - (void)startApp
{
 
UIImageView *imageToMove =
     
[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ArrowUp.png"]];
  imageToMove
.frame = CGRectMake(10, 10, 20, 100);
 
[self.view addSubview:imageToMove];
   
[self rotateImage:imageToMove duration:3.0
      curve
:UIViewAnimationCurveEaseIn degrees:180];
}


Как анимированно передвинуть изображение

Передвижение изображения
  - (void)moveImage:(UIImageView *)image duration:(NSTimeInterval)duration
     curve
:(int)curve x:(CGFloat)x y:(CGFloat)y
{
 
// Устанавливаем анимацию
 
[UIView beginAnimations:nil context:NULL];
 
[UIView setAnimationDuration:duration];
 
[UIView setAnimationCurve:curve];
 
[UIView setAnimationBeginsFromCurrentState:YES];
   
// The transform matrix
 
CGAffineTransform transform = CGAffineTransformMakeTranslation(x, y);
  image
.transform = transform;
 
[UIView commitAnimations];
 
}
Вызов передвижения
  - (void)startApp
{
 
UIImageView *imageToMove =
     
[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Square.png"]];
  imageToMove
.frame = CGRectMake(10, 10, 20, 100);
 
[self.view addSubview:imageToMove];
 
[self moveImage:imageToMove duration:3.0
     curve
:UIViewAnimationCurveLinear x:50.0 y:50.0];
}


Как сохранить UIImage в PNG или JPG
  //Создадим пути для выходных форматов
NSString  *pngPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Test.png"];
NSString  *jpgPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Test.jpg"];
 
// Запишем UIImage в JPEG с минимальным сжатием (наилучшее качество)
[UIImageJPEGRepresentation(image, 1.0) writeToFile:jpgPath atomically:YES];
 
// Запишем UIImage в PNG
[UIImagePNGRepresentation(image) writeToFile:pngPath atomically:YES];
 
// Проверим как хорошо у нас записалось:
 
// Создаем файл-менеджер
NSError *error;
NSFileManager *fileMgr = [NSFileManager defaultManager];
 
// Устанавливаем директорию Документов
NSString *documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
 
// Проверяем список файлов, который содержит директория Документов
NSLog(@"Директория Документов: %@", [fileMgr contentsOfDirectoryAtPath:documentsDirectory error:&error]);


Как сделать снимок с камеры и сохранить в фотоальбом

Запускаем камеру
  // Создаем image picker controller
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
 
// Устанавливаем сорс камеры
imagePicker
.sourceType =  UIImagePickerControllerSourceTypeCamera;
 
// Текущий Delegate
imagePicker
.delegate = self;
 
// Выключаем редактирование изображения
imagePicker
.allowsImageEditing = NO;
 
// Покажем image picker
[self presentModalViewController:imagePicker animated:YES];
Сохранение изображения в Фотоальбом
  - (void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
 
UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
   
// Сохраняем изображение
 
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
   
[picker release];
}
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
 
UIAlertView *alert;
   
// Проверяем доступность сохранения изображения
 
if (error)
    alert
= [[UIAlertView alloc] initWithTitle:@"Ошибка"
                            message
:@"Невозможно сохранить изображения."
                           
delegate:self cancelButtonTitle:@"Ok"
                            otherButtonTitles
:nil];
 
else // все хорошо
    alert
= [[UIAlertView alloc] initWithTitle:@"Успех"
                            message
:@"Изображение сохранено."
                           
delegate:self cancelButtonTitle:@"Ok"
                            otherButtonTitles
:nil];
 
[alert show];
 
[alert release];
}

Как изменить размер изображения
  UIImage *UImageFromPathScaledToSize(NSString* path, CGSize toSize)
{
 
UIImage *scaledImg = nil;
 
UIImage *img = [[UIImage alloc] initWithContentsOfFile:path]; // get the image
   
if( img )
 
{
   
float scale = GetScaleForProportionalResize( img.size, toSize, false, false );
     
CGImageRef cgImage = CreateCGImageFromUIImageScaled( img, scale );
     
[img release];
     
if( cgImage )
   
{
      scaledImg
= [UIImage imageWithCGImage:cgImage];   // autoreleased
     
CGImageRelease( cgImage );
   
}
 
}
 
return scaledImg;
}

Как разбить большое изображение на куски равного размера

Иногда бывает удобно изображение одинакового размера затолкать в одно большое изображение, чтобы не разводить кучу файлов, для разбивки используем код:
@interface UIImage (Utilities)
- (NSMutableArray*) getTilesWithSize:(CGSize)tileSize;
@end
//------------------------------------------------------------------------
@implementation UIImage (Utilities)
- (NSMutableArray*) getTilesWithSize:(CGSize)tileSize
{
   
NSUInteger      countW = self.size.width  / tileSize.width;
   
NSUInteger      countH = self.size.height / tileSize.height;
   
NSMutableArray* array  = [NSMutableArray arrayWithCapacity:(countW * countH)];
   
CGRect          rect   = CGRectMake(0, 0, tileSize.width, tileSize.height);
   
NSUInteger      w;
   
NSUInteger      h;
   
for (h = 0, rect.origin.y = 0; h < countH; h++, rect.origin.y += tileSize.height)
   
{
       
for (w = 0, rect.origin.x = 0; w < countW; w++, rect.origin.x += tileSize.width)
       
{
           
CGImageRef tileImageRef = CGImageCreateWithImageInRect(self.CGImage, rect);
           
[array addObject:[UIImage imageWithCGImage:tileImageRef]];
           
CGImageRelease(tileImageRef);
       
}
   
}
   
return array;
}
@end
Использование:
  UIImage* image;
NSMutableArray* normalImages;
NSMutableArray* highlightedImages;
image
= [UIImage imageNamed:@"ImagesSet_Buttons_Normal.png"];
normalImages
= [image getTilesWithSize:CGSizeMake(66, 66)];
image
= [UIImage imageNamed:@"ImagesSet_Buttons_Highlighted.png"];
normalImages
= [image getTilesWithSize:CGSizeMake(66, 66)];

Как обрезать картинку
  // Создаем UIImage из существующего PNG-файла
UIImage *image = [UIImage imageNamed:@"prgBinary.jpg"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
 
// Берем размер текущего изображения
CGSize size = [image size];
 
// Делаем imageView по размеру изображения и добавляем в наше представление
[imageView setFrame:CGRectMake(0, 0, size.width, size.height)];
[[self view] addSubview:imageView];
[imageView release];    
 
// Создаем прямоугольную область, которую надо обрезать
CGRect rect = CGRectMake(size.width / 4, size.height / 4 ,
   
(size.width / 2), (size.height / 2));
 
// Создаем bitmap изображения
CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], rect);
UIImage *img = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
 
// Создаем и показываем изображение из bitmap
imageView
= [[UIImageView alloc] initWithImage:img];
[imageView setFrame:CGRectMake(0, 200, (size.width / 2), (size.height / 2))];
[[self view] addSubview:imageView];
[imageView release];


Как сделать изображение черно-белым
- (UIImage *)convertImageToGrayScale:(UIImage *)image
{
 
CGRect imageRect = CGRectMake(0, 0, image.size.width, image.size.height);
   
// Выбираем Черно-белую палитру
 
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
   
// Создаем bitmap и контекст
 
CGContextRef context = CGBitmapContextCreate(nil, image.size.width, image.size.height, 8, 0, colorSpace, kCGImageAlphaNone);
 
CGContextDrawImage(context, imageRect, [image CGImage]);
 
CGImageRef imageRef = CGBitmapContextCreateImage(context);
   
// Создаем UIImage  
 
UIImage *newImage = [UIImage imageWithCGImage:imageRef];
   
// Релизим палитру, битмап и контекст
 
CGColorSpaceRelease(colorSpace);
 
CGContextRelease(context);
 
CFRelease(imageRef);
   
// Возвращаем уже черно-белое изображение
 
return newImage;
}


Как сделать снимок экрана

Настраиваем пользовательский интерфейс
  - (id)init
{
 
if (self = [super init])
 
{
   
self.view = [[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]] autorelease];
               
self.view.backgroundColor = [UIColor grayColor];
     
// Кнопка активации камеры
    button
= [[UIButton alloc] initWithFrame:CGRectMake(80, 55, 162, 53)];    
   
[button setBackgroundImage:[UIImage imageNamed:@"Camera.png"] forState:UIControlStateNormal];
   
[button addTarget:self action:@selector(buttonPressed:) forControlEvents: UIControlEventTouchUpInside];      
   
[self.view addSubview:button];
   
[button release];
 
}
   
return self;  
}
Вызов UIGetScreenImage()
  CGImageRef UIGetScreenImage(void);
- (void)buttonPressed:(UIButton *)button
{
 
// Создаем скрин экрана и добавляем его в UIImage
 
CGImageRef screen = UIGetScreenImage();
 
UIImage* image = [UIImage imageWithCGImage:screen];
 
CGImageRelease(screen);
   
// Сохраняем UIImage в фотоальбом
 
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
}
Сохранение скрина экрана в фотоальбом
  - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
 
UIAlertView *alert;
   
// Проверяем возможность сохранения
 
if (error)
    alert
= [[UIAlertView alloc] initWithTitle:@"Ошибка"
                            message
:@"Невозможно сохранить."
                           
delegate:self cancelButtonTitle:@"Ok"
                            otherButtonTitles
:nil];
 
else // Все хорошо
    alert
= [[UIAlertView alloc] initWithTitle:@"Успех"
                            message
:@"Изображение сохранено."
                           
delegate:self cancelButtonTitle:@"Ok"
                            otherButtonTitles
:nil];
 
[alert show];
 
[alert release];
}