Источник: 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.