Reacting to Network changes in SwiftUI

Federico Ramos
4 min readApr 16, 2024

We’ll see how with just a few lines of code we can observe and react to network changes in our iOS app.

We’ll use for this the Network framework

https://developer.apple.com/documentation/network

Let’s start by creating a new Project and adding the class responsible of implementing the observation. I’ll call it, NetworkStatus and I’ll adopt the Singleton Pattern so just one instance of this class is created in the entire app, I’ll explain later why.

class NetworkStatus {
/// Shared singleton instance for global access.
static let shared = NetworkStatus()

/// The network path monitor from the Network framework.
private var monitor: NWPathMonitor?

/// The dispatch queue on which the network monitor runs.
private var queue = DispatchQueue.global(qos: .background)

private init() {
startMonitoring()
}

/// Begins network monitoring on a background queue.
private func startMonitoring() {
monitor = NWPathMonitor()
monitor?.start(queue: queue)
}

/// Provides an asynchronous stream of network connection status as booleans, where `true` indicates connectivity.
func networkUpdates() -> AsyncStream<Bool> {
return AsyncStream { continuation in
self.monitor?.pathUpdateHandler = { path in
let isConnected = path.status == .satisfied
continuation.yield(isConnected)
}

// Handles cleanup when the AsyncStream no longer needs to send updates.
continuation.onTermination = { @Sendable [weak self] _ in
self?.stopMonitoring()
}
}
}

/// Stops the network monitor and cleans up resources.
private func stopMonitoring() {
monitor?.cancel()
monitor = nil
}

/// Deinitializes the instance and ensures the monitoring is stopped to prevent resource leaks.
deinit {
stopMonitoring()
}
}

Updates come from the NWPathMonitor we’re setting upon init, and this monitor provides updates on .pathUpdateHandler

With this, we’re setting an AsyncStream on the NetworkStatus class.

Now, to observe changes in our view, we just need to use async/await syntax like this:

Note that the task will await for changes in the AsyncStream and update the @State var isConnected. With that, we can do whatever we want, display a sheet, an alert, etc. In this case, an overlay with the text “No internet connection”

You can now run this on the simulator. In order to try it, you’ll have to either run on a real device or turn off wifi on the computer you’re running the simulator (this is because there is no simple way to turn off wifi just for the simulator)

And that’s it.. but wait! We can make this better. How? For me, this is a perfect situation where a SwiftUI ViewModifier will come very handy.

By moving all the logic related to the network observation into a ViewModifier, we can then just apply this modifier in whatever View we need, and by using the Singleton Pattern, we ensure only one instance of the NetworkStatus class will be initialized, no matter how many times in our app we use the viewModifier.

struct ObserveNetworkStatus: ViewModifier {

@State private var isConnected: Bool = true

func body(content: Content) -> some View {

ZStack {
content

if !isConnected {

Rectangle()
.fill(.ultraThinMaterial)
.ignoresSafeArea()

ContentUnavailableView(
"No Internet Connection",
systemImage: "network.slash",
description: Text("This app requieres an active internet connection.")
)
}
}
.animation(.easeInOut, value: isConnected)
.task {
for await status in NetworkStatus.shared.networkUpdates() {
isConnected = status
}
}
}
}

extension View {
func needsInternet() -> some View {
self.modifier(ObserveNetworkStatus())
}
}

I created the ViewModifier ObserveNetworkStatus with all the logic we need to observe Network changes.

Note that in this case, I made the overlay nicer by using ContentUnavailableView

Then extended View with a method ‘needsInternet’ that applies that modifier.

Now, we just need to use .needsInternet() on any View we want to react to network changes. Easy peasy

This project can be found in this github repo

--

--