Use a Switch on each tunnel cell.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
dc1a3865af
commit
e93e562f38
|
@ -40,22 +40,47 @@
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="TunnelTableViewCell" textLabel="hzX-lc-GyT" style="IBUITableViewCellStyleDefault" id="fM3-cC-KPN" customClass="TunnelTableViewCell" customModule="WireGuard" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="TunnelTableViewCell" rowHeight="44" id="fM3-cC-KPN" customClass="TunnelTableViewCell" customModule="WireGuard" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fM3-cC-KPN" id="Rv6-XK-aK2" customClass="TunnelTableViewCell" customModule="WireGuard" customModuleProvider="target">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fM3-cC-KPN" id="Rv6-XK-aK2" customClass="TunnelTableViewCell" customModule="WireGuard" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="307" height="43.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hzX-lc-GyT">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ySw-bB-dRd">
|
||||||
<rect key="frame" x="16" y="0.0" width="291" height="43.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<subviews>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5FX-35-lr8">
|
||||||
<nil key="textColor"/>
|
<rect key="frame" x="0.0" y="11.5" width="42" height="20.5"/>
|
||||||
<nil key="highlightedColor"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
</label>
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="uiR-NX-H3A">
|
||||||
|
<rect key="frame" x="355" y="0.0" width="20" height="43.5"/>
|
||||||
|
</activityIndicatorView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="uiR-NX-H3A" secondAttribute="trailing" id="HOh-vb-l0J"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="uiR-NX-H3A" secondAttribute="bottom" id="I0r-JW-x5J"/>
|
||||||
|
<constraint firstItem="5FX-35-lr8" firstAttribute="centerY" secondItem="ySw-bB-dRd" secondAttribute="centerY" id="I2J-iL-Kjg"/>
|
||||||
|
<constraint firstItem="uiR-NX-H3A" firstAttribute="top" secondItem="ySw-bB-dRd" secondAttribute="top" id="Rcy-gh-u8g"/>
|
||||||
|
<constraint firstItem="5FX-35-lr8" firstAttribute="leading" secondItem="ySw-bB-dRd" secondAttribute="leading" id="vna-fz-04u"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="ySw-bB-dRd" secondAttribute="trailing" id="9ZF-jL-Nsl"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="ySw-bB-dRd" secondAttribute="bottom" id="JbD-Bb-8TH"/>
|
||||||
|
<constraint firstItem="ySw-bB-dRd" firstAttribute="top" secondItem="Rv6-XK-aK2" secondAttribute="top" id="sgF-rN-vRx"/>
|
||||||
|
<constraint firstItem="ySw-bB-dRd" firstAttribute="leading" secondItem="Rv6-XK-aK2" secondAttribute="leading" id="vnM-b2-F6M"/>
|
||||||
|
</constraints>
|
||||||
</tableViewCellContentView>
|
</tableViewCellContentView>
|
||||||
|
<connections>
|
||||||
|
<outlet property="activityIndicator" destination="uiR-NX-H3A" id="Etf-NG-yCj"/>
|
||||||
|
<outlet property="tunnelTitleLabel" destination="5FX-35-lr8" id="Vav-5C-01N"/>
|
||||||
|
</connections>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</prototypes>
|
</prototypes>
|
||||||
<connections>
|
<connections>
|
||||||
|
|
|
@ -95,12 +95,31 @@ class AppCoordinator: RootViewCoordinator {
|
||||||
// MARK: - NEVPNManager handling
|
// MARK: - NEVPNManager handling
|
||||||
|
|
||||||
@objc private func VPNStatusDidChange(notification: NSNotification) {
|
@objc private func VPNStatusDidChange(notification: NSNotification) {
|
||||||
//TODO implement
|
|
||||||
guard let session = notification.object as? NETunnelProviderSession else {
|
guard let session = notification.object as? NETunnelProviderSession else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
os_log("VPNStatusDidChange: %{public}@", log: Log.general, type: .debug, description(for: session.status))
|
guard let prot = session.manager.protocolConfiguration as? NETunnelProviderProtocol else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let changedTunnelIdentifier = prot.providerConfiguration?[PCKeys.tunnelIdentifier.rawValue] as? String else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
providerManagers?.first(where: { (manager) -> Bool in
|
||||||
|
guard let prot = manager.protocolConfiguration as? NETunnelProviderProtocol else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard let candidateTunnelIdentifier = prot.providerConfiguration?[PCKeys.tunnelIdentifier.rawValue] as? String else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return changedTunnelIdentifier == candidateTunnelIdentifier
|
||||||
|
|
||||||
|
})?.loadFromPreferences(completionHandler: { [weak self] (_) in
|
||||||
|
self?.tunnelsTableViewController.updateStatus(for: changedTunnelIdentifier)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public func showError(_ error: Error) {
|
public func showError(_ error: Error) {
|
||||||
|
@ -132,6 +151,12 @@ class AppCoordinator: RootViewCoordinator {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppCoordinator: TunnelsTableViewControllerDelegate {
|
extension AppCoordinator: TunnelsTableViewControllerDelegate {
|
||||||
|
|
||||||
|
func status(for tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) -> NEVPNStatus {
|
||||||
|
let session = self.providerManager(for: tunnel)?.connection as? NETunnelProviderSession
|
||||||
|
return session?.status ?? .invalid
|
||||||
|
}
|
||||||
|
|
||||||
func addProvider(tunnelsTableViewController: TunnelsTableViewController) {
|
func addProvider(tunnelsTableViewController: TunnelsTableViewController) {
|
||||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||||
actionSheet.addAction(UIAlertAction(title: "Add Manually", style: .default) { [unowned self] _ in
|
actionSheet.addAction(UIAlertAction(title: "Add Manually", style: .default) { [unowned self] _ in
|
||||||
|
@ -166,7 +191,24 @@ extension AppCoordinator: TunnelsTableViewControllerDelegate {
|
||||||
switch manager.connection.status {
|
switch manager.connection.status {
|
||||||
case .invalid, .disconnected:
|
case .invalid, .disconnected:
|
||||||
self.connect(tunnel: tunnel)
|
self.connect(tunnel: tunnel)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.connection.status == .invalid {
|
||||||
|
manager.loadFromPreferences { (_) in
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) {
|
||||||
|
let manager = self.providerManager(for: tunnel)!
|
||||||
|
let block = {
|
||||||
|
switch manager.connection.status {
|
||||||
case .connected, .connecting:
|
case .connected, .connecting:
|
||||||
self.disconnect(tunnel: tunnel)
|
self.disconnect(tunnel: tunnel)
|
||||||
default:
|
default:
|
||||||
|
@ -253,7 +295,7 @@ extension AppCoordinator: TunnelsTableViewControllerDelegate {
|
||||||
guard let prot = $0.protocolConfiguration as? NETunnelProviderProtocol else {
|
guard let prot = $0.protocolConfiguration as? NETunnelProviderProtocol else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
guard let tunnelIdentifier = prot.providerConfiguration?["tunnelIdentifier"] as? String else {
|
guard let tunnelIdentifier = prot.providerConfiguration?[PCKeys.tunnelIdentifier.rawValue] as? String else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return tunnelIdentifier == tunnel.tunnelIdentifier
|
return tunnelIdentifier == tunnel.tunnelIdentifier
|
||||||
|
|
|
@ -10,12 +10,15 @@ import UIKit
|
||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import BNRCoreDataStack
|
import BNRCoreDataStack
|
||||||
|
import NetworkExtension
|
||||||
|
|
||||||
protocol TunnelsTableViewControllerDelegate: class {
|
protocol TunnelsTableViewControllerDelegate: class {
|
||||||
func addProvider(tunnelsTableViewController: TunnelsTableViewController)
|
func addProvider(tunnelsTableViewController: TunnelsTableViewController)
|
||||||
func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
|
func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
|
||||||
|
func disconnect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
|
||||||
func configure(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
|
func configure(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
|
||||||
func delete(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
|
func delete(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController)
|
||||||
|
func status(for tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) -> NEVPNStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
class TunnelsTableViewController: UITableViewController {
|
class TunnelsTableViewController: UITableViewController {
|
||||||
|
@ -33,6 +36,17 @@ class TunnelsTableViewController: UITableViewController {
|
||||||
return frc
|
return frc
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
public func updateStatus(for tunnelIdentifier: String) {
|
||||||
|
viewContext.perform {
|
||||||
|
let tunnel = try? Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier))
|
||||||
|
if let tunnel = tunnel {
|
||||||
|
if let indexPath = self.fetchedResultsController.indexPathForObject(tunnel!) {
|
||||||
|
self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lazy var frcDelegate: TunnelFetchedResultsControllerDelegate = { // swiftlint:disable:this weak_delegate
|
private lazy var frcDelegate: TunnelFetchedResultsControllerDelegate = { // swiftlint:disable:this weak_delegate
|
||||||
return TunnelFetchedResultsControllerDelegate(tableView: self.tableView)
|
return TunnelFetchedResultsControllerDelegate(tableView: self.tableView)
|
||||||
}()
|
}()
|
||||||
|
@ -63,6 +77,7 @@ class TunnelsTableViewController: UITableViewController {
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell = tableView.dequeueReusableCell(type: TunnelTableViewCell.self, for: indexPath)
|
let cell = tableView.dequeueReusableCell(type: TunnelTableViewCell.self, for: indexPath)
|
||||||
|
cell.delegate = self
|
||||||
|
|
||||||
guard let sections = fetchedResultsController.sections else {
|
guard let sections = fetchedResultsController.sections else {
|
||||||
fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
|
fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
|
||||||
|
@ -71,7 +86,7 @@ class TunnelsTableViewController: UITableViewController {
|
||||||
let section = sections[indexPath.section]
|
let section = sections[indexPath.section]
|
||||||
let tunnel = section.objects[indexPath.row]
|
let tunnel = section.objects[indexPath.row]
|
||||||
|
|
||||||
cell.textLabel?.text = tunnel.title
|
cell.configure(tunnel: tunnel, status: delegate?.status(for: tunnel, tunnelsTableViewController: self) ?? .invalid)
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
@ -120,6 +135,23 @@ class TunnelsTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TunnelsTableViewController: TunnelTableViewCellDelegate {
|
||||||
|
func connect(tunnelIdentifier: String) {
|
||||||
|
let tunnel = try? Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier))
|
||||||
|
if let tunnel = tunnel {
|
||||||
|
self.delegate?.connect(tunnel: tunnel!, tunnelsTableViewController: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(tunnelIdentifier: String) {
|
||||||
|
let tunnel = try? Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier))
|
||||||
|
if let tunnel = tunnel {
|
||||||
|
self.delegate?.disconnect(tunnel: tunnel!, tunnelsTableViewController: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
extension TunnelsTableViewController: Identifyable {}
|
extension TunnelsTableViewController: Identifyable {}
|
||||||
|
|
||||||
class TunnelFetchedResultsControllerDelegate: NSObject, FetchedResultsControllerDelegate {
|
class TunnelFetchedResultsControllerDelegate: NSObject, FetchedResultsControllerDelegate {
|
||||||
|
@ -172,8 +204,83 @@ class TunnelFetchedResultsControllerDelegate: NSObject, FetchedResultsController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol TunnelTableViewCellDelegate: class {
|
||||||
|
func connect(tunnelIdentifier: String)
|
||||||
|
func disconnect(tunnelIdentifier: String)
|
||||||
|
}
|
||||||
|
|
||||||
class TunnelTableViewCell: UITableViewCell {
|
class TunnelTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
@IBOutlet weak var tunnelTitleLabel: UILabel!
|
||||||
|
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
|
||||||
|
|
||||||
|
weak var delegate: TunnelTableViewCellDelegate?
|
||||||
|
private var tunnelIdentifier: String?
|
||||||
|
|
||||||
|
let tunnelSwitch = UISwitch(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
|
||||||
|
|
||||||
|
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
tunnelSwitch.addTarget(self, action: #selector(tunnelSwitchChanged(_:)), for: .valueChanged)
|
||||||
|
self.accessoryView = tunnelSwitch
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
tunnelSwitch.addTarget(self, action: #selector(tunnelSwitchChanged(_:)), for: .valueChanged)
|
||||||
|
accessoryView = tunnelSwitch
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func tunnelSwitchChanged(_ sender: Any) {
|
||||||
|
tunnelSwitch.isUserInteractionEnabled = false
|
||||||
|
guard let tunnelIdentifier = tunnelIdentifier else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tunnelSwitch.isOn {
|
||||||
|
delegate?.connect(tunnelIdentifier: tunnelIdentifier)
|
||||||
|
} else {
|
||||||
|
delegate?.disconnect(tunnelIdentifier: tunnelIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(tunnel: Tunnel, status: NEVPNStatus) {
|
||||||
|
self.tunnelTitleLabel?.text = tunnel.title
|
||||||
|
tunnelIdentifier = tunnel.tunnelIdentifier
|
||||||
|
switch status {
|
||||||
|
case .connected:
|
||||||
|
activityIndicator.stopAnimating()
|
||||||
|
tunnelSwitch.isOn = true
|
||||||
|
tunnelSwitch.isEnabled = true
|
||||||
|
tunnelSwitch.onTintColor = UIColor.green
|
||||||
|
case .connecting:
|
||||||
|
activityIndicator.startAnimating()
|
||||||
|
tunnelSwitch.isOn = true
|
||||||
|
tunnelSwitch.isEnabled = false
|
||||||
|
tunnelSwitch.onTintColor = UIColor.yellow
|
||||||
|
case .disconnected:
|
||||||
|
activityIndicator.stopAnimating()
|
||||||
|
tunnelSwitch.isOn = false
|
||||||
|
tunnelSwitch.isEnabled = true
|
||||||
|
tunnelSwitch.onTintColor = UIColor.green
|
||||||
|
case .disconnecting:
|
||||||
|
activityIndicator.startAnimating()
|
||||||
|
tunnelSwitch.isOn = false
|
||||||
|
tunnelSwitch.isEnabled = true
|
||||||
|
tunnelSwitch.onTintColor = UIColor.green
|
||||||
|
case .invalid:
|
||||||
|
activityIndicator.stopAnimating()
|
||||||
|
tunnelSwitch.isEnabled = false
|
||||||
|
tunnelSwitch.isUserInteractionEnabled = false
|
||||||
|
tunnelSwitch.onTintColor = UIColor.gray
|
||||||
|
case .reasserting:
|
||||||
|
activityIndicator.startAnimating()
|
||||||
|
tunnelSwitch.isEnabled = true
|
||||||
|
tunnelSwitch.isUserInteractionEnabled = false
|
||||||
|
tunnelSwitch.onTintColor = UIColor.yellow
|
||||||
|
}
|
||||||
|
tunnelSwitch.isUserInteractionEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TunnelTableViewCell: Identifyable {}
|
extension TunnelTableViewCell: Identifyable {}
|
||||||
|
|
Loading…
Reference in New Issue