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:
parent
7323a00612
commit
05d750539b
|
@ -10,3 +10,5 @@ file_length:
|
||||||
cyclomatic_complexity:
|
cyclomatic_complexity:
|
||||||
warning: 10
|
warning: 10
|
||||||
error: 25
|
error: 25
|
||||||
|
function_body_length:
|
||||||
|
warning: 45
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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?()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 = ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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, _):
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
Loading…
Reference in New Issue