Как стать автором
Обновить

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

Разработка под iOS*Xcode*Swift*

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

  • Как унаследовать 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 разрешений не поддерживает коллбэки.

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

Теги:приватностьконфиденциальностьразрешенияpermissionsбиблиотекинаследованиеатрибутыCocoaPodsCarthageInfo.plist
Хабы: Разработка под iOS Xcode Swift
Всего голосов 6: ↑5 и ↓1+4
Просмотры1.6K
Комментарии Комментарии 3

Похожие публикации

Лучшие публикации за сутки