iOS. Приемы программирования

Нахавандипур Вандад

Глава 12. Управление файлами и каталогами

 

 

12.0. Введение

Операционная система iOS основана на MacOS X, которая, в свою очередь, построена на базе операционной системы Unix. В iOS полная структура каталогов остается невидимой для приложения, поскольку каждое приложение, написанное iOS-разработчиком, существует в собственной песочнице. Эта защищенная среда не случайно называется песочницей: она действительно представляет собой жестко ограниченную область, и содержимое каталога песочницы доступно лишь тому приложению, которое владеет ею. У каждого приложения есть собственный каталог-песочница, по умолчанию у таких песочниц есть подкаталоги, к которым могут обращаться приложения.

Когда приложение для iOS устанавливается на устройстве, система создает для этого приложения структуру каталогов (рис. 12.1).

Рис. 12.1. Структура файловой системы в iOS

• Name.app — несмотря на странное название с расширением. app, это каталог. Все содержимое вашего основного пакета оказывается здесь. Например, все пиктограммы приложения, двоичный файл приложения, различные брендинговые изображения, шрифты, звуки и пр. автоматически отправятся в этот каталог, когда система iOS будет устанавливать приложение на устройстве. Name — это имя продукта, которое вы задали для приложения. Итак, если вы назвали приложение MyApp, то его каталог. app будет называться MyApp.app.

• Documents/ — этот каталог является местом назначения для всего контента, создаваемого пользователем. Содержимое, которое заполняется, скачивается или создается вашим приложением, не должно храниться в этом каталоге.

• Library/ — этот каталог используется для хранения кэшированных файлов, пользовательских настроек и т. д. Как правило, он не содержит никаких файлов, а только подкаталоги, в которых уже находятся другие файлы.

В корневом каталоге каждого приложения содержатся различные другие каталоги, о которых я расскажу далее.

• Library/Caches/ — в этом каталоге хранится информация, которую ваше приложение позже сможет воссоздать, если возникнет такая необходимость. iOS не выполняет резервного копирования этого каталога. Кроме того, iOS может удалить его содержимое, если дисковое пространство заканчивается, а ваше приложение в данный момент не работает! Поэтому работа приложения не должна слишком зависеть от этого каталога — будьте готовы к тому, что его содержимое потребуется создавать заново. Повторю: iOS не выполняет резервного копирования информации из этого каталога, так что, когда работа вашего приложения приостановлена, данная информация вполне может быть удалена.

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

• Library/Preferences/ — как понятно из названия, в этом приложении хранятся настройки, которые приложение должно запоминать и активизировать от запуска к запуску. Мы подробнее поговорим об этом в дальнейшем. iOS выполняет резервное копирование информации из этого каталога.

• Library/Application Support/ — данные, создаваемые вашим приложением (за исключением тех данных, которые создает сам пользователь), должны храниться в этом каталоге. Приятно отметить, что iOS выполняет резервное копирование данного каталога. Возможно, этот каталог не будет создаваться автоматически и вам придется создавать его самостоятельно, если его не существует. О создании этого каталога мы поговорим позже в данной главе.

• tmp/ — это временные файлы, которые ваше приложение может скачивать, создавать и т. д. iOS не выполняет резервного копирования данного каталога. Например, вы можете скачать из Интернета несколько фотографий и сохранить их в каталоге, чтобы повысить производительность вашего приложения, ведь в таком случае эти фотографии не придется скачивать заново при каждом запуске приложения. Каталог служит именно для этой цели. Убедитесь, что в нем не будут храниться документы или файлы, создаваемые пользователем.

Итак, теперь вы знаете, какие каталоги автоматически создаются операционной системой при установке приложения на устройстве с iOS. Далее найдем пути к остальным полезным каталогам, которые мы здесь уже упоминали. Воспользуемся теми API, которые Apple предоставляет специально для этих целей (о таких API пойдет речь в дальнейшем в этой главе).

 

12.1. Определение пути к самым полезным каталогам на диске

 

Постановка задачи

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

Программистам требуется использовать API, предоставляемые в iOS SDK, для нахождения путей к каталогам и/или файлам. Иными словами, путь к файлу или каталогу никогда не следует угадывать. Если, например, вы ищете пути, один из которых ведет к каталогу Documents (Документы), то нужно гарантировать, что для этого применяются правильные API. Никогда, ни в коем случае не рассчитывайте на то, что этот каталог будет называться в пакете вашего приложения именно Documents и никак иначе. Для нахождения пути к этому каталогу достаточно воспользоваться подходящими API. Если вас интересует не сам каталог, а отдельные содержащиеся в нем файлы, добавьте к концу обнаруженного пути имена тех или иных файлов.

 

Решение

Используйте метод экземпляра URLsForDirectory: inDomains:, относящийся к классу NSFileManager.

 

Обсуждение

Класс NSFileManager обеспечивает множество операций, связанных с файлами и каталогами, выполняемых в iOS. Все такие операции осуществляются прямо внутри ваших приложений, от вас требуется всего лишь создать экземпляр этого класса. Не рекомендую пользоваться разделяемым файловым менеджером, который предоставляется этим классом с помощью метода класса defaultManager, поскольку этот менеджер не является потокобезопасным. Лучше самостоятельно создать экземпляр класса NSFileManager и управлять им.

