diff options
-rw-r--r-- | WireGuard/Base.lproj/Main.storyboard | 43 | ||||
-rw-r--r-- | WireGuard/Coordinators/AppCoordinator.swift | 48 | ||||
-rw-r--r-- | WireGuard/ViewControllers/TunnelsTableViewController.swift | 109 |
3 files changed, 187 insertions, 13 deletions
diff --git a/WireGuard/Base.lproj/Main.storyboard b/WireGuard/Base.lproj/Main.storyboard index 80639f1..abadee8 100644 --- a/WireGuard/Base.lproj/Main.storyboard +++ b/WireGuard/Base.lproj/Main.storyboard @@ -40,22 +40,47 @@ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <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"/> <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"> - <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"/> <subviews> - <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hzX-lc-GyT"> - <rect key="frame" x="16" y="0.0" width="291" height="43.5"/> - <autoresizingMask key="autoresizingMask"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <nil key="textColor"/> - <nil key="highlightedColor"/> - </label> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ySw-bB-dRd"> + <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> + <subviews> + <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"> + <rect key="frame" x="0.0" y="11.5" width="42" height="20.5"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <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> + <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> + <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> </prototypes> <connections> diff --git a/WireGuard/Coordinators/AppCoordinator.swift b/WireGuard/Coordinators/AppCoordinator.swift index f57a9d5..bf42aa8 100644 --- a/WireGuard/Coordinators/AppCoordinator.swift +++ b/WireGuard/Coordinators/AppCoordinator.swift @@ -95,12 +95,31 @@ class AppCoordinator: RootViewCoordinator { // MARK: - NEVPNManager handling @objc private func VPNStatusDidChange(notification: NSNotification) { - //TODO implement guard let session = notification.object as? NETunnelProviderSession else { 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) { @@ -132,6 +151,12 @@ class AppCoordinator: RootViewCoordinator { } 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) { let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) actionSheet.addAction(UIAlertAction(title: "Add Manually", style: .default) { [unowned self] _ in @@ -166,7 +191,24 @@ extension AppCoordinator: TunnelsTableViewControllerDelegate { switch manager.connection.status { case .invalid, .disconnected: 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: self.disconnect(tunnel: tunnel) default: @@ -253,7 +295,7 @@ extension AppCoordinator: TunnelsTableViewControllerDelegate { guard let prot = $0.protocolConfiguration as? NETunnelProviderProtocol else { 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 tunnelIdentifier == tunnel.tunnelIdentifier diff --git a/WireGuard/ViewControllers/TunnelsTableViewController.swift b/WireGuard/ViewControllers/TunnelsTableViewController.swift index a96afa5..266017e 100644 --- a/WireGuard/ViewControllers/TunnelsTableViewController.swift +++ b/WireGuard/ViewControllers/TunnelsTableViewController.swift @@ -10,12 +10,15 @@ import UIKit import CoreData import BNRCoreDataStack +import NetworkExtension protocol TunnelsTableViewControllerDelegate: class { func addProvider(tunnelsTableViewController: TunnelsTableViewController) func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) + func disconnect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) func configure(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) func delete(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) + func status(for tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) -> NEVPNStatus } class TunnelsTableViewController: UITableViewController { @@ -33,6 +36,17 @@ class TunnelsTableViewController: UITableViewController { 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 return TunnelFetchedResultsControllerDelegate(tableView: self.tableView) }() @@ -63,6 +77,7 @@ class TunnelsTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(type: TunnelTableViewCell.self, for: indexPath) + cell.delegate = self guard let sections = fetchedResultsController.sections else { fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil") @@ -71,7 +86,7 @@ class TunnelsTableViewController: UITableViewController { let section = sections[indexPath.section] 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 } @@ -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 {} 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 { + @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 {} |