Skip to content

Commit cf81057

Browse files
committed
add ignoreFirstPathUpdate
1 parent 19dba58 commit cf81057

File tree

3 files changed

+166
-8
lines changed

3 files changed

+166
-8
lines changed

README.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# NetworkPathMonitor
22

3-
[![Swift](https://img.shields.io/badge/Swift-5.9%2B-orange.svg)](https://swift.org)
3+
[![Swift](https://img.shields.io/badge/Swift-5.10%2B-orange.svg)](https://swift.org)
44
[![Platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20macOS%20%7C%20tvOS%20%7C%20watchOS%20%7C%20visionOS-lightgrey.svg)](#requirements)
55
[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](LICENSE)
66

@@ -16,13 +16,14 @@ NetworkPathMonitor provides an easy and safe way to observe network connectivity
1616
- 🌀 AsyncStream support for async/await style observation
1717
- 🛎️ Callback and NotificationCenter support
1818
- ⏳ Debounce mechanism to avoid frequent updates
19+
- 🚫 Option to ignore the first path update
1920
- 🛠️ Simple API, easy integration
2021

2122
---
2223

2324
## Requirements
2425

25-
- Swift 5.9 or later
26+
- Swift 5.10 or later
2627
- iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+
2728

2829
---
@@ -96,9 +97,8 @@ let observer = NotificationCenter.default.addObserver(
9697
object: nil,
9798
queue: .main
9899
) { notification in
99-
if let oldPath = notification.userInfo?["oldPath"] as? NWPath,
100-
let newPath = notification.userInfo?["newPath"] as? NWPath {
101-
print("Network changed from \(oldPath.status) to \(newPath.status)")
100+
if let newPath = notification.userInfo?["newPath"] as? NWPath {
101+
print("Network status changed to: \(newPath.status)")
102102
}
103103
}
104104
```
@@ -115,16 +115,41 @@ let monitor = NetworkPathMonitor(debounceInterval: 1.0) // 1 second debounce
115115

116116
---
117117

118+
## Ignore First Path Update
119+
120+
Sometimes you may want to ignore the initial network path update that occurs when monitoring starts, especially during app launch. You can use the `ignoreFirstPathUpdate` parameter:
121+
122+
```swift
123+
// Ignore the first path update when monitoring starts
124+
let monitor = NetworkPathMonitor(ignoreFirstPathUpdate: true)
125+
await monitor.fire() // First update will be ignored
126+
127+
// Useful for avoiding immediate notifications during app startup
128+
let monitor = NetworkPathMonitor(
129+
debounceInterval: 0.5,
130+
ignoreFirstPathUpdate: true
131+
)
132+
```
133+
134+
This is particularly useful when:
135+
136+
- You want to avoid showing network status alerts immediately when the app starts
137+
- You only care about network changes after the initial connection is established
138+
- You're implementing features that should only respond to actual network transitions
139+
140+
---
141+
118142
## API
119143

120144
### Initialization
121145

122146
```swift
123-
init(queue: DispatchQueue = ..., debounceInterval: TimeInterval = 0)
147+
init(queue: DispatchQueue = ..., debounceInterval: TimeInterval = 0, ignoreFirstPathUpdate: Bool = false)
124148
```
125149

126150
- `queue`: The dispatch queue for the underlying NWPathMonitor.
127151
- `debounceInterval`: Debounce interval in seconds. Default is 0 (no debounce).
152+
- `ignoreFirstPathUpdate`: Whether to ignore the first path update. Default is false.
128153

129154
### Properties
130155

Sources/NetworkPathMonitor/NetworkPathMonitor.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public actor NetworkPathMonitor {
2929
/// Debounce interval in seconds.
3030
private let debounceInterval: TimeInterval
3131

32+
/// Ignore first path update.
33+
private let ignoreFirstPathUpdate: Bool
34+
3235
/// The network path update handler.
3336
private var networkPathUpdater: PathUpdateHandler?
3437

@@ -44,17 +47,25 @@ public actor NetworkPathMonitor {
4447
/// Current network path.
4548
public private(set) var currentPath: NWPath
4649

50+
/// Flag to track if this is the first path update.
51+
private var isFirstUpdate: Bool = true
52+
4753
/// Network path status change notification.
4854
public static let networkStatusDidChangeNotification = Notification.Name("NetworkPathMonitor.NetworkPathStatusDidChange")
4955

5056
/// Initializes a new instance of `NetworkPathMonitor`.
5157
/// - Parameter queue: The queue on which the network path monitor runs. Default is a serial queue with a unique label.
5258
/// - Parameter debounceInterval: Debounce interval in seconds. If set to 0, no debounce will be applied. Default is 0 seconds.
53-
public init(queue: DispatchQueue = .init(label: "com.networkPathMonitor.\(UUID())"), debounceInterval: TimeInterval = 0) {
59+
/// - Parameter ignoreFirstPathUpdate: Ignore first path update. Default is false.
60+
public init(queue: DispatchQueue = .init(label: "com.networkPathMonitor.\(UUID())"),
61+
debounceInterval: TimeInterval = 0,
62+
ignoreFirstPathUpdate: Bool = false)
63+
{
5464
precondition(debounceInterval >= 0, "debounceInterval must be greater than or equal to 0")
5565
monitorQueue = queue
5666
currentPath = networkMonitor.currentPath
5767
self.debounceInterval = debounceInterval
68+
self.ignoreFirstPathUpdate = ignoreFirstPathUpdate
5869
networkMonitor.pathUpdateHandler = { [weak self] path in
5970
guard let self else { return }
6071
Task { await self.handlePathUpdate(path) }
@@ -92,6 +103,13 @@ public actor NetworkPathMonitor {
92103

93104
private func handlePathUpdate(_ path: NWPath) async {
94105
currentPath = path
106+
107+
// Check if we should ignore the first path update
108+
if isFirstUpdate, ignoreFirstPathUpdate {
109+
isFirstUpdate = false
110+
return
111+
}
112+
95113
debounceTask?.cancel()
96114
guard debounceInterval > 0 else {
97115
// No debounce, yield immediately
@@ -114,6 +132,9 @@ public actor NetworkPathMonitor {
114132

115133
// Yield the path update handler
116134
private func yieldNetworkPath(_ path: NWPath) async {
135+
// Mark first update as completed when actually notifying
136+
isFirstUpdate = false
137+
117138
// Send updates via AsyncStream
118139
pathUpdateContinuation?.yield(path)
119140

Tests/NetworkPathMonitorTests/NetworkPathMonitorTests.swift

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ class NetworkPathMonitorTests: XCTestCase, @unchecked Sendable {
114114
object: nil,
115115
queue: nil
116116
) { notification in
117-
XCTAssertNotNil(notification.userInfo?["oldPath"])
118117
XCTAssertNotNil(notification.userInfo?["newPath"])
119118
expectation.fulfill()
120119
}
@@ -156,4 +155,117 @@ class NetworkPathMonitorTests: XCTestCase, @unchecked Sendable {
156155
let currentPathSatisfied = await monitor.currentPath.isSatisfied
157156
XCTAssertEqual(satisfied, currentPathSatisfied)
158157
}
158+
159+
// MARK: - IgnoreFirstPathUpdate Tests
160+
161+
func testIgnoreFirstPathUpdateEnabled() async {
162+
let ignoreFirstMonitor = NetworkPathMonitor(ignoreFirstPathUpdate: true)
163+
let expectation = XCTestExpectation(description: "First update should be ignored")
164+
expectation.isInverted = true // We expect this NOT to be fulfilled
165+
166+
var updateCount = 0
167+
await ignoreFirstMonitor.pathOnChange { @MainActor _ in
168+
updateCount += 1
169+
expectation.fulfill()
170+
}
171+
172+
await ignoreFirstMonitor.fire()
173+
174+
// Wait briefly to ensure no updates are received
175+
await fulfillment(of: [expectation], timeout: 1.0)
176+
177+
XCTAssertEqual(updateCount, 0, "First update should be ignored")
178+
179+
await ignoreFirstMonitor.invalidate()
180+
}
181+
182+
func testIgnoreFirstPathUpdateDisabled() async {
183+
let normalMonitor = NetworkPathMonitor(ignoreFirstPathUpdate: false)
184+
let expectation = XCTestExpectation(description: "First update should be received")
185+
186+
var updateCount = 0
187+
await normalMonitor.pathOnChange { @MainActor _ in
188+
updateCount += 1
189+
expectation.fulfill()
190+
}
191+
192+
await normalMonitor.fire()
193+
194+
await fulfillment(of: [expectation], timeout: 5.0)
195+
196+
XCTAssertEqual(updateCount, 1, "First update should be received")
197+
198+
await normalMonitor.invalidate()
199+
}
200+
201+
func testIgnoreFirstPathUpdateWithAsyncStream() async {
202+
let ignoreFirstMonitor = NetworkPathMonitor(ignoreFirstPathUpdate: true)
203+
await ignoreFirstMonitor.fire()
204+
205+
var updates: [NWPath] = []
206+
let expectation = XCTestExpectation(description: "First update should be ignored in stream")
207+
expectation.isInverted = true
208+
209+
let task = Task { @MainActor in
210+
for await update in await ignoreFirstMonitor.pathUpdates {
211+
updates.append(update)
212+
expectation.fulfill()
213+
break // Only check for first update
214+
}
215+
}
216+
217+
// Wait briefly to ensure no updates are received
218+
await fulfillment(of: [expectation], timeout: 1.0)
219+
220+
task.cancel()
221+
await ignoreFirstMonitor.invalidate()
222+
223+
XCTAssertTrue(updates.isEmpty, "No updates should be received in stream when ignoring first update")
224+
}
225+
226+
func testIgnoreFirstPathUpdateWithNotification() async {
227+
let ignoreFirstMonitor = NetworkPathMonitor(ignoreFirstPathUpdate: true)
228+
let expectation = XCTestExpectation(description: "First notification should be ignored")
229+
expectation.isInverted = true
230+
231+
let observer = NotificationCenter.default.addObserver(
232+
forName: NetworkPathMonitor.networkStatusDidChangeNotification,
233+
object: ignoreFirstMonitor,
234+
queue: nil
235+
) { _ in
236+
expectation.fulfill()
237+
}
238+
239+
await ignoreFirstMonitor.fire()
240+
241+
// Wait briefly to ensure no notifications are received
242+
await fulfillment(of: [expectation], timeout: 1.0)
243+
244+
NotificationCenter.default.removeObserver(observer)
245+
await ignoreFirstMonitor.invalidate()
246+
}
247+
248+
func testIgnoreFirstPathUpdateWithDebounce() async {
249+
let debounceIgnoreMonitor = NetworkPathMonitor(
250+
debounceInterval: 0.5,
251+
ignoreFirstPathUpdate: true
252+
)
253+
let expectation = XCTestExpectation(description: "First debounced update should be ignored")
254+
expectation.isInverted = true
255+
256+
var updateCount = 0
257+
await debounceIgnoreMonitor.pathOnChange { @MainActor _ in
258+
updateCount += 1
259+
expectation.fulfill()
260+
}
261+
262+
await debounceIgnoreMonitor.fire()
263+
264+
// Wait longer than debounce interval to ensure no updates
265+
await fulfillment(of: [expectation], timeout: 1.5)
266+
267+
XCTAssertEqual(updateCount, 0, "First debounced update should be ignored")
268+
269+
await debounceIgnoreMonitor.invalidate()
270+
}
159271
}

0 commit comments

Comments
 (0)