Метод экземпляра URLsForDirectory: inDomains:, относящийся к классу NSFileManager, позволяет искать конкретные каталоги в файловой системе iOS, в основном в песочнице вашего приложения. Этот метод имеет два параметра:

• URLsForDirectory: — это каталог, который вы хотите найти. Передайте этому параметру значение типа NSSearchPathDirectory (оно является перечислением). Далее поговорим о нем подробнее;

• inDomains — параметр указывает, где вы собираетесь искать конкретный каталог. Значение этого параметра должно относиться к типу NSSearchPathDomainMask (это тоже перечисление).

Предположим, вы хотите найти путь к каталогу Documents (Документы) вашего приложения. Вот как просто это делается:

NSFileManager *fileManager = [[NSFileManager alloc] init];

NSArray *urls = [fileManager URLsForDirectory: NSDocumentDirectory

inDomains: NSUserDomainMask];

if ([urls count] > 0){

NSURL *documentsFolder = urls[0];

NSLog(@"%@", documentsFolder);

} else {

NSLog(@"Could not find the Documents folder.");

}

Как видите, создав собственный экземпляр NSFileManager, мы передали значение NSDocumentDirectory в качестве искомого каталога и NSUserDomainMask — в качестве области поиска. Рассмотрим некоторые наиболее важные значения, которые можно передать каждому из параметров метода экземпляра URLsForDirectory: inDomains:, относящегося к классу NSFileManager:

• URLsForDirectory;

• NSLibraryDirectory — библиотечный каталог приложения;

• NSCachesDirectory — каталог кэша — о нем рассказано ранее;

• NSDocumentDirectory — каталог документов;

• inDomains;

• NSUserDomainMask.

Данное значение указывает, что поиск должен выполняться в актуальном пользовательском каталоге. В системе OS X этот каталог обозначался бы ~/.

С помощью этого метода мы можем найти и другие каталоги, например caches, как показано далее:

NSFileManager *fileManager = [[NSFileManager alloc] init];

NSArray *urls = [fileManager URLsForDirectory: NSCachesDirectory

inDomains: NSUserDomainMask];

if ([urls count] > 0){

NSURL *cachesFolder = urls[0];

NSLog(@"%@", cachesFolder);

} else {

NSLog(@"Could not find the Caches folder.");

}

Если вы хотите найти каталог tmp, воспользуйтесь функцией NSTemporaryDirectory() на языке C, вот так:

NSString *tempDirectory = NSTemporaryDirectory();

NSLog(@"Temp Directory = %@", tempDirectory);

Выполнив эту команду на устройстве, получим примерно следующий вывод:

Temp Directory = /private/var/mobile/

Applications/<# Здесь находится ID вашего приложения #>/tmp/

 

См. также

Раздел 12.0.

 

12.2. Запись информации в файлы и считывание информации из файлов

 

Постановка задачи

Требуется сохранить на диске информацию (например, текст, данные, изображения и т. д.).

 

Решение

Все классы Cocoa, обеспечивающие сохранение информации, например NSString, UIImage и NSData, предоставляют методы экземпляра, позволяющие сохранять данные на диске по заданному пути.

 

Обсуждение

Чтобы сохранять текст на диске (предполагается, что ваш текст сохранен в экземпляре NSString или неизменяемой версии этого класса), можно воспользоваться методом экземпляра writeToFile: atomically: encoding: error:, относящимся к этому классу. Этот метод применяется со строками, представляющими собой пути назначения. Вот его отдельные параметры.

• writeToFile — путь к файлу, в который нужно записать информацию, указывается в виде строки.

• atomically — логическое значение. Если оно установлено в YES, то файл сначала будет записываться во временное пространство, а потом перемещаться на тот адрес, где вы хотите его расположить. Так гарантируется, что содержимое файла, которое требуется сохранить, сначала будет просто перенесено на диск, а уже затем пересохранено в месте назначения. Поэтому, если вдруг отказ системы iOS произойдет прежде, чем файл будет сохранен в месте назначения, контент будет доступен и позднее, когда операционная система возобновит работу. И вы сможете сохранить информацию куда следует. При сохранении информации рекомендуется устанавливать данное значение в YES, чтобы ни при каких обстоятельствах не терять информацию работающего приложения безвозвратно.

• encoding — кодировка текста, который вы хотите сохранить по указанному адресу. Обычно в данном случае используется кодировка UTF-8, задаваемая с помощью константы NSUTF8StringEncoding.

• error — принимает указатель на объект NSError. Поэтому если операция сохранения завершится ошибкой и будет прервана, то вы сможете выяснить, какая именно ошибка произошла. Этому параметру можно передать значение nil, если вас не интересуют ошибки, которые могут возникнуть в процессе сохранения. Не забывайте, что эта функция возвращает логическое значение и вы можете воспользоваться им, чтобы просто определить, произошла какая-либо ошибка или нет.

Например, если у вас есть некий текст, который вы хотите сохранить в приложении, но резервно копировать его на уровне системы iOS не требуется, то можно поступить так:

NSString *someText = @"Random string that won't be backed up.";

NSString *destinationPath =

[NSTemporaryDirectory()

stringByAppendingPathComponent:@"MyFile.txt"];

NSError *error = nil;

BOOL succeeded = [someText writeToFile: destinationPath

atomically: YES

encoding: NSUTF8StringEncoding

error:&error];

