Примеры в 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() { } } }
- Мы зависим от Subrscriber который принимает NWPah.Status на вход.
- Мы так же создали monitor и queue зависимости итак мы позже можем запустить мониторинг сети
- Что бы соответствовать протоколу 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() } } }
- Мы используем свойство pathUpdateHandler для получения NWPath содержащий текущий статус сети. Когда статус поменяется, Subsciber получит значение NWPath.Status.
- Мы запускаем процесс мониторинга и должны получить измененный статус внутри замыкания pathUpdateHandler.
- Внутри метода 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) } }
Здесь то что мы достигаем этим кодом:
- Мы определили кастомный NetworkStatusPublisher который соответствуем протоколу Publisher.
- издатель выдает значения NWPath.Status и никогда не подводит.
- Похоже на подписку что мы определили ранее, мы также зависим от NWPathMonitor и DispatсhQueue.
- Внутри требуемого метода receive<S>(subscriber:) мы скоро создадим NetworkStatusSubscription, который создали ранее и прикрепим подписчика к NetworkStatusPublisher.
- Мы определили метод 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) } } ... }
- Мы инициализировали NetworkStatusSubscription с предоставленным подписчиком, NWPathMonitor и DispatchQueue.
- Мы сказали подписчику что мы успешно подписались на издателя.
В итоге мы готовы использовать 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() } }
- Мы создали observableObject вью модель со свойсвом Publisher с названием networkStatus.
- Внутри инициализатора, мы запускаем подписку и наблюдателя за изменением статуса внутри оператора .sink. Когда мы получаем значение, мы связываем его с издателем свойства networkStatus который мы определили ранее.
- Внутри 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.
Я надеюсь этот урок был полезен для вас. Спасибо за чтение!