Не оставляем .DS_Store в расшаренных папках

Многие пользователи MacOS, которым приходилось работать в сетях с Windows-компьютерами, наверняка знают о привычке своих Mac’ов оставлять за собой следы после визита в какую-нибудь доступную по сети папку на машине под управлением Windows. Если обладатель PC включил в свойствах проводника опцию “отображать скрытые файлы”, папка, в которой побывал mac-юзер, будет украшена файлом .DS_Store.

По сути, в этом нет ничего страшного. В .DS_Store хранится в основном информация о том, как эта папка будет выглядеть в Finder у “гостя”: позиция окна, размеры иконок и т.д. Но ведь многих подобный мусор в расшаренных папках попросту раздражает.

Чтобы отучить свою систему мусорить в чужих сетевых ресурсах достаточно выполнить совершенно не сложную манипуляцию. Запустить Terminal и выполнить следующую команду:

$ defaults write com.apple.desktopservices DSDontWriteNetworkStores true

После перезапуска сеанса проблема будет решена, но… только для текущего аккаунта. Если есть необходимо сделать тоже самое для всех пользователей на вашем Мас’е – нужна другая команда:

$ cp Library/Prefrences/com.apple.desktopservices.plist \ /Library/Prefrences/. $ sudo chmod 777 /Library/Prefrences/com.apple.desktopservices.plist

Вот и все, после рестарта обновленный файл com.apple.desktopservices обеспечит чистоту в сетевых ресурсах и Windows-пользователи будут вам благодарны.

Disable cache for Apple Mail 2

Приветствую,

Такая вот кратенькая заметочка.

Столкнулся с проблемой, нужно было отключить кеширование удаленных картинок в полученных письмах для Apple Mail 2 Mac OS X Tiger. В 3-й версии все работает нормально, там кеширование оключается программным способом, а вот во 2-й не получается, в связи с тем, что вторая версия имеет баг. Отключить удалось при помощи ограничения прав доступа на папку кеша.

 sudo chown -R root:wheel ~/Library/Caches/Mail
sudo chmod -R 700 ~/Library/Caches/Mail 

Таким образом программа не будет иметь доступ к папке хранения кеша и не будет хранить или читать картинки из нее.

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

Methods binding with NSMutableDictionary

Итак, задача.

Есть клиентское приложение, которое имеет постоянное сокетное соединение. Сервер, периодически, посылает клиентскому приложению набор команд в виде XML. Именем команды является имя ноды в XML. Задача клиентского приложения, обработать эти команды.

В принципе, задача не сложная, если бы не большое колическтво команд. В таком случае, метод-обработчик команд представлял бы из себя гигантскую сущность, состоящую из одних IF … ELSE что не дает красоты коду.

Мы поступим подругому. Мы сделаем коллекцию, имеющую тип ЗНАЧЕНИЕ-КЛЮЧ, в которой будем хранить селекторы, а в качестве ключа будет имя команды.

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

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

Например, у нас есть команда, точнее ответ сервера на команду login. В данном случае мы сделаем метод-обработчик: -(void) processLoginResponse:(Command *)command;

Этот метод обработает пришедший ответ от сервера и выполнит все необходимые проверки и действия. Что внутри этого метода будет дальше, писать не буду. Задача сделать так, чтобы обработчик сетевых пакетов, вытащил ссылку на метод-обработчик команды login и исполнил его, передав параметром команду.

Итак, в главном классе приложения, который и будет заниматься всем этим, назовем его Engine, создадим метод инициализации коллекции методов:

- (void)initCommandsHolder
{
commandsHolder = [[NSMutableDictionary alloc] init];
//commandsHolder - член класса Engine, собственно и будет той коллекцией
//Создаем селектор для нашего метода-обработчика
SEL theSelector = @selector(processLoginResponse:);
//Описываем сигнатуру для нашего метода
NSMethodSignature * aSignature = [Engine instanceMethodSignatureForSelector:theSelector];
//Создаем инстанцию, которая будет стартовать метод и храниться в коллекции
NSInvocation * anInvocation = [NSInvocation invocationWithMethodSignature:aSignature];
//Задаем этой инстанции полученный нами селектор
[anInvocation setSelector:theSelector];
//Сохраняем инстанцию в коллекции
[commandsHolder setObject:anInvocation forKey:@"login"];
}

Итак, у нас есть коллекция, со ссылкой на один метод-обработчик. Далее перейдем к методу, который обрабатывает пакеты от сервера:

