Reorganized ViewControllers (split out UIViews and UITableViewCells into their own classes)

All swiftlint warnings except one fixed up

Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
This commit is contained in:
Eric Kuck 2018-12-13 12:58:50 -06:00
parent 7323a00612
commit 05d750539b
31 changed files with 1363 additions and 722 deletions

View File

@ -10,3 +10,5 @@ file_length:
cyclomatic_complexity: cyclomatic_complexity:
warning: 10 warning: 10
error: 25 error: 25
function_body_length:
warning: 45

View File

@ -8,6 +8,19 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */; }; 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */; };
5F45418A21C2D45B00994C13 /* TunnelEditKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418921C2D45B00994C13 /* TunnelEditKeyValueCell.swift */; };
5F45418C21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418B21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift */; };
5F45418E21C2D51100994C13 /* TunnelEditButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418D21C2D51100994C13 /* TunnelEditButtonCell.swift */; };
5F45419021C2D53800994C13 /* TunnelEditSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418F21C2D53800994C13 /* TunnelEditSwitchCell.swift */; };
5F45419221C2D55800994C13 /* TunnelEditSectionListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419121C2D55800994C13 /* TunnelEditSectionListCell.swift */; };
5F45419421C2D5C500994C13 /* TunnelDetailActivateOnDemandCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419321C2D5C500994C13 /* TunnelDetailActivateOnDemandCell.swift */; };
5F45419621C2D5DB00994C13 /* TunnelDetailButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419521C2D5DB00994C13 /* TunnelDetailButtonCell.swift */; };
5F45419821C2D60500994C13 /* TunnelDetailKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419721C2D60500994C13 /* TunnelDetailKeyValueCell.swift */; };
5F45419A21C2D61D00994C13 /* TunnelDetailStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419921C2D61D00994C13 /* TunnelDetailStatusCell.swift */; };
5F45419C21C2D64800994C13 /* SettingsButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419B21C2D64800994C13 /* SettingsButtonCell.swift */; };
5F45419E21C2D66400994C13 /* SettingsKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419D21C2D66400994C13 /* SettingsKeyValueCell.swift */; };
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419F21C2D6B700994C13 /* TunnelListCell.swift */; };
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */; };
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; }; 6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; }; 6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; };
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; }; 6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
@ -96,6 +109,19 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Reuse.swift"; sourceTree = "<group>"; }; 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Reuse.swift"; sourceTree = "<group>"; };
5F45418921C2D45B00994C13 /* TunnelEditKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditKeyValueCell.swift; sourceTree = "<group>"; };
5F45418B21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditReadOnlyKeyValueCell.swift; sourceTree = "<group>"; };
5F45418D21C2D51100994C13 /* TunnelEditButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditButtonCell.swift; sourceTree = "<group>"; };
5F45418F21C2D53800994C13 /* TunnelEditSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditSwitchCell.swift; sourceTree = "<group>"; };
5F45419121C2D55800994C13 /* TunnelEditSectionListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditSectionListCell.swift; sourceTree = "<group>"; };
5F45419321C2D5C500994C13 /* TunnelDetailActivateOnDemandCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailActivateOnDemandCell.swift; sourceTree = "<group>"; };
5F45419521C2D5DB00994C13 /* TunnelDetailButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailButtonCell.swift; sourceTree = "<group>"; };
5F45419721C2D60500994C13 /* TunnelDetailKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailKeyValueCell.swift; sourceTree = "<group>"; };
5F45419921C2D61D00994C13 /* TunnelDetailStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailStatusCell.swift; sourceTree = "<group>"; };
5F45419B21C2D64800994C13 /* SettingsButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButtonCell.swift; sourceTree = "<group>"; };
5F45419D21C2D66400994C13 /* SettingsKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyValueCell.swift; sourceTree = "<group>"; };
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListCell.swift; sourceTree = "<group>"; };
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedTextButton.swift; sourceTree = "<group>"; };
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; }; 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = "<group>"; }; 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = "<group>"; };
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; }; 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; };
@ -176,6 +202,51 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
5F45418521C2C6AB00994C13 /* Settings */ = {
isa = PBXGroup;
children = (
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
5F45419B21C2D64800994C13 /* SettingsButtonCell.swift */,
5F45419D21C2D66400994C13 /* SettingsKeyValueCell.swift */,
);
path = Settings;
sourceTree = "<group>";
};
5F45418621C2C6B400994C13 /* EditTunnel */ = {
isa = PBXGroup;
children = (
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
5F45418921C2D45B00994C13 /* TunnelEditKeyValueCell.swift */,
5F45418B21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift */,
5F45418D21C2D51100994C13 /* TunnelEditButtonCell.swift */,
5F45418F21C2D53800994C13 /* TunnelEditSwitchCell.swift */,
5F45419121C2D55800994C13 /* TunnelEditSectionListCell.swift */,
);
path = EditTunnel;
sourceTree = "<group>";
};
5F45418721C2C6C100994C13 /* TunnelDetail */ = {
isa = PBXGroup;
children = (
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
5F45419321C2D5C500994C13 /* TunnelDetailActivateOnDemandCell.swift */,
5F45419521C2D5DB00994C13 /* TunnelDetailButtonCell.swift */,
5F45419721C2D60500994C13 /* TunnelDetailKeyValueCell.swift */,
5F45419921C2D61D00994C13 /* TunnelDetailStatusCell.swift */,
);
path = TunnelDetail;
sourceTree = "<group>";
};
5F45418821C2C6CC00994C13 /* TunnelList */ = {
isa = PBXGroup;
children = (
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */,
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */,
);
path = TunnelList;
sourceTree = "<group>";
};
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */ = { 6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -232,14 +303,14 @@
6F7774DE217181B1006A79B3 /* iOS */ = { 6F7774DE217181B1006A79B3 /* iOS */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5F45418821C2C6CC00994C13 /* TunnelList */,
5F45418721C2C6C100994C13 /* TunnelDetail */,
5F45418621C2C6B400994C13 /* EditTunnel */,
5F45418521C2C6AB00994C13 /* Settings */,
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */, 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */, 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
6F7774E0217181B1006A79B3 /* AppDelegate.swift */, 6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */, 6F7774DF217181B1006A79B3 /* MainViewController.swift */,
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */, 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */,
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */, 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */,
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */, 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */,
@ -621,33 +692,47 @@
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */, 6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */, 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
5F45419221C2D55800994C13 /* TunnelEditSectionListCell.swift in Sources */,
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */, 6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
5F45418E21C2D51100994C13 /* TunnelEditButtonCell.swift in Sources */,
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */, 6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */, 6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
5F45418A21C2D45B00994C13 /* TunnelEditKeyValueCell.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */, 6F6899A62180447E0012E523 /* x25519.c in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */, 6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */, 6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */, 6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
5F45419C21C2D64800994C13 /* SettingsButtonCell.swift in Sources */,
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */, 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
5F45419821C2D60500994C13 /* TunnelDetailKeyValueCell.swift in Sources */,
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */, 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */, 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
5F45418C21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift in Sources */,
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */, 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */, 6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
5F45419A21C2D61D00994C13 /* TunnelDetailStatusCell.swift in Sources */,
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
5F45419621C2D5DB00994C13 /* TunnelDetailButtonCell.swift in Sources */,
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */, 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */, 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
5F45419E21C2D66400994C13 /* SettingsKeyValueCell.swift in Sources */,
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */, 6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */, 6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */, 6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */, 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */, 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
5F45419021C2D53800994C13 /* TunnelEditSwitchCell.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */, 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */, 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */, 6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
5F45419421C2D5C500994C13 /* TunnelDetailActivateOnDemandCell.swift in Sources */,
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -20,6 +20,7 @@ class WgQuickConfigFileParser {
case invalidPeer case invalidPeer
} }
//swiftlint:disable:next cyclomatic_complexity function_body_length
static func parse(_ text: String, name: String) throws -> TunnelConfiguration { static func parse(_ text: String, name: String) throws -> TunnelConfiguration {
assert(!name.isEmpty) assert(!name.isEmpty)
@ -95,6 +96,7 @@ class WgQuickConfigFileParser {
} }
} }
//swiftlint:disable:next cyclomatic_complexity
private static func collate(interfaceAttributes attributes: [String: String], name: String) -> InterfaceConfiguration? { private static func collate(interfaceAttributes attributes: [String: String], name: String) -> InterfaceConfiguration? {
// required wg fields // required wg fields
guard let privateKeyString = attributes["privatekey"] else { return nil } guard let privateKeyString = attributes["privatekey"] else { return nil }
@ -131,6 +133,7 @@ class WgQuickConfigFileParser {
return interface return interface
} }
//swiftlint:disable:next cyclomatic_complexity
private static func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? { private static func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
// required wg fields // required wg fields
guard let publicKeyString = attributes["publickey"] else { return nil } guard let publicKeyString = attributes["publickey"] else { return nil }

View File

@ -27,7 +27,7 @@ enum TunnelsManagerActivationAttemptError: WireGuardAppError {
case failedWhileLoading // reloading config throwed case failedWhileLoading // reloading config throwed
case failedBecauseOfTooManyErrors // recursion limit reached case failedBecauseOfTooManyErrors // recursion limit reached
func alertText() -> AlertText { var alertText: AlertText {
switch self { switch self {
case .tunnelIsNotInactive: case .tunnelIsNotInactive:
return ("Activation failure", "The tunnel is already active or in the process of being activated") return ("Activation failure", "The tunnel is already active or in the process of being activated")
@ -41,13 +41,12 @@ enum TunnelsManagerActivationAttemptError: WireGuardAppError {
enum TunnelsManagerActivationError: WireGuardAppError { enum TunnelsManagerActivationError: WireGuardAppError {
case activationFailed case activationFailed
func alertText() -> AlertText { var alertText: AlertText {
return ("Activation failure", "The tunnel could not be activated. Please ensure you are connected to the Internet.") return ("Activation failure", "The tunnel could not be activated. Please ensure you are connected to the Internet.")
} }
} }
enum TunnelsManagerError: WireGuardAppError { enum TunnelsManagerError: WireGuardAppError {
// Tunnels list management
case tunnelNameEmpty case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName case tunnelAlreadyExistsWithThatName
case systemErrorOnListingTunnels case systemErrorOnListingTunnels
@ -55,7 +54,7 @@ enum TunnelsManagerError: WireGuardAppError {
case systemErrorOnModifyTunnel case systemErrorOnModifyTunnel
case systemErrorOnRemoveTunnel case systemErrorOnRemoveTunnel
func alertText() -> AlertText { var alertText: AlertText {
switch self { switch self {
case .tunnelNameEmpty: case .tunnelNameEmpty:
return ("No name provided", "Can't create tunnel with an empty name") return ("No name provided", "Can't create tunnel with an empty name")
@ -87,7 +86,6 @@ class TunnelsManager {
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) { static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
// NETunnelProviderManager APIs don't work on the simulator
completionHandler(.success(TunnelsManager(tunnelProviders: []))) completionHandler(.success(TunnelsManager(tunnelProviders: [])))
#else #else
NETunnelProviderManager.loadAllFromPreferences { managers, error in NETunnelProviderManager.loadAllFromPreferences { managers, error in
@ -101,9 +99,7 @@ class TunnelsManager {
#endif #endif
} }
func add(tunnelConfiguration: TunnelConfiguration, func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting,
completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
let tunnelName = tunnelConfiguration.interface.name let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty)) completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
@ -128,13 +124,14 @@ class TunnelsManager {
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel)) completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel))
return return
} }
if let self = self {
let tunnel = TunnelContainer(tunnel: tunnelProviderManager) guard let self = self else { return }
self.tunnels.append(tunnel)
self.tunnels.sort { $0.name < $1.name } let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!) self.tunnels.append(tunnel)
completionHandler(.success(tunnel)) self.tunnels.sort { $0.name < $1.name }
} self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
completionHandler(.success(tunnel))
} }
} }
@ -155,8 +152,7 @@ class TunnelsManager {
} }
} }
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty) completionHandler(TunnelsManagerError.tunnelNameEmpty)
@ -185,36 +181,37 @@ class TunnelsManager {
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel) completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
return return
} }
if let self = self {
if isNameChanged { guard let self = self else { return }
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.sort { $0.name < $1.name } if isNameChanged {
let newIndex = self.tunnels.firstIndex(of: tunnel)! let oldIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex) self.tunnels.sort { $0.name < $1.name }
} let newIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!) self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
}
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting { self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
// Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.status = .restarting if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel() // Turn off the tunnel, and then turn it back on, so the changes are made effective
} tunnel.status = .restarting
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
if isActivatingOnDemand { }
// Reload tunnel after saving.
// Without this, the tunnel stopes getting updates on the tunnel status from iOS. if isActivatingOnDemand {
tunnelProviderManager.loadFromPreferences { error in // Reload tunnel after saving.
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled // Without this, the tunnel stopes getting updates on the tunnel status from iOS.
guard error == nil else { tunnelProviderManager.loadFromPreferences { error in
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)") tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel) guard error == nil else {
return wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
} completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
completionHandler(nil) return
} }
} else {
completionHandler(nil) completionHandler(nil)
} }
} else {
completionHandler(nil)
} }
} }
} }
@ -274,9 +271,7 @@ class TunnelsManager {
func startDeactivation(of tunnel: TunnelContainer) { func startDeactivation(of tunnel: TunnelContainer) {
tunnel.isAttemptingActivation = false tunnel.isAttemptingActivation = false
if tunnel.status == .inactive || tunnel.status == .deactivating { guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
return
}
tunnel.startDeactivation() tunnel.startDeactivation()
} }
@ -288,10 +283,10 @@ class TunnelsManager {
guard statusObservationToken == nil else { return } guard statusObservationToken == nil else { return }
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
guard let self = self else { return } guard let self = self,
guard let session = statusChangeNotification.object as? NETunnelProviderSession else { return } let session = statusChangeNotification.object as? NETunnelProviderSession,
guard let tunnelProvider = session.manager as? NETunnelProviderManager else { return } let tunnelProvider = session.manager as? NETunnelProviderManager,
guard let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return } let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'") wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
@ -339,7 +334,7 @@ class TunnelContainer: NSObject {
@objc dynamic var isActivateOnDemandEnabled: Bool @objc dynamic var isActivateOnDemandEnabled: Bool
var isAttemptingActivation: Bool = false var isAttemptingActivation = false
fileprivate let tunnelProvider: NETunnelProviderManager fileprivate let tunnelProvider: NETunnelProviderManager
private var lastTunnelConnectionStatus: NEVPNStatus? private var lastTunnelConnectionStatus: NEVPNStatus?
@ -375,10 +370,8 @@ class TunnelContainer: NSObject {
startActivation(tunnelConfiguration: tunnelConfiguration, activationDelegate: activationDelegate) startActivation(tunnelConfiguration: tunnelConfiguration, activationDelegate: activationDelegate)
} }
fileprivate func startActivation(recursionCount: UInt = 0, //swiftlint:disable:next function_body_length
lastError: Error? = nil, fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, tunnelConfiguration: TunnelConfiguration, activationDelegate: TunnelsManagerActivationDelegate?) {
tunnelConfiguration: TunnelConfiguration,
activationDelegate: TunnelsManagerActivationDelegate?) {
if recursionCount >= 8 { if recursionCount >= 8 {
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)") wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors) activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors)
@ -457,7 +450,6 @@ class TunnelContainer: NSObject {
case active case active
case deactivating case deactivating
case reasserting // Not a possible state at present case reasserting // Not a possible state at present
case restarting // Restarting tunnel (done after saving modifications to an active tunnel) case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
case waiting // Waiting for another tunnel to be brought down case waiting // Waiting for another tunnel to be brought down

View File

@ -95,6 +95,7 @@ class TunnelViewModel {
} }
} }
//swiftlint:disable:next cyclomatic_complexity function_body_length
func save() -> SaveResult<InterfaceConfiguration> { func save() -> SaveResult<InterfaceConfiguration> {
if let validatedConfiguration = validatedConfiguration { if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved // It's already validated and saved
@ -236,6 +237,7 @@ class TunnelViewModel {
updateExcludePrivateIPsFieldState() updateExcludePrivateIPsFieldState()
} }
//swiftlint:disable:next cyclomatic_complexity
func save() -> SaveResult<PeerConfiguration> { func save() -> SaveResult<PeerConfiguration> {
if let validatedConfiguration = validatedConfiguration { if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved // It's already validated and saved

View File

@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelEditButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == UIColor.red }
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
hasDestructiveAction = false
}
}

View File

@ -0,0 +1,158 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelEditKeyValueCell: UITableViewCell {
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueTextField.text ?? "" }
set(value) { valueTextField.text = value }
}
var placeholderText: String {
get { return valueTextField.placeholder ?? "" }
set(value) { valueTextField.placeholder = value }
}
var isValueValid = true {
didSet {
if isValueValid {
keyLabel.textColor = .black
} else {
keyLabel.textColor = .red
}
}
}
var keyboardType: UIKeyboardType {
get { return valueTextField.keyboardType }
set(value) { valueTextField.keyboardType = value }
}
var onValueChanged: ((String) -> Void)?
var onValueBeingEdited: ((String) -> Void)?
let keyLabel: UILabel = {
let keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
return keyLabel
}()
let valueTextField: UITextField = {
let valueTextField = UITextField()
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
valueTextField.adjustsFontForContentSizeCategory = true
valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no
return valueTextField
}()
var isStackedHorizontally = false
var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]()
private var textFieldValueOnBeginEditing: String = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5),
widthRatioConstraint
])
contentView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 0.5)
])
valueTextField.delegate = self
configureForContentSize()
}
func configureForContentSize() {
var constraints = [NSLayoutConstraint]()
if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Stack vertically
if !isStackedVertically {
constraints = [
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueTextField.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
]
isStackedVertically = true
isStackedHorizontally = false
}
} else {
// Stack horizontally
if !isStackedHorizontally {
constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueTextField.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]
isStackedHorizontally = true
isStackedVertically = false
}
}
if !constraints.isEmpty {
NSLayoutConstraint.deactivate(self.contentSizeBasedConstraints)
NSLayoutConstraint.activate(constraints)
self.contentSizeBasedConstraints = constraints
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
placeholderText = ""
isValueValid = true
keyboardType = .default
onValueChanged = nil
onValueBeingEdited = nil
configureForContentSize()
}
}
extension TunnelEditKeyValueCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textFieldValueOnBeginEditing = textField.text ?? ""
isValueValid = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing)
guard isModified else { return }
if let onValueChanged = onValueChanged {
onValueChanged(textField.text ?? "")
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let onValueBeingEdited = onValueBeingEdited {
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
onValueBeingEdited(modifiedText)
}
return true
}
}

View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelEditReadOnlyKeyValueCell: CopyableLabelTableViewCell {
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueLabel.text }
set(value) { valueLabel.text = value }
}
let keyLabel: UILabel
let valueLabel: ScrollableLabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
valueLabel = ScrollableLabel()
valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
valueLabel.label.adjustsFontForContentSizeCategory = true
super.init(style: style, reuseIdentifier: reuseIdentifier)
keyLabel.textColor = UIColor.gray
valueLabel.textColor = UIColor.gray
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// In case the key doesn't fit into 0.4 * width,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
widthRatioConstraint
])
contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
])
}
override var textToCopy: String? {
return self.valueLabel.text
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}

View File

@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelEditSelectionListCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
var isChecked: Bool {
didSet {
accessoryType = isChecked ? .checkmark : .none
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
isChecked = false
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
isChecked = false
}
}

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelEditSwitchCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
var isOn: Bool {
get { return switchView.isOn }
set(value) { switchView.isOn = value }
}
var isEnabled: Bool {
get { return switchView.isEnabled }
set(value) {
switchView.isEnabled = value
textLabel?.textColor = value ? UIColor.black : UIColor.gray
}
}
var onSwitchToggled: ((Bool) -> Void)?
let switchView: UISwitch
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
switchView = UISwitch()
super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = switchView
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(switchView.isOn)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
isOn = false
}
}

View File

@ -79,11 +79,11 @@ class TunnelEditTableViewController: UITableViewController {
self.tableView.estimatedRowHeight = 44 self.tableView.estimatedRowHeight = 44
self.tableView.rowHeight = UITableView.automaticDimension self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.register(KeyValueCell.self) self.tableView.register(TunnelEditKeyValueCell.self)
self.tableView.register(ReadOnlyKeyValueCell.self) self.tableView.register(TunnelEditReadOnlyKeyValueCell.self)
self.tableView.register(ButtonCell.self) self.tableView.register(TunnelEditButtonCell.self)
self.tableView.register(SwitchCell.self) self.tableView.register(TunnelEditSwitchCell.self)
self.tableView.register(SelectionListCell.self) self.tableView.register(TunnelEditSelectionListCell.self)
} }
private func loadSections() { private func loadSections() {
@ -201,7 +201,7 @@ extension TunnelEditTableViewController {
} }
private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@ -218,14 +218,14 @@ extension TunnelEditTableViewController {
} }
private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: ReadOnlyKeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditReadOnlyKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.rawValue
cell.value = tunnelViewModel.interfaceData[field] cell.value = tunnelViewModel.interfaceData[field]
return cell return cell
} }
private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.rawValue
switch field { switch field {
@ -287,7 +287,7 @@ extension TunnelEditTableViewController {
} }
private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.rawValue
cell.hasDestructiveAction = true cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in cell.onTapped = { [weak self, weak peerData] in
@ -313,7 +313,7 @@ extension TunnelEditTableViewController {
} }
private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.rawValue cell.message = field.rawValue
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue cell.isOn = peerData.excludePrivateIPsValue
@ -328,7 +328,7 @@ extension TunnelEditTableViewController {
} }
private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.rawValue
switch field { switch field {
@ -377,7 +377,7 @@ extension TunnelEditTableViewController {
} }
private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Add peer" cell.buttonText = "Add peer"
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@ -398,7 +398,7 @@ extension TunnelEditTableViewController {
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 { if indexPath.row == 0 {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = "Activate on demand" cell.message = "Activate on demand"
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
@ -419,7 +419,7 @@ extension TunnelEditTableViewController {
} }
return cell return cell
} else { } else {
let cell: SelectionListCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditSelectionListCell = tableView.dequeueReusableCell(for: indexPath)
let rowOption = activateOnDemandOptions[indexPath.row - 1] let rowOption = activateOnDemandOptions[indexPath.row - 1]
let selectedOption = activateOnDemandSetting.activateOnDemandOption let selectedOption = activateOnDemandSetting.activateOnDemandOption
assert(selectedOption != .none) assert(selectedOption != .none)
@ -486,333 +486,3 @@ extension TunnelEditTableViewController {
} }
} }
} }
private class KeyValueCell: UITableViewCell {
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueTextField.text ?? "" }
set(value) { valueTextField.text = value }
}
var placeholderText: String {
get { return valueTextField.placeholder ?? "" }
set(value) { valueTextField.placeholder = value }
}
var isValueValid: Bool = true {
didSet {
if isValueValid {
keyLabel.textColor = UIColor.black
} else {
keyLabel.textColor = UIColor.red
}
}
}
var keyboardType: UIKeyboardType {
get { return valueTextField.keyboardType }
set(value) { valueTextField.keyboardType = value }
}
var onValueChanged: ((String) -> Void)?
var onValueBeingEdited: ((String) -> Void)?
let keyLabel: UILabel
let valueTextField: UITextField
var isStackedHorizontally: Bool = false
var isStackedVertically: Bool = false
var contentSizeBasedConstraints = [NSLayoutConstraint]()
private var textFieldValueOnBeginEditing: String = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
valueTextField = UITextField()
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
valueTextField.adjustsFontForContentSizeCategory = true
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5),
widthRatioConstraint
])
contentView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 0.5)
])
valueTextField.delegate = self
valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no
configureForContentSize()
}
func configureForContentSize() {
var constraints = [NSLayoutConstraint]()
if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Stack vertically
if !isStackedVertically {
constraints = [
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueTextField.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
]
isStackedVertically = true
isStackedHorizontally = false
}
} else {
// Stack horizontally
if !isStackedHorizontally {
constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueTextField.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]
isStackedHorizontally = true
isStackedVertically = false
}
}
if !constraints.isEmpty {
NSLayoutConstraint.deactivate(self.contentSizeBasedConstraints)
NSLayoutConstraint.activate(constraints)
self.contentSizeBasedConstraints = constraints
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
placeholderText = ""
isValueValid = true
keyboardType = .default
onValueChanged = nil
onValueBeingEdited = nil
configureForContentSize()
}
}
extension KeyValueCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textFieldValueOnBeginEditing = textField.text ?? ""
isValueValid = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing)
guard isModified else { return }
if let onValueChanged = onValueChanged {
onValueChanged(textField.text ?? "")
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let onValueBeingEdited = onValueBeingEdited {
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
onValueBeingEdited(modifiedText)
}
return true
}
}
private class ReadOnlyKeyValueCell: CopyableLabelTableViewCell {
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueLabel.text }
set(value) { valueLabel.text = value }
}
let keyLabel: UILabel
let valueLabel: ScrollableLabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
valueLabel = ScrollableLabel()
valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
valueLabel.label.adjustsFontForContentSizeCategory = true
super.init(style: style, reuseIdentifier: reuseIdentifier)
keyLabel.textColor = UIColor.gray
valueLabel.textColor = UIColor.gray
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// In case the key doesn't fit into 0.4 * width,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
widthRatioConstraint
])
contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
])
}
override var textToCopy: String? {
return self.valueLabel.text
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
private class ButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == UIColor.red }
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
hasDestructiveAction = false
}
}
private class SwitchCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
var isOn: Bool {
get { return switchView.isOn }
set(value) { switchView.isOn = value }
}
var isEnabled: Bool {
get { return switchView.isEnabled }
set(value) {
switchView.isEnabled = value
textLabel?.textColor = value ? UIColor.black : UIColor.gray
}
}
var onSwitchToggled: ((Bool) -> Void)?
let switchView: UISwitch
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
switchView = UISwitch()
super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = switchView
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(switchView.isOn)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
isOn = false
}
}
private class SelectionListCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
var isChecked: Bool {
didSet {
accessoryType = isChecked ? .checkmark : .none
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
isChecked = false
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
isChecked = false
}
}

View File

@ -8,7 +8,7 @@ class ErrorPresenter {
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) { static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return } guard let sourceVC = sourceVC else { return }
let (title, message) = error.alertText() let (title, message) = error.alertText
let okAction = UIAlertAction(title: "OK", style: .default) { _ in let okAction = UIAlertAction(title: "OK", style: .default) { _ in
onDismissal?() onDismissal?()
} }

View File

@ -24,7 +24,7 @@ class QRScanViewController: UIViewController {
let tipLabel = UILabel() let tipLabel = UILabel()
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`" tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
tipLabel.adjustsFontSizeToFitWidth = true tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textColor = UIColor.lightGray tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center tipLabel.textAlignment = .center
view.addSubview(tipLabel) view.addSubview(tipLabel)

View File

@ -13,33 +13,31 @@ class ScrollableLabel: UIScrollView {
set(value) { label.textColor = value } set(value) { label.textColor = value }
} }
let label: UILabel let label: UILabel = {
init() {
let label = UILabel() let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right label.textAlignment = .right
self.label = label return label
}()
init() {
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
self.isDirectionalLockEnabled = true isDirectionalLockEnabled = true
self.showsHorizontalScrollIndicator = false showsHorizontalScrollIndicator = false
self.showsVerticalScrollIndicator = false showsVerticalScrollIndicator = false
addSubview(label) addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
label.leftAnchor.constraint(equalTo: self.contentLayoutGuide.leftAnchor), label.leftAnchor.constraint(equalTo: contentLayoutGuide.leftAnchor),
label.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor), label.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
label.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor), label.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
label.rightAnchor.constraint(equalTo: self.contentLayoutGuide.rightAnchor), label.rightAnchor.constraint(equalTo: contentLayoutGuide.rightAnchor),
label.heightAnchor.constraint(equalTo: self.heightAnchor) label.heightAnchor.constraint(equalTo: heightAnchor)
]) ])
// If label has less content, it should expand to fit the scrollView,
// so that right-alignment works in the label. let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal,
toItem: self, attribute: .width, multiplier: 1, constant: 0)
expandToFitValueLabelConstraint.priority = .defaultLow + 1 expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true expandToFitValueLabelConstraint.isActive = true
} }

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class SettingsButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
}
}

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class SettingsKeyValueCell: UITableViewCell {
var key: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel?.text = value }
}
var value: String {
get { return detailTextLabel?.text ?? "" }
set(value) { detailTextLabel?.text = value }
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: SettingsKeyValueCell.reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}

View File

@ -40,8 +40,8 @@ class SettingsTableViewController: UITableViewController {
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false tableView.allowsSelection = false
tableView.register(KeyValueCell.self) tableView.register(SettingsKeyValueCell.self)
tableView.register(ButtonCell.self) tableView.register(SettingsButtonCell.self)
tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf")) tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf"))
} }
@ -167,7 +167,7 @@ extension SettingsTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let field = settingsFieldsBySection[indexPath.section][indexPath.row] let field = settingsFieldsBySection[indexPath.section][indexPath.row]
if field == .iosAppVersion || field == .goBackendVersion { if field == .iosAppVersion || field == .goBackendVersion {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: SettingsKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.rawValue
if field == .iosAppVersion { if field == .iosAppVersion {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
@ -180,7 +180,7 @@ extension SettingsTableViewController {
} }
return cell return cell
} else if field == .exportZipArchive { } else if field == .exportZipArchive {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button) self?.exportConfigurationsAsZipFile(sourceView: cell.button)
@ -188,7 +188,7 @@ extension SettingsTableViewController {
return cell return cell
} else { } else {
assert(field == .exportLogFile) assert(field == .exportLogFile)
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button) self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
@ -197,67 +197,3 @@ extension SettingsTableViewController {
} }
} }
} }
private class KeyValueCell: UITableViewCell {
var key: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel?.text = value }
}
var value: String {
get { return detailTextLabel?.text ?? "" }
set(value) { detailTextLabel?.text = value }
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: KeyValueCell.reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
private class ButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
let button: UIButton
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
}
}

View File

@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelDetailActivateOnDemandCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
update(from: tunnel?.activateOnDemandSetting())
onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
self?.update(from: tunnel.activateOnDemandSetting())
}
}
}
var onDemandStatusObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
textLabel?.text = "Activate on demand"
textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
textLabel?.adjustsFontForContentSizeCategory = true
detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body)
detailTextLabel?.adjustsFontForContentSizeCategory = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(from activateOnDemandSetting: ActivateOnDemandSetting?) {
detailTextLabel?.text = TunnelViewModel.activateOnDemandDetailText(for: activateOnDemandSetting)
}
override func prepareForReuse() {
super.prepareForReuse()
textLabel?.text = "Activate on demand"
detailTextLabel?.text = ""
}
}

View File

@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelDetailButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == UIColor.red }
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
hasDestructiveAction = false
}
}

View File

@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelDetailKeyValueCell: CopyableLabelTableViewCell {
var key: String {
get { return keyLabel.text ?? "" }
set(value) { keyLabel.text = value }
}
var value: String {
get { return valueLabel.text }
set(value) { valueLabel.text = value }
}
override var textToCopy: String? {
return self.valueLabel.text
}
let keyLabel: UILabel = {
let keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
keyLabel.textColor = .black
return keyLabel
}()
let valueLabel: ScrollableLabel = {
let valueLabel = ScrollableLabel()
valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
valueLabel.label.adjustsFontForContentSizeCategory = true
valueLabel.textColor = .gray
return valueLabel
}()
var isStackedHorizontally = false
var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .left
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
])
contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabel.bottomAnchor, multiplier: 0.5)
])
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
configureForContentSize()
}
func configureForContentSize() {
var constraints = [NSLayoutConstraint]()
if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Stack vertically
if !isStackedVertically {
constraints = [
valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
]
isStackedVertically = true
isStackedHorizontally = false
}
} else {
// Stack horizontally
if !isStackedHorizontally {
constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]
isStackedHorizontally = true
isStackedVertically = false
}
}
if !constraints.isEmpty {
NSLayoutConstraint.deactivate(self.contentSizeBasedConstraints)
NSLayoutConstraint.activate(constraints)
self.contentSizeBasedConstraints = constraints
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
configureForContentSize()
}
}

View File

@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelDetailStatusCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status)
}
}
}
var isSwitchInteractionEnabled: Bool {
get { return statusSwitch.isUserInteractionEnabled }
set(value) { statusSwitch.isUserInteractionEnabled = value }
}
var onSwitchToggled: ((Bool) -> Void)?
private var isOnSwitchToggledHandlerEnabled = true
let statusSwitch: UISwitch
private var statusObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
statusSwitch = UISwitch()
super.init(style: .default, reuseIdentifier: TunnelDetailKeyValueCell.reuseIdentifier)
accessoryView = statusSwitch
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
if isOnSwitchToggledHandlerEnabled {
onSwitchToggled?(statusSwitch.isOn)
}
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
let text: String
switch status {
case .inactive:
text = "Inactive"
case .activating:
text = "Activating"
case .active:
text = "Active"
case .deactivating:
text = "Deactivating"
case .reasserting:
text = "Reactivating"
case .restarting:
text = "Restarting"
case .waiting:
text = "Waiting"
}
textLabel?.text = text
DispatchQueue.main.async { [weak statusSwitch] in
guard let statusSwitch = statusSwitch else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
}
textLabel?.textColor = (status == .active || status == .inactive) ? UIColor.black : UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
textLabel?.text = "Invalid"
statusSwitch.isOn = false
textLabel?.textColor = UIColor.gray
statusSwitch.isUserInteractionEnabled = false
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}

View File

@ -0,0 +1,219 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
// MARK: TunnelDetailTableViewController
class TunnelDetailTableViewController: UITableViewController {
private enum Section {
case status
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case onDemand
case delete
}
let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .persistentKeepAlive
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel
private var sections = [Section]()
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
super.init(style: .grouped)
loadSections()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = tunnelViewModel.interfaceData[.name]
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
self.tableView.estimatedRowHeight = 44
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.allowsSelection = false
self.tableView.register(TunnelDetailStatusCell.self)
self.tableView.register(TunnelDetailKeyValueCell.self)
self.tableView.register(TunnelDetailButtonCell.self)
self.tableView.register(TunnelDetailActivateOnDemandCell.self)
// State restoration
self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
}
private func loadSections() {
sections.removeAll()
sections.append(.status)
sections.append(.interface)
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
sections.append(.onDemand)
sections.append(.delete)
}
@objc func editTapped() {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
editVC.delegate = self
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
present(editNC, animated: true)
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
self.present(alert, animated: true, completion: nil)
}
}
// MARK: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
loadSections()
self.title = tunnel.name
self.tableView.reloadData()
}
func tunnelEditingCancelled() {
// Nothing to do
}
}
// MARK: UITableViewDataSource
extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch sections[section] {
case .status:
return 1
case .interface:
return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
case .peer(let peerData):
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
case .onDemand:
return 1
case .delete:
return 1
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] {
case .status:
return "Status"
case .interface:
return "Interface"
case .peer:
return "Peer"
case .onDemand:
return "On-Demand Activation"
case .delete:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch sections[indexPath.section] {
case .status:
return statusCell(for: tableView, at: indexPath)
case .interface:
return interfaceCell(for: tableView, at: indexPath)
case .peer(let peer):
return peerCell(for: tableView, at: indexPath, with: peer)
case .onDemand:
return onDemandCell(for: tableView, at: indexPath)
case .delete:
return deleteConfigurationCell(for: tableView, at: indexPath)
}
}
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: TunnelDetailStatusCell = tableView.dequeueReusableCell(for: indexPath)
cell.tunnel = self.tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
if isOn {
self.tunnelsManager.startActivation(of: self.tunnel)
} else {
self.tunnelsManager.startDeactivation(of: self.tunnel)
}
}
return cell
}
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
let cell: TunnelDetailKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = tunnelViewModel.interfaceData[field]
return cell
}
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
let cell: TunnelDetailKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = peerData[field]
return cell
}
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: TunnelDetailActivateOnDemandCell = tableView.dequeueReusableCell(for: indexPath)
cell.tunnel = self.tunnel
return cell
}
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: TunnelDetailButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Delete tunnel"
cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in
guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in
guard let tunnelsManager = self?.tunnelsManager, let tunnel = self?.tunnel else { return }
tunnelsManager.remove(tunnel: tunnel) { error in
if error != nil {
print("Error removing tunnel: \(String(describing: error))")
return
}
}
self?.navigationController?.navigationController?.popToRootViewController(animated: true)
}
}
return cell
}
}

View File

@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class BorderedTextButton: UIView {
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
override var intrinsicContentSize: CGSize {
let buttonSize = button.intrinsicContentSize
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
}
var title: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
init() {
super.init(frame: CGRect.zero)
layer.borderWidth = 1
layer.cornerRadius = 5
layer.borderColor = button.tintColor.cgColor
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelListCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
// Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? ""
nameObservervationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status)
}
}
}
var onSwitchToggled: ((Bool) -> Void)?
let nameLabel: UILabel = {
let nameLabel = UILabel()
nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
nameLabel.adjustsFontForContentSizeCategory = true
nameLabel.numberOfLines = 0
return nameLabel
}()
let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}()
let statusSwitch = UISwitch()
private var statusObservervationToken: AnyObject?
private var nameObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(statusSwitch)
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.rightAnchor.constraint(equalTo: statusSwitch.rightAnchor)
])
contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.leftAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.rightAnchor, multiplier: 1)
])
contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
bottomAnchorConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1),
busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1),
bottomAnchorConstraint
])
accessoryType = .disclosureIndicator
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
DispatchQueue.main.async { [weak statusSwitch, weak busyIndicator] in
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if status == .inactive || status == .active {
busyIndicator.stopAnimating()
} else {
busyIndicator.startAnimating()
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}

View File

@ -9,95 +9,88 @@ class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager? var tunnelsManager: TunnelsManager?
var busyIndicator: UIActivityIndicatorView? let tableView: UITableView = {
var centeredAddButton: BorderedTextButton? let tableView = UITableView(frame: CGRect.zero, style: .plain)
var tableView: UITableView? tableView.estimatedRowHeight = 60
tableView.rowHeight = UITableView.automaticDimension
override func viewDidLoad() { tableView.separatorStyle = .none
super.viewDidLoad() tableView.register(TunnelListCell.self)
view.backgroundColor = UIColor.white return tableView
}()
// Set up the navigation bar
self.title = "WireGuard" let centeredAddButton: BorderedTextButton = {
let addButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:))) let button = BorderedTextButton()
self.navigationItem.rightBarButtonItem = addButtonItem button.title = "Add a tunnel"
let settingsButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:))) button.isHidden = true
self.navigationItem.leftBarButtonItem = settingsButtonItem return button
}()
// Set up the busy indicator
let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray) let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true busyIndicator.hidesWhenStopped = true
return busyIndicator
}()
override func loadView() {
view = UIView()
view.backgroundColor = .white
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// Add the busyIndicator, centered
view.addSubview(busyIndicator) view.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]) ])
view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
centeredAddButton.onTapped = { [weak self] in
guard let self = self else { return }
self.addButtonTapped(sender: self.centeredAddButton)
}
busyIndicator.startAnimating() busyIndicator.startAnimating()
self.busyIndicator = busyIndicator }
override func viewDidLoad() {
super.viewDidLoad()
// State restoration title = "WireGuard"
self.restorationIdentifier = "TunnelsListVC" navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
restorationIdentifier = "TunnelsListVC"
} }
func setTunnelsManager(tunnelsManager: TunnelsManager) { func setTunnelsManager(tunnelsManager: TunnelsManager) {
if self.tunnelsManager != nil {
// If a tunnels manager is already set, do nothing
return
}
// Create the table view
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.estimatedRowHeight = 60
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.register(TunnelCell.self)
self.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
tableView.dataSource = self
tableView.delegate = self
self.tableView = tableView
// Add button at the center
let centeredAddButton = BorderedTextButton()
centeredAddButton.title = "Add a tunnel"
centeredAddButton.isHidden = true
self.view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
centeredAddButton.onTapped = { [weak self] in
self?.addButtonTapped(sender: centeredAddButton)
}
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
self.centeredAddButton = centeredAddButton
// Hide the busy indicator
self.busyIndicator?.stopAnimating()
// Keep track of the tunnels manager
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self tunnelsManager.tunnelsListDelegate = self
busyIndicator.stopAnimating()
tableView.reloadData()
centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
} }
override func viewWillAppear(_: Bool) { override func viewWillAppear(_: Bool) {
// Remove selection when getting back to the list view on iPhone // Remove selection when getting back to the list view on iPhone
if let tableView = self.tableView, let selectedRowIndexPath = tableView.indexPathForSelectedRow { if let selectedRowIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRowIndexPath, animated: false) tableView.deselectRow(at: selectedRowIndexPath, animated: false)
} }
} }
@ -241,7 +234,7 @@ extension TunnelsListTableViewController: UITableViewDataSource {
} }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TunnelCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelListCell = tableView.dequeueReusableCell(for: indexPath)
if let tunnelsManager = tunnelsManager { if let tunnelsManager = tunnelsManager {
let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnel = tunnelsManager.tunnel(at: indexPath.row)
cell.tunnel = tunnel cell.tunnel = tunnel
@ -293,161 +286,20 @@ extension TunnelsListTableViewController: UITableViewDelegate {
extension TunnelsListTableViewController: TunnelsManagerListDelegate { extension TunnelsListTableViewController: TunnelsManagerListDelegate {
func tunnelAdded(at index: Int) { func tunnelAdded(at index: Int) {
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic) tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0) centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
} }
func tunnelModified(at index: Int) { func tunnelModified(at index: Int) {
tableView?.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
} }
func tunnelMoved(from oldIndex: Int, to newIndex: Int) { func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
tableView?.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0)) tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
} }
func tunnelRemoved(at index: Int) { func tunnelRemoved(at index: Int) {
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0) centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
}
}
private class TunnelCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
// Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? ""
nameObservervationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status)
}
}
}
var onSwitchToggled: ((Bool) -> Void)?
let nameLabel: UILabel
let busyIndicator: UIActivityIndicatorView
let statusSwitch: UISwitch
private var statusObservervationToken: AnyObject?
private var nameObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
nameLabel = UILabel()
nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
nameLabel.adjustsFontForContentSizeCategory = true
busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
statusSwitch = UISwitch()
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(statusSwitch)
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.rightAnchor.constraint(equalTo: statusSwitch.rightAnchor)
])
contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.leftAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.rightAnchor, multiplier: 1)
])
contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.numberOfLines = 0
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(
equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
bottomAnchorConstraint.priority = .defaultLow // Allow this constraint to be broken when animating a cell away during deletion
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1),
busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1),
bottomAnchorConstraint
])
self.accessoryType = .disclosureIndicator
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
DispatchQueue.main.async { [weak statusSwitch, weak busyIndicator] in
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if status == .inactive || status == .active {
busyIndicator.stopAnimating()
} else {
busyIndicator.startAnimating()
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}
class BorderedTextButton: UIView {
let button: UIButton
override var intrinsicContentSize: CGSize {
let buttonSize = button.intrinsicContentSize
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
}
var title: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
init() {
button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
super.init(frame: CGRect.zero)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor)
])
layer.borderWidth = 1
layer.cornerRadius = 5
layer.borderColor = button.tintColor.cgColor
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
} }
} }

View File

@ -3,5 +3,5 @@
protocol WireGuardAppError: Error { protocol WireGuardAppError: Error {
typealias AlertText = (title: String, message: String) typealias AlertText = (title: String, message: String)
func alertText() -> AlertText var alertText: AlertText { get }
} }

View File

@ -8,7 +8,7 @@ enum ZipArchiveError: WireGuardAppError {
case cantOpenOutputZipFileForWriting case cantOpenOutputZipFileForWriting
case badArchive case badArchive
func alertText() -> AlertText { var alertText: AlertText {
switch self { switch self {
case .cantOpenInputZipFile: case .cantOpenInputZipFile:
return ("Unable to read zip archive", "The zip archive could not be read.") return ("Unable to read zip archive", "The zip archive could not be read.")

View File

@ -6,11 +6,8 @@ import UIKit
enum ZipExporterError: WireGuardAppError { enum ZipExporterError: WireGuardAppError {
case noTunnelsToExport case noTunnelsToExport
func alertText() -> AlertText { var alertText: AlertText {
switch self { return ("Nothing to export", "There are no tunnels to export")
case .noTunnelsToExport:
return ("Nothing to export", "There are no tunnels to export")
}
} }
} }

View File

@ -6,11 +6,8 @@ import UIKit
enum ZipImporterError: WireGuardAppError { enum ZipImporterError: WireGuardAppError {
case noTunnelsInZipArchive case noTunnelsInZipArchive
func alertText() -> AlertText { var alertText: AlertText {
switch self { return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
case .noTunnelsInZipArchive:
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
}
} }
} }

View File

@ -65,6 +65,8 @@ class DNSResolver {
extension DNSResolver { extension DNSResolver {
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com> // Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase // in parse_endpoint() in src/tools/config.c in the WireGuard codebase
//swiftlint:disable:next cyclomatic_complexity
private static func resolveSync(endpoint: Endpoint) -> Endpoint? { private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
switch endpoint.host { switch endpoint.host {
case .name(let name, _): case .name(let name, _):

View File

@ -41,6 +41,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler) startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler)
} }
//swiftlint:disable:next function_body_length
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) { func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
configureLogger() configureLogger()
@ -158,7 +159,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} }
} }
// swiftlint:disable large_tuple // swiftlint:disable large_tuple identifier_name
func withStringsAsGoStrings<R>(_ s1: String, _ s2: String? = nil, _ s3: String? = nil, _ s4: String? = nil, call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R { func withStringsAsGoStrings<R>(_ s1: String, _ s2: String? = nil, _ s3: String? = nil, _ s4: String? = nil, call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R {
func helper(_ p1: UnsafePointer<Int8>?, _ p2: UnsafePointer<Int8>?, _ p3: UnsafePointer<Int8>?, _ p4: UnsafePointer<Int8>?, _ call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R { func helper(_ p1: UnsafePointer<Int8>?, _ p2: UnsafePointer<Int8>?, _ p3: UnsafePointer<Int8>?, _ p4: UnsafePointer<Int8>?, _ call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R {
return call((gostring_t(p: p1, n: s1.utf8.count), gostring_t(p: p2, n: s2?.utf8.count ?? 0), gostring_t(p: p3, n: s3?.utf8.count ?? 0), gostring_t(p: p4, n: s4?.utf8.count ?? 0))) return call((gostring_t(p: p1, n: s1.utf8.count), gostring_t(p: p2, n: s2?.utf8.count ?? 0), gostring_t(p: p3, n: s3?.utf8.count ?? 0), gostring_t(p: p4, n: s4?.utf8.count ?? 0)))