Русский
Русский
English
Статистика
Реклама

Ios разработка

Из песочницы Как и к чему готовиться на собеседование начинающему iOS-разработчику и не только

21.06.2020 16:16:20 | Автор: admin
Эй, Хаброжитель, приветствую тебя! Буду признателен ко всем твоим фидбэкам.

image

Небольшая предыстория


В 2019 году увлёкся iOS-разработкой и решил попробовать попасть на курс от Mail.ru в их Технопроект с нашим ВУЗом. Закончил данный курс с отличием. Огромное спасибо Диме и Гена за отличный курс. После этого курса начал активно посещать митапы iOS-разработчиков. Летом особо не прогал под iOS. Осенью все же надумал найти работу в этой сфере и развиваться дальше. Решил начать с небольших компаний, поэтому нагуглил топ-100 аутсорс компаний по разработке приложений. Написал всем компаниям, которые находились в Москве.

Из приблизительно 70 компаний ответили около 15 и где-то 3-4 пригласили на интервью.

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

В это время нашел курс по iOS-разработке от Яндекса и прошел его.

Собеседования в Яндексе


В начале декабря был на митапе в Яндексе для джунов. Всем гостям раздали QR-коды для прохождения первого алгоритмического собеса. Там было 3 задачи на 2 часа без перерыва.

Задачи:

  1. Вводится n и нужно посчитать сумму всех чисел от 1 до n < 1000000, где цифры не встречаются более одного раза.
  2. Есть доска M x N (2<M,N<1000000) есть белая и черная лошадь. На входе начальная точка черной и белой лошади. Нужно совершить минимальное кол-во ходов, чтобы они оказались в одной точке и вывести это кол-во.
  3. Вводиться 2 числа N-кол-во чисел в массиве и M-сумма подмассива. Нужно найти подмассив с минимальным кол-во чисел, сумма которой равнялась M, в противно случае нужно вывести что-то другое (не помню).

Есть ограничения по времени и по памяти.

Пока ждал результаты получил оффер в другую компанию, попросил у них неделю на обдумывание, и в последний день этой недели получил письмо от Яндекса. В письме говорилось, что успешно прошел первый этап и приглашают на второй собес. К большому сожалению, не уточнили какая секция будет алгоритмическая или платформенная. Т.к. поверил в свои силы решил отказаться от оффера и приостановить все другие собесы и отдаться на подготовку и прохождения в Яндекс. Достал книжку cracking coding interview, из которой решал задачки в ноябре и стал прорешивать все задачи без ноута на листочке, и забил на универ, чтобы успеть за эту недели. Не хватило времени на все задачи, а последний день перед интервью поботал платформенную часть.

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

Задачи: Сначала поговорили обо всем о чем можно было бы пообщаться на эту тему (Чем отличаются статические языки от динамических?, пару вопрос про каждый тип языков, классы, структуры, стек, куча, массив, список, хеш-таблица, очереди, деревья, графы, про сложности алгоритма, и т.д.). Потом перешли на решение задач.

  1. Есть пять человек: Т, М, А, Л, Д. Они стоят в очереди за магической колой, т.е. тот, кто выпивает его удваивается. На входе n номер следующего, кто должен выпить колу. Нужно найти того, кто будет n-ным.

    Идея решения
    Лучшее решение это hashmap, сам решил через арифметику, т.е. вывел формулу, которая посчитала бы это. //Рассказал всю идею того, как решал бы, потом дал оценку по времени и по памяти, а когда начал писать код, то интервьюер сказал, что он все понял и что код я ему точно напишу так, что давай перейдём к следущей задаче, т.к. у нас осталось чуть больше 20 минут
  2. Есть бинарное дерево поиска. Нужно найти сумму элементов лежащих в сегменте от L до R. Функция на вход получает корневой элемент, левую и правую границу. Тогда уточнил и узнал, что элементы не повторяются и границы включены.

    Идея решения
    Делал через рекурсию. (можно через цикл, но рекурсия проще), опять же сложность алгоритма и т.д.

Где-то ближе к новому году получил письмо об успешном прохождении данного этапа. В 20х числах января связался с HR-ом она сказала, что осталось последнее собеседование (платформенная секция) и назначили дату на следущий день после последнего экзамена. В новогодние праздники и во время сессии готовился еще к собесу. Одним словом было сложно готовится и к экзамена, и к собесу.

Пришел на собес, его проводил Слава из Браузера. Немного пообщались об алгоритмической секции. Дальше он попросил написать схематично код, который реализовывал бы работу многопоточности. Так как до это особо не приходилось работать с многопоточностью, то за счет неожиданности попал в ступор минут на 20-30, просто не думал, что придется писать код. По теории отвечал не плохо, но были проблемы с реализацией. При всем при этом собес очень понравился. Мы не успели реализовать то, что он просил, поэтому Слава предложил дорешать, и нам пришлось сделать это в коридоре, потом немного пообщались. Это было суперски, потому что некоторые ребята ориентируются строго по времени. Через недели две написали, что провалил собес. Было очень обидно т.к. готовился и очень-очень сильно хотел попасть.

Огромный минус собеседований в Яндексе заключается в том, что им неважно попадешь ли ты к ним или нет. Из-за чего они часто игнорят тебя или забывают написать письмо с результатом/(о назначении) собеседования и тебе самому приходится постоянно писать/звонить, чтобы что-то узнать.

Собеседование в МегаФоне


Самое лайтовое и немного не понятное собеседование из всех, что приходилось проходить в крупных компаниях. Собес длился где-то минут 20-30. Пообщались о том, что умею, чем увлекаюсь, какие оценки получаю в универе и т.д. Не было ни одного вопроса по платформе и алгоритмам. Этот собес, скорее всего, тоже не прошел, ибо никакой информации не получил.

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

Самоизоляция говорит: Нет очным собеседованиям!

image

Собеседование в Сбербанк


Было два телефонных интервью, где рассказывал о своих интересах, о том почему решил попробовать попасть в Сбербанк, пару теоретических вопросов связанных с iOS и в общем о программировании. Первое и второе интервью прошел, назначили дату для прохождения очного (последнего этапа) собеса, который из-за режима самоизоляции провели по скайпу. Отлично провели собес со Славой, просто пообщались полностью по платформенной части. Через пару дней пришло письмо о том, что успешно прошел собес, но в данной команде нет месте, поэтому предложат кандидатуру в другую команду. Через 2-3 дня пришло еще одно письмо о том, что набор на стажировку у них закрылся.

Собеседование в ВТБ


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

Прошел их успешно, потом провели заключительный этап собеседования с Мишей. Т.к. сказали, что будет просто платформенная часть и вряд ли будет что-то еще, то особо не готовился. На интервью общались с Мишей об алгоритмах, о структурах, о сети, о многопоточности и т.д. Интервью проходило больше часа, но было очень интересно и классно.

Где-то через неделю позвонили и поздравили с успешным прохождением отбора на стажировку.
На днях подписал договор и получил пропуск и технику. Стажировка должна была пройти в офисе, но в поседений момент все же, к огромному сожалению, перевели на удаленку.

Заключение


Параллельно проходил курс по iOS-разработке на CoursEra, но из-за того, что сейчас началась сессия и стажировка, пришлось приостановить активность на CoursEra.

Советы, которые могут помочь при поиске первой работы в IT


  1. Нужно хорошо разбираться в теории, ибо научиться кодить не сложно.
  2. Откликайтесь на вакансии middle/senior, т.к. найти вакансию intern/junior почти невозможно найти.
  3. Если есть возможность попасть на собеседование, то идите, потому что это поможет вам поднять ваши скилы. (Даже если мало что знаете)
  4. Изучите компанию, в которой будете проходить собеседование и покажите интервьюеру свою заинтересованность работать у них.
  5. Обязательно спрашивайте о том, что вас лично интересует и что хотели бы узнать о рабочем процессе.
  6. Когда чего-то не знаете или не помните, то задавайте уточняющие вопросы, они вам помогут. Если не смогли вспомнить, то просто скажите об это.
  7. Думайте открыто, ведь интервьюер больше смотрит на то, как вы думаете и как ищете выход при сложившихся ситуациях. Если вы просто скажете ответ, то это ни о чем не говорит. Когда вы открыто рассуждаете, то интервьюер видит где вы ошиблись и помогает вам, задавая наводящие вопросы.
  8. Прежде чем пойти на собеседование отрепетируйте его. Задавайте себе всякие вопросы связанные с тем, что должно быть и постарайтесь внятно ответить на них.

Совет компаниям


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

Если будут желающие, то сделаю еще пост с самыми популярными и важными вопросами + ответы и материалы к ним.
Подробнее..

Код ваш, призы наши принимаем заявки на онлайн-хакатон ВТБ More.Tech

25.09.2020 18:15:34 | Автор: admin
Привет! Мы начали приём заявок на ВТБ More.Tech онлайн-хакатон для молодых амбициозных айтишников. От вас профессиональные навыки, желание участвовать в web- или mobile-треках соревнования и умение работать в команде. От нас призовой фонд 900 тыс. рублей и возможность начать карьеру в системообразующем российском банке.

image

С 8 по 11 октября участники полностью погрузятся в профессиональную разработку. Их ожидают митапы с экспертами, комментарии менторов и старших коллег, карьерный коучинг, воркшоп по подготовке презентаций и другие полезные активности. Интересно? Ныряем под кат за подробностями!



Первый трек хакатона для специалистов в web-разработке, второй для разработчиков под iOS/Android. ВТБ More.Tech командное состязание, поэтому лучше заранее собрать под свои знамёна коллег-единомышленников. Впрочем, мы без проблем принимаем заявки от отдельных участников, из которых в дальнейшем сформируются команды.

Задача web-трека создание антифрод-системы для web-приложений банков. Оптимальный, на наш взгляд, состав команды для защиты от мошенников включает frontend- и backend-разработчиков, DevOps-инженера, системного аналитика и product/project-менеджера.

Участникам mobile-трека предстоит разработка приложения, которое сможет распознать модель и марку автомобиля, подобрать актуальное предложение для его продажи и сформировать кредитную заявку. Помимо мобильного и backend-разработчиков mobile-команде явно не помешают дизайнер, а также специалист по компьютерному зрению. И product/project-менеджер, куда же без него.

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

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

Проанализировав опыт крупнейших хакатонов, в том числе знаменитый Tech Fest Munich, мы решили выстроить работу над конкурсными проектами по принципу Dive-Create-Impact: 15 % времени на изучение условий задачи и мозговой штурм, 60 % непосредственно на реализацию, 25 % на доработку и создание презентации. Мы считаем, что такой тайминг способствует принятию наиболее эффективных решений. К тому же участники смогут освоить этот подход и использовать его в своей дальнейшей работе.

Несмотря на режим удалёнки, все четыре дня хакатона насыщены активностями, при этом деятельность каждой команды будут курировать наши менторы. В программе серия митапов с экспертами ВТБ (методология DevOps, гибкие технологии/Agile и другие темы) и мастер-класс по подготовке презентаций. Ознакомиться с расписанием, а также почитать о задачах и условиях участия подробнее можно на официальном сайте хакатона. Там же вы найдёте форму для подачи заявки. Решайтесь скорее: последний день приёма заявок 4 октября.
Подробнее..

Keychain API в iOS

20.11.2020 14:23:38 | Автор: admin
Всем привет!
Не так давно столкнулся с необходимостью использования Keychain-а для обеспечения дополнительной защиты при входе в приложение.
Я нашел много хороших статей по этой теме, но в основном там описываются сторонние фреймворки, упрощающие жизнь, а было интересно посмотреть, как работать с API напрямую. В этой статье я попытался объединить документацию Apple с практикой на простом примере.

Начнем с небольших определений


Keychain зашифрованная база данных, куда сохраняются небольшие объемы пользовательской информации (см. документацию Apple).
Общая схема работы продемонстрирована на рисунке.
image
Keychain API Services в свои очередь являются часть фреймворка Security, но его рассмотрение требует отдельной статьи.

Добавление элемента


let keychainItemQuery = [     kSecValueData: pass.data(using: .utf8)!,     kSecClass: kSecClassGenericPassword ] as CFDictionary let status = SecItemAdd(keychainItemQuery, nil) print("Operation finished with status: \(status)")

Выше приведен пример сохранения пароля в Keychain.
Рассмотрим функцию SecItemAdd подробнее.
func SecItemAdd(_ attributes: CFDictionary,               _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus

На вход подается объект класса CFDictionary, который в свою очередь является ссылкой на неизменяемый объект словаря.
Что в это словарь входит? На самом деле его состав зависит от решаемой задачи здесь же мы просто сохраняем простой пароль, давайте разберем этот простейший запрос.
Итак, kSecClass этот ключ используется для значений хранимых элементов, список их можно посмотреть тут, мы же выбрали стандартный пароль.
kSecValueData ключ, использующийся для передачи данных элемента.
На этом обязательные ключи заканчиваются, далее идут опциональные. Список таких параметров доступен в документации.
Возвращаемое значение типа OSStatus определяет результат операции сохранения/изменения/удаления, у него так же есть масса значений.

Получение элемента


Для получения элемента из Keychain-а используется метод SecItemCopyMatching.
Формируем запрос в виде словаря, где содержится искомый пароль.

let keychainItem = [     kSecValueData: pass.data(using: .utf8)!,     kSecClass: kSecClassGenericPassword,     kSecReturnAttributes: true,     kSecReturnData: true ] as CFDictionary         var ref: AnyObject? let status = SecItemCopyMatching(keychainItem, &ref) if let result = ref as? NSDictionary, let passwordData = result[kSecValueData] as? Data {     print("Operation finished with status: \(status)")     print(result)     let str = String(decoding: passwordData, as: UTF8.self)     print(str) }

Посмотрим логи:
Operation finished with status: 0
{
accc = "<SecAccessControlRef: ak>";
acct = "";
agrp = "xxx.com.maximenko.xxx";
cdat = "2020-11-19 20:39:43 +0000";
mdat = "2020-11-19 20:39:43 +0000";
musr = {length = 0, bytes = 0x};
pdmn = ak;
persistref = {length = 0, bytes = 0x};
sha1 = {length = 20, bytes = 0xxxxxxxxxxxxxxxxxxxxxxx};
svce = "";
sync = 0;
tomb = 0;
"v_Data" = {length = 3, bytes = 0x4b656b};
}
Kek

Как мы видим, возвращен 0, символизирующий успешный результат поиска и выведен весь список атрибутов, полученных из API (Access group параметр затерт на всякий случай:)). Эти атрибуты подробно описаны тут.
Значение по ключу kSecValueData нас собственно тут интересует, успешно разворачиваем его в строку, далее выведенную терминал.

Обновление элемента


Для этого есть метод SecItemUpdate.
На вход подается 2 CFDictionary словаря в первом информация об обновляемом элементе, во втором та информация, на которую надо будет заменить старую.
let query = [     kSecClass: kSecClassGenericPassword,] as CFDictionarylet updateFields = [     kSecValueData: pass.data(using: .utf8)!] as CFDictionarylet status = SecItemUpdate(query, updateFields)

На примере простого пароля в первом запросе указываем что именно требуется найти для замены, во втором то ключ-значение, которое мы обновим в найденном элементе.

Удаление элемента


Для удаления используем SecItemDelete.
У него на входе один параметр словарь c информацией об удаляемом элементе, который надо найти.
Возвращает статус выполнения операции типа OSStatus.
let query = [     kSecClass: kSecClassGenericPassword,     kSecValueData: pass.data(using: .utf8)!] as CFDictionarylet res = SecItemDelete(query)


Подведение итогов


В данной статье рассматривается работа с Keychain-ом на примере нескольких основных методов. Если увидите какие-то неточности, ошибки или просто хотите более подробно обсудить тему, пишите в комментарии или Telegram (skipperprivate).

P.S.
Если будет интересно, можно отдельную статью посвятить работе с более сложными сущностями вроде Интернет-пароля, с большим количеством полей и т.д., или обсудить подобное в комментариях.
Подробнее..

Sign in with Apple дедлайн уже 30 июня

17.06.2020 10:12:43 | Автор: admin

Как мы уже писали в прошлой статье, к 30 июня 2020 все новые аппстор приложения и апдейты должны поддерживать функцию Sign in with Apple.



С апреля у нас появилось несколько мелких, но, возможно, важных деталей по данной теме.


Все надо делать по ГОСТу


Гайдлайны по дизайну кнопок от эппла нельзя нарушать ни в коем случае. Но не факт, что вы узнаете об этом сразу. Сначала мы отправили на ревью билд вот с такой кнопкой для Sign in with Apple.



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


Your app uses Sign in with Apple as a login option but does not use the appropriate Sign in with Apple button design, branding, and/or user interface elements as described in the Sign in With Apple Human Interface Guidelines. Specifically:
-The Sign in with Apple says Apple but should use the following version: Sign in with Apple.
-The custom Sign in with Apple button in your app does not follow Apple button design, branding and/or user interface elements.
Next Steps
To resolve this issue, revise the Sign in with Apple button design, branding and/or user interface elements in your app so that it follows all the Sign in With Apple Human Interface Guidelines.

Мы оперативно собрали штаб, прочитали гайдлайны, поменяли пару стилей и получилось примерно так:



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


Приватные имейл адреса


При логине с айфончика, пользователь может выбрать опцию Hide my email. В этом случае вы получите его прокси имейл, созданный эпплом вида random_chars@privaterelay.appleid.com. Документация утверждает, что по умолчанию на такие адреса нельзя ничего отправить, не сделав дополнительных телодвижений.


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


Но на деле все оказалось несколько иначе:


  • Мне удавалось отправить письмо с личного ящика на прокси имейл, хотя домен моей почты не был прописан в эппловской админке.
  • Некоторые прокси имейлы переставали получать нашу рассылку с ошибкой reason: 550 5.1.1 Relay not allowed; type: bounce. При отправке с личной почты, приходило уведомление Undeliverable. XXX was not found on privaterelay.appleid.com

Если кто-то сталкивался с подобными случаями, опишите, пожалуйста, ваши решения в комментариях.


Кстати, большинство пользователей скрывают свои имейлы с помощью этой функции.


Люди не хотят аутентифицироваться каждый раз


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


User interaction is required any time a new identity token is requested. User sessions are long-lived on device, so calling for a new identity token on every launch, or more frequently than once a day, can result in your request failing due to throttling.

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


Вместо вывода


Sign in with Apple оказалась не такой простой в реализации фичей, как думалось вначале, но довольно удобной для конечных пользователей. Получить настоящий имейл пользователя теперь будет гораздо сложнее, что явно скажется на маркетинге и методах привлечения пользователей. А эппл, как всегда, хочет замкнуть все провода на себя)


Пока что мы добавили данную функцию только для устройств Apple, но далее планируем добавить ее для Android и веб приложения (док). Основная мотивация скорее унифицировать страницу логина для всех платформ, чем прирост DAU.


Если вы нашли еще какие-то интересные особенности Sign in with Apple добро пожаловать в комментарии!


Автор материала Александр Зинчук, продакт менеджер. Материал опубликован в блоге компании Alconost Inc. с разрешения автора.


Alconost занимается локализацией игр, приложений и сайтов на более 70 языков. Мы также делаем рекламные и обучающие видеоролики.
Подробнее

Подробнее..

Как мы накосячили пока делали Бриллиантовый чекаут и что из этого вышло

17.02.2021 16:08:10 | Автор: admin

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

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

При оформлении заказа у нас есть два обязательных экрана: для выбора адреса и для выбора способа оплаты.

На экране адресов мы предвыбираем адрес, куда доставляли в прошлый раз. А на экране со способами оплаты предвыбираем то, чем оплачивали в прошлый раз.

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

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

Снова смотрим в аналитику. Часть людей и правда отпадает при переходе с экрана адресов на экран способов оплаты. Причины могут быть разные: начиная от технических и UX-проблем, заканчивая человеческим фактором клиент и правда мог передумать заказывать, могли поменяться планы.

Относим проблему дизайнерам.

Думаем: дизайн и обработка ошибок в приложении

Прежде чем предложить что-то менять, Паша, наш дизайнер, мощно закопался в то, как всё работает. Прямо в самые-самые мелкие детали, чтобы понимать всю систему насквозь. Особенно изучил меню, промокоды, стопы, оплаты, ошибки. Схемы разные с нами почертил, юзер-флоу для старых и новых клиентов.

Наш дизайнер Паша изучает задачу.Наш дизайнер Паша изучает задачу.

По ходу мы отмечали проблемные места.

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

Автоматические акции. Например, если вы оформляете доставку, то мы дарим вам Додо-набор: пакетик с салфетками, зубочисткой и стикером. Этот или другой акционный подарок добавляется автоматически. А как только условия акции перестают выполняться подарок так же автоматически убирается. Например, если мы добавили вам подарочный Додо-набор, так как вы были на доставке, мы же его и уберём, когда вы переключитесь на самовывоз. В таком случае количество товаров в корзине меняется. А когда меняется количество товаров, может измениться и цена заказа. Ну, если по какой-то причине убрался не подарок, например.

