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

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
07-Дек-2020 21:30


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

Похожие новости: Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_xcode, #_swift, #_privatnost (приватность), #_konfidentsialnost (конфиденциальность), #_razreshenija (разрешения), #_permissions, #_biblioteki (библиотеки), #_nasledovanie (наследование), #_atributy (атрибуты), #_cocoapods, #_carthage, #_info.plist, #_razrabotka_pod_ios (
Разработка под iOS
)
, #_xcode, #_swift
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 22-Ноя 19:10
Часовой пояс: UTC + 5