Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет! Из этого мини-цикла статей ты узнаешь:
Как унаследовать 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 strings
pod '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 разрешений не поддерживает коллбэки.
Спасибо за внимание! ?