Применение промокода московской пиццерии в Питере. Вы знаете, но я напомню: Додо-пицца франшиза. А это значит, что нашими заведениями владеет не один предприниматель, а сразу много разных. И если московская пиццерия не успела доставить заказ за час и выдала клиенту промокод за опоздание, то питерская пиццерия не обязана этот промокод принимать она ничего не нарушила и расплачиваться за чужие ошибки не должна.

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

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

Стало ясно, что чекаут без геолокации делать будет сложно. Ну, точнее, наличие гео решило бы большую часть проблем: актуальное меню, отсутствующие позиции, промокоды.

А требование сделать без геолокации. Потому что её совсем нет, а делать с нуля долго и сложно: это затрагивает не только приложение, но и наш монолит.

Первая мысль

Первая мысль дизайнера Паши воркэраунд: спрашивать адрес, на который делать доставку, в первую очередь. Этакая геологация без доступа к геолокации. Сергей, наш тогдашний CPO, эту идею забрил. Сказал, что это лишний шаг (Серёжа, прости, если мы твои слова переврали). Из-за этого пришлось расписывать больше кейсов, особенно разные ошибочные состояния.

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

Пришлось задвинуть новый чекаут далеко и надолго.

Переключаемся на кастомизируемые комбо

Вместо чекаута мы взялись за новую задачу кастомизируемые комбо. Это когда ты сам выбираешь какая пицца, десерт или напиток входят в твой набор. Задача непростая, требует много разработчиков и времени. Делали несколько месяцев, старались успеть к федеральной рекламе. В итоге мы чуть-чуть не успели и вместо федеральной рекламы запустили таргетированную в интернете.

Переключаемся на Приложение в ресторане

Комбо доделали, а к чекауту не вернулись снова взялись за более важную задачу: Приложение в ресторане. Оно позволяет заказывать товары по пути с работы домой, мимоходом на прогулке с друзьями или перед обеденным перерывом. Заказываешь заранее, а в ресторан приходишь на всё готовенькое. Да или даже прямо в ресторане можно заказывать, если не хочешь с кассиром общаться или в очереди стоять.

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

Ну, привет, Бриллиантовый Чекаут. Мы возвращаемся к тебе.

Рисуем: 12 макетов на одно и то же

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

Примеры макетов с разных подходов и итераций.Примеры макетов с разных подходов и итераций.

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

http://personeltest.ru/aways/www.scotthurff.com/posts/how-to-design-for-thumbs-in-the-era-of-huge-screens/https://www.scotthurff.com/posts/how-to-design-for-thumbs-in-the-era-of-huge-screens/

Мы работаем во многих странах и везде свои особенности: где-то есть додо-рубли, где-то нет,где-то есть электронные чеки, где-то нет. А в Британии так вообще город можно сменить на экране оплаты. Все эти проблемы приходилось обдумывать и решать.

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

Разрабатываем и ошибаемся

Мы оценили разработку нового чекаута в 2 месяца, а закончили через 9. И вот почему.

Начали со сложного дизайна

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

Нужно было при пуше экранов внутри чекаута поджимать или растягивать эту шторку так, чтобы всё влезло, но и пустого места не было. На иосе на это ушло несколько недель, а нормально ресайзиться всё это дело так и не успели научить. Решили не тратить ещё больше времени и просто заюзать нативную модалку. А вот на андройде ребята справились и даже написали про это статью Анимация в Android: плавные переходы фрагментов внутри Bottom Sheet.

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

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

Меняли дизайн на ходу

Иногда смотрели на макеты, прикидывали поведение экранов, окали и шли делать. А потом, спустя время, понимали, что так-то в целом можно сделать и получше. Шли к дизайнеру, обсуждали, переделывали.

Решение: попробовать затащить прототипы. При планировании закладывать пару дополнительных дней на хоть как-то работающий вариант, который можно посмотреть, потыкать, ощутить.

Переиспользовали код, когда не нужно было

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

Разумеется, там старое непротестированное легаси. Разумеется, туда приходилось вкорячивать ифчики. А потом вообще оказалось, что этот экран строит ещё и вьюмодели для списка пиццерий. А в Москве их 200+ и это нифига не быстро. В конечном итоге поняли, что новый экран со всеми его зависимостями было бы написать быстрее и надежнее: тесты не просто получилось бы написать, так ещё и писать было бы гораздо быстрее.

Решение: не рассчитывать, что переиспользование кода ускорит разработку часто требуется рефактор, а это время. Особенно, если код не был покрыт тестами в таком случае его не получится менять безопасно.

Взяли в команду менторов и новичков

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

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

Решение: не допускать новичков и менторов к разработке фич с горячими дедлайнами. Ну или при планировании закладывать время на онбординг. Кстати, про онбординги у нас тоже есть статья.

Недостаточно точно описывали таски

Один разработчик заболел, его задачи пришлось передать другому. При передаче несколько требований пропустили. А когда таски получили статус Готово готово было не то что нужно. Пришлось переделывать.

Решение: детальнее описывать требования к задачам. В каждой карточке должен быть чеклист того, что надо сделать. За ним надо следить, его надо актуализировать. Ну или конвертировать в отдельные карточки, если чувствуете, что надо ещё подекомпозировать.

Обсуждали одни и те же места по нескольку раз

Фича делается уже несколько месяцев. Некоторые принятые по ней решения были странными, но никто не записал почему было решено делать именно так, а мы уже и забыть забыли. Приходилось обсуждать по второму разу, чтобы вспомнить что к чему.

Решение: копить причины принятых решений.

Неправильно оценили сроки

Оценивали сроки, сейчас не шучу, чуть ли не на глаз. Код мест, с которыми пришлось бы работать, даже не открывали. Ну а че, мы уже ого как долго в проекте, все умные и красивые и так сможем! Оказалось, что нет.

Решение: оценивать таски глядя в код. Прямо берём, открываем исходники и смотрим от начала до конца как мы будем делать фичу. Заползаем в самые дальные уголки кода, которые зацепим. Смотрим, как там написан код. Нет ли старых паттернов, от которых мы уже отходим? Покрыто ли это место тестами? Если какого-то кода нет обсуждаем, каким он будет и как подцепится к имеющемуся.

Так дольше, но оценка будет честнее.

Решили сэкономить на тестах

Начали резать скоуп, когда поняли, что не успеваем: минус там, минус тут. Под нож попали и тесты. В итоге иногда что-то отпадало, приходилось чинить. Иногда отпадало снова и снова. Классика.

Из-за то, что чекаут затаскивался за фича-флагом, а между старым и новым есть пересекающиеся места, что-то ломалось и в старом чекауте. Приходилось следить сразу за двумя модулями.

Решение: не отказываться от тестов, даже если сроки горят. Код без тестов легаси. Без тестов ты выигрываешь пару дней/недель сегодня, но проигрываешь месяцы в будущем.

Не пошарили знания

У нас есть CI. И, конечно же, его кто-то поддерживает. Со стороны иоса на тот момент это был я. Но одновременно с этим я занимался и разработкой нового чекаута.

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

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

Решение: Активнее шарить знания на встречах или во внутренней документации.

Самое главное решение

Не допускать релиза крупных фичей разом, делать всё итеративно.

Так инкремент можно получать гораздо раньше. А не раз в год, как получилось у нас.

Re: Подводим итоги Final_v3

Спустя год разработки наконец-то доталкиваем новый чекаут до релиза. Сначала на Россию, здесь 90% заказов. А потом и на остальные страны, после разных допиливаний: додо-рубли, электронные чеки и прочее, о чем рассказывал выше.

Полезли в аналитику сравнивать конверсию, А ТАМ ТАКОЕ. Цифры просто космические: миллионы посыпались на нас сверху, разработка всего чекаута окупилась буквально за неделю. Потому что конверсия выросла аж на 5%!

А потом поняли, что аналитика кривая. Собрали новую и увидели, что конверсия выросла только на 0,5%. В целом неплохо, но хотелось чуть получше.

Подумали, посовещались, посоветовались и собрали аналитику в третий раз. На этот раз точно, железобетонно и финально: конверсия выросла на 1,5%. В рублях это дополнительные 2 000 000 в неделю.

Работаем над ошибками

БЧ сдан. Возвращаемся к Приложению в Ресторане.

  • Тратим на оценку задачи несколько дней.

  • Декомпозируем до посинения.

  • Постоянно смотрим в код, включаем в оценку время тесты.

  • На аналитику время заложили.

  • И на тестирование тоже.

  • И на возможные баги с прода.

  • И ещё всякого по мелочи.

Составили, в общем, самый настоященский и максимально проработанный роадмап.

И релизнули фичу день в день с планом.

Вот такой вот хеппи енд


Также будет интересно почитать:

Подписывайтесь начат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы, а также на каналDodo Engineering, где мы постим всё, что с нами интересного происходит.

А если хочешь присоединиться к нам в Dodo Engineering, то будем рады сейчас у нас открыты вакансииiOS-разработчиков(а ещё для Android, frontend, SRE и других). Присоединяйся, будем рады!

Подробнее..

Детальный разбор навигации в Flutter

24.07.2020 14:12:14 | Автор: admin

Навигация во Flutter


image


Flutter набирает популярность среди разработчиков. Большенство подходов впостроении приложений уже устоялись иприменяются ежедневно вразработке E-commerce приложений. Тема навигации опускают навторой или третий план. Какой API навигации предоставляет Фреймворк? Какие подходы выработаны? Как использовать эти подходы иначто они годятся?


Введение


Начнём с того, что такое навигация? Навигация это метод который позволяет перемещаться между пользовательским интерфейсом с заданными параметрами.
К примеру в IOS мире организовывает навигацию UIViewController, а в Android Navigation component. А чтопредоставляет Flutter?



Экраны в Flutter называются route. Для перемещениями между route существует класс Navigator который имеющий обширный API для реализации различных видов навигации.



Начнём спростого.Навигация нановый экран(route) вызывается методом push() который принимает всебя один аргумент это Route.


Navigator.push(MaterialPageRoute(builder: (BuildContext context) => MyPage()));

Давайте детальнее разберёмся ввызове метода push:


Navigator Виджет, который управляет навигацией.
Navigator.push() метод который добавляет новый route виерархию виджетов.
MaterialPageRoute() Модальный route, который заменяет весь экран адаптивным кплатформе переходом анимации.
builder обязательный аргумент конструктора MaterialPageRoute, который возвращает пользовательский интерфейс Фреймворк для отрисовки.
[MyPage](https://miro.medium.com/max/664/1Xm96KtLeIAAMtAYWcr1-MA.png)* пользовательский интерфейс реализованный при помощи Stateful/Stateless Widget


Возвращение на предыдущий route


Для возвращения с экрана на предыдущий необходимо использовать метод pop().


Navigator.pop();

Переда данных между экранами


Данные на новый экран передаются через конструктор при создании экрана в builder методе. Этот принцип работает в методах Navigator, где нужно реализовать метод build.


Navigator.push(context, MaterialPageRoute(builder: (context) => MyPage(someData: data)));

В примере продемонстрирована передача данных в класс MyPage (в этом классе хранится пользовательский интерфейс).


Для того чтобы передать данные на предыдущий экран нужно вызвать метод pop() и передать опциональным аргументом туда данные.


Navigator.pop(data);


Состояние виджета Navigator, который вызван внутри одного из видов MaterialApp/CupertinoApp/WidgetsApp. State отвечает за хранение истории навигации и предоставляет API для управления историей.
Базовые методы навигации повторяют структуру данных Stack. В диаграмме можно наблюдать методы и "call flow" NavigatorState.


http://personeltest.ru/aways/habrastorage.org/webt/5w/dg/nb/5wdgnb-tjlngub4c8y4rlpqkeqi.png


Императивный vs Декларативный подход в навигации


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


Давайте на простом примере:


Императивный подход , отвечает на вопрос как?
Пример: Я вижу, что тот угловой столик свободен. Мы пойдём туда и сядем там.


Декларативный подход, отвечает на вопрос что?
Пример: Столик для двоих, пожалуйста.


Для более глубокого понимания разницы советую прочитать эту статью Imperative vs Declarative Programming


Императивная навигация


Вернёмся к реализации навигации. В императивном подходе описывается детали работы в вызывающем коде. В нашем случае это поля Route. В Flutter много типов route, например MaterialPageRoute и CupertinoPageRoute. Например в CupertinoPageRoute задаётся title, или settings.


Пример:


Navigator.push(    context,    CupertinoPageRoute<T>(        title: "Setting",        builder: (BuildContext context) => MyPage(),        settings: RouteSettings(name:"my_page"),    ),);

Этот код и знания о новом route будут хранитьсяв ViewModel/Controller/BLoC/ У этот подхода существует недостаток.


Представим что потребовалось внести изменения в конструкторе в MyPage или в CupertinoPageRoute. Нужно искать каждый вызов метода push в проекте и изменять кодовую базу.


Вывод:


Этот подход не имеет единообразный подход к навигации, и знание о реализации route проникает в бизнес логику.

Декларативная навигация


Принцип декларативной навигации заключается в использовании декларативного подхода в программировании. Давайте разберём на примере.


Пример:


Navigator.pushNamed(context, '/my_page');

Принцип императивной навигации выглядит куда проще. Говорите "Отправь пользователя на экран настроек" передавая путь одним из аргументов навигации.
Для хранении реализации роста в самом Фреймворке предусмотрен механизм у MaterialApp/CupertinoApp/WidgetsApp. Это 2 колбэка onGenerateRoute и onUnknownRoute отвеспющие за хранение деталей реализации.


Пример:


MaterialApp(    onUnknownRoute: (settings) => CupertinoPageRoute(      builder: (context) {                return UndefinedView(name: settings.name);            }    ),  onGenerateRoute: (settings) {    if (settings.name == '\my_page') {      return CupertinoPageRoute(                title: "MyPage",                settings: settings,        builder: (context) => MyPage(),      );    }        // Тут будут описание других роутов  },);

Разберёмся подробнее в реализации:
Метод onGenerateRoute данный метод срабатывает когда был вызван Navigator.pushNamed(). Метод должен вернуть route.
Метод onUnknownRoute срабатывает когда метод onGenerateRoute вернул null. должен вернуть дефолтный route, по аналогии с web сайтами 404 page.


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

Диалоговые и модальные окна


Для того чтобы вызвать диалоговое окна разного типа в Фреймворке предусмотрены глобальные методы. Давайте разберёмся в типах диалоговых окон.


Методы для вызова диалоговых и модальных окон:


  • showAboutDialog
  • showBottomSheet
  • showDatePicker
  • showGeneralDialog
  • showMenu
  • showModalBottomSheet
  • showSearch
  • showTimePicker
  • showCupertinoDialog
  • showDialog
  • showLicensePage
  • showCupertinoModalPopup

Эти методы покрывают базовые потребности разработчиков которые хотят работать с окнами. Если не хватает этого API, тогда стоит разобраться как эти методы работают.


Как работает это под капотом?


Давайте рассмотрим исходный код одного из методов, например showGeneralDialog.


Исходный код:


Future<T> showGeneralDialog<T>({  @required BuildContext context,  @required RoutePageBuilder pageBuilder,  bool barrierDismissible,  String barrierLabel,  Color barrierColor,  Duration transitionDuration,  RouteTransitionsBuilder transitionBuilder,  bool useRootNavigator = true,  RouteSettings routeSettings,}) {  assert(pageBuilder != null);  assert(useRootNavigator != null);  assert(!barrierDismissible || barrierLabel != null);  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(    pageBuilder: pageBuilder,    barrierDismissible: barrierDismissible,    barrierLabel: barrierLabel,    barrierColor: barrierColor,    transitionDuration: transitionDuration,    transitionBuilder: transitionBuilder,    settings: routeSettings,  ));}

Давайте детальнее разберёмся в устройстве этого метода. showGeneralDialog вызывает метод push у NavigatorState с _DialogRoute(). Нижнее подчёркивание обозначает что этот класс приватный и используется только в пределах области видимости в которой сам описан, то есть в пределах этого файла.


Диалоговые и модальные окна которые отображаются при помощи глобальных методов это кастомные route которые реализованы разработчиками Фреймворка.

Типы route в фреймворке


Теперь понятно что"every thins is a route", то есть что связанное с навигацией. Давайте взглянем на то, какие route уже реализованы в Фреймворке.


Два основных route в Flutter это PageRoute и PopupRoute.


PageRoute Модальный route, который заменяет весь экран.
PopupRoute Модальный route, который накладывает виджет поверх текущего route.


Реализации PageRoute:


  • MaterialPageRoute
  • CupertinoPageRoute
  • _SearchPageRoute
  • PageRouteBuilder

Реализации PopupRoute:


  • _ContexMenuRoute
  • _DialogRoute
  • _ModalBottomSheetRoute
  • _CupertinoModalPopupRoute
  • _PopupMenuRoute

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


Вывод:


Универсальный метод для декларативной навигации из капота реализовать невозможно, так как нужно учесть навигацию не только между экранами.

Best practices


В этой части статьи можно задаться вопросом, "а насколько мне всё это нужно?". Ответить на этот вопрос можно легко, если у есть желание сделать масштабируемый API навигации которая в любой момент будет модифицирована под нужды проекта, то это один из вариантов. Если приложение будет в будущем расширяться со сложной логикой.


Начнём с того что мы сделаем некий сервис который будет будет соблюдать следующим аспектам:


  • Декларативный вызов навигации.
  • Отказ от использования BuildContext для навигации (Это критично если сервис навигации будет вызываться в компонентах, в которых нет возможности получить BuildContext).
  • Модульность. Можно вызвать любой route, CupertinoPageRoute, BottomSheetRoute, DialogRoute и т.д.

Для нашего сервиса навигации нам понадобится интерфейс:


abstract class IRouter {  Future<T> routeTo<T extends Object>(RouteBundle bundle);  Future<bool> back<T extends Object>({T data, bool rootNavigator});  GlobalKey<NavigatorState> rootNavigatorKey;}

Разберём методы:
routeTo - выполняет навигацию на новый экран.
back возвращает на предыдущий экран.
rootNavigatorKey GlobalKey умеющий вызывать методы NavigatorState.

После того как мы сделали интерфейс навигации, давайте сделаем реализацию этого интерфейса.


class Router implements IRouter {    @override  GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();    @override  Future<T> routeTo<T>(RouteBundle bundle) async {   // Push logic here  }    @override  Future<bool> back<T>({T data, bool rootNavigator = false}) async {        // Back logic here    }}

Супер, теперь нам нужно реализовать метод routeTo().


@override  Future<T> routeTo<T>(RouteBundle bundle) async {        assert(bundle != null, "The bundle [RouteBundle.bundle] is null");    NavigatorState rootState = rootNavigatorKey.currentState;    assert(rootState != null, 'rootState [NavigatorState] is null');    switch (bundle.route) {      case "/routeExample":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: RouteExample(),          ),        );      case "/my_page":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: MyPage(),          ),        );      default:        throw Exception('Route is not found');    }  }

Данный метод вызывает у root NavigatorState (который описан в WidgetsApp) метод push и конфигурирует его относительно RouteBundle который приходит одним из аргументов в данный метод.


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


enum ContainerType {  /// The parent type is [Scaffold].  ///  /// In IOS route with an iOS transition [CupertinoPageRoute].  /// In Android route with an Android transition [MaterialPageRoute].  ///  scaffold,  /// Used for show child in dialog.  ///  /// Route with [DialogRoute].  dialog,  /// Used for show child in [BottomSheet].  ///  /// Route with [ModalBottomSheetRoute].  bottomSheet,  /// Used for show child only.  /// [AppBar] and other features is not implemented.  window,}class RouteBundle {  /// Creates a bundle that can be used for [Router].  RouteBundle({    this.route,    this.containerType,  });  /// The route for current navigation.  ///  /// See [Routes] for details.  final String route;  /// The current status of this animation.  final ContainerType containerType;}

enum ContainerType тип контейнера, котрый будет задаваться декларативно из вызываемого кода.
RouteBundle класс-холдер данных отвечающих конфигурацию нового route.


Как вы могли заметить у я использовал метод _buildRoute. Именно он отвечает за то, кой тип route будет вызван.


Route<T> _buildRoute<T>({@required RouteBundle bundle, @required Widget child}) {    assert(bundle.containerType != null, "The bundle.containerType [RouteBundle.containerType] is null");    switch (bundle.containerType) {      case ContainerType.scaffold:        return CupertinoPageRoute<T>(          title: bundle.title,          builder: (BuildContext context) => child,          settings: RouteSettings(name: bundle.route),        );      case ContainerType.dialog:        return DialogRoute<T>(          title: '123',          settings: RouteSettings(name: bundle.route),          pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {                        return child;                    },        );      case ContainerType.bottomSheet:        return ModalBottomSheetRoute<T>(          settings: RouteSettings(name: bundle.route),          isScrollControlled: true,          builder: (BuildContext context) => child,        );      case ContainerType.window:        return CupertinoPageRoute<T>(          settings: RouteSettings(name: bundle.route),          builder: (BuildContext context) => child,        );      default:        throw Exception('ContainerType is not found');    }  }

Думаю что в этой функции стоит рассказать о ModalBottomSheetRoute и DialogRoute, которые использую. Исходный код этих route позаимствован из раздела Material исходного кода Flutter.


Осталось сделать метод back.


@overrideFuture<bool> back<T>({T data, bool rootNavigator = false}) async {    NavigatorState rootState = rootNavigatorKey.currentState;  return await (rootState).maybePop<T>(data);}

Ну и конечно перед использованием сервиса необходимо передать rootNavigatorKey в App следующим образом:


MaterialApp(    navigatorKey: widget.router.rootNavigatorKey,    home: Home());

Кодовая база для нашего сервиса готова, давайте вызовем наш route. Для этого создадим инстанс нашего сервиса и каким-либо образом "прокинуть" в объект, который будет вызывать этот инстанс, например при помощи Dependency Injection.



router.routeTo(RouteBundle(route: '/my_page', containerType: ContainerType.window));

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