if (succeeded) {

NSLog(@"Successfully stored the file at: %@", destinationPath);

} else {

NSLog(@"Failed to store the file. Error = %@", error);

}

Кроме того, когда сделаете все это, можете дополнительно убедиться, что вся работа выполнена верно. Попытайтесь считать ту же строку из файла назначения в память. Для этого используется метод класса stringWithContentsOfFile: encoding: error:, относящийся к классу NSString. В ответ вы должны получить автоматически высвобожденную строку, которая представляет собой содержимое указанного файла. Если вы хотите явно инстанцировать объект типа NSString с содержимым файла, просто примените метод экземпляра initWithContentsOfFile: encoding: error:, относящийся к классу NSString, вот так:

— (BOOL) writeText:(NSString *)paramText toPath:(NSString *)paramPath{

return [paramText writeToFile: paramPath

atomically: YES

encoding: NSUTF8StringEncoding

error: nil];

}

— (NSString *) readTextFromPath:(NSString *)paramPath{

return [[NSString alloc] initWithContentsOfFile: paramPath

encoding: NSUTF8StringEncoding

error: nil];

}

— (BOOL) application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

NSString *filePath = [NSTemporaryDirectory()

stringByAppendingPathComponent:@"MyFile.txt"];

if ([self writeText:@"Hello, World!" toPath: filePath]){

NSString *readText = [self readTextFromPath: filePath];

if ([readText length] > 0){

NSLog(@"Text read from disk = %@", readText);

} else {

NSLog(@"Failed to read the text from disk.");

}

} else {

NSLog(@"Failed to write the file.");

}

self.window = [[UIWindow alloc]

initWithFrame: [[UIScreen mainScreen] bounds]];

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

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

Если вы хотите работать с URL, инкапсулированными в экземпляры NSURL (или в экземпляры изменяемой версии этого класса), используйте в данном случае метод экземпляра writeToURL: atomically: encoding: error:.

Экземпляры NSURL могут указывать на ресурсы (файлы, каталоги и т. д.), расположенные в локальной системе или на удаленных устройствах. Так, экземпляр NSURL может представлять локальный файл в каталоге Documents (Документы) в вашем приложении, а другой NSURL — соответствовать URL сайта www.apple.com . Этот класс просто предоставляет вам функции, необходимые для доступа к URL и для работы с ними, независимо от типа конкретного URL.

Другие основополагающие классы обладают примерно такими же методами, как и NSString. Возьмем, к примеру, NSArray. Чтобы сохранить содержимое массива, пользуйтесь методом экземпляра writeToFile: atomically:, относящимся к классу NSArray. Чтобы считать с диска содержимое любого массива, можно просто выделить экземпляр массива, а потом инициализировать его с помощью initWithContentsOfFile: — это метод-инициализатор для работы с массивами. Вот примеры использования обоих методов:

NSString *filePath = [NSTemporaryDirectory()

stringByAppendingPathComponent:@"MyFile.txt"];

NSArray *arrayOfNames = @[@"Steve", @"John", @"Edward"];

if ([arrayOfNames writeToFile: filePath atomically: YES]){

NSArray *readArray = [[NSArray alloc] initWithContentsOfFile: filePath];

if ([readArray count] == [arrayOfNames count]){

NSLog(@"Read the array back from disk just fine.");

} else {

NSLog(@"Failed to read the array back from disk.");

}

} else {

NSLog(@"Failed to save the array to disk.");

}

Метод экземпляра writeToFile: atomically:, относящийся к классу NSArray, может сохранять лишь массивы, содержащие объекты следующих типов:

• NSString;

• NSDictionary;

• NSArray;

• NSData;

• NSNumber;

• NSDate.

Если вы попытаетесь вставить в массив какие-либо другие объекты, то ваши данные не будут сохранены на диске, поскольку этот метод первым делом проверяет, относятся ли все объекты в составе массива к одному из вышеупомянутых типов. Это делается по той простой причине, что в противном случае среда времени исполнения Objective-C просто не разберется, как сохранять данные на диске. Предположим, мы инстанцируем класс под названием Person, создаем для этого класса два свойства, одно из которых соответствует имени, другое — фамилии. Затем инстанцируем экземпляр этого класса и добавим его к массиву. Как же массив сможет сохранить эту информацию о персоне на диск? Никак, поскольку система не будет знать, что именно требуется сохранять. Эта проблема называется «маршалинг». В операционной системе iOS она решена только для перечисленных типов.

Словари также очень похожи на массивы. Сохранение их данных на диске и считывание информации из словаря происходит практически так же, как и в случае с массивами. Имена методов такие же, как и в предыдущем примере, правила сохранения словарей не отличаются от правил сохранения массивов. Вот пример:

NSString *filePath = [NSTemporaryDirectory()

stringByAppendingPathComponent:@"MyFile.txt"];

NSDictionary *dict = @{

@"first name": @"Steven",

@"middle name": @"Paul",

@"last name": @"Jobs",

};

if ([dict writeToFile: filePath atomically: YES]){

NSDictionary *readDictionary = [[NSDictionary alloc]

initWithContentsOfFile: filePath];

/* Теперь сравним словари и проверим, является ли словарь, считываемый

нами с диска, тем самым, который мы сохранили на диске */

if ([readDictionary isEqualToDictionary: dict]){

NSLog(@"The file we read is the same one as the one we saved.");

} else {

NSLog(@"Failed to read the dictionary from disk.");

}

} else {

NSLog(@"Failed to write the dictionary to disk.");

}

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

