Следите за вашим интернет соединением используя Combine.

Перевод статьи от автора

Примеры в SwiftUI и UIKit

В этой статье мы будем изучать как наблюдать и реагировать на статусы сети с использованием библиотеки Network и Combine

Вкратце, это то что мы будем осваивать в этом уроке:

  • Как создать переиспользуемого издателя Combine для NWPathMonitor, компоненты которые будут отслеживать статус сети.
  • Как связать обновление статуса сети с компоментами UI в UIKit и SwiftUI

Исходный код проекта доступен внизу статьи.

. . .

Давайте начем

Первое, сделаем импорт библиотек Network и Combine и определим расширение  NWPathMonitor:

import Network
import Combine
 	 
extension NWPathMonitor {
}

Теперь нам нужна реализация Subscription:

import Network
import Combine

// MARK: - NWPathMonitor Subscription
extension NWPathMonitor {
    class NetworkStatusSubscription<S: Subscriber>: Subscription where S.Input == NWPath.Status {
        
        // 1
        private let subscriber: S?
        
        // 2
        private let monitor: NWPathMonitor
        private let queue: DispatchQueue
        
        init(subscriber: S,
             monitor: NWPathMonitor,
             queue: DispatchQueue) {
            
            self.subscriber = subscriber
            self.monitor = monitor
            self.queue = queue
        }
        
        // 3
        func request(_ demand: Subscribers.Demand) {
            
        }
        
        func cancel() {
            
        }
        
    }
    
}
  1. Мы зависим от Subrscriber который принимает NWPah.Status на вход.
  2. Мы так же создали monitor и queue зависимости итак мы позже можем запустить мониторинг сети
  3. Что бы соответствовать протоколу Subscription, мы реализуем методы request(_ demand) и cancel(). Внутри метода request мы запускаем процесс мониторина. Внутри cancel когда будет необходимо мы будем отменять процесс.

Давайте обновим методы request и cancel

import Network
import Combine

// MARK: - NWPathMonitor Subscription
extension NWPathMonitor {
    class NetworkStatusSubscription<S: Subscriber>: Subscription where S.Input == NWPath.Status {
        
        ...
        
        func request(_ demand: Subscribers.Demand) {
            // 1
            monitor.pathUpdateHandler = { [weak self] path in
                guard let self = self else { return }
                _ = self.subscriber?.receive(path.status)
            }
            
            // 2
            monitor.start(queue: queue)
        }
        
        func cancel() {
            // 3
            monitor.cancel()
        }
        
    }
    
}
  1. Мы используем свойство pathUpdateHandler для получения NWPath содержащий текущий статус сети. Когда статус поменяется, Subsciber получит значение NWPath.Status.
  2. Мы запускаем процесс мониторинга и должны получить измененный статус внутри замыкания pathUpdateHandler.
  3. Внутри метода cancel(), когда подписка отменена, мы соответственно запрещаем мониторинг.

С Subscription все, теперь расширим NWPathMonitor еще раз и создадим кастомный Publisher.

import Network
import Combine

// MARK: - NWPathMonitor Subscription
extension NWPathMonitor {
    ...
}

// MARK: - NWPathMonitor Publisher
extension NWPathMonitor {
    // 1
    struct NetworkStatusPublisher: Publisher {
        
        // 2
        typealias Output = NWPath.Status
        typealias Failure = Never
        
        // 3
        private let monitor: NWPathMonitor
        private let queue: DispatchQueue
        
        init(monitor: NWPathMonitor,
             queue: DispatchQueue) {
            
            self.monitor = monitor
            self.queue = queue
        }
        
        // 4
        func receive<S>(subscriber: S) where S : Subscriber,
                                             Never == S.Failure, NWPath.Status == S.Input {
            
        }
    }
    
    // 5
    func publisher(queue: DispatchQueue) -> NWPathMonitor.NetworkStatusPublisher {
        
        return NetworkStatusPublisher(monitor: self,
                                      queue: queue)
    }
}

Здесь то что мы достигаем этим кодом:

  1. Мы определили кастомный NetworkStatusPublisher который соответствуем протоколу Publisher.
  2. издатель выдает значения NWPath.Status и никогда не подводит.
  3. Похоже на подписку что мы определили ранее, мы также зависим от NWPathMonitor и DispatсhQueue.
  4. Внутри требуемого метода receive<S>(subscriber:) мы скоро создадим NetworkStatusSubscription, который создали ранее и прикрепим подписчика к NetworkStatusPublisher.
  5. Мы определили метод publisher(queue:) который позволит нам позже создать Publisher из свойства NWPathMonitor и наблюдать за изменениями статусов сети во ViewController например.