  • Декларативный вызов навигации
  • Отказ от BuildContext по средствам GlobalKey
  • Модульность достигнута возможностью конфигурирования route относительно имени пути и контейнера для View

Итог


В Фреймворке Flutter существуют различные методы для навигации, которые дают преимущества и недостатки.


Ну и конечно полезные ссылки:
Мой телеграм канал
Мои друзья Flutter Dev Podcast
Вакансии Flutter разработчиков

Подробнее..

SPM модуляризация проекта для увеличения скорости сборки

11.11.2020 12:15:25 | Автор: admin
Привет, Хабр! Меня зовут Эрик Басаргин, я iOS-разработчик в Surf.

На одном большом проекте мы столкнулись с низкой скоростью сборки от трёх минут и более. Обычно в таких случаях студии практикуют модуляризацию проектов, чтобы не работать с огромными монолитами. Мы в Surf решили поэкспериментировать и модуляризовать проект с помощью Swift Package Manager менеджера зависимостей от Apple.

О результатах расскажем в другой статье, а сейчас ответим на главные вопросы: зачем это всё нужно, почему мы выбрали SPM и как делали первые шаги.



Почему именно SPM


Ответ прост это нативно и ново. Он не создает overhead в виде xcworkspace, как Cocoapods, к примеру. К тому же SPM open-source проект, который активно развивается. Apple и сообщество исправляют в нем баги, устраняют уязвимости, обновляют вслед за Swift.

Делает ли это сборку проекта быстрее


Теоретически сборка ускорится из-за самого факта разделения приложения на модули framework'и. Это значит, что каждый модуль будет собираться только в том случае, когда в него внесли изменения. Но утверждать точно можно будет только по окончанию эксперимента.

Note: Эффективность модуляризации напрямую зависит от правильного разбиения проекта на модули.

Как сделать эффективное разбиение


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

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

  • CommonAssets набор ваших Assets'ов и public интерфейс для доступа к ним. Обычно он генерируется с помощью SwiftGen.
  • CommonExtensions набор расширений, к примеру Foundation, UIKit, дополнительные зависимости.

Разделять flow'ы приложения. Рассмотрим древовидную структуру, где MainFlow главное flow приложения. Представим, что у нас новостное приложение.

  • NewFlow экраны новостей и обзора конкретной новости.
  • FavoritesFlow экран со списком избранных новостей и экран обзора конкретной новости с дополнительным функционалом.
  • SettingsFlow экраны настроек приложения, аккаунта, категорий и т. д.

Выносить reusable компоненты в отдельные модули:

  • CommonUIComponents модуль, который содержит в себе любые небольшие UI-компоненты. Обычно они влезают в один файл.
  • Далее список бесконечен и зависит от количества внутренних кастомных UI компонентов. К примеру, нужно создать модули для генерации экранов результата, кастомной коллекции, билдера кастомных алертов и т. д.

Когда нужно выносить компонент в отдельный модуль


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

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

Создаём проект с использованием SPM


Рассмотрим создание тривиального тестового проекта. Я использую Multiplatform App project на SwiftUI. Платформа и интерфейс тут не имеют значения.

Note: Чтобы быстро создать Multiplatform App, нужен XCode 12.2 beta.

Создаём проект и видим следующее:



Теперь создадим первый модуль Common:

  • добавляем папку Frameworks без создания директории;
  • создаём SPM-пакет Common.



  • Добавляем поддерживаемые платформы в файл Package.swift. У нас это platforms: [.iOS(.v14), .macOS(.v10_15)]



  • Теперь добавляем наш модуль в каждый таргет. У нас это SPMExampleProject для iOS и SPMExampleProject для macOS.



Note: Достаточно подключать к таргетам только корневые модули. Они не добавлены как подмодули.

Подключение завершено. Теперь достаточно настроить модуль с public интерфейсом и вуаля, первый модуль готов.

Как подключить зависимость у локального SPM-пакета


Добавим пакет AdditionalInfo как Common, но без добавления к таргетам. Теперь изменим Package.swift у Common пакета.



Добавлять больше ничего не нужно. Можно использовать.

Пример, приближенный к реальности


Подключим к нашему тестовому проекту SwiftGen и добавим модуль Palette он будет отвечать за доступ к палитре цветов, утвержденной дизайнером.

  1. Создаем новый корневой модуль по инструкции выше.
  2. Добавляем к нему корневые каталоги Scripts и Templates.
  3. Добавляем в корень модуля файл Palette.xcassets и пропишем какие-либо color set'ы.
  4. Добавляем пустой файл Palette.swift в Sources/Palette.
  5. Добавим в папку Templates шаблон palette.stencil.
  6. Теперь нужно прописать конфигурационный файл для SwiftGen. Для этого добавим файл swiftgen.yml в папку Scripts и пропишем в нем следующее:

xcassets:  inputs:    - ${SRCROOT}/Palette/Sources/Palette/Palette.xcassets  outputs:    - templatePath: ${SRCROOT}/Palette/Templates/palette.stencil      params:        bundle: .module        publicAccess: true      output: ${SRCROOT}/Palette/Sources/Palette/Palette.swift


Итоговый внешний вид модуля Palette

Модуль Palette мы с вами настроили. Теперь надо настроить запуск SwiftGen, чтобы палитра генерировалась при старте сборки. Для этого заходим в конфигурацию каждого таргета и создаем новую Build Phase назовём ее Palette generator. Не забудьте перенести эту Build Phase на максимально высокую позицию.

Теперь прописываем вызов для SwiftGen:

cd ${SRCROOT}/Palette/usr/bin/xcrun --sdk macosx swift run -c release swiftgen config run --config ./Scripts/swiftgen.yml

Note: /usr/bin/xcrun --sdk macosx очень важный префикс. Без него при сборке вылетит ошибка: unable to load standard library for target 'x86_64-apple-macosx10.15.


Пример вызова для SwiftGen

Готово доступ к цветам можно получить следующим образом:
Palette.myGreen (Color type in SwiftUI) и PaletteCore.myGreen (UIColor/NSColor).

Подводные камни


Перечислю то, с чем мы успели столкнуться.

  • Всплывают ошибки архитектуры и портят всю логику разбиения на модули.
  • SwiftLint & SwiftGen не уживаются вместе при подгрузке их через SPM. Причина в разных версиях yml.
  • В крупных проектах не получится сразу избавиться от Cocoapods. А разбивать уже созданный проект с закреплёнными версиями подов настоящее испытание, потому что SPM только развивается и не везде поддерживается. Но SPM и Cocoapods более-менее работают параллельно: разве что поды могут кидать ошибку MergeSwiftModule failed with a nonzero exit code. Это происходит довольно редко, а решается очисткой и пересборкой проекта.
  • На данный момент SPM не позволяет прописать пути поиска библиотек. Приходится явно указывать их с завязкой на -L$(BUILD_DIR).

SPM замена Bundler?


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

SPM дает возможность вызывать swift run, если добавить Package.swift в корень вашего проекта. Что это нам дает? К примеру, можно вызвать fastlane или swiftlint. Пример вызова:

swift run swiftlint --autocorrect.
Подробнее..

Avito iOS meetup 8 CI-лайфхаки, санитайзеры, IndexStore, перформанс

21.07.2020 12:10:27 | Автор: admin

Привет, Хабр! Всреду 29июля мы проводим восьмой посчёту митап дляiOS-разработчиков. Впрограмме два доклада отинженеров Авито онашем CI и интересных аспектах перформанса, рассказ протехники нормализации отразработчика изSigma Software и выступление англоязычного гостя изLyft проIndexStore.


Тезисы и ссылка нарегистрацию подкатом. Приходите смотреть трансляцию сами и приглашайте коллег.



Доклады


iOS CI as a Service in da House Владислав Алексеев, Авито


image


Однажды вы поймете, что собирать приложение локально вXcode уже не то. Вам захочется истории сборок, хранения релизных бинарей и проверок наPR. Вам потребуется CI и CD. Помере роста команды будет расти нагрузка и наваш CI. Вам потребуется масштабировать сборочную ферму, ускорять компиляцию, заставлять тесты работать быстрее и стабильнее. Сразвитием ваших мобильных приложений вам рано или поздно потребуется познать лучшие практики CI/CD вiOS.

В Авито есть всё, что нужно, дляразработки iOS-приложений: дебажные и релизные сборки, юнит- и UI-тесты, ферма. Мы постоянно добавляем всё больше и больше проверок тысячи юнит-тестов, сотни нативных UI-тестов, множество performance-тестов, различные дополнительные проверки. Но всё это добро занимает почти30минут на pull request-е уже два года подряд. Киллер фича унас нет очередей насборки, они стартуют вместе соткрытием PR! Вдокладе я расскажу, как мы достигли этого. Надеюсь, что вы научитесь нанаших фейлах и воодушевитесь нашими идеями!

Затрагиваемые темы: TeamCity, bash, Python, билды и тесты, CocoaPods, build tracing, Puppet, ферма, Xcode, импакт анализ.

О спикере: Владислав работает винфраструктурных проектах, связанных сосборками и тестированием. Начал свою карьеру вЯндексе, где работал надприложениями Яндекс.Карты и Яндекс.Браузер подiOS. Затем работал вФейсбуке надпроизводительностью основного приложения и системной сборки Buck. С2017 года работает вАвито, занимается инфраструктурой мобильных приложений.



Укрощение нормализованного состояния. Граф объекты и санитайзеры Алексей Демедецкий, Sigma Software


image


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

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

О спикере: я занимаюсь мобильной разработкой около10лет. Заэто время успел попробовать много разных подходов. Последние 5лет практикую и рассказываю прооднонаправленные подходы (redux, flux, mvi) вмобильной разработке. Всвободное время пишу свой карманный язык Arrow. Задать мне вопросы можно втвиттере.



What the IndexStore Has To Say Dave Lee, Lyft


image


Code is data, but what kind ofdata? For a given token, a language server can give a JSON object ofrelevant info. For a file, a parser can provide an AST. Both of these scopes are optimized fordifferent use cases. Other use cases can benefit from having data for all the code ina project. Swift and Clang both provide a project wide view ofthe code, we know it as Xcode's index. The IndexStore has a lot ofpotential formaking tools. This talk will explore and demonstrate some uses forthe IndexStore

Dave Lee is a software engineer inthe Bay Area working onsoftware for other software engineers. Dave is a dad to two daughters who show no interest incode, except that one time I used Python to do word scramble homework.



Абстрактные техники перформанса Тимур Юсипов, Авито


image


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

О спикере: руководитель команды Performance вАвито. Люблю iOS, футбол, походы, велосипед и ролики.



Пароли и явки


Онлайн-трансляция нанашем ютуб-канале стартует 29июля в18:00 поМоскве. Закончить планируем к20:30. На трансляции можно сразу нажать кнопку напомнить, чтобы ничего не пропустить.


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


До встречи вонлайне!

Подробнее..

Создаем Swift Package на основе Cбиблиотеки

13.01.2021 02:07:50 | Автор: admin
Фото Kira auf der HeideнаUnsplashФото Kira auf der HeideнаUnsplash

Данная статья поможет вам создать свой первый Swift Package. Мы воспользуемся популярной C++ библиотекой для линейной алгебры Eigen, чтобы продемонстрировать, как можно обращаться к ней из Swift. Для простоты, мы портируем только часть возможностей Eigen.


Трудности взаимодействия C++ и Swift

Использование C++ кода из Swift в общем случае достаточно трудная задача. Все сильно зависит от того, какой именно код вы хотите портировать. Данные 2 языка не имеют соответствия API один-к-одному. Для подмножества языка C++ существуют автоматические генераторы Swift интерфейса (например,Scapix,Gluecodium). Они могут помочь вам, если разрабатывая библиотеку, вы готовы следовать некоторым ограничениям, чтобы ваш код просто конвертировался в другие языки. Тем не менее, если вы хотите портировать чужую библиотеку, то, как правило, это будет не просто. В таких ситуациях зачастую ваш единственный выбор: написать обертку вручную.

Команда Swift уже предоставляет interop дляCиObjective-Cв их инструментарии. В то же время,C++ interopтолько запланирован и не имеет четких временных рамок по реализации. Одна из сложно портируемых возможностей C++ шаблоны. Может показаться, что темплейты в C++ и дженерики в Swift схожи. Тем не менее, у них есть важные отличия. На момент написания данной статьи, Swift не поддерживает параметры шаблона не являющиеся типом, template template параметры и variadic параметры. Также, дженерики в Swift определяются для типов параметров, которые соблюдают объявленные ограничения (похоже на C++20 concepts). Также, в C++ шаблоны подставляют конкретный тип в месте вызова шаблона и проверяют поддерживает ли тип используемый синтаксис внутри шаблона.

Итого, если вам нужно портировать C++ библиотеку с обилием шаблонов, то ожидайте сложностей!


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

Давайте попробуем портировать вручную С++ библиотеку Eigen, в которой активно используются шаблоны. Эта популярная библиотека для линейной алгебры содержит определения для матриц, векторов и численных алгоритмов над ними. Базовой стратегией нашей обертки будет: выбрать конкретный тип, обернуть его в Objective-C класс, который будет импортироваться в Swift.

Один из способов импортировать Objective-C API в Swift это добавить C++ библиотеку напрямую в Xcode проект и написатьbridging header. Тем не менее, обычно удобнее, когда обертка компилируется в качестве отдельного модуля. В этом случае, вам понадобится помощь менеджера пакетов. Команда Swift активно продвигаетSwift Package Manager (SPM). Исторически, в SPM отсутствовали некоторые важные возможности, из-за чего многие разработчики не могли перейти на него. Однако, SPM активно улучшался с момента его создания. В Xcode 12, вы можете добавлять в пакет произвольные ресурсы и даже попробовать пакет в Swift playground.

В данной статье мы создадим SPM пакетSwiftyEigen. В качестве конкретного типа мы возьмем вещественную float матрицу с произвольным числом строк и колонок. КлассMatrixбудет иметь конструктор, индексатор и метод вычисляющий обратную матрицу. Полный проект можно найти наGitHub.


Структура проекта

SPM имеет удобный шаблон для создания новой библиотеки:

foo@bar:~$ mkdir SwiftyEigen && cd SwiftyEigenfoo@bar:~/SwiftyEigen$ swift package initfoo@bar:~/SwiftyEigen$ git init && git add . && git commit -m 'Initial commit'

Далее, мы добавляем стороннюю библиотеку (Eigen) в качестве сабмодуля:

foo@bar:~/SwiftyEigen$ git submodule add https://gitlab.com/libeigen/eigen Sources/CPPfoo@bar:~/SwiftyEigen$ cd Sources/CPP && git checkout 3.3.9

Отредактируем манифест нашего пакета,Package.swift:

// swift-tools-version:5.3import PackageDescriptionlet package = Package(    name: "SwiftyEigen",    products: [        .library(            name: "SwiftyEigen",            targets: ["ObjCEigen", "SwiftyEigen"]        )    ],    dependencies: [],    targets: [        .target(            name: "ObjCEigen",            path: "Sources/ObjC",            cxxSettings: [                .headerSearchPath("../CPP/"),                .define("EIGEN_MPL2_ONLY")            ]        ),        .target(            name: "SwiftyEigen",            dependencies: ["ObjCEigen"],            path: "Sources/Swift"        )    ])

Манифест является рецептом для компиляции пакета. Сборочная система Swift соберет два отдельных таргета для Objective-C и Swift кода. SPM не позволяет смешивать несколько языков в одном таргете. Таргет ObjCEigen использует файлы из папкиSources/ObjC, добавляет папкуSources/CPP в header search paths, и опеделяетEIGEN_MPL2_ONLY, чтобы гарантировать лицензию MPL2 при использовании Eigen. ТаргетSwiftyEigen зависит отObjCEigen и использует файлы из папкиSources/Swift.


Ручная обертка

Теперь напишем заголовочный файл для Objective-C класса в папкеSources/ObjCEigen/include:

#pragma once#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface EIGMatrix: NSObject@property (readonly) ptrdiff_t rows;@property (readonly) ptrdiff_t cols;- (instancetype)init NS_UNAVAILABLE;+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(zeros(rows:cols:));+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(identity(rows:cols:));- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(value(row:col:));- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(setValue(_:row:col:));- (EIGMatrix*)inverse;@endNS_ASSUME_NONNULL_END

У класса есть readonly свойства rows и cols, конструктор для нулевой и единичной матрицы, способы получить и изменить отдельные значения, и метод вычисления обратной матрицы.

Дальше напишем файл реализации вSources/ObjCEigen:

#import "EIGMatrix.h"#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdocumentation"#import <Eigen/Dense>#pragma clang diagnostic pop#import <iostream>using Matrix = Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic>;using Map = Eigen::Map<Matrix>;@interface EIGMatrix ()@property (readonly) Matrix matrix;- (instancetype)initWithMatrix:(Matrix)matrix;@end@implementation EIGMatrix- (instancetype)initWithMatrix:(Matrix)matrix {    self = [super init];    _matrix = matrix;    return self;}- (ptrdiff_t)rows {    return _matrix.rows();}- (ptrdiff_t)cols {    return _matrix.cols();}+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Zero(rows, cols)];}+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Identity(rows, cols)];}- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)col {    return _matrix(row, col);}- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)col {    _matrix(row, col) = value;}- (instancetype)inverse {    const Matrix result = _matrix.inverse();    return [[EIGMatrix alloc] initWithMatrix:result];}- (NSString*)description {    std::stringstream buffer;    buffer << _matrix;    const std::string string = buffer.str();    return [NSString stringWithUTF8String:string.c_str()];}@end

Теперь сделаем Objective-C код видимым из Swift с помощью файла вSources/Swift(смотритеSwift Forums):

@_exported import ObjCEigen

И добавим индексирование для более чистого API:

extension EIGMatrix {    public subscript(row: Int, col: Int) -> Float {        get { return value(row: row, col: col) }        set { setValue(newValue, row: row, col: col) }    }}

Пример использования

Теперь мы можем воспользоваться классом вот так:

import SwiftyEigen// Create a new 3x3 identity matrixlet matrix = EIGMatrix.identity(rows: 3, cols: 3)// Change a specific valuelet row = 0let col = 1matrix[row, col] = -2// Calculate the inverse of a matrixlet inverseMatrix = matrix.inverse()

Наконец, мы можем составить простой проект, который продемонстрирует возможности нашего пакета, SwiftyEigen. Приложение позволит вносить значения в матрицу 2x2 и вычислять обратную матрицу. Для этого, создаем новый iOS проект в Xcode, перетаскиваем папку с пакетом из Finder в project navigator, чтобы добавить локальную зависимость, и добавляем фреймворк SwiftyEigen в общие настройки проекта. Далее пишем UI и радуемся:

Смотрите полный проект наGitHub.


Ссылки

Спасибо за внимание!

Подробнее..

IOS in-app purchases Конфигурация и добавление в проект

22.06.2020 16:10:43 | Автор: admin

Всем привет, меня зовут Виталий, я основатель Adapty. Подписки один из способов монетизировать приложение. С их помощью вы можете дать пользователю возможность получить постоянный доступ к обновляемому контенту в приложении или же к предоставляемому сервису. В отличие от обычных покупок, где Apple берет себе 30% комиссию, на подписках эта комиссия сокращена до 15% в случае, если пользователь подписан в течение 1 года и более.Важныймомент: если пользователь отменит подписку, то данный счетчик сбросится через 60 дней.


В этой части мы научимся:


  • Создавать покупки в App Store Connect
  • Конфигурировать подписки указывать длительность, стоимость, пробные периоды
  • Получать список покупок в приложении

когда подключаешь покупки в приложении


Создание покупок


Перед тем, как начать внедрять покупки внутри приложения необходимо:


