Tunnel creation: Start off with tunnel creation
This commit is contained in:
parent
e337427eae
commit
bbd13ad4cf
|
@ -14,6 +14,7 @@
|
|||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
|
||||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; };
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; };
|
||||
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
|
||||
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; };
|
||||
6FF4AC472120B9E0002C96EB /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */; };
|
||||
|
@ -27,6 +28,7 @@
|
|||
6F7774E72172020C006A79B3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = "<group>"; };
|
||||
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; };
|
||||
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
|
||||
6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
|
@ -62,6 +64,7 @@
|
|||
6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
|
||||
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
|
||||
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
|
||||
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
|
||||
);
|
||||
path = iOS;
|
||||
sourceTree = "<group>";
|
||||
|
@ -205,6 +208,7 @@
|
|||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
|
||||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
|
||||
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -0,0 +1,451 @@
|
|||
//
|
||||
// TunnelEditTableViewController.swift
|
||||
// WireGuard
|
||||
//
|
||||
// Created by Roopesh Chander on 17/10/18.
|
||||
// Copyright © 2018 WireGuard LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: TunnelEditTableViewController
|
||||
|
||||
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]] = [
|
||||
[.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]] = [
|
||||
[.publicKey, .preSharedKey, .endpoint,
|
||||
.allowedIPs, .excludePrivateIPs,
|
||||
.persistentKeepAlive,
|
||||
.deletePeer]
|
||||
]
|
||||
|
||||
// Scratchpad for entered data
|
||||
|
||||
class InterfaceDataSource {
|
||||
var scratchpad: [InterfaceEditField: (value: String, isValid: Bool)] = [:]
|
||||
}
|
||||
|
||||
class PeerDataSource {
|
||||
var index: Int
|
||||
var scratchpad: [PeerEditField: (value: String, isValid: Bool)] = [:]
|
||||
init(index: Int) {
|
||||
self.index = index
|
||||
}
|
||||
}
|
||||
|
||||
var interfaceData: InterfaceDataSource
|
||||
var peersData: [PeerDataSource]
|
||||
|
||||
// MARK: TunnelEditTableViewController methods
|
||||
|
||||
init() {
|
||||
interfaceData = InterfaceDataSource()
|
||||
peersData = []
|
||||
super.init(style: .grouped)
|
||||
self.modalPresentationStyle = .formSheet
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.title = "New configuration"
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
|
||||
|
||||
self.tableView.rowHeight = 44
|
||||
self.tableView.allowsSelection = false
|
||||
|
||||
self.tableView.register(TunnelsEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelsEditTableViewKeyValueCell.id)
|
||||
self.tableView.register(TunnelsEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelsEditTableViewButtonCell.id)
|
||||
self.tableView.register(TunnelsEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelsEditTableViewSwitchCell.id)
|
||||
}
|
||||
|
||||
@objc func saveTapped() {
|
||||
print("Save")
|
||||
}
|
||||
|
||||
@objc func cancelTapped() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
|
||||
extension TunnelEditTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
|
||||
return numberOfInterfaceSections + (numberOfPeers * numberOfPeerSections) + 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
return interfaceEditFieldsBySection[section].count
|
||||
} else if ((numberOfPeers > 0) && (section < (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections))) {
|
||||
// Peer
|
||||
let fieldIndex = (section - numberOfInterfaceSections) % numberOfPeerSections
|
||||
return peerEditFieldsBySection[fieldIndex].count
|
||||
} else {
|
||||
// Add peer
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
return (section == 0) ? "Interface" : nil
|
||||
} else if ((numberOfPeers > 0) && (section < (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections))) {
|
||||
// Peer
|
||||
let fieldIndex = (section - numberOfInterfaceSections) % numberOfPeerSections
|
||||
return (fieldIndex == 0) ? "Peer" : nil
|
||||
} else {
|
||||
// Add peer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
let field = interfaceEditFieldsBySection[section][row]
|
||||
if (field == .generateKeyPair) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = {
|
||||
print("Generating keypair is unimplemented") // TODO
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell
|
||||
cell.key = field.rawValue
|
||||
switch (field) {
|
||||
case .name:
|
||||
cell.placeholderText = "Required"
|
||||
case .privateKey:
|
||||
cell.placeholderText = "Required"
|
||||
case .publicKey:
|
||||
cell.isValueEditable = false
|
||||
case .generateKeyPair:
|
||||
break
|
||||
case .addresses:
|
||||
break
|
||||
case .listenPort:
|
||||
break
|
||||
case .mtu:
|
||||
cell.placeholderText = "Automatic"
|
||||
case .dns:
|
||||
break
|
||||
}
|
||||
return cell
|
||||
}
|
||||
} else if ((numberOfPeers > 0) && (section < (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections))) {
|
||||
// Peer
|
||||
let peerIndex = Int((section - numberOfInterfaceSections) / numberOfPeerSections)
|
||||
let peerSectionIndex = (section - numberOfInterfaceSections) % numberOfPeerSections
|
||||
let peerData = peersData[peerIndex]
|
||||
let field = peerEditFieldsBySection[peerSectionIndex][row]
|
||||
if (field == .deletePeer) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self, weak peerData] in
|
||||
guard let peerData = peerData else { return }
|
||||
guard let s = self else { return }
|
||||
s.showConfirmationAlert(message: "Delete this peer?",
|
||||
buttonTitle: "Delete", from: cell,
|
||||
onConfirmed: { [weak s] in
|
||||
guard let s = s else { return }
|
||||
let removedSectionIndices = s.deletePeer(peer: peerData)
|
||||
s.tableView.deleteSections(removedSectionIndices, with: .automatic)
|
||||
})
|
||||
}
|
||||
return cell
|
||||
} else if (field == .excludePrivateIPs) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewSwitchCell.id, for: indexPath) as! TunnelsEditTableViewSwitchCell
|
||||
cell.message = field.rawValue
|
||||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell
|
||||
cell.key = field.rawValue
|
||||
switch (field) {
|
||||
case .publicKey:
|
||||
cell.placeholderText = "Required"
|
||||
case .preSharedKey:
|
||||
break
|
||||
case .endpoint:
|
||||
break
|
||||
case .persistentKeepAlive:
|
||||
cell.hasLongKey = true
|
||||
break
|
||||
case .allowedIPs:
|
||||
break
|
||||
case .excludePrivateIPs:
|
||||
break
|
||||
case .deletePeer:
|
||||
break
|
||||
}
|
||||
return cell
|
||||
}
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections))
|
||||
// Add peer
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell
|
||||
cell.buttonText = "Add peer"
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let s = self else { return }
|
||||
let addedSectionIndices = s.appendEmptyPeer()
|
||||
tableView.insertSections(addedSectionIndices, with: .automatic)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
func appendEmptyPeer() -> IndexSet {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
|
||||
let peer = PeerDataSource(index: peersData.count)
|
||||
peersData.append(peer)
|
||||
|
||||
let firstAddedSectionIndex = (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections)
|
||||
let addedSectionIndices = IndexSet(integersIn: firstAddedSectionIndex ..< firstAddedSectionIndex + numberOfPeerSections)
|
||||
return addedSectionIndices
|
||||
}
|
||||
|
||||
func deletePeer(peer: PeerDataSource) -> 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
|
||||
}
|
||||
|
||||
let firstRemovedSectionIndex = (numberOfInterfaceSections + peer.index * numberOfPeerSections)
|
||||
let removedSectionIndices = IndexSet(integersIn: firstRemovedSectionIndex ..< firstRemovedSectionIndex + numberOfPeerSections)
|
||||
return removedSectionIndices
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
|
||||
onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (action) 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
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelsEditTableViewKeyValueCell: UITableViewCell {
|
||||
static let id: String = "TunnelsEditTableViewKeyValueCell"
|
||||
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 isValueEditable: Bool {
|
||||
get { return valueTextField.isEnabled }
|
||||
set(value) {
|
||||
valueTextField.isEnabled = value
|
||||
keyLabel.textColor = value ? UIColor.black : UIColor.gray
|
||||
}
|
||||
}
|
||||
var hasLongKey: Bool {
|
||||
get { return modifiableWidthRatioConstraint!.constant > 0 }
|
||||
set(value) {
|
||||
if (value) {
|
||||
modifiableWidthRatioConstraint!.constant = 40
|
||||
} else {
|
||||
modifiableWidthRatioConstraint!.constant = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
var isValueValid: Bool = true {
|
||||
didSet(value) {
|
||||
if (value) {
|
||||
keyLabel.textColor = isValueEditable ? UIColor.black : UIColor.gray
|
||||
} else {
|
||||
keyLabel.textColor = UIColor.red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let keyLabel: UILabel
|
||||
let valueTextField: UITextField
|
||||
private var modifiableWidthRatioConstraint: NSLayoutConstraint? = nil
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
keyLabel = UILabel()
|
||||
valueTextField = UITextField()
|
||||
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)
|
||||
NSLayoutConstraint.activate([
|
||||
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
keyLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 8),
|
||||
widthRatioConstraint
|
||||
])
|
||||
modifiableWidthRatioConstraint = widthRatioConstraint
|
||||
contentView.addSubview(valueTextField)
|
||||
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
valueTextField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
valueTextField.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 16),
|
||||
valueTextField.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -8),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
key = ""
|
||||
value = ""
|
||||
placeholderText = ""
|
||||
isValueEditable = true
|
||||
hasLongKey = false
|
||||
isValueValid = true
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelsEditTableViewButtonCell: UITableViewCell {
|
||||
static let id: String = "TunnelsEditTableViewButtonCell"
|
||||
var buttonText: String {
|
||||
get { return button.title(for: .normal) ?? "" }
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
var onTapped: (() -> Void)? = nil
|
||||
|
||||
let button: UIButton
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
button = UIButton(type: .system)
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
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() {
|
||||
buttonText = ""
|
||||
onTapped = nil
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelsEditTableViewSwitchCell: UITableViewCell {
|
||||
static let id: String = "TunnelsEditTableViewSwitchCell"
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel!.text = value }
|
||||
}
|
||||
var isOn: Bool {
|
||||
get { return switchView.isOn }
|
||||
set(value) { switchView.isOn = value }
|
||||
}
|
||||
|
||||
let switchView: UISwitch
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
switchView = UISwitch()
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
accessoryView = switchView
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
message = ""
|
||||
isOn = false
|
||||
}
|
||||
}
|
|
@ -41,7 +41,11 @@ class TunnelsListTableViewController: UITableViewController {
|
|||
preferredStyle: .actionSheet)
|
||||
alert.addAction(
|
||||
UIAlertAction(title: "Create from scratch", style: .default) { (action) in
|
||||
print("Write")
|
||||
let editVC = TunnelEditTableViewController()
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
self.present(editNC, animated: true) {
|
||||
print("Done")
|
||||
}
|
||||
}
|
||||
)
|
||||
alert.addAction(
|
||||
|
|
Loading…
Reference in New Issue