Источник: https://marcosantadev.com/network-reachability-swift/
Вступление
Один день я использовал класс Alamofire NetworkReachabilityManager
для проверки сетевого состояния устройства пользователя - и внезапно я обнаружил ошибку из-за крайнего сценария. На этом этапе я решил хорошо изучить, как работает этот класс и как внести свой вклад в решение проблемы.
Результат - запрос на слияние: Alamofire # 2060.
Я думаю, что доступность сети - это интересная тема, поэтому я хочу поделиться с вами, как это работает.
Приятного чтения!
Что такое доступность сети?
Network Reachability
- это состояние сети устройства пользователя. Мы можем использовать его, чтобы понять, находится ли устройство в автономном режиме или в сети, используя Wi-Fi или мобильные данные.
Чтобы прочитать эту информацию, мы можем использовать интерфейс SCNetworkReachability
, предоставляемый фреймворком SystemConfiguration
. Мы можем использовать его для чтения сетевой информации как синхронно, так и асинхронно.
Давайте начнем шаг за шагом, используя этот интерфейс.
Создание
После импортирования фреймворка SystemConfiguration
просто:
import SystemConfiguration
Первый шаг - создание объекта SCNetworkReachability
. В основном это можно сделать двумя способами:
1. Использование имени хоста:
Мы можем использовать функцию SCNetworkReachabilityCreateWithName
, которой требуется имя хоста в качестве аргумента:
let reachability = SCNetworkReachabilityCreateWithName(nil, "www.google.com")
// Or also with localhost
let reachability = SCNetworkReachabilityCreateWithName(nil, "localhost")
Первый параметр - это распределитель, мы можем передать nil
, чтобы использовать значение по умолчанию. SCNetworkReachabilityCreateWithName
возвращает необязательное значение, поэтому мы должны его развернуть.
2. Использование ссылки на сетевой адрес:
// Initializes the socket IPv4 address struct var address = sockaddr_in() address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size) address.sin_family = sa_family_t(AF_INET)
// Passes the reference of the struct let reachability = withUnsafePointer(to: &address, { pointer in // Converts to a generic socket address return pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size) { // $0 is the pointer to `sockaddr` return SCNetworkReachabilityCreateWithAddress(nil, $0) } })
Сетевые флаги
Когда у нас есть объект SCNetworkReachability
, мы готовы использовать информацию о состоянии сети.
SCNetworkReachability
предоставляет эту информацию с помощью набора флагов - SCNetworkReachabilityFlags
.
Я копирую сюда из официальной документации список флагов:
- transientConnection
- Указанное имя или адрес узла могут быть достигнуты через временное соединение, такое как PPP.
- доступен
- Указанное имя или адрес узла можно получить, используя текущую конфигурацию сети.
- Требуется подключение
- Указанное имя или адрес узла могут быть достигнуты с использованием текущей конфигурации сети, но сначала необходимо установить соединение. Если этот флаг установлен, флаг kSCNetworkReachabilityFlagsConnectionOnTraffic, флаг kSCNetworkReachabilityFlagsConnectionOnDemand или флаг kSCNetworkReachabilityFlagsIsWWAN также обычно устанавливаются для указания типа необходимого соединения. Если пользователю необходимо вручную установить соединение, также устанавливается флаг kSCNetworkReachabilityFlagsInterventionRequired.
- connectionOnTraffic
- Указанное имя узла или адрес могут быть достигнуты с использованием текущей конфигурации сети, но сначала необходимо установить соединение. Любой трафик, направленный на указанное имя или адрес, инициирует соединение.
- Требуется вмешательство
- Указанное имя или адрес узла могут быть достигнуты с использованием текущей конфигурации сети, но сначала необходимо установить соединение.
- connectionOnDemand
- Указанное имя или адрес узла могут быть достигнуты с использованием текущей конфигурации сети, но сначала необходимо установить соединение. Соединение будет установлено «по требованию» программным интерфейсом CFSocketStream (информацию об этом см. В разделе «Дополнения к сокетам CFStream»). Другие функции не устанавливают соединение.
- isLocalAddress
- Указанное имя или адрес узла - это тот, который связан с сетевым интерфейсом в текущей системе.
- isDirect
- Сетевой трафик на указанное имя или адрес узла не проходит через шлюз, а направляется непосредственно на один из интерфейсов в системе.
- isWWAN
- Указанное имя или адрес узла можно получить через сотовое соединение, такое как EDGE или GPRS.
- connectionAutomatic
- Указанное имя или адрес узла могут быть достигнуты с использованием текущей конфигурации сети, но сначала необходимо установить соединение. Любой трафик, направленный на указанное имя или адрес, инициирует соединение. Этот флаг является синонимом connectionOnTraffic.
Мы можем легко прочитать эти флаги, используя функцию SCNetworkReachabilityGetFlags
, которая хочет иметь два параметра: - Объект SCNetworkReachability
. - Пустой объект SCNetworkReachabilityFlags
, переданный по ссылке - таким образом внутренняя реализация SCNetworkReachabilityGetFlags
добавляет к этому объекту флаги.
var flags = SCNetworkReachabilityFlags()
SCNetworkReachabilityGetFlags(reachability!, &flags)
// Now `flags` has the right data set by `SCNetworkReachabilityGetFlags`
Затем, поскольку flags
- это Set, мы можем проверить, доступен ли флаг с помощью метода contains
:
let isReachable: Bool = flags.contains(.reachable)
Использование синхронно
На данный момент у нас есть экземпляр SCNetworkReachability
, и мы знаем, как читать флаги. Мы готовы проверить, есть ли у пользователя подключение к Интернету.
Чтобы получить эту информацию, мы можем прочитать содержимое SCNetworkReachabilityFlags
следующим методом:
func isNetworkReachable(with flags: SCNetworkReachabilityFlags) -> Bool { let isReachable = flags.contains(.reachable) let needsConnection = flags.contains(.connectionRequired) let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic) let canConnectWithoutUserInteraction = canConnectAutomatically && !flags.contains(.interventionRequired)
return isReachable && (!needsConnection || canConnectWithoutUserInteraction) }
Если у вас есть сомнения по поводу значения некоторых флагов, вы можете проверить список флагов в Сетевые флаги.
И мы можем использовать указанную выше функцию следующим образом:
// Optional binding since `SCNetworkReachabilityCreateWithName` return an optional object guard let reachability = SCNetworkReachabilityCreateWithName(nil, "www.google.com") else { return }
var flags = SCNetworkReachabilityFlags() SCNetworkReachabilityGetFlags(reachability, &flags)
if !isNetworkReachable(with: flags) { // Device doesn't have internet connection return }
#if os(iOS) // It's available just for iOS because it's checking if the device is using mobile data if flags.contains(.isWWAN) { // Device is using mobile data } #endif
// At this point we are sure that the device is using Wifi since it's online and without using mobile data
Приведенный выше пример полностью синхронен. Это означает, что нам не нужно ждать обработчика завершения, чтобы получить информацию, которую мы ищем.
Асинхронное использование
Есть приложения, где не хватает синхронной информации. Нам может потребоваться асинхронный обратный вызов, который сообщает об изменении состояния сети.
К счастью, SCNetworkReachability
предоставляет возможность установить прослушиватель сетевых изменений. Мы можем добиться этого на следующем примере:
class Handler {
private let reachability = SCNetworkReachabilityCreateWithName(nil, "www.google.com")
// Queue where the `SCNetworkReachability` callbacks run private let queue = DispatchQueue.main
// We use it to keep a backup of the last flags read. private var currentReachabilityFlags: SCNetworkReachabilityFlags?
// Flag used to avoid starting listening if we are already listening private var isListening = false
// Starts listening func start() { // Checks if we are already listening guard !isListening else { return }
// Optional binding since `SCNetworkReachabilityCreateWithName` returns an optional object guard let reachability = reachability else { return }
// Creates a context var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) // Sets `self` as listener object context.info = UnsafeMutableRawPointer(Unmanaged<Handler>.passUnretained(self).toOpaque())
let callbackClosure: SCNetworkReachabilityCallBack? = { (reachability:SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutableRawPointer?) in guard let info = info else { return }
// Gets the `Handler` object from the context info let handler = Unmanaged<Handler>.fromOpaque(info).takeUnretainedValue()
DispatchQueue.main.async { handler.checkReachability(flags: flags) } }
// Registers the callback. `callbackClosure` is the closure where we manage the callback implementation if !SCNetworkReachabilitySetCallback(reachability, callbackClosure, &context) { // Not able to set the callback }
// Sets the dispatch queue which is `DispatchQueue.main` for this example. It can be also a background queue if !SCNetworkReachabilitySetDispatchQueue(reachability, queue) { // Not able to set the queue }
// Runs the first time to set the current flags queue.async { // Resets the flags stored, in this way `checkReachability` will set the new ones self.currentReachabilityFlags = nil
// Reads the new flags var flags = SCNetworkReachabilityFlags() SCNetworkReachabilityGetFlags(reachability, &flags)
self.checkReachability(flags: flags) }
isListening = true }
// Called inside `callbackClosure` private func checkReachability(flags: SCNetworkReachabilityFlags) { if currentReachabilityFlags != flags { // 🚨 Network state is changed 🚨
// Stores the new flags currentReachabilityFlags = flags } }
// Stops listening func stop() { // Skips if we are not listening // Optional binding since `SCNetworkReachabilityCreateWithName` returns an optional object guard isListening, let reachability = reachability else { return }
// Remove callback and dispatch queue SCNetworkReachabilitySetCallback(reachability, nil, nil) SCNetworkReachabilitySetDispatchQueue(reachability, nil)
isListening = false } }
Благодаря классу Handler
мы можем запускать / останавливать прослушивание с помощью методов _58 _ / _ 59_.
Примечание.
Чтобы сделать этот пример как можно более понятным, checkReachability(flags:)
проверяет не доступность сети, а только изменение флагов. В реальном сценарии мы можем также захотеть проверить, есть ли у устройства подключение к Интернету, используя метод isNetworkReachable
, использованный в 'Синхронном использовании'.
Избегайте ручного внедрения
Если мы не хотим утруждать себя ручной реализацией, мы можем использовать библиотеку с открытым исходным кодом, которая предоставляет высокоуровневый интерфейс для обертывания SCNetworkReachability
.
Самые известные из них:
iOS 11+
Примером, в котором мы должны использовать доступность сети, может быть приложение, которое считывает некоторую информацию из общедоступного API. Если на устройстве пользователя нет подключения к Интернету, пытаться получить данные не имеет смысла. Вместо этого мы должны прослушать изменения состояния сети и начать сбор данных, как только мы получим уведомление о том, что на устройстве доступно подключение к Интернету.
В iOS 11 Apple представила свойство waitsForConnectivity
в URLSessionConfiguration
для ожидания подключения к Интернету перед выполнением каких-либо сетевых действий с URLSession
. Это означает, что с этим свойством, установленным на true
, нам не нужно беспокоиться о том, чтобы вручную проверять подключение устройства и ждать допустимого подключения к Интернету. URLSession
сделает это под капотом.
К сожалению, если нам необходимо поддерживать также версии более ранние, чем iOS 11, или если мы выполняем сетевые операции без использования URLSession
, мы должны продолжать делать это вручную.
Заключение
Большинство разрабатываемых нами приложений должны использовать подключение к Интернету для передачи данных. Следовательно, мы должны правильно обрабатывать состояние доступности устройства пользователя - чтобы понять, можем ли мы выполнить или отложить некоторые действия, связанные с отсутствующим соединением. Это может улучшить время автономной работы и удобство использования.
использованная литература
Для написания этой статьи я использовал реализацию как my Alamofire Pull Request, так и библиотеки с открытым исходным кодом Reachability.swift.