【SwiftUI】オフラインになったら画面を切り替える

# はじめに
Webの場合、インターネット接続がないと以下のような表示になります。
![スクリーンショット 2022-11-25 16.07.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/6de8e1e2-b44f-76ff-80cb-4df74e55af20.png)

これをモバイルアプリでも再現したいと思います。

# サンプルアプリ
![RPReplay_Final1669361474_MP4_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/ef6f4cd1-b1f5-91bd-1e16-ae79f8724bf3.gif)

# 実装
「[NWPathMonitor を Combine の Publisher として扱えるようにする拡張](https://gist.github.com/YusukeHosonuma/0a39266dd5c2232d6d0a42f826adb8e6)」を使用してCombineで使えるように拡張します。
```swift:NWPathMonitor
import Combine
import Network

extension NWPathMonitor {
    func publisher(queue: DispatchQueue = DispatchQueue.main) -> NWPathMonitor.Publisher {
        Publisher(monitor: self, queue: queue)
    }

    final class Subscription<S: Subscriber>: Combine.Subscription where S.Input == NWPath {
        private let subscriber: S
        private let monitor: NWPathMonitor
        private let queue: DispatchQueue
        private var isStarted = false

        init(subscriber: S, monitor: NWPathMonitor, queue: DispatchQueue) {
            self.subscriber = subscriber
            self.monitor = monitor
            self.queue = queue
        }

        func request(_ demand: Subscribers.Demand) {
            precondition(demand == .unlimited, "This subscription is only supported to `Demand.unlimited`.")

            guard !isStarted else { return }
            isStarted = true

            monitor.pathUpdateHandler = { [unowned self] path in
                _ = self.subscriber.receive(path)
            }
            monitor.start(queue: queue)
        }

        func cancel() {
            monitor.cancel()
        }
    }

    struct Publisher: Combine.Publisher {
        typealias Output = NWPath
        typealias Failure = Never

        private let monitor: NWPathMonitor
        private let queue: DispatchQueue

        init(monitor: NWPathMonitor, queue: DispatchQueue) {
            self.monitor = monitor
            self.queue = queue
        }

        func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
            let subscription = Subscription(subscriber: subscriber, monitor: monitor, queue: queue)
            subscriber.receive(subscription: subscription)
        }
    }
}
```

```swift:NetworkMonitor
import Combine
import Network
import SwiftUI

fileprivate final class NetworkMonitorViewModel: ObservableObject {
    @Published var status: NWPath.Status = .satisfied

    private let monitor = NWPathMonitor()

    init() {
        monitor.publisher()
            .map(\.status)
            .assign(to: &$status)
    }
}

struct NetworkMonitorView<T: View>: View {
    @StateObject private var viewModel = NetworkMonitorViewModel()
    private let online: () -> T
    private let offline: () -> T
    init(@ViewBuilder online: @escaping () -> T,
         @ViewBuilder offline: @escaping () -> T
    ) {
        self.online = online
        self.offline = offline
    }
    var body: some View {
        switch viewModel.status {
        case .satisfied:
            online()
        case .unsatisfied:
            offline()
        case .requiresConnection:
            offline()
        @unknown default:
            fatalError()
        }
    }
}
```

# 使い方
```swift:ContentView
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            NetworkMonitorView {
                Image(systemName: "wifi")
                    .foregroundColor(.green)
                    .font(.system(size: 50))
                Text("オンライン")
                    .foregroundColor(.green)
                    .font(.system(size: 50))
            } offline: {
                Image(systemName: "wifi.slash")
                    .foregroundColor(.red)
                    .font(.system(size: 50))
                Text("オフライン")
                    .foregroundColor(.red)
                    .font(.system(size: 50))
            }
        }
    }
}
```

# おわり
「[NWPathMonitor を Combine の Publisher として扱えるようにする拡張](https://gist.github.com/YusukeHosonuma/0a39266dd5c2232d6d0a42f826adb8e6)」
これめっちゃ勉強になりました！