diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index 3c90cbd..253248f 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; }; 6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; }; 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; }; 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; }; @@ -21,6 +22,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = ""; }; 6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; 6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -54,6 +56,7 @@ isa = PBXGroup; children = ( 6F7774DE217181B1006A79B3 /* iOS */, + 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */, ); path = UI; sourceTree = ""; @@ -206,6 +209,7 @@ 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, + 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */, 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */, diff --git a/WireGuard/WireGuard/UI/TunnelViewModel.swift b/WireGuard/WireGuard/UI/TunnelViewModel.swift new file mode 100644 index 0000000..6b6285c --- /dev/null +++ b/WireGuard/WireGuard/UI/TunnelViewModel.swift @@ -0,0 +1,309 @@ +// +// TunnelViewModel.swift +// WireGuard +// +// Created by Roopesh Chander on 23/10/18. +// Copyright © 2018 WireGuard LLC. All rights reserved. +// + +import UIKit + +class TunnelViewModel { + + enum InterfaceEditField: String { + case name = "Name" + case privateKey = "Private key" + case publicKey = "Public key" + case generateKeyPair = "Generate keypair" + case addresses = "Addresses" + case listenPort = "Listen port" + case mtu = "MTU" + case dns = "DNS servers" + } + + enum PeerEditField: String { + case publicKey = "Public key" + case preSharedKey = "Pre-shared key" + case endpoint = "Endpoint" + case persistentKeepAlive = "Persistent Keepalive" + case allowedIPs = "Allowed IPs" + case excludePrivateIPs = "Exclude private IPs" + case deletePeer = "Delete peer" + } + + class InterfaceData { + var scratchpad: [InterfaceEditField: String] = [:] + var fieldsWithError: Set = [] + var validatedConfiguration: InterfaceConfiguration? = nil + + subscript(field: InterfaceEditField) -> String { + get { + if (scratchpad.isEmpty) { + // When starting to read a config, setup the scratchpad. + // The scratchpad shall serve as a cache of what we want to show in the UI. + populateScratchpad() + } + return scratchpad[field] ?? "" + } + set(stringValue) { + if (scratchpad.isEmpty) { + // When starting to edit a config, setup the scratchpad and remove the configuration. + // The scratchpad shall be the sole source of the being-edited configuration. + populateScratchpad() + } + validatedConfiguration = nil + if (stringValue.isEmpty) { + scratchpad.removeValue(forKey: field) + } else { + scratchpad[field] = stringValue + } + } + } + + func populateScratchpad() { + // Populate the scratchpad from the configuration object + guard let config = validatedConfiguration else { return } + scratchpad[.name] = config.name + scratchpad[.privateKey] = config.privateKey.base64EncodedString() + if (!config.addresses.isEmpty) { + scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ") + } + if let listenPort = config.listenPort { + scratchpad[.listenPort] = String(listenPort) + } + if let mtu = config.mtu { + scratchpad[.mtu] = String(mtu) + } + if let dns = config.dns { + scratchpad[.dns] = String(dns) + } + } + + func save() -> SaveResult { + fieldsWithError.removeAll() + guard let name = scratchpad[.name] else { + fieldsWithError.insert(.name) + return .error("Interface name is required") + } + guard let privateKeyString = scratchpad[.privateKey] else { + fieldsWithError.insert(.privateKey) + return .error("Interface's private key is required") + } + guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else { + fieldsWithError.insert(.privateKey) + return .error("Interface's private key should be a 32-byte key in base64 encoding") + } + var config = InterfaceConfiguration(name: name, privateKey: privateKey) + var errorMessages: [String] = [] + if let addressesString = scratchpad[.addresses] { + var addresses: [IPAddressRange] = [] + for addressString in addressesString.split(separator: ",") { + let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + if let address = IPAddressRange(from: trimmedString) { + addresses.append(address) + } else { + fieldsWithError.insert(.addresses) + errorMessages.append("Interface addresses should be a list of comma-separated IP addresses in CIDR notation") + } + } + config.addresses = addresses + } + if let listenPortString = scratchpad[.listenPort] { + if let listenPort = UInt64(listenPortString) { + config.listenPort = listenPort + } else { + fieldsWithError.insert(.listenPort) + errorMessages.append("Interface's listen port should be a number") + } + } + if let mtuString = scratchpad[.mtu] { + if let mtu = UInt64(mtuString) { + config.mtu = mtu + } else { + fieldsWithError.insert(.mtu) + errorMessages.append("Interface's MTU should be a number") + } + } + // TODO: Validate DNS + if let dnsString = scratchpad[.dns] { + config.dns = dnsString + } + + guard (errorMessages.isEmpty) else { + return .error(errorMessages.first!) + } + validatedConfiguration = config + return .saved(config) + } + } + + class PeerData { + var index: Int + var scratchpad: [PeerEditField: String] = [:] + var fieldsWithError: Set = [] + var validatedConfiguration: PeerConfiguration? = nil + + init(index: Int) { + self.index = index + } + + subscript(field: PeerEditField) -> String { + get { + if (scratchpad.isEmpty) { + // When starting to read a config, setup the scratchpad. + // The scratchpad shall serve as a cache of what we want to show in the UI. + populateScratchpad() + } + return scratchpad[field] ?? "" + } + set(stringValue) { + if (scratchpad.isEmpty) { + // When starting to edit a config, setup the scratchpad and remove the configuration. + // The scratchpad shall be the sole source of the being-edited configuration. + populateScratchpad() + } + validatedConfiguration = nil + if (stringValue.isEmpty) { + scratchpad.removeValue(forKey: field) + } else { + scratchpad[field] = stringValue + } + } + } + + func populateScratchpad() { + // Populate the scratchpad from the configuration object + guard let config = validatedConfiguration else { return } + scratchpad[.publicKey] = config.publicKey.base64EncodedString() + if let preSharedKey = config.preSharedKey { + scratchpad[.preSharedKey] = preSharedKey.base64EncodedString() + } + if (!config.allowedIPs.isEmpty) { + scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ") + } + if let endpoint = config.endpoint { + scratchpad[.endpoint] = endpoint.stringRepresentation() + } + if let persistentKeepAlive = config.persistentKeepAlive { + scratchpad[.persistentKeepAlive] = String(persistentKeepAlive) + } + } + + func save() -> SaveResult { + fieldsWithError.removeAll() + guard let publicKeyString = scratchpad[.publicKey] else { + fieldsWithError.insert(.publicKey) + return .error("Peer's public key is required") + } + guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else { + fieldsWithError.insert(.publicKey) + return .error("Peer's public key should be a 32-byte key in base64 encoding") + } + var config = PeerConfiguration(publicKey: publicKey) + var errorMessages: [String] = [] + if let preSharedKeyString = scratchpad[.preSharedKey] { + if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 { + config.preSharedKey = preSharedKey + } else { + fieldsWithError.insert(.preSharedKey) + errorMessages.append("Peer's pre-shared key should be a 32-byte key in base64 encoding") + } + } + if let allowedIPsString = scratchpad[.allowedIPs] { + var allowedIPs: [IPAddressRange] = [] + for allowedIPString in allowedIPsString.split(separator: ",") { + let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + if let allowedIP = IPAddressRange(from: trimmedString) { + allowedIPs.append(allowedIP) + } else { + fieldsWithError.insert(.allowedIPs) + errorMessages.append("Peer's allowedIPs should be a list of comma-separated IP addresses in CIDR notation") + } + } + config.allowedIPs = allowedIPs + } + if let endpointString = scratchpad[.endpoint] { + if let endpoint = Endpoint(from: endpointString) { + config.endpoint = endpoint + } else { + fieldsWithError.insert(.endpoint) + errorMessages.append("Peer's endpoint should be of the form 'host:port' or '[host]:port'") + } + } + if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] { + if let persistentKeepAlive = UInt64(persistentKeepAliveString) { + config.persistentKeepAlive = persistentKeepAlive + } else { + fieldsWithError.insert(.persistentKeepAlive) + errorMessages.append("Peer's persistent keepalive should be a number") + } + } + + guard (errorMessages.isEmpty) else { + return .error(errorMessages.first!) + } + validatedConfiguration = config + return .saved(config) + } + } + + enum SaveResult { + case saved(Configuration) + case error(String) // TODO: Localize error messages + } + + var interfaceData: InterfaceData + var peersData: [PeerData] + + init(tunnelConfiguration: TunnelConfiguration?) { + interfaceData = InterfaceData() + peersData = [] + if let tunnelConfiguration = tunnelConfiguration { + interfaceData.validatedConfiguration = tunnelConfiguration.interface + for (i, peerConfiguration) in tunnelConfiguration.peers.enumerated() { + let peerData = PeerData(index: i) + peerData.validatedConfiguration = peerConfiguration + peersData.append(peerData) + } + } + } + + func appendEmptyPeer() { + let peer = PeerData(index: peersData.count) + peersData.append(peer) + } + + func deletePeer(peer: PeerData) { + let removedPeer = peersData.remove(at: peer.index) + assert(removedPeer.index == peer.index) + for p in peersData[peer.index ..< peersData.count] { + assert(p.index > 0) + p.index = p.index - 1 + } + } + + func save() -> SaveResult { + // Attempt to save the interface and all peers, so that all erroring fields are collected + let interfaceSaveResult = interfaceData.save() + let peerSaveResults = peersData.map { $0.save() } + // Collate the results + switch (interfaceSaveResult) { + case .error(let errorMessage): + return .error(errorMessage) + case .saved(let interfaceConfiguration): + var peerConfigurations: [PeerConfiguration] = [] + peerConfigurations.reserveCapacity(peerSaveResults.count) + for peerSaveResult in peerSaveResults { + switch (peerSaveResult) { + case .error(let errorMessage): + return .error(errorMessage) + case .saved(let peerConfiguration): + peerConfigurations.append(peerConfiguration) + } + } + let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration) + tunnelConfiguration.peers = peerConfigurations + return .saved(tunnelConfiguration) + } + } +} diff --git a/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift index 5703099..9a239c5 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift @@ -12,256 +12,23 @@ import UIKit class TunnelEditTableViewController: UITableViewController { - // MARK: View model - - enum InterfaceEditField: String { - case name = "Name" - case privateKey = "Private key" - case publicKey = "Public key" - case generateKeyPair = "Generate keypair" - case addresses = "Addresses" - case listenPort = "Listen port" - case mtu = "MTU" - case dns = "DNS servers" - } - - let interfaceEditFieldsBySection: [[InterfaceEditField]] = [ + let interfaceEditFieldsBySection: [[TunnelViewModel.InterfaceEditField]] = [ [.name], [.privateKey, .publicKey, .generateKeyPair], [.addresses, .listenPort, .mtu, .dns] ] - enum PeerEditField: String { - case publicKey = "Public key" - case preSharedKey = "Pre-shared key" - case endpoint = "Endpoint" - case persistentKeepAlive = "Persistent Keepalive" - case allowedIPs = "Allowed IPs" - case excludePrivateIPs = "Exclude private IPs" - case deletePeer = "Delete peer" - } - - let peerEditFieldsBySection: [[PeerEditField]] = [ + let peerEditFieldsBySection: [[TunnelViewModel.PeerEditField]] = [ [.publicKey, .preSharedKey, .endpoint, .allowedIPs, .excludePrivateIPs, .persistentKeepAlive, .deletePeer] ] - // Scratchpad for entered data - - class InterfaceData { - var scratchpad: [InterfaceEditField: String] = [:] - var fieldsWithError: Set = [] - var validatedConfiguration: InterfaceConfiguration? = nil - subscript(field: InterfaceEditField) -> String { - get { - ensureScratchpadIsPrepared() // When starting to read a config, setup the scratchpad to serve as a cache - return scratchpad[field] ?? "" - } - set(stringValue) { - ensureScratchpadIsPrepared() // When starting to edit a config, setup the scratchpad - validatedConfiguration = nil // The configuration will need to be revalidated - if (stringValue.isEmpty) { - scratchpad.removeValue(forKey: field) - } else { - scratchpad[field] = stringValue - } - } - } - func ensureScratchpadIsPrepared() { - guard (scratchpad.isEmpty) else { return } // Already prepared - guard let config = validatedConfiguration else { return } // Nothing to prepare it with - scratchpad[.name] = config.name - scratchpad[.privateKey] = config.privateKey.base64EncodedString() - if (!config.addresses.isEmpty) { - scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ") - } - if let listenPort = config.listenPort { - scratchpad[.listenPort] = String(listenPort) - } - if let mtu = config.mtu { - scratchpad[.mtu] = String(mtu) - } - if let dns = config.dns { - scratchpad[.dns] = String(dns) - } - } - func validate() -> (success: Bool, errorMessage: String) { - var firstErrorMessage: String? = nil - func setErrorMessage(_ errorMessage: String) { - if (firstErrorMessage == nil) { - firstErrorMessage = errorMessage - } - } - fieldsWithError.removeAll() - guard let name = scratchpad[.name] else { - fieldsWithError.insert(.name) - return(false, "Interface name is required") - } - guard let privateKeyString = scratchpad[.privateKey] else { - fieldsWithError.insert(.privateKey) - return (false, "Interface's private key is required") - } - guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else { - fieldsWithError.insert(.privateKey) - return(false, "Interface's private key should be a 32-byte key in base64 encoding") - } - var config = InterfaceConfiguration(name: name, privateKey: privateKey) - if let addressesString = scratchpad[.addresses] { - var addresses: [IPAddressRange] = [] - for addressString in addressesString.split(separator: ",") { - let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - if let address = IPAddressRange(from: trimmedString) { - addresses.append(address) - } else { - fieldsWithError.insert(.addresses) - setErrorMessage("Interface addresses should be a list of comma-separated IP addresses in CIDR notation") - } - } - config.addresses = addresses - } - if let listenPortString = scratchpad[.listenPort] { - if let listenPort = UInt64(listenPortString) { - config.listenPort = listenPort - } else { - fieldsWithError.insert(.listenPort) - setErrorMessage("Interface's listen port should be a number") - } - } - if let mtuString = scratchpad[.mtu] { - if let mtu = UInt64(mtuString) { - config.mtu = mtu - } else { - fieldsWithError.insert(.mtu) - setErrorMessage("Interface's MTU should be a number") - } - } - // TODO: Validate DNS - if let dnsString = scratchpad[.dns] { - config.dns = dnsString - } - - if let firstErrorMessage = firstErrorMessage { - return (false, firstErrorMessage) - } - validatedConfiguration = config - return (true, "") - } - } - - class PeerData { - var index: Int - var scratchpad: [PeerEditField: String] = [:] - var fieldsWithError: Set = [] - var validatedConfiguration: PeerConfiguration? = nil - init(index: Int) { - self.index = index - } - subscript(field: PeerEditField) -> String { - get { - ensureScratchpadIsPrepared() // When starting to read a config, setup the scratchpad to serve as a cache - return scratchpad[field] ?? "" - } - set(stringValue) { - ensureScratchpadIsPrepared() // When starting to edit a config, setup the scratchpad - validatedConfiguration = nil // The configuration will need to be revalidated - if (stringValue.isEmpty) { - scratchpad.removeValue(forKey: field) - } else { - scratchpad[field] = stringValue - } - } - } - func ensureScratchpadIsPrepared() { - guard (scratchpad.isEmpty) else { return } - guard let config = validatedConfiguration else { return } - scratchpad[.publicKey] = config.publicKey.base64EncodedString() - if let preSharedKey = config.preSharedKey { - scratchpad[.preSharedKey] = preSharedKey.base64EncodedString() - } - if (!config.allowedIPs.isEmpty) { - scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ") - } - if let endpoint = config.endpoint { - scratchpad[.endpoint] = endpoint.stringRepresentation() - } - if let persistentKeepAlive = config.persistentKeepAlive { - scratchpad[.persistentKeepAlive] = String(persistentKeepAlive) - } - } - func validate() -> (success: Bool, errorMessage: String) { - var firstErrorMessage: String? = nil - func setErrorMessage(_ errorMessage: String) { - if (firstErrorMessage == nil) { - firstErrorMessage = errorMessage - } - } - fieldsWithError.removeAll() - guard let publicKeyString = scratchpad[.publicKey] else { - fieldsWithError.insert(.publicKey) - return (success: false, errorMessage: "Peer's public key is required") - } - guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else { - fieldsWithError.insert(.publicKey) - return (success: false, errorMessage: "Peer's public key should be a 32-byte key in base64 encoding") - } - var config = PeerConfiguration(publicKey: publicKey) - if let preSharedKeyString = scratchpad[.publicKey] { - if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 { - config.preSharedKey = preSharedKey - } else { - fieldsWithError.insert(.preSharedKey) - setErrorMessage("Peer's pre-shared key should be a 32-byte key in base64 encoding") - } - } - if let allowedIPsString = scratchpad[.allowedIPs] { - var allowedIPs: [IPAddressRange] = [] - for allowedIPString in allowedIPsString.split(separator: ",") { - let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - if let allowedIP = IPAddressRange(from: trimmedString) { - allowedIPs.append(allowedIP) - } else { - fieldsWithError.insert(.allowedIPs) - setErrorMessage("Peer's allowedIPs should be a list of comma-separated IP addresses in CIDR notation") - } - } - config.allowedIPs = allowedIPs - } - if let endpointString = scratchpad[.endpoint] { - if let endpoint = Endpoint(from: endpointString) { - config.endpoint = endpoint - } else { - fieldsWithError.insert(.endpoint) - setErrorMessage("Peer's endpoint should be of the form 'host:port' or '[host]:port'") - } - } - if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] { - if let persistentKeepAlive = UInt64(persistentKeepAliveString) { - config.persistentKeepAlive = persistentKeepAlive - } else { - fieldsWithError.insert(.persistentKeepAlive) - setErrorMessage("Peer's persistent keepalive should be a number") - } - } - - if let firstErrorMessage = firstErrorMessage { - return (false, firstErrorMessage) - } - validatedConfiguration = config - scratchpad = [:] - return (true, "") - } - } - - var interfaceData: InterfaceData - var peersData: [PeerData] - - // MARK: TunnelEditTableViewController methods + let tunnelViewModel: TunnelViewModel init() { - interfaceData = InterfaceData() - peersData = [] + tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil) super.init(style: .grouped) self.modalPresentationStyle = .formSheet } @@ -299,7 +66,7 @@ extension TunnelEditTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count - let numberOfPeers = peersData.count + let numberOfPeers = tunnelViewModel.peersData.count return numberOfInterfaceSections + (numberOfPeers * numberOfPeerSections) + 1 } @@ -307,7 +74,7 @@ extension TunnelEditTableViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count - let numberOfPeers = peersData.count + let numberOfPeers = tunnelViewModel.peersData.count if (section < numberOfInterfaceSections) { // Interface @@ -325,7 +92,7 @@ extension TunnelEditTableViewController { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count - let numberOfPeers = peersData.count + let numberOfPeers = tunnelViewModel.peersData.count if (section < numberOfInterfaceSections) { // Interface @@ -343,13 +110,14 @@ extension TunnelEditTableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count - let numberOfPeers = peersData.count + let numberOfPeers = tunnelViewModel.peersData.count let section = indexPath.section let row = indexPath.row if (section < numberOfInterfaceSections) { // Interface + let interfaceData = tunnelViewModel.interfaceData let field = interfaceEditFieldsBySection[section][row] if (field == .generateKeyPair) { let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell @@ -360,49 +128,18 @@ extension TunnelEditTableViewController { return cell } else { let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell + // Set key cell.key = field.rawValue - switch (field) { - case .name: + // Set placeholder text + if (field == .name || field == .privateKey) { cell.placeholderText = "Required" - cell.value = interfaceData[.name] - cell.onValueChanged = { [weak interfaceData] value in - interfaceData?[.name] = value - } - case .privateKey: - cell.placeholderText = "Required" - cell.value = interfaceData[.privateKey] - cell.onValueChanged = { [weak interfaceData] value in - interfaceData?[.privateKey] = value - } - case .publicKey: - cell.isValueEditable = false - cell.value = "Unimplemented" - case .generateKeyPair: - break - case .addresses: - cell.value = interfaceData[.addresses] - cell.onValueChanged = { [weak interfaceData] value in - interfaceData?[.addresses] = value - } - break - case .listenPort: - cell.value = interfaceData[.listenPort] - cell.onValueChanged = { [weak interfaceData] value in - interfaceData?[.listenPort] = value - } - break - case .mtu: + } else if (field == .mtu) { cell.placeholderText = "Automatic" - cell.value = interfaceData[.mtu] - cell.onValueChanged = { [weak interfaceData] value in - interfaceData?[.mtu] = value - } - case .dns: - cell.value = interfaceData[.dns] - cell.onValueChanged = { [weak interfaceData] value in - interfaceData?[.dns] = value - } - break + } + // Bind values to view model + cell.value = interfaceData[field] + cell.onValueChanged = { [weak interfaceData] value in + interfaceData?[field] = value } return cell } @@ -410,7 +147,7 @@ extension TunnelEditTableViewController { // Peer let peerIndex = Int((section - numberOfInterfaceSections) / numberOfPeerSections) let peerSectionIndex = (section - numberOfInterfaceSections) % numberOfPeerSections - let peerData = peersData[peerIndex] + let peerData = tunnelViewModel.peersData[peerIndex] let field = peerEditFieldsBySection[peerSectionIndex][row] if (field == .deletePeer) { let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell @@ -433,42 +170,16 @@ extension TunnelEditTableViewController { return cell } else { let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell + // Set key cell.key = field.rawValue - switch (field) { - case .publicKey: + // Set placeholder text + if (field == .publicKey) { cell.placeholderText = "Required" - cell.value = peerData[.publicKey] - cell.onValueChanged = { [weak peerData] value in - peerData?[.publicKey] = value - } - case .preSharedKey: - cell.value = peerData[.preSharedKey] - cell.onValueChanged = { [weak peerData] value in - peerData?[.preSharedKey] = value - } - break - case .endpoint: - cell.value = peerData[.endpoint] - cell.onValueChanged = { [weak peerData] value in - peerData?[.endpoint] = value - } - break - case .persistentKeepAlive: - cell.value = peerData[.persistentKeepAlive] - cell.onValueChanged = { [weak peerData] value in - peerData?[.persistentKeepAlive] = value - } - break - case .allowedIPs: - cell.value = peerData[.allowedIPs] - cell.onValueChanged = { [weak peerData] value in - peerData?[.allowedIPs] = value - } - break - case .excludePrivateIPs: - break - case .deletePeer: - break + } + // Bind values to view model + cell.value = peerData[field] + cell.onValueChanged = { [weak peerData] value in + peerData?[field] = value } return cell } @@ -489,29 +200,21 @@ extension TunnelEditTableViewController { func appendEmptyPeer() -> IndexSet { let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count - let numberOfPeers = peersData.count - let peer = PeerData(index: peersData.count) - peersData.append(peer) + tunnelViewModel.appendEmptyPeer() + let addedPeerIndex = tunnelViewModel.peersData.count - 1 - let firstAddedSectionIndex = (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections) + let firstAddedSectionIndex = (numberOfInterfaceSections + addedPeerIndex * numberOfPeerSections) let addedSectionIndices = IndexSet(integersIn: firstAddedSectionIndex ..< firstAddedSectionIndex + numberOfPeerSections) return addedSectionIndices } - func deletePeer(peer: PeerData) -> IndexSet { + func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet { let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count - let numberOfPeers = peersData.count - assert(peer.index < numberOfPeers) - - let removedPeer = peersData.remove(at: peer.index) - assert(removedPeer.index == peer.index) - for p in peersData[peer.index ..< peersData.count] { - assert(p.index > 0) - p.index = p.index - 1 - } + assert(peer.index < tunnelViewModel.peersData.count) + tunnelViewModel.deletePeer(peer: peer) let firstRemovedSectionIndex = (numberOfInterfaceSections + peer.index * numberOfPeerSections) let removedSectionIndices = IndexSet(integersIn: firstRemovedSectionIndex ..< firstRemovedSectionIndex + numberOfPeerSections)