- (void) processResponse:(NSString *)response
{
//Десериализуем пакет в инстанс класса Command для дальнейшего использования
Command * command = [[Command alloc]initWithXML:response];
//Проверяем, есть ли в коллекции обработчики
if([commandsHolder count] > 0)
{
//Получаем инстанцию стартера для метода-обработчика по имени команды
NSInvocation * anInvocation = [commandsHolder objectForKey:[command name]];
//Проверяем, удалось ли получить инстанцию
if(anInvocation)
{
//Устанавливаем инстанции целевой класс, у которого есть этот метод-обработчик
[anInvocation setTarget:self];
//Задаем аргумент для запуска
//Индекс равен 2, потому как индексы 0 и 1 являются зарезервированными и скрытыми
//Индекс 0 содержит ссылку на инстанцию стартера, а индекс 1 ссылку на сигнатуру метода
[anInvocation setArgument:&command atIndex:2];
//Запускаем метод-обработчик
[anInvocation invoke];
}
}
}

Все готово. Пока у нас в коллекции один метод, но мы с легкостью можем добавить множество методов и избавить себя от кучи IF ELSE, которые мозолят глаза.

WebView snapshot

Недавно мой заказчик заказал мне написать плугин (bundle) для Apple Mail и Safari. Данный плугин должен был отслеживать прибытие новой почты в Apple Mail, открытие письма, получение события о том, что письмо успешно отрисовалось в форме и снятие snapshot (картинки) полного контента письма.

Я не буду описывать разработку самого плугина в этой статье, опишу это в другой. Вопрос написания плугина довольно сложен. Данная область является недокументированной со стороны Apple!!! Приходилось учиться на примерах :)

Итак, для отображения письма Apple Mail, как и Safari для контента страницы, использует многоуровневый компонент WebView. Задача, сделать изображение ПОЛНОГО контента письма. Я испробовал много методов, но все они приводили к тому, что картинка снималась только с видимой части контрола + скролбары. Это не входило в мои планы. Ссылки команды Apple и многих девелоперов не давали решения. Долгие поиски (две недели!!!) были вознаграждены. Ошибкой оказался выбор дочернего контрола для WebView. Итак, мое решение:

Итак, первое что мы делаем, подписываемся на нотификацию, которая говорит о том, что рендеринг страницы прошел успешно и полностью (это же касается и Safari)

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(webViewLoadFinished:)
name:WebViewProgressFinishedNotification
object:nil];

WebViewProgressFinishedNotification – это глобальная константа фреймворка, содержащая имя нотификации о том, что страница загружена.

Далее, пишем обработчик данной нотификации:

- (void)webViewLoadFinished:(NSNotification *)notification
{
// получаем ссылку на WebView. Она приходит в нотификации
WebView * webView = (WebView *)[notification object];
// а вот самое главное. картинку нужно снимать не с контрола, а с главного фрейма контрола, именно в нем происходит рендеринг страницы в полном маштабе.
WebFrame * frame = [webView mainFrame];
// получаем ссылку на контрол, содержащий этот фрейм.
WebFrameView * view = [frame frameView];
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// подготавливаем репозитарий для хранения картинки, он будет получать картинку путем кеширования содержимого как изображение.
NSBitmapImageRep *imageRep = [[view documentView]
bitmapImageRepForCachingDisplayInRect:[[view documentView] frame]];
// собственно кеширование, теперь картинка в хранилище, в виде bitmap.
[[view documentView] cacheDisplayInRect:[[view documentView] frame] toBitmapImageRep:imageRep];
// перебрасываем нашу картинку в экземпляр класса NSImage
NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(1280, 1024)];
[image addRepresentation:imageRep];
// преобразуем картинку в набор байтов, используя JPEG компрессию, типов компрессий есть много, но я выбрал именно эту.
NSData * imageData = [image TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.0];
[pool release];
}

Вот и все, теперь у нас есть картинка JPEG формата полного контента страницы и мы можем делать с ней что хотим, например сохранить на диск:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES);
NSString *downloadDirectory = [paths objectAtIndex:0];
srand([[NSDate date] timeIntervalSince1970]);
long ext = rand();
NSString * path = [NSString stringWithFormat:@"%@/images/%@%i%@", downloadDirectory, @"web", ext, @".jpg"];
[imageData writeToFile:path atomically:YES];

UIProgressView и потоки

Тема может показаться банальной, но все же.

Столкнулся я как-то с проблемой визуализации процесса поиска на форме приложения iPhone.

Проблема оказалась намного сложнее чем я думал. Все формы приложения для iPhone работают в главном (main) потоке приложения и посему менять значение UIProgressView из другого потока не имеет смысла, графически это не отобразится никак, появится полная полоса прогресса только по окончанию работы метода второстепенного потока. Что я только не делал: пользовался делегатами, пытался сделать Events при помощи нотификаций. Все это не дало успеха.

Но, как оказалось, решить проблему можно и не так сложно.

Буду надеяться, что читатель хоть немного знаком с xCode студией и имеет хоть какое представление о Cocoa Framework и Obj-C.

Итак, задача: у нас есть форма приложения iPhone, контролер-класс к ней и некий класс, называемый Engine который выполняет функции бизнес-процессов, в том числе и наш поиск. Это правильный подход программирования, когда формы не отвечают за логику вообще, а есть специальные объекты для этого, реализующие бизнес-логику (такое вот небольшое лирическое отступление :) ). На нашей форме находятся следующие контролы: UIProgressView – для отображения процесса поиска, дабы нетерпеливый юзер вдруг не подумал, что приложение повисло, UIButton – кнопка, дабы начать процесс поиска. Подразумевается, что при нажатии на кнопку, экземпляру класса Engine, взятому из глобального контекста (туда этот экземпляр попадает при старте приложения), передается сообщение (не забываем, что в Obj-C в Cocoa нет понятия методов класса, а есть понятие посылки сообщения с аргументами) о том, что пора бы запустить поиск.

Итак, обработка нажатия кнопки в контролере формы:

- (IBAction)onSearch {
// получаем делегат нашего приложения, дабы получить доступ к контексту приложения
OurApplicationAppDelegate * appDelegate = (OurApplicationAppDelegate *)[[UIApplication sharedApplication] delegate];
//получаем экземпляр, ссылку на Engine
Engine * engine = [appDelegate getEngine];
// Теперь важное. Подписываемся на нотификации от движка, это своего рода аналог events в других языках
// Нотификация об изменении прогресса поиска. На нее мы меняем значение контрола UIProgressView
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateProgress:)
name:@"progressChanged"
object:nil];
// нотификация о том, что поиск полностью окончен
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(processSearchResult:)
name:@"searchEnd"
object:nil];
// стартуем поиск
[engine onSearch];
}

Итак, это был кусок кода обработки нажатия кнопки.

Далее, обработчик нотификации об изменении значения прогресса:

-(void) updateProgress:(NSNotification *)progressNotification
{
// получаем объект, переданный в нотификации, этот объект - значение для прогресс-бара
NSString * sval = [progressNotification object];
// устанавливаем значение.
progressBar.progress = [sval floatValue];
// передать простой тип данных через нотификацию не удается, посему я передал его как указатель на строку
}

Теперь кусок кода из класса Engine.

Обработчик, который стартует второстепенный поток:

-(void) onSearch
{
// progressThread - член класса Engine: NSThread * progressThread;
progressThread = [[NSThread alloc] initWithTarget:self selector:@selector(doSearch:) object:nil];
[progressThread start];
}

А теперь, собственно, обработчик поиска:

-(void)doSearch:(id)param
{
while(<какое_либо_условие>)
{
// здесь делаем наши действия и нотифицируем подписчиков об изменении прогресса
// проверяем, находимся ли мы в Main потоке
if( !pthread_main_np() )
{
// если нет, то нам сделать расширенную нотификацию
// создаем массив аргументов нотификации
NSMutableDictionary *info = [[NSMutableDictionary allocWithZone:nil] init];
// имя нотификации
[info setObject:@"progressChanged" forKey:@"name"];
//значение для UIProgressView
[info setObject:[NSString stringWithFormat:@"%f", searchProgress] forKey:@"object"];
// вызываем обработчик запуска нотификации из основного потока
[[self class] performSelectorOnMainThread:@selector( _postNotificationName: ) withObject:info waitUntilDone:wait];
[info release];
}
else
{
// если мы в основном потоке, то вызываем обычную нотификацию
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:@"progressChanged" object:[NSString stringWithFormat:@"%f", searchProgress]];
}
}
// нотифицируем об окончании обработки поиска
if( !pthread_main_np() )
{
NSMutableDictionary *info = [[NSMutableDictionary allocWithZone:nil] init];
[info setObject:@"searchEnd" forKey:@"name"];
[[self class] performSelectorOnMainThread:@selector( _postNotificationName: ) withObject:info waitUntilDone:wait];
[info release];
}
else
{
[[NSNotificationCenter defaultCenter] postNotificationName:@"searchEnd" object:nil];
}
[NSThread exit];
}

Ну и последний обработчик, это посылка нотификации:

+ (void) _postNotificationName:(NSDictionary *) info {
NSString *name = [info objectForKey:@"name"];
id object = [info objectForKey:@"object"];
[[NSNotificationCenter defaultCenter] postNotificationName:name object:object userInfo:nil];
}

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