Русский
Русский
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. Прежде чем пойти на собеседование отрепетируйте его. Задавайте себе всякие вопросы связанные с тем, что должно быть и постарайтесь внятно ответить на них.

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


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

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

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 языков. Мы также делаем рекламные и обучающие видеоролики.
Подробнее

Подробнее..

Дайджест интересных материалов для мобильного разработчика 348 (8 14 июня)

14.06.2020 16:43:19 | Автор: admin
Наш новый дайджест рассказывает про странный поиск вирусов в приложении для изучения иностранного языка, про применение Kotlin и автоматизацию локализации, про лучшие интерфейсы и вратарей напрокат.


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

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Apple представляет программу WWDC20
Apple переведет компьютеры на собственные ARM-процессоры
Разработчики за пределами США жалуются на проблемы с оформлением и продлением подписки Apple Developer Program
Создаем iOS-приложение с позитивными новостями, используя возможности машинного обучения
Создаем собственный шаблон в Xcode и сокращаем времени разработки
SwiftUI: интеграция Mapbox SDK
Рисуем в iOS используя CAShapeLayer
Дополненная реальность на Swift 5 как начать
5 iOS-библиотек, которые сделают ваше приложение более стильным
Многоразовые всплывающие окна и оповещения в iOS
Создаем простой прогресс-бар, который можно добавить в любое приложении
Изучение SwiftUI сделает вас лучшим программистом
Анализ open source iOS-приложений
Как применять условные модификаторы View в SwiftUI
Обзор указателей в Swift
Улучшите ваш рабочий процесс с UIKit и Swift Live Previews
FSPagerView: элегантный слайдер

Android

(+26) Spring Boot, Hibernate и Kotlin для новичков шаг за шагом
(+4) Практическое применение Kotlin в стартапах и энтерпрайзе
Android Dev Podcast #115. Новости
Вышла первая бета Android 11
Google Play Asset Delivery открыли для всех
Mobile People Talks: Jetpack Compose взгляд изнутри
Как сделать приложение-календарь для Android
Как анимировать и строить графики с помощью Android Interpolator
Создаем приложения Scrum Poker с применением MotionLayout
7 главных языков программирования для разработки Android-приложений
Создание вращающийся ручки на Kotlin
Android Studio: Layout Inspector
Kotlin и Exceptions
Темный режим в Android-приложении с Kotlin
Инъекция зависимостей на Android с Hilt
Kotlin Symbol Processing: первые мысли
Важные изменения в Android Studio 4.0
Simple Dialer: звонилка для Android
Compose Academy: изучаем Jetpack Compose
Trinity: короткие видео для Android

Разработка

(+20) Phrase.com или как мы автоматизировали флоу локализации
(+16) Оптимизация рендера под Mobile. Часть 3. Шейдеры
(+12) 20 платформ для заработка на тестировании
(+11) QA-процесс в Miro: отказ от водопада и ручного тестирования, передача ответственности за качество всей команде
(+11) Великобритания запускает приложение, следящее за вашим кругом общения: как оно будет работать и когда будет доступным
(+10) Красивое удобнее, чем некрасивое? Обзор исследований
(+7) Как сделать заказной веб- или mobile-проект с нуля: процессы, правила и немного крови
(+5) SSL pinning во Flutter
(+5) Нативная разработка vs кросс-платформенная нужно ли выбирать?
(0) React Native: Push-уведомления с помощью AWS Amplify
Исследование The State of Developer Ecosystem 2020 от JetBrains
Snapchat запускает мини-приложения внутри чата
make sense: о связке продукт коммуникации, нарративе и воспринимаемой енности
Podlodka #167: Компиляторы
LOVEMOBILE #06: Издательство с 101XP
9 советов ля быстрого улучшения дизайна вашего пользовательского интерфейса
Дизайн приложений: примеры для вдохновения #4
Открылся прием навыков для Маруси
10 правил NASA для написания критически важного кода
Мобильное приложение на Flutter. Стоимость, сроки, подводные камни. Часть 1
Как правильно интегрировать исследования пользователей и рынка в вашу продуктовую команду
Дизайнеру приложений: как создать и передать в разработку тёмную тему
BindingX: нативная разработка без нативной разработки
Как работает шумоподавление в Google Meet
10 новых и многообещающих трендов в дизайне интерфейсов
Как записывать автоматизированные тесты для мобильных приложений

Аналитика, маркетинг и монетизация

(+4) Как продвигать мобильные игры и приложения в Японии, Корее и Китае
В Бразилии сделали приложение Вратарь напрокат
Как обстоит дело с ретаргетингом в риложениях. 2020 год: отчет AppsFlyer
Axiom: анализ данных на предприятиях
Сбербанк покупает 2ГИС
Сheckaso ищет приложения для бесплатного ASO аудита
Рост гиперказуальных игр в 1 квартале 2020: отчет djust и Unity
Drop привлек $13.3 млн. на платформу умной кухни
myTarget расширил инструменты атрибуции екламных кампаний
Влияние коронавируса на стоимость рекламы: исследование AB
AppsFlyer открывает бесплатный доступ к своим нструментам
Российский рынок ИТ-услуг сократится на треть
Tajir: онлайн-магазин для офлайн-магазинов
Гайд: как сделать текстовое ASO быстро и эффективно?
Тудурант менеджер задач, который заставляет примитивный мозг работать

AI, Устройства, IoT

(+35) Люди ломаются на логике, роботы на всем понемногу. Экзамены по русскому для NLP-моделей
(+30) Как мы отказались от нейросетей, а затем вернули их в прогноз осадков Яндекс.Погоды
(+17) Автоматизация квартиры
Snap пускает сторонние ML-модели в свои Линзы
Стоимость тренировки ИИ упала в 100 раз за 2 года
Как я сдал сертификационный экзамен разработчика TensorFlow

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

Самое важное с конференции Apple WWDC20

23.06.2020 00:11:29 | Автор: admin
Сегодня вечером прошла 31-я конференция Apple для разработчиков впервые полностью в формате онлайн. В этом репортаже мы перечислили наиболее значимые нововведения, которые были представлены сотрудниками компании для своих операционных систем и устройств.



iOS 14


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

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



Появилась функция Picture in Picture: теперь видео можно закрепить на экране и просматривать в фоновом режиме, пока вы занимаетесь своими делами.

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

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



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

Теперь владельцам iPhone доступна цифровая версия ключей от машины. Для ее использования нужно просто использовать iPhone с NFC-чипом, чтобы разблокировать дверь. Можно отключить эти ключи через iCloud или настроить индивидуальный доступ к авто например, по возрасту.



В App Store появилось нововведение под названием App Clip. С его помощью можно запустить лишь малую часть приложения и использовать ее только тогда, когда она вам нужна.


iPadOS 14


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

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



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

Звонки и FaceTime отныне занимают не весь экран, а появляются виджетами на его верху. То же самое изменение касается и iOS.

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

Большие изменения ждут Apple Pencil с новой функцией Scribble. Рукописный текст теперь автоматически переводится в машинописный, а нарисованные фигуры аппроксимируются до идеальной формы. Этот рукописный текст можно выделить двойным нажатием, а затем вставить, например, в Pages, и он тут же переформатируется в машинный. Также появится возможность писать от руки прямо в строке поиска или на двух языках в одном предложении одновременно.




AirPods


Появится автоматическое переключение между девайсами. Так, если вы слушали музыку на iPad, и в это время вам поступил звонок на iPhone, наушники автоматически переключатся на нужное устройство.

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




watchOS 7


При помощи функции Face Sharing watchOS 7 позволит делиться циферблатами со своими друзьями и семьей.

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

В режимы тренировок добавлены танцы различных стилей (индийские, хип-хоп и даже латинские), а также произведен редизайн сохранения прогресса тренировок.

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

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


Безопасность


Вход при помощи Apple теперь можно конвертировать в уже существующие аккаунты.

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

Теперь в App Store на странице приложения будет возможность увидеть, какие данные приложение у вас запросит.




HomeKit


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

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

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




tvOS 14


Введена поддержка нескольких пользователей, а также синхронизация с Xbox Elite 2 и контроллерами Xbox.

Apple TV+ теперь можно смотреть в приложении Apple TV App.

На конференции анонсировали новый сериал Apple TV+ по Айзеку Азимову Основание, который выйдет в 2021 году.


macOS


Новая операционная система macOS Big Sur перетерпела самый крупный редизайн со времен macOS X. Система стала более простои и минималистичной и в целом гораздо больше похожей на iOS и iPadOS, и вместе с тем переняла многие их нововведения.

Появился Центр управления, во многом копирующий таковой из iOS с теми же регулировкой яркости, доступом к настройкам и переключением между светлой и темной темами. Появились виджеты опять-таки, настраиваемые аналогично iOS 14. iMessage также приобрело вид, максимально приближенный к аналогичному мобильному приложению. Все это стало возможным благодаря новой технологии портирования iOS-приложений macOS Catalyst.



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

И самое главное: теперь компьютеры MacBook будут выпускаться на собственных чипсетах. Apple Silicon SoC будут иметь более высокую производительность и прекрасно подходить для гейминга. Для плавного перехода с чипов Intel на ARM-процессоры Apple будет использоваться эмулятор Rosetta 2.



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

Выступления и презентации в стиле Apple на примере WWDC20

23.06.2020 22:21:05 | Автор: admin
К нам часто приходят клиенты с запросом Хотим в стиле Apple, и теперь на один такой запрос станет больше он будет касаться презентаций для онлайн-конференций. Сооснователь студии дизайна информации VisualMethod сделала подробный разбор, на каких элементах строился сценарий вводного дня #WWDC20. Разбор состоит из трех блоков: цель, контент и дизайн. Полезен всем, кто делает презентации и выступает публично.

image

Иллюстрация VisualMethod

Меня зовут Виктория. Я сооснователь студии дизайна информации VisualMethod и тренер по публичным выступлениям. Мне всегда интересно смотреть презентации крупных компаний и анализировать их сценарии. 22 июня на #WWDC20 компания #Apple не только представила свои обновленные продукты, но и задала тренд публичных выступлений в новом формате.

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

Разберем презентацию Apple от начала до конца


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

image

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

Давайте предположим, как могли звучать ответы на эти вопросы у Apple, исходя из того, что озвучивали спикеры с экранов всех форматов на #WWDC20.

1. Цель. Что зритель должен знать, думать или делать после WWDC20?


Здесь все, на первый взгляд, очевидно зритель после презентации должен отдавать предпочтение продукции Apple. Значит ли это просто покупку очередного гаджета? В этом году Apple расширила границы и призвала не просто голосовать рублем в магазине, а пустить себя в жизнь человека на 100%.

image
Интересный жест занять место зрителя

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

Поэтому если сформулировать цель кратко, то она прозвучит примерно: жить с Apple.

2. Аудитория. Кто должен совершить целевое действие после WWDC20?


Хотя всемирная конференция Apple и обозначена как мероприятие для разработчиков, на деле это PR-событие, и вторая (если не первая) его аудитория пресса, блогеры и общественные институты. Именно поэтому CEO компании Tim Cook начал Всемирную конференцию разработчиков с мнения о событиях, которые в настоящее время влияют на мир: движения Black Lives Matter и всемирной эпидемии коронавируса.

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

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

image
Обратите внимание на символы позади Кука все они со смыслом о важных событиях в обществе.

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

Смотрите, два супер-важных повода и два аргумента в пользу Apple. Вот она работа с репутацией. И эта работа нацелена не на разработчиков. Акции Apple также выросли во время основного доклада и закрылись на 2,6% до 358,87 долл. Это является признаком того, что инвесторы были довольны новостями о программном обеспечении и компании.

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

3. Сообщение. Какие аргументы и контент?


Ключевые сообщения для инвесторов и СМИ мы разобрали в блоке выше они про социальную ответственность и экосистемность Apple. Что же компания хотела сообщить разработчикам?

Первое новые обновления. Повторять вышедшие на VC.RU и Хабре отчеты не буду, там все подробно расписано. Интересное сообщение было о том, что мероприятие бесплатное и без регистрации, раньше Apple продавала доступ на очные конференции. Напомню, сообщество разработчиков Apple около 23 миллионов человек, и в очном формате собрать всех нереально. В онлайн пожалуйста, но и из этого сделан отдельный инфоповод. Неплохо.

Еще для разработчиков были фоновые сообщения об удобстве работы именно в этой экосистеме. Для этого, предполагаю, были собраны самые массовые запросы и детально освещены в обзоре от организации иконок до трансформации надписи стилусом в текст. Ведь разработчики прежде всего люди, поэтому новости об измерениях сна, физической активности через часы, открытие машины с помощью телефона, возможности персонализации и установки фото ребенка на рабочем столе это жизненно и интересно.
Сообщение это не только слова, а еще интонация и внешний вид того, кто говорит. Это тоже часть коммуникационной стратегии и называется Tone of voice (ToV), то, с какой интонацией и в каком стиле говорят спикеры бренда и звучат сообщения от имени компании.

Заметили, сколько раз в разных формах спикеры конференции сказали слово великолепный? Еще более волшебный, восхитительный, невероятный. Фанаты инфо стиля наверняка были в шоке. Так вот это и есть стиль бренда. Вы можете увидеть его же на сайте компании. Если бы они сказали мы лучшие один раз это звучало бы как хвастовство. Но если восхищенные прилагательные звучат в каждом предложении это воспринимается как естественный стиль компании, отмечает Виктория Рындина.

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

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

image
Ценности Apple

Финальные наблюдения в этом блоке. У Apple есть прописанные ценности на сайте, в материалах о компании. Ценности нужны в том числе для имиджа, чтобы создавалось цельное представление о компании, но они работают, если подтверждаются примерами. Поэтому в каждом программном выступлении и на мероприятиях представители Apple называют свои ценности и то, как они воплощены в жизнь. Это об универсальном доступе, окружающей среде, конфиденциальности. Заметили, как эти ценности были вшиты в выступления спикеров? Обычно это самая скучная часть, и у Apple этот блок тоже был затянут и спокоен так, что хотелось взять в руки iPhone, но при многократном повторении все же работает прекрасно, создавая нужное впечатление о компании.

4. Канал. Откуда аудитория получит сообщение?


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

Блогеры и авторы статей отметили перемещение камеры из театра в офис, я зафиксировала для себя важный нюанс с представлением продуктов Apple там, где они используются на рабочем месте, в спортивном зале, гараже, квартире. Это погружает в нужную атмосферу. Единственное, что не смогли показать это перемещение по городу, но вместо этого был симпатичный ролик в 3D.

image
Тайная лаборатория Apple тоже была среди локаций

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

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

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

Логика выступлений строго по цели


  1. Сделать заявление компании о важных событиях в мире. Это нужно, чтобы обозначить позицию компании и задать тон.
  2. Подстроиться под аудиторию и привлечь внимание. Презентацию начали с простых изменений, которые интересны всем зрителям. Например, виджетов. Организация приложений была зудящей проблемой, о которой часто писали на форумах. Решение простое, охват аудитории максимальный, тема вызывает доверие и затягивает.
  3. В середине конференции дали новые продукты, которые еще недостаточно популярны это подписки на ТВ и прочие электронно-развлекательные продукты. Тизер Foundation|Apple TV+ разбудил немного заснувшую аудиторию и дал возможность отдохнуть от спикеров.
  4. В конце конференции поставили самые важные анонсы, например, о постепенном отказе использования чипов Intel. Это давняя уловка ставить в конце самое интересное, чтобы удержать внимание зрителей до конца.
  5. Кук отпустил аудиторию чуть раньше, чем закончились 2 часа. Это хороший прием, чтобы оставить ощущение недосказанности и заинтересовать зрителя подумать о том, что он услышал и заставить действовать.

Все вместе было собрано с помощью красивого монтажа.


Тизер Foundation|Apple TV+

О дизайне скажу отдельно


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

image

Текст на слайдах


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

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

image
Текст это отдельный объект на слайде, если спикер зачитывает текст, то его нет в кадре.


Анимация


Это тренд в дизайне презентаций 2020 года, и он останется с нами и дальше. Что отличало анимацию в презентации Apple? Естественность. Текст, иконки, объекты появлялись ровно так, как мы ожидаем этого в жизни, а точнее, как мы привыкли это видеть на своих экранах в приложениях и на сайтах. Возьмите этот пункт во внимания, работая в PowerPoint. Анимация снова в моде, но она должна быть ненавязчивой.

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

Градиент


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

image

Прозрачные плашки


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

image

3D-модели


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

image

5. Активация. Конференция закончилась что дальше делать?


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

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

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

Что было интересно Вам?
Подробнее..

Дайджест интересных материалов для мобильного разработчика 349 (15 21 июня)

21.06.2020 14:07:34 | Автор: admin
В новом дайджесте снова разбираемся с темными темами, с монополизмом Apple, с недавним релизом Android 11 и сложностями тестирования, с границами UX и масштабированием загрузок.


С точки зрения методологии в образовательных проектах есть интересная деталь: мы используем в обучении два подхода индивидуальный и командный. Одни преподаватели выстраивают программу курса, исходя из плотной командной работы, другие, наоборот, опираются на индивидуальную работу каждого студента. Но, оставив в стороне рассуждения об эстетике тёмной темы, так ли уж она полезна для глаз? На самом ли деле тёмная тема повышает продуктивность работы с текстом? Ралука Будиу (Raluca Budiu) из Nielsen Norman Group даёт исчерпывающие ответы.

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

(+9)simctl: управление симуляторами Apple через терминал
(+3)Sign in with Apple дедлайн уже 30 июня
(0)HorizontalList с помощью SwiftUI
(0)SwiftUI по полочкам: Анимация, часть 2 тайминги
Apple открыла новый форум для разработчиков
Создатели почтового клиента Hey обвинили Apple в вымогательстве +Match Group и Epic Games поддержали разбирательство с Apple
Оборот приложений в App Store в 2019 году превысил $500 млрд.
Объяснение CAGradientLayer
6 советов по повышению производительности Core Data
Как в реальном времени обрабатывать изображения с камеры iOS
Как реализовать Динамический загрузчик с Lottie и Firebase
Создайте свою собственную библиотеку кнопок с нуля в SwiftUI
MemoryLayout в Swift
Создаем потрясающие индикаторы загрузки с помощью SwiftUI
HorizonCalendar: календарь от Airbnb
MultiProgressView: анимированные прогресс-бары

Android