До сих пор мы применяли для сохранения содержимого на диске высокоуровневые классы, например NSString и NSArray. А что, если потребуется сохранить необработанный массив байтов? Это тоже делается просто. Предположим, у нас есть массив из четырех символов и его требуется сохранить на диск:

char bytes[4]О том, что такое аутлет и чем такая связь отличается от action, подробно рассказано в статье по адресу http://habrahabr.ru/post/30553 /. — Примеч. пер .
 = {'a', 'b', 'c', 'd'};

Чтобы сохранить этот необработанный массив байтов на диске простейшим способом, достаточно инкапсулировать его в другой высокоуровневой структуре данных, например NSData, а потом пользоваться соответствующими методами NSData для считывания данных с диска и записи их на диск. Методы сохранения и загрузки данных, применяемые в классе NSData, практически идентичны соответствующим методам классов NSArray и NSDictionary. Вот пример сохранения необработанных данных на диске и считывания их с диска:

NSString *filePath = [NSTemporaryDirectory()

stringByAppendingPathComponent:@"MyFile.txt"];

char bytes[4]О том, что такое аутлет и чем такая связь отличается от action, подробно рассказано в статье по адресу http://habrahabr.ru/post/30553 /. — Примеч. пер .
 = {'a', 'b', 'c', 'd'};

NSData *dataFromBytes = [[NSData alloc] initWithBytes: bytes

length: sizeof(bytes)];

if ([dataFromBytes writeToFile: filePath atomically: YES]){

NSData *readData = [[NSData alloc] initWithContentsOfFile: filePath];

if ([readData isEqualToData: dataFromBytes]){

NSLog(@"The data read is the same data as was written to disk.");

} else {

NSLog(@"Failed to read the data from disk.");

}

} else {

NSLog(@"Failed to save the data to disk.");

}

 

См. также

Раздел 12.0.

 

12.3. Создание каталогов на диске

 

Постановка задачи

Требуется возможность создавать на диске каталоги и сохранять в них определенные файлы из вашего приложения.

 

Решение

Пользуйтесь методом экземпляра createDirectoryAtPath: withIntermediateDirectories: attributes: error:, относящимся к классу NSFileManager, как показано далее:

— (BOOL) application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

NSFileManager *fileManager = [[NSFileManager alloc] init];

NSString *tempDir = NSTemporaryDirectory();

NSString *imagesDir = [tempDir stringByAppendingPathComponent:@"images"];

NSError *error = nil;

if ([fileManager createDirectoryAtPath: imagesDir

withIntermediateDirectories: YES

attributes: nil

error:&error]){

NSLog(@"Successfully created the directory.");

} else {

NSLog(@"Failed to create the directory. Error = %@", error);

}

self.window = [[UIWindow alloc]

initWithFrame: [[UIScreen mainScreen] bounds]];

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

 

Обсуждение

API, предоставляемые классом NSFileManager, очень просты в использовании. Ничего удивительного в том, что для сохранения каталогов на диске эти API требуют написания всего нескольких строк кода. На первый взгляд метод createDirectoryAtPath: withIntermediateDirectories: attributes: error: может показаться страшноватым, но на самом деле не все так плохо. В дальнейшем я расскажу о различных параметрах, которые можно передать этому методу:

• createDirectoryAtPath — путь к тому каталогу, который требуется создать;

• withIntermediateDirectories — логический параметр. Если он имеет значение YES, то перед созданием конечного каталога метод создаст и все промежуточные каталоги. Например, если вы хотите создать каталог images, вложенный в каталог data, который, в свою очередь, вложен в каталог tmp, а каталог data пока не создан, то можете приказать этому методу создать каталог tmp/data/images/, установив для параметра withIntermediateDirectories значение YES. В таком случае система создаст и каталог data, и каталог images;

• attributes — словарь атрибутов, который можно передать системе. Эти атрибуты будут определять детали создания каталога. Здесь мы не будем использовать этот параметр, чтобы не усложнять пример. Но тут вы можете изменять такую информацию, как дата и время внесения изменений, дата и время создания, а также при желании и другие атрибуты созданного каталога;

• error — данный параметр принимает указатель на объект-ошибку типа NSObject. Этот объект будет заполняться любыми ошибками, которые могут возникать в процессе создания каталога. В принципе, целесообразно передавать объект-ошибку в этом параметре, так что, если он завершится неудачно (возвратит NO), вы сможете обратиться к ошибке и определить, что именно пошло не так.

 

См. также

Раздел 12.1.

 

12.4. Перечисление файлов и каталогов

 

Постановка задачи

Вы хотите построить перечень подкаталогов, содержащихся в каталоге, либо построить список файлов, содержащихся в каталоге. Акт перечисления означает, что вы просто хотите найти все каталоги и/или файлы, расположенные внутри другого каталога.

 

Решение

Используйте метод экземпляра contentsOfDirectoryAtPath: error:, относящийся к классу NSFileManager, как показано далее. В данном примере мы перечисляем все файлы, каталоги и символьные ссылки, расположенные в каталоге пакета с приложением:

— (BOOL) application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

NSFileManager *fileManager = [[NSFileManager alloc] init];

NSString *bundleDir = [[NSBundle mainBundle] bundlePath];

