using NWPathMonitor with BehaviorSubject to monitor network connectivity - ios

I need an observer to keep my app updated on the connectivity status of the device.
NWPathMonitor seems to be the standard approach. So I go like this:
class NetworkService {
let monitor = NWPathMonitor()
let connected = BehaviorSubject(value: true)
private init() {
monitor.pathUpdateHandler = { path in
let value = path.status == .satisfied
self.connected.onNext(value)
}
let queue = DispatchQueue(label: "NetworkMonitor")
monitor.start(queue: queue)
}
}
And this is where I subscribe to connected
NetworkService.shared.connected.subscribe(onNext: { connected in
print("network connected: \(connected)")
}).disposed(by: disposeBag)
As soon as the app starts, onNext starts firing like crazy, flooding the console with network connected: true until the app crashes.
I tried adding a local cache variable so the onNext part fires only if there's been a change on the value.
if (value != self.previousValue) {
self.previousValue = value
self.connected.onNext(value)
}
Same happens still. So I guessed maybe the monitor is updating too frequently to allow the cache variable to get assigned, and I tried adding a semaphore ...
self.semaphore.wait()
if (value != self.previousValue) {
self.previousValue = value
self.connected.onNext(value)
}
self.semaphore.signal()
And event that didn't help. Still getting a flood of print messages and the app crashes.
BTW, if you were wondering this is how I declare the semaphore in my class:
let semaphore = DispatchSemaphore( value: 1)

I'm not seeing the same behavior from the class as you, but a simple solution is to use .distinctUntilChanged() which will stop an event from propagating unless it is different than the previous event.
If the above doesn't stop the flood of events, then the problem isn't with the code you have presented, but with something else you haven't told us about.
Also, I would have written it like this:
extension NWPathMonitor {
var rx_path: Observable<NWPath> {
Observable.create { [self] observer in
self.pathUpdateHandler = { path in
observer.onNext(path)
}
let queue = DispatchQueue(label: "NetworkMonitor")
self.start(queue: queue)
return Disposables.create {
self.cancel()
}
}
}
}
With the above, it's easy to access by doing:
let disposable = NWPathMonitor().rx_path
.map { $0.status == .satisfied }
.debug()
.subscribe()
The subscription will keep the NWPathMonitor object alive for the duration of the subscription. Calling dispose() on the disposable will shut down the subscription and release the NWPathMonitor object.

Related

Using reachability library to Swiftui based app to notify when network is lost

I'm very much new to ios && using cocoapods.
I scanned over SO to find easiest way to detect network status and lots of answers directed me to Reachability git by AshleyMills.
I'm writing my webView app in swiftui & I want to pop up an alert/notifier when user's internet connection is lost. (So they don't sit idle while my webview tries to load)
It would be best if listner to network changes keeps running in the background while the app is on.
Most of the answers in SO seem to be Swift-based (appdelegate, ViewDidload, etc), which I don't know how to use because I started off with SwiftUI
Thanks in advance.
Edit(With attempts for Workaround that Lukas provided)
I tried the below. It compiles and runs BUT the alert wouldn't show up. Nor does it respond to connection change.
I have number of subViews in my ContentView. So I called timer && reachability on app level.
#State var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let reachability = try! Reachability()
#State private var showAlert: Bool = false
It seems to be not working:
WindowGroup {
ContentView()
.onAppear() {
if !isConnected() {
self.showAlert = true
}
}
.onReceive(timer) { _ in
if !isConnected() {
self.showAlert = true
}else{
self.showAlert = false
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Error"), message: Text("Your internet connection is too slow."), dismissButton: .default(Text("ok")))
}
}
NWPathMonitor was introduced in iOS 12 as a replacement for Reachability. A naive implementation would be as follows.
We could expose the status that comes back from pathUpdateHandler, however that would require you to import Network everywhere you wanted to use the status - not ideal. It would be better to create your own enum that maps to each of the cases provided by NWPath.Status, you can see the values here. I’ve created one that just handles the connected or unconnected states.
So we create a very simple ObservedObject that publishes our status. Note that as the monitor operates on its own queue and we may want to update things in the view we will need to make sure that we publish on the main queue.
import Network
import SwiftUI
// An enum to handle the network status
enum NetworkStatus: String {
case connected
case disconnected
}
class Monitor: ObservableObject {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "Monitor")
#Published var status: NetworkStatus = .connected
init() {
monitor.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
// Monitor runs on a background thread so we need to publish
// on the main thread
DispatchQueue.main.async {
if path.status == .satisfied {
print("We're connected!")
self.status = .connected
} else {
print("No connection.")
self.status = .disconnected
}
}
}
monitor.start(queue: queue)
}
}
Then we can use our Monitor class how we like, you could use it as a StateObject as I have done below, or you could use it as an EnvironmentObject. The choice is yours. Ideally you should have only a single instance of this class in your app.
struct ContentView: View {
#StateObject var monitor = Monitor()
// #EnvironmentObject var monitor: Monitor
var body: some View {
Text(monitor.status.rawValue)
}
}
Tested in Playgrounds on an iPad Pro running iOS 14.2, and on Xcode 12.2 using iPhone X (real device) on iOS 14.3.
I worked one time with rechabiltiy and had the same problem. My solution is a work-around but in my case it was fine.
When the user get to the view where the connection should be constantly checked you can start a time(https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-a-timer-with-swiftui). The timer calls a function in an interval of your choice where you can check the connection with the following code:
let reachability = try! Reachability()
...
func isConnected() -> Bool{
if reachability.connection == .none{
return "false" //no Connection
}
return true
}
}
You can also call this function .onAppear

How does DispatchQueue.main.async store it's blocks

I have a code similar to this:
func fetchBalances() -> Observable<Result<[User], Error>> {
Observable.create { observer in
var dataChangeDisposable: Disposable?
DispatchQueue.main.async {
let realm = try! Realm()
let user = realm.objects(UserData.self)
dataChangeDisposable = Observable.collection(from: user)
.map { $0.map { UserData.convert($0) } }
.subscribe(onNext: {
observer.onNext(.success($0))
})
}
return Disposables.create {
dataChangeDisposable?.dispose()
}
}
}
I need to use some thread with run loop in order to maintain subscription to Realm database (Realm's restriction). For now I'm using DispatchQueue.main.async {} method and I noticed that subscription remains active all the time, how does DispatchQueue.main stores it's submitted blocks and if Observable destroys does it mean that I'm leaking blocks in memory?
The block sent to the dispatch queue is deleted immediately after execution. It isn't stored for very long at all.
If your subscription "remains active all the time" then it's because it's not being disposed of properly. Likely what is happening here is that the block sent to Disposables.create is being called before dataChangeDisposable contains a value.
Test my hypothesis by changing the code to:
return Disposables.create {
dataChangeDisposable!.dispose()
}
If your app crashes because dataChangeDisposable is nil, then that's your problem.

Posting notification halts the further execution of code

I have a singleton object for observing Network status, it's based around NWPathMonitor class.
In the pathUpdateHandler callback I want to post a custom notification with current interface type of the path and then the strangest thing happens: I have place breakpoints before and after the .post method and the second breakpoint is never reached and the notification does is not posted. What could be the issue? It's the first time I've encountered such situation
class NetworkMonitor {
static let shared = NetworkMonitor()
private let monitor: NWPathMonitor
enum InterfaceType: String {
case wifi
case cellular
case other
case none
}
private init() {
monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
print("Before notification")
NotificationCenter.default.post(name: NetworkMonitor.connectionChangedNotification, object: nil, userInfo: ["interface": path.interfaceType])
print("Notification posted") // This line is not being printed
}
}
public func start() {
let queue = DispatchQueue.global(qos: .utility)
monitor.start(queue: queue)
}
}
fileprivate extension NWPath {
var interfaceType: NetworkMonitor.InterfaceType {
guard status == .satisfied else {
return .none
}
if usesInterfaceType(.wifi) {
return .wifi
} else if usesInterfaceType(.cellular) {
return .cellular
} else {
return .other
}
}
}
EDIT:
I have subscribed to this notification in two places, A and B. I noticed that the callback in object B gets called twice and in A not one time. I have no idea what is going on here, for now I have commented out the code in B and everything is working properly, but I mean, what the heck.
In both objects I have
var connectionObserver: Any?
connectionObserver = NotificationCenter.default.addObserver(forName:
NetworkMonitor.connectionChangedNotification, object: nil, queue: nil) {
...
}
If I don't use the connectionObserver variable the bug also exists
Changing the rawValue of notification name does not change anything

How to suspend dispatch queue inside for loop?

I have play and pause button. When I pressed play button, I want to play async talking inside for loop. I used dispatch group for async method's waiting inside for loop. But I cannot achieve pause.
startStopButton.rx.tap.bind {
if self.isPaused {
self.isPaused = false
dispatchGroup.suspend()
dispatchQueue.suspend()
} else {
self.isPaused = true
self.dispatchQueue.async {
for i in 0..<self.textBlocks.count {
self.dispatchGroup.enter()
self.startTalking(string: self.textBlocks[i]) { isFinished in
self.dispatchGroup.leave()
}
self.dispatchGroup.wait()
}
}
}
}.disposed(by: disposeBag)
And i tried to do with operationqueue but still not working. It is still continue talking.
startStopButton.rx.tap.bind {
if self.isPaused {
self.isPaused = false
self.talkingQueue.isSuspended = true
self.talkingQueue.cancelAllOperations()
} else {
self.isPaused = true
self.talkingQueue.addOperation {
for i in 0..<self.textBlocks.count {
self.dispatchGroup.enter()
self.startTalking(string: self.textBlocks[i]) { isFinished in
self.dispatchGroup.leave()
}
self.dispatchGroup.wait()
}
}
}
}.disposed(by: disposeBag)
Is there any advice?
A few observations:
Pausing a group doesn’t do anything. You suspend queues, not groups.
Suspending a queue stops new items from starting on that queue, but it does not suspend anything already running on that queue. So, if you’ve added all the textBlock calls in a single dispatched item of work, then once it’s started, it won’t suspend.
So, rather than dispatching all of these text blocks to the queue as a single task, instead, submit them individually (presuming, of course, that your queue is serial). So, for example, let’s say you had a DispatchQueue:
let queue = DispatchQueue(label: "...")
And then, to queue the tasks, put the async call inside the for loop, so each text block is a separate item in your queue:
for textBlock in textBlocks {
queue.async { [weak self] in
guard let self = self else { return }
let semaphore = DispatchSemaphore(value: 0)
self.startTalking(string: textBlock) {
semaphore.signal()
}
semaphore.wait()
}
}
FYI, while dispatch groups work, a semaphore (great for coordinating a single signal with a wait) might be a more logical choice here, rather than a group (which is intended for coordinating groups of dispatched tasks).
Anyway, when you suspend that queue, the queue will be preventing from starting anything queued (but will finish the current textBlock).
Or you can use an asynchronous Operation, e.g., create your queue:
let queue: OperationQueue = {
let queue = OperationQueue()
queue.name = "..."
queue.maxConcurrentOperationCount = 1
return queue
}()
Then, again, you queue up each spoken word, each respectively a separate operation on that queue:
for textBlock in textBlocks {
queue.addOperation(TalkingOperation(string: textBlock))
}
That of course assumes you encapsulated your talking routine in an operation, e.g.:
class TalkingOperation: AsynchronousOperation {
let string: String
init(string: String) {
self.string = string
}
override func main() {
startTalking(string: string) {
self.finish()
}
}
func startTalking(string: String, completion: #escaping () -> Void) { ... }
}
I prefer this approach because
we’re not blocking any threads;
the logic for talking is nicely encapsulated in that TalkingOperation, in the spirit of the single responsibility principle; and
you can easily suspend the queue or cancel all the operations.
By the way, this is a subclass of an AsynchronousOperation, which abstracts the complexity of asynchronous operation out of the TalkingOperation class. There are many ways to do this, but here’s one random implementation. FWIW, the idea is that you define an AsynchronousOperation subclass that does all the KVO necessary for asynchronous operations outlined in the documentation, and then you can enjoy the benefits of operation queues without making each of your asynchronous operation subclasses too complicated.
For what it’s worth, if you don’t need suspend, but would be happy just canceling, the other approach is to dispatching the whole for loop as a single work item or operation, but check to see if the operation has been canceled inside the for loop:
So, define a few properties:
let queue = DispatchQueue(label: "...")
var item: DispatchWorkItem?
Then you can start the task:
item = DispatchWorkItem { [weak self] in
guard let textBlocks = self?.textBlocks else { return }
for textBlock in textBlocks where self?.item?.isCancelled == false {
let semaphore = DispatchSemaphore(value: 0)
self?.startTalking(string: textBlock) {
semaphore.signal()
}
semaphore.wait()
}
self?.item = nil
}
queue.async(execute: item!)
And then, when you want to stop it, just call item?.cancel(). You can do this same pattern with a non-asynchronous Operation, too.

How can you stop/cancel callback in Swift3?

In my app I have a method that makes cloud calls. It has a completion handler. At some point I have a situation when a users makes this call to the cloud and while waiting for the completion, the user might hit log out.
This will remove the controller from the stack, so the completion block will be returned to the controller that is no longer on a stack.
This causes a crash since I do some UI tasks on that completion return.
I did a workaround where, I'm not doing anything with the UI is the controller in no longer on a stack.
However, I'm curious if it's possible to cancel/stop all pending callbacks somehow on logout?
I'm not sure, but I think something is tightly coupled. Try doing:
{ [weak self] () -> Void in
guard let _ = self else { return }
//rest of your code
}
If you get deinitialized then your completioHanlder would just not proceed.
For the granular control over operations' cancellation, you can return a cancellation token out from your function. Call it upon a need to cancel an operation.
Here is an example how it can be achieved:
typealias CancellationToken = () -> Void
func performWithDelay(callback: #escaping () -> Void) -> CancellationToken {
var cancelled = false
// For the sake of example delayed async execution
// used to emulate callback behavior.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if !cancelled {
callback()
}
}
return { cancelled = true }
}
let cancellationToken = performWithDelay {
print("test")
}
cancellationToken()
For the cases where you just need to ensure that within a block execution there are still all necessary prerequisites and conditions met you can use guard:
{ [weak self] in
guard let `self` = self else { return }
// Your code here... You can write a code down there
// without worrying about unwrapping self or
// creating retain cycles.
}

Resources