  • Оплатить Apple Developer аккаунт как физическое лицо или организация.
  • Приняты все соглашения в App Store Connect. Обновленные соглашения будут появляться сверху в личном кабинете App Store Connect, их легко заметить.

Предположим, что в нашем приложении будет два вида подписок: месячная и годовая. У каждой из них мы сделаем пробный период длиной в 7 дней.


На странице нашего приложения в App Store Connect открываем вкладку In-App Purchases Manage. На этой вкладке отображается список созданных нами покупок. Для того, чтобы создать новую покупку, необходимо нажать на кнопку(+),которая находится около заголовка In-App Purchases.



Интерфейс создания покупок


Далее мы попадаем в диалог создания покупки. Наш выбор Auto-Renewable Subscription.



Выбираем 3 пункт


Следующим шагом нам будет предложено создать группу подписок(SubscriptionGroup). Группа подписок это множество подписок с главным свойством, что пользователь не может активировать две подписки одновременно из одной группы. Дополнительно, все introductory offers такие как триал применяются ко всей группе сразу. Группы нужны для того, чтобы отделять бизнес логику внутри приложения.


Назовем нашу группу Premium Access. При добавление следующей подписки интерфейс предложит добавить ее в уже существующую группу. Позже вы можете управлять группами в меню In-App Purchases Subscription Groups



Создание Группы подписок


Далее конфигурируем название подписки


  • Reference Name то, как будет отображаться подписка в App Store Connect, а также в разделе Sales и в отчетах
  • Product ID уникальныйидентификатор продукта, который используется в коде приложения

Рекомендую выбирать читаемое название для Product ID, из которого можно почерпнуть максимальное количество информации. Хорошей практикой является указание длины подписки, так меньше запутаетесь потом в аналитике.



Добавим второй продукт точно так же, в итоге наш интерфейс вкладки In-App Purchases Manage будет выглядеть следующим образом:



Две подписки в приложении


Конфигурация подписок


Покупки добавили, но пока они не готовы к использованию: Status выше имеет значение Missing Metadata. Это означает, что мы пока не добавили информацию по цене и периоду подписки. Сейчас мы это исправим.


Длительность и цены


Кликаем на продукт и конфигурируем его.



Здесь нам необходимо выбрать период(SubscriptionDuration). В нашем случае, выбираем 1 Month или 1 Year. После чего переходим в меню конфигурации цен. Можно гибко настраивать цены в зависимости от стран, но мы ограничимся автоматическими ценами, выбрав только цену в USD. App Store Connect автоматически переведет цены в другую валюту, не всегда ясно, как это происходит. Скорее всего для ваших целевых рынков вы захотите поменять цену руками.



Бесплатный пробный период (free trial)


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


Чтобы зайти в нужное нам меню, нажмите на кнопку(+)рядом с заголовком и выберите пунктCreate Introductory Offerиз выпадающего списка:



Выбираем список стран



Выбираем длительность оффера. Можно поставить No End Date, если не хотите себя ограничивать.



Последний этап выбор типа оффера. Как видно на следующем скриншоте, существует три типа:


  • Pay as you go использование со скидкой: пользователь платит сниженную цену в течение нескольких начальных периодов, а после становится обычным подписчиком со стандартными ценами.
  • Pay up front предоплата за использование приложения: пользователь сразу платит некоторую стоимость и получает возможность использовать приложение в течение определенного времени, а затем, также становится обычным подписчиком.
  • Free бесплатный пробный период, по истечение которого пользователь может стать подписчиком.

Нас интересует третий вариант, а продолжительность(Duration)устанавливаем на 1 неделю.



Сохраняем настройки.


Получение списка SKProduct


Проверим, что мобильное приложение видит покупки и заложим фундамент для дальнейшей реализации проведения покупок.


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


import StoreKitclass Purchases: NSObject {    static let `default` = Purchases()    private let productIdentifiers = Set<String>(        arrayLiteral: "barcode_month_subscription", "barcode_year_subscription"    )    private var productRequest: SKProductsRequest?    func initialize() {        requestProducts()    }    private func requestProducts() {        // Will implement later    }}

Только идентификаторов недостаточно, чтобы полноценно пользоваться покупками необходимо получить: стоимость, валюту, локализацию, скидки. Возвращает всю эту и дальше большую информацию класс SKProduct. Чтобы получить эту информацию, нам необходимо сделать запрос к Apple. Создадим объектSKProductsRequest, назначим емуdelegate вметодыdelegateбудет приходить результат запроса. Вызываем методstart(), который инициализирует асинхронную процедуру:


private func requestProducts() {        productRequest?.cancel()        let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)        productRequest.delegate = self        productRequest.start()        self.productRequest = productRequest}

Если операция пройдет успешно, будет вызван методproductsRequest(didReceive response:), в котором и будет содержаться вся необходимая нам информация:


extension Purchases: SKProductsRequestDelegate {    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {        guard !response.products.isEmpty else {            print("Found 0 products")            return        }        for product in response.products {            print("Found product: \(product.productIdentifier)")        }    }    func request(_ request: SKRequest, didFailWithError error: Error) {        print("Failed to load products with error:\n \(error)")    }}

Если все прошло успешно, то в результате в лог будет выведены две строчки:


Found product: barcode_month_subscriptionFound product: barcode_year_subscription

При этом, если будет ошибка, то SKProduct с таким ID не вернется. Такое может быть, если продукт по какой то причине стал не валидный.


Ну вот и все, очередной забор за нашей спиной.


Спасибо Алексею Гончарову x401om за подготовку статьи. В следующей статье мы разберемся, как проводить покупки внутри приложения: открывать/закрывать транзакции, обрабатывать ошибки, валидировать receipt и другое.

Подробнее..

Мой Covid-19 lockdown проект, или как я полез в кастомный UICollectionViewLayout и получил ChatLayout

14.10.2020 20:13:37 | Автор: admin

image


Да, да. Я понимаю что на дворе 2020 год, что все хардкорные IOS разработчики пишут исключительно на SwiftUI и Combine, и писать статьи про UIKit как то не айс. И, тем не менее, 2020 год выдался не таким как все предыдущие года. Совсем не таким.


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


Чат использует MessageKit в качестве UI компонента. MessageKit swift библиотека поддерживаемая комьюнити призванная заменить устаревшую JSQMessagesViewController. Мне довелось работать с JSQMessagesViewController лет эдак 5 назад. Он справлялся со своими задачами, был минимально гибок, написан в лучших традициях UIKit где все наследуется от всего и, конечно, к выходу swift-а уже полностью морально устарел. К счастью, мне не пришлось к нему больше возвращаться и я только порадовался появившейся тогда инициативе написать библиотеку для создания UI для чата которая бы заменила JSQMessagesViewController. И забыл об этом до марта 2020-го.


Как же я возненавидел MessageKit в марте 2020-го. Она мне показалась построчной перепиской JSQMessagesViewController с Objective-C на Swift и приветом из IOS разработки 2009 года. Я не буду вдаваться в подробности и ругать чужую работу в которой я не принимал участие.


Но, среди огромного количества issues, которые остаются открытыми на гит хабе, или тех, которые прямо документированы в коде библиотеки, остро выделялась проблема скроллинга. UIScrollView по умолчанию ведет себя так, что якорь скроллинга закреплен в в верхнем левом углу контента и новый контент добавляется в низ без сдвига остального контента. Это поведение подходит для 99% случаев использования, но не для чата, где ожидается обратное поведение. При добавлении нового контента в низ нам нужно сместить коэффициент сдвига (contentOffset) на величину добавленного контента.


Этот факт натолкнул меня на простую мысль. Все это делается какими-то странными хуками и паразитированием на UICollectionViewFlowLayout. Почему бы просто не написать лайаут который делал бы все это из коробки?


Второй момент, который подтолкнул меня к тому что хорошо бы разобраться с UICollectionViewLayout было то что несмотря на паразитирование на UICollectionViewFlowLayout, MessageKit не поддерживала автоматические размеры ячеек используя Auto-Layout и нужно было все размеры считать в коде самому. Не смотря на то что данная фича доступна в UICollectionViewFlowLayout из коробки.


Ну значит решено. Я напишу свой кастомный UICollectionViewLayout где постараюсь добавить из коробки все что нужно для чата. Сказано сделано. Я создал проект и создал класс который унаследовал от UICollectionViewLayout.
Все казалось довольно простым и я полез в документацию.


Анимированные вставка, удаление, обновление и изменение порядка


И вот тут то меня ждал первый сюрприз. Официальная документация на UICollectionViewLayout отсутствовала. Точнее она как бы номинально присутствует. Но слабо соответсвует действительности или опускает некоторые очень важные моменты.


Так, например, первым делом, я решил разобраться даже не с размером ячеек, а с анимацией вставки, удаления, обновления и изменения порядка ячеек. Как видим тут 4 типа обновления ячейки. За это дело в UICollectionViewLayout отвечают 2 метода:
initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?
finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?
А вот и официальная документация initialLayoutAttributesForAppearingItem и finalLayoutAttributesForDisappearingItem
Давайте прочтем документацию к initialLayoutAttributesForAppearingItem вместе:


When your app inserts new items into the collection view, the collection view calls this method for each item you insert. Because new items are not yet visible in the collection view, the attributes you return represent the items starting state. For example, you might return attributes that position the item offscreen or set its initial alpha to 0. The collection view uses the attributes you return as the starting point for any animations. (The end point of the animation is the items new location and attributes.) If you return nil, the layout uses the items final attributes for both the start point and end point of the animation.
The default implementation of this method returns nil. Subclasses are expected to override this method, as needed, and provide any initial attributes.

Кроме сообщения нам как делать вставку и, еще, немного не очень ясной дополнительной информации, ни о каком апдейте ячейки или сдвиге ячейки речи не идет. С finalLayoutAttributesForDisappearingItem тоже самое. Ладно, можно попробовать по разному толковать


If you return nil, the layout uses the items final attributes for both the start point and end point of the animation.

и попробовать реализовать все остальное используя вот это утверждение.


Не тут то было. Оно работает так для самых примитивных случаев. Но, еще и самое забавное что никак не описан момент а что же происходит c itemIndexPath? Вот вставил я ячейку с индексом 0. Та которая была до этого 0 вероятно станет 1? А в какой момент? А если я вставляю ячейку 0 и сдвину ячейку которая была до этого номером 0 за ячейку номер 2?


Я не считаю себя шибко умным, но, мне всегда казалось, что с базовой арифметикой и логикой у меня порядок. Но с какими бы теориями я не подходил к данному вопросу, с базовой логикой, с нахрапу, я ответить на эти вопросы не мог. Самое забавное что еще и поведение на IOS 12 и IOS 13 отличалось. Один подход мог работать в одной и не работать в другой и наоборот. Ну что ж.


Тогда я решил что я буду логировать вызовы функций UICollectionViewFlowLayout и то, что выводит мой ChatLayout, и, когда приведу их к одному и тому же состоянию, наверняка, мой код будет вести себя идентично. Я провел несколько вечером вглядываясь в листинги вызова функций и отдаваемых значений UICollectionViewFlowLayout и моего лайаута, пытаясь уловить логику и подкручивая мой лайаут что бы он выдавал те же значения. О, Наивный. В тот момент я еще не знал/не думал что UICollectionViewFlowLayout использует private API мне не доступное Нетленная классика от Apple и UIKit. Вообще, это все тянет на отдельную статью. Скажу только что в какой то момент, когда я уже был готов сдаться, у меня все типы этой анимации стали получаться. При том что мой лог вызовов/значений отличался от того что выдавал UICollectionViewFlowLayout. Ну работает и работает.


Определение размера ячейки при помощи AutoLayout



Настало время идти дальше и разобраться с автоматическим определением размеров используя AutoLayout. Тут я стал задаваться вопросом. А не изобретаю ли я велосипед. Почему же все так сложно? Померить то еще можно, но как это все впихнуть еще и в анимацию? Может уже есть кто-то кто разобрался со всеми этими проблемами и готовое решение лежит на гит-хабе?


Первая странность была в том что кастомных UICollectionViewLayout не очень то и много. Да они существуют. Но они могут только разложить ячейки в зависимости от задачи которые они решают, а вот анимацию или не поддерживают толком или анимированная вставка будет из коробки. Либо они наследуются от UICollectionViewFlowLayout и как то там подменяют ему атрибуты в prepare методе и пытаются как то с этим взлететь. Причем, вся эта анимация и поддержка AutoLayout ячеек отсутсвует или оставляет желать лучшего. И тут, я натолкнулся на луч света в темном царстве: airbnb/MagazineLayout. Вот прям реально и без сарказма. Это был единственный реально кастомный лайаут который делал почти все что я хотел. И я с удивлением обнаружил что моя реализация initialLayoutAttributesForAppearingItem /finalLayoutAttributesForDisappearingItem ну прям очень похожа на то что написали ребята из AirBnB.


Но дальше по вопросу задачи. Я подсмотрел у них то, что заняло бы у меня "сложно предположить" сколько времени как при обновлении ячейки заменить размеры ячейки с анимацией! Я пробовал разные способы и они работали, но всегда находился случай когда они работали криво.


Так вот, некое подобие состояния ячейки описано в UICollectionViewLayoutAttributes, и если во время анимации вы сохраните именно тот объект который вы вернули из initialLayoutAttributesForAppearingItem а потом в методе invalidationContext(forPreferredLayoutAttributes:, withOriginalAttributes originalAttributes:) вы возьмете этот объект и поменяете у него значение frame то внутри UICollectionView сработает какое то KVO (Key-Value-Observing) и она выдаст вам анимацию которую вы ожидаете. Забудьте о preferredAttributes, забудьте originalAttributes. Стабильно работает только вот так. РукаЛицо.


О прокрутке


Теперь, когда вроде что то как то заработало настала очередь разбираться с прокруткой. Точнее мне нужно было изменить поведение UIScrollView на обратное стандартному, что бы когда мы добавляем контент в конец, он не уходил вниз, а наоборот все остальное сдвигалось вверх. Некоторые разработчики решают это поворачивая коллекцию вверх ногами а потом переворачивая контент внутри обратно. Но тогда вы теряете стандартные вещи такие как adjustedContextInsets, и отступ клавиатуры надо отсчитывать сверху а не снизу, и вообще вся геометрия встает с ног на голову. Я подумал что решить это наверняка можно прям из UICollectionViewLayout. Казалось бы, простая задача. У UICollectionViewLayout есть метод targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint Переопределяй его и будет тебе счастье, UICollectionView перед обновлением скажет тебе я мол вот собираюсь сделать свой contentOffset вот таким, а если ты хочешь его поправить верни мне свой.


Вот только проблема в том что этот вызов происходит еще до того как ты можешь получить какие то реальные размеры ячеек, и, если какие то расчеты еще происходят во время анимированного апдейта, использовать его не получится. Да, можно частично в процессе анимированного апдейта возложить расчеты на имеющийся у UICollectionViewLayoutInvalidationContext contentOffsetAdjustment, но, вы не всегда можете им воспользоваться до завершения транзакции анимации. Поэтому пришлось рассчитывать 2 разных величины, proposedContentOffset то, что можно понять до начала анимации, и вторую которой я воспользуюсь в конце транзакции в методе finalizeCollectionViewUpdates. Вроде добился того что надо. Все это работало хорошо пока вы не удаляете ячейки таким образом, что у вас сверху не появляются ячейки, которые еще не были рассчитаны с помощью AutoLayout, и которые вовремя анимации могут изменить свой размер. А вот UICollectionView при уменьшении размера контента (contentSize) начинает игнорировать contentOffsetAdjustment и никаким образом я не смог заставить ее работать так как хотелось мне: ни используя contentSizeAdjustment в различных комбинациях, ни даже меняя contentOffset напрямую. Решением оказалось игнорировать необработанные ячейки при анимации и рассчитывать все остальное потом.


Но главное что желаемого удалось достичь.


Немного о багах и private API



У меня, при прокрутке, изображение слегка подергивалось и я никак не мог понять почему. Но решил не сосредотачиваться пока более менее не прояснится картинка. А потом начал гуглить и нашел вот такой rdar://40926834: Self Sizing + Prefetching Bugs От тех же замечательных ребят из AirBnB. Выключив префетчинг который включен по умолчанию, я моментально избавился от подобного поведения.


А вот еще один от них же rdar://46865293: Cell's autoresizing mask breaks self-sizing. Ну вот не вызывается func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? столько раз сколько обещано, хоть ты тресни.


Мой любимый, так же подсмотренный в MagazineLayout это использование UICollectionViewFlowLayout приватного метода _prepareForCollectionViewUpdates:withDataSourceTranslator: для того что бы все красиво рассчитать до вызова открытого метода prepareForCollectionViewUpdates. Ай да Apple, Ай да UIKit. Это удалось обойти введение специального флага: если транзакция начинается скажи UICollectionView что в ней ничего нет и начинай рассчитывать исходя из того что получишь в prepareForCollectionViewUpdates. Это позволило избавиться от некоторых артефактов.


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


Может, конечно, это и не баги вовсе. Возможно, я что то не понял. И, вообще, зря я на ребят из UIKit наезжаю. Я оставлю этот вопрос открытым.


Немного о хрупкости


Вообще, все эти UICollectionView + UICollectionViewLayout вещи довольно хрупкие. Меняете размер коллекции и, в этот момент, что то вставляете получите артефакты. Меняете анимированно contentInset, например, для клавиатуры, и что то меняете в коллекции получите артефакты. Скроллите коллекцию и меняете ее получите артефакты. Просто что то делаете не так получите артефакты. Почему я не исправлял некоторые вещи? Если я подставлял вместо своего лайаута UICollectionViewFlowLayout и получал точно такое же поведение я записывал это в недостатки самого устройства связки UICollectionView + UICollectionViewLayout и это означало, что они должны быть исправлены "из вне". Поэтому, вы увидите в приложении-примере, которое идет вместе с библиотекой, набор флагов из серии: выдвигаешь клавиатуру не обновляй/Выдвинул обнови и т.д.


Пора в продакшен?



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


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


Больше мы не используем MessageKit. И пока что всем довольны. Производительность и удобство внесения изменений оказались достойными периодических похвал коллег. А большинство открытых багов чата в Jira закрылись сами собой.


Что в остатке?


А в остатке оказалась открытая библиотека ChatLayout. Не смотря на то, что я, пока, считаю ее preview, она вполне пригодна для использования в продакшене. И, мне кажется, что если кто-то еще начнет ее использовать, то, благодаря возможностям комьюнити, получится устранить ее возможные изъяны и добавить функции, которые мне не были нужны. Цель этой статьи представить ее сообществу и привлечь разработчиков.


Я оставил пока код слегка не оптимизированым для того что бы было удобнее вносить изменения. Без всякой там прямой записи в память при изменении и т.д. Производительность в релизной сборке на актуальных устройствах достойная и без этого. Кроме того: надо вычистить приложение-пример. Да и тестов добавить. Я сам помимо основной работы поддерживаю библиотеку RouteComposer. и, боюсь, что 2 open-source проекта я просто не потяну. Я, вот, едва нашел время и вдохновение написать эту статью.


Перечислю то что я на данный момент считаю достоинствами данного подхода.


О библиотеке


ChatLayout альтернативный подход к созданию UI для чата. Фактически библиотека состоит кастомного UICollectionViewLayout, который предоставляет некоторые дополнительные методы, которые могут потребоваться для решения этой задачи. Плюсом, в Extras лежат небольшие UIView, которые, при желании, можно использовать для каких-то тривиальных задач.


Достоинства


  • Полностью поддерживает динамический лайаут для ячеек (UICollectionViewCell) и вспомогательных UIView (UICollectionReusableView).
  • Поддерживает анимированную вставку/удаление/обновление/перемещение ячеек.
  • Удерживает contentOffset у основания видимой области UICollectionView во время апдейтов.
  • Предоставляет методы для точной прокрутки к нужной ячейке.
  • Поставляется с простыми UIView-контейнерами, которые могут облегчить создание кастомных ячеек сообщений.

Недостатки (но, по-моему, все равно достоинства)


ChatLayout это кастомный UICollectionViewLayout, поэтому:


  • Вам не нужно наследоваться от какого либо UIViewController или UICollectionView. Вам нужно написать их самим. Так как вам удобно
  • Вам не нужно использовать какие то особые UICollectionViewCell которые идут с библиотекой. Создавайте их так как вам удобно.
  • Вам не нужно в обязательном порядке рассчитывать размеры ячеек. ChatLayout сделает это за вас. При том, только для видимых в данный момент ячеек, не важно, сколько их в всего в коллекции. Но, если вы укажите хотя бы приблизительный размер производительность только выиграет.
  • Вам не предоставляется никакая базовая модель данных. Создавайте ее как вам удобно. И напишите свой UICollectionViewDataSource который будет отображать ее на экране. Приложение-пример использует для расчета изменений модели DifferenceKit, но вы можете использовать что угодно.
  • ChatLayout не работает с клавиатурой. Вы можете написать свое решение или можете использовать любую подходящую для этого библиотеку. Все что требуется от вас в конечно итоге для поддержки клавиатуры это изменить contentInsets вашего UICollectionView.
  • ChatLayout не предоставляет вам никакого поля для ввода текста. Приложение-пример использует стороннюю библиотеку InputBarAccessoryView (ту же что использует MessageKit). Вы можете использовать ее или любое другое решение.

Собственно все. Спасибо за внимание. Буду рад Вашим комментариям.


Ах, да. Гифки. (Осторожно эпилептикам)






Подробнее..

Мой Covid-19 lockdown проект, или, как я полез в кастомный UICollectionViewLayout и получил ChatLayout

14.10.2020 22:06:16 | Автор: admin

image


Да, да. Я понимаю что на дворе 2020 год, что все хардкорные IOS разработчики пишут исключительно на SwiftUI и Combine, и писать статьи про UIKit как то не айс. Тем не менее, 2020 год выдался не таким как все предыдущие года. Совсем не таким.


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


Наш чат использует MessageKit в качестве UI компонента. MessageKit swift библиотека, поддерживаемая комьюнити, призванная заменить устаревшую JSQMessagesViewController. Мне довелось работать с JSQMessagesViewController лет эдак 5 назад. Он справлялся со своими задачами, был минимально гибок, написан в лучших традициях UIKit где все наследуется от всего, и он, конечно, к выходу swift-а уже полностью морально устарел. К счастью, мне не пришлось к нему больше возвращаться и я только порадовался появившейся тогда инициативе написать библиотеку для создания UI для чата которая бы заменила JSQMessagesViewController. И забыл об этом до марта 2020-го.


Как же я возненавидел MessageKit в марте 2020-го. Она мне показалась построчной перепиской JSQMessagesViewController с Objective-C на Swift и приветом из IOS разработки 2009 года. Я не буду вдаваться в подробности и ругать чужую работу, в которой я не принимал участие.


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


UIScrollView по умолчанию ведет себя так, что якорь скроллинга закреплен в в верхнем левом углу, и новый контент добавляется в низ без сдвига остального контента. Это поведение подходит для 99% случаев использования, но не для чата, где ожидается обратное поведение. При добавлении нового контента в низ нам нужно сместить коэффициент сдвига (contentOffset) на величину добавленного контента.


Этот факт натолкнул меня на простую мысль. Все это делается какими-то странными хуками и паразитированием на UICollectionViewFlowLayout. Почему бы просто не написать лайаут который делал бы все это из коробки?


Второй момент, который подтолкнул меня к тому что хорошо бы разобраться с UICollectionViewLayout было то что несмотря на паразитирование на UICollectionViewFlowLayout, MessageKit не поддерживала автоматические размеры ячеек используя Auto-Layout и нужно было все размеры считать в коде самому. Не смотря на то, что данная фича доступна в UICollectionViewFlowLayout из коробки.


Ну значит решено. Я напишу свой кастомный UICollectionViewLayout где постараюсь добавить из коробки все что нужно для чата. Сказано сделано. Я создал проект и создал класс который унаследовал от UICollectionViewLayout.


Все казалось довольно простым и я полез в документацию.


Анимированные вставка, удаление, обновление и изменение порядка


И вот тут то меня ждал первый сюрприз. Официальная документация на UICollectionViewLayout отсутствовала. Точнее она как бы номинально присутствует. Но слабо соответсвует действительности или опускает некоторые очень важные моменты.


Так, например, первым делом, я решил разобраться даже не с размером ячеек, а с анимацией вставки, удаления, обновления и изменения порядка ячеек. Как видим тут 4 типа обновления ячейки.


За это дело в UICollectionViewLayout отвечают 2 метода:
initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?
finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?


А вот и официальная документация initialLayoutAttributesForAppearingItem и finalLayoutAttributesForDisappearingItem


Давайте прочтем документацию к initialLayoutAttributesForAppearingItem вместе:


When your app inserts new items into the collection view, the collection view calls this method for each item you insert. Because new items are not yet visible in the collection view, the attributes you return represent the items starting state. For example, you might return attributes that position the item offscreen or set its initial alpha to 0. The collection view uses the attributes you return as the starting point for any animations. (The end point of the animation is the items new location and attributes.) If you return nil, the layout uses the items final attributes for both the start point and end point of the animation.
The default implementation of this method returns nil. Subclasses are expected to override this method, as needed, and provide any initial attributes.

Кроме сообщения нам как делать вставку и, еще, немного не очень ясной дополнительной информации, ни о каком апдейте ячейки или сдвиге ячейки речи не идет. С finalLayoutAttributesForDisappearingItem тоже самое. Ладно, можно попробовать по разному толковать


If you return nil, the layout uses the items final attributes for both the start point and end point of the animation.

и попробовать реализовать все остальное используя вот это утверждение.


Не тут то было. Оно работает так как описано только для самых примитивных случаев. Еще и самое забавное, что никак не описан момент а что же происходит c itemIndexPath? Вот вставил я ячейку с индексом 0. Та которая была до этого 0 вероятно станет 1? А в какой момент? А если я вставляю ячейку 0 и сдвину ячейку которая была до этого номером 0 за ячейку номер 2?


Я не считаю себя шибко умным, но, мне всегда казалось, что с базовой арифметикой и логикой у меня порядок. Но с какими бы теориями я не подходил к данному вопросу: с базовой логикой, с нахрапом, ответить на эти вопросы я не мог. Самое забавное, что еще и поведение на IOS 12 и IOS 13 отличалось. Один подход мог работать в одной и не работать в другой и наоборот. Ну что ж.


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


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


О, Наивный. В тот момент я еще не знал/не думал что UICollectionViewFlowLayout использует private API мне не доступное Нетленная классика от Apple и UIKit. Вообще, это все тянет на отдельную статью. Скажу только, что в какой то момент, когда я уже был готов сдаться, у меня все типы этой анимации стали получаться. При том, что мой лог вызовов/значений отличался от того что выдавал UICollectionViewFlowLayout. Ну работает и работает.


Определение размера ячейки при помощи AutoLayout



Настало время идти дальше и разобраться с автоматическим определением размеров используя AutoLayout. Тут я стал задаваться вопросом. А не изобретаю ли я велосипед? Почему же все так сложно? Измерить ячейку то еще можно, но как это все впихнуть еще и в анимацию? Может уже есть кто-то кто разобрался со всеми этими проблемами и готовое решение лежит на гит-хабе?


Первая странность была в том что кастомных UICollectionViewLayout не очень то и много. Да они существуют. Но они могут только разложить ячейки в зависимости от задачи которые они решают, а вот анимацию или не поддерживают толком или анимированная вставка будет из коробки. Либо они наследуются от UICollectionViewFlowLayout и, как то там подменяя ему атрибуты в prepare методе, пытаются как то с этим взлететь. Причем, вся эта анимация и поддержка AutoLayout ячеек отсутсвует или оставляет желать лучшего.


И тут, я натолкнулся на луч света в темном царстве: airbnb/MagazineLayout. Вот прям реально и без сарказма. Это был единственный реально кастомный лайаут который делал почти все что я хотел. И я с удивлением обнаружил, что моя реализация initialLayoutAttributesForAppearingItem /finalLayoutAttributesForDisappearingItem ну прям очень похожа на то, что написали ребята из AirBnB.


Но дальше по вопросу задачи. Я подсмотрел у них то, что заняло бы у меня "сложно предположить" сколько времени как при обновлении ячейки заменить размеры ячейки с анимацией! Я пробовал разные способы и они работали, но всегда находился случай когда они работали криво.


Для справки: некое подобие состояния ячейки описывается в UICollectionViewLayoutAttributes, и, если во время анимации вы сохраните именно тот объект который вы вернули из initialLayoutAttributesForAppearingItem, а потом в методе invalidationContext(forPreferredLayoutAttributes:, withOriginalAttributes originalAttributes:) вы возьмете этот объект и поменяете у него значение frame то внутри UICollectionView сработает какое то KVO (Key-Value-Observing) и она выдаст вам анимацию которую вы ожидаете. Забудьте о preferredAttributes, забудьте originalAttributes. Стабильно работает только вот так. РукаЛицо.


О прокрутке


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


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


Я подумал, что решить это наверняка можно прямо из UICollectionViewLayout. Казалось бы, простая задача. У UICollectionViewLayout есть метод targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint Переопределяй его и будет тебе счастье, UICollectionView перед обновлением скажет тебе: я, мол, вот, собираюсь сделать свой contentOffset вот таким то, а если ты хочешь его поправить верни мне свой.


Вот только проблема в том, что этот вызов происходит еще до того как ты можешь получить какие то реальные размеры ячеек, и, если какие то расчеты еще происходят во время анимированного апдейта, использовать его не получится. Да, можно частично в процессе анимированного апдейта возложить расчеты на имеющийся у UICollectionViewLayoutInvalidationContext contentOffsetAdjustment, но вы не всегда можете им воспользоваться до завершения транзакции анимации. Поэтому пришлось рассчитывать 2 разных величины, proposedContentOffset то, что можно понять до начала анимации, и вторую которой я воспользуюсь в конце транзакции в методе finalizeCollectionViewUpdates. Вроде добился того что надо.


Все это работает хорошо пока вы не удаляете ячейки таким образом, что у вас сверху не начинают появляться ячейки, которые еще не были рассчитаны с помощью AutoLayout и которые вовремя анимации могут изменить свой размер. А вот UICollectionView при уменьшении размера контента (contentSize) начинает игнорировать contentOffsetAdjustment и, никаким образом, я не смог заставить ее работать так как хотелось мне: ни используя contentSizeAdjustment в различных комбинациях, ни даже меняя contentOffset напрямую. Решением оказалось игнорировать необработанные ячейки при анимации и рассчитывать все остальное потом.


Но главное что желаемого результата удалось достичь.


Немного о багах и private API



У меня, при прокрутке, изображение слегка подергивалось и я никак не мог понять почему. Но решил не сосредотачиваться на этом пока более менее не прояснится картинка. А, потом, начал гуглить и нашел вот такой rdar://40926834: Self Sizing + Prefetching Bugs От тех же замечательных ребят из AirBnB. Выключив префетчинг который включен по умолчанию, я моментально избавился от подобного поведения.


А вот еще один от них же rdar://46865293: Cell's autoresizing mask breaks self-sizing. Ну вот не вызывается layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? столько раз сколько обещано, хоть ты тресни.


Мой любимый трюк, так же подсмотренный в MagazineLayout это наглое использование UICollectionViewFlowLayout приватного метода _prepareForCollectionViewUpdates:withDataSourceTranslator: для того что бы все красиво рассчитать до вызова открытого метода prepareForCollectionViewUpdates. Ай да Apple, ай да UIKit. Это удалось обойти введением специального флага: если транзакция начинается скажи UICollectionView что в ней ничего нет и начинай рассчитывать исходя из того что получишь в prepareForCollectionViewUpdates. Это позволило избавиться от некоторых артефактов.


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


Может, конечно, это и не баги вовсе. Возможно, я что то не понял. И, вообще, зря я на ребят из UIKit наезжаю. Я оставлю этот вопрос открытым.


Немного о хрупкости


Вообще, все эти UICollectionView + UICollectionViewLayout вещи довольно хрупкие. Меняете размер коллекции и, в этот момент, что то вставляете получите артефакты. Меняете анимированно contentInset, например, для клавиатуры, и что то меняете в коллекции получите артефакты. Скроллите коллекцию и меняете ее получите артефакты. Просто что то делаете не так получите артефакты. Почему я не исправлял некоторые вещи? Если я подставлял вместо своего лайаута стандартный UICollectionViewFlowLayout и получал точно такое же поведение я записывал это в недостатки самого устройства связки UICollectionView + UICollectionViewLayout и, это означало, что они должны быть исправлены "из вне". Поэтому, вы увидите в приложении-примере, которое идет вместе с библиотекой, набор флагов из серии: выдвигаешь клавиатуру не обновляй/Выдвинул обнови и т.д.


Пора в продакшен?



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


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


Больше мы не используем MessageKit. И пока что всем довольны. Производительность и удобство внесения изменений оказались достойными периодических похвал коллег. А большинство открытых багов чата в Jira закрылись сами собой.


Что в остатке?


А в остатке оказалась открытая библиотека ChatLayout.


Не смотря на то, что я, пока, считаю ее preview, она вполне пригодна для использования в в реальном. И, мне кажется, что если кто-то еще начнет ее использовать, то, благодаря возможностям комьюнити, получится устранить ее возможные изъяны и добавить функции, которые мне не были нужны. Цель этой статьи представить ее сообществу и привлечь разработчиков.


Я оставил пока код слегка не оптимизированым, для того что бы было удобнее вносить изменения. Без всякой там прямой записи в память при изменении и т.д. Производительность в релизной сборке на актуальных устройствах достойная и без этого. Кроме того: надо вычистить приложение-пример. Да и тестов добавить. Я сам помимо основной работы поддерживаю библиотеку RouteComposer. и, боюсь, что 2 open-source проекта я просто не потяну. Я, вот, едва нашел время и вдохновение написать эту статью.


Перечислю то, что я на данный момент считаю достоинствами данного подхода.


О библиотеке


ChatLayout альтернативный подход к созданию UI для чата. Фактически, библиотека состоит кастомного UICollectionViewLayout, который предоставляет некоторые дополнительные методы, которые могут потребоваться для решения этой задачи. Плюсом, в Extras лежат небольшие UIView, которые, при желании, можно использовать для каких-то тривиальных задач.


Достоинства


  • Полностью поддерживает динамический лайаут для ячеек (UICollectionViewCell) и вспомогательных UIView (UICollectionReusableView).
  • Поддерживает анимированную вставку/удаление/обновление/перемещение ячеек.
  • Удерживает contentOffset у основания видимой области UICollectionView во время апдейтов.
  • Предоставляет методы для точной прокрутки к нужной ячейке.
  • Поставляется с простыми UIView-контейнерами, которые могут облегчить создание кастомных ячеек сообщений.

Недостатки (но, по-моему, все равно достоинства)


ChatLayout это кастомный UICollectionViewLayout, поэтому:


  • Вам не нужно наследоваться от какого либо UIViewController или UICollectionView. Вам нужно написать их самим. Так, как вам удобно
  • Вам не нужно использовать какие то особые UICollectionViewCell которые идут с библиотекой. Создавайте их так, как вам удобно.
  • Вам не нужно в обязательном порядке рассчитывать размеры ячеек. ChatLayout сделает это за вас. При том, только для видимых в данный момент ячеек, не важно сколько их в всего в коллекции. Но, если вы укажите хотя бы приблизительный размер производительность только выиграет.
  • Вам не предоставляется никакая базовая модель данных. Создавайте ее как вам удобно. Напишите свой UICollectionViewDataSource который будет отображать ее на экране. Приложение-пример использует для расчета изменений модели DifferenceKit, но вы можете использовать что вам угодно.
  • ChatLayout не работает с клавиатурой. Вы можете написать свое решение или можете использовать любую подходящую для этого библиотеку. Все что требуется от вас в конечно итоге для поддержки клавиатуры это изменить contentInsets вашего UICollectionView.
  • ChatLayout не предоставляет вам никакого поля для ввода текста. Приложение-пример использует стороннюю библиотеку InputBarAccessoryView (ту же, что использует MessageKit). Вы можете использовать ее или любое другое решение.

Собственно все. Спасибо за внимание. Буду рад Вашим комментариям.


Ах, да. Гифки. (Осторожно эпилептикам)






Подробнее..

Перевод 15 лучших приложений 2020 года по версии Apple и чему мы можем у них научиться

22.12.2020 14:23:50 | Автор: admin
Обзор лучших приложений, которые помогли нам остаться в здравом уме в этом году.


События 2020года сделали его совершенно уникальным. Но Apple всегда объявляла лучшие приложения года, независимо от того, насколько хорошим или плохим он был. Список лучших из лучших раньше был сосредоточен на iPhone, но теперь в него входят и приложения для Apple TV, Apple Watch, iPad и Mac, и среди претендентов что угодно, от обычных программ до стриминговых развлекательных сервисов и игр.

1декабря Apple объявила список 15лучших приложений года. Некоторые пункты списка удивления не взывают: например, приложение года для iPad Zoom. Среди критериев отбора были не только удобство использования и дизайн, но и технические инновации, культурное влияние и, конечно же, быстрое приспособление к ситуации, которое помогло нам преодолеть все те проблемы, что свалились в 2020году.

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



Победители категории Лучшее в App Store 2020:


Лучшие приложения:


  • iPad: Zoom (Zoom, США)
  • iPhone: Wakeout (Андрес Канелла, США)
  • Mac: Fantastical (Flexibits, США)
  • Apple TV: Disney+ (Disney, США)
  • Apple Watch: Endel (Endel, Германия)


Лучшие тренды в приложениях:


  • Тренд года: Shine поощрение пользователей к заботе о себе (Shine, США)
  • Тренд года: Explain Everything Whiteboard оживление удаленных занятий (Explain Everything, Польша)
  • Тренд года: Caribu поддержка контакта семей с близкими (Caribu, США)
  • Тренд года: ShareTheMeal борьба с голодом (ООН, Германия)


Лучшие игры:


  • iPhone: Genshin Impact (miHoYo, Китай)
  • iPad: Legends of Runeterra (Riot Games, США)
  • MacOS: Disco Elysium (ZA/UM, Великобритания и Эстония)
  • Apple TV: Dandara Trials of Fear (Raw Fury, Швеция)
  • Игра года для Apple Arcade: Sneaky Sasquatch (RAC7, Канада)
  • Тренд года: Pokmon Go переосмысление подхода к игре (Niantic, США)


А теперь давайте взглянем на эти приложения поближе и посмотрим, чем они выделились.


Zoom лучшее приложение 2020 г. для iPad


Отличительная черта: простой путь взаимодействия пользователя в три касания.

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

  1. Присоединиться к встрече.
  2. Ввести пароль или номер комнаты (при необходимости).
  3. Ввести свое имя.




Caribu для iPhone и iPad тренд года


Отличительная черта: повышение удобства онлайн-общения для детей и родителей или родственников, когда семьи особенно в этом нуждаются!

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


Неудивительно, что приложение Caribu попало в список лучших 2020 года: раньше мне приходилось использовать Facetime и держать в руках огромную книгу чтобы рассказать сказку 4-летнему племяннику. Как UX-специалист я понимаю, что это не самый оптимальный вариант.

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

Это почти как виртуальный присмотр за детьми,

Максем Тухман, генеральный директор Caribu


Shine для iPhone и Apple Watch тренд года


Отличительная черта: отличное использование цвета и быстрое приспособление к событиям 2020 года пандемии и движению Black Lives Matters.

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

Ирина Коршак, иллюстратор

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

По словам Наоми Хирабаяши, соучредителя Lidey и генерального директора Shine, задача приложения не только поддержка психического здоровья, но и содействие пониманию проблем, связанных с BLM, нетемнокожими американцами.


ShareTheMeal для iPhone и iPad тренд года


Отличительная черта: содействие важному благородному делу во время кризиса.

Приложение Всемирной продовольственной программы ООН ShareTheMeal дает любому человеку возможность участвовать в благотворительности: через него передано уже более 87 миллионов обедов для нуждающихся,
Apple


Для Всемирной продовольственной программы ООН лауреата Нобелевской премии мира этого года пандемия COVID-19 стала еще одним кризисом в дополнение к уже существующим, которые вызывают проблемы с продовольствием во всем мире. ShareTheMeal стремится максимально упростить благотворительность и сделать ее беспрепятственной

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

Fast Company


Wakeout! лучшее приложение 2020 г. для iPhone


Отличительная черта: делает упражнения дома доступными для всех особенно для тех, кому не нравится сама идея упражнений!

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


Fantastical лучшее приложение 2020 г. для macOS


Отличительная черта: единый интерфейс управления расписанием и его визуализации, чего не хватает в Календаре, Напоминаниях от Apple и других аналогичных приложениях для macOS.

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

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


Источник: Fantastical, разработчик Flexibits


Disney+ лучшее приложение 2020 г. для Apple TV


Отличительная черта: актуальность и доступная цена: 7$ в месяц.

Приложение Disney+ вышло в ноябре 2019 года, его интерфейс всё еще совершенствуется, но для этого сервиса сейчас самое время. Кроме того, цена по сравнению с другими платформами более доступная.


Сравнение различных стриминговых платформ и стоимости ежемесячной подписки.


Explain Everything Whiteboard для iPhone и iPad тренд года


Отличительная черта: простота использования для любого возраста, особенно для учащихся (пользователи помоложе) и преподавателей (пользователи постарше).

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

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


Endel лучшее приложение 2020 г. для Apple Watch


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

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

  • текущая деятельность,
  • время суток,
  • пульс (измеряется через Apple Watch),
  • погода
  • и многое другое

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


Лучшие игры года



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


Pokmon GO тренд года


Отличительная черта: превращение игрового процесса на открытом воздухе в домашние упражнения + геймификация упражнений и социального взаимодействия.

  • Упражнения: позволяет отслеживать в качестве игровых достижений сделанные в помещении шаги например, бег на беговой дорожке.
  • Социальное взаимодействие: виртуальные социальные функции в игре были улучшены, с тем чтобы игроки могли поддерживать контакт, даже когда они не могут встретиться в реальной жизни.

Когда в начале этого года разразилась пандемия COVID-19, Niantic быстро отреагировали и изменили Pokmon Go так, чтобы люди по-прежнему могли играть, но не выходя на улицу. Аудитория хорошо приняла нововведения, и в конце марта доходы от игры выросли это была лучшая неделя в 2020 году,

Game Industry


Legends of Runeterra для iPad


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

Приятно, когда есть возможность развлечь людей, которым остается лишь ждать, пока всё это закончится,
Джефф Джу, исполнительный продюсер Legends of Runeterra


Когда разразилась пандемия, Riot Games уже работали над Legends of Runeterra И оказалось, что застрявшие дома люди активно искали новые игры, похожие на Runeterra: это виртуальная карточная игра, поэтому у нее не было проблем из-за коронавируса, как в случае оффлайн игр например, Magic: The Gathering в ее традиционной бумажной форме,

Fast Company


Sneaky Sasquatch игра года для Apple Arcade


Отличительная черта: легкий и приятный игровой процесс, чувство свободы и бегства от реальности, когда игроки в этом особенно нуждаются.

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


Сооснователь RAC7 Games Джесси Рингрос рассказал изданию Complex (Канадское подразделение):

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

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


Dandara: Trials of Fear для Apple TV


Отличительная черта: сходство сюжета с текущей ситуацией в мире.

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

Switch Player


Disco Elysium для MacOS


Отличительная черта: новаторский подход в ролевых играх: увлекательное приключение, построенное на чтении текста!

Да, всё верно: в основе игры чтение увлекательных текстов!

Disco Elysium это не привычная ролевая игра, а скорее даже наоборот: здесь нет сражений, у героя нет невероятных суперсил, и объем текстов в Disco Elysium больше, чем полное собрание сочинений Шекспира. Главное здесь это внимание к деталям, размышление о своих действиях и нестандартное мышление. По сути, это детективная ролевая игра,

обзор от Mef Tech


Если вам интересно копнуть глубже, чем просто лучшие приложения 2020 года, вот список самых загружаемых приложений:

Топ бесплатных приложений для iPhone


  1. ZOOM Cloud Meetings
  2. TikTok
  3. Disney+
  4. YouTube
  5. Instagram
  6. Facebook
  7. Snapchat
  8. Messenger
  9. Gmail
  10. Cash App


Топ платных приложений для iPhone


  1. TouchRetouch
  2. Procreate Pocket
  3. Dark Sky Weather
  4. Facetune
  5. HotSchedules
  6. AutoSleep Track Sleep
  7. The Wonder Weeks
  8. SkyView
  9. Shadowrocket
  10. Sky Guide


Топ бесплатных игр для iPhone


  1. Among Us!
  2. Call of Duty: Mobile
  3. Roblox
  4. Subway Surfers
  5. Ink Inc. Tattoo Drawing
  6. Magic Tiles 3: Piano Game
  7. Brain Test: Tricky Puzzles
  8. Brain Out
  9. Coin Master
  10. Cube Surfer!


Топ платных игр для iPhone


  1. Minecraft
  2. Plague Inc
  3. Heads Up!
  4. Monopoly
  5. Bloons TD6
  6. Geometry Dash
  7. NBA 2K20
  8. Grand Theft Auto: San Andreas
  9. The Game of Life
  10. True Skate


Топ бесплатных приложений для iPad


  1. ZOOM Cloud Meetings.
  2. Disney+
  3. YouTube
  4. Netflix
  5. Google Chrome
  6. TikTok
  7. Amazon Prime Video
  8. Gmail
  9. Hulu
  10. Google Класс


Топ платных приложений для iPad


  1. Procreate
  2. GoodNotes 5
  3. Notability
  4. Duet Display
  5. Teach Your Monster
  6. LumaFusion
  7. Affinity Designer
  8. Toca Hair Salon 3
  9. Toca Life: Hospital
  10. Toca Kitchen 2


Топ бесплатных игр для iPad


  1. Among Us!
  2. Roblox
  3. Magic Tiles 3: Piano Game
  4. Ink Inc. Tattoo Drawing
  5. Call of Duty: Mobile
  6. Subway Surfers
  7. Dancing Road: Color Ball Run!
  8. Tiles Hop EDM Rush
  9. Mario Kart Tour
  10. Save The Girl!


Топ платных игр для iPad


  1. Minecraft
  2. Monopoly
  3. Bloons TD 6
  4. Plague Inc.
  5. Geometry Dash
  6. The Game of Life
  7. Five Nights at Freddys
  8. Human: Fall Flat
  9. Stardew Valley
  10. Terraria


Топ игр Arcade


  1. Sneaky Sasquatch
  2. Hot Lava
  3. Skate City
  4. Sonic Racing
  5. PAC-MAN Party Royale
  6. SpongeBob: Patty Pursuit
  7. Oceanhorn 2
  8. Crossy Road Castle
  9. WHAT THE GOLF?
  10. LEGO Brawls


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


О переводчике


Перевод статьи выполнен в Alconost.

Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.

Мы также делаем рекламные и обучающие видеоролики для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.
Подробнее..

SwiftUI 2020. Что изменилось?

24.06.2020 16:09:31 | Автор: admin
Приветствую вас, жители Хабра и все интересующиеся разработкой под IOS. На связи Анна Жаркова, Senior iOS/Android разработчик компании Usetech
Сегодня мы поговорим о тех изменениях и новшествах, которые нам представляет Apple на WWDC 2020. А именно про доработанную и даже переработанную версию фреймворка SwiftUI.

Эта технология для декларативной разработки приложений была представлен в прошлом году на WWDC 2019. Если вы с ней еще не работали, то рекомендую заглянуть сюда

Итак. Apple и их инженеры внимательно весь этот год следили за обзорами, статьями, решениями и комментариями от разработчиков-энтузиастов. В конце видео What's new in SwiftUI они выражают благодарность всем неравнодушным за помощь.

Теперь SwiftUI позиционируется как полноценный инструмент для разработки под разные платформы (от watchOS до macOS):



Причем разработку под разные платформы можно вести в едином проекте. В Xcode 12 появляется шаблон для упрощения создания такого решения:



Система создаст проект с Shared-блоком и платформенными таргетами, где вы можете добавлять что-то специфичное для конкретной платформы:



Может показаться, что где-то это вы уже видели в Kotlin Multiplatform. Что ж, видимо, в этом году тренд на явное заимствование у конкурентов.

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

Расширили поддержку контролов. Теперь практически все контролы UIKit портированы на SwiftUI



Немаловажно, что в SwiftUI появляется аналог UICollectionView LazyVGrid/LazyHGrid, использующий GridItem:



Кстати, в процессе работы над новым контролом Apple оптимизировали работу на UITableView/UICollectionView в UIKit.

Появились средства поддержки адаптивности в настройке параметров UI (размеры контролов, шрифт, отступы и т.п). Например, атрибут @ScaledMetric у настраиваемой величины:



Также расширили поддержку фреймворков на SwiftUI.



Теперь можно использовать их вместе с ViewState:

Map(coordinateRegion: <#T##Binding<MKCoordinateRegion>#>)

Добавили поддержку Document Based Apps, Widgets, App Clips. Последние 2 являются новыми фичами iOS SDK 14. Виджеты практически аналог того, что было в Android.

Туториалы и видео работы с ними будут представлены вот-вот на WWDC 2020.

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

1. Оптимизирована работа с памятью. Memory Performance.

Это глобальное изменение для Swift 5.3 в целом. Переход внутри на структуры позволяет использовать передачу по значению вместо ссылок, тем самым сокращая размер кучи (heap), размер бинарников и времени на компиляцию.



Кстати, некоторые в самом SwiftUI стали использовать Lazy подход. Например, те же списки и LazyVGrid/LazyHGrid(аналог UICollectionView). По идее, это должно убрать проблему с инициализацией View сразу. Однако, пока еще ничего не было сказано про NavigationLink .

2. Использование DSL внутри блоков ViewBuilder.

Ура, теперь мы можем добавить if/else или switch-case в декларативных блоках в своих целях. Например, сделать фабрику Child View внутри родительского View. Или внутри NavigationView.



Это очень круто.

Также теперь можно проверять условие по @PropertyWrapper внутри блока:

@State private var isVisible = trueif isVisible == true {    Text("Hello") // Only rendered when isVisible is true.}

3. Теперь можно создать приложение 100% на компонентах SwiftUI.

Да-да. Для этого Apple придумали, как обойтись без AppDelegate, SceneDelegate и UIHostingViewController.

С помощью аннотации main и протоколов App, Scene вы сможете этого достичь:

@mainstruct testmultiplatformApp: App {  @SceneBuilder var body: some Scene {        WindowGroup {            MailViewer()        }        Settings {            SettingsView()        }    }}

main сигнализирует, что это новая входная точка вашего приложения. Реализация протокола App обязательна. Обратите внимание на тип свойства body структуры App. В приложении может быть одна или несколько так называемых сцен, каждая из которых реализует протокол Scene. У каждой сцены есть свой root View и свой жизненный цикл. Если вы хотите использовать несколько сцен (как, например, в многооконном приложении), то необходимо поставить у body атрибут SceneBuilder. По сути это механизм инкапсуляции UISceneDelegate и его логики.

WindowGroup используется для создания единого полноразмерного экрана, как и UIWindow/UIWindowScene.

Однако, мы помним, что SwiftUI это надстройка над UIKit. И UIKit остался внутри. Если мы запустим даже вот такое шаблонное приложение, то в иерархии View мы увидим:



И UIHostingViewController на месте, и UIWindowScene. Но внутри. Да, для инициализации это очень упрощает работу. Но продумали ли они решение, чтобы не пришлось возвращать в приложение UISceneDelegate с явным заданием всей навигационной структуры.

Отслеживание lifecycle сцены сокращается до метода:

public func onChange<V>(of value: V, perform action: @escaping (V) -> Void) -> some Scene where V : Equatable

Вот так предлагают использовать данный метод для отслеживания перехода приложения в фоновое состояние:

  ///    /// Use this modifier to trigger a side effect when a value changes, like    /// the value associated with an ``SwiftUI/Environment`` key or a    /// ``SwiftUI/Binding``. For example, you can clear a cache when you notice    /// that a scene moves to the background:    ///    ///     struct MyScene: Scene {    ///         @Environment(\.scenePhase) private var scenePhase    ///         @StateObject private var cache = DataCache()    ///    ///         var body: some Scene {    ///             WindowGroup {    ///                 MyRootView()    ///             }    ///             .onChange(of: scenePhase) { newScenePhase in    ///                 if newScenePhase == .background {    ///                     cache.empty()    ///                 }    ///             }    ///         }    ///     }    ///    /// The system calls the `action` closure on the main thread, so avoid    /// long-running tasks in the closure. If you need to perform such tasks,    /// dispatch to a background queue:    ///    ///     .onChange(of: scenePhase) { newScenePhase in    ///         if newScenePhase == .background {    ///             DispatchQueue.global(qos: .background).async {    ///                 // ...    ///             }    ///         }    ///     }    ///    /// The system passes the new value into the closure. If you need the old    /// value, capture it in the closure.    ///

4. Изменение предлагаемой архитектуры для SwiftUI.

Видео-презентации еще не было, но судя по документации на сайте, это уже не MVVM, а MVI или Redux:



Да-да, обратите внимание на изменение связей.

Что ж, это еще одно подтверждение тренда на заимствование у конкурентов. Пока Google вдохновляются SwiftUI для новой версии JetPack Compose, Apple вдохновляются предложениями Google. Ну а факт заимствования у энтузиастов Apple и не скрывали. Статей, посвященных использованию именно Redux/MVI с SwiftUI, в сети очень много на разных языках еще с прошлого года: ссылка.

Как пример.

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

Не расстраивайтесь, если у вас были планы на работу с несовершенствами SwiftUi в прошлой реализации. По крайней мере, вы знаете базу и сможете перестроиться на новую версию.

В общем, присоединяйтесь к WWDC 2020. И ждите новых статей на Хабре)

Подробнее..

Apple WWDC 2020 что нового в тестировании iOS

03.07.2020 18:07:41 | Автор: admin
Привет, меня зовут Сергей, и я тестирую iOS приложения в Exness. В конце июня 2020 г. закончилась очередная WWDC. Давайте разберемся, что же она принесла нового в мир тестирования iOS приложений.

image

Но вначале краткий исторический экскурс: Apple WWDC (WorldWide Developers Conference), или просто даб-даб, это конфа, которую Apple с конца восьмидесятых проводит в Калифорнии. В этом году конференция впервые прошла в онлайн-формате. И если раньше билеты разыгрывались в лотерее, и тем, кто не получил желанного email, оставалось довольствоваться видео с сайта https://developer.apple.com/videos/, то в этом году по понятным причинам других вариантов не было: видео смотрели все.
Итак, что же там можно было высмотреть по тестированию?
Сразу оговорюсь, что на WWDC 2020 не было какой-то большой общей сессии, посвященной тестированию в экосистеме Apple, как в прошлые годы (Testing in Xcode 2019 и Whats new in testing 2018, 2017). Новинки тестирования в 2020 размазали на шесть мини-сессий. Поехали!

XCTSkip для ваших тестов


В Xcode 11.4 добавили новый API для управления запуском тестов в зависимости от условий XCTSkip.
Часто в тестах, особенно интеграционных, есть условия или требования, которые нелегко замокать. Например, приложение имеет какой-то специфический функционал для айпада, который не работает на айфоне. Или какие-то фичи для конкретной версии операционной системы.
И раньше, когда тесты доходили до подобных кейсов (проверка айпад-only функционала на iPhone), стоял выбор:

  • Закончить выполнение тестового набора;
  • Пометить тест как пройденный и пойти дальше;
  • Зафейлить тест.

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

image

Подробнее тут и тут.

Обработка прерываний и алертов в UI тестах


Обработка прерываний и алертов была в XCTest и раньше, однако в сессии механизм его работы был раскрыт более подробно. Мне показалась интересной новая функциональность, добавленная в Xcode 11.4, iOS/tvOS 13.4 и macOS 10.15.4, а именно, сброс пермишенов (aka protected resources).
Суть в следующем: если раньше вы, например, в тесте #1 дали приложению доступ к камере или контактам, то потом, в тесте #2, #n этот доступ так просто не отобрать. Чтобы сделать это, придется переустанавливать приложение.
Теперь с помощью API для сброса авторизации для protected resources можно отобрать ранее выданный доступ:

Class XCUIApplication {open func resetAuthorizationStatus(for: XCUIProtectedResource)}

Сброс настроек для пермишенов заставляет приложение вести себя так, как будто оно ни разу до этого не запрашивало у пользователя доступ к protected resources.
Это позволяет пройти все пути с выдачей и забором пермишенов для контактов, календаря, фото, микрофона, камеры и геолокации. На iOS еще дополнительно можно сбросить доступ к Bluetooth и Keyboard network access, а начиная с Xcode 12 / iOS 14, к данным Health. На Mac OS можно сбросить доступ к директориям Desktop и Downloads.
Ниже пример, как сбросить доступ приложения к фото:

// Examplefunc testAddingPhotosFirstTime() throws {let app = XCUIApplication()app.resetAuthorizationStatus(for: .photos)app.launch()// Test code...}

Важно помнить, что часто (но не всегда) при сбросе пермишенов приложение убивается.

Подробнее тут, тут и тут.

Устраняем лаги анимации с помощью XCTest


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

image

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

Триаж и диагностика упавших тестов


Часто починка упавших тестов это боль, которая занимает много времени и ресурсов.
В Xcode 12 появится новое API, которое должно облегчить починку упавших тестов. API должно помочь быстрее ответить на вопросы: что, как, почему и самое главное где упало?
Если раньше после того, как тест упал, приходилось искать место падения в
Issue navigator или report navigator, то с Xcode 12 процесс поиска упростился: теперь место падения подсвечивается в самом тесте.
Ошибка с выделением серым цветом появляется, если строка обращается к какой-то другой строке в дальнейшем:

image

И красным цветом, если ошибка произошла непосредственно в этой строке:

image

Удобная новая фича открытие редактора кода не в отдельном окне, а прямо в report navigator:

image

Кроме того, в Xcode 12 добавился новый объект XCTIssue, который, помимо того, что инкапсулировал в себя данные об ошибках, которые ранее собирал в себе XCTest (сообщение, путь, номер строки и флаг Expected), теперь добавляет:

  • Distinct types;
  • Detailed description;
  • Associated error;
  • Attachments.

Подробнее тут и тут.

Пишите тесты для того, чтобы они падали


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

Используйте человекочитаемые сообщения в ассерt`ах:

image

Убедитесь, что используйте подходящий для вашей ситуации тип ассерt`а:

image

Unwrap'те optional'ы, чтобы ваши тесты падали, выбрасывая ошибку, а не крашились. Swift предоставляет несколько способов для этого, но в тестах, как правило, используют XCTUnwrap, который являет собой упрощение конструкции guard let.

image

Используйте waitForExistence() вместо sleep() для асинхронных ожиданий.

Используйте XCTContext.runActivity() для повышения читабельности лога выполнения теста:

image

И если хотите добавить дополнительное логирование, можно добавить вложение, приложить скриншот или вывод дебагера, как здесь. Это функция особенно полезна, если ваши тесты запускаются в CI/CD.

image

Подробнее тут.

Получайте результат тест рана быстрее


Обидно, когда утром в понедельник вы обнаруживаете, что запущенная в пятницу вечером длинная джоба так и не отработала до конца, зависнув на середине или вообще в самом начале. И вам предстоит начать рабочую неделю с разбора полетов: почему это произошло? Как избежать подобной ситуации в будущем? Как я мог спустить девять тысяч на коктейли за один вечер?
В Xcode 12 появились инструменты для защиты от зависаний. Это новая опция тест плана Execution Time Allowance.
Когда опция включена, Xcode устанавливает временной лимит исполнения каждого теста.
Если лимит превышен, Xcode делает следующее:

  1. Собирает отчет (spindump);
  2. Убивает зависший тест;
  3. Перезапускает тест раннер, чтобы оставшаяся часть сьюта смогла выполниться.

В отчете (spindump) отображается, какой из тредов, какой функции потратил наибольшее количество времени. Это позволит вам увидеть глазами бутылочное горлышко ваших тестов еще до того, как остынет ваш утренний кофе/ чай.
По дефолту на каждый тест выделяется по 10 минут, и если тест завершился быстрее, то для следующего теста таймер обнуляется. Если вам нужно больше/ меньше времени на каждый тест вашего тест сьюта, то можно изменить дефолтную величину в настройках тест плана.

image

Еще это можно сделать опцией команды xcodebuild:

xcodebuild option-default-test-execution-time-allowance <seconds>

Аналогично можно задать максимальное время выполнения теста:

image

xcodebuild option-maximun-test-execution-time-allowance <seconds>

Даже если вам нужно задать время выполнения для какого-то конкретного теста или тестового класса, то это тоже возможно с помощью executionTimeAllowance API:

Class XCTestCase: XCTest {var executionTimeAllowance: TimeInterval // с округлением до минуты}

Точная настройка выполнения того или иного теста позволит вам сэкономить время, но и это еще не все, что можно сделать для ускорения прохождения длинного тест сьюта.
Xcode 12 позволяет запускать тесты на нескольких девайсах одновременно. Эту фичу назвали Parallel Distributed Testing. Польза от запуска тестов на нескольких девайсах очевидна приличная экономия времени.

image
image

Но, к сожалению, есть и подводные камни: порядок запуска тестов в параллели не детерминирован, нет никакой гарантии, что на девайсе #1 после теста номер 5, будет выполнен тест номер 6. Этот факт обязательно нужно учитывать при планировании запуска тестов с помощью Parallel Distributed Testing.
Вообще идея запусков тестов в параллели не нова. Такая возможность была и до Xcode 12, но именно в Xcode 12 появилась возможность запускать тесты на реальных девайсах (пока только с помощью xcodebuild).

Команда для запуска параллельных распределенных тестов выглядит следующим образом:

xcodebuild test    -project MyProject.xcodeproj    -scheme MyProject    -parallel-testing-enabled YES    -parallelize-test-among-desinations    -destination 'platform=iOS,name=iPhone 11'    -destination 'platform=iOS,name=iPad pro' 

Подробнее тут.

На этом обзор новых тесто-фич с WWDC 2020 закончен. Спасибо, что дочитали до конца. Надеюсь, эта статья будет вам полезной. Happy testing!
Подробнее..

Проектируем работу с iOS подписками клиентское или серверное хранение продуктов

30.07.2020 14:08:36 | Автор: admin

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



При встраивании подписок, иногда упускаются важные вопросы:


  • как и где хранить id для предлагаемых продуктов?
  • как часто планируется итерация по новым продуктам?
  • на каком этапе работы приложения стоит запрашивать продукт? На запуске или перед показом экрана
  • есть ли время на встраивание сложной системы для работы с продуктами или необходим быстрый запуск?

В отличие от огромного количества UX-подходов для работы с подписками, техническое решение имеет всего два пути развития:


  1. Мы переносим всю работу на сервер
  2. Вся логика работы с продуктами остается на клиенте

У каждого подхода есть плюсы и минусы, далее мы рассмотрим следующие вопросы:


  1. Хранение id продуктов: клиент или сервер?
  2. В какой момент работы приложения стоит запросить продукты?
  3. Кэшировать или не кешировать продукты?
  4. Аналитика покупок: и снова клиент или сервер?

Итак, погнали!


Хранение id продукта


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


И появляется головная боль, как и где хранить этот самый id: захардкодить на устройстве или получать с сервера.


При хранении на клиенте вам не нужно задумываться о контрактах работы с бекендом или поиском SaaS решения, если в вашем проекте отсутствует надобность в сервере. Вы просто сохраняете массив нужных вам id продуктов внутри приложения, запрашиваете их и показываете. В целом вы даже можете довольно быстро итерироваться по разным подпискам, просто обновляя приложение в store.


Но что если нам хочется чего-то большего? Что если нам хочется AB-тестов, что если мы хотим взять и при помощи парочки кнопок заменить во всем приложении старый продукт на новый? А, ну и, конечно, у нас при этом достаточно свободного времени. Тут на сцену и выходит сервер.


В случае серверного хранения id продуктов на длинной дистанции ограничения различных подходов заключаются только в возможностях нашей фантазии, ну и, конечно, умениями разработчиков в команде. Однако c большими возможностями приходит и большая ответственность. Поскольку в таком случае клиенту и серверу стоит заранее продумать контракт по-которому они будут взаимодействовать. В минимальном виде необходимо учитывать такие вещи как:


  1. Как клиент будет понимать к какому виду продуктов относится данный id
  2. Как различать на экране покупки какие продукты где показывать

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


Возможность быстрой интеграции Гибкость системы к изменениям Скорость интеграции
Клиент * ** ***
Сервер ** *** *

Запрос продуктов и кэширование


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



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



Сконцентрируемся немного на процессе запроса id продуктов с сервера и самих продуктов уже от Apple.



На данной диаграмме изображен процесс получения id продуктов с учетом возможности, что сервер может отвалиться, в таком случае мы всегда имеем список запасных id продуктов для загрузки. Если сервер возвращает ошибку, то чтобы не запрашивать бесконечно id, мы можем выставить лимит на попытки и, при достижении лимита, запросить продукты из локального стора. Но можно пойти "хитрым путем" и, запросив запасные продукты, продолжить стучаться на сервер раз в n-времени, и если id все же будут получены, то в таком случае можно по ним запросить актуальные продукты и подменить их кэше во время работы с приложением.


Однако стоит учитывать, что при таком подходе появляется промежуточный результат, при котором любой A/B тест способен сломаться. Представим ситуацию: пользователь заходит на экран покупки и видит продукты, загруженные из запасного списка. Затем пользователь выходит с экрана и продолжает пользоваться приложением. За это время загружаются актуальные продукты и снова показываются пользователю, и в этот раз пользователь совершает покупку. В такой ситуации перед бизнесом появляется вопрос, на который есть множество ответов, среди которых придется долго искать истину:Что мотивировало купить пользователя совершить покупку в этот раз?


Данный вопрос можно разрешить следующим образом


image

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


Итог


Встраивание покупок само по себе несложное действие, но стоит задуматься о том, как в дальнейшем будет развиваться ваш проект и продукт. Задайте себе вопросы:


  • Я планирую делать большой продукт или маленький пет-проект?
  • Как в дальнейшем я планирую развивать подписки?
  • У меня есть время на разработку сложного решения?

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

Подробнее..

Разделяй и властвуй. Модульное приложение из монолита на Objective-C и Swift

11.08.2020 14:11:07 | Автор: admin


Привет, Хабр! Меня зовут Василий Козлов, я iOS-техлид в Delivery Club, и застал проект в его монолитном виде. Признаюсь, что приложил руку к тому, борьбе с чем посвящена эта статья, но раскаялся и трансформировал своё сознание вместе с проектом.

Я хочу рассказать, как разбивал существующий проект на Objective-C и Swift на отдельные модули frameworkи. Согласно Apple, framework это директория определенной структуры.

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

Неожиданно пресловутые принципы SOLID начали обретать очертания, а главное сама постановка задачи вынуждала организовывать код в соответствии с ними. Вынося какую-то сущность в отдельный модуль, вы автоматически сталкиваетесь со всеми её зависимостями, которые не должны находиться в этом модуле, а также дублироваться в главном проекте приложения. Поэтому назрел вопрос об организации дополнительного модуля с общей функциональностью. Это ли не принцип единой ответственности, когда одна сущность должна иметь одно предназначение?

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

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

Проект содержал много legacy-кода, перекрестных зависимостей от классов на Objective-C и Swift, разных targetов в терминах iOS-разработки, внушительный список CocoaPods. Любой шаг в сторону от этого монолита приводил к тому, что проект переставал собираться в Xcode, обнаруживая порой ошибки в самых неожиданных местах.

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

Первые шаги


Они очевидны, о них написано много статей. Apple постаралась сделать их максимально удобными.

1. Создаем первый модуль: File New Project Cocoa Touch Framework

2. Добавляем модуль в workspace проекта





3. Создаем зависимость основного проекта от модуля, указав последний в разделе Embedded Binaries. Если в проекте несколько targetов, то модуль надо будет включить в раздел Embedded Binaries каждого зависящего от него targetа.

От себя добавлю только один комментарий: не спешите.

Знаете ли вы, что будет размещено в этом модуле, по какому признаку будут разделены модули? В моём варианте это должен был быть UIViewController для чата с таблицей и ячейками. К модулю должен был быть привязан Cocoapods с чатом. Но вышло всё немного по-другому. Реализацию чата мне пришлось отложить, потому что и UIViewController, и его presenter, и даже ячейка основывались на базовых классах и протоколах, о которых новый модуль ничего не знал.

Как выделить модуль? Наиболее логичный подход по фичам (features), то есть по какой-то пользовательской задаче. Например, чат с техподдержкой, экраны регистрации/авторизации, bottom sheet с настройками основного экрана. Кроме этого, скорее всего, понадобится какая-то базовая функциональность, которая представляет из себя не feature, а лишь набор UI-элементов, базовых классов и т.д. Эту функциональность следует вынести в общий модуль, аналогичный знаменитому файлу Utils. Не бойтесь раздробить и этот модуль. Чем меньше кубики, тем проще их вписать в основную постройку. Мне кажется, так можно сформулировать еще один из принципов SOLID.

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

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

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

Чтобы сделать все обособленные сущности доступными извне модуля, придётся принять во внимание особенности Swift и Objective-C.

5. В Swift все классы, перечисления и протоколы должны быть помечены модификатором доступа public, тогда к ним можно будет получить доступ снаружи модуля. Если в отдельный framework перемещается базовый класс, его следует пометить модификатором open, иначе не получится создать от него класс-потомок.

Сразу следует вспомнить (или впервые узнать), какие есть уровни доступа в Swift, и получить profit!



При изменении уровня доступа у перенесённого класса Xcode потребует изменить уровень доступа у всех переопределённых методов на идентичный.



Затем необходимо добавить импорт нового frameworkа в Swift-файл, где используется выделенная функциональность, наряду с каким-нибудь UIKit. После этого ошибок в Xcode должно стать меньше.

import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {//..}

С Objective-C последовательность действий немного сложнее. Кроме того, использование bridging headerа для импорта классов Objective-C в Swift не поддерживается во frameworkах.



Поэтому поле Objective-C Bridging Header должно быть пустым в настройках frameworkа.



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

6. У каждого frameworkа есть собственный заголовочный файл umbrella header, через который будут смотреть во внешний мир все публичные интерфейсы Objective-C.

Если в этом umbrella header указать импорт всех прочих заголовочных файлов, то они будут доступны в Swift.



import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {        var vc: Obj2ViewController?        override func viewDidLoad() {        super.viewDidLoad()        // Do any additional setup after loading the view, typically from a nib.    }

В Objective-C, чтобы получить доступ к классам снаружи модуля, нужно поиграться с его настройками: сделать заголовочные файлы публичными.



7. Когда все файлы поодиночке перенесены в отдельный модуль, нужно не забыть о Cocoapods. Файл Podfile требует реорганизации, если какая-то функциональность окажется в отдельном frameworkе. У меня так и было: pod с графическими индикаторами надлежало вынести в общий framework, а чат новый pod был включён в свой собственный отдельный framework.

Необходимо явно указать, что проект теперь не просто проект, а рабочее пространство с подпроектами:

workspace 'myFrameworkTest'

Общие для frameworkов зависимости следует вынести в отдельные переменные, например, networkPods и uiPods:

def networkPods     pod 'Alamofire'end def uiPods     pod 'GoogleMaps' end

Тогда зависимости основного проекта будут описаны следующим образом:

target 'myFrameworkTest' doproject 'myFrameworkTest'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend 

Зависимости frameworkа с чатом таким образом:

target 'FeatureOne' do    project 'FeatureOne/FeatureOne'    uiPods    pod 'ChatThatMustNotBeNamed'end


Подводные камни


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

Все общие зависимости вынесены в один отдельный framework, чат в другой, код стал немного чище, проект собирается, но при запуске падает.

Первая проблема скрывалась в реализации чата. На просторах сети проблема встречается и в других podах, достаточно загуглить Library not loaded: Reason: image not found. Именно с таким сообщением происходило падение.

Более элегантного решения я не нашёл и был вынужден продублировать подключение podа с чатом в основном приложении:

target 'myFrameworkTest' do    project 'myFrameworkTest'    pod 'ChatThatMustNotBeNamed'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend

Таким образом Cocoapods позволяет приложению видеть динамически подключенную библиотеку при запуске и при компиляции проекта.

Другая проблема заключалась в ресурсах, про которые я благополучно забыл и нигде не встречал упоминания о том, что этот аспект надо держать в уме. Приложение падало при попытке зарегистрировать xib-файл ячейки: Could not load NIB in bundle.

Конструктор init(nibName:bundle:) класса UINib по умолчанию ищет ресурс в модуле главного приложения. Естественно, об этом ничего не знаешь, когда разработка ведется в монолитном проекте.

Решение указывать bundle, в котором определен класс ресурса, либо позволить компилятору сделать это самому, используя конструктор init(for:) класса Bundle. Ну и, конечно, впредь не забывать о том, что ресурсы теперь могут быть общими для всех модулей или специфичными для одного модуля.

Если в модуле используются xibы, то Xcode будет, как обычно, предлагать для кнопок и UIImageView выбирать графические ресурсы из всего проекта, но в run time все расположенные в других модулях ресурсы окажутся не загруженными. Я загружал изображения в коде, используя конструктор init(named:in:compatibleWith:) класса UIImage, где вторым параметром идёт Bundle, в котором расположен файл изображения.

Ячейки в UITableView и UICollectionView теперь также должны регистрироваться подобным образом. Причем надо помнить, что Swift-классы в строковом представлении включают в себя ещё и имя модуля, а метод NSClassFromString() из Objective-C возвращает nil, поэтому рекомендую регистрировать ячейки, указывая не строку, а класс. Для UITableView можно воспользоваться таким вспомогательным методом:

@objc public extension UITableView {    func registerClass(_ classType: AnyClass) {        let bundle = Bundle(for: classType)        let name = String(describing: classType)        register(UINib(nibName: name, bundle: bundle), forCellReuseIdentifier: name)    }}


Выводы


Теперь можно не переживать, если в одном pull request окажутся изменения в структуре проекта, сделанные в разных модулях, потому что у каждого модуля свой xcodeproj-файл. Можно распределять работу так, чтобы не приходилось тратить несколько часов на сведение файла проекта воедино. Полезно иметь модульную архитектуру в больших и распределенных командах. Как следствие, должна увеличиться скорость разработки, но верно и обратное. На свой самый первый модуль я потратил гораздо больше времени, чем если бы создавал чат внутри монолита.

Из очевидных плюсов, на которые также указывает Apple, возможность снова использовать код. Если в приложении имеются различные targetы (app extensions), то это самый доступный подход. Возможно, чат не самый лучший вариант для примера. Следовало начать с вынесения сетевого слоя, но давайте будем честными сами с собой, это очень длинная и опасная дорога, которую лучше разбить на небольшие отрезки. А так как за последние пару лет это было внедрение второго сервиса для организации технической поддержки, хотелось внедрить его не внедряя. Где гарантии, что скоро не появится третий?

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

Swift Best Practices которые не стыдно знать

31.08.2020 18:16:08 | Автор: admin

Предисловие


Всем, по традиции, 404! Я собрал коллекцию и частью Swift Best Practices ( которые не только упростят вам жизнь, но и покажут ваш профессионализм на код ревью) поделюсь с вами в этой статье. Хочу чтоб ваш код был чистым, красивым и вы сами ему радовались!

Стартуем от сюда


0. Начнём с UserDefaults, не буду говорить как много я штук видел с ними. Вот небольшая хитрость, которую я использую, чтобы добиться согласованности ключей UserDefault в Swift (#function заменяется именем свойства в геттерах / сеттерах). Просто не забудьте написать хороший набор тестов, которые защитят вас от ошибок при изменении имен свойств.

extension UserDefaults {    var onboardingCompleted: Bool {        get { return bool(forKey: #function) }        set { set(newValue, forKey: #function) }    }}


1. Все знают оператор === с js но почему-то мало кто им пользуется в Swift. (Напомню: Оператор === позволяет проверить, являются ли два объекта одним и тем же экземпляром.) Очень полезно при проверке того, что массив содержит экземпляр в тесте:

protocol InstanceEquatable: class, Equatable {}extension InstanceEquatable {    static func ==(lhs: Self, rhs: Self) -> Bool {        return lhs === rhs    }}extension Enemy: InstanceEquatable {}func testDestroyingEnemy() {    player.attack(enemy)    XCTAssertTrue(player.destroyedEnemies.contains(enemy))}

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

let workItem = DispatchWorkItem {    // Ваш асинхронный код здесь}// Вызовем его через 1 секундуDispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)// А теперь можем прекратить его работу(в любое удобное для нас время)workItem.cancel()

3. С map можно придумать много крутых штук, но эта одна из самых полезных. Используя map, вы можете преобразовать Optional значение в optional Result тип, просто передав его в enum.

enum Result<Value> {    case value(Value)    case error(Error)}class Promise<Value> {    private var result: Result<Value>?        init(value: Value? = nil) {        result = value.map(Result.value)    }}

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

До

public func generate() throws {    let contentFolder = try folder.subfolder(named: "content")    let articleFolder = try contentFolder.subfolder(named: "posts")    let articleProcessor = ContentProcessor(folder: articleFolder)    let articles = try articleProcessor.process()    ...}

После

public func generate() throws {    let contentFolder = try folder.subfolder(named: "content")    let articles = try processArticles(in: contentFolder)    ...}private func processArticles(in folder: Folder) throws -> [ContentItem] {    let folder = try folder.subfolder(named: "posts")    let processor = ContentProcessor(folder: folder)    return try processor.process()}


Ну красиво, удобно, элегантно же, да? Да. Надеюсь был полезен, всем спасибо за прочтение. Красивого UI и чистого кода!
Подробнее..

Сценарий идеального технического собеседования

07.10.2020 14:10:47 | Автор: admin


Дисклеймер: это сценарий идеального технического собеседования в Delivery Club Tech. Мнение нашей команды может не совпадать с мнением читателей.

Привет, Хабр! Меня зовут Василий Козлов, я iOS-техлид в Delivery Club. Я часто и много провожу собеседования. В этой статье я собрал накопленный опыт и собственные наблюдения, которыми хочу поделиться. Во второй части статьи приведу пример собеса с комментариями со своей стороны. Итак, начнём.

1. Собесы бывают разные: жёлтые, зелёные, красные (лирическое отступление)


Есть мнение, что сложные технические собесы не работают. Сооснователь платформы для рекрутинга Interviewing.io Алин Лернер ранее писала, что компании, которые подбирают сотрудников, опираясь на сложные технические собеседования, тратят ресурсы на множество кандидатов, которые не понимают игровую сущность собеседований. В результате на финишную прямую в таких компаниях выходят кандидаты, которые хороши именно в прохождении интервью.

Добавьте сюда стресс от собеседований, разнообразие и непредсказуемость вопросов на технических собеседованиях в разных компаниях. И вспомните свои неожиданные неудачи на этих встречах. Статистика это лишь подтверждает: только около 25% кандидатов способны раскрыть и продемонстрировать свой потенциал, и даже первоклассные специалисты в 22% случаев заваливают технические собеседования.

Эта особенность задавать на технических интервью сложные, не имеющие никакой связи с реальностью, головоломкие вопросы появилась в 1950-х годах в Соединенных Штатах в период холодной войны. Тренд задала лаборатория полупроводников Шокли из долины, тогда ещё не носившей имя Кремниевой, вынужденная привлекать на службу безумных гениев для противостояния красной угрозе. Невозможность писать код на техническом собеседовании по телефону заставляла интервьюеров искать альтернативные варианты для быстрой оценки аналитических способностей, интеллекта и потенциала собеседника. Так появились задачи про фальшивую монету и два взвешивания.

В 1990-х годах с бумом доткомов последовал рост найма технических специалистов, и Microsoft взяла на вооружение подход прошлых лет. Их примеру некоторое время следовала Google.

Впоследствии Google и Microsoft отказались от популярных головоломок из серии как передвинуть гору Фудзи. Что касается найма, то мы обнаружили, что головоломки это пустая трата времени. Сколько мячей для гольфа вы можете поместить в самолет? Сколько заправочных станций на Манхэттене? Полная трата времени. Они ничего не предсказывают. Они служат, в первую очередь, для того, чтобы интервьюер чувствовал себя умным, признал старший вице-президент по работе с персоналом в Google в интервью New York Times.

Итак, есть мнение, что сложные технические собесы не работают, и я его разделяю, поэтому все технические собеседования в Delivery Club я проводил, не задавая подобных вопросов. Невозможно без соответствующей подготовки к собеседованию взять и пройти таковое. На практические тренировки может уйти 1-2 месяца хождения. Это нормальный процесс, с которым так или иначе согласны все опрошенные мной коллеги.

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

IT-индустрия в России с точки зрения IT-найма никак не стандартизована. Способы оценки знаний принимают, зачастую, очень изощренные формы. Один из самых бестолковых примеров на моей памяти телефонное интервью с HR-специалистом, который записывал ответы кандидата на технические вопросы, чтобы далее передать их техническому специалисту. В этом случае какой-либо диалог полностью исключается, и невозможно поделиться ни мнением, ни оспорить вариант ответа. Всякий диалог также исключен, когда перед соискателем предстает online-тест с выбором из заготовленных ответов, также порой являющийся порождением ума другого технического специалиста с его собственным, уникальным опытом и знанием английского языка. На мой взгляд, английский в разработке важен настолько, что порой проще на собесе объясняться на нём.

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

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

2. Идеальный формат, идеальный кандидат (формируем требования)


После каждого интервью у вас должно сложиться чёткое ощущение, может ли этот человек повысить вероятность успеха вашей компании, говорит Нил Роузман в статье Анатомия идеального технического интервью от бывшего вице-президента Amazon. Пусть эти слова для кого-то прозвучат высокопарно, особенно от вице-президента, пусть и бывшего, но это правда. В конечном счёте именно вам или вашему коллеге предстоит работать с этим кандидатом, если он успешно пройдет собес.

Техническую проблему можно решить, освежив матчасть, а выработать обоюдное для каждой заинтересованной стороны решение задача, требующая от кандидата понимания общей цели, когнитивных и коммуникативных навыков. Про такое в книжках не напишут, вернее напишут, но без собственного опыта это не работает. Поэтому soft skills, или личные качества, становятся неотъемлемой частью технического собеседования наряду с профессиональными знаниями кандидата (hard skills).

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

На помощь пришел давнишний подход, впервые применённый в Гарвардской бизнес-школе в 1924 году ситуационное интервью, или кейс-интервью. Условно его можно разделить на три большие части:

  • ценности и взгляды кандидата, soft skills;
  • профессиональные навыки и умения, hard skills;
  • модели поведения и индивидуально-личностные качества.

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

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

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

Конечно, эпидемиологическая ситуация в мире внесла свои коррективы, и Zoom вытеснил Skype, но скрининг был и остаётся нашим бессменным первым этапом.

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

Оставим детализацию этого процесса на откуп самим руководителям, перейдём к техническому интервью главному этапу в собеседовании разработчика.

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

3. О бедном эйчаре замолвите слово (про важность хорошего HR-специалиста)


В статье я рассуждаю с выигрышной позиции: крупная компания, известный бренд многие специалисты даже без уточнения технических деталей согласятся здесь работать. Но в любой компании нельзя недооценивать работу HR-специалиста. Он является своеобразным фильтром на входе в компанию. Сотрудники, которых вы нанимаете, будут так же хороши, как и команда по найму, которую вы собираете, подтверждает Роузман.

Главным активом любой компании являются люди. Хороший рекрутер даже при отсутствии бренда, в никому не известном стартапе может обеспечить специалистов с приемлемым уровнем знаний, и сделать даже нечто большее: найти единомышленников тех, кто будет радеть за продукт.

Крупная компания, известный бренд могут также осложнять работу HR-специалиста, будучи у всех на слуху. Хорошее знание проекта, процессов разработки и команды позволяют рекрутеру уже на первом этапе принять решение о том, подходит ли кандидат для вакансии. В результате до 60% кандидатов доходят до технического интервью, говорит руководитель направления подбора персонала Mail.ru Group Карина Пушкина. Однако объёмы таковы, что 60% это не 6 человек.

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

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

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

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



4. Как играть на поле кандидата, не забывая про себя (требования к интервьюерам)


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

Многообразие технологий, архитектурных приёмов и фреймворков усложняет поиск кандидатов, но говорить на общем с кандидатами языке можно и нужно. Специалист с головой на плечах сможет адаптироваться к новым для него практикам и даже привнести оригинальные решения в устоявшиеся подходы. Снятие ограничения при поиске кандидатов, скажем, на архитектурные паттерны или языки программирования в рамках одной технологической платформы, конечно, потребует от интервьюера знаний в соответствующих предметных областях. Где взять такие знания? Опыт проведения собеседований подскажет интервьюеру, в каком направлении стоит подтянуть знания, какие технологии и подходы распространены на рынке сейчас и в чем их отличие от принятых в компании. Необязательно становиться экспертом насильно. Достаточно представлять в общих чертах предметную область и вести с кандидатом диалог, позволив ему самому объяснить тонкости реализации. Такой подход позволит вам избежать чрезмерных затрат на подготовку к конкретному собесу, а кандидату продемонстрировать свои гибкие навыки, выраженные в способности объяснять свою точку зрения другому специалисту.

Это отнюдь не означает, что к собеседованию не нужно готовиться. Здесь важно соблюдать баланс. Предложите кандидату самому выбрать и объяснить задачу и её решение при помощи его любимой технологии. Попросите решить типовую задачу из вашей предметной области, но при помощи его инструментов. Так или иначе вы сможете совместно поработать над проблемой, знакомясь и с точкой зрения кандидата, и с привычной ему технологией. Типовые задачи, которые подходят для такого кейс-интервью, как правило, хорошо иллюстрируются проектированием архитектуры сервиса, модуля или экрана. Кандидат абстрагируется от конкретной реализации, а вам не нужно погружаться в техническую специфику. Поэтому открывайте draw.io или любой другой сайт для проектирования блок-схем и диаграмм, и вперёд! Этот формат отлично подходит для Zoom.

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

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

Задавать технические вопросы, как и проводить техническое собеседование, интервьюер должен структурировано. Нил Роузман предостерегает: Если вы провели собеседование и всё, что можете сказать это: Ну да, он вроде ничего, мне понравился, тогда вы потратили время зря. Структурированный подход должен стать рутиной в подготовке и проведении собесов.

5. Сценарий идеального технического собеседования


Конечно, здесь я не представлю универсальный, идеальный план проведения технического собеса. Это уникальный для каждой компании и каждого интервьюера процесс. Но я хочу предложить несколько идей, опробованных в реальной практике найма в Delivery Club, применение которых известным образом усилило нашу команду.

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

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

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

Сделайте техническое интервью диалогом двух специалистов. Начните ваш диалог, кратко перечислив темы, которые собираетесь затронуть.

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

Предложите кандидату выполнить code review, вместо того, чтоб заставлять его писать новый код. Практика code review отлично подходит для технической части ситуационного интервью. Подготовьте заранее неоптимальный код или код, в котором допущена ошибка. А если в листинге не содержится секретной информации, то лучше показать тот pull request, для которого коллегами было оставлено больше всего замечаний. Тем самым вы переосмыслите подход к коду на листочке: многие кандидаты испытывают трудности при написании кода вне предпочитаемой среды разработки или при стороннем наблюдателе. А code review на собесе позволит вам убедиться в навыках чтения и понимания кода, оценить способность командного взаимодействия по тону и содержанию комментариев кандидата.

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

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

Проведите собеседование от начала и до конца. Даже если вы понимаете через 15 минут после начала собеседования, что кандидат не подойдет, важно пройти все эти этапы, говорит Нил Роузман, Вы должны постараться провести почти полное интервью, потому что мир очень тесен, и пусть лучше человек думает, что у него только что было отличное интервью, даже если он не получил предложение о работе.

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

Сценарий идеального технического интервью. Драма в пяти действиях


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

Действующие лица:

  • Олег молодой, перспективный iOS-разработчик
  • Василий умудрённый опытом тех лид команды iOS

Действие первое




Никто из действующих лиц не знает, как пойдёт собеседование дальше. Олег может начать плутать в коде, скакать по наименованиям методов, тщетно предполагая, где они могут вызываться, а может ввести в поиск 1.5 и точно определить место, где задаётся пауза, так и не сумев объяснить механику работы. Тогда Василий постарается скорее завершить эту часть собеседования, поблагодарив и указав собеседнику на возможность постановки таких задач перед сотрудником только при наличии соответствующих знаний в области Objective-C.

С другой стороны, Олег может логически дойти до требуемого метода, где организована очередь, объяснить выбор параметров очереди и механизм её работы, рассказав попутно про другие виды очередей и способы реализации подобной задачи.

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


Действие второе




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

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


Действие третье




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

Действие четвертое




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

Для понимания последнего кейса Василий предлагает Олегу архитектурную задачу.


Действие пятое




Процесс проектирования архитектуры какой-либо системы всегда творческий. Он сопровождается диалогом, постоянными правками. Он уникален и ценен сам по себе, потому что однозначно даёт понять, разбирается ли автор в том, что объясняет, а заодно умеет ли объяснять.

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




Что в результате нарисовал Олег, мы опустим, и предоставим читателю на основании иллюстраций самому пофантазировать о том, понимает ли Олег выбранную архитектуру.



На этом всё. Спасибо, что дочитали!
Подробнее..

Зачем нужно понимать ООП

03.12.2020 00:05:42 | Автор: admin


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

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

Примеры кода буду приводить из iOS разработки.

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

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

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

Наследование


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

Так как ООП это про моделирование, то код мы пишем начиная с абстракции частей системы и взаимодействие между этими частями, которые мы должны записать в виде кода. Например, социальная сеть, которая состоит из пользователей, взаимодействующих друг с другом. Помимо пользователей, система состоит из более мелких компонентов, таких как сообщения, посты, лайки, комментарии. Даже сам пользователь может являть собой подсистему в системе. Как человек состоит из различных органов и частей тела: сердце, мозг, руки, пальцы, так и пользователь в системе может состоять из более мелких составляющих. Но уже не руки или глаза, а адрес, интересы, записи об образовании, которые можно выносить как отдельные объекты. Уже на этапе анализа можно проследить принципы объектно-ориентированного подхода. У нас есть пользователь абстракция реального человека. Но у пользователя могут быть разные роли: админ, обычный пользователь, VIP пользователь, анонимный посетитель. Они все являются абстракциями реальных людей и пользователями данной системы. Но каждая из вышеперечисленных ролей имеет свои особенности и при этом все имеют общее они пользуются системой и должны зарегистрироваться в системе (у каждого может быть свой способ) и они все должны пройти процедуру входа в систему.

Это и есть принцип наследования, где каждый админ/VIP-клиент/аноним являются пользователями, но не каждый пользователь должен быть админом или VIP-пользователем.

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

Еще пример ошибочной трактовки принципа наследования, это когда базовый класс и наследник являются представителями разных логических групп. Выглядит это следующим образом, реализуем MVC в iOS проекте, где UIViewController это Controller с абстрактными методами, которые должен реализовать наследник. А наследник это уже Model. Там где по логике проектирования должно быть взаимодействие между двумя группами классов, один класс становиться одновременно и Model и Controller. Не говорим уже о том, что UIViewController в реалиях iOS разработки еще и берет на себя роль View. В итоге мы получаем один объект, который делает все сам. Если у нас есть пользователь (User), то он будет и Controller и View одновременно.

Пример наследования в iOS
/**    Нужно сделать экран профиля пользователя, в котором отображаются имя и фамилия. Этот экран должен переиспользоваться. */// INCORRECTclass UserProfileViewController: UIViewController {    // MARK: - IBOutlets    @IBOutlet private var firstNameLabel: UILabel!    @IBOutlet private var lastNameLabel: UILabel!    /**    Заполнение данных оставляем классу наследнику в виде абстрактных методов     */    // MARK: - Abstract methods    func firstName() -> String? {        return nil    }    func lastName() -> String? {        return nil    }    // MARK: - Lifecycle    override func viewDidLoad() {        super.viewDidLoad()        firstNameLabel.text = firstName()        lastNameLabel.text = lastName()    }}/**    Создаем класс-наследник, который отвечает за функцию заполнения данных. В таком случае, наследник будет выполнять роль не только UIViewController, а и роль модели, которая предоставляет данные для отображения. */class UserProfileModel: UserProfileViewController {    override func firstName() -> String? {        return "Name"    }    override func lastName() -> String? {        return "Last name"    }}// CORRECT    /**    Корректней будет, добавить новый класс-модель, которая будет предоставлять данные.     */class UserProfileModel {    func firstName() -> String? {        return "Name"    }    func lastName() -> String? {        return "Last name"    }}/**    В таком случае у нас будут два отдельных класса, каждый из которых имеет свою зону ответственности. */class UserProfileViewController: UIViewController {    var model: UserProfileModel?    // MARK: - IBOutlets    @IBOutlet private var firstNameLabel: UILabel!    @IBOutlet private var lastNameLabel: UILabel!    // MARK: - Lifecycle    override func viewDidLoad() {        super.viewDidLoad()        setupUserInfo()    }    // MARK: - Private    private func setupUserInfo() {        firstNameLabel.text = model?.firstName()        lastNameLabel.text = model?.lastName()    }}// PERFECT/**    Еще лучше, взаимодействие между двумя типами классов, контроллер и модель, сделать через протокол, чтобы можно было создавать и использовать разные модели. */protocol UserProfileProtocol {    func firstName() -> String?    func lastName() -> String?}class UserProfileViewController: UIViewController {    var model: UserProfileProtocol?    // MARK: - IBOutlets    @IBOutlet private var firstNameLabel: UILabel!    @IBOutlet private var lastNameLabel: UILabel!    // MARK: - Lifecycle    override func viewDidLoad() {        super.viewDidLoad()        setupUserInfo()    }    // MARK: - Private    private func setupUserInfo() {        firstNameLabel.text = model?.firstName()        lastNameLabel.text = model?.lastName()    }}class UserProfileModel: UserProfileProtocol {        // MARK: - UserProfileProtocol        func firstName() -> String? {        return "Name"    }    func lastName() -> String? {        return "Last name"    }}


Представляю эту картину в реальной жизни, у нас есть регистратура, где некий администратор ведет записи о посетителях в тетрадку. Также в этой тетради можно читать данные о посетителях. Мы получаем посетителя Model, человека в регистратуре Controller и тетрадь View. И в вышеуказанной интерпретации принципа наследования мы получаем, что посетитель всегда является регистратором и тетрадкой. Уже дико выглядит то, что человек и тетрадка одно целое. И логика наследования нарушается. Если рассматривать, что посетитель и регистратор люди и принять тот факт, что представитель класса регистратора может стать посетителем, то логичней сказать, что регистратор является наследником посетителя.

Это пример, когда непонимание принципа наследования приводит к сложности понимания системы.

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

Абстракция


Я уже затронул принцип наследования, хотя хотелось бы начать с такого принципа как абстракция. На мой взгляд это базовый принцип ООП (также абстракция относится и к остальным парадигмам) и он незаслуженно перешел в разряд опционального принципа.

Абстракция гласит останавливаем внимание на важных и необходимых аспектах объекта и игнорируем ненужные для нас.

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

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

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

Также к абстракции я бы отнес декомпозицию, когда сложный объект разбивается на систему. Мы абстрагируемся от некоторых особенностей и переносим их в отдельный компонент. Пример: пользователь у которого есть место проживание, то есть адрес. Адрес в свою очередь состоит из города, улицы, номера дома и т. д. В этот момент мы думаем, а нужно ли указывать страну или регион? Если это приложения для пользования администрацией конкретного района города, то можно упустить такие детали. В итоге мы получаем пользователя, который абстрагируется от некоторых деталей адреса. Опять-таки, непонимание того, что мы не только пишем код, но и занимаемся моделированием, приводит к тому, что у нас есть, допустим, MenuViewController, который состоит из 5000+ строк кода.

Пример абстракции через декомпозицию
/**    Распространенная ситуация: создаем класс, к примеру, простую модель пользователя. Но с добавлением функционала, все больше появляется полей и методов в этом классе. */// INCORRECTclass User {    let firstName: String    let lastName: String    let fullName: String    let age: Int    let birthday: Date    let street: String    let postalCode: Int    let city: String    var phoneNumber: String?    var phoneCode: String?    var phoneFlag: UIImage?    var isLoggined: Bool = false    var isAdmin: Bool = false    // MARK: - Init        init(firstName: String,         lastName: String,         fullName: String,         age: Int,         birthday: Date,         street: String,         postalCode: Int,         city: String) {        self.firstName = firstName        self.lastName = lastName        self.fullName = fullName        self.age = age        self.birthday = birthday        self.street = street        self.postalCode = postalCode        self.city = city    }    // MARK: - Admin functionality        func createNewReport() {        guard isAdmin else { return }                print("New report created")    }        func updateReport(for user: User) {        guard isAdmin else { return }                print("Update report for \(user.fullName)")    }}// CORRECT/**    Правильней будет, декомпозировать код, абстрагируя части большого сложного класса на маленькие компоненты. */class Address {    let street: String    let postalCode: Int    let city: String    init(street: String,         postalCode: Int,         city: String) {        self.street = street        self.postalCode = postalCode        self.city = city    }}class Name {    let firstName: String    let lastName: String    init(firstName: String,         lastName: String) {        self.firstName = firstName        self.lastName = lastName    }    var fullName: String {        firstName + " " + lastName    }}class PhoneNumber {    let phone: String    let code: String    let flag: UIImage    init(phone: String,         code: String,         flag: UIImage) {        self.phone = phone        self.code = code        self.flag = flag    }}class User {    /**    В результате, класс User уменьшился в размерах, при этом мы абстрагируемся от деталей имени и адреса.     */    let name: Name    let address: Address    let birthday: Date    var phoneNumber: PhoneNumber?    init(name: Name,         address: Address,         birthday: Date) {        self.name = name        self.address = address        self.birthday = birthday    }}/**    Так как после логина система получает залогиненого Пользователя, то класс User не должен отвечать за состояния системы. За статус логина будет отвечать новая сущность, тем самым система абстрагируется от деталей логики этого статуса. */class LoginSession {    var user: User?    var isLoggined: Bool {        user != nil    }}/**Дополнительные свойства Администратора выносяться в класс-наследник Пользователя. */class Admin: User {    func createNewReport() {        print("New report created")    }        func updateReport(for user: User) {        print("Update report for \(user.fullName)")    }}


Полиморфизм


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

Полиморфизм плавно вытекает с наследования. Гласит он следующее: можно создавать классы наследники, которые будут имитировать интерфейс базового класса, но со своей собственной реализацией. Этот принцип отражается в таком принципе SOLID как принцип Барбары Лисков: мы можем подставлять объекты классов наследников там, где предполагается использование базового класса, при этом замена не должна никак себя проявлять.

Если взять пример с регистратурой, когда у нас есть просто человек (посетитель, не будем абстрагироваться и делать посетителя наследником человека) и есть регистратор. Должна быть возможность регистратору с другого отдела пройти через текущую регистратуру как обычному посетителю. В жизни это вполне реальный пример.

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

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

Пример: есть базовый класс автомобиль, который предположительно должен заехать в гараж. Создаем летающий автомобиль-наследник (как в фильме Назад в будущее 2). Какой будет результат при попытке загнать этого монстра в гараж? Да, он может залететь в гараж, если функция езды будет полностью заменена на функцию полета. А если функция полета была новым функционалом, а функция езды вообще заблокирована? То мы не сможем спрятать нашего летуна от непогоды. Это уже не будет автомобиль, это будет что-то новое. Результат из-за неправильного моделирования и наследования был нарушен принцип полиморфизма и получился нежелательный результат.

Летающий автомобиль

Добавлю, что полиморфизм тесно связан с наследованием и проблемы в наследовании отзываются еще большими проблемами в полиморфизме.

Инкапсуляция


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

Первый вариант иногда воспринимается как инкапсуляция = сокрытие, что, как я считаю, не совсем верное понимание.

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

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

Для этого я создаю класс наследник UIButton и в наследнике добавляю метод, который устанавливает цвет кнопки в цвет с альфа каналом 50% от оригинального. А также я добавляю метод, который возвращает бэкграунд цвет кнопки в оригинальный, без альфа канала.

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

Пример кнопки с открытыми методами, которые могут нарушить повидение этой кнопки
/**    Создаем кнопку, у которой при нажатии цвет бэкграунда устанавливается в оригинальный цвет но с альфаканалом 0,5 */// INCORRECTclass Button: UIButton {     /**    Добавляем два метода, которые устанавливают цвет бекграунда для состояния нажатой кнопки и нормального состояния кнопки     */    func decorateSelected() {        backgroundColor = backgroundColor?.withAlphaComponent(0.5)    }        func decorateDeselected() {        backgroundColor = backgroundColor?.withAlphaComponent(1)    }    override var isSelected: Bool {        didSet {            if isSelected {                decorateSelected()            } else {                decorateDeselected()            }        }    }}// SAMPLE /**    Проблемой будет то, что методы, декорирующие кнопку в разных состояних, являются публичными. А это значит, что можно нарушить логику работы кнопки, вызвав метод в неправильный момент. */let button = Button()button.decorateSelected()// CORRECTclass Button: UIButton {        override var isSelected: Bool {        didSet {            if isSelected {                decorateSelected()            } else {                decorateDeselected()            }        }    }     /**    Мы сделали методы, настраивающие внешний вид кнопки, приватными, тем самым обеспечили правильную логику отображения.     */    // MARK: - Private    private func decorateSelected() {        backgroundColor = backgroundColor?.withAlphaComponent(0.5)    }        private func decorateDeselected() {        backgroundColor = backgroundColor?.withAlphaComponent(1)    }}// PERFECT /**    Но! У кнопки остаеться возможнось измененить цвет через базовое поле var backgroundColor: UIColor?. Поэтому, немного заморочившись, делаем невозможным менять цвет в момент, когда кнопка нажата. */class Button: UIButton {            override var backgroundColor: UIColor? {        get {            super.backgroundColor        }        set {            if isHighlighted == false {                super.backgroundColor = newValue            }        }    }        override var isHighlighted: Bool {        willSet {            if newValue {                decorateSelected()            }        }                didSet {            if isHighlighted == false {                decorateDeselected()            }        }    }    // MARK: - Private    private func decorateSelected() {        backgroundColor = backgroundColor?.withAlphaComponent(0.5)    }        private func decorateDeselected() {        backgroundColor = backgroundColor?.withAlphaComponent(1)    }}


Инкапсуляция не бездумное сокрытие каких-то полей или методов, а проектирование класса с определенным набором возможностей, которые не должны нарушаться.

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

Если просуммировать, то инкапсуляция проектирование самостоятельной единицы (объекта), которая выполняет некую роль в системе, имеет набор параметров и методов. При этом все, что может нарушить роль этого объекта, скрыто.

Заключение


Что в целом я хотел сказать? Я считаю, что программирование сложных систем, которые состоят из множества компонентов, начинается с моделирования, а не с кодинга. А для моделирования лучше всего подходит ООП парадигма, так как она собрала в себя принципы, позволяющие упростить моделирование и дальнейшее написания кода. Поэтому правильное понимание принципов ООП ключ к грамотному моделированию. В свою очередь хорошо спроектированная система проще переноситься в код и этот код проще поддерживается в будущем.
Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru