- PVSM.RU - https://www.pvsm.ru -
В современном мире наличие публичного Wi-Fi в общественных заведениях считается само собой разумеющимся. Посещая кафе, торговые центры, отели, аэропорты, парки отдыха и многие другие места, мы сразу ищем заветный сигнал без пароля. А это бывает нелегко, поскольку, во-первых, точек в списке может оказаться несколько, а во-вторых, бесплатный Wi-Fi может быть запаролен, так что единственный выход — ловить сотрудника, который сможет указать на правильную сеть и назвать пароль. Но даже после этого случается так, что ничего не работает. Пользователь должен догадаться о том, что ему необходимо открыть браузер (причем еще вопрос, какую страницу следует загружать) и совершить дополнительные действия (авторизоваться, просмотреть рекламу, подтвердить пользовательское соглашение), прежде чем ему предоставят полноценный доступ в сеть.
Впрочем, сейчас многие популярные заведения предлагают приложения, облегчающие подключение к бесплатным точкам. Уверен, что каждый из нас сможет легко припомнить пару-тройку подобных примеров, поэтому обойдусь без названий и рекламы. Тем более что ниже пойдет речь о другом варианте решения этой проблемы — мы будем писать собственный Network Helper! С таким подходом больше не придется гадать, к какой сетке подключаться. Даже дополнительные действия для получения доступа в сеть можно будет производить в удобном нативном UI и гораздо быстрее, чем в браузере.
Все просто. Достаточно задействовать технологию NEHotspotHelper, которая стала доступна разработчикам еще со времен выхода iOS 9. Основная задача этого инструмента — классификация Wi-Fi-сетей и авторизация пользователя в них. NEHotspotHelper входит в состав фреймворка NetworkExtension. Чуть ниже вы найдете схему входящих в него на момент выхода iOS 11 инструментов:
Основная документация находится тут: Hotspot Network Subsystem Programming Guide [1]. Помимо этого никакой информации в сети обнаружить не удалось, в связи с чем я и пишу эту статью. Надеюсь, мой материал поможет восполнить пробелы документации и объяснит, как реализовать свой собственный Hotspot Helper. Пример использования этой технологии можно найти на GitHub [2].
В основе работы Hotspot Helper — машина состояний Wi-Fi-соединения и посылаемые системой Helper команды, обработка которых переводит машину из одного состояния в другое. Ниже представлена приблизительная схема состояний, которую приводит сама Apple в своей документации:
Поначалу столь сложная картинка пугает, однако не стоит беспокоиться — на практике все довольно просто.
Достаточно понимать следующее:
Единственный неочевидный момент: после первого подключения к сети система кеширует выбранный для нее Hotspot Helper, поэтому при последующем подключении машина cразу переключается в состояние Maintain, минуя все предыдущие.
Hotspot Helper участвует в авторизации пользователя в Wi-Fi-сети на всех этапах — от отображения списка сетей и подключения к выбранной до поддержки авторизации и самостоятельного логаута. При этом для установки соединения Hotspot Helper обрабатывает все требуемые команды, благодаря чему система обеспечивает его запуск в любой ситуации (даже если пользователь принудительно выключит приложение (что привело бы к игнорированию silent-push уведомлений). Ведь от этого зависит работа всего устройства. Надо понимать, что для пользователя весь процесс протекает прозрачно. Таким образом, наиболее частый сценарий — это запуск приложения в фоне.
Итак, повторим: для установки Wi-Fi-соединения Hotspot Helper должен обработать все требуемые команды. Иными словами, устройство не будет считать себя подключенным к сети до тех пор, пока StateMachine не перейдет в состояние Authenticated. И это несмотря на то, что Hotspot Helper начнет видеть соединение уже при получении команды Evaluate. Этот момент отлично отслеживается средствами Reachability, о чем мы поговорим чуть ниже.
Надо сказать, что NEHotspotHelper — не отдельный target, как это часто бывает с другими extension, а основное приложение, зарегистрированное как Hotspot Helper. Это значит, что, как только ему потребуется обработать какую-либо команду, будет запущено основное приложение со всеми вытекающими последствиями. То есть у приложения будет возможность выполнять любой код, однако при запуске в фоне оно может развернуть полномасштабные действия, как будто инициированные самим пользователем. Однако такая деятельность будет означать лишь расход ресурсов впустую, поэтому за происходящим в фоне стоит следить.
Для регистрации приложения как Hotspot Helper нужно получить разрешение у Apple. Для этого Team Agent должен перейти по ссылке [3] и заполнить опросник.
На момент написания статьи он выглядит так:
Если все пройдет хорошо, то при создании provisioning profile на https://developer.apple.com [4] появится возможность выбрать для него entitlement с ключом com.apple.developer.networking.HotspotHelper, дающий право на использование всех плюшек.
Кроме того, необходимо включить в проекте Background Mode Capability и прописать в Info.plist в раздел UIBackgroundModes строку network-authentication. После этого можно приступать к самому интересному — кодингу.
Для того чтобы приложение стало Hotspot Helper, его необходимо зарегистрировать в системе. Для этого надо вызвать следующий метод:
class NEHotspotHelper
class func register(options: [String : NSObject]? = nil, queue: DispatchQueue,
handler: @escaping NetworkExtension.NEHotspotHelperHandler) -> Bool
Метод принимает три параметра:
typealias NEHotspotHelperHandler = (NEHotspotHelperCommand) -> Void
Он принимает команду системы в качестве единственного параметра и не возвращает ничего. Блок будет вызываться на очереди, переданной в качестве второго параметра.
Регистрировать Helper надо ровно один раз на каждом запуске. Повторный вызов в том же запуске не дает ничего и возвращает false. Важно отметить, что до завершения повторной регистрации приложение не получит команды, для обработки которой оно было запущено системой.
Отменить регистрацию никак нельзя. Можно только перестать регистрировать блок, тогда приложение никак не будет обрабатывать соединение, но запускатьcя все равно будет — подробнее тут [6].
Кроме того, в отличие от многих других функций системы (таких как фотогалерея, календарь и даже уведомления), обработка Wi-Fi-соединения средствами Hotspot Helper не требует никаких разрешений от пользователя и происходит прозрачно для него (он попросту не сталкивается с подобными понятиями).
Команда представляет собой объект класса NEHotspotHelperCommand, содержащий тип и набор данных, характерный для каждой из команд (сеть либо список сетей, если это подразумевает команда).
После обработки каждой из команд следует создать NEHotspotHelperResponse с результатом выполнения и набором данных, который также зависит от конкретной команды.
Объект NEHotspotHelperResponse создается вызовом на полученной команде данного метода:
func createResponse(_ result: NEHotspotHelperResult) -> NEHotspotHelperResponse
Кроме того, использование объекта команды позволяет установить TCP- либо UDP-соединение на основании сети, к которой команда относится, вызывая соответствующие методы:
func createTCPConnection(_ endpoint: NWEndpoint) -> NWTCPConnection
func createUDPSession(_ endpoint: NWEndpoint) -> NWUDPSession
Для более высокоуровневого общения с сервером можно создать NSURLRequest. Приложив к нему команду, Hotspot Helper получает возможность взаимодействия с сервером в условиях, когда устройство не видит Wi-Fi-соединения. Установленное таким образом соединение можно использовать для авторизации «по-своему». IYKWIM
func bind(to command: NEHotspotHelperCommand)
Ниже рассмотрим каждую команду, которую может получить Hotspot Helper, в порядке, соответствующем базовому сценарию подключения к сети. Большинство команд названы аналогично состояниям State Machine, при которых они вызываются.
Официально на выполнение каждой команды отводится не более 45 секунд (однако, если посмотреть на доступное время работы в фоне, можно увидеть цифру 60 секунд). После этого команда считается необработанной, а работа Hotspot Helper приостанавливается. Это ограничение необходимо, чтобы устранить излишние задержки при подключении к сети, ведь пока команда не будет обработана, пользователь не увидит заветного значка Wi-Fi в Status Bar. При этом надо понимать, что, если в системе несколько Hotspot Helper, которые обрабатывают одну и ту же сеть, будет выбран самый быстрый из них (об этом опять же ниже).
Это особенная команда, которая, в отличие от остальных, не привязана ни к одному из состояний StateMachine и может быть вызвана в любой момент. Команда вызывается на всех Hotspot Helper, известных системе, каждые 5 секунд. Происходит это все время, пока пользователь находится на экране списка Wi-Fi-сетей в системном Settings.app.
Команда служит единственной цели — продемонстрировать пользователю, какие из сетей обрабатывает Hotspot Helper. Для этого команда содержит список доступных сетей в соответствующем поле:
var networkList: [NEHotspotNetwork]? { get }
Это тот же список, который увидит пользователь. При этом список сетей может меняться с каждым новым вызовом команды.
Предполагается, что Hotspot Helper должен проанализировать список сетей и в ответе на команду вернуть те из них, которые он готов обслуживать. Пустой массив в ответе будет означать, что сетей доступных для обслуживания этим Helper в списке нет. Скрыть для пользователя сети из списка не получится.
У тех сетей в списке, которые вернул Hotspot Helper в ответ на эту команду, появится подпись. Она будет соответствовать тому значению, которое было передано при последней регистрации Hotspot Helper в качестве опции kNEHotspotHelperOptionDisplayName. Значение подписи может быть только одно. Оно задается при регистрации и не может быть изменено до следующей регистрации (а это происходит после перезапуска приложения), так что подписывать сети по-разному, увы, не получится.
Важно отметить, что кроме передачи самой сети в ответ ей необходимо задать пароль, если она им защищена. Без этого подпись не появится. Если же пароль задан, то кроме появления подписи пользователю не потребуется вводить пароль самому. Надо сказать, это тот самый единственный момент, когда можно установить пароль для сети. Если не задать его сейчас, то это придется делать самому пользователю.
В итоге обрабатывать команду следует так:
let network = <A network from command.networkList>
network.setPassword("PASSWORD")
let response = command.createResponse(.success)
response.setNetworkList([network])
response.deliver()
В результате пользователь увидит нечто подобное:
Надо понимать, что команда filterScanList целиком и полностью служит для демонстрации пользователю списка сетей и никак не влияет на остальную обработку подключения к сети. Если в ответ на команду Hotspot Helper не вернул ни одну сеть, ему все равно предложат обработать подключение к любой из них посредством команд, описанных ниже.
Интересный факт: если удалить приложение, подписи в списке сетей сохранятся вплоть до перезапуска устройства.
Данная команда вызывается при первом подключении к сети на всех Hotspot Helper, известных системе.
Note: при последующих подключениях evaluate не вызовется, сразу последует maintain на том Hotspot Helper, который был выбран в процессе evaluate.
Цель этой команды — первичное выявление наиболее подходящего для обработки подключения к выбранной сети Hotspot Helper. Для этого вместе с командой Hotspot Helper получает и данные по сети, к которой производится подключение:
var network: NEHotspotNetwork? { get }
Network содержит ряд свойств, однако в процессе обработки команды evaluate значения имеют только следующие:
// Идентифицируют сеть
var ssid: String { get }
var bssid: String { get }
// Отражают силу сигнала по шкале от 0.0 до 1.0 (дБ, увы, не предоставляются)
var signalStrength: Double { get }
// Признак необходимости ввода пароля для подключения к сети
var isSecure: Bool { get }
// Признак, показывающий, было ли подключение выполнено автоматически
// или пользователь явно выбрал эту сеть в настройках
var didAutoJoin: Bool { get }
Получив эту информацию, Hotspot Helper должен проанализировать сеть любым способом (начиная от локальной таблицы и заканчивая запросом на сервер) и выставить сети соответствующий уровень confidence:
// Helper уверен, что не обрабатывает эту сеть.
case none
// Helper предполагает, что сможет обработать подключение к этой сети, но не уверен полностью*.
case low
// Helper уверен, что может полноценно обработать подключение к этой сети.
case high
*В этом случае ему могут предоставить возможность авторизовать пользователя, однако если в процессе Helper поймет, что он несовместим с данной сетью, то он сможет отказаться от работы с ней, и StateMachine вновь перейдет в состояние evaluate, а Helper будет добавлен в список исключений по данной сети.
Важно отметить: Apple настоятельно дает понять [5], что уровень confidence надо выбирать тщательно и не следует бездумно выставлять всем сетям high (и даже low), так как это напрямую сказывается на UX всей системы.
В итоге обрабатывать команду следует так:
let network = command.network
// Оценить сеть и определить уровень confidence...
network.setConfidence(<Appropriate level of confidence>)
let response = command.createResponse(.success)
response.setNetwork(network)
response.deliver()
На обработку команды предоставляется 45 секунд, при этом есть смысл постараться сделать это как можно быстрее. Потому что, как только система получает первый ответ с high confidence, обработка команды прекращается. Затем ответивший Hotspot Helper выбирается для дальнейший обработки подключения в сети, а все остальные прекращают свою работу и переходят в suspended state.
Команда authenticate вызывается на наиболее подходящем Hotspot Helper по результатам выполнения команды evaluate.
Целью этой команды служит выполнение всех действий, необходимых для предоставления полного доступа пользователя к выбранной Wi-Fi-сети. Для этого вместе с командой Hotspot Helper получает и данные по сети, к которой производится подключение:
var network: NEHotspotNetwork? { get }
В обработке этой команды и заключается основная суть работы Hotspot Helper. На этом этапе барьером между пользователем и доступом к сети является только Hotspot Helper, и он должен любым доступным фантазии разработчика способом решить, предоставлять пользователю доступ или нет.
На обработку команды дается 45 секунд, по истечении которых необходимо вернуть response с одним из следующих результатов:
.success — авторизация успешно завершена. Доступ в сеть полностью открыт. StateMachine переходит в состояние authenticated.
.unsupportedNetwork — данная сеть не поддерживается Helper: произведен неправильный анализ сети на этапе evaluate. StateMachine возвращается на этап evaluate, а Helper добавляется в исключения для данной сети. Такое может произойти, например, если Helper вернул для этой сети low confidence на этапе evaluate, а сейчас убедился, что не справится.
.uiRequired — требуется взаимодействие с пользователем. Этот результат возвращается в случае, если для авторизации требуется ввести какие-либо дополнительные данные. Однако не все так просто: UI не покажется сам собой при возвращении этого результата.
Работает это так: Hotspot Helper генерируется UILocalNotification, посредством которой сообщает пользователю о необходимости дополнительного взаимодействия. Если пользователь ее проигнорирует, ничего больше не произойдет. Получив результат обработки, StateMachine переходит в состояние presentingUI и остается в нем до завершения авторизации либо отключения от сети.
.temporaryFailure — исправимая ошибка. Например, в ходе авторизации произошла сетевая ошибка. StateMachine переходит в состояние failure, устройство отключается от выбранной сети.
.failure (или любой другой результат, а равно и его отсутствие) — неисправимая ошибка. Что-то пошло совсем не так, и обработать подключение нельзя: например, поменялся протокол авторизации на стороне сервера, а клиент к этому не готов. StateMachine переходит в состояние failure, устройство отключается от выбранной сети. Дополнительно, в отличие от temporaryFailure, для такой сети отключается функция auto-join.
В итоге обрабатывать команду следует так:
let network = command.network
// Авторизовать пользователя необходимым образом и сформировать результат обработки команды
// Вывести UILocalNotification в случае необходимости дополнительного взаимодействия (.uiRequired)
command.createResponse(<Command result>).deliver()
Эта команда вызывается на выбранном Hotspot Helper в случае, если он вернул результат uiRequired в процессе обработки команды authenticate.
Это единственная команда, которая не пробуждает приложение в фоне и имеет неограниченное время на выполнение. Прилетает она только после того, как пользователь запустит приложение, например, получив UILocalNotification о необходимости дополнительного взаимодействия в процессе обработки команды authenticate.
Как раз на этом этапе следует просить пользователя ввести его доменные креды, если сеть корпоративная, либо показывать, например, рекламу, если сеть коммерческая. Словом, варианты ограничены только фантазией и здравым смыслом разработчика.
В итоге необходимо вернуть response с одним из следующих результатов:
.success — авторизация успешно завершена. Доступ в сеть полностью открыт. StateMachine переходит в состояние authenticated.
.unsupportedNetwork — данная сеть не поддерживается Helper: произведен неправильный анализ сети на этапе evaluate. StateMachine возвращается на этап evaluate, а Helper добавляется в исключения для данной сети. Такое может произойти, например, если Helper вернул для этой сети low confidence на этапе evaluate, а сейчас убедился, что не справится.
.temporaryFailure — исправимая ошибка. Скажем, в ходе авторизации произошла сетевая ошибка. StateMachine переходит в состояние failure, устройство отключается от выбранной сети.
.failure (или любой другой результат, а равно и его отсутствие) — неисправимая ошибка. Что-то пошло не так, и обработать подключение нельзя: например, поменялся протокол авторизации на стороне сервера, а клиент к этому не готов. StateMachine переходит в состояние failure, устройство отключается от выбранной сети. Дополнительно, в отличие от temporaryFailure, для такой сети отключается функция auto-join.
В итоге обрабатывать команду следует так:
let network = command.network
// Произвести любые необходимые действия для авторизации пользователя любыми средствами
// за неограниченные период времени и сформировать результат выполнения команды
command.createResponse(<Command result>).deliver()
Команда maintain, как можно догадаться по её названию, предназначена для поддержания сессии авторизации пользователя в текущей сети. Она вызывается на выбранном для сети Hotspot Helper в процессе evaluate в двух случаях:
Предполагается, что в обоих случаях Hotspot Helper проанализирует текущее состояние сессии авторизации и выполнит одно из следующих действий:
Для отличия первого случая вызова команды maintain от второго в массиве данных по сети, передаваемых вместе с командой, предусмотрен специальный флаг — didJustJoin.
В итоге обрабатывать команду следует так:
let network = command.network
if network.didJustJoin {
// Новое подключение к сети, для обработки которой выбран данный Helper
}
else {
// Поддержка сессии в сети, в которой была произведена авторизация (раз в 300 сек.)
}
// Обеспечить авторизацию пользователя в сети любым способом
// и сформировать результат обработки команды
command.createResponse(<Command result>).deliver()
Следует отметить, что во время повторной авторизации соединение будет недоступно для устройства вплоть до возвращения в состояние Authenticated.
Команда logoff, как можно было бы ожидать, не посылается при отключении от сети. Отследить отключение от сети средствами Hotspot Helper невозможно.
Эта команда предназначена для завершения внутренней сессии авторизации выбранного Hotspot Helper и посылается ему в ответ на вызов статического метода NEHotspotHelper:
class func logoff(_ network: NEHotspotNetwork) -> Bool
Данный метод может быть успешно вызван только с текущей сетью в качестве параметра, только тем Hotspot Helper, который производил авторизацию в ней, и только когда приложение активно. В противном случае метод вернет false и команда не будет вызвана.
В результате StateMachine переходит в состояние LoggingOff, а Hotspot Helper получает заветную команду и 45 секунд на её выполнение.
Получить сеть, которую необходимо передать в метод, можно следующим образом:
let network = NEHotspotHelper.supportedNetworkInterfaces().first
Основной use-case: выполнение логаута из UI приложения, что актуально для сценария авторизации с использованием команды presentUI.
Как только Hotspot Helper завершит выполнение команды (либо время выполнения истечет), StateMachine переходит в состояние inactive, и устройство отсоединяется от Wi-Fi-сети.
В итоге обрабатывать команду следует так:
let network = command.network
// Произвести logoff и сбросить внутренние данные сессии авторизации
let response = command.createResponse(.success).deliver()
Следует отметить, что до получения этой команды приложение не должно пытаться выполнить какие-либо действия по отключению от сети, так как это негативно повлияет на UX всей системы (фактически пользователь будет видеть наличие соединения, но данные при этом ходить не будут).
Выше описаны принцип работы и детали реализации Hotspot Helper. Однако есть еще несколько особенностей, о которых следует знать, приступая к разработке, а именно:
let network = NEHotspotHelper.supportedNetworkInterfaces().first
Следует отметить, что, несмотря на swift-сигнатуру данного метода (в которой указан non-optional-массив в качестве результата) и ожидаемое поведение (отсутствие сети представляется пустым массивом), при отсутствии соединения можно получить объект сети с пустыми строками в качестве SSID и BSSID и силой сигнала 0.0. Иногда, что еще страшнее, на выходе можно получить и nil (а вместе с ним и crash). В примере [2] приведен код, позволяющий избежать этих ситуаций.
Note: C выходом iOS 11 эта проблема устранена [7].
Следует отметить, что существует единственная ситуация, в которой reachability уже видит Wi-Fi, а Hotspot Helper не получает никаких команд. Происходит это, когда пользователь может видеть следующее состояние в настройках:
Чем обусловлена такая ситуация, понять сложно. Если у вас есть идеи, поделитесь ими, пожалуйста.
Конфликт решается на этапе обработки команды evaluate: выбирается тот Hotspot Helper, который вернул high-confidence для сети быстрее остальных. Вся дальнейшая обработка для этой сети происходит с использованием только этого Helper. Выбранный Helper может потом отказаться от обработки данной сети, вернув соответствующий код результата в процессе обработки очередной команды. Но если он этого не сделает, для остальных Hotspot Helper нет никакой возможности поучаствовать в обработке соединения.
Ситуация усугубляется еще и тем, что пользователь ничего не знает и не может знать о существовании каких-либо Helper. Все это происходит незаметно для него: нигде не указывается данный функционал, никакие разрешения у него не запрашиваются. Именно по этой причине одно из требований Apple [5] заключается в необходимости предоставить пользователю в UI приложения возможность отключить обработку всех сетей либо конкретной сети (по SSID).
Следует отметить, что какого-либо надежного способа определить наличие другого Hotspot Helper в системе нет. Единственное, что можно сделать, — проверить, выбран ли в текущий момент Hotspot Helper основным для активной сети. Это можно сделать так:
let network = NEHotspotHelper.supportedNetworkInterfaces().first
if !network.isChosenHelper {
// Hotspot Helper не обрабатывает активную сеть
}
Заметьте, false в этом флаге может означать, что для сети выбран другой Helper или даже что пока не выбран никакой (например, в процессе evaluate). Кроме того, сеть может уже появиться, но обработка подключения еще не начаться. Такая ситуация описана выше.
func createTCPConnection(_ endpoint: NWEndpoint) -> NWTCPConnection
func createUDPSession(_ endpoint: NWEndpoint) -> NWUDPSession
А также расширение на NSMutableURLRequest:
func bind(to command: NEHotspotHelperCommand)
Если у вас есть идеи как можно такие ситуации отличить или хотябы спрогнозировать, поделитесь ими, пожалуйста.
Технология NEHotspotHelper, появившаяся несколько лет назад, не утратила своей актуальности и по сей день. Этот инструмент позволяет значительно улучшить и облегчить процесс пользования сетевыми сервисами. Здесь я рассмотрел основные принципы работы, способы применения и все шаги, которые следует предпринять для его эффективного использования. Кроме того, рассказал и о некоторых особенностях Helper, о которых тактично умалчивает документация.
Надеюсь, теперь вы имеете полное представление о том, для чего и как стоит использовать эту штуку в вашем проекте. Впрочем, если у вас возникли какие-либо вопросы, напишите, я готов ответить на них.
Автор: AndreyGusev
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/261828
Ссылки в тексте:
[1] Hotspot Network Subsystem Programming Guide: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/Hotspot_Network_Subsystem_Guide/Contents/Introduction.html#//apple_ref/doc/uid/TP40016639
[2] GitHub: https://github.com/GusevAndrey/HotspotHelperExample
[3] ссылке: https://developer.apple.com/contact/network-extension
[4] https://developer.apple.com: https://developer.apple.com
[5] Согласно требованиям Apple: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/Hotspot_Network_Subsystem_Guide/Contents/ImportantGuidelines.html#//apple_ref/doc/uid/TP40016639-CH5-SW1
[6] подробнее тут: https://forums.developer.apple.com/message/227038#227038
[7] устранена: https://developer.apple.com/documentation/networkextension/nehotspothelper/1618921-supportednetworkinterfaces?changes=latest_minor
[8] NEHotspotConfigurationManager: https://developer.apple.com/documentation/networkextension/nehotspotconfigurationmanager
[9] Network Extension: https://developer.apple.com/reference/networkextension
[10] NEHotspotHelper reference: https://developer.apple.com/reference/networkextension/nehotspothelper
[11] WWDC’15 What's New in Network Extension and VPN: https://developer.apple.com/videos/play/wwdc2015/717/
[12] её конспект: http://asciiwwdc.com/2015/sessions/717
[13] WWDC’17 Advances in Networking, Part 1: https://developer.apple.com/videos/play/wwdc2017/707/
[14] Forum: Как получить entitlements через почту: https://forums.developer.apple.com/thread/9015
[15] Forum: Как показать U: https://forums.developer.apple.com/thread/48011
[16] Небольшая статья про HotspotHelper: https://www.google.ru/amp/s/mobiarch.wordpress.com/2016/11/02/working-with-nehotspothelper/amp/
[17] Источник: https://habrahabr.ru/post/335028/
Нажмите здесь для печати.