NSError *error = nil;

NSArray *bundleContents = [fileManager

contentsOfDirectoryAtPath: bundleDir

error:&error];

if ([bundleContents count] > 0 &&

error == nil){

NSLog(@"Contents of the app bundle = %@", bundleContents);

}

else if ([bundleContents count] == 0 &&

error == nil){

NSLog(@"Call the police! The app bundle is empty.");

}

else {

NSLog(@"An error happened = %@", error);

}

self.window = [[UIWindow alloc]

initWithFrame: [[UIScreen mainScreen] bounds]];

// Точка переопределения для дополнительной настройки после запуска приложения

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

 

Обсуждение

В некоторых приложениях для iOS иногда требуется строить перечень содержимого каталога. Возможно, вы пока не вполне понимаете, зачем это может понадобиться, поэтому рассмотрим соответствующий пример. Допустим, пользователь хочет скачать из Интернета 10 изображений и кэшировать их в вашем приложении. Вы выполняете эту операцию и сохраняете их, допустим, в каталоге tmp/images/, который создали вручную. Затем пользователь закрывает ваше приложение и вновь открывает его, а вы хотите отобразить в пользовательском интерфейсе вашей программы список уже загруженных файлов-изображений (в табличном виде). Как это сделать? Ничего сложного. Вам всего лишь потребуется перечислить содержимое вышеупомянутого каталога с помощью класса NSFileManager. Как было показано в подразделе «Решение» данного раздела, метод экземпляра contentsOfDirectoryAtPath: error:, относящийся к классу NSFileManager, возвращает массив объектов NSString, которые и соответствуют файлам, подкаталогам и символьным ссылкам внутри заданного каталога. Тем не менее непросто определить, какой из этих объектов является файлом, какой — подкаталогом и т. д. Чтобы получить от файлового менеджера более детализированную информацию, вызовите метод contentsOfDirec toryAtURL: includingPropertiesForKeys: options: error:. Рассмотрим параметры, которые можно передавать этому методу.

• contentsOfDirectoryAtURL — путь к каталогу, который вы хотите просмотреть. Этот путь должен предоставляться как экземпляр NSURL. Не волнуйтесь, если не знаете, как построить этот экземпляр. Вскоре мы об этом поговорим.

• includingPropertiesForKeys — это массив свойств, которые система iOS должна выбирать для каждого файла, каталога или элемента, найденного в конкретной директории. Например, вы можете указать, что в результатах должна возвращаться дата создания каждого элемента. Эта информация должна возвращаться в составе приходящего к вам экземпляра URL (или в экземплярах NSURL, получаемых от фреймворка). Вот список некоторых наиболее важных значений, которые могут находиться в этом массиве:

• NSURLIsDirectoryKey — позволяет постфактум определить, указывает ли один из возвращенных URL на каталог;

• NSURLIsReadableKey — возвращает дату создания того элемента, который расположен по возвращенному URL;

• NSURLContentAccessDateKey — в возвращаемых результатах передает дату последнего обращения к содержимому;

• NSURLContentModificationDateKey — как понятно из названия, это значение позволяет определять дату последнего изменения информации, расположенной по возвращенному URL.

• options — для этого параметра можно передать только одно из двух значений: 0 или NSDirectoryEnumerationSkipsHiddenFiles. Если введено второе значение, то, как понятно из его названия, при построении перечня будут пропущены все скрытые элементы.

• error — ссылка на объект, в который будет записываться информация об ошибке, если методу не удастся выполнить стоящую перед ним задачу. Обычно целесообразно передавать этому методу объекты-ошибки, если есть такая возможность. Если какие-то ошибки и будут возникать, то такие объекты помогут вам более уверенно с ними справляться.

Теперь, когда мы значительно более полно контролируем перечисление элементов, построим перечень всех элементов из каталога. app и выведем даты создания, последнего изменения элемента и последнего обращения к нему. Кроме того, будем выводить информацию о том, является ли этот элемент скрытым, а также есть ли у нас право считывания конкретного файла. Наконец, мы также укажем, являются конкретные элементы каталогами или нет. Приступим:

— (NSArray *) contentsOfAppBundle{

NSFileManager *manager = [[NSFileManager alloc] init];

NSURL *bundleDir = [[NSBundle mainBundle] bundleURL];

NSArray *propertiesToGet = @[

NSURLIsDirectoryKey,

NSURLIsReadableKey,

NSURLCreationDateKey,

NSURLContentAccessDateKey,

NSURLContentModificationDateKey

];

NSError *error = nil;

NSArray *result = [manager contentsOfDirectoryAtURL: bundleDir

includingPropertiesForKeys: propertiesToGet

options:0

error:&error];

if (error!= nil){

NSLog(@"An error happened = %@", error);

}

return result;

}

— (NSString *) stringValueOfBoolProperty:(NSString *)paramProperty

ofURL:(NSURL *)paramURL{

NSNumber *boolValue = nil;

NSError *error = nil;

[paramURL getResourceValue:&boolValue

forKey: paramProperty

error:&error];

if (error!= nil){

NSLog(@"Failed to get property of URL. Error = %@", error);

}

return [boolValue isEqualToNumber:@YES]? @"Yes": @"No";

}

— (NSString *) isURLDirectory:(NSURL *)paramURL{

return [self stringValueOfBoolProperty: NSURLIsDirectoryKey ofURL: paramURL];

}

