aboutsummaryrefslogtreecommitdiffstats
path: root/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift
diff options
context:
space:
mode:
authorEric Kuck <eric@bluelinelabs.com>2018-12-13 12:58:50 -0600
committerEric Kuck <eric@bluelinelabs.com>2018-12-13 12:58:50 -0600
commit05d750539b91eff582ff6a789fcdcab73bb5f7bb (patch)
tree0a59939a0805567ea1c4b310d78e4d4c9394cb96 /WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift
parentAvoid escaping heap allocation (diff)
downloadwireguard-apple-05d750539b91eff582ff6a789fcdcab73bb5f7bb.tar.xz
wireguard-apple-05d750539b91eff582ff6a789fcdcab73bb5f7bb.zip
Reorganized ViewControllers (split out UIViews and UITableViewCells into their own classes)
All swiftlint warnings except one fixed up Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
Diffstat (limited to 'WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift')
-rw-r--r--WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift453
1 files changed, 0 insertions, 453 deletions
diff --git a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift
deleted file mode 100644
index efa85e6..0000000
--- a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift
+++ /dev/null
@@ -1,453 +0,0 @@
-// SPDX-License-Identifier: MIT
-// Copyright © 2018 WireGuard LLC. All Rights Reserved.
-
-import UIKit
-import MobileCoreServices
-import UserNotifications
-
-class TunnelsListTableViewController: UIViewController {
-
- var tunnelsManager: TunnelsManager?
-
- var busyIndicator: UIActivityIndicatorView?
- var centeredAddButton: BorderedTextButton?
- var tableView: UITableView?
-
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = UIColor.white
-
- // 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(style: .gray)
- busyIndicator.hidesWhenStopped = true
-
- // 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"
- }
-
- 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
- }
-
- override func viewWillAppear(_: Bool) {
- // Remove selection when getting back to the list view on iPhone
- if let tableView = self.tableView, let selectedRowIndexPath = tableView.indexPathForSelectedRow {
- tableView.deselectRow(at: selectedRowIndexPath, animated: false)
- }
- }
-
- @objc func addButtonTapped(sender: AnyObject) {
- if self.tunnelsManager == nil { return } // Do nothing until we've loaded the tunnels
- let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet)
- let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in
- self?.presentViewControllerForFileImport()
- }
- alert.addAction(importFileAction)
-
- let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] _ in
- self?.presentViewControllerForScanningQRCode()
- }
- alert.addAction(scanQRCodeAction)
-
- let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] _ in
- if let self = self, let tunnelsManager = self.tunnelsManager {
- self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil)
- }
- }
- alert.addAction(createFromScratchAction)
-
- let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
- alert.addAction(cancelAction)
-
- // popoverPresentationController will be nil on iPhone and non-nil on iPad
- if let sender = sender as? UIBarButtonItem {
- alert.popoverPresentationController?.barButtonItem = sender
- } else if let sender = sender as? UIView {
- alert.popoverPresentationController?.sourceView = sender
- alert.popoverPresentationController?.sourceRect = sender.bounds
- }
- self.present(alert, animated: true, completion: nil)
- }
-
- @objc func settingsButtonTapped(sender: UIBarButtonItem!) {
- if self.tunnelsManager == nil { return } // Do nothing until we've loaded the tunnels
- let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
- let settingsNC = UINavigationController(rootViewController: settingsVC)
- settingsNC.modalPresentationStyle = .formSheet
- self.present(settingsNC, animated: true)
- }
-
- func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
- let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
- let editNC = UINavigationController(rootViewController: editVC)
- editNC.modalPresentationStyle = .formSheet
- self.present(editNC, animated: true)
- }
-
- func presentViewControllerForFileImport() {
- let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
- let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
- filePicker.delegate = self
- self.present(filePicker, animated: true)
- }
-
- func presentViewControllerForScanningQRCode() {
- let scanQRCodeVC = QRScanViewController()
- scanQRCodeVC.delegate = self
- let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
- scanQRCodeNC.modalPresentationStyle = .fullScreen
- self.present(scanQRCodeNC, animated: true)
- }
-
- func importFromFile(url: URL, completionHandler: (() -> Void)?) {
- guard let tunnelsManager = tunnelsManager else { return }
- if url.pathExtension == "zip" {
- ZipImporter.importConfigFiles(from: url) { [weak self] result in
- if let error = result.error {
- ErrorPresenter.showErrorAlert(error: error, from: self)
- return
- }
- let configs = result.value!
- tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] numberSuccessful in
- if numberSuccessful == configs.count {
- completionHandler?()
- return
- }
- ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
- message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive",
- from: self, onPresented: completionHandler)
- }
- }
- } else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
- let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
- if let fileContents = try? String(contentsOf: url),
- let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
- tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
- if let error = result.error {
- ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: completionHandler)
- } else {
- completionHandler?()
- }
- }
- } else {
- ErrorPresenter.showErrorAlert(title: "Unable to import tunnel",
- message: "An error occured when importing the tunnel configuration.",
- from: self, onPresented: completionHandler)
- }
- }
- }
-}
-
-// MARK: UIDocumentPickerDelegate
-
-extension TunnelsListTableViewController: UIDocumentPickerDelegate {
- func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
- urls.forEach {
- importFromFile(url: $0, completionHandler: nil)
- }
- }
-}
-
-// MARK: QRScanViewControllerDelegate
-
-extension TunnelsListTableViewController: QRScanViewControllerDelegate {
- func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
- completionHandler: (() -> Void)?) {
- tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
- if let error = result.error {
- ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
- } else {
- completionHandler?()
- }
- }
- }
-}
-
-// MARK: UITableViewDataSource
-
-extension TunnelsListTableViewController: UITableViewDataSource {
- func numberOfSections(in tableView: UITableView) -> Int {
- return 1
- }
-
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return (tunnelsManager?.numberOfTunnels() ?? 0)
- }
-
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell: TunnelCell = tableView.dequeueReusableCell(for: indexPath)
- if let tunnelsManager = tunnelsManager {
- let tunnel = tunnelsManager.tunnel(at: indexPath.row)
- cell.tunnel = tunnel
- cell.onSwitchToggled = { [weak self] isOn in
- guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
- if isOn {
- tunnelsManager.startActivation(of: tunnel)
- } else {
- tunnelsManager.startDeactivation(of: tunnel)
- }
- }
- }
- return cell
- }
-}
-
-// MARK: UITableViewDelegate
-
-extension TunnelsListTableViewController: UITableViewDelegate {
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- guard let tunnelsManager = tunnelsManager else { return }
- let tunnel = tunnelsManager.tunnel(at: indexPath.row)
- let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
- tunnel: tunnel)
- let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
- tunnelDetailNC.restorationIdentifier = "DetailNC"
- showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
- }
-
- func tableView(_ tableView: UITableView,
- trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
- let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in
- guard let tunnelsManager = self?.tunnelsManager else { return }
- let tunnel = tunnelsManager.tunnel(at: indexPath.row)
- tunnelsManager.remove(tunnel: tunnel) { error in
- if error != nil {
- ErrorPresenter.showErrorAlert(error: error!, from: self)
- completionHandler(false)
- } else {
- completionHandler(true)
- }
- }
- }
- return UISwipeActionsConfiguration(actions: [deleteAction])
- }
-}
-
-// MARK: TunnelsManagerDelegate
-
-extension TunnelsListTableViewController: TunnelsManagerListDelegate {
- func tunnelAdded(at index: Int) {
- 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)
- }
-
- func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
- 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")
- }
-}