(+15)Android 11 Beta и обновления для разработчиков
(+18)Litho: лучшие практики для создания эффективного UI в Android
(+6)Как отключить предупреждение о вреде долгого прослушивания аудио (Android)
(+5)Android Camera2 API от чайника, часть 6. Стрим видео сначала кодировали, теперь декодируем
(+5)MVP для Android преимущества использования Moxy в качестве вспомогательной библиотеки
(+4)Настраиваем GitHub Actions для Android с последующим деплоем в PlayMarket
(+1)Как и зачем мы используем несколько движков карт в inDriver
(+1)Android-разработка: Карьерный обзор за май 2020
По следам Android 11 Beta
Исследуем новую Google Play Console: большой шаг вперед
Вышла Google Play Billing Library Version 3
Start в Android с Дмитрием Виноградовым
Выбор правильного лейаута для Android
AndroidX: App Startup
Изучение внедрение зависимостей в Android Dagger, Koin и Kodein
Практическое руководство для решения OutOfMemoryError в Android-приложении
Зачем нам нужен Jetpack Compose?
WebRTC на Android: как включить аппаратное кодирование на нескольких устройствах
Что нового в Android Studio System Trace
Делаем снукер для Android с анимацией на основе физики
Представляем Pixel: новую Kotlin-библиотеку загрузки изображений для Android
Amaz Timer: таймер для умных часов
Meow Framework: MVVM и материальный дизайн

Разработка

(+28)Как мы обвесили механику баллистического расчета для мобильного шутера алгоритмом компенсации сетевой задержки
(+21)Многократное использование UI-компонентов в масштабах организации
(+15)Как сократить оформление ущерба по ОСАГО с нескольких дней до 60 минут
(+14)Оля, тесты и фабрика путь к красивой архитектуре и чистоте кода
(+5)Async/await в Unity
(+3)Хочешь, чтобы тебе поставили корректную дизайн-задачу? Помоги продакту ее поставить
(+2)Как устранить слепые зоны с помощью визуального тестирования
(+2)Обучение умных игровых соперников в Unity методом игра с самим собой средствами ML-Agents
(+1)Маски тестировщика (вопросы для успешного перехода к тестерскому расстройству личности)
Podlodka #168: геймификация процессов
Дизайн приложений: примеры для вдохновения #5
5 сервисов для управления мобильными подписками
Вы не Google
Чрезмерно сложно? Слишком просто? Эффективная граница UX
8 советов, как по-быстрому улучшить свой интерфейс
Как сделать ui анимацию естественной и приятной глазу: физические законы в анимации интерфейсов на практике
Swift или Kotlin что лучше?
Добавление облачной функции обнаружения объектов к системе домашних камер
Классические ошибки, которые совершил каждый разработчик
Как скрыть ваши API ключи
10 идей из руководства Apple по разработке пользовательского интерфейса
Редизайн банковского приложения с неоморфизмом

Аналитика, маркетинг и монетизация

(0)Быстрый лайфхак для роста приложений ASO на других языках
Как я масштабировал приложение с 0 до 100,000 загрузок без единого потраченного доллара
Мобильные магазины показывают невероятный уровень вовлечения продажи в 2020 году выросли на 40%
Google запустил аналог Pinterest социальную сеть Keen
ФАС отказалась от смягчения предустановки российских приложений
Исследование: объем российского рынка мобильных игр вырос на 49% в 2019 году
ASO аудит приложений и универсальные рекомендации
myTarget добавили новые метрики в аналитике рекламных кампаний
The Pokemon Company учит детей чистить зубы с Pokemon Smile
Spike получил $8 млн. на то, чтобы сделать почту похожей на чат
Мультиформат, квадратное и вертикальное видео: лучшие форматы мобильной рекламы для получения установок
Антикейс: почему расходится количество конверсий в Google Ads и в аналитике для iOS-приложения
Что влияет на позиции приложения в App Store и Google Play

AI, Устройства, IoT

(+123)Самая сложная задача в Computer Vision
(+46)ABBYY NeoML: как мы делали библиотеку машинного обучения и зачем она нужна
(+37)Как понять, что нейросеть решит вашу проблему. Прагматичное руководство
(+37)Полный цикл создания устройства и работа сфабриками в Китае. Доклад Яндекса
(+19)Древности: десятилетие Apple iPad
(+14)Event2Mind для русского языка. Как мы обучили модель читать между строк и понимать намерения собеседника
(+8)Умный дом в умном городе
Huawei впервые стал 1 на рынке смартфонов
Можно ли удалить 99% нейронной сети без потери точности?

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

Дайджест интересных материалов для мобильного разработчика 350 (22 28 июня)

28.06.2020 14:08:19 | Автор: admin
В этом выпуске, конечно, у нас много материалов с WWDC App Clips, виджеты, новый Xcode, macOS Big Sur, изменения в StoreKit, UIKit и SwiftUI. Кроме них стилизация, полезные инновации, логистика, анимации и многое другое.


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

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

(+16)Тестирование StoreKit в Xcode 12 и iOS 14
(+15)SwiftUI 2020. Что изменилось?
(+10)Как и к чему готовиться на собеседование начинающему iOS-разработчику и не только
(+6)iOS in-app purchases: Конфигурация и добавление в проект
(+5)Выступления и презентации в стиле Apple: на примере WWDC20
iOS 14 позволит получать уведомления о важных звуках
Core ML теперь можно шифровать и обновлять независимо от приложения
Загрузка данных в iOS в Background-режиме
Что нового в Xcode 12
Тестирование покупок и семейная подписка: обновление IAP с WWDC
Apple пропустила Hey в App Store и меняет политику модерации
Создавайте виджеты с помощью WidgetKit
Apple представила мини-приложения App Clips
Apple запустила программу поддержки разработки универсальных приложений
Apple представляет macOS Big Sur
watchOS 7: новые настройки и функции для тех, кто следит за здоровьем
Apple раскрывает новые возможности iPhone с iOS 14
iPadOS 14: новые функции, созданные специально для iPad
Что iOS-разработчики думают о переходе Mac на процессоры Apple и других анонсах WWDC 2020
Рисуем в 3D, используя SwiftUI
iOS 14: важные изменения в UIKit
Автоматизация создания скриншотов в iOS с Bitrise и Fastlane
Цепная анимация в Swift
Создаем SwiftUI + Core ML игру для iOS
Как анимировать изображения в Swift
Как Apple делает soft-UI будущим

Android

(+9)Стилизация Android-приложений и дизайн-система: как это сделать и подружить одно с другим
(+3)Android и 3D камера. Распознавание лиц с защитой от Fraud
(0)Кодовая база. Расширяем RecyclerView
ARCore Depth API открыли для всех
Победители конкурса Полезные Инновации от Google
Android Broadcast: все тайны MVI
Huawei выпустил HMS Core 5.0
Создание безопасных Android-приложений
Мигрируем с Retrofit на Ktor
Внедрение темной темы в ваше Android-приложение
Как создать REST API для вашего приложения с помощью Spring Boot, Kotlin и Gradle
Создание масштабируемой навигационной системы в Android
Отточенный и гибкий Progress View для Android
Шаблон моего Android-проекта
Бифуркация Android
Merge Adapter: объединяйте списки в Android
Освоение шаблонов дизайна в Android с Kotlin
Создание адаптера RecyclerView, который можно использовать с любыми данными и любым представлением
Wizard Camera: эффекты для фото на OpenGL
Checked Android App: ToDo на Kotlin
CornerSheet: расширяемое окно

Разработка

(+54)Как мы сэкономили время курьерам. Логистика в Яндекс.Еде
(+26)Создание шейдерной анимации в Unity
(+21)Мобильные антивирусы не работают
(+15)Разбор UI/UX на примере прототипа в Figma и основные принципы
(+7)Создаем прогрессивное веб-приложение на ReactJS и размещаем его в Netlify и PWA Store
(+7)Как устроен Selenium: Эпизоды 3 5
(+6)Usability Testing от А до Я: подробный гид
(+3)Разработка мобильных приложений: как формируется цена?
(+3)UX/UI-ДИЗАЙН: нельзя просто взять и нарисовать экран
Podlodka #169: увольнения
В AWS запустили конструктор приложений Amazon Honeycode
Unity сделала все Premium курсы бесплатными
Дизайн приложений: примеры для вдохновения #6
ML Kit становится отдельным продуктом
Серьезные ошибки в UX, которые могут снижать ваши продажи
Мое 10-летнее путешествие в разработке игр
13 моих любимых UI/UX ресурсов
Быстрая навигация во Flutter с Get

Аналитика, маркетинг и монетизация

(+1)Локализация мобильных приложений: основные сложности и лайфхаки
(0)Получение данных Amplitude через API
Одноклассники выплатили создателям мобильных игр более 360 млн. рублей
myTarget расширил возможности закупки видеорекламы в форматах Rewarded и Interstitial видео
9 способов повысить точность прогноза дохода
Kaia Health: физиотерапия под присмотром приложения
TikTok вкладывает $50 млн. в образовательный контент
Приложение для детей: от идеи до запуска
Пять лайфхаков по оптимизации приложения в App Store и Google Play

AI, Устройства, IoT

(+19)От AI до VR: как промышленность и ритейл используют новые технологии
(+15)Управление Яндекс.Станцией и другими колонками с Алисой из Home Assistant
(+10)Опыт построения умного дома на Raspberry Pi и открытой платформе OpenHAB. Часть 1
(+9)HMI на основе Node-red и Scadavis.io
(+3)Из чего состоит набор для разработчиков NB-IoT DevKit?

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

Дайджест интересных материалов для мобильного разработчика 351 (29 июня 05 июля)

06.07.2020 06:04:37 | Автор: admin
В новом дайджесте разбираемся с последствиями WWDC, выпускаем приложения одной кнопкой, сравниваем быстродействие кроссплатформенных фреймворков, проводим нагрузочное тестирование, растим доходы приложений и занимаемся многими другими интересными вещами!


Сегодня я подведу некоторые итоги: расскажу, к чему мы пришли за это время. Long story short: любой причастный к процессу сотрудник может зарелизить хоть все наши приложения на обеих платформах в несколько кликов без головной боли, больших затрат времени, регистрации и СМС. Так, наш отдел релиз-инженеров за 2019 год сэкономил около 830 часов. inVerita и ее команда разработчиков мобильных приложений постоянно изучают производительность кроссплатформенных мобильных фреймворков, доступных на рынке, чтобы ответить на вопрос, какая технология лучше всего подходит для вашего продукта.

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

(+27)Бюджетный DI на антипаттернах
(+21)Почему разработчики отказываются от авторизации через Apple с фейковым email
(+6)BoxView удобный autolayout для iOS
(+4)Как смотреть WWDC 2020, если ты не разработчик
(+3)Apple WWDC 2020: что нового в тестировании iOS
(+3)Развитие ARKit в этом году и новая возможность в ARKit 4: Location Anchors
Презентации WWDC и Platforms State of the Union доступны с субтитрами на русском языке
Apple пытается улучшить вовлеченность игр Arcade
Apple назвала 8 лауреатов ежегодной премии Apple Design Awards
Погружаемся в мир дополненной реальности с ARKit
Когда вам нужно сообщить о применении шифрования в приложении?
Новое в iOS 14: определение контуров
iOS 14 App Clips
Ваше первое сложное приложение на SwiftUI
Внедряем чистую VIP-архитектуру в Swift 5
Создайте свою собственную библиотеку CocoaPods
Как создавать виджеты в iOS 14
Новый жизненный цикл и замены для AppDelegate и SceneDelegate в SwiftUI в iOS 14
NewYorkAlert: красивые предупреждения для iOS

Android

(+12)Редактор кода на Android: часть 1
(+5)Приручая MVI
(+5)Блокировка двойного клика. Велосипед?
Huawei объявляет конкурс приложений с призовым фондом в 1 млн. долларов
Представляем RainbowCake
JetPack Compose с Server Driven UI
Новый способ передачи данных между Фрагментами
Динамическое изменение цвета градиента в Android
Создаем приложение с новостями для Android за 5 простых шагах
MVVM с Hilt, RxJava 3, Retrofit, Room, Live Data и View Binding
Быстрое тестирование на Android с Mobile Test Orchestrator
Кастомные Android View: Drag and Drop
Дилемма Kotlin: Extension или Member
Рендеринг PDF-файлов на Android: простой способ
Укрепление безопасности системы в Android 11
Юнит-тестирование кастомных View в Android
Оптимизация работы с батареей для избежания Doze Mode и App Standby
Видимость пакетов в Android 11
RainbowCake: новая Android-архитектура
Разработка с Actions Builder и Actions SDK

Разработка

(+21)NewNode децентрализованная CDN от разработчика FireChat
(+9)Как мы решаем проблему отсутствия UI\UX дизайна в 1С с помощью Java Script и React.js
(+6)Godot, 1000 мелочей
(+5)Оптимизация SQL запросов или розыск опасных преступников
(+4)6 советов по нагрузочному тестированию к Черной пятнице
Podlodka #170: искусство простых иллюстраций
Flutter Dev Podcast #17: Flutter Day 2020
Думай, как CEO: самый важный навык, который выделит тебя среди разработчиков
Dfinity открывает платформу Internet Computer для разработчиков
Дизайн приложений: примеры для вдохновения #7
AWS запускает CodeGuru для автоматического анализа кода
7 подходов к тестированию
Мы упростили сайт до приложения с действием в один клик и провалились
8 правил, которые помогут вам спроектировать лучший дизайн карточки
Возврат скевоморфизма
14 популярных программ для создания анимации, прототипирования и дизайна интерфейсов
Полгода ежемесячного создания игр
Простой игровой движок с Flutter Animations
47 ключевых уроков для UI и UX дизайнеров
Возглавляя команду разработчиков программного обеспечения
20 лучших идей для дизайна пользовательского интерфейса
Мой опыт создания приложения с no-code инструментами
Давайте сделаем мобильную многопользовательскую игру на Unity
Делаем музыкальный плеер, играющий в фоновом режиме, на Flutter
7 инструментов для удаленных команд 2020
GetStorage: быстрое key-value хранилище
Fluent System Icons: мобильные иконки от Microsoft

Аналитика, маркетинг и монетизация

(+6)Как работать с Google Analytics и Яндекс Метрикой?
(+6)Сколько стоит сделать ролик об игре своими силами
(+3)Материалы с митапа для аналитиков: модель роста, A/B-тесты, управление стоком и доставкой товаров
Яндекс дарит подключившимся к РСЯ до 600,000 рублей на продвижение приложений
Рейтинг доступности банковских приложений 2020 от UsabilityLab
Расходы на мобильные приложения в 1 половине 2020 выросли на 23.4%
TikTok заподозрили в шпионаже
В Индии забанили TikTok и десятки других китайских приложений
Сигнал от звёзд: Газпром-медиа запустил приложение с персональным гороскопом
Как работать с восточными языками в App Store и Google Play

AI, Устройства, IoT

(+8)9 ключевых алгоритмов машинного обучения простым языком
(+1)Опыт создания облачного решения по мониторингу цифрового киоска на Azure IoT Central
Niantic делает AR-проект по настольной игре Колонизаторы
Машинное обучение поможет спасать людей на пляжах

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

Simctl управление симуляторами Apple через терминал

15.06.2020 00:20:27 | Автор: admin
Всем привет! Меня зовут Паша Лесюк, я работаю мобильным тестировщиком в компании Циан. В этой статье я расскажу о возможностях управления яблочными симуляторами из командной строки.


simctl утилита командной строки для взаимодействия с симуляторами. Она очень похожа на ADB для Android, устанавливается вместе со средой разработки Xcode и используется вместе с xcrun (Xcode-раннер командной строки). Двоичный файл программы можно найти по пути:

/Applications/Xcode.app/Contents/Developer/usr/bin/simctl

Содержание
Работа с объектами устройств
Просмотр списка доступных команд и информации по ним
Просмотр списка доступных устройств, сред выполнения, устройств и пар устройств
Создание нового устройства
Запуск устройства
Апгрейд устройства
Клонирование устройства
Очистка данных и настроек устройства
Переименование устройства
Вывод переменных среды устройства
Проверка состояния загрузки устройства
Выключение устройства
Удаление устройств

Работа с контентом устройств
Снятие скриншота и видео с устройства
Добавление медиа на устройство
Открытие URL на устройстве
Управление сертификатами устройства
Установка приложения на устройство
Запуск приложения на устройстве
Предоставление, отзыв и сброс разрешений приложения
Отображение информации о приложении
Отображение пути к контейнерам установленного приложения
Закрытие приложения на устройстве
Удаление приложения с устройства
Симуляция отправки пуш-уведомления
Изменение и очистка статус-бара устройства
Установка темной или светлой темы

Работа с логами и внутренними механизмами устройств
Выполнение указанной операции на устройстве
Включение и отключение подробного логирования на устройстве
Отображение логов с устройства
Сбор диагностической информации и логов

Заключение
Включение и отключение полноэкранного режима окна симулятора
Включение и отключение отображения нажатий на устройстве

Работа с объектами устройств


Просмотр списка доступных команд и информации по ним


Команда simctl без подкоманд выводит в консоль список всех доступных подкоманд (далее будет использоваться термин команда).

$ xcrun simctl

Для просмотра подробной информации по команде нужно использовать команду help с названием искомой команды.

$ xcrun simctl help list

Просмотр списка доступных устройств, сред выполнения, устройств и пар устройств


Команда list выводит список всех установленных устройств и окружений. Рядом с каждым устройством будет отображен UDID (уникальный идентификатор устройства), который можно прокидывать в некоторые подкоманды simctl.

$ xcrun simctl list

Пример UDID:

4599F586-F482-4E9C-92A7-8AC4EF348BD9

Список можно фильтровать по заголовкам: devices, devicetypes, runtimes, pairs.

$ xcrun simctl list devices== Devices ==-- iOS 11.0 --    iPhone 7 (422566D6-AD4C-40E5-AC64-233043A00814) (Shutdown)-- iOS 13.4 --    iPhone 8 (CB87B315-F01A-41AA-9C85-6FE24E5A66B9) (Shutdown)

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

$ xcrun simctl list devices available

Примером поискового запроса может служить выборка по устройствам c экраном 12.9 дюймов.

$ xcrun simctl list devicetypes 12.9== Device Types ==iPad Pro (12.9-inch) (com.apple.CoreSimulator.SimDeviceType.iPad-Pro)iPad Pro (12.9-inch) (2nd generation) (com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---2nd-generation-)iPad Pro (12.9-inch) (3rd generation) (com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---3rd-generation-)iPad Pro (12.9-inch) (4th generation) (com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---4th-generation-)

Можно вывести более подробную информацию списка c помощью параметра -v.