— (NSString *) isURLReadable:(NSURL *)paramURL{

return [self stringValueOfBoolProperty: NSURLIsReadableKey ofURL: paramURL];

}

— (NSDate *) dateOfType:(NSString *)paramType inURL:(NSURL *)paramURL{

NSDate *result = nil;

NSError *error = nil;

[paramURL getResourceValue:&result

forKey: paramType

error:&error];

if (error!= nil){

NSLog(@"Failed to get property of URL. Error = %@", error);

}

return result;

}

— (void) printURLPropertiesToConsole:(NSURL *)paramURL{

NSLog(@"Item name = %@", [paramURL lastPathComponent]);

NSLog(@"Is a Directory? %@", [self isURLDirectory: paramURL]);

NSLog(@"Is Readable? %@", [self isURLReadable: paramURL]);

NSLog(@"Creation Date = %@",

[self dateOfType: NSURLCreationDateKey inURL: paramURL]);

NSLog(@"Access Date = %@",

[self dateOfType: NSURLContentAccessDateKey inURL: paramURL]);

NSLog(@"Modification Date = %@",

[self dateOfType: NSURLContentModificationDateKey inURL: paramURL]);

NSLog(@"—");

}

— (BOOL) application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

NSArray *itemsInAppBundle = [self contentsOfAppBundle];

for (NSURL *item in itemsInAppBundle){

[self printURLPropertiesToConsole: item];

}

self.window = [[UIWindow alloc]

initWithFrame: [[UIScreen mainScreen] bounds]];

// Точка переопределения для дополнительной настройки после запуска приложения

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

Вывод этой программы получится примерно таким:

Item name = Assets.car

Is a Directory? No Is Readable? Yes

Creation Date = 2013-06-25 16:12:53 +0000

Access Date = 2013-06-25 16:12:53 +0000

Modification Date = 2013-06-25 16:12:53 +0000

Item name = en.lproj

Is a Directory? Yes

Is Readable? Yes

Creation Date = 2013-06-25 16:12:53 +0000

Access Date = 2013-06-25 16:15:02 +0000

Modification Date = 2013-06-25 16:12:53 +0000

Item name = Enumerating Files and Folders

Is a Directory? No Is Readable? Yes

Creation Date = 2013-06-25 16:15:01 +0000

Access Date = 2013-06-25 16:15:04 +0000

Modification Date = 2013-06-25 16:15:01 +0000

Говоря об этом приложении, необходимо отметить, что мы используем метод экземпляра getResourceValue: forKey: error:, относящийся к классу NSURL, чтобы получить значение каждого из ключей, запрашиваемых у файлового менеджера, — например, даты создания и последнего изменения элемента. Эти требования мы передаем файловому менеджеру и приказываем ему выбрать эту информацию. Затем, как только у нас будут нужные URL, воспользуемся вышеупомянутым методом для получения различных свойств от результирующих URL.

Итак, рассмотрим различные части приложения. Я просто объясню, что делает каждый из написанных нами методов.

• contentsOfAppBundle — этот метод выполняет поиск в каталоге. app и находит все его элементы (файлы, подкаталоги, символьные ссылки и др.), после чего возвращает результат в виде массива. Все элементы в этом массиве относятся к типу NSURL и содержат дату собственного создания, последнего изменения, а также другие атрибуты, рассмотренные ранее.

• stringValueOfBoolProperty: ofURL: — этот метод выбирает строковый эквивалент (Yes или No) логического свойства URL. Например, информация о том, указывает конкретный URL на каталог или нет, сохраняется как двоичное логическое значение. Однако если вы хотите вывести это логическое значение на консоль, то его нужно преобразовать в строку. Для каждого URL у нас есть два элемента запроса, которые будут возвращать экземпляры NSNumber. Каждый из этих экземпляров (NSURLIsDirectoryKey и NSURLIsReadableKey) содержит логическое значение. Итак, нам не приходится писать этот код для преобразования дважды, поскольку есть специальные методы для преобразования NSNumber в строку Yes или No.

• isURLDirectory: — принимает URL и проверяет, является ли он каталогом. На внутрисистемном уровне этот метод использует метод stringValueOfBoolProperty: ofURL: и передает ему ключ NSURLIsDirectoryKey.

• isURLReadable: — определяет, обладает ли ваше приложение доступом на чтение по указанному URL. На внутрисистемном уровне этот метод также использует метод stringValueOfBoolProperty: ofURL: и передает ему ключ NSURLIsDirectoryKey.

• dateOfType: inURL: — поскольку мы собираемся просматривать у каждого URL, соответствующего NSDate, свойства трех типов, просто инкапсулируем в данный метод нужный для этого код. Метод будет принимать ключ и возвращать в URL дату, ассоциированную с конкретным ключом.

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

 

См. также

Разделы 12.1 и 12.2.

 

12.5. Удаление файлов и каталогов

 

Постановка задачи

Вы создали на диске ряд файлов и/или каталогов, и они вам больше не нужны. Вы хотите их удалить.

 

Решение

Используйте один из двух методов экземпляра, removeItemAtPath: error: или removeItemAtURL: error:, относящихся к классу NSFileManager. Первый метод принимает путь как строку, а второй — как URL.

 

Обсуждение

