Привет! Из этого мини-цикла статей ты узнаешь:
-
Как унаследовать 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 разрешений не поддерживает коллбэки.
Спасибо за внимание!