Давайте закончим метод receive<S>(subscriber:) сейчас:

import Network
import Combine

// MARK: - NWPathMonitor Subscription
extension NWPathMonitor {
    ...
}

// MARK: - NWPathMonitor Publisher
extension NWPathMonitor {
    
    struct NetworkStatusPublisher: Publisher {
        ...
        
        func receive<S>(subscriber: S) where S : Subscriber,
                                             Never == S.Failure, NWPath.Status == S.Input {
            
            // 1
            let subscription = NetworkStatusSubscription(
                subscriber: subscriber,
                monitor: monitor,
                queue: queue
            )
            
            // 2
            subscriber.receive(subscription: subscription)
        }
    }
    
    ...
}
  1. Мы инициализировали NetworkStatusSubscription с предоставленным подписчиком, NWPathMonitor и DispatchQueue.
  2. Мы сказали подписчику что мы успешно подписались на издателя.

В итоге мы готовы использовать NetworkStatusPublisher в нашем приложении.

. . .

Реализация SwiftUI

Вот как мы можем использовать это в SwiftUI совместно в архитектурным шаблоном MVVM:

import SwiftUI
import Combine
import Network

class ViewModel: ObservableObject {
    private var cancellables = Set<AnyCancellable>()
    private let monitorQueue = DispatchQueue(label: "monitor")
    
    // 1
    @Published var networkStatus: NWPath.Status = .satisfied
    
    init() {
        // 2
        NWPathMonitor()
            .publisher(queue: monitorQueue)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] status in
                self?.networkStatus = status
            }
            .store(in: &cancellables)
    }
}

struct ContentView: View {
    // 3
    @ObservedObject var viewModel = ViewModel()
    
    var body: some View {
        // 4
        Text(viewModel.networkStatus == .satisfied ?
                "Connection is OK" : "Connection lost"
        )
        .padding()
    }
}
  1. Мы создали observableObject вью модель со свойсвом Publisher с названием networkStatus.
  2. Внутри инициализатора, мы запускаем подписку и наблюдателя за изменением статуса внутри оператора .sink. Когда мы получаем значение, мы связываем его с издателем свойства networkStatus который мы определили ранее.
  3. Внутри ContentView, мы просто показываем различный текст зависящий от текущего статуса сети. Когда статус меняется, мы автоматически обновляем текст.

. . .

Реализация UIKit

Реализация внутри UIKit очень похожа

import UIKit

import Combine
import Network

class ViewController: UIViewController {
    
    // MARK: - Properties
    private var cancellables = Set<AnyCancellable>()
    private let monitorQueue = DispatchQueue(label: "monitor")
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.setupUI()
        self.observeNetworkStatus()
    }
    
    // MARK: - Network Status Observation
    private func observeNetworkStatus() {
        NWPathMonitor()
            .publisher(queue: monitorQueue)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] status in
                
                self?.textLabel.text = status == .satisfied ?
                    "Connection is OK" : "Connection lost"
            }
            .store(in: &cancellables)
    }
    
    // MARK: - UI Code
    lazy var textLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 20)
        label.textColor = .orange
        label.translatesAutoresizingMaskIntoConstraints = true
        return label
    }()
    
    private func setupUI() {
        self.view.backgroundColor = .white
        self.view.addSubview(textLabel)
        
        NSLayoutConstraint.activate([
            self.textLabel.centerXAnchor
                .constraint(equalTo: self.view.centerXAnchor),
            self.textLabel.centerYAnchor
                .constraint(equalTo: self.view.centerYAnchor),
        ])
    }
    
}

Здесь мы имеем метод observeNetworkStatus() внутри которого мы наблюдаем за статусом и обновляем textLabel соответственно.

Великолепно! Мы успешно реализовали переиспользуемую работу издателя статуса сети, мы можем с легкостью использовать это в нашем приложении.

Заметьте что тестирование изменение сети лучше делать на реально устройстве потому что симулятор не точно показывает изменения сети в приложении.

. . .

Ресурсы

Исходный код доступен на GitHub.

. . .

Заключение

Для большего изучения NWPathMonitor, не стесняйтесь посмотреть эту великолепную статью от Ross Butler.

Я надеюсь этот урок был полезен для вас. Спасибо за чтение!