-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathSTHostnameReachability.swift
More file actions
186 lines (162 loc) · 6.29 KB
/
Copy pathSTHostnameReachability.swift
File metadata and controls
186 lines (162 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//
// STHostnameReachability.swift
// STBaseProject
//
// Created by 寒江孤影 on 2018/12/10.
//
import Foundation
import SystemConfiguration
/// 基于 SCNetworkReachability 的主机可达性监听
///
/// 与 `STNetworkMonitoring`(NWPathMonitor)的区别:
/// - 该类可以跟踪特定主机名的可达性
/// - 可区分 Wi-Fi/蜂窝/离线
/// - 可选是否允许走蜂窝
public final class STHostnameReachability {
public enum STReachabilityStatus: Int, Sendable {
case unknown
case offline
case onlineViaCellular
case onlineViaWiFi
}
public static let statusChangedNotification = Notification.Name("STHostnameReachabilityStatusChanged")
public static let didGoOnlineNotification = Notification.Name("STHostnameReachabilityDidGoOnline")
public static let didGoOfflineNotification = Notification.Name("STHostnameReachabilityDidGoOffline")
public static let cellularPolicyChangedNotification = Notification.Name("STHostnameReachabilityCellularPolicyChanged")
private let hostname: String
private var reachabilityRef: SCNetworkReachability?
private let lock = NSLock()
private var currentStatus: STReachabilityStatus = .unknown
private var previousStatus: STReachabilityStatus = .unknown
private var cachedIsOnline: Bool = false
private var requireWiFi: Bool
public var status: STReachabilityStatus {
lock.lock()
defer { lock.unlock() }
return currentStatus
}
public var isOnline: Bool {
lock.lock()
defer { lock.unlock() }
return cachedIsOnline
}
public var isOnlineViaWiFi: Bool {
status == .onlineViaWiFi
}
/// 是否允许使用蜂窝网络视为在线
public var allowsCellular: Bool {
get {
lock.lock()
defer { lock.unlock() }
return !requireWiFi
}
set {
let changed: Bool = lock.withLock {
let oldValue = !requireWiFi
requireWiFi = !newValue
return oldValue != newValue
}
if changed {
NotificationCenter.default.post(name: Self.cellularPolicyChangedNotification, object: self)
refresh()
}
}
}
/// 初始化
/// - Parameters:
/// - hostname: 需要监听的主机名
/// - allowsCellular: 是否允许蜂窝网络视为在线
public init(hostname: String, allowsCellular: Bool = true) {
self.hostname = hostname
self.requireWiFi = !allowsCellular
setupReachability(hostname: hostname)
}
deinit {
if let ref = reachabilityRef {
SCNetworkReachabilitySetCallback(ref, nil, nil)
SCNetworkReachabilityUnscheduleFromRunLoop(ref, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue)
}
}
/// 根据 URLError 判断是否是本机离线导致的错误
public func isOfflineError(_ error: Error) -> Bool {
let nsError = error as NSError
guard nsError.domain == NSURLErrorDomain else { return false }
let offlineCodes: Set<Int> = [
NSURLErrorNotConnectedToInternet,
NSURLErrorNetworkConnectionLost,
NSURLErrorDataNotAllowed
]
if offlineCodes.contains(nsError.code) {
refresh()
return true
}
return false
}
/// 主动刷新当前状态
public func refresh() {
guard let ref = reachabilityRef else { return }
var flags = SCNetworkReachabilityFlags()
guard SCNetworkReachabilityGetFlags(ref, &flags) else { return }
updateStatus(for: flags)
}
private func setupReachability(hostname: String) {
guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { return }
reachabilityRef = ref
var context = SCNetworkReachabilityContext(
version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil
)
let callback: SCNetworkReachabilityCallBack = { _, flags, info in
guard let info else { return }
let instance = Unmanaged<STHostnameReachability>.fromOpaque(info).takeUnretainedValue()
instance.updateStatus(for: flags)
}
SCNetworkReachabilitySetCallback(ref, callback, &context)
SCNetworkReachabilityScheduleWithRunLoop(ref, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue)
// 初始读取一次
refresh()
}
private func updateStatus(for flags: SCNetworkReachabilityFlags) {
let newStatus: STReachabilityStatus
if Self.isReachable(flags) {
newStatus = flags.contains(.isWWAN) ? .onlineViaCellular : .onlineViaWiFi
} else {
newStatus = .offline
}
lock.lock()
previousStatus = currentStatus
currentStatus = newStatus
let effectiveOnline = newStatus == .onlineViaWiFi || (!requireWiFi && newStatus == .onlineViaCellular)
let wentOnline = effectiveOnline && !cachedIsOnline
let wentOffline = !effectiveOnline && cachedIsOnline
let wasUnknown = previousStatus == .unknown
cachedIsOnline = effectiveOnline
lock.unlock()
DispatchQueue.main.async {
let center = NotificationCenter.default
if wentOnline, !wasUnknown {
center.post(name: Self.didGoOnlineNotification, object: self)
} else if wentOffline, !wasUnknown {
center.post(name: Self.didGoOfflineNotification, object: self)
}
center.post(name: Self.statusChangedNotification, object: self)
}
}
private static func isReachable(_ flags: SCNetworkReachabilityFlags) -> Bool {
guard flags.contains(.reachable) else { return false }
guard flags.contains(.connectionRequired) else { return true }
let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
let requiresUserIntervention = flags.contains(.interventionRequired)
return canConnectAutomatically && !requiresUserIntervention
}
}
private extension NSLock {
func withLock<T>(_ body: () -> T) -> T {
lock()
defer { unlock() }
return body()
}
}