Use a Switch on each tunnel cell.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jeroen Leenarts 2018-08-21 22:27:26 +02:00
parent dc1a3865af
commit e93e562f38
3 changed files with 187 additions and 13 deletions

View File

@ -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>

View File

@ -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

View File

@ -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 {}