Пожалуй, удаление файлов и каталогов — одна из простейших операций, которые можно совершать в файловом менеджере. В iOS нужно обязательно помнить о том, где вы храните ваши файлы и каталоги, а когда хранить их больше не требуется — избавляться от файлов и каталогов. Например, создадим пять текстовых файлов в каталоге tmp/text, а когда закончим работу с ними — удалим эти файлы. Тем временем мы успеем перечислить содержимое каталога по состоянию до и после удаления. Будем делать перечень лишь для того, чтобы убедиться, что все работает правильно. Как вы помните, на момент установки приложения каталог tmp/ существует, а каталог tmp/text — нет. Поэтому для начала потребуется создать второй каталог. Как только закончим работу с файлами, удалим и сам каталог:

/* Создаем каталог по заданному пути */

— (void) createFolder:(NSString *)paramPath{

NSError *error = nil;

if ([self.fileManager createDirectoryAtPath: paramPath

withIntermediateDirectories: YES

attributes: nil

error:&error] == NO){

NSLog(@"Failed to create folder %@. Error = %@",

paramPath,

error);

}

}

/* Создаем пять файлов с расширением. txt в заданном каталоге, называем

их 1.txt, 2.txt и т. д. */

— (void) createFilesInFolder:(NSString *)paramPath{

/* Создаем 10 файлов */

for (NSUInteger counter = 0; counter < 5; counter++){

NSString *fileName = [NSString stringWithFormat:@"%lu.txt",

(unsigned long)counter+1];

NSString *path = [paramPath stringByAppendingPathComponent: fileName];

NSString *fileContents = [NSString stringWithFormat:@"Some text"];

NSError *error = nil;

if ([fileContents writeToFile: path

atomically: YES

encoding: NSUTF8StringEncoding

error:&error] == NO){

NSLog(@"Failed to save file to %@. Error = %@", path, error);

}

}

}

/* Перечисляем все файлы/каталоги, расположенные по заданному пути */

— (void) enumerateFilesInFolder:(NSString *)paramPath{

NSError *error = nil;

NSArray *contents = [self.fileManager contentsOfDirectoryAtPath: paramPath

error:&error];

if ([contents count] > 0 &&

error == nil){

NSLog(@"Contents of path %@ = \n%@", paramPath, contents);

}

else if ([contents count] == 0 &&

error == nil){

NSLog(@"Contents of path %@ is empty!", paramPath);

}

else {

NSLog(@"Failed to enumerate path %@. Error = %@", paramPath, error);

}

}

/* Удаляем все файлы/каталоги по заданному пути */

— (void) deleteFilesInFolder:(NSString *)paramPath{

NSError *error = nil;

NSArray *contents = [self.fileManager contentsOfDirectoryAtPath: paramPath

error:&error];

if (error == nil){

error = nil;

for (NSString *fileName in contents){

/* У нас есть имя файла, но, чтобы удалить этот файл,

нужен полный путь к нему */

NSString *filePath = [paramPath

stringByAppendingPathComponent: fileName];

if ([self.fileManager removeItemAtPath: filePath

error:&error] == NO){

NSLog(@"Failed to remove item at path %@. Error = %@",

fileName,

error);

}

}

} else {

NSLog(@"Failed to enumerate path %@. Error = %@", paramPath, error);

}

}

/* Удаляем каталог, к которому ведет заданный путь*/

— (void) deleteFolder:(NSString *)paramPath{

NSError *error = nil;

if ([self.fileManager removeItemAtPath: paramPath error:&error] == NO){

NSLog(@"Failed to remove path %@. Error = %@", paramPath, error);

}

}

Не забывайте: свойство fileManager, которое мы используем в различных методах делегата нашего приложения, — это свойство самого делегата приложения, определяемое следующим образом:

#import «AppDelegate.h»

@interface AppDelegate ()

@property (nonatomic, strong) NSFileManager *fileManager;

@end

@implementation AppDelegate

<# Здесь находится остаток кода делегата приложения #>

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

1. Создание каталога tmp/txt. Мы знаем, что каталог tmp создается в iOS для каждого приложения, но подкаталог txt на момент установки приложения отсутствует.

2. Создание пяти файлов в каталоге tmp/txt.

3. Перечисление всех файлов в каталоге tmp/txt. Эту операцию нужно выполнить лишь для того, чтобы убедиться, что мы успешно создали в этом каталоге все пять файлов.

4. Удаление всех созданных файлов — собственно, именно эта операция интересовала нас в данном разделе.

5. Повторное перечисление файлов в каталоге tmp/txt. Эту операцию мы выполняем для того, чтобы убедиться, что механизм удаления сработал правильно.

6. Удаление каталога tmp/txt, ведь он нам больше не нужен. Повторюсь: обязательно учитывайте, какие файлы и каталоги вы создаете на диске. Дисковое пространство на дороге не валяется! Поэтому, если какие-то файлы или каталоги вам больше не нужны, обязательно их удаляйте.

 

См. также

Раздел 12.2.

 

12.6. Сохранение объектов в файлах

 

Постановка задачи

Вы добавили в ваш проект новый класс и теперь хотите сохранить этот объект на диск в виде файла, а потом в случае необходимости считать этот файл с диска.

 

Решение

Убедитесь, что ваш класс соответствует протоколу NSCoding, и реализуйте все необходимые методы данного протокола. Не волнуйтесь, я все подробно объясню в подразделе «Обсуждение» данного раздела.

 

Обсуждение

В iOS SDK есть два очень удобных класса, предназначенных именно для этой цели. Процесс, который будет описан в этом разделе, в программировании называется «маршалинг». Вот эти классы.

• NSKeyedArchiver — класс, позволяющий архивировать или сохранять содержимое объекта или дерева объектов по ключам. Каждое значение в классе, скажем каждое свойство, может быть сохранено в архиве с применением ключа, выбранного программистом. Вы получаете архивный файл (далее мы обсудим этот момент подробнее) и просто сохраняете ваши значения с ключами, которые выбраны вами же. Точно как в словаре!

• NSKeyedUnarchiver — этот класс работает совершенно противоположным образом. Он просто дает вам неархивированный словарь и предлагает считать значения в свойства вашего объекта.

Для обеспечения работы класса-архиватора и класса-деархиватора необходимо гарантировать, что те объекты, архивацию и деархивацию которых вы запрашиваете, соответствуют протоколу NSCoding. Начнем с простого класса Person. Вот заголовок этого класса:

#import 

@interface Person: NSObject 

@property (nonatomic, copy) NSString *firstName;

@property (nonatomic, copy) NSString *lastName;

@end

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

• (void)encodeWithCoder:(NSCoder *)aCoder — от этого метода мы получаем сущность-кодировщик. Кодировщик используется точно так же, как словарь. Просто храните в нем значения с ключами на ваш выбор;

• (instancetype)initWithCoder:(NSCoder *)aDecoder; — этот метод вызывается в вашем классе всякий раз, когда вы пытаетесь разархивировать класс с помощью NSKeyedUnarchiver. Просто считывайте значения их экземпляра NSCoder, передаваемого этому методу.

Итак, учитывая сказанное, реализуем наш класс:

#import «Person.h»

NSString *const kFirstNameKey = @"FirstNameKey";

NSString *const kLastNameKey = @"LastNameKey";

@implementation Person

— (void)encodeWithCoder:(NSCoder *)aCoder{

[aCoder encodeObject: self.firstName forKey: kFirstNameKey];

[aCoder encodeObject: self.lastName forKey: kLastNameKey];

}

— (instancetype)initWithCoder:(NSCoder *)aDecoder{

self = [super init];

if (self!= nil){

_firstName = [aDecoder decodeObjectForKey: kFirstNameKey];

_lastName = [aDecoder decodeObjectForKey: kLastNameKey];

}

return self;

}

@end

Как видите, мы работаем с экземпляром класса NSCoder практически так же, как и со словарем. Разница заключается в том, что вместо словарного метода setValue: forKey: мы пользуемся encodeObject: forKey:, а вместо словарного метода objectForKey: задействуем decodeObjectForKey:. Отличия от словарей минимальны.

Итак, с этим классом все понятно. Теперь реализуем механизм архивации и деархивации, пользуясь двумя вышеупомянутыми классами. Мы собираемся сначала инстанцировать объект типа Person, затем заархивировать его, убрать из памяти, потом считать обратно из файла и убедиться, что разархивированное значение совпадает с тем, которое мы изначально записали в класс. Все это мы реализуем в делегате приложения, поскольку там это будет сделать проще всего:

#import «AppDelegate.h»

#import «Person.h»

@implementation AppDelegate

— (BOOL) application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

/* Определяем имя и фамилию, которые собираемся задать в объекте */

NSString *const kFirstName = @"Steven";

NSString *const kLastName = @"Jobs";

/* Определяем, где хотим заархивировать объект */

NSString *filePath = [NSTemporaryDirectory()

stringByAppendingPathComponent:@"steveJobs.txt"];

/* Инстанцируем объект */

Person *steveJobs = [[Person alloc] init];

steveJobs.firstName = kFirstName;

steveJobs.lastName = kLastName;

/* Архивируем объект в файл */

[NSKeyedArchiver archiveRootObject: steveJobs toFile: filePath];

/* Теперь разархивируем этот же класс в другой объект */

Person *cloneOfSteveJobs =

[NSKeyedUnarchiver unarchiveObjectWithFile: filePath];

/* Проверяем, совпадают ли имя и фамилия в разархивированном объекте

с именем и фамилией, которые находились в ранее архивированном объекте */

if ([cloneOfSteveJobs.firstName isEqualToString: kFirstName] &&

[cloneOfSteveJobs.lastName isEqualToString: kLastName]){

NSLog(@"Unarchiving worked");

} else {

NSLog(@"Could not read the same values back. Oh no!");

}

/* Временный файл нам больше не нужен, удаляем его */

NSFileManager *fileManager = [[NSFileManager alloc] init];

[fileManager removeItemAtPath: filePath error: nil];

self.window = [[UIWindow alloc]

initWithFrame: [[UIScreen mainScreen] bounds]];

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

Итак, при архивации просто используется метод класса archiveRootObject: toFile, относящийся к классу NSKeyedArchiver. Этот метод принимает объект и файл, в котором должно быть сохранено содержимое. Все просто. А если нужно разархивировать информацию? Не сложнее архивации. Нам просто нужно найти путь к заархивированному файлу, передать его методу класса unarchiveObjectWithFile:, относящемуся к классу NSKeyedUnarchiver. Всю остальную работу класс выполнит за вас.

 

См. также

Раздел 12.1.