Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
brennanMKE committed Nov 27, 2021
0 parents commit 5156c25
Show file tree
Hide file tree
Showing 11 changed files with 689 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.DS_Store
xcuserdata
7 changes: 7 additions & 0 deletions ConnectivityKit.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
35 changes: 35 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "ConnectivityKit",
platforms: [
.iOS(.v10),
.macOS(.v10_14),
.tvOS(.v12),
.watchOS(.v6)
],
products: [
.library(
name: "ConnectivityKit",
targets: ["ConnectivityKit"]),
],
dependencies: [],
targets: [
.target(
name: "ConnectivityKit",
dependencies: ["Reachability"],
path: "Sources/ConnectivityKit"),
.target(
name: "Reachability",
dependencies: [],
path: "Sources/Reachability",
publicHeadersPath: ""
),
.testTarget(
name: "ConnectivityKitTests",
dependencies: ["ConnectivityKit"]),
]
)
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ConnectivityKit

Adapting to changes in network connectivity can allow for suspending or resuming network activity. When entering an elevator or going through a tunnel our devices typically lose connectivity completely. We also move out of range of WiFi and transition to a cellular connection. Apple's [Network Framework] includes [NWPathMonitor] which provides updates in response to changes to [NWPath]. This framework was introduced in 2018 and is a modern replacement to [SCNetworkReachability], simply known as the Reachability API which does not support many of the features supported by modern network protocols such as WiFi 6 and 5G.

For apps which still support older OS versions it is necessary to use Reachability while most users are able to use the Network framework. This package automatically users the API which is available based on the OS version.

See: [Introduction to Network.framework]

## Usage

This package includes `ConnectivityMonitor` which internally uses `NetworkMonitor` or `ReachabilityMonitor` which are simply available as `AnyConnectivityMonitor`. For recent OS versions of iOS, macOS, tvOS and watchOS the `NetworkMonitor` will be used. For earlier versions `ReachabilityMonitor` will be used.

Simply call the `start` function and provide a path handler to get updates. Call the `cancel` function to discontinue monitoring.

## Swift Package

This project is set up as a Swift Package which can be used by the [Swift Package Manager] (SPM) with Xcode. In your `Package.swift` add this package with the code below.

```swift
dependencies: [
.package(url: "https://github.com/brennanMKE/ConnectivityKit", from: "1.0.0"),
],
```

## Supporting iOS 10.0 and Later

Since this package automatically handles the selection of the implementation your code can just use `ConnectivityMonitor` to get updates to the network path. The Reachability API comes from the [System Configuration Framework] which is available for all of Apple's platforms except watchOS. The implementation for the `ReachabilityMonitor` will get an empty implementation for watchOS prior to watchOS 6.0 which is when [Network Framework] was first made available to watchOS.

If your Deployment Target for any of Apple's platforms supports [Network Framework] then it will always use the modern implementation. This package will allow you to use the same code across all platforms and respond to changes to network connectivity.

---
[Network Framework]: https://developer.apple.com/documentation/network
[NWPathMonitor]: https://developer.apple.com/documentation/network/nwpathmonitor
[NWPath]: https://developer.apple.com/documentation/network/nwpath
[SCNetworkReachability]: https://developer.apple.com/documentation/systemconfiguration/scnetworkreachability-g7d
[System Configuration Framework]: https://developer.apple.com/documentation/systemconfiguration
[Introduction to Network.framework]: https://developer.apple.com/videos/play/wwdc2018/715
[Swift Package Manager]: https://swift.org/package-manager/
96 changes: 96 additions & 0 deletions Sources/ConnectivityKit/ConnectivityMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Foundation

public enum ConnectivityStatus: String {
case satisfied
case unsatisfied
case requiresConnection
}

extension ConnectivityStatus: CustomStringConvertible {
public var description: String {
rawValue
}
}

public enum ConnectivityInterfaceType: String {
case other
case wifi
case cellular
case wiredEthernet
case loopback
}

public struct ConnectivityInterface {
public let name: String
public let type: ConnectivityInterfaceType
}

extension ConnectivityInterface: CustomStringConvertible {
public var description: String {
"\(name) (\(type))"
}
}

extension Array where Element == ConnectivityInterface {
public var description: String {
self.map { "\($0.name) (\($0.type))" }.joined(separator: ", ")
}
}

public struct ConnectivityPath {
public let status: ConnectivityStatus
public let availableInterfaces: [ConnectivityInterface]
public let isExpensive: Bool
public let supportsDNS: Bool
public let supportsIPv4: Bool
public let supportsIPv6: Bool
}

extension ConnectivityPath: CustomStringConvertible {
public var description: String {
[
"\(status): \(availableInterfaces.description)",
"Expensive = \(isExpensive ? "YES" : "NO")",
"DNS = \(supportsDNS ? "YES" : "NO")",
"IPv4 = \(supportsIPv4 ? "YES" : "NO")",
"IPv6 = \(supportsIPv6 ? "YES" : "NO")"
].joined(separator: "; ")
}
}

public typealias PathUpdateHandler = (ConnectivityPath) -> Void

public protocol AnyConnectivityMonitor {
func start(pathUpdateQueue: DispatchQueue, pathUpdateHandler: @escaping PathUpdateHandler)
func cancel()
}

public class ConnectivityMonitor {
private var monitor: AnyConnectivityMonitor?
private let queue = DispatchQueue(label: "com.acme.connectivity", qos: .background)

public init() {}

public func start(pathUpdateQueue: DispatchQueue, pathUpdateHandler: @escaping PathUpdateHandler) {
let monitor = createMonitor()
monitor.start(pathUpdateQueue: pathUpdateQueue, pathUpdateHandler: pathUpdateHandler)
self.monitor = monitor
}

public func cancel() {
guard let monitor = monitor else { return }
monitor.cancel()
self.monitor = nil
}

private func createMonitor() -> AnyConnectivityMonitor {
let result: AnyConnectivityMonitor
if #available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *) {
result = NetworkMonitor()
} else {
result = ReachabilityMonitor()
}
return result
}

}
94 changes: 94 additions & 0 deletions Sources/ConnectivityKit/NetworkMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import Foundation
import Network

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
extension ConnectivityStatus {
init(status: NWPath.Status) {
switch status {
case .satisfied:
self = .satisfied
case .unsatisfied:
self = .unsatisfied
case .requiresConnection:
self = .requiresConnection
@unknown default:
self = .unsatisfied
}
}
}

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
extension ConnectivityInterfaceType {
init(interfaceType: NWInterface.InterfaceType) {
switch interfaceType {
case .other:
self = .other
case .wifi:
self = .wifi
case .cellular:
self = .cellular
case .wiredEthernet:
self = .wiredEthernet
case .loopback:
self = .loopback
@unknown default:
self = .other
}
}
}

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
extension ConnectivityInterface {
init(interface: NWInterface) {
name = interface.name
type = ConnectivityInterfaceType(interfaceType: interface.type)
}
}

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
extension ConnectivityPath {
init(path: NWPath) {
status = ConnectivityStatus(status: path.status)
availableInterfaces = path.availableInterfaces.map { ConnectivityInterface(interface: $0) }
isExpensive = path.isExpensive
supportsDNS = path.supportsDNS
supportsIPv4 = path.supportsIPv4
supportsIPv6 = path.supportsIPv6
}
}

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
class NetworkMonitor: AnyConnectivityMonitor {
private var monitor: NWPathMonitor?
private var pathUpdateQueue: DispatchQueue?
private var pathUpdateHandler: PathUpdateHandler?

private let queue = DispatchQueue(label: "com.acme.connectivity.network-monitor", qos: .background)

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
func start(pathUpdateQueue: DispatchQueue, pathUpdateHandler: @escaping PathUpdateHandler) {
self.pathUpdateQueue = pathUpdateQueue
self.pathUpdateHandler = pathUpdateHandler
// A new instance is required each time a monitor is started
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = didUpdate(path:)
monitor.start(queue: queue)
self.monitor = monitor
}

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
func cancel() {
guard let monitor = monitor else { return }
monitor.cancel()
}

@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 6.0, *)
func didUpdate(path: NWPath) {
guard let pathUpdateHandler = pathUpdateHandler,
let pathUpdateQueue = pathUpdateQueue else { return }
let connectivityPath = ConnectivityPath(path: path)
pathUpdateQueue.async {
pathUpdateHandler(connectivityPath)
}
}
}
67 changes: 67 additions & 0 deletions Sources/ConnectivityKit/ReachabilityMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Foundation
import Reachability

extension ConnectivityStatus {

init(networkStatus: NetworkStatus) {
switch networkStatus {
case .notReachable:
self = .unsatisfied
case .reachableViaWiFi, .reachableViaWWAN:
self = .satisfied
@unknown default:
self = .unsatisfied
}
}
}

extension ConnectivityPath {

init(networkStatus: NetworkStatus, connectionRequired: Bool) {
status = ConnectivityStatus(networkStatus: networkStatus)
availableInterfaces = []
isExpensive = networkStatus == .reachableViaWWAN
supportsDNS = false
supportsIPv4 = false
supportsIPv6 = false
}
}

public class ReachabilityMonitor: AnyConnectivityMonitor {
var reachability: Reachability?
private var pathUpdateQueue: DispatchQueue?
private var pathUpdateHandler: PathUpdateHandler?

var hostname: String {
let result = Bundle.main.infoDictionary?["ReachabilityHostname"] as? String ?? "github.com"
return result
}

public func start(pathUpdateQueue: DispatchQueue, pathUpdateHandler: @escaping PathUpdateHandler) {
debugPrint(#function)
self.pathUpdateQueue = pathUpdateQueue
self.pathUpdateHandler = pathUpdateHandler
let reachability = Reachability(hostname: hostname)
reachability.didChangeHandler = didChange(reachability:)
reachability.start()
self.reachability = reachability
}

public func cancel() {
debugPrint(#function)
guard let reachability = reachability else { return }
reachability.cancel()
self.reachability = nil
}

func didChange(reachability: Reachability) {
debugPrint(#function)
guard let pathUpdateHandler = pathUpdateHandler,
let pathUpdateQueue = pathUpdateQueue else { return }
let connectivityPath = ConnectivityPath(networkStatus: reachability.currentStatus, connectionRequired: reachability.connectionRequired)
debugPrint("\(connectivityPath)")
pathUpdateQueue.async {
pathUpdateHandler(connectivityPath)
}
}
}
Loading

0 comments on commit 5156c25

Please sign in to comment.