CXHubSDK представляет собой библиотеку для обработки и отображения push-
и inApp-
нотификаций на платформе iOS.
Библиотека поставляется в виде динамических фреймворков.
Библиотека состоит из двух модулей: CXHubCore и CXHubNotify Оба модуля библиотеки должны быть добавлены в виде зависимостей ко всем компонентам приложения (основному и расширениям: NotificationServiceExtension, NotificationContentExtension). Для корректного добавления библиотеки в проект используется CocoaPods (см. 1. Добавление библиотеки в проект )
CXHubNotify (основной "пользовательский" модуль, использующий CXHubCore) предоставляет возможность работы с различными типами нотификаций (push-
и inApp-
),
а также позволяет отправлять сообщения о различных действиях пользователя с нотификациями и системные сообщения о состоянии приложения.
Для базовой настройки библиотеки используется конфигурационный файл (см. ниже - Notify.plist по умолчанию),
который содержит основные параметры для работы обоих модулей библиотеки.
Настройки CXHubNotify содержатся под ключом LibNotify
.
Некоторые настройки могут быть изменены после загрузки конфигурационного файла.
Внимание: изменение настроек возможно только ДО инициализации библиотеки
Доступ к конфигурации CXHubNotify осуществляется через свойство CXApp.currentConfig.notifyConfig
Для более детальной информации см. CXAppConfig.h и CXNotifyConfig.h
Основной интерфейс взаимодействия с библиотекой предоставлен через класс CXApp (см. CXApp.h) Также для корректной работы библиотеки необходимо реализовать несколько протоколов (см. ниже)
CXHubSDK обеспечивает поддержку GDPR Вы можете разрешить или запретить передачу на сервер VendorID и информации об используемой WiFi-сети. Для разрешения\запрета передачи данной информации используйте:
BOOL boolValue = <true or false>;
- [[CXNotify getInstance] setDeviceIdTrackingEnabled:boolValue];
В случае наличия авторизации\уникального идентификатора пользователя в приложении, библиотека предоставляет возможность сохранить некритичные (обезличенные) данные пользователя. Для этого после успешной авторизации пользователя в приложении необходимо вызвать:
__block NSString *userId = @<your_app_user_id>; //уникальный для вашего приложения идентификатор пользователя
[[CXNotify getInstance] setUserId:userId];//Передаем userId в CXHub
userId = [[CXNotify getInstance] getUserId]; //Получаем текущий userId
//устанавливаем отслеживание изменений userId
[[CXNotify getInstance]setUserIdChangeListener: ^(NSString *userId, NSString *prevUserId){
userId = [[CXNotify getInstance] getUserId];
}];
После этого у приложения появляется возможность устанавливать и передавать в CXHub необходимые параметры пользователя (user properties), например, ник, номер телефона, е-mail итд.
Вы можете устанавливать пользовательские свойства, используя следующий пример:
// Set string-value property
[[CXNotify getInstance] setInstanceProperty:@"TestPropertyStringName"
withStringValue:@"TestPropertyStringValue"];
// Set array-value property (items must be enumeratable)
[[CXNotify getInstance] setInstanceProperty:@"TestPropertyArrayName"
withArrayValue:
@[@"Test", @"Property", @"Array",@"Value"]
];
// Set dictionary-value property
[[CXNotify getInstance] setInstanceProperty: @"TestPropertyDictionaryName"
withDictionaryValue:
@{@"key1": @"value1", @"key2": @"value2"}
];
CXHubSDK (модуль CXHubNotify) предоставляет возможность работы с темной темой (для работы с Activity).
Если ваше приложение поддерживает темную тему, то включить поддержку можно,
установив свойство CXApp.currentConfig.notifyConfig.activitySettings.colorSettings.darkModeSupported = true
NB: при этом необходимо сконфигурировать правильные цвета (в конфигурационном файле )
CXHubSDK (модуль CXHubNotify) позволяет использовать динамические шрифты для Activity. Вы можете использовать статические шрифты (по умолчанию).
Также возможно использование шрифтов по вашему усмотрению.
Чтобы реализовать этот функционал, необходимо установить CXApp.currentConfig.notifyConfig.activitySettings.fontType = Custom
и
реализовать протокол (optional- методы // MARK: - Fonts
).
После этого библиотека будет запрашивать шрифты у приложения через делегат.
Также библиотека будет отслеживать изменения системных настроек
и запрашивать нужные шрифта при каждом изменении настроек приложения.
CXHubSDK обеспечивает корректное отображение обработку UNNotificationAction(s)`toast-buttons` (только при условии реализации ServiceExtension и ContentExtension), установленных через Web-интерфейс CXHub. При создании кнопки Web-интерфейс CXHub позволяет добавить пользовательское событие, которое при нажатии на кнопку будет отправлено на сервер. Т.о. обеспечивается возможность отслеживать нажатие на конкретную кнопку. Если событие не добавлено к кнопке, то библиотека будет передавать только системные события, связанные с логикой работы самой кнопки (AppOpen - запуск приложения, Landing- события, итп). Поэтому, если для анализа важно понимать, какой элемент управления выбрал пользователь, то необходимо связать с ним соответствующее пользовательское событие через Web-интерфейс CXHub.
Для корректной работы библиотеки необходимо добавить список категорий (не менее 5, лучше 10) в конфигурационный файл под ключи:
LibNotify -> UNNotificationExtensionCategory
Точно такой же список категорий должен быть добавлен в ContentExtension Info.plist: NSExtension -> NSExtensionAttributes -> UNNotificationExtensionCategory
для более детальной информации см. 2.2 NotificationServiceExtension (расширение) и 2.3 ContentExtension (расширение)
NB: CXHubSDK использует NSUserDefaults ([[NSUserDefaults alloc] initWithSuiteName:**_your_app_group_id_**]
) для сохранения и передачи общих данных об использовании категорий между модулями (главным приложением и расширениями). Поэтому, если ваше приложение также использует NSUserDefaults, пожалуйста, не вызывайте метод reset
, иначе push-нотификации могут отображаться некорректно и поведение при нажатии на кнопки будет непредсказуемым.
inApp
- это специальный вид нотификаций, который библиотека запрашивает с сервера самостоятельно, и они отображаются только при работающем (foreground) приложении в соответствии с расписанием и логикой, установленным через Web-интерфейс CXHub.
Для работы с inApp
не требуется никаких дополнительных вызовов методов библиотеки. Необходимо только правильно выполнить интеграцию и инициализацию.
Далее CXHubSDK самостоятельно (при запуске основного модуля приложения и далее с интервалом в 1 час) будет проверять наличие запланированных к показу inApp
на сервере и при получении - отображать их в соответствии с заданным на сервере расписанием и параметрами.
Пример Pod-файла:
project "notify-demo-app.xcodeproj"
use_frameworks!
platform :ios, '12.1'
abstract_target 'CXHubSDK' do
pod 'CXHubSDK', '2.0.38'
target 'notify-demo-app-objc'
target 'notify-demo-service-extension-objc'
target 'notify-demo-content-extension-objc'
end
Для подключения библиотеки необходимо сохранить Pod-файл в корневой каталог проекта и выполнить (в терминале из корневого каталога проекта):
pod deintegrate
pod install
После этого открыть xcworkspace файл. Все зависимости будут добавлены в workspace.
Для корректной работы библиотеки в приложение должны быть добавлены следующие Capabilities:
Access Wi-Fi Information
App Groups (значение должно соответствовать SharedGroupId в конфигурационном файле библиотеки (см. Подключение и базовая настройка библиотеки / 2. Конфигурационный файл
))
Background Modes: BackgroundFetch, Remote notifications
Communication Notifications (для корректного отображения пользовательских иконок в пушах)
Push Notifications
Для работы библиотеки используется базовый конфигурационный файл (один и тот же файл должен использоваться для всех компонентов: основного приложения и всех расширений, использующих библиотеку), наименование по умолчанию: Notify.plist
NB: Пример Notify.plist можно найти в папке с Pod-файлом или в архиве, содержащем подключаемые файлы библиотеки.
Данный файл содержит наборы пар ключ-значение, содержащее информацию о необходимых для работы параметрах CXHubSDK.
Пример Notify.plist :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LibNotify</key>
<dict>
<key>UNNotificationExtensionCategory</key>
<array>
<string>libnotify_default</string>
<string>libnotify_button</string>
<string>libnotify_button_queue_1</string>
<string>libnotify_button_queue_2</string>
<string>libnotify_button_queue_3</string>
<string>libnotify_button_queue_4</string>
<string>libnotify_button_queue_5</string>
</array>
<key>Enabled</key>
<true/>
<key>Landing</key>
<dict>
<key>CloseButtonColor</key>
<string>#70D05A</string>
<key>ButtonTextColor</key>
<string>#FFFFFF</string>
<key>AccentColor</key>
<string>#FF0000</string>
<key>TextColor</key>
<string>#000000</string>
<key>BackgroundColor</key>
<string>#FFFFFF</string>
</dict>
<key>Application</key>
<dict>
<key>Secret</key>
<string>**your CXHub-app secret**</string>
<key>Name</key>
<string>**your CXHub-app name**</string>
<key>Id</key>
<string>**your CXHub-app Id**</string>
</dict>
</dict>
<key>SharedGroupId</key>
<string> **your shared group ID ** </string>
<key>Enabled</key>
<true/>
<key>Debug</key>
<false/>
</dict>
</plist>
Конфигурационный файл обязательно должен содержать следующую информацию (ключи):
- LibNotify | Dictionary - базовый, содержит общий конфиг для CXHubNotify
- Application | Dictionary - информация о приложении CXHub
- Secret | String - секрет приложения CXHub
- Name | String - наименование приложения CXHub
- Id | String - идентификатор приложения CXHub
- UNNotificationExtensionCategory | Array (of strings) - список категорий для push-notification, необходим для корректной работы toast-buttons, длина массива >= 5 (см. пример выше)
- Landing | Dictionary - цветовые настройки элементов управления страницы-лэндинга (см. пример выше)
- Enabled | Boolean - разрешает или запрещает использование CXHubNotify, для корректной работы должен быть true
- Application | Dictionary - информация о приложении CXHub
- SharedGroupId | String - идентификатор общей группы приложения (основное, расширения)
- Enabled | Boolean - разрешает или запрещает использование CXHubCore, для корректной работы должен быть true
- Debug | Boolean - определяет режим работы вывода информации в консоль. При Debug == true в консоль будет выводиться максимум детальной информации о работе библиотеки
NB: не забудьте для конфигурации в релизе установить Debug == false
Более подробную информацию о ключах и значениях параметров конфигурационного файла см. в документации к CXHub (TODO)
При необходимости, возможно создание нескольких конфигурационных файлов, например:
Notify-debug.plist (Debug [Boolean] True )
Notify-release.plist (Debug [Boolean] False)
NB: смысл и возможные значения ключей см. в документации к CXHubSDK
При работе приложения в случае использования различных конфигурационных файлов при разных сценариях (например, debug\release), для каждого сценария во всех модулях (основное приложение и расширения) необходимо использовать один и тот же файл конфигурации библиотеки!
2. В пользовательском приложении должны быть реализованы 3 компонета (AppDelegate, Notification Service extension, Notification content extension):
Реализация всех 3 компонентов позволит использовать весь функционал платформы CXHub, включая обработку нотификаций при полностью закрытом приложении и работе приложения в background-режиме, а также обработку UNNotificationAction(s) (toast-buttons
)
Если ваша бизнес-логика не предусматривает обработку нотификаций при выключенном (terminated) приложении,
а также если вы не планируете использовать функционал toast-buttons
, то достаточно реализовать только п. 2.1 AppDelegate из данной инструкции
В противном случае в дополнение к п. 2.1 AppDelegate необходимо добавить в проект NotificationServiceExtension и NotificationContentExtension и выполнить интеграцию в соответствии с п. 2.2 NotificationServiceExtension (расширение) и п. 2.3 ContentExtension (расширение)
при интеграции CXHubSDK в AppDelegate необходимо выполнить следующие действия:
В случае, если предполагается использовать инициализацию по умолчанию, инициализация производится путем вызова:
if([CXApp initWithDefaultConfigAndEventsReceiver:{ nil or EventsHandler : NSObject <CXUnhandledErrorReceiver, CXMonitoringEventReceiver> }] {
[[CXNotify getInstance] setDelegate:self]; //задаем делегата <CXNotifyDelegate>
}
В этом случае для инициализации библиотеки используется конфигурационный файл Notify.plist
EventsHandler - экземпляр класса, реализующего протоколы CXUnhandledErrorReceiver, CXMonitoringEventReceiver
В случае использования нескольких конфигурационных файлов инициализация осуществляется путем вызова:
NSString *configFile = [[NSBundle mainBundle] pathForResource:@<config_file_name> ofType:@"plist"];
CXAppConfig *config = [[CXAppConfig alloc] initWithConfig:configFile];
if([CXApp initWithConfig:config withEventsReceiver:<nil or EventsHandler>]) {
[[CXNotify getInstance] setDelegate:self]; //задаем делегата <CXNotifyDelegate>
}
где <config_file_name>
- имя необходимого конфигурационного файла, структура которого должна совпадать с описанной выше
Инициализацию библиотеки необходимо производить в методе
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
до вызова метода регистрации на пуш-нотификации
[CXApp applicationDidBecomeActive:application];
[CXApp applicationWillResignActive:application];
[CXApp applicationWillTerminate:application];
[CXApp applicationDidEnterBackground:application];
[CXApp applicationWillEnterForeground:application];
2.1.3 Информируют CXHub о регистрации (ошибки регистрации) на пуш-нотификации (в соответствующих методах AppDelegate):
[CXApp applicationDidRegisterForRemoteNotificationsWithDeviceToken:deviceToken] // регистрируем токен
[CXApp applicationDidFailToRegisterForRemoteNotificationsWithError:error] // сообщаем об ошибке регистрации
AppDelegate:
- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
//Передаем полученную пуш-нотификацию в библиотеку на обработку:
CXBackgroundFetchCallback joinedCompletionHandler = [CXApp didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
joinedCompletionHandler(UIBackgroundFetchResultNoData);
}
AppDelegate.UserNotificationCenterDelegate:
-(void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
CXWillPresentNotificationResult result = [CXApp userNotificationCenter:center
willPresentNotification:notification
withCompletionHandler:completionHandler];
if (result == CXWillPresentNotificationResultFailure) {
//в таком случае обработка на стороне приложения!
completionHandler(UNNotificationPresentationOptionAlert);
}
}
AppDelegate.UserNotificationCenterDelegate:
-(void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void(^)(void))completionHandler {
CXCallBack joinedCompletionHandler = [CXApp didReceiveNotificationResponse:response
withCompletionHandler:completionHandler];
/*... здесь можно реализовать дополнительную обработку\логику в соответствии с задачами приложения ...*/
joinedCompletionHandler();
}
@protocol CXNotifyDelegate <NSObject>
@required
//This method is called when user select action open_main
//on landing. It's called in main thread.
- (bool) cxOpenMainInterface;
@optional
//You can implement this method in your class.
//All incoming pushes with url will be tried to open url with this method.
//If your implementation returns true handling of url will be completed else
//logic will call method -[UIApplication openUrl:].
//This method will be called in the main thread.
- (bool)openURL:(NSURL *)url;
//Next methods allow to configure fonts for landing with type "activity".
//These methods will be called if NotifyConfig->ActivitySettings configured with custom font type.
//In this case library expects that all next methods will be implemented else all custom fonts
//will be ignored and library will use system static fonts.
// MARK: - Fonts
// Body font. Library uses this font as a base font for activity text. According to formatting text library can
// add various styles to this font. Like as Bold or Italic.
@property (nonatomic, readonly) UIFont * activityBodyFont;
// Title font. This font will be used as a main font for activity title.
@property (nonatomic, readonly) UIFont * activityTitleFont;
// Button title font. This font will be used as a font for buttons in activities.
@property (nonatomic, readonly) UIFont * activityButtonTitleFont;
@end
Данное расширение необходимо для корректной обработки сообщений при полностью закрытом приложении. Наличие данного расширения перекрывает вызов соответствующих методов AppDelegate при получении и обработке нотификаций (см. UNNotificationServiceExtension в документации Apple)
2.2.1 Инициализация модуля библиотеки (происходит аналогично инициализации в AppDelegate, но вызывается соответствующий метод для расширения):
//Глобальные переменные-флаги
static bool appIsInitialized = false;
static bool libnotifyIsInitialized = false;
+(void)initialize {
//Если используем конфиг по умолчанию:
apiIsInitialized = [CXApp initExtensionWithDefaultConfigAndEventsReceiver:<nil or EventsHandler>];
//Если используем custom-config (например, различные конфигурационные файлы для debug\release)
CXAppConfig *appConfig = [[CXAppConfig alloc] initWithConfig:[[NSBundle mainBundle] pathForResource:@<config_file_name> ofType:@"plist"]];
apiIsInitialized = [CXApp initExtensionWithConfig:appConfig withEventsReceiver:<nil or EventsHandler>];
}
В случае успешной инициализации библиотеки appIsInitialized
будет true
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request
withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
if(apiIsInitialised && [CXApp didReceiveExtensionNotificationRequest:request
withContentHandler:contentHandler]) {
//Библиотека успешно инициализирована и пуш-нотификация пришла от CXHub, она будет обработана библиотекой и `contentHandler` будет вызван библиотекой
} else {
contentHandler(request.content);
}
}
Если (см. документацию Apple) обработка не произошла за отведенное время:
- (void)serviceExtensionTimeWillExpire {
if (apiIsInitialized && [CXApp serviceExtensionTimeWillExpire]) {
//Библиотека сама вызовет `contentHandler`
} else {
//Необходимо реализовать свою логику и вызвать `contentHandler` (см. документацию Apple)
}
}
Данное расширение отвечает за отображение расширенной информации, содержащейся в пуш-нотификации (по long-tap по баннеру сообщения), а также: - обеспечивает загрузку расширенной информации (например, дополнительного изображения для отображения) - обрабатывает (при наличии) нажатия на toast-buttons (кнопки, отображающиеся под расширенным баннером после его раскрытия по long-tap по баннеру сообщения). Представляет собой UIViewController, реализующий протокол
Пример:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>UNNotificationExtensionDefaultContentHidden</key>
<false/>
<key>UNNotificationExtensionUserInteractionEnabled</key>
<true/>
<key>UNNotificationExtensionCategory</key>
<array>
<string>libnotify_default</string>
<string>libnotify_button_queue_1</string>
<string>libnotify_button_queue_2</string>
<string>libnotify_button_queue_3</string>
<string>libnotify_button_queue_4</string>
<string>libnotify_button_queue_5</string>
</array>
<key>UNNotificationExtensionInitialContentSizeRatio</key>
<real>1</real>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.content-extension</string>
</dict>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
</dict>
</plist>
В Info.plist необходимо добавить ключ: NSUserActivityTypes : Array и значение Item0 : String : INSendMessageIntent это необходимо для корректного отображения левой пользовательской иконки в пуше (вместе с иконкой приложения в правом нижнем углу пользовательской иконки)
NB: Info.plist для расширения ContentExtension должен содержать тот же список категорий, который содержится в Notify.plist (см. Подключение и базовая настройка библиотеки / 2. Конфигурационный файл
)
Info.plist -> NSExtension -> NSExtensionAttributes -> UNNotificationExtensionCategory | Array (of strings) - тот же список, что и в Notify.plist
Cмысл и значение других параметров см. в документации Apple
2.3.2 Инициализация модуля библиотеки (происходит аналогично инициализации в AppDelegate, но вызывается соответствующий метод для расширения):
//Глобальные переменные-флаги
static bool appIsInitialized = false;
static bool libnotifyIsInitialized = false;
+(void)initialize {
//Если используем конфиг по умолчанию:
apiIsInitialized = [CXApp initExtensionWithDefaultConfigAndEventsReceiver:<nil or EventsHandler>];
//Если используем custom-config (например, различные конфигурационные файлы для debug\release)
CXAppConfig *appConfig = [[CXAppConfig alloc] initWithConfig:[[NSBundle mainBundle] pathForResource:@<config_file_name> ofType:@"plist"]];
apiIsInitialized = [CXApp initExtensionWithConfig:appConfig withEventsReceiver:<nil or EventsHandler>];
}
В случае успешной инициализации библиотеки appIsInitialized
будет true
//Для загрузки большой картинки создаем свойство и связываем его со storyboard
@property (weak, nonatomic) IBOutlet UIImageView *bigContentImage;
...
//Получили нотификацию, вызывается в момент долгого нажатия (long-tap) по баннеру нотификации
- (void)didReceiveNotification:(UNNotification *)notification {
if (apiIsInitialized) {
//Ошибка инициализации библиотеки, необходимо добавить собственную логику, если нужно
return;
}
//Запрашиваем расширенную информацию, содержащуюся в нотификации (на данный момент (v2.0.38) поддерживается только большая картинка).
//При успешной загрузке будет вызван метод 'onContentUpdated... '
//Внутри библиотеки <CXContentExtensionDelegate> делегат будет установлен, как 'self' (т.е. данный ViewController, реализующий ContentExtension)
BOOL processed = [CXNotify requestNotificationExtensionContent:notification
withDelegate:self];
if (!processed) {
//Если библиотека не смогла выполнить запрос, при необходимости добавляем логику обработки подобной ситуации.
//Например, заменяем bigContentImage статической картинкой (содержащейся в ресурсах расширения на момент публикации)
//Метод делегата вызван не будет, поэтому для корректного отображения нотификации по long-tap это предпочтительное поведение.
}
}
//Информируем библиотеку, что пользователь вызвал UNNotificationAction
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response
completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion {
if (apiIsInitialized) {
//Библиотека сама вызовет `completion` c нужной опцией в зависимости от UNNotificationAction
NSExtensionContext *context = self.extensionContext;
[CXApp didReceiveExtensionNotificationResponse:response inContext:context completionHandler:completion];
} else {
//Библиотека не инициализирована или при инициализации возникли ошибки, необходимо реализовать свою логику и вызвать `completion` с нужной опцией
}
}
Протокол содержит единственный метод, на данный момент (v2.0.38) поддерживается только большая картинка:
- (void)onContentUpdated:(CXContentExtensionData *)content
forNotification:(UNNotification *)notification
withError:(NSError *)error {
//Обновляем bigContentImage (см. п. 2.3.2)
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil || content == nil || content.attachmentData == nil) {
return;
}
[self.bigContentImage setImage:[UIImage imageWithData:content.attachmentData]];
});
}
При отображении Landing-
и inApp-
интерфейсов в CXHubSDK используется отдельное окно. Это позволяет отобразить интерфейс, сохраняя текущий стэк контроллеров приложения (view controllers stack) неизменным. Однако иногда данное окно становится key window, поэтому, если ваше приложение использует keyWindow
-метод (для поиска главного окна), то оно может столкнуться с непредсказуемым поведением.
Постарайтесь избегать вызова [[UIApplication sharedApplication] keyWindow]
We open landing interface in libnotify and message settings interface in libverify in separate window. It helps us present required interfaces saving view controllers stack of main application. But sometimes we mark our window as key. Due to this circumstance you can get invalid behaviour if you try to find main window of your application with keyWindow
method.
iOS 12.1+
LGPL