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

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

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру 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 разрешений не поддерживает коллбэки.

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

Источник: https://habr.com/ru/post/531778/


Интересные статьи

Интересные статьи

Я начинающий JS front-end разработчик. Сейчас я учусь и стажируюсь в одной минской IT компании. Изучение основ web-ui проходит на примере JS библиотеки Webix и я хочу поделиться своим первым ...
22 ноября завершилась программа преакселерации конкурса «Цифровой прорыв», участие в которой приняли 53 лучших команд финалистов. В сегодняшнем посте мы расскажем о команде, которая в ближайшем б...
В предыдущей статье мы научились запускать Hello World ядро и написали пару функций для работы со строками. Теперь пришло время расширить библиотеку С чтобы можно было реализовать kprintf и други...
Приветствую вас, хабровчане! Как и обещал в конце прошлой статьи, я обратил свой взгляд на игровой движок. Правда мне больше приглянулся Godot. Почему? Тема для отдельной статьи размышления, а...
IETF одобрили стандарт Automatic Certificate Management Environment (ACME), который поможет автоматизировать получение SSL-сертификатов. Расскажем, как это работает.