NEHotspotConfigurationSample/HotspotManager.swift
/* |
Copyright (C) 2018 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Wraps NEHotspotConfigurationManager to make it easier for the view controller to use. |
*/ |
import NetworkExtension |
#if targetEnvironment(simulator) |
let error = _NEHotspotConfigurationManager_is_not_supported_the_simulator_ |
// Swift doesn’t support the equivalent of C’s #error (r. 16015824), |
// so we use the above to get a meaningful error in the Issues navigator. |
#endif |
/// HotspotManager wraps NEHotspotConfigurationManager to make it easier for the |
/// view controller to use. |
/// |
/// The main benefit to this class is that it exports a `configurationNames` |
/// that (more-or-less) automatically updates to reflect the current persistent |
/// configurations. It also posts a notification, `configurationsDidChange`, |
/// when that list changes. |
/// |
/// The only weird part about this class is the `refresh()` method. See its doc |
/// comment for the details. |
class HotspotManager { |
init() { |
self.manager = NEHotspotConfigurationManager.shared |
self.startUpdate() |
} |
/// A reference to the underlying iOS object for managing configurations. |
private let manager: NEHotspotConfigurationManager |
/// A list of persistent SSIDs created by this app. |
private(set) var configurationNames: [String] = [] { |
didSet { |
NotificationCenter.default.post(name: HotspotManager.configurationsDidChange, object: self) |
} |
} |
/// Posted when `configurationNames` changes. |
static let configurationsDidChange = Notification.Name("HotspotManagerConfigurationsDidChange") |
/// True when this object is in the process of updating the |
/// `configurationNames` list. |
/// |
/// This prevents us from redundantly calling |
/// `getConfiguredSSIDs(completionHandler:)`, which works but is kinda |
/// pointless. |
private var isUpdating: Bool = false |
/// Starts the process of updating the `configurationNames` list. |
private func startUpdate() { |
guard !self.isUpdating else { |
return |
} |
NSLog("will start update") |
self.isUpdating = true |
self.manager.getConfiguredSSIDs { (names) in |
assert(Thread.isMainThread) |
if names != self.configurationNames { |
self.configurationNames = names |
} |
self.isUpdating = false |
NSLog("did end update") |
} |
} |
/// A simple data type that isolates our clients from |
/// NEHotspotConfiguration. |
struct Network { |
/// The name of the network to join. |
var ssid: String |
/// The password of that network, or nil if there’s no password. |
var password: String? |
/// True if the password is for WEP, that is, a string of hex digits. |
/// |
/// See the discussion of |
/// `NEHotspotConfiguration.init(ssid:passphrase:isWEP:)` for details. |
var isWEP: Bool |
/// Indicates whether the network is persistent (false) or one-off |
/// (true). |
var joinOnce: Bool |
/// Returns a `NEHotspotConfiguration` object derived from the |
/// properties of this value. |
fileprivate var configuration: NEHotspotConfiguration { |
let config: NEHotspotConfiguration |
if let password = password { |
config = NEHotspotConfiguration(ssid: self.ssid, passphrase: password, isWEP: self.isWEP) |
} else { |
config = NEHotspotConfiguration(ssid: self.ssid) |
} |
config.joinOnce = self.joinOnce |
return config |
} |
} |
/// Asks the user whether the device to join the specified network and, if |
/// they agree, joins that network. |
/// |
/// - Parameters: |
/// - network: The network to join. |
/// - completion: Called on the main queue once the join operation has |
/// completed. `error` will be nil if the join was successful, or an |
/// error otherwise. Note that some errors are not actual errors, most |
/// notably `NEHotspotConfigurationErrorAlreadyAssociated`. |
func add(network: Network, completion: @escaping (Error?) -> Void) { |
self.manager.apply(network.configuration) { (error) in |
assert(Thread.isMainThread) |
self.startUpdate() |
completion(error) |
} |
// Start an update now. The comments associated with `refresh()` |
// explain why I do this. |
self.startUpdate() |
} |
/// Removes a persistent configuration added via `add(network:completion:)`. |
/// |
/// - Parameters: |
/// - ssid: The SSID to remove. |
/// - completion: Called on the main queue once the remove operation has |
/// completed. `error` will be nil if the remove was successful, which |
/// is currently always the case. |
func remove(ssid: String, completion: @escaping (Error?) -> Void) { |
self.manager.removeConfiguration(forSSID: ssid) |
self.startUpdate() |
DispatchQueue.main.async { |
completion(nil) |
} |
} |
/// Forces a refresh of the configuration names list. |
/// |
/// I’d rather not have this but it turned out to be necessary. |
/// Specifically, when adding a configuration I found that there’s no |
/// obvious place to call `startUpdate()` in order to get a new |
/// configuration names list: |
/// |
/// * If I call it when `apply(:completionHandler:)` calls its completion |
/// handler, it doesn’t show up in the list for a long time while the |
/// Wi-Fi subsystem tries to find and join the network. |
/// |
/// * If I call it immediately after returing from |
/// `apply(:completionHandler:)`, it doesn’t show up because the user |
/// hasn’t confirmed that they want to allow this operation yet. |
/// |
/// I resolved this by having the view controller do a refresh when the app |
/// activates, which happens when the join alert is dismissed. It’s also a |
/// generally good idea for other reasons, for example, the user might have |
/// deleted the configuration from *Settings* > *Wi-Fi*. |
func refresh() { |
self.startUpdate() |
} |
} |
import SystemConfiguration.CaptiveNetwork |
extension HotspotManager { |
/// Gets the current SSID using the legacy Captive Network API. |
/// |
/// During debugging it’s helpful to know what network you’re on. The |
/// legacy Captive Network API has a way to do this, but it’s kinda tricky |
/// to call from Swift so we provide a wrapper here. |
/// |
/// - important: Using `CNCopyCurrentNetworkInfo` in production code is not |
/// a good idea. If you do use it, make sure you code defensively so that |
/// your app behaves in a reasonable fashion if it fails. This code, for |
/// example, carefully checks for failures and returns nil if anything |
/// goes wrong. |
var currentSSID: String? { |
guard let interfaces = CNCopySupportedInterfaces() as? [String], |
let interface = interfaces.first else { |
return nil |
} |
guard let interfaceDict = CNCopyCurrentNetworkInfo(interface as NSString) as? [String:Any] else { |
return nil |
} |
guard let ssid = interfaceDict[kCNNetworkInfoKeySSID as String] as? String else { |
return nil |
} |
return ssid |
} |
} |
Copyright © 2018 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2018-05-10