iOS Permission Library, From Idea to Release (Part 1)

Sergei Moskvin
7 min readMay 10, 2021

Hi. From these articles you will learn how to:

  • Inherit a Swift class not entirely, only what you need in it;
  • Allow a user of your CocoaPods or Carthage library to compile only the parts that are actually used;
  • Extract iOS resources to get specific system icons and localized strings;
  • Support completion blocks even where it is not provided by default system permission API.

In general, here is how I tried to write an ultimate library for working with iOS permissions. What surprises I encountered and what non-obvious solutions I found for some problems. I will be glad if it turns out to be interesting and useful.

A few words about the library itself

One day I came up with the following idea — all mobile developers have to work with system permissions. Do you want to use a camera? Get a user’s permission. Decided to send him notifications? You have to get permission.

In all these situations, an ideal developer goes to study Apple documentation. But more often we save time and just google ready-made solutions on the Stack Overflow. I do not say that it is always a bad practice, but it is easy to miss some important nuance.

My quick GitHub search has shown that libs for working with iOS permissions exist for a long time, and there are a lot of them. But whatever you take, there are the same problems everywhere — either it does not receive updates, or something is not supported, or the documentation is in Chinese.

Finally, I decided to write my own library. I tried to make it perfect. I would be grateful if you write in the comments, did I succeed. In general, PermissionWizard

  • Supports the latest features of iOS 14 and macOS 11 Big Sur;
  • Works great with Mac Catalyst;
  • Supports all existing types of system permissions;
  • Validates your ”Info.plist“ and protects you from crashes if something is wrong with it;
  • Supports callbacks even where it is not provided by default system API;
  • Allows you to do not worry about risks that a response to a request for some permission will be returned in an unknown thread while you are waiting for it in the DispatchQueue.main, for example;
  • Completely written in pure Swift;
  • Provides unified API regardless of permission types you are working with;
  • Optionally includes native icons and localized strings for your UI;
  • Modular, integrate only the components that you need.

But let’s finally get to the really interesting part…

How to inherit a class not entirely, only what you need in it?

When I started working on PermissionWizard, I quickly realized that I need the same elements for most of the supported permission types:

  • A usageDescriptionPlistKey property;
  • checkStatus and requestAccess methods.

It would be strange not to inherit each class responsible for a particular permission type from a universal parent class, where all it is already declared and partially implemented.

In addition, I was going to document every method, every property in the library, and since Swift and Xcode do not allow you to reuse code comments, such an inheritance kills two birds with one stone — I don’t need to copy the same comments from class to class.

But there were problems:

  • Some permission types (local network, for example) do not allow you to check the current status of the permission without actually requesting access to it. The inherited checkStatus declaration turns out to be inappropriate in this case. It is only confusing because it is stuck in autocomplete, although it has no implementation.
  • To work with the location permission, the standard requestAccess(completion:) is not suitable because to request access, you need to decide, whether we always need it, or only when a user is actively using the app. Some requestAccess(whenInUseOnly:completion:) is more suitable here, inherited declarations are useless again.
  • The photos permission uses two different plist keys — one for full access (”NSPhotoLibraryUsageDescription“) and one for only adding new photos and videos (”NSPhotoLibraryAddUsageDescription“). We see that the inherited usageDescriptionPlistKey property turns out to be unsuitable again. It is more logical to have two separate and more telling constants.

I have given only a few examples of the problems that have arisen. However, only some types of system permissions required such exceptions. Most of them, and there are 18 such types, are built on the same unchanging skeleton, which you do not want to customize.

There are different ways to solve this situation. For example, you can spread all these property and method declarations across different protocols, and in each case inherit only the necessary ones. But this is confusing and inconvenient. In this case, there is a more elegant way — an attribute.

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) { ... }
}

Redefining a method marking it with @available(*, unavailable) attribute not only makes it impossible to invoke it, returning an error during project building, but also hides it from autocomplete in Xcode. It seems to exclude the method from inheritance.

Of course, I did not reinvent the wheel here, but the solution is not widely known, so I decided to share it.

How to allow a user of your CocoaPods or Carthage library to compile only its actually used parts?

PermissionWizard supports 18 types of system permissions — from photos and contacts to Siri and tracking, which appeared in iOS 14. It means that the library imports and uses AVKit, CoreBluetooth, CoreLocation, CoreMotion, EventKit, HealthKit, HomeKit, and many more system frameworks.

It is not difficult to guess that if you connect such a library to your project entirely, even if you use it for working only with a single permission type, Apple will not pass your app to the App Store, because it will see that it uses suspiciously many different privacy APIs. In addition, such a project will take a little longer to build, and your app will weigh a little more. Some way out is required.

CocoaPods

Working with this dependency manager, a solution is relatively easy to find. We can divide the library into independent components, allowing to install only those parts that are needed by a developer selectively. At the same time, we separate the component with icons and localized strings, since not everyone needs them.

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'

In this case, the ”Podspec“ of our library (a file describing it for CocoaPods) looks something like this…

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

Connecting each new component adds new code files to the library distributive and sets flags in the project settings, based on which we can exclude certain parts of code from building.

#if BLUETOOTH
final class Bluetooth { ... }
#endif

Carthage

This case is more complicated. The Carthage does not support library splitting, unless you really split it into different repositories, for example. We need some workaround.

In the root of our library, we create a ”Settings.xcconfig“ file and write the following…

#include? "../../../../PermissionWizard.xcconfig"

By default, the Carthage installs dependencies to a ”Carthage/Build/iOS“ directory, so the above guide refers to some ”PermissionWizard.xcconfig“ file, which can be located by our library user in the root folder of his project.

Let’s outline its approximate content…

ENABLED_FEATURES = ASSETS BLUETOOTH ...
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) $(ENABLED_FEATURES) CUSTOM_SETTINGS

Finally, we need to inform our library that it should refer to ”Settings.xcconfig“ as an additional source of building settings. To do it, add this file to the library project. Then open the ”project.pbxproj“ file with any convenient text editor. Here we need to find the identifier assigned to just added ”Settings.xcconfig“, as in the example below.

A53DFF50255AAB8200995A85 /* Settings.xcconfig */ = { isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Settings.xcconfig; sourceTree = "<group>"; };

Now, for each ”XCBuildConfiguration“ block we have in the ”project.pbxproj“, add a line according to the following example (line 3)…

B6DAF0412528D771002483A6 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A53DFF50255AAB8200995A85 /* Settings.xcconfig */;
buildSettings = {
...
};
name = Release;
};

Why in addition to the flags with the necessary library components we also specify a certain CUSTOM_SETTINGS? The answer is simple — in the absence of this flag, we believe that a user of the library didn’t try to configure it. He did not create a ”PermissionWizard.xcconfig“ in the root of his project. So without the CUSTOM_SETTINGS flag, we include all available library components.

#if BLUETOOTH || !CUSTOM_SETTINGS
final class Bluetooth { ... }
#endif

That’s all for now

In the next part, we will talk about how I found required localized strings and icons among the 5 gigabytes of iOS 14 firmware. And I will also tell you how I managed to implement requestAccess(completion:) even where default system permission API does not support callbacks.

Thanks for your attention!

--

--