$ xcrun simctl list -v devices== Devices ==-- iOS 11.0 (15A8401) [/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 11.0.simruntime] --    iPhone 7 (7B68E927-161C-440C-AABE-654CD96E8694) (Shutdown)-- iOS 13.3 (17C45) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime] --    iPhone 8 (F3909F6E-E227-4BD7-939F-D3D05B1B8AAD) (Shutdown)

Можно вывести информацию в формате JSON с помощью параметра -j или --json.

$ xcrun simctl list -j -v devices{    "devices" : {    "com.apple.CoreSimulator.SimRuntime.iOS-13-3" : [        {        "dataPath" : "\/Users\/pavel\/Library\/Developer\/CoreSimulator\/Devices\/9EA47EEB-F19F-44EE-9854-EA06BEB8FBD1\/data",        "logPath" : "\/Users\/pavel\/Library\/Logs\/CoreSimulator\/9EA47EEB-F19F-44EE-9854-EA06BEB8FBD1",        "udid" : "9EA47EEB-F19F-44EE-9854-EA06BEB8FBD1",        "isAvailable" : true,        "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-7-Plus",        "state" : "Shutdown",        "name" : "iPhone-7-Plus"        },        {        "dataPath" : "\/Users\/pavel\/Library\/Developer\/CoreSimulator\/Devices\/F3909F6E-E227-4BD7-939F-D3D05B1B8AAD\/data",        "logPath" : "\/Users\/pavel\/Library\/Logs\/CoreSimulator\/F3909F6E-E227-4BD7-939F-D3D05B1B8AAD",        "udid" : "F3909F6E-E227-4BD7-939F-D3D05B1B8AAD",        "isAvailable" : true,        "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8",        "state" : "Shutdown",        "name" : "iPhone 8"        }    ],    ...

Создание нового устройства


Для создания нового симулятора используется команда create, после которой указываются имя устройства, его тип и среда (эти данные есть в выводе команды list). После выполнения команды отобразится UDID созданного симулятора.

$ xcrun simctl create iPhone-7-Plus com.apple.CoreSimulator.SimDeviceType.iPhone-7-Plus com.apple.CoreSimulator.SimRuntime.iOS-13-49EA47EEB-F19F-44EE-9854-EA06BEB8FBD1

Запуск устройства


Команда boot запускает устройство с указанным UDID, делая его доступным для взаимодействия.

$ xcrun simctl boot CB87B315-F01A-41AA-9C85-6FE24E5A66B9

После запуска устройства можно передавать команду booted вместо UDID. Если запущено несколько устройств, то simctl выберет одно из них.

Чтобы увидеть симулятор в действии нужно запустить приложение Simulator.

$ open /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/

Апгрейд устройства


Команда upgrade позволяет повысить версию среды устройства до необходимой.

$ xcrun simctl upgrade 422566D6-AD4C-40E5-AC64-233043A00814 com.apple.CoreSimulator.SimRuntime.iOS-13-4

Клонирование устройства


Команда clone позволяет клонировать существующее устройство, копируя его тип и среду.

$ xcrun simctl clone booted NewPhone

Очистка данных и настроек устройства


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

$ xcrun simctl erase booted

Если нужно сбросить данные всех симуляторов, то нужно использовать команду erase all.

$ xcrun simctl erase all

Переименование устройства


Изменить имя устройства можно с помощью команды rename.

$ xcrun simctl rename booted MyiPhone

Вывод переменных среды устройства


Команда getenv позволяет выводить значения переменных среды устройства. Например, встроенная переменная симулятора SIMULATOR_SHARED_RESOURCES_DIRECTORY указывает на путь, где хранятся данные симулятора.

$ xcrun simctl getenv booted SIMULATOR_SHARED_RESOURCES_DIRECTORY/Users/pavel/Library/Developer/CoreSimulator/Devices/F3909F6E-E227-4BD7-939F-D3D05B1B8AAD/data

Проверка состояния загрузки устройства


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

  • -b загружает указанный симулятор, если он не загружен.
  • -d отображает информацию о миграции данных.
  • -c постоянно отслеживает состояние загрузки и выключения.

$ xcrun simctl bootstatus booted$ xcrun simctl bootstatus booted -c

Выключение устройства


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

$ xcrun simctl shutdown booted

Для выключения всех симуляторов используется команда shutdown all.

$ xcrun simctl shutdown all

Удаление устройств


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

$ xcrun simctl delete unavailable

Для удаления конкретного симулятора используется команда delete.

$ xcrun simctl delete booted

Для удаления всех устройств используется команда delete all.

$ xcrun simctl delete all

Работа с контентом устройств


Снятие скриншота и видео с устройства


Можно просто использовать шорткат 4, навести курсором на окно симулятора, нажать пробел и сделать скриншот. Но тогда на скриншоте кроме самого экрана будут видны рамки симулятора.
Для снятия скриншота только экрана используется команда io в связке со screenshot. Можно сохранять изображения в формате .png, .tiff, .bmp, .gif и .jpeg.

$ xcrun simctl io booted screenshot ~/Pictures/app-screenshot.png


Аналогично, можно просто использовать шорткат S в открытом приложении Simulator, скриншот окна сохранится на рабочем столе.

Также можно использовать команду io в связке с recordVideo для записи видео взаимодействия с экраном симулятора. Можно сохранять видео в формате .mov, .h264, .mp4 и .fmp4.

$ xcrun simctl io booted recordVideo ~/Movies/app-preview.mp4

Для завершения записи нужно нажать C в окне терминала.

Добавление медиа на устройство


Команда addmedia используется для добавления фото или видео на симулятор.

$ xcrun simctl addmedia booted ~/Pictures/test.png$ xcrun simctl addmedia booted ~/Pictures/test.gif$ xcrun simctl addmedia booted ~/Pictures/test.mp4

Также можно просто перетащить файл из Finder в окно симулятора.

Открытие URL на устройстве


Команда openurl открывает указанный URL на симуляторе.

$ xcrun simctl openurl booted "https://www.google.com/"

Также может использоваться кастомная схема, ассоциированная с нативным приложением.

$ xcrun simctl openurl booted maps://

Управление сертификатами устройства


Команда keychain позволяет добавлять сертификаты в доверенное корневое хранилище или keychain, а также сбрасывать keychain.

$ xcrun simctl keychain booted add-root-cert ~/my-selfsigned.cer$ xcrun simctl keychain booted add-cert ~/my-selfsigned.cer$ xcrun simctl keychain booted reset

Установка приложения на устройство


Можно легко установить приложение на симулятор если известен путь к файлу .app. Для этого используется команда install.

$ xcrun simctl install booted ~/Циан.app

Также можно просто перетащить приложение из Finder в окно симулятора.

Перечень opensource-приложений для iOS можно найти тут.

Запуск приложения на устройстве


Запуск приложения осуществляется с помощью команды launch и указанием bundle ID.

$ xcrun simctl launch booted ru.cian.mobile

Как узнать bundle ID описано тут.

Предоставление, отзыв и сброс разрешений приложения


Команда privacy может предоставлять, отзывать и сбрасывать разрешения приложения.

$ xcrun simctl privacy <device> <action> <service> <bundle ID>

Значения action (действия) могут быть следующие:

  • grant предоставляет доступ к сервису. Неободим bundle ID.
  • revoke запрещает доступ к сервису. Необходим bundle ID.
  • reset сбрасывает доступ к сервису. bundle ID опционален.

Значения service (службы) могут быть следующие:

  • all применяет действие ко всем службам.
  • calendar предоставляеть доступ к календарю.
  • contacts-limited предоставляет доступ к основной контактной информации.
  • contacts предоставляет полный доступ к контактной информации.
  • location предоставляет доступ к службам определения местоположения при использовании приложения.
  • location-always предоставляет доступ к службам определения местоположения в любое время.
  • photos-add предоставляет доступ на добавление фотографий в библиотеку фотографий.
  • photos предоставляет полный доступ к библиотеке фотографий.
  • media-library предоставляет доступ к медиа-библиотеке.
  • microphone предоставляет доступ к микрофону.
  • motion предоставляет доступ к фитнес-данным.
  • reminders предоставляет доступ к напоминаниям.
  • siri предоставляет возможность использовать приложение вместе с Siri.

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

$ xcrun simctl privacy booted grant photos ru.cian.mobile$ xcrun simctl privacy booted grant location ru.cian.mobile$ xcrun simctl privacy booted revoke all ru.cian.mobile

На данный момент не все сервисы доступны для настройки. Отсутствуют уведомления, здоровье, Bluetooth и Face ID.

Отображение информации о приложении


Команда appinfo выводит информацию о приложении.

$ xcrun simctl appinfo booted ru.cian.mobile

Формат отображения информации следующий:

{    ApplicationType = User;    Bundle = <PATH_TO_APP_FILE>;    BundleContainer = <PATH_TO_APP_FILE_FOLDER>;    CFBundleDisplayName = "Циан";    CFBundleExecutable = "Циан";    CFBundleIdentifier = "ru.cian.mobile";    CFBundleName = "Циан";    CFBundleVersion = 1;    DataContainer = <PATH_TO_DATA_FOLDER>;    GroupContainers =     {        <GROUP_CONTAINER_NAME> = <PATH_TO_GROUP_CONTAINER_FOLDER>;    };    Path = <PATH_TO_APP_FILE>;    SBAppTags =     (    );}

Отображение пути к контейнерам установленного приложения


Можно вывести путь к контейнерам приложения через команду get_app_container с указанием bundle ID. У команды есть несколько опций для указания типа контейнера:

  • app указывает на расположение самого приложения и используется по умолчанию.

$ xcrun simctl get_app_container booted ru.cian.mobile$ xcrun simctl get_app_container booted ru.cian.mobile app

  • data указывает на расположение данных приложения.

$ xcrun simctl get_app_container booted ru.cian.mobile data

  • groups указывает на расположение группы приложений. Если групп несколько, то нужно использовать имя искомой группы.

$ xcrun simctl get_app_container booted ru.cian.mobile groups$ xcrun simctl get_app_container booted ru.cian.mobile group.ru.cian.mobile.widget

Закрытие приложения на устройстве


Закрытие приложения осуществляется с помощью команды terminate и указанием bundle ID.

$ xcrun simctl launch terminate ru.cian.mobile

Удаление приложения с устройства


Можно удалить приложение с симулятора с помощью команды uninstall, используя bundle ID.

$ xcrun simctl uninstall booted ru.cian.mobile

Симуляция отправки пуш-уведомления


Для отправки пуша нужно подготовить файл, который должен содержать структуру в формате JSON и сохранить его с расширением .apns (Apple Push Notification service):

{    "aps": {        "alert": {            "title": "Tester on Steroids",            "body": "About mobile apps testing"        },        "badge": 3,        "sound": "default"    }}

Затем нужно дать разрешение на отправку приложению уведомлений. После этого нужно выполнить команду push с указанием устройства, bundle ID и пути до файла .apns.

$ xcrun simctl push booted ru.cian.mobile ~/Documents/mocks/push.apns

Если добавить в файл параметр "Simulator Target Bundle", то необходимость указывать каждый раз bundle ID отпадает.

{    "aps": {        "alert": {            "title": "Tester on Steroids",            "body": "About mobile apps testing"        },        "badge": 3,        "sound": "default"    },    "Simulator Target Bundle": "ru.cian.mobile"}

$ xcrun simctl push booted ~/Documents/mocks/push.apns

Также, если указан параметр "Simulator Target Bundle", то файл можно просто перетащить в окно симулятора.

Изменение и очистка статус-бара устройства


У команды status_bar есть три подкоманды:

  • override принимает параметры для элементов статус-бара и меняет их в зависимости от значений.

$ xcrun simctl status_bar booted override --time 10:30 --dataNetwork wifi --wifiMode active --wifiBars 2 --cellularMode active --cellularBars 3 --operatorName @tester_on_steroids --batteryState charging --batteryLevel 75

  • list выводит значения перезаписанных параметров.

$ simctl status_bar booted listCurrent Status Bar Overrides:=============================Time: 10:30DataNetworkType: 1Cell Mode: 3, Cell Bars: 3Operator Name: @tester_on_steroidsBattery State: 1, Battery Level: 75, Not Charging: 1

  • clear очищает перезаписанный статуc-бар.

$ simctl status_bar booted clear

Установка темной или светлой темы


Данная опция доступна для симуляторов с iOS от 13.0 и выше. C помощью команды ui appearance можно поменять тему устройства на темную или светлую.

$ xcrun simctl ui booted appearance dark$ xcrun simctl ui booted appearance light

Работа с логами и внутренними механизмами устройств


Выполнение указанной операции на устройстве


Команда spawn создает указанный процесс на симуляторе.

$ xcrun simctl spawn booted defaults write ru.cian.mobile ResetDatabase -bool YES

Здесь используется интерфейс defaults, в котором флагу ResetDatabase устанавливается значение YES. Это удобный способ менять пользовательские настройки до запуска приложения.

Включение и отключение подробного логирования на устройстве


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

$ simctl logverbose booted enable$ simctl logverbose booted disable

Отображение логов с устройства


В iOS существует пять уровней логов:

  1. Default. Используется для сбора информации о вещах, которые могут привести к сбою.
  2. Info. Полезная, но необязательная информация для устранения ошибок.
  3. Debug. Информация, которая может быть полезна во время разработки или устранения конкретной проблемы. Отслеживание debug-логов предназначено для использования на стадии разработки, а не на стадии эксплуатации программы конечными пользователями.
  4. Error. Используется для сбора информации об ошибках процесса.
  5. Fault. Используется для сбора информации об ошибках системного уровня или мульти-процессов.

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

$ xcrun simctl spawn booted log stream

По умолчанию вывод логов будет включать в себя уровни default, error и fault. Для использования уровней info и debug нужно использовать аргумент --level. --level=info будет отображать уровни по умолчанию, а так же info. --level=debug будет отображать уровни по умолчанию, а так же info и debug.

$ xcrun simctl spawn booted log stream --level=info$ xcrun simctl spawn booted log stream --level=debug

Также можно фильтровать логи. Например, по конкретному приложению или по типу ивентов и сообщений.

$ xcrun simctl spawn booted log stream --predicate 'processImagePath endswith "Циан"'$ xcrun simctl spawn booted log stream --predicate 'eventMessage contains "error" and messageType == info'

Чтобы выключить вывод логов нужно нажать C в окне терминала.

C помощью операции log collect можно сделать дамп журнала логов.

$ xcrun simctl spawn booted log collect

Сбор диагностической информации и логов


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

$ xcrun simctl diagnose

Журнал будет включать в себя:

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

В журнале будет содержаться конфиденциальная информация, поэтому с этими данными нужно быть осторожным.

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

Заключение


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

Включение и отключение полноэкранного режима окна симулятора


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

defaults write com.apple.iphonesimulator AllowFullscreenMode 1defaults write com.apple.iphonesimulator AllowFullscreenMode 0

Включение и отключение отображения нажатий на устройстве


Отображает тапы на симуляторе. Удобно при записи видео.

defaults write com.apple.iphonesimulator ShowSingleTouches 1defaults write com.apple.iphonesimulator ShowSingleTouches 0



P.S.
Если вам интересна тема тестирования, то приглашаю вас подписаться на мой блог в телеграме.
Подробнее..

SwiftUI по полочкам Анимация, часть 2

16.06.2020 00:11:35 | Автор: admin
image

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

image

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

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

Вот так выглядит анимация в готовом виде:

image

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

return p.applying(.init(translationX: 0, y: height * self.phase))

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



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

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

Работа с градиентами


В самом начале, я выбрал другой подход. Я посчитал, что намного полезнее разобраться с анимацией смещения объектов, чем с анимацией форм (я по-прежнему называю shape-структуру формой). Смещение может работать с чем угодно различными фигурами, изображениями, другими View. В качестве учебной задачи, я захотел реализовать сглаженный цветовой переход от одной волны к другой. В оригинальном концепте что-то такое вроде бы такое было.

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



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



Линейный градиент характеризуется точкой начала и окончания градиента. Это не CGPoint точки с абсолютными координатами (x:y:), а UnitPoint точки, т.е. относительными координатами, где x:y: задаются в долях от ширины и высоты области, выделенной под данную View. Также есть предопределенные точки, соответствующие углам(.topLeading, .bottomTrailing и т.д.) и серединам сторон (.top, .trailing и т.д.).

Rectangle()    .fill(LinearGradient(         gradient: Gradient(stops: [             .init(color: self.end, location: 0),             .init(color: self.middle, location: 1 - self.middleGradientStop),             .init(color: self.start, location: 1)]),          startPoint: .leading,          endPoint: .trailing))    .frame(width: self.gradientLength)

Сегмент с линейным градиентом резиновый. Он имеет фиксированную ширину, но его высота не указана. Таким образом он заполнит весь предоставленный объем.

Чуть сложнее история с круговым градиентом. Мы указываем точку центр окружности, радиус начала градиента и радиус окончания градиента. Радиусы уже в абсолютном выражении, т.е. поинтах.
Центр окружности все еще задается в виде UnitPoint.

Rectangle()     .fill(RadialGradient(          gradient: Gradient(stops: [              .init(color: self.start, location: 0),              .init(color: self.middle, location: self.middleGradientStop),              .init(color: self.end, location: 1)]),          center: .bottomTrailing,          startRadius: self.topRadius,          endRadius: self.topRadius + self.gradientLength)                )    .frame(height: self.topRadius)

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

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

       Rectangle()      .fill(AngularGradient(            gradient: Gradient(stops: [                  .init(color: self.end, location: 0),                  .init(color: self.middle, location: 1 - self.angularGradientMiddleStop(blockWidth: geometry.size.width)),                  .init(color: self.start, location: 1)]),            center: .bottomLeading,            startAngle: self.directionTo(gradientPart: self.start, blockWidth: geometry.size.width),            endAngle: Angle(degrees: 360)))...   func directionTo(gradientPart: Color, blockWidth: CGFloat) -> Angle{        let angleOf = gradientAngles(blockWidth: blockWidth)        var angle = Angle.zero        switch gradientPart{            case start: angle = angleOf.start            case middle: angle = angleOf.middle            case end: angle = angleOf.end            default: fatalError("there is no gradient stop with that color: \(gradientPart)")        }        return angle    }    func gradientAngles(blockWidth: CGFloat) -> (start: Angle, middle: Angle, end: Angle){        let blockHeight = self.bottomRadius        let center = CGPoint(x: 0, y: blockHeight)        let topRight = CGPoint(x: blockWidth, y: blockHeight - self.gradientLength)        let topGradientStarts = CGPoint(x: blockWidth, y: blockHeight - self.gradientLength * (1 - self.middleGradientStop))        let startAngle = center.radialDirection(to: topRight)        let middleAngle = center.radialDirection(to: topGradientStarts)        let endAngle = Angle(degrees: 360)        return (start: startAngle, middle: middleAngle, end: endAngle)    }...extension CGPoint{    func radialDirection(to point: CGPoint) -> Angle{        let deltaX =  point.x - self.x        let deltaY =  point.y - self.y        var angle = Angle(degrees: 0)        if deltaX == 0{            if deltaY > 0{                angle = Angle(degrees: 90)            }else{                angle = Angle(degrees: 270)            }        }else if deltaY == 0{            if deltaX > 0{                angle = Angle(degrees: 0)            }else{                angle = Angle(degrees: 180)            }        }else if deltaX > 0 && deltaY > 0{                angle = Angle(radians: atan(Double(deltaY / deltaX)))        }else if deltaX > 0 && deltaY < 0{                angle = Angle(degrees: 270) + Angle(radians: atan(Double(deltaX / -deltaY)))        }else if deltaX < 0 && deltaY > 0{                angle = Angle(degrees: 90) + Angle(radians: atan(Double(-deltaX / deltaY)))        }else if deltaX < 0 && deltaY < 0{                angle = Angle(degrees: 180) + Angle(radians: atan(Double(deltaY / deltaX)))        }        return angle    }}

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

Все три варианта используют единое описание цветов внутри градиента. Это простое перечисление цветов в массиве. Их распределение будет равномерным на заданном отрезке. Можно уточнить распределение, используя массив элементов Stop. Фактически, это то же перечисление цветов, только для каждого из них указывается еще и координата на единичном отрезке, где он будет расположен.

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

Color это не то чем кажется


Есть один момент связанный с Color. Для создания эффекта накатывающей волны, я решил сделать переход между цветами не совсем линейным. Если присмотреться, можно заметить, что у самой кромки градиент резче, а чем дальше от кромки, тем плавнее он перетекает в основной цвет. Я добился этого вычислением промежуточного цвета, и созданием градиента с таким распределением:
начальный цвет 0
промежуточный цвет 0.3
конечный цвет 1

Проблема возникла с вычислением среднего двух цветов. SwiftUI подразумевает использование объекта Color для градиентов, а я и повелся. На самом деле, если вы хотите работать с цветами именно как с RGB-объектами, закладывайте изначально в свою модель использование UIColor, потому что в Color нет доступа непосредственно к цвету. Обратно в UIColor его тоже так просто не конвертируешь. Единственное (не)адекватное решение, которое я нашел вот тут подразумевает получение Mirror reflection с разбором его строкового представления. Такой себе бойлерплейт, но других вариантов пока нет.

И это не ошибка, не упущение. Смысл в том, что SwiftUI в объекте Color не дублирует функционал UIColor. Если вам нужна работа с rgb каналами используйте именно его. Color в SwiftUI это View имеющая некоторый базовый цвет, конкретное значение которого может несколько изменяться в зависимости от расположения звезд на небе конкретный rgb цвет определяется только в момент отрисовки на экране. В документации сказано
SwiftUI only resolves it to a concrete value just before using it in a given environment.
, но что имеется в виду под environment: цвет стенки за спиной пользователя, или тема оформления IOS непонятно. Если вам это не подходит, используйте UIColor изначально.

Бесконечная анимация


Анимация указывается модификатором .animation() и запускается после инициализации View в модификаторе .onAppear() путем изменения @State переменной. В результате, модификатор .rotationEffect() подписывается на получение animatableData в промежутке от было к стало, согласованных с системным таймером. Мы говорили об этом в прошлой части.

struct AnimatedRectObservedObject: View{    @State var angle: Double = 0    var body: some View{       return VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false))            Spacer()       }       .onAppear{            self.angle += 90        }    }}



Запуская бесконечную анимацию таким образом, вы должны понимать, что любое последующее изменение анимированных модификаторов ее сломает. Например, если после запуска, вы по какой-то причине измените значение angle на 0, получите вот такой эффект:



Почему? Ответ в механизме анимации перехода от одной анимации к другой. В нашем примере, мы при первом появлении View на экране запустили анимированный переход от 0 к 90. View на самом деле хранит в себе только конечное значение 90, а исходное значение 0 вообще нигде не хранится. Механизм анимации знает текущее положение во времени анимации, и текущее значение AnimatableData. В точке времени 0.5 оно будет 45. Что произойдет, если в этот момент пользователь изменит значение на 0? Ответ: начнется анимация изменения значения с 45 до 0. Все так же зацикленная. Вот только визуально, цикл получается не замкнутым, а разорванным.

Кроме того, есть случаи, когда анимация так же ломается если в вашей View используется @ObservedObject, или иные параметры, вызывающие повторную отрисовку View. Для решения этой проблемы, у модификатора .animation есть параметр .animation(: value:). Передавая туда значение, мы указываем рендеру, что рестарт анимации нужен только при изменении этого значения.

Однако, давайте разбираться, как же остановить анимацию, если она нам больше не требуется. Для этого, нужно обеспечить две вещи. Очевидно, что мы должны сообщить SwiftUI что бесконечная анимация более не нужна, заменив ее обычной, конечной. Но этого недостаточно. Модификатор .animation() представляет собой инструкцию, какие тайминги нужно использовать, но само вращение описано внутри модификатора .rotationEffect(), и он уже подписан на получение новых значений угла поворота по таймеру. Для остановки вращения нам потребуется изменить еще и его значение.

struct AnimatedRectStopButton: View{    @State var angle: Double = 0    @State var animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    var body: some View{        VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(animation)            Spacer()            Text("toggle Animation").onTapGesture {                if self.angle == 90{                    self.angle = 0                    self.animation = .default                }else{                    self.angle = 90                    self.animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)                }            }        }        .onAppear{            self.angle = 90        }    }}



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

То же самое можно написать чуть более лаконично, если подвесить весь функционал на одну @Stateпеременную типа вкл/выкл:

struct AnimatedRectStopButton: View{    @State var isStarted = false    let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    var body: some View{        VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: isStarted ? 90 : 0))                .animation(isStarted ? animation : .default)            Spacer()            Text("toggle Animation").onTapGesture {                self.isStarted.toggle()            }        }        .onAppear{            self.isStarted.toggle()        }    }}

До этого момента все было относительно легко. Но обратите внимание, что все события здесь генерируются внутри View. .onAppear{} вызывается системой, а onTapGesture{}, понятно пользователем. Однако, как быть, если вы хотите инкапсулировать всю анимацию внутри одной View, передавая в нее лишь вкл/выкл? SwiftUI не предполагает возможности из родительской view каких-то методов дочерних. Теоретически, вы можете хранить дочернюю View как структуру, и вызывать ее mutating-методы, но вот @State переменные дочерних View таким образом поменять не получится, я пробовал не работает. Единственный способ сделать что-то подобное, это воспользоваться PassthroughSubject из Combine, как это и сделали в упомянутой статье.

На самом деле все намного проще. Если четко уложить по полочкам в голове, что @State это внутреннее состояние View, и не пытаться манипулировать им извне, то правильное решение окажется очень простым:

struct AnimatedRectStopButtonFromOutside: View{    var isStarted: Bool    let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    var body: some View{        VStack{            Spacer()            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: isStarted ? 90 : 0))                .animation(isStarted ? animation : .default)            Spacer()        }    }}struct AnimatedRectParentView: View{    @State var isOn = false    var body: some View{        VStack{            AnimatedRectStopButtonFromOutside(isStarted: isOn)            Text("toggle Animation").onTapGesture {                self.isOn.toggle()            }        }.onAppear(){            self.isOn = true        }    }}

Нам не нужна в данном случае @State подписка на обновление View внутри дочерней view она и так обновляется целиком при изменении внешнего для нее параметра. Иногда это не удобно. Иногда мы не хотели бы лишний раз инициализировать дочернюю view например, чтобы не сломать анимацию, или в init() происходят какие-то сложные и ресурсоемкие вычисления (запросы например). В этих случаях лучше пользоваться объектными сущностями, за изменениями которых можно следить с помощью модификатора onReceive.

Modifying state during view update


Есть еще с одним момент, достойный освещения. Мы оперировали только двумя значениями 0 и 90 градусов. Выключение анимации приводило к сбросу угла на 0. Но можно ли поставить анимацию на паузу прямо в тот момент, когда мы нажали кнопку, и снова ее продолжить с того же места при возобновлении? Давайте рассмотрим код, позволяющий это:

struct AnimatedRect: View{    @State var startTime: Date = Date()    @State var angle: Double = 0    @State var internalStarted: Bool    let externalStarted = true    var animation: Animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    init(started externalStarted: Bool){        self.externalStarted = externalStarted        self._internalStarted = State(initialValue: !externalStarted)//forse to start animation    }    var body: some View{        //thats wrong. It just hiding a problem from SwiftUI not solving it        DispatchQueue.main.async {            if self.internalStarted && self.externalStarted == false{                // print("stop animation")                 let timePassed = Date().timeIntervalSince(self.startTime)                 let fullSecondsPassed = Double(Int(timePassed))                 let currentStage = timePassed - fullSecondsPassed                 self.internalStarted = false                 let newAngle = self.angle - 90 + currentStage * 90                 self.angle = newAngle            }else if self.internalStarted == false && self.externalStarted {             //    print("start animation")                 self.startTime = Date()                 self.internalStarted = true                 self.angle += 90             }       }                return  VStack{            Rectangle()                .fill(Color.red)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(internalStarted ? animation : .default)            Spacer()        }    }}



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

Для работы такого подхода, нам нужно отследить момент старта и окончания анимации. Init() не подойдет. Мы не можем обращаться к уже существующим @State параметрам для получения предыдущего времени старта анимации. Это фишка @State переменных. Внутри init вы можете установить лишь начальное состояние этой переменной, но по окончании инициализации, если View уже существовала до init(), значение @State переменных будет восстановлено.

Поэтому тут реализована параметр let externalStarted (которым мы управляем извне), и внутренний параметр @State var internalStarted, с помощью которого мы управляем непосредственно анимацией.

Не хватало только одного какого-то модификатора, который бы проверял их соответствие и обновлял при необходимости, наподобие .onRecieve(), только чтобы отрабатывал при каждой отрисовке. И тут я подумал ведь body и так вызывается для каждой отрисовке, почему бы прямо в нем не делать эту проверку?

Оказалось, что SwiftUI очень ругается, если в процессе отрисовки View менять значение @State переменных, выдает
Modifying state during view update, this will cause undefined behavior.
и блокирует такое изменение. Тогда я пошел на грязный хак, и использовал DispatchQueue.main.async. Но давайте разберемся, почему же это грязный хак, и почему так делать не следует никогда?

На самом деле, проблема вот в чем. Если мы напишем внутри body какую-то очевидную глупость вроде i += 1, где i это какая-то @State переменная, то мы получим бесконечный цикл. В момент рендера мы делаем View в памяти не актуальной ведь мы изменили исходные данные для отрисовки. Значит, сразу по окончании отрисовки наша View попадет в очередь на повторный рендер, но и тогда мы тут же снова сделаем ее неактуальной. Мы своими руками создаем бесконечный цикл. Асинхронный вызов в данном случае вообще ничего не меняет. Он лишь немного сдвигает инициирование очередного витка на момент сразу после рендера. Таким образом, асинхронный вызов не решает проблему, а лишь маскирует ее, не давая SwiftUI ткнуть нас в нее носом. Это как в автомобиле лампочку check engine на приборке обрывать.

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

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

struct AnimatedRectObservedObject: View{    @State var startTime: Date = Date()    @State var angle: Double = 0    @ObservedObject var animationHandler: AnimationHandlerTest    let animation: Animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    init(animationHandler: AnimationHandlerTest){        self.animationHandler = animationHandler    }    var body: some View{        VStack{            Rectangle()                .fill(Color.green)                .frame(width: 200, height: 200)                .rotationEffect(Angle(degrees: angle))                .animation(animationHandler.isStarted ? animation : .default)       }.onReceive(animationHandler.objectWillChange){            let newValue = self.animationHandler.isStarted            if newValue == false{                 let timePassed = Date().timeIntervalSince(self.startTime)                 let fullSecondsPassed = Double(Int(timePassed))                 let currentStage = timePassed - fullSecondsPassed                 let newAngle = self.angle - 90 + currentStage * 90                withAnimation(.none){//not working:(((                 self.angle = newAngle                }            }else {                 self.startTime = Date()                 self.angle += 90             }        }       .onAppear{            self.angle += 90            self.startTime = Date()        }    }}

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



Анимация изменения анимации


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

.animation(animationHandler.isStarted ? animation : .default)

Это ведет к необычному поведению. Несмотря на относительную простоту всего того, что я описываю, под капотом остается куча нюансов по взаимодействию, чтобы картинка выглядела плавно всегда, даже если безалаберный программист чего-то не указал. Поэтому дефолтная анимация это spring. Она больше всего подходит под имитацию законов физики. Причем это довольно короткая анимация, порядка 0.2 секунды. Если анимация перехода в новое состояние (вычисленный и указанный нами угло) не успевает красиво уложиться в дефолтные 0.2 секунды так, чтобы перейти от текущей скорости анимации с красивым замедлением до полной остановки, SwiftUI сама добавит еще один цикл бесконечной анимации, проигранный в ускоренном варианте.



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

                .animation(animationHandler.isStarted ? animation : Animation.linear(duration: 0))

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

Движение нескольких волн одной анимацией


Вернемся к радужной анимации. Итак, у нас есть сами волны, давайте научим их двигаться. Первое что я хотел бы сделать это запустить бесконечную анимацию. Я решил сделать следующим образом: у меня будет одна @State переменная, отвечающая за текущее положение анимации. При состоянии 0 первая волна будет в самом начале, а последняя в самом конце. Сами волны будут накладываться друг на друга. Таким образом, длина видимой части каждой волны будет зависеть от количества волн. Я реализовал это с помощью ZStack, в котором перечислены все волны, и собственным модификатором .wavePosition, внутри которого я вычисляю текущее положение каждой волны в данный момент анимации, и порядок их наложения друг на друга.

много кода
struct SharpWavePosition: AnimatableModifier {    let wave: WaveDescription    let animationHandler: AnimationHandler    var time: CGFloat    var currentPosition: CGFloat    public var animatableData: CGFloat {        get { time}        set {                self.time = newValue            let currentTime = newValue - CGFloat(Int(newValue))            self.currentPosition = SharpWavePosition.calculate(forWave: wave.ind, ofWaves: wave.totalWavesCount, overTime: currentTime)        }    }    init(wave: WaveDescription, time: CGFloat, animationHandler: AnimationHandler){        self.wave = wave        self.time = time        self.animationHandler = animationHandler        self.currentPosition = 0    }        static func calculate(forWave: Int, ofWaves: Int, overTime: CGFloat) -> CGFloat{        let time = overTime - CGFloat(Int(overTime))        let oneWaveWidth = CGFloat(1) / CGFloat(ofWaves)        let initialPosition = oneWaveWidth * CGFloat(forWave)        let currentPosition = initialPosition + time        let fullRounds = Int(currentPosition)        var result = currentPosition - CGFloat(fullRounds)        if fullRounds > 0 && result == 0{            // at the end of the round it should be 1, not 0            result = 1        } //       print("wave \(forWave) in time \(overTime) was at position \(result)")        return result    }    func body(content: Content) -> some View {        let oneWaveWidth = CGFloat(1) / CGFloat(wave.totalWavesCount)        var thisIsFirstWave = false        if currentPosition < oneWaveWidth{            thisIsFirstWave = true        }        return            Group{                content                            .offset(x: -wave.width + currentPosition * (wave.width +  wave.gradientLength),                            //to watch how waves move uncoment this                           // y: CGFloat(self.waveInd * 20))                        y:0)                    .zIndex(-Double(currentPosition))                    .transition(.identity)                    .animation(nil)                if thisIsFirstWave{                    content                        .offset(x: wave.gradientLength, y: 0)                        .zIndex(-2)                        .transition(.identity)                        .animation(nil)                }            }    }}extension View{    func positionOfSharp(wave: WaveDescription, inTime: CGFloat, animationHandler: AnimationHandler) -> some View {        return self.modifier(SharpWavePosition(wave: wave, time: inTime, animationHandler: animationHandler))    }}struct SharpRainbowView: View{    let waves: [SharpGradientBorder]    var animation: Animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)    //@ObservedObject    var animationHandler: AnimationHandler    @State var rainbowPosition: CGFloat = 0    init(animationHandler: AnimationHandler,        backgroundColor: Color = .clear    ){        self.animationHandler = animationHandler        let bottomRadius = animationHandler.waveGeometry.bottomRadius        let topRadius = animationHandler.waveGeometry.topRadius        let gradientLength = animationHandler.waveGeometry.gradientLength        let rainbowColors = animationHandler.rainbowColors        guard var lastColor = rainbowColors.last else {fatalError("no colors to display in rainbow")}        var allWaves = [SharpGradientBorder]()        for color in rainbowColors{            let view = SharpGradientBorder(start: color,                                      end: lastColor,                                      bottomRadius: bottomRadius,                                      topRadius: topRadius,                                      gradientLength: gradientLength)            allWaves.append(view)            lastColor = color        }        self.waves = allWaves    }    var body: some View{        GeometryReader{geometry in            VStack{                ZStack{                    ForEach(self.waves.indices, id: \.self){ind in                        self.waves[ind]                            .positionOfSharp(wave: WaveDescription(ind: ind,                                                    totalWavesCount: self.waves.count,                                                    width: geometry.size.width,                                                    baseColor: self.waves[ind].end,                                                    gradientLength: self.waves[ind].bottomRadius + self.waves[ind].topRadius),                                             inTime: self.rainbowPosition,                                             animationHandler: self.animationHandler)                            .animation(self.animationHandler.isStarted ? self.animation : .linear(duration: 0))                    }                }     //           .clipped()            }        }        .onAppear(){            if self.animationHandler.isStarted{                self.rainbowPosition = 1            }        }        .onReceive(animationHandler.objectWillChange){             let newValue = self.animationHandler.isStarted             if newValue == false{                let newPosition = self.animationHandler.currentAnimationPosition                print("animated from \(self.rainbowPosition - 1) to \(self.rainbowPosition) stopped at \(newPosition)")                    self.rainbowPosition = newPosition             }else {                  self.rainbowPosition += 1            }        }    }} 


При первом отображении View на экране, мы меняем состояние с 0 на 1, что с учетом объявленной линейной анимации заставляет волны бесконечно бегать по экрану.



Когда край волны достигнет правого края, следующая ее позиция будет находится в самом начале, за краем экрана. Выглядеть это будет так, как будто из конца волны она просто исчезнет, обнажив фоновую подложку, чего мы хотели бы избежать. Именно для этой цели в модификаторе, в функции body() появился блок if{...}, в котором отрисовывается копия первой волны, пропавшей с экрана.



Обратите внимание, что мне пришлось передавать геометрические характеристики каждой волны в виде параметров модификатора. Проблема была в том, что мне требуется переопределять z-index каждой волны исходя из текущей фазы анимации. Я мог бы извлечь ширину видимой области, использовав внутри модификатора GeometryReader{}, однако столкнулся с тем, что он блокирует изменение порядка наложения волн. Модификатор .zIndex() работает только в контексте первого ZStack{} контейнера вверх по иерархии View.

GeometryReader обрубает эту связь, и zIndex() перестает работать. Если до этого момента вы думали, что GeometryReader это безобидный способ получить данные о размере текущей View это не совсем так.

Анимация потока


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



Решить эту проблему можно вставив модификатор .animation(nil). Таким образом, я отключаю анимацию всех модификаторов, примененных выше по тексту. Это общая логика работы любого модификатора: он меняет то, что у него на входе, а на входе у нас результат работы всех предыдущих модификаторов.

Вообще говоря, здесь работает целая иерархия разных анимаций. Дефолтной считается та, которая будет использована для отображения изменений всех модификаторов, если только вы не указали иное для вашей View. У нас есть поток исполнения (допустим, это main), в рамках которого вызывается body. Будем считать, что у него есть параметр .animation, который проверяется каждый раз, когда какой-то модификатор получает новое значение. Если модификатор поддерживает анимацию (удовлетворяет протоколу AnimatableModifier), и для потока включена анимация (используется какая-то конкретная анимация, а не .none и не nil), то изменение будет анимировано. Именно это мы делаем, заключая какой-то код по изменению @State параметров в блок withAnimation{} прописываем определенную анимацию в текущем потоке, а затем выполняем изменение какой-то @State переменной. В этом случае withAnimation{}, это своего рода эквивалент транзакции, и все изменения выполненные в этой транзакции будут анимированы. В результате, внутри этого же потока запускается цикл трансляции этих изменений во все зависимые View, модификаторы этих View получают новое значение, и подписываются на получение промежуточных значений AnimatableData.

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

Таким образом, withAnimation() это способ запустить анимацию по-умолчанию для выполняемого действия. Но для определенных View вы можете в явном виде указать собственную анимацию с помощью модификатора .animation(). В этом случае, модификаторы и формы, которые составляют контент, передаваемый в .animation() (все что перечислено в коде до применения .animation()) получают описанный вами тайминг, и игнорируют анимацию потока.

В своем модификаторе WavePosition, я использую .animation(nil) для того чтобы избавиться от встроенной анимации offset, дав таким образом указание игнорировать текущую анимацию потока.

Та же история и с концевой заглушкой. Напомню выводы из прошлой статьи. Фактически, у нас в памяти N структур-модификаторов WavePosition, по одной на каждую волну. И все они получают новое значение position по системному таймеру, вычисляя положение каждой волны в данный момент времени. Это значит, что у нас так же N концевых заглушек под каждую волну. Просто в каждый момент времени показывается только один из них, благодаря блоку if{}. Однако, этот же блок подкладывает нам свинью. Исчезновение и появление View также выполняются с анимацией потока. Это значит, что задействуется модификатор .transition для анимации появления и исчезновения View после изменения условия. Обычно, по-умолчанию используется .opacity, однако у меня почему-то вместо этого использовался .slide. Ни то ни другое мне не подходит, потому я просто отменил эту анимацию, используя .transition(.identity).

upd. Пока я готовил статью, вышел XCode 11.4, в котором, похоже, transition по-умолчанию переработали. По крайней мере, сейчас я без проблем закомментировал .transition(.identity) и не получил той проблемы, из-за которой мне пришлось его добавлять.

Тайминги анимации


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

На самом деле Animation это описание скорости данной анимации. Маршрут из пункта А в пункт Б мы можем определить внутри самого модификатора, но SwiftUI сам решает, когда и какое именно значение AnimatableData передать в него. Делает он это с помощью тайминговой кривой. Предположим, пункт А мы возьмем за начало координат, а пункт Б отметим как точку с координатами (1; 1). По горизонтали мы будем отмечать прошедшее время (движение из пункта А в пункт Б занимает ровно 1 единицу времени). По вертикали пройденное расстояние в долях единицы. При использовании линейной анимации, мы получим прямую. Если же мы хотим получить движение хоть чуть-чуть похожее на настоящее, то вначале нам нужно потратить немного времени на разгон, а в конце на торможение. Вот тут можно поиграться с разными вариантами, рисуя свою кривую и сравнивая ее анимацию движения со стандартными.

В SwiftUI, для различных типов анимации используются кривые Безье. В любом случае она должна начинаться в (0;0) и заканчиваться в (1;1). Кривизна линии определяется контрольными точками.

Вложенная тайминговая кривая внутри линейной бесконечной анимации


Так вот, что нам нужно сделать, чтобы добиться ускоренного движения каждой волны (easeIn) внутри линейной зацикленной анимации?

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

Идея простая. Мы сделаем свой класс, в основе которого будет path кривая Bezier с помощью которой мы будем определять расстояние из пункта А в пункт Б, которое нужно показать в каждый определенный момент времени. На самом деле именно так и работает анимация в SwiftUI, да и в любом другом фреймворке с подобным функционалом. С помощью тайминговой кривой, реальные секунды и миллисекунды (которые чаще всего текут, все же, линейно) превращаются в доли расстояния(точнее, доли анимируемого отрезка вектора AnimatableData, который представляет собой выполненное изменение).

Вот моя первая реализация:

    func getActualPosition(of position: CGFloat) -> CGFloat{        let correctPosition = max(min(position, 1), 0) / duration        let trimmingCurve = TimingCurve.superEaseInPath        if correctPosition < 0.0000001{            let reversedCurve = Path(UIBezierPath(cgPath: trimmingCurve.cgPath).reversing().cgPath)            //trim to start point is impossible, so reverce the curve and get last point            guard let point = reversedCurve.currentPoint else{fatalError("cant get current timing curve start point")}            return point.y        }        guard let point = trimmingCurve.trimmedPath(from: 0, to: correctPosition).currentPoint else{fatalError("cant get current timing curve point at \(position)")}        return point.y * self.duration    }

Как видно, я немного схалтурил. По определению, я должен найти точку на кривой, соответствующую данному значению X, и вернуть ее Y. Мне не удалось найти какого-то стандартного встроенного метода для решения этой задачи, или популярного паттерна, потому я решил адаптировать для этих целей имеющийся метод trim(). Я решил что я вполне могу немного пересмотреть подход, и получить искомую точку с помощью получения части пути, соответствующей доли пройденного расстояния. Для нуля этот метод вернет 0, для 1 вернет 1, ну и в середине, наверное, все тоже будет примерно правильно.

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



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

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

В итоге, точки лежат довольно близко к графику:



Я сделал singlton объект для создания timing кривой по контрольным точкам и кэширования массива отрезков, с помощью которых происходит поиск Y. Таким образом, все что мне нужно для поиска конкретной точки на кривой найти отрезок, внутри которого будет лежать эта точка, поделить его попалам до тех пор, пока один из концов отрезка не будет достаточно близко к этой точке, для попадания в заданную погрешность, и затем, линейно интерполировать значение Y по заданной X, подменив часть кривой этим отрезком. Не утверждаю что это лучший способ, и что он будет работать во всех случаях. Опять же, пишите в комментариях, если знаете способы лучше.

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

        self.timing = TimingCurve.superEaseIn(duration: 1)let animatedPosition = timing.getY(onX: currentPosition)

Вот так в итоге выглядит анимация вместе с нашей тайминговой кривой:



За полным кодом добро пожаловать на гитхаб, смотреть файл TimingCurveView.

Разобравшись с этим примером, вы на 100% поймете как устроены тайминги анимации. Вы, кстати, сможете использовать это знание, ведь SwiftUI позволяет создавать свою тайминговую кривую по контрольным точкам с помощью функции timingCurve(), и использовать ее как любую другую анимацию.

Transition


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

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

   let animation = Animation.timingCurve(Double(TimingCurve.control.point1.x),                                Double(TimingCurve.control.point1.y),                                Double(TimingCurve.control.point2.x),                                Double(TimingCurve.control.point2.y),                                duration: 1)

Animation.timingCurve(x:y:) позволяет задать свой тайминг анимации на основе контрольных точек кривых Безье. На выходе мы получим полноценный объект Animation, как например привычный .linear(duration: 1), который можно использовать без ограничений. А учитывая, что я для анимации волн использую ровно те же контрольные точки, анимация будет синхронной.

можно было бы поудобнее организовать код...
Но я ленивая жопа.

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

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

Первая проблема возникла в тот момент, когда мне потребовалось получить текущее состояние анимации, чтобы подобрать цвет шторки. Для этого я создал ObservableObject, который передаю внутрь модификатора, и который я изменяю внутри сеттера AnimatableData. Здесь важно понимать, что изменение @Published свойства 60 раз в секунду это совсем не то что вам нужно. Это как изменять @State переменные внутри блока body. Нам не нужно инициировать что-либо при изменении состояния анимации. Но вот если нам потребуется, мы сможем узнать состояние анимации в любой момент.

  public var animatableData: CGFloat {        get { time}        set {            if animationHandler.isStarted{                self.time = newValue                if self.time != self.animationHandler.currentAnimationPosition{                    self.animationHandler.currentAnimationPosition = self.time                }            }            let currentTime = newValue - CGFloat(Int(newValue))            self.currentPosition = SharpWavePosition.calculate(forWave: wave.ind, ofWaves: wave.totalWavesCount, overTime: currentTime)            if currentPosition < 0.01{                animationHandler.currentWaveBaseColor = wave.baseColor            }        }

Именно поэтому я не подписывал SharpRainbowView на отслеживание изменений AnimationHendler:

   //@ObservedObject    var animationHandler: AnimationHandler

Для иллюстрации работы этих шторок, я включу отображение того, что происходит за границами View, закомментировав .clipped(). Кроме того, я сдвину шторки чуть выше анимации, для наглядности. Теперь стало понятнее, неправда ли?



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

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

Примерно те же причины заставили сделать замыкающую шторку так же трехцветной. Иначе граница смотрелась неестественной. Но в результате, первая волна (а теперь мы знаем, что на самом деле это шторка, описанная в Transition.truncate) получилась короткой. И хоть она движется полностью синхронно с первой волной, т.е. ускоренно, а не линейно, ее ширина не меняется со временем, хотя ширина последующих волн визуально увеличивается. Это можно победить, динамически изменяя ширину шторки, но мне откровенно было уже лень. Давайте расценим это как домашнее задание для тех, кто хочет погрузиться в эти механизмы, и раскурить код (заранее прошу у таких людей прощения, там полно следов моих экспериментов, которые могут вас запутать).

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

    Text("toggle animation").onTapGesture {                withAnimation(){                    var delay: Double = 0                    if self.isShown{                        let waveChangeTime: Double = Double(1) /  Double(self.animationHandler.rainbowColors.count)                        let currentTime = Double(self.animationHandler.currentAnimationPosition)                        let wavesPassed = Double(Int(currentTime / waveChangeTime))                        delay =  (wavesPassed + 1) * waveChangeTime - currentTime                        delay = max(delay - 0.05, 0)                        print("currentTime: \(currentTime); delay \(delay)")                    }                    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {                        self.isShown.toggle()                    }                }            }

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

Transition любит подкладывать свинью


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

Так, .rotation3DEffect() модификатор вполне успешно переворачивает View с какими-то сложными анимациями внутри, но вот .transition она переворачивать не умеет. Так обидно было написать лаконичный код, увидеть как твои волны зеркально разбегаются от центра экрана, а потом увидеть, что на левой View шторки так и бегают слева направо. Пришлось писать кучу бойлерплейта, дабы transition модификатор умел работать в обе стороны.



Еще, drowingGroup() модификатор, с помощью которого вы можете подключить Metal переложить на GPU отрисовку ZStack с большим количеством вложенных View, особенно View с градиентами, не умеет в transition. Он не понимает описанную вами анимацию появления и исчезновения и заменяет ее какой-то своей.

А как же мы прячем статус-бар?


Очень просто. В SwiftUI есть модификатор .statisBar(hidden:). Вот только api для управлением transition для статус-бара SwiftUI не предоставляет. Для этого нам придется воспользоваться возможностями UIKit. В файле SceneDelegate используется UIHostingController для превращения SwiftUI View в UIKit ViewController. Именно на этом этапе удобнее всего использовать какие-то глобальные функции UIKit, как то работа со статусбаром, или отключение системных жестов связанных с краем экрана (preferredScreenEdgesDeferringSystemGestures). Вы можете наследоваться от UIHostingController, переопределив значения каких-то системных свойств, и использовать этого наследника для передачи своей View в rootViewController. К сожалению, статусбар может принимать только ограниченное число transition: .fade, .slide и .none. По-умолчанию используется fade, и он сюда подходит лучше всего, так что оставим как есть. Будем надеяться, что этот функционал все же будут расширять.

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

struct StatusBarHider: View{    var isShown: Bool    @State var internalIsShown = true    var body: some View{        if isShown == false && self.internalIsShown == true{            DispatchQueue.main.asyncAfter(deadline: .now() + 0.7){                self.internalIsShown = self.isShown            }        }else if isShown == true && self.internalIsShown == false{            DispatchQueue.main.async(){                self.internalIsShown = self.isShown            }        }        return Spacer()            .statusBar(hidden: internalIsShown)            .animation(Animation.linear(duration: 0.3))    }}

По сути, здесь формируется простой Spacer(), который мне и так был нужен, но на него сверху накручен модификатор statusBar. Вообще, конечно, со стороны Apple было нелогично выводить этот функционал в модификаторы, т.к. это не относится к конкретной View, которую ты модифицируешь, но, видимо, просто не придумал как сделать это лучше. Куда интереснее другое.

Здесь используется тот же грязный хак, что мы рассматривали ранее: внутри body я выполняю какие-то действия в рантайме, меняя при этом значение @State переменной. Логика в целом та же, есть параметр структуры, который передается извне, и @State переменная, которая модифицируется спустя какое-то время, вызывая исчезновение или появление статусбара.

В данном случае, этот хак мне понадобился потому, что управление анимацией статусбара это функционал UIKit, который еще довольно плохо проработан в SwiftUI. По идее, я бы прикрутил анимацию с отложенным стартом к модификатору .statusBar(hidden:), но это не работает. Анимация скрытия и появления статусбара фиксирована, и не подлежит изменению со стороны SwiftUI.

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

А как запихнули размеры и положение статусбара в @Environment?


@Environment (не путать с @EnvironmentObject) это обертка, дающая доступ к фиксированному перечню переменных, отражающих окружение нашего приложения. Например, ориентацию экрана, или цветовую тему ОС. Эта же обертка позволяет вашим view быть подписанными на изменение этих параметров.

Этот перечень можно расширить. Я посчитал, что иметь в @Environment доступ к размеру и положению статусбара это было бы правильно. Вот как я это сделал:

struct StatusBarFrame: EnvironmentKey {    static var defaultValue: CGRect {        CGRect()    }}extension EnvironmentValues {    var statusBarFrame: CGRect{        get {            return self[StatusBarFrame.self]        }        set {            self[StatusBarFrame.self] = newValue        }    }}

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

Доступ к значению в @Environment осуществляется с помощью keyPath \.statusBarFrame. Например, для для передачи environment-значения всем view вниз по иерархии:

.environment(\.statusBarFrame, statusBarFrame) 

И в самих View для извлечения значения из хранилища:

@Environment(\.statusBarFrame) var statusBarframe: CGRect 

Кстати, для работы со статусбаром в объекте UIWindowScene в IOS 13 появился реквизит statusBarManager. Из него можно вытянуть некоторые параметры. А вот управлять ими теперь нельзя. Насколько я понял, раньше можно было получить доступ к ViewController-у статусбара, и добавить в него subView. Видимо, лавочку прикрыли.

Вообще говоря, я бы перенес функционал модификатора .statusBar(hidden:) именно сюда, тут он был бы более уместен, как по мне. Думаю рано или поздно, у разработчиков дойдут до этого руки.

Заключение


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

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

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

Послесловие


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

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 и другое.

Подробнее..

Как смотреть WWDC 2020, если ты не разработчик

30.06.2020 14:13:12 | Автор: admin

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


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



Даб-даб ди си


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


У большинства людей конференция ассоциируется с Keynote, на которой Apple рассказывает о программных новинках, для кого-то ещё и с Apple Design Awards, где награждают лучшие приложения по версии экспертов эппла. Но самое интересное начинается дальше. В течении одной недели абсолютно закрытая компания Apple приоткрывает завесу тайны над разработками и даёт пообщаться с инженерами и сотрудниками. Для этого проводится около 100 сессий, на которых инженеры рассказывают про различные аспекты связанные с анонсированными новинками и про то, как правильно разрабатывать свои продукты с их учётом. Если сессий недостаточно или есть вопросы, то на лабах можно задавать инженерам любые вопросы, связанные со своими проектами или с только анонсированными технологиями. Также проходит много событий, концертов встреч и подкастов вживую.


Если хочется окунуться в атмосферу, то можно почитать текстовую трансляцию Егора Толстого про поездку на WWDC 2017 года или ребят из RedMadRobot в прошлом году.


Но если раньше для этого нужно было испытать удачу и получить билет, прилететь в США, то в 2020 конференция стала ближе как никогда. И Apple подошли к этому основательно.


keynote_epic


Только посмотрите Keynote и Platform state of the Union, который из стандартной презентации со сцены и сменой ведущих сменился в шоу с эпичными переходами. Остальные сессии хоть и не такие эпичные, но стали заметно живее и теперь лучше смотрятся онлайн.


Где смотреть?


Но для начала определимся как смотреть.
В этом году Apple неплохо обновили своё приложение для разработчиков и добавили возможность просматривать сессии прямо в нём. Если по каким-то причинам официальное приложение не подходит, то все ещё актуально приложение WWDC для macOS.


Если приложения не для вас, то в браузере всё также доступно и все сессии можно посмотреть на странице в портале разработчиков.


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


  • На сайте https://asciiwwdc.com собираются текстовые версии докладов. (Транскрипты обновляются обычно в течении месяца после завершения очередной WWDC)
  • Сообщество делится конспектами сессий на GitHub, например https://github.com/Blackjacx/WWDC и https://wwdcnotes.com

Что смотреть?


Чтобы не привязываться к конкретным ролям пройдёмся по основным этапам жизни любой фичи проекта:


  • Идея и гипотезы.
  • Проектирование и прототипирование.
  • Разработка и контроль качества.
  • Бета тестирования и релиз.

Для каждой мы подобрали топ наиболее интересных и полезных по нашему мнению сессий. Поехали!





Идея / гипотеза


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



Проектирование и планирование


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


Всем кто отвечает и за эту стадию работы над проектом рекомендуем посмотреть:



Также все материалы про дизайн уже с любовью собраны Apple:



Разработка и контроль качества


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


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


Сессии про тестирование собраны, как и сессии для дизайнеров, в отдельный раздел и в подборке The suite life of testing.


Бета тестирования и релиз


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


Также на этой WWDC особое внимание было уделено работе Entreprise. Под это направление отвели отдельную подборку и анонсировали разные интересный фичи в iOS. Чего только стоит Local Push Connectivity, благодаря которому уведомления можно будет рассылать в рамках локальной сети.


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


Наш топ сессий на эти тему тестирования:



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


Today@WWDC



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



А что, если нужно поддерживать старые iOS?


Это отличный повод пересмотреть сессии с WWDC 2-х летней давности.


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


  • Гайды по просмотру WWDC 2016-2019 от UseYourLoaf.
  • Подборка нововведений WWDC 2019 от Патрика Балестры.

Как смотреть?


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


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


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


Также, мы дополнительно отранжировали темы по релевантности к нашим проектам и платформам.


После просмотра сессии пишется небольшое резюме в Slack, оценивается полезность, чем интересен и что хотелось бы применить на практике.


Пример обзора
Пример обзора


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


Часть результатов прошлогоднего брейншторма
Часть результатов прошлогоднего брейншторма


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


На вышеописанные процедуры в прошлом году у нас ушло около месяца: 2 недели на просмотр сессий, 1 неделя на обсуждение и отбор тем, пара дней на проведение общего мозгового штурма и оформление результатов. Тем самым мы продлили праздник WWDC у себя в компании ещё на один месяц после его завершения, получили на выходе большое количество идей для новых фич для интеграции в наши проекты и планов для внутренних исследований.


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


Желаем приятного просмотра!

Подробнее..

Релиз мобильных приложений одной кнопкой

02.07.2020 18:23:23 | Автор: admin


Всем привет! Меня зовут Михаил Булгаков (нет, не родственник), я работаю релиз-инженером в Badoo. Пять лет назад я занялся автоматизацией релизов iOS-приложений, о чём подробно рассказывал в этой статье. А после взялся и за Android-приложения.

Сегодня я подведу некоторые итоги: расскажу, к чему мы пришли за это время. Long story short: любой причастный к процессу сотрудник может зарелизить хоть все наши приложения на обеих платформах в несколько кликов без головной боли, больших затрат времени, регистрации и СМС. Так, наш отдел релиз-инженеров за 2019 год сэкономил около 830 часов.

За подробностями добро пожаловать под кат!

Что стоит за мобильным релизом


Выпуск приложения в Badoo состоит из трёх этапов:

  1. Разработка.
  2. Подготовка витрины в магазине приложений: тексты, картинки всё то, что видит пользователь в App Store или Google Play.
  3. Релиз, которым занимается команда релиз-инжиниринга.

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

Большая часть времени уходит на подготовку витрины приложения в App Store или Google Play: необходимо залить красивые скриншоты, сделать завлекающее описание, оптимизированное для лучшей индексации, выбрать ключевые слова для поиска. От качества этой работы напрямую зависит популярность приложения, то есть по факту результат деятельности разработчиков, тестировщиков, дизайнеров, продакт-менеджеров, маркетологов всех причастных к созданию продукта.

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

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

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



Первые шаги на пути к автоматизации: загрузка метаданных


Как это работало в самом начале: для каждого релиза создавалась таблица в Google Sheets, в которую продакт-менеджер заливал выверенный мастер-текст на английском, после чего переводчики адаптировали его под конкретную страну, диалект и аудиторию, а затем релиз-инженер переносил всю информацию из этой таблицы в App Store или Google Play.

Первый шаг к автоматизации, который мы сделали, интегрировали перевод текстов в наш общий процесс переводов. Останавливаться на этом не буду это отдельная большая система, про которую можно прочитать в нашей недавней статье. Основной смысл в том, что переводчики не тратят время на таблички и работают с интерфейсом для удобной загрузки руками (читай: ctrl+c ctrl+v) переведённых вариантов в стор. Кроме того, присутствуют задатки версионирования и фундамент для Infrastructure-as-Code.

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

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


Наша реальность по состоянию на 2015 год

В среднем на релиз одного приложения при наличии актуальной версии скриншотов уходило около полутора-двух часов работы релиз-инженера в случае с iOS и около получаса в случае с Android. Разница обусловлена тем, что iOS-приложения должны пройти так называемый Processing, который занимает некоторое время (отправить приложение на Review до успешного завершения Processing невозможно). Кроме того, App Store сам по себе по большинству операций в тот момент работал гораздо медленнее, чем Google Play.

Стало очевидно, что нам нужен дополнительный инструмент для доставки приложений в сторы. И как раз в тот момент на open-source-рынке начал набирать популярность продукт под названием Fastlane. Несмотря на то, что он тогда ещё был сыроватый, он уже мог решить огромный пласт наших проблем

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

Коротко о Fastlane


Сегодня Fastlane это продукт, который способен практически полностью автоматизировать все действия от момента окончания разработки до релиза приложения в App Store и Google Play. И речь не только о загрузке текстов, скриншотов и самого приложения здесь и управление сертификатами, и бета-тестирование, и подписывание кода, и многое другое.

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

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

Со временем мы внедрили большинство предоставляемых Fastlane возможностей в системы сборки, подписания, заливки и т. д. наших приложений. И несказанно этому рады. Зачем изобретать колесо, да ещё и поддерживать его правильную форму, когда можно один раз написать унифицированный сценарий, который будет сам крутиться в CI/CD-системе?

Автоматизация iOS-релизов


По причине того, что Google Play более дружелюбен к разработчикам, на релиз Android-приложения уходило очень мало времени: без обновления текстов, видео и скриншотов пара минут. Отсюда и отсутствие необходимости в автоматизации. А вот с App Store проблема была очень даже осязаемой: слишком много времени уходило на отправку приложений на Review. Поэтому было решено начать автоматизацию именно с iOS.

Подобие своей системы автоматизации взаимодействия с App Store мы обдумывали (и даже сделали прототипы), но у нас не было ресурсов на допиливание и актуализацию. Также не было никакого мало-мальски адекватного API от Apple. Ну и последний гвоздь в гроб нашего кастомного решения вбили регулярные обновления App Store и его механизмов. В общем, мы решили попробовать Fastlane тогда ещё версии 2015 года.

Первым делом был написан механизм выгрузки переведённых текстов для приложений в нужную структуру как компонент нашей общей внутренней системы AIDA (Automated Interactive Deploy Assistant). Эта система своеобразный хаб, связующее звено между всеми системами, технологиями и компонентами, используемыми в Badoo. Работает она на самописной системе очередей, реализованной на Golang и MySQL. Поддерживает и совершенствует её в основном отдел Release Engineering. Подробнее о ней мы рассказывали в статье ещё в 2013 году, с тех пор многое изменилось. Обещаем рассказать про неё снова AIDA классная!

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

Это сократило время подготовки релиза с пары часов до примерно 30 минут, из которых только полторы минуты надо было что-то делать руками! Остальное время ждать. Ждать окончания Processing. Механизм стал прорывом на тот момент как раз потому, что почти полностью избавил нас от ручной работы при подготовке AppStore к релизу. Под скрипт мы сделали репозиторий, к которому дали доступ людям, имеющим непосредственное отношение к релизам (проджект-менеджерам, релиз-инженерам).

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

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

  1. Нужно было идти в TeamCity за свежей сборкой, скачивать оттуда IPA-файл, загружать его в App Store через Application Manager.
  2. Потом идти в интерфейс с переводами в AIDA, смотреть, готовы ли все переводы, запускать скрипт, убеждаться, что он правильно сработал (всё-таки на тот момент Fastlane был ещё сыроват).
  3. После этого залезать в App Store и обновлять страницу с версией до того момента, пока не завершится Processing.
  4. И только после этого отправлять приложение на Review.

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

Следующим действием было решено перенести скрипт в нашу AIDA, заодно объединив и автоматизировав все шаги до момента отправки приложения: проверку на готовность переводов, сбор данных из TeamCity, оповещение, логирование и все остальные блага XXI века. Параллельно с этим мы начали загружать все собранные версии в TestFlight на этапе сборки.

TestFlight это приложение сторонних разработчиков, когда-то купленное Apple для тестирования готового приложения внешними тестировщиками практически в продакшен-окружении, то есть с push-оповещениями и вот этим всем.


AIDA молодец, будь как AIDA!

Всё это привело к сокращению времени с получаса до полутора минут на всё про всё: IPA-файл успевал пройти Processing ещё до того момента, когда команда QA-инженеров давала отмашку на запуск релиза. Тем не менее нам всё равно приходилось идти в App Store, выбирать нужную версию и отправлять её на Review.

Плюс, был нарисован простенький интерфейс: мы же все любим клац-клац.


Вот так, вкладка за вкладкой, Ctrl+C Ctrl+V...

Автоматизация Android-релизов


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

  1. Заходить в консоль Google Play, чтобы убедиться, что предыдущая версия раскатана на 100% пользователей или заморожена.
  2. Создавать новую версию релиза с обновлёнными текстами и скриншотами (при наличии).
  3. Загружать APK-файл (Android Package), загружать Mapping-файл.
  4. Идти в HockeyApp (использовался в то время для логирования крашей), загружать туда APK-файл и Mapping-файл.
  5. Идти в чат и отписываться о статусе релиза.

И так с каждым приложением.

Да, у Google Play есть свой API. Но зачем делать обёртку, следить за изменениями в протоколе, поддерживать её и плодить сущности без необходимости, если мы уже используем Fastlane для iOS-релизов? К тому же он комфортно существует на нашем сервере, варится в своём соку и вообще обновляется. А к тому времени он ещё и научился адекватно релизить Android-приложения. Звёзды сошлись!

Первым делом мы выпилили отовсюду всё старое, что было: отдельные скрипты, наброски автоматизации, старые обёртки для API это создавалось когда-то в качестве эксперимента и не представляло особой ценности. Сразу после этого мы добавили команду в AIDA, которая уже умела забирать что нужно из TeamCity, загружать что надо куда надо в HockeyApp, отправлять оповещения, логировать активность, и вообще она член команды.

Заливкой APK- и Mapping-файлов в Google Play занимался Fastlane. Надо сказать, что по проторенной тропе идти гораздо проще: реализовано это было достаточно быстро с минимальным количеством усилий.

На определённом этапе реализации автоматизации случился переход с APK-архивов на AAB (Android App Bundle). Опять же, нам повезло, что по горячим следам довольно быстро получилось всё поправить, но и развлечений добавилось в связи с этим переходом. Например, подгадил HockeyApp, который не умел использовать AAB-архивы в связи с подготовкой к самовыпиливанию. Так что для того чтобы комфортно продолжать его использовать, нужно было после сборки AAB разобрать собранный архив, доставать оттуда Mapping-файл, который полетит в HockeyApp, а из AAB нужно было отдельно собрать APK-файл и только потом загружать его в тот же HockeyApp. Звучит весело. При этом сам Google Play отлично раскладывает AAB, достаёт оттуда Mapping-файл и вставляет его куда нужно. Так что мы избавились от одного шага и добавили несколько, но от этого было никуда не деться.

Был написан интерфейс (опять же, по аналогии с iOS), который умел загружать новую версию, проверять релиз вдоль и поперёк, управлять текущим активным релизом (например, повышать rollout percentage). В таком виде мы отдали его ответственным за релизы членам команды Android QA, стали собирать фидбэк, исправлять недочёты, допиливать логику (и что там ещё бывает после релиза 1.0?).

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

Унификация флоу мобильных релизов


К моменту автоматизации Android-релизов Fastlane наконец-то научился отправлять версии iOS-приложений на ревью. А мы немного усовершенствовали систему проверки версий в AIDA.

Пришла пора отдать iOS-релизы на откуп команде QA-инженеров. Для этого мы решили нарисовать красивую формочку, которая бы полностью покрывала потребности, возникающие в процессе релиза iOS-приложений: давала бы возможность выбирать нужный билд в TeamCity по предопределённым параметрам, выбирать вариант загружаемых текстов, обновлять или нет опциональные поля (например, Promotional Text).

Сказано сделано. Формочка получилась очень симпатичная и полностью удовлетворяет все запросы. Более того, с её внедрением появилась возможность выбирать сразу все необходимые приложения со всеми требуемыми параметрами, так что и взаимодействие с интерфейсом свелось к минимуму. AIDA по команде присылает ссылку на build log, по которому можно отслеживать возникающие ошибки, убеждаться, что всё прошло хорошо, получать какую-то debug-информацию вроде версии загружаемого IPA-файла, версии релиза и т. д. Вот так красиво iOS-релизы и были переданы команде iOS QA.


Ну симпатично же?

Идея с формочкой понравилась нам настолько, что мы решили сделать аналогичную и для Android-релизов. Принимая во внимание то, что у нас есть приложение, полностью написанное на React Native, и что команда QA-инженеров этого приложения отвечает как за iOS-, так и за Android-релизы.

Уже используемый командой Android QA интерфейс был интегрирован с изменениями в аналогичную формочку, процесс был адаптирован под новые реалии всё было максимально приближено к процессам iOS-команды. Это дало стимул наконец набросать более-менее конечный вариант документации для обеих платформ (в процессе постоянных изменений делать это категорически не хотелось), а ещё отвязать процесс релиза от всех искусственных ограничений, которые сложились исторически и приводили к лишним телодвижениям в нестандартных ситуациях, требуя действий команды релиз-инженеров.

Вывод


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

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

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

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

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

Бюджетный DI на антипаттернах

03.07.2020 08:04:12 | Автор: admin

image


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


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


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


Введение


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


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


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


Хорошее содержание



Принципы


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


  1. Не выпендривайся. Тупой и понятный код в большинстве случаев лучше умного и непонятного.
  2. Будь краток. Кода должно быть настолько мало, чтобы его не жалко было в любой момент выкинуть и написать заново за один день.
  3. Удобство превыше правил. Если можно облегчить себе жизнь, пожертвовав принципами SOLID, пожертвуй принципами SOLID.
  4. Получай удовольствие. Если есть разные варианты решения проблемы, выбирай более веселый.

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


Проблема управления зависимостями


Проблема управления зависимостями довольно типичная в программировании. Мало какая сущность в коде может похвастаться независимостью как твоя бывшая. Обычно все от кого-нибудь зависят. В MVVM, например, вью-контроллер зависит от вью-модели, которая подготавливает для него данные. Вью-модель зависит от сервиса, который за этими данными ходит в сеть. Сервис зависит от другого сервиса низкоуровневой реализации сети, и так далее. Все эти сущности, которых может быть великое множество, нужно где-то создавать и как-то доставлять до потребителей. Для любой типичной проблемы, как правило, есть типичное решение паттерн. В случае с проблемой управления зависимостями таким паттерном является Dependency Injection (DI) контейнер.


У меня нет намерения подробно объяснять, что такое DI-контейнер. Про это классно рассказывают в двух статьях из репозитория Ninject: раз, два (уберите от экрана детей, там код на С#). Еще есть небольшое объяснение в репозитории самого популярного DI-контейнера под iOS Swinject (заметили, что Swinject это Ninject на Swift?). Хардкорщикам могу предложить статью Фаулера от 2004 года.


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


Решение


Существует несколько довольно популярных реализаций DI-контейнеров под iOS (Swinject, Cleanse, Dip, DITranquility, EasyDI), но использовать чужую реализацию, согласитесь, скучно. Гораздо веселее использовать мою.


Готовы немного развлечься и написать DI-контейнер с нуля? Похожую реализацию мне показал однажды один из самых крутых iOS-разработчиков, простой сибирский парень teanet, за что ему огромное спасибо. Я ее немного переосмыслил и готов поделиться с вами. Начнем с протокола IContainer:


protocol IContainer: AnyObject {    func resolve<T: IResolvable>(args: T.Arguments) -> T}

Привычка из прошлой жизни я всегда пишу I перед протоколами. Буква I значит interface. У нашего интерфейса протокола всего один метод resolve(args:), который от нас принимает какие-то аргументы T.Arguments, а взамен возвращает экземпляр типа T. Как видно, не любая сущность может быть Т. Чтобы стать полноправным T, нужно реализовать IResolvable. IResolvable это еще один протокол, о чем нам услужливо подсказывает буква I в начале имени. Он выглядит вот так:


protocol IResolvable: AnyObject {    associatedtype Arguments    static var instanceScope: InstanceScope { get }    init(container: IContainer, args: Arguments)}

Все кролики, которые хотят быть доступны из шляпы, обязаны реализовать IResolvable.


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


Свойство instanceScope отвечает за область видимости, в которой будет существовать экземпляр объекта:


enum InstanceScope {    case perRequst    case singleton}

Это довольно стандартная для DI-контейнеров штуковина. Значение perRequest означает, что для каждого вызова resolve(args:) будет создан новый экземпляр T. Значение singleton означает, что экземпляр T будет создан единожды при первом вызове resolve(args:). При последующих вызовах resolve(args:) в случае singleton будет отдаваться закэшированная копия.


С протоколами разобрались, приступаем к реализации:


class Container {    private var singletons: [ObjectIdentifier: AnyObject] = [:]    func makeInstance<T: IResolvable>(args: T.Arguments) -> T {        return T(container: self, args: args)    }}

Тут ничего особенного: кэш синглтонов будем хранить в виде словаря singletons. Ключом словаря нам послужит ObjectIdentifier это стандартный тип, поддерживающий Hashable и представляющий собой уникальный идентификатор объекта ссылочного типа (через него, кстати, реализован оператор === в Swift). Метод makeInstance(args:) умеет на лету создавать любые экземпляры T благодаря тому, что мы обязали все T реализовать один и тот же инициализатор.


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


extension Container: IContainer {    func resolve<T: IResolvable>(args: T.Arguments) -> T {        switch T.instanceScope {        case .perRequst:            return makeInstance(args: args)        case .singleton:            let key = ObjectIdentifier(T.self)            if let cached = singletons[key], let instance = cached as? T {                return instance            } else {                let instance: T = makeInstance(args: args)                singletons[key] = instance                return instance            }        }    }}

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


Вот, собственно, и все. Мы только что написали свой DI-контейнер в 50 строк кода. Но как этой штукой вообще пользоваться? Да очень просто.


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


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


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


Полезный экстеншен номер раз:


protocol ISingleton: IResolvable where Arguments == Void { }extension ISingleton {    static var instanceScope: InstanceScope {        return .singleton    }}

И второй такой же, но другой:


protocol IPerRequest: IResolvable { }extension IPerRequest {    static var instanceScope: InstanceScope {        return .perRequst    }}

Теперь вместо IResolvable можно конформить более лаконичным ISingleton/IPerRequest и сэкономить тем самым несколько секунд жизни, потратив их на саморазвитие. А вот и реализация OrdersProvider подъехала:


class OrdersProvider: ISingleton {    required init(container: IContainer, args: Void) { }    func loadOrders(for customerId: Int, date: Date) {        print("Loading orders for customer '\(customerId)', date '\(date)'")    }}

Мы предоставили required init, как того требует протокол, но, так как OrdersProvider ни от чего не зависит, этот инициализатор у нас пустой. Каждый раз, когда мы будем доставать OrdersProvider из контейнера, мы будем получать один и тот же экземпляр, потому что такова дефолтная реализация instanceScope для ISingleton.


А вот и модель представления собственной персоной:


final class OrdersVM: IPerRequest {    struct Args {        let customerId: Int        let date: Date    }    private let ordersProvider: OrdersProvider    private let args: Args    required init(container: IContainer, args: Args) {        self.ordersProvider = container.resolve()        self.args = args    }    func loadOrders() {        ordersProvider.loadOrders(for: args.customerId, date: args.date)    }}

Эта вью-модель не может существовать без аргументов OrdersVM.Args, которые мы получаем через required init. В этот инициализатор также попадает сам контейнер, из которого мы без лишней суеты извлекаем экземпляр OrdersProvider посредством вызова resolve().


Вызов метода loadOrders() использует ordersProvider для загрузки заказов, предоставляя ему необходимые для работы аргументы. Каждый раз, когда мы будем доставать OrdersVM из контейнера, мы будем получать новый экземпляр, потому что такова дефолтная реализация instanceScope для IPerRequest.


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


let container = Container()let viewModel: OrdersVM = container.resolve(args: .init(customerId: 42, date: Date()))viewModel.loadOrders()

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


Loading orders for customer '42', date '2020-04-22 17:41:49 +0000'

Критика


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


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


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


final class OrdersVM {    private let ordersProvider: IOrdersProvider    init(ordersProvider: IOrdersProvider) {       self.ordersProvider = ordersProvider    }}

Если вы осмелились использовать Service Locator, тогда ваша сущность, вероятно, достает зависимости из какого-нибудь сомнительного места типа статической фабрики. Например, вот так:


final class OrdersVM {    private let ordersProvider: IOrdersProvider    init() {        self.ordersProvider = ServiceLocator.shared.resolve()    }}

Программисты недолюбливают Service Locator в первую очередь за то, что он скрывает истинные зависимости сущностей при их создании.


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


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


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


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


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


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


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


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


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


Короче, минусы


  • Зависимости достаем в конструкторе прямо из контейнера (Service Locator).
  • Не получится закрыть зависимость протоколом (принцип на букву D).

Короче, плюсы


  • Простая и лаконичная реализация (50 строк кода).
  • Не надо регистрировать зависимости (вообще не надо).
  • Извлечение из контейнера никогда не сломается (совсем никогда).
  • Нельзя передать невалидные аргументы (не скомпилируется).

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


One More Thing: автоматическое внедрение зависимостей через обертки свойств


В 2019 году в компании Apple придумали инкапсулировать повторяющуюся логику гетеров и сетеров в переиспользуемые атрибуты и назвали это обертками свойств (property wrappers). С помощью таких оберток ваши свойства волшебным образом могут получить новое поведение: запись значения в Keychain или UserDefaults, потокобезопасность, валидацию, логирование да много чего.


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


Чтобы написать свою обертку свойства в минимальной комплектации, нужно создать класс или структуру, предоставить свойство wrappedValue и пометить все это дело атрибутом @propertyWrapper:


@propertyWrapperstruct Resolvable<T: IResolvable> where T.Arguments == Void {    private var cache: T?    var wrappedValue: T {        mutating get {            if let cache = cache {                return cache            }            let resolved: T = ContainerHolder.container.resolve()            cache = resolved            return resolved        }    }}

Из этого незамысловатого кода мы видим, что наш property wrapper называется Resolvable. Он работает со всеми типами Т, которые реализуют одноименный протокол и не требуют аргументов при инициализации.


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


final class ContainerHolder {    static var container: IContainer!}

Имея в своем арсенале обертку Resolvable<T>, мы можем применить ее к какой-нибудь зависимости, например к ordersProvider:


@Resolvableprivate var ordersProvider: OrdersProvider

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


private var _ordersProvider = Resolvable<OrdersProvider>()var ordersProvider: OrdersProvider {  get { return _ordersProvider.wrappedValue }}

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


Теперь знакомая нам модель представления может позволить себе не извлекать из контейнера OrdersProvider в инициализаторе, а просто пометить соответствующее свойство атрибутом @Resolvable. Вот так:


final class OrdersVM: IPerRequest {    struct Args {        let customerId: Int        let date: Date    }    @Resolvable    private var ordersProvider: OrdersProvider    private let args: Args    required init(container: IContainer, args: Args) {        self.args = args    }    func loadOrders() {        ordersProvider.loadOrders(for: args.customerId, date: args.date)    }}

Самое время собрать все вместе и порадоваться, что все работает как прежде:


ContainerHolder.container = Container()let viewModel: OrdersVM = ContainerHolder.container.resolve(    args: .init(customerId: 42, date: Date()))viewModel.loadOrders()

Для справки. Этот код производит следующий консольный вывод:


Loading orders for customer '42', date '2020-04-23 18:47:36 +0000'



Unit-тесты, раздел под звездочкой


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


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


protocol IOrdersProvider {    func loadOrders(for customerId: Int, date: Date)}extension OrdersProvider: IOrdersProvider {}

Теперь во вью-модели можем сделать второй инициализатор, который будет принимать этот протокол:


final class OrdersVM: IPerRequest {    struct Args {        let customerId: Int        let date: Date    }    private let ordersProvider: IOrdersProvider    private let args: Args    required convenience init(container: IContainer, args: Args) {        self.init(            ordersProvider: container.resolve() as OrdersProvider,            args: args)    }    init(ordersProvider: IOrdersProvider, args: Args) {        self.args = args        self.ordersProvider = ordersProvider    }    func loadOrders() {        ordersProvider.loadOrders(for: args.customerId, date: args.date)    }}

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


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


Забегая вперед, скажу, что далее от нас потребуется хранить объекты IResolvable в некоторой коллекции. Однако если мы попробуем сделать это, то столкнемся с суровой действительностью в виде ошибки, до боли знакомой каждому iOS-разработчику: protocol 'IResolvable' can only be used as a generic constraint because it has Self or associated type requirements. Типичный способ как-то справиться с этой ситуацией налить себе чего-нибудь покрепче и применить механизм с пугающим названием стирание типов (type erasure).


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


struct AnyResolvable {    private let factory: (IContainer, Any) -> Any?    init<T: IResolvable>(resolvable: T.Type) {        self.factory = { container, args in            guard let args = args as? T.Arguments else { return nil }            return T(container: container, args: args)        }    }    func resolve(container: IContainer, args: Any) -> Any? {        return factory(container, args)    }}

Кода здесь немного, но он хитрый. В инициализатор мы принимаем настоящий живой тип T, который не можем никуда сохранить. Вместо этого мы сохраняем замыкание, обученное создавать экземпляры этого типа. Замыкание впоследствии используется по своему прямому назначению в методе resolve(container:args:), который понадобится нам позже.


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


final class ContainerMock: Container {    private var substitutions: [ObjectIdentifier: AnyResolvable] = [:]    public func replace<Type: IResolvable, SubstitutionType: IResolvable>(        _ type: Type.Type, with substitution: SubstitutionType.Type) {        let key = ObjectIdentifier(type)        substitutions[key] = AnyResolvable(resolvable: substitution)    }    override func makeInstance<T: IResolvable>(args: T.Arguments) -> T {        return makeSubstitution(args: args) ?? super.makeInstance(args: args)    }    private func makeSubstitution<T: IResolvable>(args: T.Arguments) -> T? {        let key = ObjectIdentifier(T.self)        let substitution = substitutions[key]        let instance = substitution?.resolve(container: self, args: args)        return instance as? T    }}

Давайте разбираться.


Класс ContainerMock наследуется от обычного Container, переопределяя метод makeInstance(args:), используемый контейнером для создания сущностей. Новая реализация пытается создать подставную зависимость вместо настоящей. Если ей это не удается, она печально разводит руками и фолбечится на реализацию базового класса.


Метод replace(_:with:) позволяет сконфигурировать моковый контейнер, указав тип зависимости и соответствующий ей тип мока. Эта информация хранится в словаре substitutions, который использует уже знакомый нам ObjectIdentifier для ключа и AnyResolvable для хранения типа мока.


Для создания моков используется метод makeInstance(args:), который по ключу пытается достать нужный AnyResolvable из словаря substitutions и создать соответствующий экземпляр с помощью метода resolve(container:args:).


Использовать все это дело мы будем следующим образом. Создаем моковый OrdersProvider, переопределяя метод loadOrders(for:date:):


final class OrdersProviderMock: OrdersProvider {    override func loadOrders(for customerId: Int, date: Date) {        print("Loading mock orders for customer '\(customerId)', date '\(date)'")    }}

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


let container = ContainerMock()container.replace(OrdersProvider.self, with: OrdersProviderMock.self)let viewModel: OrdersVM = container.resolve(args: .init(customerId: 42, date: Date()))viewModel.loadOrders()

Для справки, этот код производит следующий консольный вывод:


Loading mock orders for customer '42', date '2020-04-24 17:47:40 +0000'

Заключение


Сегодня мы вероломно поступились принципом инверсии зависимостей и в очередной раз изобрели велосипед, реализовав бюджетный DI с помощью анти-паттерна Service Locator. Попутно мы познакомились с парой полезных техник iOS-разработки, таких как type erasure и property wrappers, и не забыли про unit-тесты.


Автор не рекомендует использовать код из этой статьи в приложении для управления ядерным реактором, но если у вас небольшой проект и вы не боитесь экспериментировать свайп вправо, its a match <3




Весь код из этой статьи можно скачать в виде Swift Playground.

Подробнее..

HorizontalList с помощью SwiftUI

21.06.2020 10:10:12 | Автор: admin

Вступление.


SwiftUI это современный UI framework, который позволяет разработчикам быстро и легко создавать собственные приложения на всех платформах Apple.
Используя простой, понятный декларативный стиль, разработчики могут создавать потрясающие пользовательские интерфейсы с плавной анимацией. SwiftUI экономит время разработчиков, предоставляя огромное количество готовых решений, включая Interface Layout, Dark Mode, Accessibility, интернационализацию и многое другое. Приложения SwiftUI работают нативно и невероятно быстро. А поскольку SwiftUI это один и тот же API, встроенный в iOS, iPadOS, macOS, watchOS и tvOS, разработчики могут быстрее и проще создавать отличные нативные приложения для всех платформ Apple.


Звучит amazing, не правда ли?


Введение.


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



а сразу перейдем к практике и сделаем достаточно стандартную в повседневной жизни задачу создание горизонтального списка.


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


Глава 1. Что нам стоит горизонтальный ScrollView построить.


Горизонтальный список можно создать достаточно просто. Для этого необходимо поместить HStack в ScrollView и заполнить HStack нашими элементами:


var body: some View {    ScrollView(.horizontal) {        HStack {            ForEach(0...9, id: \.self) { index in                SomeAmazingView(atIndex: index)            }        }    }}

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


struct ContentView: View {    struct Constants {        static var itemsCount = 100    }    // MARK: - State        @State var items: [String] = []    // MARK: - Initialization    init() {        items = generateData()    }    // MARK: - View    var body: some View {        ScrollView(.horizontal) {            HStack {                ForEach(0..<items.count, id: \.self) { index in                    CardView(index: index, title: self.items[index])                        .frame(width: 150, height: 200)                        .padding(10)                }            }        }    }    // MARK: - Private Helpers    private func generateData() -> [String] {        var data: [String] = []        for _ in 0..<Constants.itemsCount {            data.append(String.randomEmoji())        }        return data    }}

Запускаем и вуаля, как и обещала Apple все нативно и невероятно быстро.


enter image description here




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


    ...    struct Constants {        static var itemsCount = 1000        ...    }    ...

Запускаем и ...


enter image description here


Глава 2. Если хочешь сделать что-то хорошо, сделай это сам.


Для решения этой проблемы в UIKit мы бы использовали UICollectionView. Но, к сожалению, не всё, что было возможно при использовании UIKit, имеет аналог в SwiftUI.


Конечно, можно было бы использовать UICollectionView напрямую в SwiftUI. Как это сделать можно прочитать здесь. Но это уже не SwiftUI и определенно не наш путь.


На данный момент единственная структура в SwiftUI, которая загружает и отображает данные только по необходимости (on demand) это List.


@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)public struct List<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {...}@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)extension List {    ...    /// Creates a List that computes its rows on demand from an underlying    /// collection of identified data.    @available(watchOS, unavailable)    public init<Data, RowContent>(_ data: Data, selection: Binding<Set<SelectionValue>>?, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent) where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable    }    ...}

Возьмем это решение от Apple и создадим схожее API для нашей структуры HorizontalList:


public struct HorizontalList<Content, Data> : View where Content : View, Data: RandomAccessCollection, Data.Element: Hashable {    // MARK: - Properties    private let data: [Data.Element]    private let itemContent: (Data.Element) -> Content    // MARK: - Initialization    public init(_ data: Data, @ViewBuilder itemContent: @escaping (Data.Element) -> Content) {        self.itemContent = itemContent        if let range = data as? Range<Int> {            self.data = Array(range.lowerBound..<range.upperBound) as! [Data.Element]        } else if let closedRange = data as? ClosedRange<Int> {            self.data = Array(closedRange.lowerBound..<closedRange.upperBound) as! [Data.Element]        } else if let array = data as? [Data.Element] {            self.data = array        } else {            fatalError("Unsupported data type.")        }    }    // MARK: - View    public var body: some View {        ZStack {            if !self.data.isEmpty {                ForEach(0..<self.data.count, id: \.self) { index in                    self.makeView(atIndex: index)                }            }        }    }    // MARK: - Private Helpers    private func makeView(atIndex index: Int) -> some View {        let item = data[index]        let content = itemContent(item)        return content    }}

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


var body: some View {    HorizontalList(0..<items.count) { index in        CardView(index: index, title: self.items[index])            .frame(width: Constants.itemSize.width, height: Constants.itemSize.height)            .padding(10)    }}

Запускаем и (через какое-то время..) видим, что карточки успешно загрузились и отобразились:


enter image description here


Глава 2. Это особая, Layout магия.


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


GeometryReader - A container view that defines its content as a function of its own size and coordinate space.


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


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


В SwiftUI есть механизм, который позволяет добавлять некоторые атрибуты к View. Эти атрибуты называются "Preferences". При изменении этих атрибутов мы будем получать callback.


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


struct ViewRectPreferenceData: Equatable {    let index: Int    let rect: CGRect}

Следующим шагом создадим сам аттрибут, поддерживающий протокол PreferenceKey и содержащий в себе массив значений ViewRectPreferenceData.


struct ViewRectPreferenceKey: PreferenceKey {    typealias Value = [ViewRectPreferenceData]    static var defaultValue: [ViewRectPreferenceData] = []    static func reduce(value: inout [ViewRectPreferenceData], nextValue: () -> [ViewRectPreferenceData]) {        value.append(contentsOf: nextValue())    }}

Так как нам нужно будет знать размеры элементов, а сделать это можно только с помощью GeometryReader, мы создадим специальную View, которая будет добавлена как child к нашим элементам и, как результат, иметь GeometryReader с размерами нашего элемента.


struct PreferenceSetterView: View {    let index: Int    let coordinateSpaceName: String    var body: some View {        GeometryReader { geometry in            Rectangle()                .fill(Color.clear)                .preference(key: ViewRectPreferenceKey.self,                            value: [ViewRectPreferenceData(index: self.index, rect: geometry.frame(in: .named(self.coordinateSpaceName)))])        }    }}

Добавим созданный PreferenceSetterView к нашим элементам, как background:


private func makeView(atIndex index: Int) -> some View {    ...    return content            .background(PreferenceSetterView(index: index, coordinateSpaceName: Constants.coordinateSpaceName))}

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


...struct  Constants {    static  var  coordinateSpaceName: String {        return  "HorizontalListCoordinateSpaceName"    }}@State private var rects: [Int: CGRect] = [:]...public var body: some View {    GeometryReader { geometry in        ZStack {            ...         }         .onPreferenceChange(ViewRectPreferenceKey.self) { preferences in                for preference in preferences {                    var rect = preference.rect                    if let prevRect = self.rects[preference.index - 1] {                        rect = CGRect(x: prevRect.maxX, y: rect.minY, width: rect.width, height: rect.height)                    }                    self.rects[preference.index] = rect          }          .coordinateSpace(name: Constants.coordinateSpaceName)        }}

Про Preferences eсть хорошая серия статей из 3-х частей:
Часть 1
Часть 2
Часть 3


Глава 3. Ты видишь только то, что тебе показывают.


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


@State  private  var  visibleIndices: ClosedRange<Int> = 0...0...public var body: some View {        GeometryReader { geometry in            ZStack {                if !self.data.isEmpty {                    ForEach(self.model.visibleIndices, id: \.self) { index in                        self.makeView(atIndex: index)                     }                 }            }            .onAppear() {                self.updateVisibleIndices(geometry: geometry)            }            .onPreferenceChange(ViewRectPreferenceKey.self) { preferences in                 ...                 self.updateVisibleIndices(geometry: geometry)             }        }}...private func updateVisibleIndices(geometry: GeometryProxy) {    let bounds = geometry.frame(in: .named(Constants.coordinateSpaceName))    let visibleFrame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)    var frameIndices: [Int] = []    for (index, rect) in rects {        if rect.intersects(visibleFrame) {            frameIndices.append(index)        }    }    frameIndices.sort()    let firstIndex = frameIndices.first ?? 0    var lastIndex = frameIndices.last ?? 0    if rects[lastIndex]?.maxX ?? 0 < visibleFrame.maxX, lastIndex < data.count - 1 {        lastIndex += 1    }    visibleIndices = firstIndex...lastIndex}

(пытливый критический взгляд может заметить, что количество visibleIndices не может быть меньше 1 и правильней было бы иметь это значение опциональным, но для простоты оставим как есть)


Протестируем, используя метод onAppear(), который вызывается при появлении элемента на экране:


CardView(index: index, title: self.items[index])    .onAppear() {        print("Appeared index: \(index)")    }

Appeared index: 0Appeared index: 1Appeared index: 2

Отлично, как видно из лога после запуска появились на экран только 3 элемента.


Глава 4. Тянем-потянем.


Следующим шагом развития нашего компонента будет поддержка Drag Gestures для скроллинга данных. Так как изменение позиции скролла влияет на отображаемые элементы, переменные offset и dragOffset будут State переменными.


@State private var offset: CGFloat = 0@State private var dragOffset: CGFloat = 0var contentOffset: CGFloat {    return offset + dragOffset}

Добавим к нашему компоненту DragGesture и его обработчики:


public var body: some View {    GeometryReader { geometry in        ZStack {            ...        }        .gesture(             DragGesture()                .onChanged({ value in                     // Scroll by dragging                     self.dragOffset = -value.translation.width                     self.updateVisibleIndices(geometry: geometry)                 })                 .onEnded({ value in                      self.offset = self.offset + self.dragOffset                      self.dragOffset = 0                      self.updateVisibleIndices(geometry: geometry)                }))    }}

В результате у нас появится вычисляемый contentOffset, который мы будем применять для калькуляции видимого фрейма и позиций элементов:


private func makeView(atIndex index: Int) -> some View {    ...    return content              .offset(x: itemRect.minX - contentOffset)}private func updateVisibleIndices(geometry: GeometryProxy) {    ...    let visibleFrame = CGRect(x: contentOffset, y: 0, width: bounds.width, height: bounds.height)    ...}

Запускаем приложение:


enter image description here


Appeared index 0Appeared index 1Appeared index 2Appeared index 3Appeared index 4Appeared index 5Appeared index 6Appeared index 7Appeared index 8Appeared index 9Appeared index 10

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


Глава 5. Крутите барабан.


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


Любая анимация строится на изменении значений за какой-то период времени. Для того чтобы фиксировать периоды времени нам необходим таймер:


@State private var animationTimer = Timer.publish (every: 1/60, on: .current, in: .common).autoconnect()

Теперь когда у нас есть таймер, надо знать, что анимировать. Список получается довольно простой: startPosition, endPosition и scrollDuration. Единственное, так как нам не надо перегружать View при изменении какого-либо из этих значений, мы их упакуем в модель класса:


class HorizontalListScrollAnimator {    var isAnimationFinished: Bool = true    private var startPosition: CGFloat = 0    private var endPosition: CGFloat = 0    private var scrollDuration: Double = 0    private var startTime: TimeInterval = 0    func start(from start: CGFloat, to end: CGFloat, duration: Double = 1.0) {        startPosition = start        endPosition = end        scrollDuration = duration        isAnimationFinished = false        startTime = CACurrentMediaTime()    }    func stop() {        startPosition = 0        endPosition = 0        scrollDuration = 0        isAnimationFinished = true        startTime = 0    }    func nextStep() -> CGFloat {        let currentTime = CACurrentMediaTime()        let time = TimeInterval(min(1.0, (currentTime - startTime) / scrollDuration))        if time >= 1.0 {            isAnimationFinished = true            return endPosition        }        let delta = easeOut(time: time)        let scrollOffset = startPosition + (endPosition - startPosition) * CGFloat(delta)        return scrollOffset    }    private func easeOut(time: TimeInterval) -> TimeInterval {        return 1 - pow((1 - time), 4)    }}

И завершающим шагом интегрируем модель анимации с таймером в наше View:


private var scrollAnimator = HorizontalListScrollAnimator()public var body: some View {        GeometryReader { geometry in            ...        }        .gesture(            DragGesture()                ...                .onEnded({ value in                    let predictedWidth = value.predictedEndTranslation.width * 0.75                    if abs(predictedWidth) - abs(self.dragOffset) > geometry.size.width / 2 {                        // Scroll with animation to predicted offset                    self.dragOffset = 0                  self.scrollAnimator.start(from: self.offset, to: (self.offset - predictedWidth), duration: 2)                        self.animationTimer = Timer.publish (every: 1/60, on: .current, in:.common).autoconnect()                    } else {                        // Save dragging offset                        self.offset = self.offset + self.dragOffset                         self.dragOffset = 0                     self.updateVisibleIndices(geometry: geometry)         .gesture(             TapGesture()                 .onEnded({ _ in                      // Stop scroll animation on tap                            self.scrollAnimator.stop()                      self.animationTimer.upstream.connect().cancel()                    }))            }))            .onReceive(self.animationTimer) { _ in             if self.scrollAnimator.isAnimationFinished {                 // We don't need it when we start off                 self.animationTimer.upstream.connect().cancel()                 return             }             self.offset = self.scrollAnimator.nextStep()                self.updateVisibleIndices(geometry: geometry)            }        }    }

Глава 6. Граница на замке.


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


func safeOffset(x: CGFloat) -> CGFloat {    return x.clamped(to: 0...(maxOffset ?? CGFloat.greatestFiniteMagnitude))}

У CGFloat нет метода clamped, но его можно легко добавить с помощью расширения:


extension Comparable {    func clamped(to limits: ClosedRange<Self>) -> Self {        return min(max(self, limits.lowerBound), limits.upperBound)    }}

Ниже представлен полный код с получением возможного максимального отступа и использованием метода safeOffset:


...@State private var maxOffset: CGFloat?var contentOffset: CGFloat {    return safeOffset(x: offset + dragOffset)}...public var body: some View {        GeometryReader { geometry in            ...        }        .gesture(            DragGesture()                ...                .onEnded({ value in                    ...                    self.offset = self.safeOffset(x: self.offset + self.dragOffset)                     ...                 }))                 ...                 .onPreferenceChange(ViewRectPreferenceKey.self) { preferences in                      // Update subviews rects                      for preference in preferences {                          ...                          // Update max valid offset if needed                          if self.maxOffset == nil, let lastRect = self.rects[self.data.count - 1] {                              self.maxOffset = max(0, lastRect.maxX - geometry.frame(in: .global).width)                          }                      }                      ...                   }                   .onReceive(self.animationTimer) { _ in                        ....                        self.offset = self.scrollAnimator.nextStep()                        // Check if out of bounds                        let safeOffset = self.safeOffset(x: self.offset)                        if self.offset != safeOffset {                        self.offset = safeOffset                        self.dragOffset = 0                        ...                    }}

Глава 7. Кэш.


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


class HorizontalListModel<Content> where Content : View {    var cachedContent: [Int: Content] = [:]    init() {        NotificationCenter.default.addObserver(self,                                               selector: #selector(clearCacheData),                                               name: UIApplication.didReceiveMemoryWarningNotification,                                               object: nil)    }    @objc func clearCacheData() {        cachedContent.removeAll()    }}

...private let model = HorizontalListModel<Content>()...    private func makeView(atIndex index: Int) -> some View {        ...        var content = model.cachedContent[index]        if content == nil {            content = itemContent(item)            model.cachedContent[index] = content        }        return content                ...    }

enter image description here


Послесловие.


Готовый компонент вы можете найти по адресу: https://github.com/DistilleryTech/HorizontalList
Предыдущая наша статья на тему SwiftUI доступна здесь: http://personeltest.ru/aways/habr.com/ru/post/501790/


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


Всем счастливого WWDC2020!

Подробнее..

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. И ждите новых статей на Хабре)

Подробнее..

Развитие ARKit в этом году и новая возможность в ARKit 4 Location Anchors

28.06.2020 16:16:57 | Автор: admin

В последние годы я очень активно работаю с сфере разработки нативных игр под платформу Apple и интересуюсь возможной интеграцией игровых процессов в AR. Поэтому стараюсь следить за всеми обновлениями которые ежегодно анонсируются на WWDC. Хотя за последние годы не было никаких обновлений для SpriteKit, SceneKit и GameplayKit, но Apple продолжает активно обновлять и продвигать ARKit и RealityKit которые можно интегрировать с этими игровыми инструментами.

Хотя Apple мало уделила внимания дополненной реальности во время презентации на WWDC, но выпущенная на этой неделе новая версия комплекта программного обеспечения для разработчиков (SDK), действительно может оказаться весьма полезной и интересной для всех, кто занимается созданием AR-приложений. В ARKit 4 представлены новые возможности для разработчиков, которые доступны на всех iOS/iPadOS устройствах с процессором A12 Bionic и выше.

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

Инструмент также добавляет новые важные возможности обнаружения глубины, доступные на устройствах, оснащенных Apple LiDAR Scanner (в настоящее время доступно только в последних iPad Pro). Но, пожалуй, самое важное, что имеется в ARKit 4 это привязка к местоположению, которая позволяет разработчикам размещать виртуальный объект в определенном месте в реальном мире.

LiDAR: усовершенствованная система дистанционного отслеживания


Apple представила API Scene Geometry в ARKit 3.5 после выпуска линейки iPad Pro со сканерами LiDAR. Я ожидаю, что Apple добавит сканеры LiDAR в свои iPhone следующего поколении, которые планирует выпустить к концу этого года, так что именно эта функция, скорее всего, будет самой обсуждаемой во время следующего запуска.

Новый API Scene Geometry позволяет собирать информацию с помощью LiDAR-сканера и на базе полученных данных создавать топологическую карту окружающего мира. Эта информация может использоваться для идентификации определенных физических объектов, размещения их на сцене и создания игровой симуляции взаимодействия между объектами реального и виртуального мира.

ARKit 4 поможет размещать виртуальные объекты впереди или позади реальных людей и реалистично показывать события, используя функцию Ray Casting, чтобы размыть грань между тем, что действительно реально, и цифровым контентом.

В iOS 14 Apple расширяет возможности iOS устройств при поддержке сканера LiDAR, чтобы лучше определять расстояние между ними и объектами в виртуальной среде. Машинное обучение позволяет объединять цветное RGB-изображение, полученное с широкоугольной камеры устройства, с показателями глубины со сканера LiDAR для создания плотной глубокой картины. Эти данные глубины обновляются с частотой 60hz, что позволяет iOS не просто отобразить объекты на сцене, а обеспечить их размещение в реальном времени в виртуальной среде.

LiDAR также позволяет усовершенствовать функцию, называемую Ray Casting, которая представляет собой метод рендеринга, использующий вычислительную геометрию для создания трехмерного пространства на двухмерной плоскости. К тому же, Apple предоставила возможности отслеживания объектов еще в предыдущей версии ARKit, но их применение было доступно только для устройств с фронтальной камерой True-Depth. ARKit 4 существенно расширяет эти возможности, сделав доступной функцию отслеживания лица для всех устройств, оснащенных процессором A12 Bionic или более поздней версии, включая недавно выпущенный iPhone SE нового поколения. Отслеживание лиц позволяет разработчикам создавать приложения, которые помещают изображение человека поверх виртуального контента, и наблюдать за его выражением в режиме реального времени.


Location Anchors


Безусловно, возможности, предоставляемые сканером LiDAR весьма впечатляюще, на фоне всех новых функций, анонсированных Apple. Новая технология Location Anchors в ARKit 4 теперь выводит AR-контент более высокого качества на улицу, позволяя разработчикам задавать долготу и широту для размещения виртуальных объектов. Затем ARKit 4 использует эти координаты и данные из Apple Maps для размещения AR объекта в определенном месте, на определенной высоте в реальном мире.

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

В результате, когда разработчик помещает виртуальный объект в реальный мир, например, виртуальную скульптуру на оживленной площади, данный объект будет сохраняться и отображаться в этом месте таким образом, чтобы каждый, кто просматривает его с помощью AR-устройства Apple, мог его увидеть в данной локации. Location Anchors вначале появится в крупных городах, таких как Лос-Анджелес, Сан-Франциско, Чикаго, Майами и Нью-Йорк, а затем, уже в конце этого лета, станет доступно еще несколько городов.
Пример работы Location Anchor из WWDC сессии


Важность Location Anchors могут оценить многие разработчики и это демонстрирует далеко идущие планы Apple в развитии устройств и технологий, связанных с AR. Сегодня многие стартапы, ориентированные на технологии дополненной реальности, пытаются занять инновационный сегмент рынка, чтобы развивать ее функциональные возможности, а Apple спокойно запустила множество новых функций в ARKit 4 на этой неделе, причем, без всякой помпезности.

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

Как работает Location Anchors


С ARKit 4 теперь без проблем можно привязать AR-контент к определенному месту в реальном мире, просто задав координаты. Приложения на основе AR, которые ранее базировались на распознавании изображений или сканировании кода для запуска действий в определенном месте, теперь имеют возможность обновляться в реальном времени и использовать преимущества привязки к местоположению.

В рамках преобразования базы данных Apple Maps, компания длительное время собирала данные с камер и 3D LiDAR на городских улицах в разных уголках мира. Для привязки к местоположению ARKit загружает виртуальную карту, окружающую ваше устройство из облака и сопоставляет ее с данными камеры. Используя GPS, ARKit может быстро и точно определить ваше местоположение в реальном мире. Вся обработка информации происходит при помощи технологии машинного обучения прямо на вашем устройстве.

Доступность


Как я уже отмечал, функция Location Anchors или, другими словами Geo-Tracking, поддерживается на всех устройствах с GPS и чипом A12 и более новых моделях. Поскольку функция требует, чтобы эта область уже была ранее нанесена на карту Apple, она доступна только в определенных городах США. По состоянию на июнь 2020 года, в зоне ее поддержки оказалось только пять городов, но я очень надеюсь, что далее Apple будет расширять зону поддержки.

Geo-Tracking базируется на новой ARConfiguration, получившей название ARGeoTrackingConfiguration, которая позволяет легко проверять наличие совместимых устройств и их доступность.

Вначале следует проверить поддерживает ли ваше устройство данную фичу:
guard ARGeoTrackingConfiguration.isSupported else { return }

Теперь вы можете проверить, находится ли устройство в поддерживаемом городе. Если это так, запустите конфигурацию геотрекинга в ARView. Если вы используете RealityKit, вы не можете применить автоматическую настройку, поэтому придется запустить ее вручную.
ARGeoTrackingConfiguration.checkAvailability { available, error in    guard available else { return }    arView.session.run(ARGeoTrackingConfiguration())}

Важное примечание. Функция checkAvailability требует наличия сетевого подключения для загрузки AR ресурсов. Available будет false, если устройство не подключено к интернету.

Построение Location Anchors


Как же происходит функционирование Location Anchors? Ведь известно, что ARKit пользуется собственной системой координат для определения относительного положения устройства, в то время как в реальном мире местоположение описывается с широтой и долготой. В геотрекинге эта проблема решена созданием единой системы координат. Оси ARKit автоматически совпадают с компасом, где ось X указывает направление на восток, а ось Z на юг.

Все, что вам нужно знать для создания ARGeoAnchor это отдельные GPS-координаты. Вот как можно создать привязку, например, для определения местоположения моста Golden Gate Bridge. Для большей надежности используем систему координат с точностью до десятичных знаков.
let coordinate = CLLocationCoordinate2D(latitude: 37.8185, longitude: -122.4738)let geoAnchor = ARGeoAnchor(name: "Golden Gate Bridge", coordinate: coordinate)

При желании можно указать высоту в метрах, которая, по умолчанию, принята заданием параметров над уровнем моря.
let geoAnchor = ARGeoAnchor(name: "Golden Gate Bridge", coordinate: coordinate, altitude: 67)

Теперь можно добавить анкор к главной сцене. В RealityKit это работает на основе инструмента AnchorEntity из ARGeoAnchor.
arView.session.add(anchor: geoAnchor)let geoAnchorEntity = AnchorEntity(anchor: geoAnchor)arView.scene.addAnchor(geoAnchorEntity)

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

Преобразования между пространственными координатами


Если вы хотите получить координаты GPS для определенной точки в сцене вашего приложения, ARKit позволит легко сделать это с помощью ARGeoAnchor и системы координат XYZ ARKit. Достаточно одного касания на экране и привязка к местоположению готова!
let point = SIMD3<Float>([0, 1, -2])arView.session.getGeoLocation(forPoint: point) { coordinate, altitude, error inlet geoAnchor = ARGeoAnchor(coordinate: coordinate, altitude: altitude)}

Вот так все просто и доступно.

Прогресс AR-технологи неизбежен


Многие, кто пристально следит за развитием AR-технологий, периодически сетуют на медленный темп продвижения данного сектора технологий. Безусловно, многие из нас не отказались бы иметь, например, AR-очки от Apple уже сегодня и сейчас. В действительности, это достаточно сложная технология, и здесь более важна не скорость, а правильное ее внедрение. В дополнение к реальным проблемам по созданию таких устройств, которые связаны с оптикой, временем автономной работы, беспроводным подключением и многими другими аспектами, требуется еще и хороший AR контент с глубоким пониманием и отражением нашего реального, постоянно меняющегося мира. Лишь немногие компании имеют ресурсы, чтобы осилить данную проблему самостоятельно, среди них, кроме Apple, в числе более успешных, можно выделить Microsoft и Niantic, недавно приобретшей стартап 6D.AI.

Еще одну проблему по-прежнему создает недостаток аппаратных и программных платформ, на которых сможет работать AR-контент. С помощью ARKit 4 и iOS 14 Apple может существенно укрепить свои позиции в качестве крупнейшей в мире AR-платформы на рынке современных технологий, предоставив разработчикам новые инструменты для создания AR-приложений, которые давно ждут пользователи.

WWDC сессия посвященная новинкам в ARKit 4:
developer.apple.com/videos/play/wwdc2020/10611
Пример использования Location Anchors c исходным кодом:
developer.apple.com/documentation/arkit/tracking_geographic_locations_in_ar
Подробнее..

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!
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru