Думаю, что большинству разработчиков под iOS известно как легко включить iTunes File Sharing в своем приложении, добавив лишь одну строчку в Info.plist:
UIFileSharingEnabled = YES
Но это даже не полдела. Соль в том, что, по-хорошему, приложение теперь должно остлеживать все изменения с файлами, происходящие в директории Documents и соответственно обновлять свои данные. Как это релизовать в своём коде и расскажет данная статья.
Для начала совсем немного теории из Concurrency Programming Guide. В GCD есть такое понятие как dispatch source – фундаментальный тип данных, который ответчает за координацию оброботки специфических низкоуровневых событий. Для решения нашей задачи, нас более всего интересует такая его разновидность как descriptor sources, оповещающий о различных операциях с файлами или сокетами.
Получается, что нам нужно методом dispatch_source_create
создать диспетчер событий, источником которых послужит дескриптор файлов (directory file descriptor), а по самому событию (запись файла) отработает необходимый блок обновления данных приложения.
Итак, ближе к телу. Создадим два основных метода startMonitor
и stopMonitor
, соответственно запускающих и останавлювающи мониторинг нужной нам директории, а также пару-тройку вспомогательных методов проверки изменений в этой директории, которые будут запускаться через handler block.
- (void)startMonitor
{
// проверяем создана ли уже dispatch_source_t
if (_src != NULL) return;
// путь к директории Documents на устройстве
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
// дескриптор файлов, только для нотификации событий (O_EVTONLY)
_fileDescriptor = open([docPath fileSystemRepresentation], O_EVTONLY);
// работаем в главной thread, так как будем обновлять UI
dispatch_queue_t queue = dispatch_get_main_queue();
// создаем тот самый dispatch source для мониторинга событий (write)
_src = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, _fileDescriptor, DISPATCH_VNODE_WRITE, queue);
// handler block отрабатывающий при изменениях в директории
dispatch_source_set_event_handler(_src, ^{
[self directoryDidChange];
});
// при отмене закрываем дескриптор
dispatch_source_set_cancel_handler(_src, ^{
close(_fileDescriptor);
});
dispatch_resume(_src);
}
- (void)stopMonitor
{
if (_src) { // тут все просто, ломать не строить
dispatch_source_cancel(_src);
_src = NULL;
}
}
- (void)directoryDidChange
{
if(!waitingForDocumentsDirectoryTimeout) {
waitingForDocumentsDirectoryTimeout = YES; // включаем флаг ожидания
_lastDocumentsDirectoryReferenceArray=[self documentsDirectoryReferenceArray]; // получаем массив с файлами в директории
//...и запускаем блок проверки файлов с таймаутом в одну секунду, например
[self performSelector:@selector(checkForDocumentsDirectoryChanges) withObject:nil afterDelay:1.0 inModes:[NSArray arrayWithObjects:NSRunLoopCommonModes,nil]];
}
}
-(void)checkForDocumentsDirectoryChanges{
NSArray *newDocumentsDirectoryReferenceArray=[self documentsDirectoryReferenceArray];
// банальный алгоритм сравнения двух массивов
if(![newDocumentsDirectoryReferenceArray isEqualToArray:_lastDocumentsDirectoryReferenceArray]) {
// рекурсивно продолжаем проверку файлов с таймаутом
_lastDocumentsDirectoryReferenceArray=newDocumentsDirectoryReferenceArray;
[self performSelector:@selector(checkForDocumentsDirectoryChanges) withObject:nil afterDelay:1.0 inModes:[NSArray arrayWithObjects:NSRunLoopCommonModes,nil]];
} else {
// синхронизация файлов завершена
waitingForDocumentsDirectoryTimeout=NO;
_lastDocumentsDirectoryReferenceArray=nil;
// ...тут уж нужно вставить ваш блок обновления данных в программе, например
[self scanDocumentsDerectory];
}
}
-(NSArray *)documentsDirectoryReferenceArray {
// возвращает массив со списком файлов в директории формата Название-Размер
NSString *documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSArray *documentsDirectoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectoryPath error:NULL];
NSMutableArray *documentsDirectoryReferenceArray=[NSMutableArray arrayWithCapacity:10];
for(NSString *fileName in documentsDirectoryContents){
NSString *filePath=[documentsDirectoryPath stringByAppendingPathComponent:fileName];
NSError *error;
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
NSInteger fileSize = [[fileAttributes objectForKey:NSFileSize] intValue];
NSString *fileWithLength=[NSString stringWithFormat:@"%@-%d",fileName,fileSize];
[documentsDirectoryReferenceArray addObject:fileWithLength];
}
return documentsDirectoryReferenceArray;
}
Ну, а для тех кто пробежался глазами по тектсу и кому лень разбираться, подобный подход уже реализован в Cocoanetics/DTFoundation класс DTFolderMonitor
.
Еще, из опыта работы над своим приложением, использующем iTunes File Sharing, хочу напомнить о необходимости запуска метода типа scanDocumentDerectory
, помимо мониторинга, как только приложение становится активным. Его назначение не только проверять, но и обновлять данные о файлах в приложении с их фактическим наличием в директории, так как синхронизация файлов с iTunes может происходить и в момент, когда приложение находится в бэкграунде, либо совсем не запущено.
- (void)applicationWillEnterForeground:(UIApplication *)application
{
[self scanDocumentsDirectory];
}
Directory Monitor – олдскульный монитор от Michael Heyeck
Directory Monitoring and GCD он же, но обновленный
Monitoring a Folder with GCD решение от Сocoanetics
Автор: aplekhanov