Резюме встречи Apple Developers Community #7
Побывал сегодня на Apple Developers Community #7. Рассказывали про продвижение ios-приложений. По сути докладывались два человека - представитель компании Nevosoft, которая, похоже, на конвейере фигачит игры, и один инди-разработчик.
Было довольно скучно. То, что они рассказывали, можно было рассказать быстрее. Девушке из Nevosoft очень долго задавали глупые вопросы. Как всегда, в зале была пара придурков с жопой в шиле, которым не терпелось показать какие они умные.
Не то, чтобы были какие-то откровения, но многие вещи, пока за тебя их не озвучат, не выглядят важными и не всегда в центре твоего внимания.
Основные моменты. Очень важна иконка приложения. Просто офигеть как важна (я без сарказма) - первое, что видит пользователь - иконка в списке. Она должна быть яркой, советуют делать скрины App Store, вставлять туда свою иконку и смотреть - выделяется ли она, цепляет ли глаз. Плюс советовали смотреть - как выглядит она на экране iPhone среди других программ - пользователь должен быстро её находить визуально.

Второе, что видит пользователь - скрины приложения. Как они выглядят - тоже важно, им тоже нужно уделить внимание. Если они не заинтересуют пользователя - он не будет ставить ваше приложение, даже если оно бесплатное (по своему опыту - согласен). Озвучено мнение, что делать просто скрины приложения не всегда стоит - они куда сочнее выглядят, если их как-то преподнести, например на картинках сделать изображение iPhone с вашим приложением на нём. Apple не требует, чтобы скрины чётко были исключительно скринами приложения, этим многие пользуются, создавая целые коллажи.



В день старта получаешь фору, попадая в список новых приложений в своей категории. Поэтому во многом дальнейшие продажи/скачивания зависят от первого дня.
Дальше говорилось об описании и выборе ключевых слов. Ключевые слова обычно стараются подбирать с умом, чтобы при поиске по ним было меньше конкурентов и тебя увидели. Палка о двух концах - можно найти слово, по которому ты будешь один, только оно хрен кому в голову придёт. Инди девелопер утверждал, что если у тебя ключевое слово присутствует и в названии приложения, и в списке ключевых слов - то ты ранжируешься выше. Это его личный опыт.
Проработать описание тоже стоит, т.к. его обычно если и читают - то максимум первые строки. Если там много букв сразу - то читать далее не хочется (по своему опыту - опять же согласен).
Apple требует, чтобы у приложения был сайт/своя страница в интернете. Это обязательно условие, чтобы у пользователей была обратная связь с разработчиком. Ну и опять же - генерирует в какой-то мере поток пользователей.
Далее обсуждали всякие рекламы, клики, преимущества и недостатки Full и Lite-версии - тоже палка о двух концах. С одной стороны Lite-версия генерирует поток на Full-версию. С другой два приложения раскручивать тяжелее, чем одно.
Инди-разработчик говорил, что надо всё вылизывать - делать все возможные локализации (сам он заказывал перевод у переводчиков на европейские и азиатские языки), каждая новая локализация приносила ему определенный процент новых пользователей. Как следует работать над пресс-релизами, особенно на других языках, вплоть до того, чтобы отдавать на вычитку на сторону их специальным людям.
Ну и самое главное - приложение не должно быть УГ, но это очевидно.
Отправка сообщений в Twitter через php
Давно хотел выложить свой простенький набор функций для отправки твитов в Twitter. Может кому полезно будет, т.к. мне не сразу всё далось, особенно формирование подписи. Плюс это моя попытка закрепить в голове то, что было познано и закодено. По-моему нет лучше способа это сделать, чем попытаться кому-то это объяснить :) Под катом описание и код.
В сети есть прилично скриптов для отправки сообщений в Twitter, но когда встала задача сделать автоматическую отправку твитов (по работе) - все они чем-то не устраивали - или излишней сложностью, или откровенным говнокодом. К тому же в процессе осваивания Twitter API полезно было написать этот велосипед для себя.
Общение с Twitter API происходит посредством авторизации через OAuth (именно OAuth, а не OAuth 2.0, хотя может уже и 2.0 можно). Для отправки запросов их необходимо будет формировать определенным образом и прилагать специально сформированную подпись.
Для начала нужно зарегистрировать приложение на dev.twitter.com, после чего вам выдадут Consumer key и Consumer secret. Также лично вам выдадут Access token и Access token secret, чтобы приложение могло сразу писать от имени вашего пользователя.
Зададим настройки доступа для скрипта:
# settings
$oauth_token = 'Your access token';
$oauth_token_secret = 'Your access token secret';
$oauth_consumer_key = 'Your consumer key';
$oauth_consumer_secret = 'Your consumer secret';
$url = 'http://api.twitter.com/1/statuses/update.json';
Отправка твита состоит в моём случае из 4 этапов:
- Формирование текста твита (строка в 140 символов с текстом, возможно ссылкой).
- Формирование параметров запроса и подписи.
- Формирование заголовков запроса.
- Отправка запроса.
Сам набор состоит из 3-х функций. Главная функция - функция postTweet(). Получает в качестве параметра текст твита. Он должен быть предварительно подготовлен - то есть иметь длину 140 символов. Обычно у меня это делается где-то вне этой функции. Внутри postTweet вызывается 2 остальные функции - функция формирования подписи (makeSignature), и собственно функция отправки твита в Twitter (postTweet).
Пойдем по порядку. Функция создания подписи makeSignature:
/**
* Создаёт подпись к данным
* @global array $oauth_consumer_secret
* @global string $oauth_token_secret
* @param string $url
* @param Array $data
* @return string
*/
function makeSignature( $url, $data ){
global $oauth_consumer_secret, $oauth_token_secret;
$txt = 'POST&' . rawurlencode( $url ) . '&';
$tmp = array();
foreach( $data as $key => $value )
$tmp[] = rawurlencode( $key ) . "%3D" . rawurlencode( $value );
$txt .= implode( "%26", $tmp );
$key = $oauth_consumer_secret . '&' . $oauth_token_secret ;
return base64_encode( hash_hmac( 'sha1', $txt, $key, true ) ) ;
}
На входе функция получает необходимые данные для формирования подписи ($data) и $url, на который будет идти запрос. В двух словах - мы формируем строку. Сначала в ней мы пишем POST, что означает, что мы будем использовать POST-запрос. Входные параметры собираются в нужном виде в строку, параметры должны идти в алфавитном порядке, затем всё это безобразие кодируется по алгоритму HMAC-SHA1, ключом для кодирования является строка:
$key = $oauth_consumer_secret . '&' . $oauth_token_secret;
Закодированная строка и является нашей подписью (signature). На эту простую функцию было потрачено прилично времени и нервов =(
Теперь главная функция - postTweet:
/**
* Отправка сообщения в Твиттер
* @global string $url
* @param string $oauth_consumer_key
* @param string $oauth_token
* @param string $statusText
*/
function postTweet( $statusText ){
global $url, $oauth_consumer_key, $oauth_token;
$nonce = md5( microtime() . mt_rand() );
$time = time();
$date = date( 'r' );
$data = array(
'oauth_consumer_key' => $oauth_consumer_key,
'oauth_nonce' => $nonce,
'oauth_signature_method' => "HMAC-SHA1",
'oauth_timestamp' => $time,
'oauth_token' => $oauth_token,
'oauth_version' => '1.0',
'status' => rawurlencode( $statusText )
);
$signature = makeSignature( $url, $data );
$data['status'] = $statusText;
$data['oauth_signature'] = $signature;
$header = 'OAuth oauth_nonce="' . rawurlencode( $nonce ) . '", ';
$header .= 'oauth_signature_method="HMAC-SHA1", ';
$header .= 'oauth_timestamp="' . rawurlencode( $time ) . '", ';
$header .= 'oauth_consumer_key="' . rawurlencode( $oauth_consumer_key ) . '", ';
$header .= 'oauth_token="' . rawurlencode( $oauth_token ) . '", ';
$header .= 'oauth_signature="' . rawurlencode( $signature ) . '", ';
$header .= 'oauth_version="1.0"';
return curlPostTweet( $url, $header, $date, $data );
}
В эту функцию передаётся уже подготовленная строка нового твита (140 символов), в массив $data формируются данные для запроса. Затем из этих же данных формируется подпись к запросу (вызывается функция makeSignature, описанная выше). Подпись добавляется к данным. Формируются заголовки для HTTP-запроса - по сути это текст "OAuth" + все те же данные массива $data. Всё это дальше передается функции curlPostTweet(), которая собственно уже и постит в Twitter. Делает она это через CURL.
function curlPostTweet( $url, $header, $date, $postData ){
$ci = curl_init();
curl_setopt( $ci, CURLOPT_URL, $url );
curl_setopt( $ci, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ci, CURLOPT_HTTPHEADER, array( 'Authorization', $header, 'Date: ' . $date ) );
curl_setopt( $ci, CURLOPT_POST, 1 );
curl_setopt( $ci, CURLOPT_POSTFIELDS, http_build_query( $postData ) );
return curl_exec( $ci );
}
Думаю в ней нечего пояснять. Можно также посмотреть результат, который вернет запрос. Если что-то не так - в нём будет ошибка и её описание. В противном случае запрос вернет свежедобавленный твит и информацию о нём. А в вашей ленте у свежедобавленного твита будет гордо красоваться в качестве клиента название вашего приложения, которое вы зарегистрировали.
В качестве бонуса прилагаю функцию для получение сокращённой ссылки. В данном случае используется сокращатель ссылок clck.ru от @bobuk
function curlGetShortLink( $url ){
$link = "http://clck.ru/--?url=" . $url;
$ci = curl_init();
curl_setopt( $ci, CURLOPT_URL, $link );
curl_setopt( $ci, CURLOPT_RETURNTRANSFER, true );
while ( true ) {
$returned = curl_exec( $ci );
$status = curl_getinfo( $ci, CURLINFO_HTTP_CODE );
# если сервис недоступен или превышен лимит:
if ( $status == "200" ) {
break;
}
sleep( 2 );
}
return $returned;
}
На этих функциях работают пока 4 бота, которые успешно справляются со своей задачей уже несколько месяцев. Если интересно - @funkysouls и @rutracker_ios, которые парсят rss и постят в Twitter, ну и ещё пара наших ботов по работе.
Спасибо @stay_positive за своевременную помощь, плюс часть кода тут его. Ах да

Проблемы с PUT при обновлении CouchDB до 1.1.0
Сегодня обновили на продакшн-сервере CouchDB до версии 1.1.0. Столкнулись с проблемой - PUT-запросы не работали, возвращали странную ошибку:
[error] => unknown_error
[reason] => function_clause
То есть мы не могли обновить ни один документ в БД, но при этом POST-запросы на создание новых документов вполне себе работали.
Выяснилось, что при обновлении CouchDB с младшей версии до 1.1.0 в системе остаётся две версии какого-то модуля (может быть это касается нескольких модулей) - старая, и новая. Эти две версии друг с другом конфликтуют.
Решение оказалось простым, хотя и довольно странным. Нужно найти в системе, где хранятся *.beam файлы (в нашем случае это /opt/couchdb), удалить их (если страшно - то можно переместить), после чего вернуться в папку с исходными кодами CouchDB и выполнить снова команду:
make install
Потоковое вещание звука на Mac OS X

Так получилось, что в моей комнате колонки находятся в одной части, а компьютер в другой. Провод от колонок тянуть не представляется разумным. Итак - имеются колонки, wi-fi, а также мобильное устройство с wi-fi (в моём случае iPad, но может подойти всё что угодно, что умеет играть по wi-fi музыку). Вспомнил, что есть замечательная программа Nicecast - с её помощью можно организовать в 1 клик онлайн-вещание. Суть в том, чтобы оставаться за своим рабочим местом и рулить музыкой за ним, но играла она в колонках на другой стороне. Кому-то покажется, что проще провод протянуть, но... Fuck yeah, почему бы не заморочиться?
Собственно сама программа проста, правда платная, но при желании это не помеха, если знать места. Во вкладке Source можно выбрать приложение, из которого осуществляется вещание. Я выбрал iTunes. После нажатия Start Broadcast начинается вещание. Не важно, что у вас в программе, из которой идёт вещание, играет. Если ничего - будет вещаться тишина. По умолчанию идёт вещание на 8000 порт, во вкладке Share ссылки для прослушивания с помощью m3u-файла, который понимается всеми современными проигрывателями («настольными»). При желании можно вещать и в интернет, если располагаете внешним IP-адресом - когда то я так слушал музыку на работе, а плеером на другой стороне рулил через TeamViewer.

На другой стороне, как я писал выше, у меня iPad. Сначала я попробовал плеер OPlayer HD, который вроде как умеет играть файлы по URL, однако ссылку он не принял. Других плееров у меня на девайсе не нашлось. Затем я просто попробовал открыть в стандартном браузере Safari - и вуаля, музыка играет. Значит и iPhone, и iPod Touch тоже годятся для проигрывания. Не знаю, как дела обстоят на Android и Windows Phone 7 с этим, но наверняка там m3u-файлы, которые по сути простые плейлисты, тоже чем-то играются.

Поскольку вещание по локальной сети - то мы серьезно не ограничены в скорости и можем выставить качество вещания во вкладке Quality на максимум.
Задержка составляет около 4-5 секунд, так что рассчитывать на то, чтобы посмотреть так кино - не приходится. Хотя, возможно можно от неё избавиться. Насколько я понимаю, Nicecast для вещания предварительно буферизует несколько секунд, и только потом начинает вещать. Возможно в настройках это отключается. Посмотреть не успел, ибо убегал на работу.
Пара настроек FCKEditor

В качестве шпаргалки - чтобы быстро настроить FCKeditor. Хоть он уже и устарел и разработчики делают CKEditor - но в нём нет бесплатного файлового менеджера, а в CKEditor - есть.
Это что-то вроде шпаргалки для быстрого подключения FCKEditor.
-
Как сделать так, чтобы редактор делал HTML, а не XHTML:
В папке с редактором конфиг-файл fckconfig.js, в нём параметр FCKConfig.DocType. По умолчанию он пуст и редактор генерирует код в xhtml-стандарте. Для перевода на html надо вбить туда:FCKConfig.DocType = ''; -
Настройка файлового менеджера:
По адресу fckeditor/editor/filemanager/connectors/php есть файл config.php, в нём то и нужно установить $Config["Enabled"] = true ; (по умолчанию false) и задать пути для $Config["UserFilesPath"] и $Config["UserFilesAbsolutePath"].
Если вы хотите выдавать в коде textarea, а потом через JavaScript заменять их на wysiwyg-редактор, в CKEditor можно просто присвоить им class="ckeditor", в FCKEditor такого нет (или я не нашёл), поэтому если у на странице не 1 textarea, хочется как то все их автоматически заменить на визуальный редактор. FCKEditor, при таком варианте (замена через JavaScript) может обращаться к элементу только через атрибуты id или name. Т.к. id у двух элементов совпадать не может, а name используется частенько для отправки данных через форму, пришлось писать своё. Я загружаю редактор через script и присваиваю всем textarea class="fckeditor". Написал небольшой код на JavaScript с помощью jQuery, который сам заменяет все textarea:
$(document).ready( function() {
var $textareas = $('textarea.fckeditor');
if ( $textareas.length > 0 ) {
$textareas.each(function() {
var textareaName = $(this).attr('name');
var oFCKeditor = new FCKeditor(textareaName);
oFCKeditor.BasePath = "fckeditor/"; // тут должен быть ваш путь до fckeditor
oFCKeditor.ReplaceTextarea();
});
}
});
Ресайз анимированных gif-изображений с помощью Imagick
По работе столкнулся с необходимостью обработки анимированных gif-аватарок. Исходные картинки могут быть любого размера, и их нужно уменьшить до нужного размера с кадрированием до квадрата. Под катом - как мы это решали.
Поскольку проект наш на серверной стороне написан на php - недолго думая мы решили использовать утилиту Imagick. Работаем мы в Ubuntu, посему в 1-2 строчки установить Imagick и php-модуль для него - совсем недолго.
Если взглянуть на документацию Imagick - то видно, что возможностей там полно. В конце поста будет наша готовая функция.
Так как же работать с gif?
Нужно создать два объекта Imagick
<php
# создаём новый пустой объект
$newFileObj = new Imagick();
# оригинальное изображение
$im = new Imagick( $sourceFile );
В данном случае $sourceFile - путь до файла на сервере.
Суть довольно простая - $im - это объект, при помощи которого мы будем работать с gif-изображением. $newFileObj - это объект, который будет хранить данные нового изображения. Простым циклом foreach мы проходим по объекту $im:
<php
foreach ( $im as $newFileObj ) {
$newFileObj->setFormat("gif");
...
}
Имена $newFileObj не случайно совпали. По сути на каждой итерации цикла мы работаем с 1 кадром gif-файла как с отдельным изображением.
Глядя в документацию - не долго искать, как получить информацию о ширине и высоте кадра. После определенных расчётов - сколько и где отрезать от картинки, если она не квадратная, используя метод $newFileObj->cropImage - кадр обрезается. Затем, через метод $newFileObj->setImagePage, мы по сути в новый пустой объект добавляем преобразованный кадр.
Сегодня мы столкнулись с нюансом - не все анимированные gif-изображения корректно обрабатывались. Проблема была в том, что в целях оптимизации фон изображения был в 1 кадре в полном размере, а во всех последующих кадрах были только изменяющиеся фрагменты, которые просто при воспроизведении накладываются на фон. Визуально вы этого не заметите, но по факту каждый такой кадр - это отдельное изображение разного размера. И размер этот был меньше первого кадра, который и определял размер изображения на экране. Поскольку с каждый кадром gif-файла мы работали как с отдельным изображением, изначально мы думали, что все кадры там одного размера. Оказалось, что нет, и это пришлось учитывать.
Для каждого изображения-кадра необходимо было не только расчитать новые размеры, чтобы они были пропорциональны всему изображению, но и расчитать координаты этого изображения, чтобы анимация была на своём месте. Пришлось изрядно поломать мозг (мне, по крайней мере).
Расписывать решение не вижу смысла. Ключевой момент функции:
$im = $im->coalesceImages();
foreach ( $im as $newFileObj ) {
$newFileObj->setFormat("gif");
$new_x = 0;
$new_y = 0;
$tmp_new_width = $newWidth;
$tmp_new_height = $newHeight;
$imagePage = $newFileObj->getImagePage();
# ширина и высота обрезаемой области
# вертикальная картинка
if ( $originalWidth < $originalHeight ) {
$cutedWidth = $originalWidth;
$cutedHeight = $originalWidth;
} else {
#горизонтальная картинка
$cutedWidth = $originalHeight;
$cutedHeight = $originalHeight;
}
$resize_ratio = $cutedHeight / $biggestSideSize ;
$offset_y = $imagePage['y'];
# если размер кадра не совпадает с размером самой картинки
if ( $newFileObj->getImageWidth() < $newWidth ) {
$tmp_new_width = round( $newFileObj->getImageWidth() / $resize_ratio );
$tmp_new_height = round( $newFileObj->getImageHeight() / $resize_ratio );
$offset_x = $imagePage['x'];
$new_x = round( $offset_x / $resize_ratio );
$new_y = round( $offset_y / $resize_ratio );
} else if ( $newFileObj->getImageHeight() < $newHeight ) {
$tmp_new_width = round( $newFileObj->getImageWidth() / $resize_ratio );
$tmp_new_height = round( $newFileObj->getImageHeight() / $resize_ratio );
$offset_x = $imagePage['x'] - ( $originalWidth - $cutedWidth )/2;
$new_x = round( $offset_x / $resize_ratio );
$new_y = round( $offset_y / $resize_ratio );
}
//Выполняется resize до 200 пикселей по ширине и сколько получится по высоте (с соблюдением пропорций, конечно)
$newFileObj->thumbnailImage( $tmp_new_width, $tmp_new_height );
if ( $newFileObj->getImageHeight() >= $biggestSideSize || $newFileObj->getImageWidth() >= $biggestSideSize ) {
$newFileObj->cropImage( $biggestSideSize, $biggestSideSize, $src_x, $src_y );
} else {
$newFileObj->cropImage( $biggestSideSize, $biggestSideSize, 0, $src_y );
}
$newFileObj->setImagePage( $newFileObj->getImageWidth(), $newFileObj->getImageHeight(), $new_x, $new_y );
}
$newFileObj->writeImages( $destinationFile, true);
return image_type_to_extension( $info[2], false );
Отдельно стоит отметить строку:
$im = $im->coalesceImages();
Во время тестирования выяснилось, что при создании на выходе маленьких аватарок - там появлялись артефакты. Эта строчка позволяет избавиться от них. За подсказку спасибо suxxes.
Вот отдельно вся получившаяся функция. Функция создаёт квадрат нужного размера в зависимости от типа изображения. Понимает gif, jpg/jpeg и png. Содержит внутри закомментиованные дебаг-строки (функция dbg), можно раскомментировать и посмотреть, как оно работает.
Функция целиком с кодом и возможностью скачать.
В качестве примера gif-изображения, в котором все кадры - разного размера, предлагаю попробовать этот:
