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

Xcode

Keychain API в iOS

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

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


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

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


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

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

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

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


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

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

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

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

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


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

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

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


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


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


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

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

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

14.02.2021 14:06:25 | Автор: admin
В новом дайджесте локализация и кастомные плагины, защита прав и неготовность Flutter, документация и тестирование, доходы подписок и легендарный симулятор Кобаяси Мару. Подключайтесь!



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

iOS

Создание пользовательских функций запросов с key paths
Процесс локализации iOS приложения в компании Vivid Money
AppsFlyer запускает предиктивную аналитику для iOS
Начинаем работу с Combine
Мошенничество в App Store: разработчик раскрывает многомиллионные аферы с приложениями
Hyundai не ведет переговоров с Apple
Советы по реализации темного режима в iOS
Набор инструментов iOS-разработчика на 2021 год
Поиск лучшего CI/CD для разработки под iOS
Swift простая обработка ошибок
Уловки iOS: автоматическая обработка клавиатуры
5 способов улучшить рабочий процесс в Xcode
Быстрое погружение в iOS-разработку
SPPermissions: получение разрешений в Swift
PermissionsSwiftUI: получение разрешений в SwiftUI

Android

SafetyNet Attestation описание и реализация проверки на PHP
Как заблокировать приложение с помощью runBlocking
Android Broadcast: новости #3
Чем различаются Dagger, Hilt и Koin
Android туториал: учим CRUD
Готовимся к декларативному UI
Три вещи, которые я перестала делать вручную как Android-разработчик
Внедрение нативного кода Kotlin во Flutter-приложение
ShapeableView в Jetpack Compose
Как мы ускорили запуск приложения Dropbox для Android на 30%
Компоненты Android View Binding: Диалоги и Адаптеры
GitHub Actions для Android-разработчиков
Expenso: контроль расходов
Auxio: плеер для Android

Разработка

Как создать кастомный плагин для Dart-анализатора
Защита авторских прав на ваши Pet-projects
На GitHub предлагают запустить каталог мобильных приложений
Выпускные проекты: как позаботиться о себе, завести питомца, найти пункт переработки и получить ответ на любой вопрос
Match-3 Framework это просто
Магия асинхронных операций: взгляд изнутри. Future
Connected! Самое главное о дизайне VPN-приложения
Mockito. Из чего он приготовлен и как его подавать?
Микромодульный подход к дизайну продукта
Стики и работа с Event System в Unity 3D
Какая бывает документация
Мобильное тестирование, автоматизация тестирования, тестирование API: с чем нужно уметь работать в 2021 году
Что разработчику нужно знать о работе с дизайном/дизайнером
Принципы нарративного дизайна
Оценка трудозатрат в веб- и мобильных проектах
Flutter. Асинхронность (async) <> параллельность (isolate). Совсем
Работа с адаптивным программируемым интерфейсом APIs во Flutter
Podlodka #202: офисная политика
Flutter Dev Podcast #24: Dart Null Safety
Flutter пока не смог стать надежным кроссплатформенным решением
Как попасть в геймдев: 5 игр, с которых стоит начать свой путь в разработке игр
Cocos переходит в 3D
Дизайн приложений: примеры для вдохновения #31
Эстетический и минималистичный дизайн как часть юзабилити
Unity за 1 минуту
Учимся программировать и писать игры на Nintendo Game Boy
Как 3 месяца парного программирования повлияли на мою карьеру разработчика
Устали от императивных циклов For? Используйте функциональные операторы
Объектно-ориентированное мышление слишком сложно для вас
Создание IoT-приложения, совместимого со смарт-устройствами, на Flutter
Ускоряем разработки приложений с помощью Flutter
20 лучших движков и платформ/инструментов для разработки мобильных игр в 2021 году
Как Material Design помогает брендировать ваше приложение
Как вести переговоры продуктового дизайнера и разработчика
Boardgame.io: движок для пошаговых игр

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

Scopely запустила симулятор Кобаяси Мару
Отчет Flurry 2021 State of Mobile
Расходы в Топ-100 приложений с подпиской выросли на 34% до $13 млрд
App Annie Pulse: инсайты рынка приложений
78% пользователей отказывалось от покупки, если требовалась установка приложения
Новый рейтинг мобильных рекламных сетей Singular 2021 ROI Index
Роскомнадзор выпустил мобильное приложение
Apple начала показывать рекламу на странице поиска
Electronic Arts покупает Glu Mobile
Beam: осмысленный браузер
Blizzard готовит несколько мобильных игр World of Warcraft
Как увеличить revenue мобильного приложения на 10-15% с помощью специальных инструментов от Apple
Оптимизируйте удержание приложений с помощью модели Hooked
20 ужасных ASO-ошибок, которых нужно избежать в 2021

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

ESP32-C3: первое знакомство. Заменим ESP8266?
Как машинное обучение и TensorFlow помогают готовить гибридную выпечку: хобби-кейс разработчика Google
Как построить AI-друга. Расшифровка доклада

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

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

21.02.2021 14:07:08 | Автор: admin
В этом выпуске цвета Swift, переиспользуемый чистый Kotlin, выход первой версии Android 12 и страсти по IDFA, дефекты Qt и бриллиантовый чекаут, секреты маркетинга приложений, игровые боты, знания за 5 минут и многое другое.



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

iOS

Предотвращаем мерж-конфликты с XcodeGen
Цвета в Swift: UIColor
Распознание блоков текста в iOS-приложении с помощью Vision
Apple начала бороться с иррационально высокими ценами в приложениях?
Забанила ли Apple аналитические SDK? Ээ ну
Взлом нативных двоичных файлов ARM64 для запуска на симуляторе iOS
Погружение в CFRunLoop
Создайте новостное приложение в SwiftUI 2.0 (Combine, API, MVVM & Swift Package Manager)
Используем Charles для переписывания ответов при разработке приложений для iOS
Clubhouse-подобное изображение в профиле на Swift
Создаем анимированные круговые и кольцевые диаграммы в SwiftUI
Создание рулетки на SwiftUI
OnTap: документация по SwiftUI
WatchLayout: круги в UICollectionView
SPAlert: уведомления в стиле Apple

Android

Как писать и переиспользовать код на чистом Kotlin. Заметки Android-разработчика
Как найти подходящую абстракцию для работы со строками в Android
Темы, стили и атрибуты
Вышла превью-версия Android 12
GitHub Actions для Android-разработки
Как мы ускорили запуск приложения Dropbox для Android на 30%
Как изменится дизайн в Android 12
Контрольный список качества приложения
Анти-паттерны RecyclerView
StateFlow с одно- и двусторонним DataBinding-ом на Android
Как на самом деле работает RxJava
Готовим наши приложения к Jetpack Compose
Простое создание параллакса на Jetpack Compose
5 расширений Kotlin, которые сделают ваш Android-код более выразительным
IridescentView: переливающиеся изображения для Android
stackzyr: Jetpack Compose для десктопов

Разработка

Обработка дат притягивает ошибки или 77 дефектов в Qt 6
Запуск топ-приложения в одиночку, бесплатно и без кодинга (ну почти)
Как мы накосячили пока делали Бриллиантовый чекаут 9 месяцев, а планировали 2
1 год с Flutter в продакшне
Тесты должна писать разработка (?)
Опыт разработки первой мобильной игры на Unity или как полностью перевернуть свою жизнь
О поиске утечек памяти в С++/Qt приложениях
Стратегия тестирования краткосрочного проекта
Готовим Большую Фичу на Kotlin Multiplatform. Доклад Яндекса
ZERG что за зверь?
Podlodka #203: платежи
Microsoft открывает Dapr для простого развертывания микросервисов
Задачи с собеседований: 2 в 64 степени
Дизайн приложений: примеры для вдохновения #32
Как сделать инсайты UX-исследований видимыми, прослеживаемыми и увлекательными?
5 вопросов на интервью для выявления выдающихся программистов
Как создать простое шахматное приложение с помощью Flutter
Создавая бэкенд Uber: пошаговое руководство по системному дизайну
5 удивительных преимуществ обмена знаниями в качестве разработчика
Чтение кода это навык
Почему я перестал читать статьи Как стать разработчиком программного обеспечения
Психология дизайна и нейробиология, стоящая за классным UX
Удаленное определение частоты пульса с помощью веб-камеры и 50 строк кода
Как разозлить разработчика
7 обязательных навыков, чтобы стать выдающимся разработчиком

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

Кратко о продуктовых метриках
Маркетологи в мобайле: Денис Нуждин (Пятёрочка Доставка)
Секреты маркетинга приложений для знакомств новое руководство Adjust
Среда совместного программирования Replit получила $20 млн
Photomath получил еще $23 млн.
Post-IDFA Alliance открыл сайт Нет IDFA? Нет проблем
Взрослые в США в 2020 прибавили сразу час цифрового времени
ВКонтакте запустил новый инструмент для автоматизированной рекламы приложений
Отчет Состояние рынка приложений для фитнеса и здоровья 2021
Jigsaw получает $3.7 млн на дейтинг с головоломкой
Uptime: знания за пять минут
Как запустить wellness-стартап на свои деньги, совмещать с постоянной работой и не сойти с ума
Что будет с трекингом мобильных приложений в 2021 году
Новая норма: обучение в приложениях и как добиться успеха в меняющиеся времена
Лучшие маркетинговые метрики для отслеживания показателей роста
Вот почему разработчикам не удается добиться успеха в карьере
Как я занимался маркетингом своей игры, продажи которой за год составили 128 тысяч долларов

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

Cчетчик газа в Home Assistant без паяльника
Устройство игрового бота: 16-е место в финале Russian AI Cup 2020 (и 5-е после)
Умный дом с нуля своими руками или путешествие длиною в год
Как распознать рукописный текст с помощью ИИ на микроконтроллерах
Часы для обнаружения жестов на основе машинного обучения, ESP8266 и Arduino
Как преобразовать текст в речь с использованием Google Tesseract и Arm NN на Raspberry Pi
Быстрый прототип IIoT-решения на Raspberry PI и Yandex IoT. Часть вторая
Первый опыт с Raspberry Pi или микросервисы для дома
Google сворачивает Swift для TensorFlow

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

1008F или как раскирпичить свой Mac

29.12.2020 16:19:06 | Автор: admin

Всем привет! В этом посте речь пойдет о бесконечном режиме восстановления macOS, ошибках 1008F, 2003F, 2004F и о том как их побороть.

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

Предыстория

Одним осенним прохладным днем, пришло мне обновление Xcode 12.2 , а вместе с ним и macOS Big Sur. После обновления Xcode, он стал жутко тормозить, зависать, вылетать и терять последние изменения. Через несколько попыток переустановки Xcode, было принято решение обновиться до Big Sur. В принципе, проблему это не решило, зато багов докинуло. Затем, начался процесс переустановки macOS Big Sur с загрузочной флешки и из проблем осталось только отсутствие поддержки симуляторов iOS < 12. Для меня это было критично (#яжеразработчик) и,не долго думая, было решено вернуть обратно macOS Catalina.

Тут стоить отметить, что далее речь идет о MacBook pro 2018 с чипом безопасности T2, опыт работы с macOS с точки зрения откатов, переустановок, загрузочных дисков и т.п. имелся богатый, а потому..ничто не предвещало беды.

Поехали!

Мне было лениво делать загрузочную флешку, поэтому идея с Internet Recovery показалась заманчивой (более того эту процедуру я уже обкатывал ранее на MacBook pro 2013). Далее список действий, которые повторять НЕ НАДО:

1. загрузка в рекавери (cmd + R);

2. форматирование жесткого диска;

3. запуск Internet Recovery на версию, которая поставлялась при продаже MacBook (или близкую к ней (Shift-Option-Command-R при загрузке Mac).

**более подробно о сочетаниях клавиш можно прочитать тут

После всех этих нехитрых манипуляций мы получаем не Mac, а кирпич, который игнорит все подряд и валится в вечный Internet Recovery с ошибкой 1008F.

1008F

1008F - это ошибка, указывающая на то, что ваш Mac заблокирован на серверах Apple. Звучит страшно. Решается просто, но не всегда.

Дальше у вас, как говорится, два путя:

Путь простой:

1. Зайти в учетную запись icloud;

2. Выбрать "Найти iPhone";

3. Переключить дроп-лист на пункт "Все устройства":

4. Выбрать проблемный MacBook и нажать "удалить из Найти айфон";

5. Зайти в программу бета-тестирования;

6. Покинуть программу:

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

Я позвонил в службу поддержки Apple, где мне сообщили о том, что в моем случае 1008F возникает исключительно из - за плохого интернета (на самом деле из-за плохого интернета возникают ошибки 200+F). Также, мне посоветовали обратиться в авторизованный сервис (что логично) т.к. там и специалисты граммотные, и интернет хороший. Тут стоить отметить, что претензий к поддержке Apple я не имею. Было опробовано несколько Wi-Fi сетей в т.ч. и с мобильных устройств. Итог один - не помогло.

Путь сложный:

Далее возникла идея: поскольку жесткий диск несъёмный, слишком дорого было бы для Apple решать такие вопросы заменой материнских плат. Должна была быть какая-то лазейка, типа DFU режима, который был очень популярен на айфонах 3gs и 3g. Легкий гуглинг навел меня на несколько интересных статей: тут и тут. Дублировать содержимое статей смысла не вижу, в целом, они о том как вводить Mac в DFU режим и как с ним работать.

1. Нам нужен еще один Mac (к счастью такой нашелся);

2. Соединяем наш Mac (клиент) со вторым Mac (сервер) кабелем питания UCB-C - UCB-C(руководство по ссылкам выше);

3. Скачиваем на Mac (сервер) утилиту Apple Configurator 2 и запускаем ее;

4. Вводим Mac (клиент) в DFU;

5. В утилите Apple Configurator 2: Правая кнопка мыши > Actions > Advanced > Revive Device:

6. После того как все loading - индикаторы прокрутятся:

а на Mac (клиент) произойдет вот это:

нужно попробовать запустить процедуру восстановления через Shift-Option-Command-R.

7. Если вы по прежнему получаете 1008F (не 2003F, 2004F - о них позже), переходите к п8.

8. Требуется повторить действия с п.1 по п.4. После чего выбрать Apple Configurator 2 пункт Restore.

9. У вас надеюсь все будет хорошо, а вот я получил сообщение об ошибке:

что-то типа такого, только код был другой.

10. Далее я вывел Mac (клиент) из DFU режима и загрузил его через Shift-Option-Command-R.

11. Начался заветный процесс восстановления, который переодически падал в ошибки 2003F и 2004F.

2003F, 2004F

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

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

  1. На роутере я прописал DNS: основной сервер 8.8.8.8, альтернативный 8.8.4.4;

  2. Сделал WI-FI сеть без пароля, но с фильтрацией по MAC - адресам, поскольку наткнулся на информацию о том, что Mac в процессе Internet Recovery может забывать пароль от WI-FI;

  3. Также могут помочь сброс NVRAM или PRAM;

  4. Запускать Mac через Shift-Option-Command-R, можно даже после того, как вы получили ошибку 200+F. Бывают случаи, когда загрузка происходит не с первого раза;

  5. В моем случае, я дождался 6 утра, пока основные пользователи моего провайдера спят, а в Купертино - ночь, значит нагрузка на сервера Apple значительно меньше. Загрузил Mac через Shift-Option-Command-R и случилось чудо.

  6. Дальше у меня загрузился Recovery macOS Mojave, т.к. именно с ней поставлялся MacBook. В дисковой утилите жесткий диск определялся как неизвестное устройство, после форматирования его со схемой разделов GUID, установка macOS продолжается в обычном режиме.

Заключение

На всю эту историю у меня ушло в сумме около трех дней, поэтому если этот пост сэкономит кому-нибудь хоть каплю времени и нервов - будет отлично. Тем не менее, прошу обратить внимание: описанное выше происходило со мной, у вас может быть иначе. Все действия вы выполняете на свой страх и риск. От себя - я бы рекомендовал перед переустановкой macOS включать загрузку с USB - носителей, отвязывать Mac от учетки и выполнять установку с флешки. Жалею ли я о том, что не сделал так сам? - Нет :)

Желаю вам легких апдейтов, даунгрейдов и вообще поменьше багов и лагов.

Подробнее..

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

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

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


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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

@_exported import ObjCEigen

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

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

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

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

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

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

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


Ссылки

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

Подробнее..

Перевод Запускаем модель машинного обучения на iPhone

18.04.2021 18:19:42 | Автор: admin

Чего уж только на Хабре не было, и DOOM на осциллографе, тесте на беременности и калькуляторе запускали, даже сервер Minecraftна зеркалке Canon 200D поднимали. Сегодня же, специально к старту нового потока курса по Machine Learning и углубленного Machine Learning и Deep Learning, попробуем описать кратчайший путь от обучения модели машинного обучения на Python до доказательства концепции iOS-приложения, которое можно развернуть на iPhone. Цель статьи дать базовый скаффолдинг, оставляя место для дальнейшей настройки, подходящей для конкретного случая использования.


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

Шаг 1. Настройка среды

Во-первых, давайте создадим виртуальную среду Python под названием .core_ml_demo, а затем установим необходимые библиотеки: pandas, scikit-learn и coremltools. Чтобы создать виртуальную среду, выполните в терминале эти команды:

python3 -m venv ~/.core_ml_demosource  ~/.core_ml_demo/bin/activatepython3 -m pip install \pandas==1.1.1 \scikit-learn==0.19.2 \coremltools==4.0

Далее установим Xcode. XCode это инструментарий разработки для продуктов Apple. Обратите внимание, что Xcode довольно большой (больше 10 ГБ). Я бы порекомендовал выпить чашку кофе или запустить установку на ночь.

Примечание: в этом туториале используется Xcode 12.3 (12C33) на MacOS Catalina 10.15.5.

Шаг 2. Обучение модели

Мы будем использовать набор данных Boston Housing Price от scikit-learn для обучения модели линейной регрессии и прогнозирования цен на жильё на основе свойств и социально-экономических атрибутов.

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

import pandas as pdfrom sklearn.linear_model import LinearRegressionfrom sklearn.datasets import load_boston# Load databoston = load_boston()boston_df = pd.DataFrame(boston["data"])boston_df.columns = boston["feature_names"]boston_df["PRICE"]= boston["target"]y = boston_df["PRICE"]X = boston_df.loc[:,["RM", "AGE", "PTRATIO"]]# Train a modellm = LinearRegression()lm.fit(X, y)

Шаг 3. Преобразование модели в Core ML

Apple предоставляет два способа разработки моделей для iOS. Первый, Create ML, позволяет создавать модели полностью в рамках экосистемы Apple. Второй, Core ML, позволяет интегрировать модели от третьих лиц в платформу Apple, преобразовав их в формат Core ML. Поскольку мы заинтересованы в запуске обученной модели на iOS, воспользуемся вторым способом.

Перед импортом в Xcode мы преобразуем нашу модель sklearn в формат Core ML (.mlmodel) с помощью пакета python coremltool; coremltools позволяет назначать метаданные объекту модели, такие как информация об авторстве, описание функций модели и результатов.

# Convert sklearn model to CoreMLimport coremltools as cmlmodel = cml.converters.sklearn. \convert(lm,        ["RM", "AGE", "PTRATIO"],        "PRICE")# Assign model metadatamodel.author = "Medium Author"model.license = "MIT"model.short_description = \"Predicts house price in Boston"# Assign feature descriptionsmodel.input_description["RM"] = \"Number of bedrooms"model.input_description["AGE"] = \"Proportion of units built pre 1940"model.input_description["PTRATIO"] = \"Pupil-teacher ratio by town"# Assign the output descriptionmodel.output_description["PRICE"] = \"Median Value in 1k (USD)"# Save modelmodel.save('bhousing.mlmodel')

Шаг 4. Создание нового проекта в Xcode

С Python мы закончили. Теперь можно завершить прототип приложения при помощи только Xcode и Swift. Это можно сделать так:

  1. Откройте Xcode и создайте новый проект.

  2. Выберите iOS как тип мультиплатформы.

  3. Выберите тип приложения App.

Создание нового проекта Xcode для iOSСоздание нового проекта Xcode для iOS

4. Дайте проекту название и выберите интерфейс SwiftUI.

Конфигурация проектаКонфигурация проекта

Теперь просто перетащите созданный на третьем шаге файл модели .ml в каталог Xcode. Xcode автоматически сгенерирует класс Swift для вашей модели, как показано в редакторе ниже. Если вы посмотрите на класс, то заметите, что он содержит детали, которые мы ввели при сохранении нашей модели на Python с помощью coremltools, такие как описания объектов и целевых полей. Это удобно при управлении моделью.

Импорт файла .coreml в проект XcodeИмпорт файла .coreml в проект Xcode

Шаг 5. Создание UI модели

Далее создадим базовый пользовательский интерфейс, изменив файл contentView.swift. Приведённый ниже код на Swift отображает пользовательский интерфейс, который позволяет пользователям настраивать атрибуты дома, а затем прогнозировать его цену. Есть несколько элементов, которые мы можем здесь рассмотреть.

NavigationView содержит необходимый пользовательский интерфейс. Он включает:

  • Степпер (строки 1930) для каждой из наших трёх функций, который позволяет пользователям изменять значения функций. Степперы это в основном виджеты, которые изменяют @State атрибутных переменных нашего дома (строки 68).

  • Кнопку на панели навигации (строки 3140) для вызова нашей модели из функции predictPrice (строка 46). На экране появится предупреждающее сообщение с прогнозируемой ценой.

За пределами NavigationView у нас есть функция predictPrice (строки 4662). Она создаёт экземпляр класса Swift Core ML model и генерирует прогноз в соответствии с хранящимися в состояниях объектов значениями.

import SwiftUIimport CoreMLimport Foundationstruct ContentView: View {  @State private var rm = 6.5  @State private var age = 84.0  @State private var ptratio = 16.5      @State private var alertTitle = ""  @State private var alertMessage = ""  @State private var showingAlert = false      var body: some View {      NavigationView {        VStack {        Text("House Attributes")            .font(.title)        Stepper(value: $rm, in: 1...10,                step: 0.5) {            Text("Rooms: \(rm)")          }          Stepper(value: $age, in: 1...100,              step: 0.5) {          Text("Age: \(age)")          }          Stepper(value: $ptratio, in: 12...22,              step: 0.5) {          Text("Pupil-teacher ratio: \(ptratio)")          }          .navigationBarTitle("Price Predictor")          .navigationBarItems(trailing:              Button(action: predictPrice) {                  Text("Predict Price")              }          )          .alert(isPresented: $showingAlert) {              Alert(title: Text(alertTitle),                    message: Text(alertMessage),              dismissButton: .default(Text("OK")))          }        }      }  }            func predictPrice() {    let model = bhousing()    do { let p = try      model.prediction(          RM: Double(rm),          AGE: Double(age),          PTRATIO: Double(ptratio))        alertMessage = "$\(String((p.PRICE * 1000)))"      alertTitle = "The predicted price is:"  } catch {    alertTitle = "Error"    alertMessage = "Please retry."  }    showingAlert = true}}struct ContentView_Previews: PreviewProvider {    static var previews: some View {        ContentView()    }}

И, наконец, самое интересное: мы можем создать и запустить симуляцию приложения в Xcode, чтобы увидеть нашу модель в действии. В приведённом ниже примере я создал симуляцию с помощью iPhone 12.

Симуляция модели работает на iOS.Симуляция модели работает на iOS.

Заключение

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

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

Если вы кодите на Python и столкнулись в работе с задачами машинного обучения обратите внимание на наш курс Machine Learning, на котором вы сможете систематизировать и углубить полученные самостоятельно знания, пообщаться с профессионалами и применить модели Machine Learning на практике. Даже можно будет ворваться на хакатоны Kaggle.

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

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

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

image


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


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


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


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


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


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


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


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


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


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


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


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

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


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

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


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


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


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


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



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


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


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


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


О прокрутке


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


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


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


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



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


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


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


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


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


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


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


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



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


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


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


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


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


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


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


О библиотеке


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


Достоинства


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

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


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


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

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


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






Подробнее..

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

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

image


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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


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

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


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


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


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


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


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


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



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


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


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


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


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


О прокрутке


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


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


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


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


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


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


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



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


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


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


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


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


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


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


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



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


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


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


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


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


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


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


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


О библиотеке


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


Достоинства


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

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


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


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

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


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






Подробнее..

AppCode 2020.3 локализация для Swift, переход к определению до индексации, улучшенные рефакторинги и многое другое

14.12.2020 12:16:00 | Автор: admin

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


КПДВ



Поддержка Swift


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


  • SE-0279, SE-0286: Multiple trailing closure syntax.
  • Allow synthesis of Equatable and Hashable in conditional conformances (see the SE-0185 amendment).
  • SE-0276: Multi-pattern catch clauses.
  • SE-0269: Increased availability of implicit self in @escaping closures when reference cycles are unlikely to occur.
  • SE-0044: Import as member (OC-20445).
  • SE-0280: Enum cases as protocol witnesses.

Локализация


В AppCode давно есть локализация для строк в Objective-C, в этом релизе реализовали то же самое для Swift:


  • Добавили действие для выделения строки в .strings-файл: Локализация строки
  • Сделали фолдинг для NSLocalizedString: Фолдинг для локализованных строк
  • Реализовали навигацию, автодополнение и поиск использований для ключей локализации.

Действия для изменения кода


Добавили несколько небольших, но полезных действий по модификации кода:


  • Проверку и удаление ненужных self:Проверка и удаление ненужных self
  • Действие для удаления ненужных аргументов в замыканиях: Удаление ненужных списков аргументов
  • Конвертацию замыканий в конце выражения в аргументы метода (и наоборот):Замыкание в аргумент метода
  • Превью для быстрых исправлений: Превью

Change Signature


Rename, который работает для смешанного Objective-C/Swift кода, у нас уже есть. А в этом релизе доработали Change Signature, чтобы он тоже работал сразу же со смешанным кодом. Кроме этого:


  • Добавили выбор типа throw в диалог рефакторинга: Change Signature
  • Стали нормально обрабатывать значения по умолчанию для аргументов и variadic-параметры
  • Стали правильно показывать превью для init-методов.

Rename


Сделали новое отображение для настроек рефакторинга Rename открыть их можно по :


Rename


Переход к определению типа


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


Отладчик


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


  • Возможность просмотреть поля переменной прямо в редакторе и добавить ее в Inline Watches:
    Inline watches
  • Отображение Inline Watches в табе Variables:Inline Watches
  • Стрелочка счетчика команд, которую можно двигать во время отладки: Program counter

Code With Me


Code With Me


Многие, наверное, слышали про новый сервис от JetBrains для совместного редактирования кода Code With Me. Теперь он работает в AppCode через соответствующий плагин. Подробнее про него можно прочитать вот тут.


Контроль версий


Теперь вместо changelistов можно включить git stage:


Git stage


А Search Everywhere получил новый таб для поиска по коммитам:


Git tab


Поддержка XCFrameworks


Это про сущности из .xcframework теперь они корректно определяются IDE.


Просмотр определения


Возможен прямо из Project view с помощью Space:


Просмотр определения


На этом всё! Все вопросы и пожелания пишите прямо тут в комментариях будем рады ответить!


Команда AppCode

Подробнее..

XCResult как и зачем читать

04.03.2021 16:23:55 | Автор: admin


В 2018 году Apple в очередной (третий) раз обновили формат, в котором выдаётся информация о прогоне тестов. Если раньше это был plist файл, который представлял из себя большой xml, то теперь это большой файл с расширением xcresult, который открывается через Xcode и содержит в себе кучу полезной информации, начиная c результатов тестов с логами, скриншотами и заканчивая покрытием таргетов, диагностической информацией о сборке и многим другим. Большинство разработчиков не работает каждый день с этим, но инфраструктурщики в данной статье могут найти что-то полезное.

Разложим по полочкам плюсы и минусы обновления формата


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

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

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

Зачем читать XCResult не через Xcode?


Если у вас в компании настроены процессы CI&CD, то наверняка вы собираете метрики по сборкам проекта, по стабильности и количеству тестов, и, конечно, данные по тестовому покрытию. Скорее всего, где-нибудь на Bamboo, Jenkins, Github у вас рисуются упавшие тесты или статус CI, или процент покрытия. Такие операции принято автоматизировать и отдавать на откуп бездушным машинам. Какие инструменты есть у нас для этого?
Apple, вместе с релизом нового формата, выпустили и инструменты xcresulttool и xccov, с которыми можно работать из терминала.

Что мы можем достать, используя xccov?


xcrun xccov view --report --json /path/to/your/TestScheme.xcresult

Запрос вернёт исчерпывающую информацию о том, каким покрытием обладают все таргеты, какие методы и каких классов покрыты, сколько раз они были выполнены и какие строчки выполнялись. Объекты обладают схожей структурой. Всего там 4 уровня: корень, таргет, файл, функция. Все уровни, кроме корневого, имеют поле name. Во всех уровнях есть поля coveredLines и lineCoverage. Важно отметить, что объекты имеют какой-то собственный контекст. Всю структуру можно описать в несколько протоколов.



Помимо протоколов выделим следующие структуры: CoverageReport агрегатор всего и корень. Он содержит в себе массив объектов Target. Каждый Target содержит в себе массив File, которые, в свою очередь, содержат массив Function. Эти объекты будут реализовывать протоколы, которые описаны выше.
Нас интересует поле lineCoverage. Для составления красивого отчета (как в fastlane) обратимся к полю lineCoverage и пройдем по всем объектам нехитрой функцией:



Получим что-то похожее на:

Coverage Report Summary:

Utils.framework: 51,04 %

NavigationAssistantKit.framework: 0,0 %

NavigationKit.framework: 35,85 %

Logger.framework: 20,32 %

FTCCardData.framework: 78,21 %

FTCFeeSDK.framework: 25,25 %

ErrorPresenter.framework: 2,8 %

MTUIKit.framework: 0,24 %

AnalyticsKit.framework: 47,52 %

EdaSDK.framework: 1,18 %

Alerts.framework: 85,19 %

Resources.framework: 39,16 %

QpayApiTests.xctest: 88,37 %

FTCFeeSDKTests.xctest: 97,91 %


P.S. Для того, чтобы coverage собирался, необходимо добавить в вашу команду тестирования параметр -enableCodeCoverage YES или включить в настройках схемы в Xcode.

Какие возможности даст xcresulttool?


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

Для начала неплохо ознакомиться с самим интерфейсом:

xcrun xcresulttool --help

OVERVIEW: Xcode Result Bundle Tool (version 16015)

USAGE: xcresulttool subcommand [options] ...

SUBCOMMANDS:

export Export File or Directory from Result Bundle

formatDescription Result Bundle Format Description

get Get Result Bundle Object

graph Print Result Bundle Object Graph

merge Merge Result Bundles

metadata Result Bundle Metadata

version XCResultKit Version


Чтобы прочитать структуру, нам достаточно вызвать команду:

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json

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

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json --id {id}

Тогда мы получим объекты с тест-таргетами, типом тестов, которые разбиты по тест-классам и test suits с отчётами с логами, скриншотами, временем выполнения и прочей информацией по каждому тесту.
К сожалению, причину падения красных тестов не получится вытащить просто для этого придётся делать ещё один запрос на каждый упавший тест (а на самом деле даже не один! Если тест крэшнул, то крэшлоги вместе со стректрейсом лежат в другом месте и это ещё один запрос!

Для Failure Summary используется тот же запрос:

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json --id {id}

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

Что делать с этими справочными знаниями дальше?


Автоматизировать, конечно же! Если вы попробуете выполнить эти команды, то увидите, что ответы гигантские и их тяжело читать. Как автоматизировать? Ruby, Python Или Swift?
Конечно же, swift. Его знает любой современный iOS разработчик. Проект открывается в Xcode, доступна отладка, подсветка синтаксиса, строгая типизация. Короче, мечта! Особенно при появлении Swift package manager.
Ни для кого не секрет, что с помощью swift мы легко можем запускать процессы, слушать ошибки и получать выходные данные. В самом простом случае мы можем обойтись такой конструкцией:



Нам остается теперь только исследовать формат XCResult через уже знакомые нам xcrun xcov и xcrun xcresulttool. Например, чтобы прочитать покрытие тестами, мы используем:



А чтоб получить оглавление XCResult нам нужно выполнить:



Но как нам получить наши заветные структуры CoverageReport и XCResult?
Получаем строку из Data, которую вернет нам первая Shell команда и помещаем содержимое сюда: quicktype.io.
Сервис сгенерирует нам что-то похожее на нужные свифтовые структуры. Правда использовать результат как есть не получится. Придётся пристальнее изучать структуру ответа и выбрасывать дубли. Тем не менее такая работа не составляет большого труда. Можно отбрасывать ненужные части, а можно заняться исследованием и выделить несколько основных кирпичиков:



На основании этого описать уже остальные структуры, например:



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



Ну а этим-то как пользоваться?


Есть два пути, как пользоваться нашим скраппером. Первый как executable, и здесь здорово помогает библиотека swift-argument-parser от Apple. До этого приходилось писать обработку аргументов самим, покрывать тестами, поддерживать. Сейчас эту работу взяла на себя популярная библиотека, меинтейнерам которой можно доверять.
Есть две команды: получить отчёт по покрытию тестами и сгенерировать junit отчёт о результатах тестирования. Нужно сбилдить проект и запускать бинарник, передавая необходимые аргументы:



Второй путь использовать этот проект как библиотеку. У нас есть большой CI проект, который отвечает за сборку, тестирование и доставку нашего продукта KoronaPay. Например, мы можем по результатам прохождения тестов извлекать все assertion failures и крэши в тестах приблизительно так:



Или получать красные тесты, анализировать флаки и перезапускать только их.
А как анализировать? Всё просто и непросто одновременно. Чтобы достать детали причины падения теста, надо сделать дополнительный запрос к xcresult по идентификатору failure summary. А затем из failure summary вытаскивать информацию. На сегодняшний момент мы научились искать крэши в тестах и lost connection случаи, а также вытаскивать причины. Понять, что произошел крэш несложно. Надо лишь найти в failureSummaries заветные слова crashed in.



Чуть сложнее вытащить причину крэша.
Здесь нам пригодится механизм рефлексии в swift, который хоть и несколько ограничен, но отлично подходит для решения этой задачи. Необходимо найти все объекты типа Attachment с именем kXCTAttachmentLegacyDiagnosticReportData.



В методе reflectProperties нет ничего магического, это простенький extension для Mirror:



Еще одна категория красных тестов ассерты. В отличие от крэшей здесь не получится просто поискать строку crashed in. Такие тесты могут маскироваться под lost connection случаи. Чтобы докопаться до причины, придется пройтись по нескольким массивам внутри объекта TestCase примерно так:



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



Вместо заключения


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

Apple убивает TeamCity, Bitrise, Appcenter, Fastlane, Firebase, Sentry и иже с ними. Краткий обзор Xcode Cloud

10.06.2021 14:04:32 | Автор: admin

Заголовок конечно громковат, может не убивает, но уменьшит им доходы точно. Давайте кратко посмотрим что представила Apple на WWDC 2021, что такое Xcode Cloud?

Xcode Cloud - это сервис CI/CD, встроенный в Xcode и разработанный специально для разработчиков Apple. Он ускоряет разработку и доставку приложений, объединяя облачные инструменты, которые помогают создавать приложения, параллельно запускать автоматические тесты, доставлять приложения тестировщикам, а также просматривать отзывы пользователей и управлять ими.

Цикл разработки по мнению Apple заключается в этапах 1) Написать код 2) протестировать его 3) Интегрировать (в текущий) 4) Доставить до пользователя 5) Улучшить, и по новой. На то он и цикл. В принципе похоже на правду, так оно и есть.

Если вы хоть раз настраивали CI/CD для iOS приложений, вы знаете примерно какие там шаги, ничего сложного, но это может включать в себя использование нескольких сервисов, генерации сертификатов и тд и тп.

Теперь же Apple предлагает нам сделать это все не выходя из Xcode, давайте взглянем на процесс.

Для начала нам нужно настроить первый workflow, а потом уже который будет пробегать при PR/MR (pull request/merge request) на main/develop ветку в системе контроля версий.

I CI/CD

1) Жмем в новом Xcode при подключенном сервисе Xcode Cloud кнопку "создать workflow" и видим настройки

Name - название воркфлоу, Start condition когда запускать воркфлоу (например при изменении в main ветке), Environment - можно выбрать стабильную версию Xcode или новую бета версию, Actions - что собственно надо сделать, обычно выполнить archive и опубликовать например в TestFlight, после прогонки тестов, Post-Actions - что сделать после того как воркфлоу пройден, например написать в slack/telegram канал об этом событии

2) Выбираем репозиторий где хранится наш код

3) Выбираем с какой ветки собрать билд (при первой настройке)

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

Давайте теперь посмотрим как выглядит управлени воркфлоу (выше показан путь настройки первой сборки)

1) Выбираем "управление воркфлоу"

2) Выбираем настройки (например при pull/merge request что-то выполнять)

3) Выбираем какие тесты мы хотим прогнать в воркфлоу (UI или Unit тесты), я так понимаю речь именно про нативные тесты, про Appium и тд, пока ничего не известно.

4) И выбираем отправить сообщение в Slack после того как воркфлоу пройден

5) Готово

II Тесты

1) Давайте посмотрим как выглядит интерфейс работы с тестами, мы видим тут тесты которые пройдены при сборке а также устройства на которых они прогонялись

2) Посмотрим какие конкретно тесты прогнались на iPad Air, видим что тест кейс с Light mode, портретный режим, с Английским языком, далее видим какие конкретно тесты пройдены

3) Ну и совсем чудеса, можно смотреть скриншоты пройденных тестов

4) Можно также посмотреть какой тест упал, можно также пометить тест как Flaky (Флаки тест или другими словами тест неактуальный, который надо либо удалить либо переписать), для этого используется XCTExpectFailure (что в переводе логично видно по названию метода ожидаемый фейл)

Удобно.

III - Работа с системой контроля версий (и переписка прямо в коде в Xcode)

1) Изменения теперь видно еще нагляднее (привет всем кто пользуется визуальными штуками, а не через консоль при работе с git). Сверху мы видим наши локальные изменения (которые мы накодили) а снизу "висящие" pr/mr реквесты, которые можно посмотреть, и дать свой комментарий или approve (одобрение на слияние кода)

2) Даже видно какой тест план для этой фичи, которая просится в главную ветку

3) Переписка,комменты прямо в Xcode при pr/mr (а не на веб мордах gitlab/github/bitbucket и тд)

В общем очень круто и удобно

IV - Улучшения (Crashes/Сбои/Ошибки)

1) Все краши/сбои теперь видно прямо в Xcode (а не в веб морде Firebase или Sentry), код приходит сразу символизированный (symbolized log), то есть человекопонятный с указанием что и как произошло

2) А тестер (возможно и пользователь) может оставить комментарий при краше который вы сможете прочитать (и даже ответить!)

3) Ну и самое интересное вы сможете кликнуть открыть место краша в проекте

4) И вас за ручку проведут к вашему куску кода который натворил зло

Послесловие

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

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

Можете подать заявку на бета-тест здесь https://developer.apple.com/xcode-cloud/

Сколько он будет стоить пока тоже неизвестно.

И пока непонятно что с Android потому что обычно сервисы CI/CD используют сразу для двух платформ, так как приложения обычно тоже для двух платформ разрабатывают. Но может быть когда нибудь приложения для Android можно будет писать и в Xcode))

Изображение и информацию брал из видеосессий WWDC 2021, кому интересно как это выглядит вот видео про Xcode Cloud https://developer.apple.com/videos/play/wwdc2021/102/

Подробнее..

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

20.06.2021 12:09:43 | Автор: admin
В этой подборке исследуем StoreKit 2, распознаем лица и позы на Android, улучшаем производительность React-приложений, учим сквирклморфизм и многое другое!



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

iOS

За что App Store может отклонить приложение: чек-лист
Meet StoreKit 2
Тим Кук: на Android в 47 раз больше вредоносных программ, чем на iOS
Новый антимонопольный акт может заставить Apple продать App Store
Что нового во встроенных покупках в iOS 15 WWDC 21
Строим лабиринты с SwiftUI
iOS 15: заметные дополнения к UIKit
Info.plist отсутствует в Xcode 13 вот как его вернуть
ScrollView в XCode 11
Создаем игры на SwiftUI с помощью SpriteKit
Мастерим списки в SwiftUI
Как лучше структурировать свои проекты в Xcode
Глубокое погружение в Акторы в Swift 5.5
Разработка функций iOS-приложения в виде модулей в Xcode
Как делать видеозвонки с помощью SwiftUI
Euler: вычислительный фреймворк на Swift
WorldMotion: положение устройства относительно Земли

Android

Как использовать Android Data Binding в пользовательских представлениях?
AppSearch из Jetpack вышел в альфа-версии
Распознавание лиц и поз за 40 минут
Android Broadcast: новости #10
Создайте свою библиотеку KMM
История моего первого а-ха-момента с Jetpack Compose
Как стать ассоциированным разработчиком Android (Kotlin Edition)
Анимации Jetpack Compose в реальном времени
RecyclerView с NestedScrollView: лучшие практики
Android Bitbucket Pipeline CI/CD с Firebase App Distribution
CompileSdkVersion и targetSdkVersion в чем отличие?
Нижняя панель навигации Android с Jetpack Compose
Интеграция Google Sign-in в Android-приложение
Focus в Jetpack Compose
DashedView: полосатые View
Screen Tracker: название видимого Activity/Fragment
SquircleView: красивые View

Разработка

5 000 000 строк кода, 500 репозиториев: зачем мы адаптировали приложение AliExpress для Рунета
Десятикратное улучшение производительности React-приложения
gRPC + Dart, Сервис + Клиент, напишем
Podlodka #220: волонтерство в IT
Хороший день разработчика: Good Day Project от GitHub
К 2024 году 80% технологических продуктов будут создавать непрофессионалы
Сквирклморфизм (Squirclemorphism) в дизайне интерфейсов
12 рекомендаций, которые помогут улучшить процесс регистрации и входа в систему
React Native в Wix Архитектура (глубокое погружение)
Как узнать плохой код? 8 вещей
5 лучших пакетов Flutter, которые вы должны знать
Советы по кодинг интервью в Google
Как стать плохим разработчиком

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

Гайд по тестированию рекламы для мобильных приложений
Вслед за Apple и Google комиссию магазина приложений снизила Amazon
make sense: О инфлюенсер-маркетинге
UserLeap получает еще $38 млн на отслеживание пользовательского опыта
Классическая MMORPG RuneScape запускается на iOS и Android
Маркетологи в мобайле: Александр Плёнкин (Vprok.ru Перекрёсток)
Почему такие скриншоты пустая трата времени? (пока у вас нет 4,000 загрузок в месяц)
Amplitude получил еще $150 млн
$100 млн для Free Fire: как младший брат может обогнать старшего на уже сложившемся рынке?
App Annie: рынок мобильных игр в России в 2020 вырос на 25% до $933 млн
Темные паттерны и уловки в мобильных приложениях
Использование BigQuery и Firebase Analytics для привлечения, вовлечения и оценки пользователей

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

Запускаем DOOM на лампочке
Быстрое обнаружение Covid-19 на рентгеновских снимках с помощью Raspberry Pi
Как я учу Python на Raspberry Pi 400 в библиотеке
Топ-5 преемников GPT-3, о которых вы должны знать в 2021 году

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

Библиотека дляработы сiOS-пермишенами, отидеи дорелиза (часть1)

07.12.2020 20:14:08 | Автор: admin

Привет! Из этого мини-цикла статей ты узнаешь:

  • Как унаследовать Swift-класс не целиком, а лишь то в нём, что тебе нужно?

  • Как позволить юзеру твоей CocoaPods- или Carthage-библиотеки компилировать лишь те её части, что он действительно использует?

  • Как раздербанить ресурсы iOS, чтобы достать оттуда конкретные системные иконки и локализованные строки?

  • Как поддержать completion blocks даже там, где это не предусмотрено дефолтным API системных разрешений?

А вообще, здесь о том, как я попытался написать ультимативную библиотеку для работы с пермишенами в iOS с какими неожиданностями столкнулся и какие неочевидные решения нашёл для некоторых проблем. Буду рад, если окажется интересно и полезно!

Немного о самой либе

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

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

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

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

  • Поддерживает новейшие фичи iOS 14 и macOS 11 Big Sur

  • Отлично работает с Mac Catalyst

  • Поддерживает все существующие типы системных разрешений

  • Валидирует твой Info.plist и защищает от падений, если с ним что-то не так

  • Поддерживает коллбэки даже там, где этого нет в дефолтном системном API

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

  • Полностью написан на чистом Swift

  • Обеспечивает унифицированное API вне зависимости от типа разрешения, с которым ты прямо сейчас работаешь

  • Опционально включает нативные иконки и локализованные строки для твоего UI

  • Модульный, подключай лишь те компоненты, что тебе нужны

Но перейдём наконец-то к действительно интересному...

Как унаследовать класс не целиком, а лишь то в нём, что тебе нужно?

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

  • Свойство usageDescriptionPlistKey

  • Методы checkStatus и requestAccess

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

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

Только вот оказалось всё сложнее, чем можно было ожидать:

  • Некоторые типы пермишенов (например, дом и локальная сеть) не позволяют проверить текущий статус разрешения, не выполнив собственно запрос на доступ к нему, и унаследованное объявление checkStatus оказывается в таком случае неуместным. Оно лишь сбивает с толку торчит в автоподстановке, хотя не имеет имплементации.

  • Для работы с пермишеном геолокации не годится стандартное объявление requestAccess(completion:), поскольку для запроса на доступ необходимо определиться, нужен он нам всегда, или только когда юзер активно пользуется приложением. Здесь подходит requestAccess(whenInUseOnly:completion:), но тогда опять-таки выходит, что унаследованная перегрузка метода болтается не в тему.

  • Пермишен на доступ к фотографиям использует сразу два разных plist-ключа один на полный доступ (NSPhotoLibraryUsageDescription) и один, чтобы только добавлять новые фото и видео (NSPhotoLibraryAddUsageDescription). Видим, что опять-таки наследуемое свойство usageDescriptionPlistKey получается лишним логичнее иметь два отдельных и с более говорящими названиями.

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

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

class SupportedType {    func requestAccess(completion: (Status) -> Void) { }}final class Bluetooth: SupportedType { ... }final class Location: SupportedType {    @available(*, unavailable)    override func requestAccess(completion: (Status) -> Void) { }        func requestAccess(whenInUseOnly: Bool, completion: (Status) -> Void) { ... }}

Переопределение метода, помеченное атрибутом @available(*, unavailable), не только делает его вызов невозможным, возвращая при сборке ошибку, но и полностью скрывает его из автоподстановки в Xcode, то есть фактически как будто исключает метод из наследования.

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

Как позволить юзеру твоей CocoaPods- или Carthage-библиотеки компилировать лишь те её части, что он действительно использует?

PermissionWizard поддерживает 18 видов системных разрешений от фото и контактов до Siri и появившегося в iOS 14 трекинга. Это в свою очередь означает, что библиотека импортирует и использует AVKit, CoreBluetooth, CoreLocation, CoreMotion, EventKit, HealthKit, HomeKit и ещё много разных системных фреймворков.

Нетрудно догадаться, что если ты подключишь такую либу к своему проекту целиком, пусть даже будешь использовать её исключительно для работы с каким-то одним пермишеном, Apple не пропустит твоё приложение в App Store, поскольку увидит, что оно использует подозрительно много разных API конфиденциальности. Да и собираться такой проект будет чуть дольше, а готовое приложение весить чуть больше. Требуется какой-то выход.

CocoaPods

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

pod 'PermissionWizard/Assets' # Icons and localized stringspod 'PermissionWizard/Bluetooth'pod 'PermissionWizard/Calendars'pod 'PermissionWizard/Camera'pod 'PermissionWizard/Contacts'pod 'PermissionWizard/FaceID'pod 'PermissionWizard/Health'pod 'PermissionWizard/Home'pod 'PermissionWizard/LocalNetwork'pod 'PermissionWizard/Location'pod 'PermissionWizard/Microphone'pod 'PermissionWizard/Motion'pod 'PermissionWizard/Music'pod 'PermissionWizard/Notifications'pod 'PermissionWizard/Photos'pod 'PermissionWizard/Reminders'pod 'PermissionWizard/Siri'pod 'PermissionWizard/SpeechRecognition'pod 'PermissionWizard/Tracking'

В свою очередь, Podspec нашей библиотеки (файл, описывающий её для CocoaPods) выглядит примерно следующим образом:

Pod::Spec.new do |spec|    ...    spec.subspec 'Core' do |core|    core.source_files = 'Source/Permission.swift', 'Source/Framework'  end    spec.subspec 'Assets' do |assets|    assets.dependency 'PermissionWizard/Core'    assets.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'ASSETS' }        assets.resource_bundles = {      'Icons' => 'Source/Icons.xcassets',      'Localizations' => 'Source/Localizations/*.lproj'    }  end    spec.subspec 'Bluetooth' do |bluetooth|    bluetooth.dependency 'PermissionWizard/Core'    bluetooth.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'BLUETOOTH' }    bluetooth.source_files = 'Source/Supported Types/Bluetooth*.swift'  end    ...    spec.default_subspec = 'Assets', 'Bluetooth', 'Calendars', 'Camera', 'Contacts', 'FaceID', 'Health', 'Home', 'LocalNetwork', 'Location', 'Microphone', 'Motion', 'Music', 'Notifications', 'Photos', 'Reminders', 'Siri', 'SpeechRecognition', 'Tracking'  end

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

#if BLUETOOTH    final class Bluetooth { ... }#endif

Carthage

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

В корне нашей либы создаём файл Settings.xcconfig и пишем в нём следующее:

#include? "../../../../PermissionWizard.xcconfig"

По умолчанию Carthage устанавливает зависимости в директорию Carthage/Build/iOS, так что вышеприведённая инструкция ссылается на некий файл PermissionWizard.xcconfig, который может быть расположен юзером нашей библиотеки в корневой папке своего проекта.

Очертим и его примерное содержимое:

ENABLED_FEATURES = ASSETS BLUETOOTH ...SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) $(ENABLED_FEATURES) CUSTOM_SETTINGS

Наконец, необходимо указать нашей либе, что она должна ссылаться на Settings.xcconfig как на дополнительный источник настроек для сборки. Чтобы это сделать, добавляем в проект библиотеки ссылку на указанный файл, а затем открываем project.pbxproj любым удобным текстовым редактором. Здесь ищем идентификатор, присвоенный только что добавленному в проект файлу, как на примере ниже.

A53DFF50255AAB8200995A85 /* Settings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Settings.xcconfig; sourceTree = "<group>"; };

Теперь для каждого имеющегося у нас блока XCBuildConfiguration добавляем строку с базовыми настройками по следующему образцу (строка 3):

B6DAF0412528D771002483A6 /* Release */ = {isa = XCBuildConfiguration;baseConfigurationReference = A53DFF50255AAB8200995A85 /* Settings.xcconfig */;buildSettings = {...};name = Release;};

Ты можешь спросить, зачем помимо флагов с нужными компонентами мы также проставляем некое CUSTOM_SETTINGS. Всё просто в отсутствие этого флага мы считаем, что юзер библиотеки не попытался её настроить, то есть не создал PermissionWizard.xcconfig в корне своего проекта, и включаем сразу все поддерживаемые либой компоненты.

#if BLUETOOTH || !CUSTOM_SETTINGS    final class Bluetooth { ... }#endif

На этом пока всё

В следующей части поговорим о том, как я среди 5 гигабайт прошивки iOS 14 нашёл нужные мне локализованные строки и как добыл иконки всех системных пермишенов. А ещё расскажу, как мне удалось запилить requestAccess(completion:) даже там, где дефолтное системное API разрешений не поддерживает коллбэки.

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

Подробнее..

7 Кругов SPM или как сделать модульное приложение на Swift Package Manager

28.03.2021 20:06:28 | Автор: admin
Спасибо Jackie Zhao @jiaweizhao за фото на Unsplash Спасибо Jackie Zhao @jiaweizhao за фото на Unsplash

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

Возможно, у многих возникнет вопрос: Зачем разбивать с помощью SPM?. Ведь можно просто создавать подпроекты. Можно, но в использовании SPM для разбиения есть несколько преимуществ:

  • С помощью SPM мы избавляемся от .xcodeproj файлов (забываем про конфликты в них);

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

  • Нет альтернатив для проектов под разные операционные системы Linux/Windows;

  • Пакеты не требуют xcode для разработки.

Как выглядит сам процесс?

Для начала создаём Package.swift файл, в котором будут храниться все необходимые зависимости и информация о том, что из себя этот модуль представляет.

Пример файла с параметрами, с которыми вы скорей всего столкнётесь при работе:

import PackageDescription let package = Package(    // 1. Название нашего пакета    name: "Resources",    // 2. Платформы, которые поддерживаются нашим пакетом    platforms: [        .iOS(.v11),    ],    // 3. То, что другие программы будут брать в себя    // Продуктов может быть огромное колличество, хороший пример для этого Firebase SPM пакет    products: [        .library(            name: "Resources",            // Динамический или статический продукт          // по дефолту значение nil - SPM сам будет понимать что лучше подходит            //  преференция скорей всего будет отдаваться .static            type: .dynamic,            targets: ["Resources"]),    ],    // 4. Зависимости необходимые для работы нашего пакета,  // здесь они просто загружаются, добавляются они в targets    dependencies: [        // name - это название пакета(пункт 1), здесь нельзя указать кастомное название, необязательный параметр        .package(name: "R.swift.Library", url: "https://github.com/mac-cain13/R.swift.Library", .branch("master")),        // Пример подключения локального пакета        .package(path: "../Core")    ],    targets: [        // Это то из чего мы будем складывать наш продукт        // Для таргета обязательно нужно создать папку       // в Sources/имя_таргета для его работы      // либо если мы не хотим размещать его в Sources, можем указать "path:"        .target(            name: "Resources",            dependencies: [                // Здесь мы указываем зависимости которые мы хотим использовать в таргете                // name(пункт 3), package(пункт 1)                .product(name: "RswiftDynamic", package: "R.swift.Library")            ],            resources: [                // Все ресурсы которые мы хотим использовать нужно явно указать                // Путь к ним относительный от Sources/имя_пакета/то_что_мы_указали                // Если указываем папку, поиск идет рекурсивно                .process("Resources")            ])    ])

После создания модуля, подключаем его к проекту (либо к другому модулю, если модуль который вы делали лишь вспомогательный для других). Для удобства можно добавить пакет в workspace, просто drag and drop корневую папку с пакетом в Project navigator.

Введение на этом можно закончить и перейти к основной части статьи, где мы рассмотрим проблемы, с которыми вы можете столкнуться. На момент написания статьи мною использовались swift-tools-version:5.3, Xcode Version 12.2

Круг 1. Небольшое комьюнити.

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

Круг 2. Отсутствует фаза скриптов SPM пакета.

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

Круг 3. R.swift и SPM.

Так как у нас нет .xcodeproject файла, вызвать R.swift скрипт для генерации не получится, для этого нам нужно этот файл создать.

Для создания можно использовать XcodeGen и его аналоги. Либо swift package generate-xcodeproj, стандартного скрипта для генерации .xcodeproj файла в SPM.

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

error: [R.swift] Project file at 'file:///Users/.../Resources.xcodeproj/' could not be parsed, is this a valid Xcode project file ending in *.xcodeproj?

Чтобы локализовать проблему и найти почему файл проекта не может распарситься нужен XcodeEdit фреймворк который R.swift использует для парсинга. Билдим его и получаем exec файл, которому нужно передать .xcodeproj. Вызываем его:

pathtoexec/XcodeEdit-Example Resources.xcodeproj

и видим

Fatal error: 'try!' expression unexpectedly raised an error: XcodeEdit_Example.AllObjectsError.fieldMissing(key: "buildRules"): file XcodeEdit_Example/main.swift, line 21

Вот как можно это решить:

sed -i '' -e 's/isa = "PBXNativeTarget";/isa = "PBXNativeTarget";buildRules = ();/' Resources.xcodeproj/project.pbxproj

К сожалению, на этом проблемы не заканчиваются. generate-xcodeproj не добавляет файлы ресурсов в проект. То есть R.swift будет парсить .xcodeproj/.pbproject, но нужных файлов ресурсов там нет.

Это можно решить подключением ruby gem xcodeproj, с помощью которого можно добавить необходимые файлы. Но всё же такой подход более трудозатратный, и лучше вернуться к варианту с .xcodeproj генераторам по типу XcodeGen.

Пример минимального конфиг файла для XcodeGen:

# Название генерируемого xcodeprojname: Resourcestargets: Resources:   # Не важно что вы тут укажите, но это обязательные параметры   type: framework   platform: iOS   # Root folder с которого начинать генерировать   sources:     - Sources

Команда которая создаст .xcodeproj файл:

xcodegen generate --spec Resources.yml

Проблема с .xcodeproj решена, вот пример полного скрипта для генерации R.swift:

# Создание xcodeprojgenerateXcodeProject() { xcodegen generate --spec Resources.yml} # Получаем buildSettings с помощью логов xcodebuild команды, лучше проверять, создан ли этот файл# Чтобы сократить время скрипта и не билдить каждый разgetBuildSettings() { xcodebuild -project "Resources.xcodeproj" -target "Resources" -showBuildSettings > buildSettings.txt} # Создание enviroment переменных без которых R.swift скрипт работать не будетparseEnvironmentVariables() { export SRCROOT="$(cat buildSettings.txt | grep -m1 "SRCROOT" | sed 's/^.*= //' )" export TARGET_NAME="$(cat buildSettings.txt | grep -m1 "TARGET_NAME" | sed 's/^.*= //' )" export PROJECT_FILE_PATH="$(cat buildSettings.txt | grep -m1 "PROJECT_FILE_PATH" | sed 's/^.*= //' )" export TARGET_NAME="$(cat buildSettings.txt | grep -m1 "TARGET_NAME" | sed 's/^.*= //' )" export PRODUCT_BUNDLE_IDENTIFIER="$(cat buildSettings.txt | grep -m1 "PRODUCT_BUNDLE_IDENTIFIER" | sed 's/^.*= //' )" export PRODUCT_MODULE_NAME="$(cat buildSettings.txt | grep -m1 "PRODUCT_MODULE_NAME" | sed 's/^.*= //' )" export TEMP_DIR="$(cat buildSettings.txt | grep -m1 "TEMP_DIR" | sed 's/^.*= //' )" export BUILT_PRODUCTS_DIR="$(cat buildSettings.txt | grep -m1 "BUILT_PRODUCTS_DIR" | sed 's/^.*= //' )" export DEVELOPER_DIR="$(cat buildSettings.txt | grep -m1 "DEVELOPER_DIR" | sed 's/^.*= //' )" export SOURCE_ROOT="$(cat buildSettings.txt | grep -m1 "SOURCE_ROOT" | sed 's/^.*= //' )" export SDKROOT="$(cat buildSettings.txt | grep -m1 "SDKROOT" | sed 's/^.*= //' )" export PLATFORM_DIR="$(cat buildSettings.txt | grep -m1 "PLATFORM_DIR" | sed 's/^.*= //' )" export INFOPLIST_FILE="$(cat buildSettings.txt | grep -m1 "INFOPLIST_FILE" | sed 's/^.*= //' )" export SCRIPT_INPUT_FILE_COUNT=1 export SCRIPT_INPUT_FILE_0="$TEMP_DIR/rswift-lastrun" export SCRIPT_OUTPUT_FILE_COUNT=1 export SCRIPT_OUTPUT_FILE_0="$SRCROOT/Sources/Resources/Generated/R.generated.swift"} # Вызов скрипта для генерацииrswift() { R.swift generate --accessLevel public "$SCRIPT_OUTPUT_FILE_0"} # Заменяем бандл в котором R.swift пытается найти ресурсы по дефолту# Bundle.module - расширение генерируется SPM, если вы в пакете указываете для таргета ресурсные зависимостиreplaceRSwiftHostingBundle() { sed -i '' -e 's/Bundle(for: R.Class.self)/Bundle.module/' ./Sources/Resources/Generated/R.generated.swift} mkdir Sources/Resources/GeneratedgenerateXcodeProjectgetBuildSettingsparseEnvironmentVariablesrswiftreplaceRSwiftHostingBundle

Круг 4. Cocoapods зависимость в SPM пакете.

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

Если Pod распространяется как собранный бинарник через .framework, то придется действовать немного по-другому.

Как и в предыдущем случае, нужно собрать XCFramework. Для начала из fat фреймворка, который поставляется Pod-ом необходимо собрать 2 фреймворка для симулятора и для девайса. После этого объединим их в универсальный фреймворк. Вот скрипт, который делает XCFramework из фремворка с архитектурами arm64 и x86_64. Узнать архитектуры, которые поддерживает фреймворк можно с помощью команды lipo -info pathtoframework.

# Делаем 2 копии фреймворка для симулятора и устройстваcp -a YandexMapsMobile YandexMapsMobile_simcp -a YandexMapsMobile YandexMapsMobile_device cd YandexMapsMobile_sim/YandexMapsMobile.framework/Versions/A# Здесь мы выбираем какая архитектура нам нужна из fat фреймворка и выносим ее в отдельный фреймворкlipo -thin x86_64 YandexMapsMobile -output YandexMapsMobile_x86_64# Создание universal framework-a для симулятора# Если вы хотите собрать не под одну архитектуру симулятора# а под несколько (i386), для этого нужно будет сделать два раза thin# и соединить их в createlipo -create YandexMapsMobile_x86_64 -output YandexMapsMobile_simrm -rf YandexMapsMobile YandexMapsMobile_x86_64mv YandexMapsMobile_sim YandexMapsMobilecd ../../../.. # Просто дублирование кода, но сборка под девайсcd YandexMapsMobile_device/YandexMapsMobile.framework/Versions/Alipo -thin arm64 YandexMapsMobile -output YandexMapsMobile_arm64lipo -create YandexMapsMobile_arm64 -output YandexMapsMobile_devicerm -rf YandexMapsMobile YandexMapsMobile_arm64mv YandexMapsMobile_device YandexMapsMobilecd ../../../.. # Объединяем в xcframeworkxcodebuild -create-xcframework -framework YandexMapsMobile_sim/YandexMapsMobile.framework -framework YandexMapsMobile_device/YandexMapsMobile.framework -output YandexMapsMobile.xcframework

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

let package = Package(    name: "YandexMapsMobileWrapper",    platforms: [        .iOS(.v11),    ],    products: [        .library(            name: "YandexMapsMobileWrapper",            type: .static,            // Используем таргет обертку над XCFramework и его зависимостями            targets: ["YandexMapsMobileWrapper"]),    ],    dependencies: [    ],    targets: [        // Подключаем наш фреймворк локально, также его можно добавлять и через url        .binaryTarget(name: "YandexMapsMobileBinary", path: "YandexMapsMobile.xcframework"),        // обертываем наш фреймворк зависимостями        .target(            name: "YandexMapsMobileWrapper",            dependencies: [                .target(name: "YandexMapsMobileBinary"),            ],            linkerSettings: [                .linkedFramework("CoreLocation"),                .linkedFramework("CoreTelephony"),                .linkedFramework("SystemConfiguration"),                .linkedLibrary("c++"),                .unsafeFlags(["-ObjC"]),            ]),    ])

Круг 5. Краш при попытке получить бандл SPM пакета (Bundle.module).

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

private class BundleFinder {}  // Это копия автосгенерированого SPM расширения, есть 2 отличияpublic extension Bundle {        // Отличие 1, другое название, чтобы не было конфликтов    static var resourceBundle: Bundle = {         let bundleName = "Resources_Resources"        let candidates = [            Bundle.main.resourceURL,            Bundle(for: BundleFinder.self).resourceURL,            Bundle.main.bundleURL,            // Отличие 2, еще один путь где может лежать бандл с ресурсами            Bundle(for: BundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),        ]        for candidate in candidates {            let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")            if let bundle = bundlePath.flatMap(Bundle.init(url:)) {                return bundle            }        }        fatalError("unable to find bundle named \(bundleName)")    }()    }

Круг 6. Блокирующая Resolve Swift Packages стадия.

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

Круг 7. Other Linker Flags и SPM.

Может возникнуть ситуация, когда пакет должен линковаться со специальными флагами. Частый пример это ObjC флаг. Эти флаги для линковщика можно указать с помощью likerSettings: [.unsafeFlags([ObjC])] в таргете вашего пакета. К сожалению, если ваш пакет использует unsafeFlags, то его нельзя подключить к проекту напрямую, только через прокси-пакет, и только с указанием его как локальной или branch зависимости.

Вывод

Swift Package Manager удобный и интуитивно понятный нативный менеджер зависимостей. Однако, если вы планируете использовать его для разбивки приложения на подпроекты-пакеты, то нужно быть готовым к временным рискам и проблемам. К счастью, почти любая проблема связанная с SPM имеет свой workaround, но готовы ли вы идти на них и искать их?

Надеюсь решения проблем, с которыми я столкнулся помогут вам!

Подробнее..

Перевод Переход вашего приложения на модули пакетов Swift

28.04.2021 16:12:49 | Автор: admin

Введение

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

http://personeltest.ru/aways/github.com/apple/swift-package-managerhttps://github.com/apple/swift-package-manager

Еще одно большое преимущество Swift Packages это возможность запускать их в одиночку, чтобы сократить время компиляции во время разработки и упростить тестирование. Теперь мы подошли к тому моменту, когда наше тестовое приложение может скомпилировать более 20 модулей и 10 зависимостей из чистой сборки менее чем за минуту, а для инкрементальных изменений отдельных модулей может потребоваться всего 10 секунд для компиляции и запуска.

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

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

Настройка

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

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

Сначала создадим папку под названием Modules в корневом каталоге git, где мы сможем хранить все наши пакеты Swift.

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

Теперь нажмите символ плюс в левом нижнем углу и затем выберите "New Swift Package". Убедитесь, что новый пакет Swift помещён в папку Modules.

После этого все должно выглядеть так:

Package.swift

Теперь давайте исправим и очистим файл Package.swift и рассмотрим различные части.

let package = Package(    name: "NewModule", // This is the name of the package    defaultLocalization: "en", // This allows for localization    platforms: [.iOS(.v12)], // Our minimum deployment target is 12    products: [        .library(            name: "NewModule",            type: .static, // This is a static library            targets: ["NewModule"]        )    ],    dependencies: [    ],    targets: [        .target(            name: "NewModule",            dependencies: [            ],            path: "Sources", // This allows us to have a better folder structure            resources: [                .process("Media.xcassets") // We will store out assets here            ]        ),        .testTarget(            name: "NewModuleTests", // Unit tests            dependencies: ["NewModule"]        )    ])

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

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

Очистка

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

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

Давайте удалим файлы LinuxMain.swift и XCTestManifests.swift, так как мы не будем запускать их в Linux. Затем переименуем папку NewModule в Public и создадим папку Internal для лучшей организации контроля доступа. Теперь давайте переименуем NewModule.swift в NewModuleViewController.swift, чтобы мы могли проверить, работает ли он.

В итоге все должно выглядеть так.

Подключайте!

Код выше должен дать нам UIViewController c оранжевым фоном, подтверждающим, что он работает правильно.

Когда проект выбран и наша главная цель (target) выделена, нажмите "плюс" в разделе Frameworks, Libraries, and Embedded Content.

Теперь выберите нашу новую библиотеку и нажмите "Add ".

Теперь вернемся в приложение и используем наш новый модуль, отредактировав Main.storyboard и изменив класс на NewModuleViewController.swift, а "Модуль" на NewModule.

Тест

Теперь, когда мы запускаем приложение, становится видно, что оно использует ViewController из нашего нового модуля! ?

Заключение

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


Перевод статьи подготовлен в рамках старта набора учащихся на курс "iOS Developer. Basic" подготовили традиционный перевод статьи.

Приглашаем также всех желающих на двухдневный интенсив Создание простейшего приложения без единой строчки кода. В первый день обсудим и сделаем:
Что такое XCode?
Как "рисуются экраны"
Добавим на экраны кнопки и поля ввода. Создадим экран авторизации.
Создадим второй экран нашего приложения и добавим переход на него из окна авторизации.

Подробнее..

Установка Midnight Commander на Mac OS X Catalina (2020)

15.11.2020 20:17:49 | Автор: admin
Государственный флаг СССРГосударственный флаг СССР

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

ПерфокартаПерфокарта

Так вот, вернемся к установки MC на Mac OS. Уверен, те кто давно работает за компьютером, помнит времена Norton Commander и Volkov Commander (Российская версия) и прочих файловых менеджеров, которые помогали работать на компьютере. До того как появился Windows 3.11 и вообще полноценный Windows, основным интерфейсом был MS-DOS. А для UNIX систем был создан Midnight Commander.

Сегодня я решил установить MC на Mac OS Catalina. Зачем? Иногда нужен доступ к папкам и всем каталогам, а их нет, Apple все пытается спрятать. Через терминал - это отдельный танец с бубном, не очень удобно.

Установка не в 2 клика, все оказалось не так просто, как хотелось бы.

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

Процесс установки

1. Требуется установить Xcode

Берем бесплатную версию Xcode с App Store и устанавливаем.

После установки Xcode выполнить команду в строке терминала:

 xcode-select --install

Если при запуске получили ответ: xcode-select: error: command line tools are already installed, use "Software Update" to install updates. Значит у вас уже установлен Xcode, тогда пропускайте пункт 1 установки Xcode.

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

Если этого не сделать то в последующей Homebrew будет выдавать предупреждение.

2. Установка Homebrew

Чтобы скачать и установить Homebrew, запустим сценарий установки в командной строке который приведен ниже и ждем когда скрипт сделает свое дело:

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Далее идет примерно такая установка.

3. Установка MC

Теперь можно установить Midnight Commander. В командной строке набираем:

brew install mc

После установки в командной строке терминала набираем:

mc
Окно MC в Mac OS (из Терминала)Окно MC в Mac OS (из Терминала)

Источники, которые использовались при написании статьи:
Homebrew менеджер пакетов для OS X Mavericks 10.9 (04.02.2014)
Midnight Commander на Mac OS X установка и настройка (09.09.2016)
Midnight Commander на Mac OS X установка и настройка (12.02.2014)

Подробнее..

MacOS и мистический minOS

24.12.2020 02:20:22 | Автор: admin

После трёхлетнего перерыва актуальная версия sView стала снова доступна на macOS. Релиз sView 20.08 обещал поддержку macOS 10.10+, но что-то пошло не так и несколько пользователей обратились со странной проблемой - системы macOS 10.13 и 10.14 отказались запускать приложение с сообщением о необходимости обновиться до macOS 10.15

Сказать, что ошибка меня озадачила - сильно преуменьшить степень моего негодования, ведь магическая цифра 10.15 нигде не фигурировала ни в скриптах сборки, ни в ресурсах sView! Более того, приложение лично было проверено на более старой версии системы, а именно - на macOS 10.10.

Немного предыстории. В далёком 2011 году вышла первая сборка sView для OS X 10.6 Snow Leopard, и шесть лет именно эта версия системы оставалась минимальным требованием для запуска sView. Поддержка относительно старых версий операционных систем даёт максимальный охват потенциальных пользователей, но требует дополнительных усилий.

Практика разработки Windows, Linux, Android и macOS приложений показывает, что предположения о том, что собранное приложение "вроде должно работать" на всех версиях систем периодически дают сбой, и проблемы совместимости всплывают самым неожиданным образом. В таких случаях возможность проверить работоспособность приложения на разных (в том числе самых старых, формально поддерживаемых) системах становится жизненно необходимой.

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

Также понадобится и подходящий сборочный инструментарий. В прошлом, сборка приложения для нужной версии OS X требовала наличия нужной версии SDK в XCode. Однако упаковка нескольких SDK в XCode существенно увеличивала размер установки и старые версии SDK быстро исключались из новых версий XCode, осложняя сборку приложений для старых систем.

Для обеспечения совместимости с OS X 10.6 Snow Leopard, приложение sView долгое время собиралось на OS X той же версии, предустановленной на старом MacBook. При этом несколько версий OS X было установлено на внешний жёсткий диск для тестирования.

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

  • переменной окружения MACOSX_DEPLOYMENT_TARGET
    (т.е., export MACOSX_DEPLOYMENT_TARGET=10.0);

  • или флага компилятора -mmacosx-version-min
    (т.е., EXTRA_CXXFLAGS += -mmacosx-version-min=10.0).

В случае CMake соответствующий параметр называется CMAKE_OSX_DEPLOYMENT, а у qmake - QMAKE_MACOSX_DEPLOYMENT_TARGET.

Настройки проекта в XCode 11 позволяют выбрать минимальной платформой даже OS X 10.6, но данный выбор приводит только к ошибкам при сборке и Hello World удалось собрать только при выборе 10.7 или версия новее. Впрочем, OS X 10.6 Snow Leopard вышла в далёком 2009 году - то есть одиннадцать лет назад, - и едва ли имеет активных пользователей. Какую же версию выбрать в качестве минимальной?

OS X 10.10 Yosemite была выпущена около 6 лет назад и на 6 релизов "старее" самой актуальной на данный момент macOS 11.0 Big Sur. Трудно представить пользователей более старой OS X с учётом агрессивной политики обновлений Apple. Помимо прочего, OS X 10.10 уже была установлена на моём старом MacBook - слишком старым для разработки, но ещё живом для проверки работоспособности собранного приложения.

В попытке обновить старичка mid-2010 MacBook выяснилось, что свежие версии macOS более не поддерживают такие устройства , а последней совместимой версией оказалась macOS 10.13 High Sierra выпущенная в 2017 году.
Таким образом, Apple лишила свой продукт программных обновлений спустя 7 лет! При этом магазин приложений Apple более не позволяет загрузить старые версии macOS - то есть и обновить OS X 10.10 до macOS 10.13 не получится обычным способом.

Для сборки sView на свежем инструментарии в Makefile проекта была прописана версия 10.10, а в Info.plist был указан параметр LSMinimumSystemVersion=10.0. Сама сборка была осуществлена на macOS 10.15, установленной на относительно свежем Mac mini 2018, и протестирована на макбуке с OS X 10.10 - приложение заработало и было опубликовано на сайте!

и тут, как снег на голову, пришли сообщения пользователей об ошибках запуска sView на версиях macOS, новеепротестированной. Вздор! Откуда система вообще могла взять цифру 10.15, если LSMinimumSystemVersion указывает на 10.10 - а это единственный ранее известный мне источник для подобных сообщений macOS об ошибках?

В слепую локализовать проблему не удавалось - поиски 10.15 в архиве с приложением и в сборочных скриптах ни к чему не привели. Поэтому было найдено временное подопытное устройство с macOS 10.13, выводящее такое же сообщение об ошибке. Удивительно, но запуск исполнительного файла sView из терминала происходил без всяких проблем и ошибок!

Эксперименты показали, что что-то не так непосредственно с исполнительным файлом sView, и в конце концов, утилита otool -l выявила источник проблемы:

Load command 9        cmd LC_BUILD_VERSION    cmdsize 32   platform macos        sdk 10.15      minos 10.15     ntools 1       tool ld    version 450.3

Информации о загадочном minos нашлось не много в интернете, но удалось выяснить, что данное поле появилось в заголовке бинарный файлов macOS относительно недавно. Но этого факта оказалось достаточно, чтобы ответить на первый вопрос - как так получилось, что более старая версия OS X 10.10 запускала sView без проблем, а новые macOS 10.13-10.14 выдавали ошибки? Да просто OS X 10.10 ничего не знает о существовании нового поля minos!

Оставался последний вопрос - где в процессе сборки приложения закралась ошибка? Изучение пакета sView выявило, что поле minos присутствовало только библиотеках и исполняемом файле самого проекта, но не в библиотеках FFmpeg, собранных схожим образом. То есть проблема была явно в Makefile проекта. Как оказалось,флаг -mmacosx-version-min передавался компилятору через переменную EXTRA_CXXFLAGS, но не передавался линковщику. Добавление флага в переменную EXTRA_LDFLAGS наконец-то решило проблему:

TARGET_OS_VERSION = 10.10EXTRA_CFLAGS   += -mmacosx-version-min=$(TARGET_OS_VERSION)EXTRA_CXXFLAGS += -mmacosx-version-min=$(TARGET_OS_VERSION)EXTRA_LDFLAGS  += -mmacosx-version-min=$(TARGET_OS_VERSION)

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

Подробнее..

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

08.02.2021 14:10:06 | Автор: admin

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

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

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

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

Очень быстро накидал основной код, который вытаскивает кадры из видео, распознает обьекты с помощью модели Resnet50, которую рекомендовали яблочники у себя на сайте, она очень шустро работала и позволяла настраивать процент при котором считать объект распознанным. Сам код спокойно раздается на том же apple.com для всех желающих. Подключил библиотеку SQLite.swift, обернул ее функции в свои методы, все работает!

Потом еще пришлось неплохо повозиться с алгоритмами создания очереди обработки списка файлов и в этот момент я обратил внимание что программа то разрослась! Уже после 1000-й строчки кода вдруг пришло понимание что mvc-паттерн уже совсем не подходит для этого проекта, а именно он обычно и предлагается на всех туториалах и подсказках из Stackoverflow. Как же затягивает процесс когда все получается и даже не обращаешь внимания что у тебя весь код навален в одном файле. Стал раскидывать все по классам, синглтонам и прочим сущностям. Вроде стало полегче, но это не надолго, ибо впереди еще нужно распаралелить процессы на потоки, что бы программа не замирала пока идет процесс распознавания в большом количестве файлов.

Почитал статьи о многопоточных приложениях, о Grand Central Dispatch (GCD) - технологии Apple, предназначенная для многоядерных процессоров, вроде бы тоже все просто - кидаешь фоновую работу в основной поток а обновление интерфейса в главный поток и опять все работает! Но что то подсказывало что так легко и быстро не бывает! Начался процесс тестирования.

Первый серьезный глюк дал о себе знать когда запустил сканировать большой архив семейных видеофайлов, 70 гигов, видео снятые в разное время на разные телефоны и поэтому и разные форматы - идеально! Как раз то что надо для тестирования! Сканирование останавливалось на 420-ом файле, снятом на какой-то старый Самсунг под windows mobile, ну да ладно, может битый файл, подумал я и удалил его, запустил снова. опять 420 файл! Совершенно в другом формате, с яблофона, не битый! Что за магия такая? Ну давайте и его удалим. еще раз опять 420 файл пора лезть в дебаггер.

Две недели, две недели жизни (в свободное от работы время) я посвятил поиску этой ошибки! Виновником оказался объект VNCoreMLRequest, работающий с запросами к ML-модели и который не любит когда его используют в нескольких потоках, при этом он никак не проявляет себя в логах дебаггера а просто выдает ошибку времени выполнения, проще говоря кладет один из потоков. Так же порадовал метод обработки изображений copyCGImage, который отказывался работать стабильно, правда яблочники предупредили об этом на своем ресурсе для разработчиков и предлагали использовать вместо него другой асинхронный метод generateCGImagesAsynchronously, который как ни странно работал еще хуже, в итоге я вернулся к первому методу окружив его блоком try catch.

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

Слоты для CoreML моделей в настройках программыСлоты для CoreML моделей в настройках программы

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

Интерфейс программы Apple Create MLИнтерфейс программы Apple Create ML

Программа сформировывалась в завершенный продукт, не хватало одного - как пользователю выводить найденные видео фрагменты в программу видеомонтажа. Я наметил два варианта - это форматы EDL и XML. Реализовать первый формат не составляло особого труда, это старый известный с ленточных времен формат, используемый киношниками для переноса намеченных фрагментов в системы монтажа. Но проблема состояла в том, что EDL не содержит информацию о передаваемых файлах, а только о таймкодах, точках входа и выхода фрагмента, то есть в итоге пользователь получит набор фрагментов в секвенции, но они все будут оффлайн, потому что не известно из каких файлов брать эти куски, а ведь их, этих файлов может быть много, для каждого фрагмента свой файл. Другое дело XML! Он содержит всю информацию которую ты только можешь в него запихнуть: и путь, и формат файлов, и настройки звука, и даже применяемые маркеры, все что нужно, современный формат! Но вот реализовать всю эту крутизну это дело далеко не простое, и прочитать надо литературу которой нигде нет, ибо нужна информация именно по XML, используемом для экспорта секвенции именно с видео данными, а не какого нибудь там каталога для инет-магазина. Эту задачу я стал решать с изучения выведенного изAdobe Premiere шаблонной секвенции с парой файлов на таймлайне в XML. Полученный файл я открыл в текстовом редакторе и стал изучать. Постепенно стали вырисовываться блоки кода, для каждого плана на секвенции три блока - один для видео и два для звука, в общем теле кода сначала идут видео блоки а потом привязанные к ним аудио блоки, так же есть начальные и завершающее блоки файла с тегами описывающими, видимо, формат секвенции. Я разделил все эти блоки в отдельные файлы, которые обозначил как многострочные String ресурсы в Xcode. Создал отдельный класс, который оперирует этими блоками в цикле, собирая их в нужной последовательности в один код и подставляя в нужные места строковые данные с именем файла и информацией о таймкоде. Та еще работка! Хотя может быть абсолютно привычно для html-верстальщика.На первый взгляд сложная задача, но решена была довольно быстро, хотя это можно назвать хакерским методом) Но формат то по сути открытый! Другое дело что мы используем версию XML , сгенерированную Аdobe Premiere, с его тэгами, но насколько эти теги имеют проприетарный формат я рассуждать не берусь, знаю только что все работает, и в Final Cut Pro (в полной версии), и вдругих монтажках

Интерфейс программы VideoindexИнтерфейс программы Videoindex

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

Сейчас я думаю, чего еще можно добавить в приложение, есть уже некоторые идеи, и собственные и присланные пользователями, которые уже пользуются приложением. Например сейчас с появлением новых процессоров Apple Silicon, которые имеют аппаратное ускорение ML процессов до 16x, нужно обязательно сделать поддержку этой платформы в новых версиях. Ну а пока программа уже доступна в Mac App Store, называется Videoindex.

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

Подробнее..

Recovery mode Наш Automator, или генерация снимков экрана для AppStore

19.03.2021 20:17:05 | Автор: admin

В один замечательный вечер мы с коллегой публиковали небольшое приложение в AppStore. Публикация приложения довольно-таки долгий процесс и состоит из множества этапов. Один из этапов - подготовка картинок для магазина приложений. Задача, на первый взгляд простая - запустить приложение в симуляторе и сделать снимок экрана приложения, а нужны экраны на шести языка, в нескольких размерах, с демонстрацией пяти разных состояния приложения. За часик можно было управиться просто делая снимки руками, при этом попивая кофе и обсуждая общие темы. Но мы же программисты и руками делать не наш метод. Надо автоматизировать процесс. Хоть мы и никогда такого не делали у нас получилось. Мы узнали как легко программно управлять приложениями MacOS. И написали AppleScript который управляет приложениями XCode и Simulator.

Постановка

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

Инструмент

Краткий поиск навел нас на Automator.

Запускаем.

Нас интересует WorkFlow - создаем. Программа выдает окошко из двух частей. Слева Actions - различные действия над приложениями и системой. Справа сам WorkFlow. Перетаскиваем Действия в WorkFlow и можем запускать последовательность действий. Это графический построитель программы WorkFlow.

Программа дает много возможностей, но после того, как мы наигрались с возможностями графического построителя - мы поняли, что нам проще было бы просто писать код. Привычней. И Automator дает нам такую возможность. Очистим все из панели WorkFlow, и добавим одно действие - Run AppleScript.

Обратите внимание, в библиотеке действий, помимо Run AppleScript, есть еще очень интересные действия, такие как Run JavaScript или Run Shell Script. В одном WorkFlow может быть много действий, и можно запускать другие Workflow (Run WorkFlow).

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

Реализация

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

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

Создаем переменные и пишем руками какие устройства нам нужны в массив sizes.

set ipad to "iPad Pro (12.9-inch) (3rd generation)"set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}А

А список схем в массив schemes

set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}

И запускаем перебор по размерам и по языкам:

repeat with size in sizes    repeat with lang in schemes    -- .....repeat with size in sizesrepeat with lang in schemes 

Внутри цикла у нас есть доступ к переменой size, и lang.

Говорим приложению XCode выбрать схему и размер, согласно переменным цикла, и просим запустить Simulator:

        tell application "Xcode" to activate        tell application "System Events"            tell process "Xcode"                tell menu bar 1                    tell menu "Product"                        tell menu item "Scheme"                            tell menu "Scheme"                                click menu item lang                            end tell                        end tell                           tell menu item "Destination"                            tell menu "Destination"                                click menu item size                            end tell                        end tell                        click menu item "Run"                    end tell                end tell            end tell        end tell

И выводим диалог с кнопкой продолжить:

        tell application "System Events"            display dialog "Continue"        end tell

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

Ну а дальше программа просит Симулятор сделать снимок экрана, который на десктоп.

        tell application "Automator" to activate        tell application "System Events"            tell process "Simulator"                tell menu bar 1                    tell menu "File"                        click menu item "Save Screen"                    end tell                end tell            end tell        end tell

Далее просим валовый менеджер переименовать файл как нам удобно:

        tell application "Finder"            set the source_folder to (path to desktop folder) as alias            sort (get files of source_folder) by creation date            set theFile to (item 1 of reverse of result) as alias            set newName to lang & "-" & size & " .png"            set name of theFile to newName        end tell

Если это снимок экрана для iPad то мы меняем ориентацию экрана и делаем еще снимок:

        if size as string is equal to ipad then            tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                                                tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Landscape Right"                                end tell                            end tell                        end tell                                                delay 2                        tell menu "File"                            click menu item "New Screen Shot"                        end tell                                                tell application "Finder"                            set the source_folder to (path to desktop folder) as alias                            sort (get files of source_folder) by creation date                            set theFile to (item 1 of reverse of result) as alias                            set newName to lang & "-" & size & "-landscape" & " .png"                            set name of theFile to newName                        end tell                                                                        tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Portrait"                                end tell                            end tell                        end tell                                            end tell                end tell            end tell        end if

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

Мы конечно могли еще автоматизировать и копирование файлов, но мы на этом остановились.

Итог

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

А вот и Код полностью:

on run {input, parameters}    set ipad to "iPad Pro (12.9-inch) (3rd generation)"    set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}    set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}    repeat with size in sizes        repeat with lang in schemes            tell application "Xcode" to activate            tell application "System Events"                tell process "Xcode"                    tell menu bar 1                        tell menu "Product"                                                        tell menu item "Scheme"                                tell menu "Scheme"                                    click menu item lang                                end tell                            end tell                                                        tell menu item "Destination"                                tell menu "Destination"                                    click menu item size                                end tell                            end tell                            click menu item "Run"                        end tell                    end tell                end tell            end tell                        tell application "System Events"                display dialog "Continue"            end tell                        tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                        tell menu "File"                            click menu item "Save Screen"                        end tell                    end tell                end tell            end tell                        tell application "Finder"                set the source_folder to (path to desktop folder) as alias                sort (get files of source_folder) by creation date                set theFile to (item 1 of reverse of result) as alias                set newName to lang & "-" & size & " .png"                set name of theFile to newName            end tell                                    --iPad            if size as string is equal to ipad then                                                tell application "Automator" to activate                tell application "System Events"                    tell process "Simulator"                        tell menu bar 1                                                        tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Landscape Right"                                    end tell                                end tell                            end tell                            delay 2                            tell menu "File"                                click menu item "New Screen Shot"                            end tell                                                        tell application "Finder"                                set the source_folder to (path to desktop folder) as alias                                sort (get files of source_folder) by creation date                                set theFile to (item 1 of reverse of result) as alias                                set newName to lang & "-" & size & "-landscape" & " .png"                                set name of theFile to newName                            end tell                                                                          tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Portrait"                                    end tell                                end tell                            end tell                                                    end tell                    end tell                end tell            end if                                end repeat    end repeat            return inputend run
Подробнее..

Recovery mode Наш Automator, управляем приложениями MacOS на AppleScript

19.03.2021 22:15:52 | Автор: admin

В один замечательный вечер мы с коллегой публиковали небольшое приложение в AppStore. Публикация приложения довольно-таки долгий процесс и состоит из множества этапов. Один из этапов - подготовка картинок для магазина приложений. Задача, на первый взгляд простая - запустить приложение в симуляторе и сделать снимок экрана приложения, а нужны экраны на шести языка, в нескольких размерах, с демонстрацией пяти разных состояния приложения. За часик можно было управиться просто делая снимки руками, при этом попивая кофе и обсуждая общие темы. Но мы же программисты и руками делать не наш метод. Надо автоматизировать процесс. Хоть мы и никогда такого не делали у нас получилось. Мы узнали как легко программно управлять приложениями MacOS. И написали AppleScript который управляет приложениями XCode и Simulator.

Постановка

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

Инструмент

Краткий поиск навел нас на Automator.

Запускаем.

Нас интересует WorkFlow - создаем. Программа выдает окошко из двух частей. Слева Actions - различные действия над приложениями и системой. Справа сам WorkFlow. Перетаскиваем Действия в WorkFlow и можем запускать последовательность действий. Это графический построитель программы WorkFlow.

Программа дает много возможностей, но после того, как мы наигрались с возможностями графического построителя - мы поняли, что нам проще было бы просто писать код. Привычней. И Automator дает нам такую возможность. Очистим все из панели WorkFlow, и добавим одно действие - Run AppleScript.

Обратите внимание, в библиотеке действий, помимо Run AppleScript, есть еще очень интересные действия, такие как Run JavaScript или Run Shell Script. В одном WorkFlow может быть много действий, и можно запускать другие Workflow (Run WorkFlow).

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

Реализация

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

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

Создаем переменные и пишем руками какие устройства нам нужны в массив sizes.

set ipad to "iPad Pro (12.9-inch) (3rd generation)"set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}А

А список схем в массив schemes

set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}

И запускаем перебор по размерам и по языкам:

repeat with size in sizes    repeat with lang in schemes    -- .....repeat with size in sizesrepeat with lang in schemes 

Внутри цикла у нас есть доступ к переменой size, и lang.

Говорим приложению XCode выбрать схему и размер, согласно переменным цикла, и просим запустить Simulator:

        tell application "Xcode" to activate        tell application "System Events"            tell process "Xcode"                tell menu bar 1                    tell menu "Product"                        tell menu item "Scheme"                            tell menu "Scheme"                                click menu item lang                            end tell                        end tell                           tell menu item "Destination"                            tell menu "Destination"                                click menu item size                            end tell                        end tell                        click menu item "Run"                    end tell                end tell            end tell        end tell

И выводим диалог с кнопкой продолжить:

        tell application "System Events"            display dialog "Continue"        end tell

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

Ну а дальше программа просит Симулятор сделать снимок экрана, который на десктопе.

        tell application "Automator" to activate        tell application "System Events"            tell process "Simulator"                tell menu bar 1                    tell menu "File"                        click menu item "Save Screen"                    end tell                end tell            end tell        end tell

Далее просим файловый менеджер переименовать файл как нам удобно:

        tell application "Finder"            set the source_folder to (path to desktop folder) as alias            sort (get files of source_folder) by creation date            set theFile to (item 1 of reverse of result) as alias            set newName to lang & "-" & size & " .png"            set name of theFile to newName        end tell

Если это снимок экрана для iPad то мы меняем ориентацию экрана и делаем еще снимок:

        if size as string is equal to ipad then            tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                                                tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Landscape Right"                                end tell                            end tell                        end tell                                                delay 2                        tell menu "File"                            click menu item "New Screen Shot"                        end tell                                                tell application "Finder"                            set the source_folder to (path to desktop folder) as alias                            sort (get files of source_folder) by creation date                            set theFile to (item 1 of reverse of result) as alias                            set newName to lang & "-" & size & "-landscape" & " .png"                            set name of theFile to newName                        end tell                                                                        tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Portrait"                                end tell                            end tell                        end tell                                            end tell                end tell            end tell        end if

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

Мы конечно могли еще автоматизировать и копирование файлов, но мы на этом остановились.

Итог

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

А вот и Код полностью:

on run {input, parameters}    set ipad to "iPad Pro (12.9-inch) (3rd generation)"    set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}    set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}    repeat with size in sizes        repeat with lang in schemes            tell application "Xcode" to activate            tell application "System Events"                tell process "Xcode"                    tell menu bar 1                        tell menu "Product"                                                        tell menu item "Scheme"                                tell menu "Scheme"                                    click menu item lang                                end tell                            end tell                                                        tell menu item "Destination"                                tell menu "Destination"                                    click menu item size                                end tell                            end tell                            click menu item "Run"                        end tell                    end tell                end tell            end tell                        tell application "System Events"                display dialog "Continue"            end tell                        tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                        tell menu "File"                            click menu item "Save Screen"                        end tell                    end tell                end tell            end tell                        tell application "Finder"                set the source_folder to (path to desktop folder) as alias                sort (get files of source_folder) by creation date                set theFile to (item 1 of reverse of result) as alias                set newName to lang & "-" & size & " .png"                set name of theFile to newName            end tell                                    --iPad            if size as string is equal to ipad then                                                tell application "Automator" to activate                tell application "System Events"                    tell process "Simulator"                        tell menu bar 1                                                        tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Landscape Right"                                    end tell                                end tell                            end tell                            delay 2                            tell menu "File"                                click menu item "New Screen Shot"                            end tell                                                        tell application "Finder"                                set the source_folder to (path to desktop folder) as alias                                sort (get files of source_folder) by creation date                                set theFile to (item 1 of reverse of result) as alias                                set newName to lang & "-" & size & "-landscape" & " .png"                                set name of theFile to newName                            end tell                                                                          tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Portrait"                                    end tell                                end tell                            end tell                                                    end tell                    end tell                end tell            end if                                end repeat    end repeat            return inputend run
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru