Reorganized ViewControllers (split out UIViews and UITableViewCells into their own classes)
All swiftlint warnings except one fixed up
This commit is contained in:
parent
a5aaec673a
commit
a8cbe0d683
|
@ -10,3 +10,5 @@ file_length:
|
|||
cyclomatic_complexity:
|
||||
warning: 10
|
||||
error: 25
|
||||
function_body_length:
|
||||
warning: 45
|
||||
|
|
|
@ -8,6 +8,19 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
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 */; };
|
||||
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; };
|
||||
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
|
||||
|
@ -96,6 +109,19 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -176,6 +202,51 @@
|
|||
/* End PBXFrameworksBuildPhase 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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -232,14 +303,14 @@
|
|||
6F7774DE217181B1006A79B3 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5F45418821C2C6CC00994C13 /* TunnelList */,
|
||||
5F45418721C2C6C100994C13 /* TunnelDetail */,
|
||||
5F45418621C2C6B400994C13 /* EditTunnel */,
|
||||
5F45418521C2C6AB00994C13 /* Settings */,
|
||||
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
|
||||
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
|
||||
6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
|
||||
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
|
||||
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
|
||||
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
|
||||
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
|
||||
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
|
||||
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */,
|
||||
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */,
|
||||
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */,
|
||||
|
@ -621,33 +692,47 @@
|
|||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
|
||||
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
|
||||
5F45419221C2D55800994C13 /* TunnelEditSectionListCell.swift in Sources */,
|
||||
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
|
||||
5F45418E21C2D51100994C13 /* TunnelEditButtonCell.swift in Sources */,
|
||||
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
|
||||
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
|
||||
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
|
||||
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
|
||||
5F45418A21C2D45B00994C13 /* TunnelEditKeyValueCell.swift in Sources */,
|
||||
6F6899A62180447E0012E523 /* x25519.c in Sources */,
|
||||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
|
||||
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
|
||||
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
|
||||
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
|
||||
5F45419C21C2D64800994C13 /* SettingsButtonCell.swift in Sources */,
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
|
||||
5F45419821C2D60500994C13 /* TunnelDetailKeyValueCell.swift in Sources */,
|
||||
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
|
||||
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
|
||||
5F45418C21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift in Sources */,
|
||||
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
|
||||
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
|
||||
5F45419A21C2D61D00994C13 /* TunnelDetailStatusCell.swift in Sources */,
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
|
||||
5F45419621C2D5DB00994C13 /* TunnelDetailButtonCell.swift in Sources */,
|
||||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
|
||||
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
|
||||
5F45419E21C2D66400994C13 /* SettingsKeyValueCell.swift in Sources */,
|
||||
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
|
||||
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
|
||||
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
|
||||
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
|
||||
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
|
||||
5F45419021C2D53800994C13 /* TunnelEditSwitchCell.swift in Sources */,
|
||||
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
|
||||
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
|
||||
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
|
||||
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
|
||||
5F45419421C2D5C500994C13 /* TunnelDetailActivateOnDemandCell.swift in Sources */,
|
||||
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ class WgQuickConfigFileParser {
|
|||
case invalidPeer
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
static func parse(_ text: String, name: String) throws -> TunnelConfiguration {
|
||||
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? {
|
||||
// required wg fields
|
||||
guard let privateKeyString = attributes["privatekey"] else { return nil }
|
||||
|
@ -131,6 +133,7 @@ class WgQuickConfigFileParser {
|
|||
return interface
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
private static func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
|
||||
// required wg fields
|
||||
guard let publicKeyString = attributes["publickey"] else { return nil }
|
||||
|
|
|
@ -27,7 +27,7 @@ enum TunnelsManagerActivationAttemptError: WireGuardAppError {
|
|||
case failedWhileLoading // reloading config throwed
|
||||
case failedBecauseOfTooManyErrors // recursion limit reached
|
||||
|
||||
func alertText() -> AlertText {
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .tunnelIsNotInactive:
|
||||
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 {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
enum TunnelsManagerError: WireGuardAppError {
|
||||
// Tunnels list management
|
||||
case tunnelNameEmpty
|
||||
case tunnelAlreadyExistsWithThatName
|
||||
case systemErrorOnListingTunnels
|
||||
|
@ -55,7 +54,7 @@ enum TunnelsManagerError: WireGuardAppError {
|
|||
case systemErrorOnModifyTunnel
|
||||
case systemErrorOnRemoveTunnel
|
||||
|
||||
func alertText() -> AlertText {
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .tunnelNameEmpty:
|
||||
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) {
|
||||
#if targetEnvironment(simulator)
|
||||
// NETunnelProviderManager APIs don't work on the simulator
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: [])))
|
||||
#else
|
||||
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
||||
|
@ -101,9 +99,7 @@ class TunnelsManager {
|
|||
#endif
|
||||
}
|
||||
|
||||
func add(tunnelConfiguration: TunnelConfiguration,
|
||||
activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting,
|
||||
completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
||||
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
|
||||
|
@ -128,7 +124,9 @@ class TunnelsManager {
|
|||
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel))
|
||||
return
|
||||
}
|
||||
if let self = self {
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
||||
self.tunnels.append(tunnel)
|
||||
self.tunnels.sort { $0.name < $1.name }
|
||||
|
@ -136,7 +134,6 @@ class TunnelsManager {
|
|||
completionHandler(.success(tunnel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
|
||||
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
|
||||
|
@ -155,8 +152,7 @@ class TunnelsManager {
|
|||
}
|
||||
}
|
||||
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration,
|
||||
activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
||||
|
@ -185,7 +181,9 @@ class TunnelsManager {
|
|||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
|
||||
return
|
||||
}
|
||||
if let self = self {
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
if isNameChanged {
|
||||
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
|
||||
self.tunnels.sort { $0.name < $1.name }
|
||||
|
@ -217,7 +215,6 @@ class TunnelsManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
|
@ -274,9 +271,7 @@ class TunnelsManager {
|
|||
|
||||
func startDeactivation(of tunnel: TunnelContainer) {
|
||||
tunnel.isAttemptingActivation = false
|
||||
if tunnel.status == .inactive || tunnel.status == .deactivating {
|
||||
return
|
||||
}
|
||||
guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
|
||||
tunnel.startDeactivation()
|
||||
}
|
||||
|
||||
|
@ -288,10 +283,10 @@ class TunnelsManager {
|
|||
guard statusObservationToken == nil else { return }
|
||||
|
||||
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
|
||||
guard let self = self else { return }
|
||||
guard let session = statusChangeNotification.object as? NETunnelProviderSession else { return }
|
||||
guard let tunnelProvider = session.manager as? NETunnelProviderManager else { return }
|
||||
guard let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
|
||||
guard let self = self,
|
||||
let session = statusChangeNotification.object as? NETunnelProviderSession,
|
||||
let tunnelProvider = session.manager as? NETunnelProviderManager,
|
||||
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)'")
|
||||
|
||||
|
@ -339,7 +334,7 @@ class TunnelContainer: NSObject {
|
|||
|
||||
@objc dynamic var isActivateOnDemandEnabled: Bool
|
||||
|
||||
var isAttemptingActivation: Bool = false
|
||||
var isAttemptingActivation = false
|
||||
|
||||
fileprivate let tunnelProvider: NETunnelProviderManager
|
||||
private var lastTunnelConnectionStatus: NEVPNStatus?
|
||||
|
@ -375,10 +370,8 @@ class TunnelContainer: NSObject {
|
|||
startActivation(tunnelConfiguration: tunnelConfiguration, activationDelegate: activationDelegate)
|
||||
}
|
||||
|
||||
fileprivate func startActivation(recursionCount: UInt = 0,
|
||||
lastError: Error? = nil,
|
||||
tunnelConfiguration: TunnelConfiguration,
|
||||
activationDelegate: TunnelsManagerActivationDelegate?) {
|
||||
//swiftlint:disable:next function_body_length
|
||||
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, tunnelConfiguration: TunnelConfiguration, activationDelegate: TunnelsManagerActivationDelegate?) {
|
||||
if recursionCount >= 8 {
|
||||
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
|
||||
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors)
|
||||
|
@ -457,7 +450,6 @@ class TunnelContainer: NSObject {
|
|||
case active
|
||||
case deactivating
|
||||
case reasserting // Not a possible state at present
|
||||
|
||||
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
|
||||
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> {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
// It's already validated and saved
|
||||
|
@ -236,6 +237,7 @@ class TunnelViewModel {
|
|||
updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
func save() -> SaveResult<PeerConfiguration> {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
// 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.rowHeight = UITableView.automaticDimension
|
||||
|
||||
self.tableView.register(KeyValueCell.self)
|
||||
self.tableView.register(ReadOnlyKeyValueCell.self)
|
||||
self.tableView.register(ButtonCell.self)
|
||||
self.tableView.register(SwitchCell.self)
|
||||
self.tableView.register(SelectionListCell.self)
|
||||
self.tableView.register(TunnelEditKeyValueCell.self)
|
||||
self.tableView.register(TunnelEditReadOnlyKeyValueCell.self)
|
||||
self.tableView.register(TunnelEditButtonCell.self)
|
||||
self.tableView.register(TunnelEditSwitchCell.self)
|
||||
self.tableView.register(TunnelEditSelectionListCell.self)
|
||||
}
|
||||
|
||||
private func loadSections() {
|
||||
|
@ -201,7 +201,7 @@ extension TunnelEditTableViewController {
|
|||
}
|
||||
|
||||
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.onTapped = { [weak self] in
|
||||
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 {
|
||||
let cell: ReadOnlyKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: TunnelEditReadOnlyKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.rawValue
|
||||
cell.value = tunnelViewModel.interfaceData[field]
|
||||
return cell
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
switch field {
|
||||
|
@ -287,7 +287,7 @@ extension TunnelEditTableViewController {
|
|||
}
|
||||
|
||||
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.hasDestructiveAction = true
|
||||
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 {
|
||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = field.rawValue
|
||||
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
|
||||
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 {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.rawValue
|
||||
|
||||
switch field {
|
||||
|
@ -377,7 +377,7 @@ extension TunnelEditTableViewController {
|
|||
}
|
||||
|
||||
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.onTapped = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
@ -398,7 +398,7 @@ extension TunnelEditTableViewController {
|
|||
|
||||
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.row == 0 {
|
||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: TunnelEditSwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = "Activate on demand"
|
||||
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
|
@ -419,7 +419,7 @@ extension TunnelEditTableViewController {
|
|||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell: SelectionListCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: TunnelEditSelectionListCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let rowOption = activateOnDemandOptions[indexPath.row - 1]
|
||||
let selectedOption = activateOnDemandSetting.activateOnDemandOption
|
||||
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) {
|
||||
guard let sourceVC = sourceVC else { return }
|
||||
|
||||
let (title, message) = error.alertText()
|
||||
let (title, message) = error.alertText
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||
onDismissal?()
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class QRScanViewController: UIViewController {
|
|||
let tipLabel = UILabel()
|
||||
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
|
||||
tipLabel.adjustsFontSizeToFitWidth = true
|
||||
tipLabel.textColor = UIColor.lightGray
|
||||
tipLabel.textColor = .lightGray
|
||||
tipLabel.textAlignment = .center
|
||||
|
||||
view.addSubview(tipLabel)
|
||||
|
|
|
@ -13,33 +13,31 @@ class ScrollableLabel: UIScrollView {
|
|||
set(value) { label.textColor = value }
|
||||
}
|
||||
|
||||
let label: UILabel
|
||||
|
||||
init() {
|
||||
let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.textAlignment = .right
|
||||
self.label = label
|
||||
return label
|
||||
}()
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
self.isDirectionalLockEnabled = true
|
||||
self.showsHorizontalScrollIndicator = false
|
||||
self.showsVerticalScrollIndicator = false
|
||||
isDirectionalLockEnabled = true
|
||||
showsHorizontalScrollIndicator = false
|
||||
showsVerticalScrollIndicator = false
|
||||
|
||||
addSubview(label)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.leftAnchor.constraint(equalTo: self.contentLayoutGuide.leftAnchor),
|
||||
label.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor),
|
||||
label.rightAnchor.constraint(equalTo: self.contentLayoutGuide.rightAnchor),
|
||||
label.heightAnchor.constraint(equalTo: self.heightAnchor)
|
||||
label.leftAnchor.constraint(equalTo: contentLayoutGuide.leftAnchor),
|
||||
label.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
|
||||
label.rightAnchor.constraint(equalTo: contentLayoutGuide.rightAnchor),
|
||||
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.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.allowsSelection = false
|
||||
|
||||
tableView.register(KeyValueCell.self)
|
||||
tableView.register(ButtonCell.self)
|
||||
tableView.register(SettingsKeyValueCell.self)
|
||||
tableView.register(SettingsButtonCell.self)
|
||||
|
||||
tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf"))
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ extension SettingsTableViewController {
|
|||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = settingsFieldsBySection[indexPath.section][indexPath.row]
|
||||
if field == .iosAppVersion || field == .goBackendVersion {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: SettingsKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.rawValue
|
||||
if field == .iosAppVersion {
|
||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
|
@ -180,7 +180,7 @@ extension SettingsTableViewController {
|
|||
}
|
||||
return cell
|
||||
} else if field == .exportZipArchive {
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self] in
|
||||
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
|
||||
|
@ -188,7 +188,7 @@ extension SettingsTableViewController {
|
|||
return cell
|
||||
} else {
|
||||
assert(field == .exportLogFile)
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let cell: SettingsButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self] in
|
||||
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 busyIndicator: UIActivityIndicatorView?
|
||||
var centeredAddButton: BorderedTextButton?
|
||||
var tableView: UITableView?
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.estimatedRowHeight = 60
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TunnelListCell.self)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor.white
|
||||
let centeredAddButton: BorderedTextButton = {
|
||||
let button = BorderedTextButton()
|
||||
button.title = "Add a tunnel"
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
// Set up the navigation bar
|
||||
self.title = "WireGuard"
|
||||
let addButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
|
||||
self.navigationItem.rightBarButtonItem = addButtonItem
|
||||
let settingsButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
|
||||
self.navigationItem.leftBarButtonItem = settingsButtonItem
|
||||
|
||||
// Set up the busy indicator
|
||||
let busyIndicator: UIActivityIndicatorView = {
|
||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||
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)
|
||||
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||
])
|
||||
busyIndicator.startAnimating()
|
||||
self.busyIndicator = busyIndicator
|
||||
|
||||
// State restoration
|
||||
self.restorationIdentifier = "TunnelsListVC"
|
||||
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()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "WireGuard"
|
||||
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) {
|
||||
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
|
||||
tunnelsManager.tunnelsListDelegate = self
|
||||
|
||||
busyIndicator.stopAnimating()
|
||||
tableView.reloadData()
|
||||
centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
|
||||
}
|
||||
|
||||
override func viewWillAppear(_: Bool) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +234,7 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
|||
}
|
||||
|
||||
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 {
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
cell.tunnel = tunnel
|
||||
|
@ -293,161 +286,20 @@ extension TunnelsListTableViewController: UITableViewDelegate {
|
|||
|
||||
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
||||
func tunnelAdded(at index: Int) {
|
||||
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
|
||||
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
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")
|
||||
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
|
||||
}
|
||||
}
|
|
@ -3,5 +3,5 @@
|
|||
|
||||
protocol WireGuardAppError: Error {
|
||||
typealias AlertText = (title: String, message: String)
|
||||
func alertText() -> AlertText
|
||||
var alertText: AlertText { get }
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ enum ZipArchiveError: WireGuardAppError {
|
|||
case cantOpenOutputZipFileForWriting
|
||||
case badArchive
|
||||
|
||||
func alertText() -> AlertText {
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .cantOpenInputZipFile:
|
||||
return ("Unable to read zip archive", "The zip archive could not be read.")
|
||||
|
|
|
@ -6,12 +6,9 @@ import UIKit
|
|||
enum ZipExporterError: WireGuardAppError {
|
||||
case noTunnelsToExport
|
||||
|
||||
func alertText() -> AlertText {
|
||||
switch self {
|
||||
case .noTunnelsToExport:
|
||||
var alertText: AlertText {
|
||||
return ("Nothing to export", "There are no tunnels to export")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipExporter {
|
||||
|
|
|
@ -6,12 +6,9 @@ import UIKit
|
|||
enum ZipImporterError: WireGuardAppError {
|
||||
case noTunnelsInZipArchive
|
||||
|
||||
func alertText() -> AlertText {
|
||||
switch self {
|
||||
case .noTunnelsInZipArchive:
|
||||
var alertText: AlertText {
|
||||
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipImporter {
|
||||
|
|
|
@ -65,6 +65,8 @@ class DNSResolver {
|
|||
extension DNSResolver {
|
||||
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
|
||||
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
|
||||
switch endpoint.host {
|
||||
case .name(let name, _):
|
||||
|
|
|
@ -41,6 +41,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler)
|
||||
}
|
||||
|
||||
//swiftlint:disable:next function_body_length
|
||||
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
|
||||
|
||||
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 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)))
|
||||
|
|
Loading…
Reference in New Issue