Tunnel creation: Refactor by creating a separate view model

This commit is contained in:
Roopesh Chander 2018-10-23 15:23:18 +05:30
parent 9f252d4e37
commit 0fa97c38ed
3 changed files with 346 additions and 330 deletions

View File

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; }; 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; };
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; }; 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; };
@ -21,6 +22,7 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; };
6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; }; 6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; }; 6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -54,6 +56,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F7774DE217181B1006A79B3 /* iOS */, 6F7774DE217181B1006A79B3 /* iOS */,
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */,
); );
path = UI; path = UI;
sourceTree = "<group>"; sourceTree = "<group>";
@ -206,6 +209,7 @@
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */, 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */, 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,

View File

@ -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<InterfaceEditField> = []
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<InterfaceConfiguration> {
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<PeerEditField> = []
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<PeerConfiguration> {
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<Configuration> {
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<TunnelConfiguration> {
// 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)
}
}
}

View File

@ -12,256 +12,23 @@ import UIKit
class TunnelEditTableViewController: UITableViewController { class TunnelEditTableViewController: UITableViewController {
// MARK: View model let interfaceEditFieldsBySection: [[TunnelViewModel.InterfaceEditField]] = [
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]] = [
[.name], [.name],
[.privateKey, .publicKey, .generateKeyPair], [.privateKey, .publicKey, .generateKeyPair],
[.addresses, .listenPort, .mtu, .dns] [.addresses, .listenPort, .mtu, .dns]
] ]
enum PeerEditField: String { let peerEditFieldsBySection: [[TunnelViewModel.PeerEditField]] = [
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]] = [
[.publicKey, .preSharedKey, .endpoint, [.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .excludePrivateIPs, .allowedIPs, .excludePrivateIPs,
.persistentKeepAlive, .persistentKeepAlive,
.deletePeer] .deletePeer]
] ]
// Scratchpad for entered data let tunnelViewModel: TunnelViewModel
class InterfaceData {
var scratchpad: [InterfaceEditField: String] = [:]
var fieldsWithError: Set<InterfaceEditField> = []
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<PeerEditField> = []
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
init() { init() {
interfaceData = InterfaceData() tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
peersData = []
super.init(style: .grouped) super.init(style: .grouped)
self.modalPresentationStyle = .formSheet self.modalPresentationStyle = .formSheet
} }
@ -299,7 +66,7 @@ extension TunnelEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfInterfaceSections = interfaceEditFieldsBySection.count
let numberOfPeerSections = peerEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count
let numberOfPeers = peersData.count let numberOfPeers = tunnelViewModel.peersData.count
return numberOfInterfaceSections + (numberOfPeers * numberOfPeerSections) + 1 return numberOfInterfaceSections + (numberOfPeers * numberOfPeerSections) + 1
} }
@ -307,7 +74,7 @@ extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfInterfaceSections = interfaceEditFieldsBySection.count
let numberOfPeerSections = peerEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count
let numberOfPeers = peersData.count let numberOfPeers = tunnelViewModel.peersData.count
if (section < numberOfInterfaceSections) { if (section < numberOfInterfaceSections) {
// Interface // Interface
@ -325,7 +92,7 @@ extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfInterfaceSections = interfaceEditFieldsBySection.count
let numberOfPeerSections = peerEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count
let numberOfPeers = peersData.count let numberOfPeers = tunnelViewModel.peersData.count
if (section < numberOfInterfaceSections) { if (section < numberOfInterfaceSections) {
// Interface // Interface
@ -343,13 +110,14 @@ extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfInterfaceSections = interfaceEditFieldsBySection.count
let numberOfPeerSections = peerEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count
let numberOfPeers = peersData.count let numberOfPeers = tunnelViewModel.peersData.count
let section = indexPath.section let section = indexPath.section
let row = indexPath.row let row = indexPath.row
if (section < numberOfInterfaceSections) { if (section < numberOfInterfaceSections) {
// Interface // Interface
let interfaceData = tunnelViewModel.interfaceData
let field = interfaceEditFieldsBySection[section][row] let field = interfaceEditFieldsBySection[section][row]
if (field == .generateKeyPair) { if (field == .generateKeyPair) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell
@ -360,49 +128,18 @@ extension TunnelEditTableViewController {
return cell return cell
} else { } else {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell
// Set key
cell.key = field.rawValue cell.key = field.rawValue
switch (field) { // Set placeholder text
case .name: if (field == .name || field == .privateKey) {
cell.placeholderText = "Required" cell.placeholderText = "Required"
cell.value = interfaceData[.name] } else if (field == .mtu) {
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:
cell.placeholderText = "Automatic" cell.placeholderText = "Automatic"
cell.value = interfaceData[.mtu]
cell.onValueChanged = { [weak interfaceData] value in
interfaceData?[.mtu] = value
} }
case .dns: // Bind values to view model
cell.value = interfaceData[.dns] cell.value = interfaceData[field]
cell.onValueChanged = { [weak interfaceData] value in cell.onValueChanged = { [weak interfaceData] value in
interfaceData?[.dns] = value interfaceData?[field] = value
}
break
} }
return cell return cell
} }
@ -410,7 +147,7 @@ extension TunnelEditTableViewController {
// Peer // Peer
let peerIndex = Int((section - numberOfInterfaceSections) / numberOfPeerSections) let peerIndex = Int((section - numberOfInterfaceSections) / numberOfPeerSections)
let peerSectionIndex = (section - numberOfInterfaceSections) % numberOfPeerSections let peerSectionIndex = (section - numberOfInterfaceSections) % numberOfPeerSections
let peerData = peersData[peerIndex] let peerData = tunnelViewModel.peersData[peerIndex]
let field = peerEditFieldsBySection[peerSectionIndex][row] let field = peerEditFieldsBySection[peerSectionIndex][row]
if (field == .deletePeer) { if (field == .deletePeer) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell
@ -433,42 +170,16 @@ extension TunnelEditTableViewController {
return cell return cell
} else { } else {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell
// Set key
cell.key = field.rawValue cell.key = field.rawValue
switch (field) { // Set placeholder text
case .publicKey: if (field == .publicKey) {
cell.placeholderText = "Required" cell.placeholderText = "Required"
cell.value = peerData[.publicKey]
cell.onValueChanged = { [weak peerData] value in
peerData?[.publicKey] = value
} }
case .preSharedKey: // Bind values to view model
cell.value = peerData[.preSharedKey] cell.value = peerData[field]
cell.onValueChanged = { [weak peerData] value in cell.onValueChanged = { [weak peerData] value in
peerData?[.preSharedKey] = value peerData?[field] = 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
} }
return cell return cell
} }
@ -489,29 +200,21 @@ extension TunnelEditTableViewController {
func appendEmptyPeer() -> IndexSet { func appendEmptyPeer() -> IndexSet {
let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfInterfaceSections = interfaceEditFieldsBySection.count
let numberOfPeerSections = peerEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count
let numberOfPeers = peersData.count
let peer = PeerData(index: peersData.count) tunnelViewModel.appendEmptyPeer()
peersData.append(peer) let addedPeerIndex = tunnelViewModel.peersData.count - 1
let firstAddedSectionIndex = (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections) let firstAddedSectionIndex = (numberOfInterfaceSections + addedPeerIndex * numberOfPeerSections)
let addedSectionIndices = IndexSet(integersIn: firstAddedSectionIndex ..< firstAddedSectionIndex + numberOfPeerSections) let addedSectionIndices = IndexSet(integersIn: firstAddedSectionIndex ..< firstAddedSectionIndex + numberOfPeerSections)
return addedSectionIndices return addedSectionIndices
} }
func deletePeer(peer: PeerData) -> IndexSet { func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
let numberOfInterfaceSections = interfaceEditFieldsBySection.count let numberOfInterfaceSections = interfaceEditFieldsBySection.count
let numberOfPeerSections = peerEditFieldsBySection.count let numberOfPeerSections = peerEditFieldsBySection.count
let numberOfPeers = peersData.count
assert(peer.index < numberOfPeers) assert(peer.index < tunnelViewModel.peersData.count)
tunnelViewModel.deletePeer(peer: peer)
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
}
let firstRemovedSectionIndex = (numberOfInterfaceSections + peer.index * numberOfPeerSections) let firstRemovedSectionIndex = (numberOfInterfaceSections + peer.index * numberOfPeerSections)
let removedSectionIndices = IndexSet(integersIn: firstRemovedSectionIndex ..< firstRemovedSectionIndex + numberOfPeerSections) let removedSectionIndices = IndexSet(integersIn: firstRemovedSectionIndex ..< firstRemovedSectionIndex + numberOfPeerSections)