UI: More elegant copy-to-clipboard behavior

This commit is contained in:
Jason A. Donenfeld 2018-10-31 01:00:27 +01:00
parent 264adcdc9a
commit 34b9023f67
5 changed files with 87 additions and 59 deletions

View File

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; }; 6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; }; 6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -72,6 +73,7 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; }; 6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; }; 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
@ -186,6 +188,7 @@
6F7774DE217181B1006A79B3 /* iOS */ = { 6F7774DE217181B1006A79B3 /* iOS */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */, 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
6F7774E0217181B1006A79B3 /* AppDelegate.swift */, 6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */, 6F7774DF217181B1006A79B3 /* MainViewController.swift */,
@ -439,6 +442,7 @@
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */, 6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */, 6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */, 6F6899A62180447E0012E523 /* x25519.c in Sources */,

View File

@ -10,7 +10,6 @@ class TunnelViewModel {
case privateKey = "Private key" case privateKey = "Private key"
case publicKey = "Public key" case publicKey = "Public key"
case generateKeyPair = "Generate keypair" case generateKeyPair = "Generate keypair"
case copyPublicKey = "Copy public key"
case addresses = "Addresses" case addresses = "Addresses"
case listenPort = "Listen port" case listenPort = "Listen port"
case mtu = "MTU" case mtu = "MTU"
@ -18,7 +17,7 @@ class TunnelViewModel {
} }
static let interfaceFieldsWithControl: Set<InterfaceField> = [ static let interfaceFieldsWithControl: Set<InterfaceField> = [
.generateKeyPair, .copyPublicKey .generateKeyPair
] ]
enum PeerField: String { enum PeerField: String {

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class CopyableLabelTableViewCell: UITableViewCell {
var copyableGesture = true
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
self.addGestureRecognizer(gestureRecognizer)
self.isUserInteractionEnabled = true
}
// MARK: - UIGestureRecognizer
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
if !self.copyableGesture {
return
}
guard recognizer.state == .recognized else { return }
if let recognizerView = recognizer.view,
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
let menuController = UIMenuController.shared
menuController.setTargetRect(self.detailTextLabel?.frame ?? recognizerView.frame, in: self.detailTextLabel?.superview ?? recognizerSuperView)
menuController.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = self.detailTextLabel?.text
}
override func prepareForReuse() {
super.prepareForReuse()
self.copyableGesture = true
}
}

View File

@ -7,10 +7,9 @@ import UIKit
class TunnelDetailTableViewController: UITableViewController { class TunnelDetailTableViewController: UITableViewController {
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [ let interfaceFields: [TunnelViewModel.InterfaceField] = [
[.name], .name, .publicKey, .addresses,
[.publicKey, .copyPublicKey], .listenPort, .mtu, .dns
[.addresses, .listenPort, .mtu, .dns]
] ]
let peerFields: [TunnelViewModel.PeerField] = [ let peerFields: [TunnelViewModel.PeerField] = [
@ -95,32 +94,22 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController { extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
let interfaceData = tunnelViewModel.interfaceData return 3 + tunnelViewModel.peersData.count
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
}.count
let numberOfPeerSections = tunnelViewModel.peersData.count
return 1 + numberOfInterfaceSections + numberOfPeerSections + 1
} }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let interfaceData = tunnelViewModel.interfaceData let interfaceData = tunnelViewModel.interfaceData
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
}.count
let numberOfPeerSections = tunnelViewModel.peersData.count let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) { if (section == 0) {
// Status // Status
return 1 return 1
} else if (section < (1 + numberOfInterfaceSections)) { } else if (section == 1) {
// Interface // Interface
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section - 1]).count return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) { } else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer // Peer
let peerIndex = (section - numberOfInterfaceSections - 1) let peerData = tunnelViewModel.peersData[section - 2]
let peerData = tunnelViewModel.peersData[peerIndex]
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
} else { } else {
// Delete tunnel // Delete tunnel
@ -129,32 +118,25 @@ extension TunnelDetailTableViewController {
} }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let interfaceData = tunnelViewModel.interfaceData
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
}.count
let numberOfPeerSections = tunnelViewModel.peersData.count let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) { if (section == 0) {
// Status // Status
return "Status" return "Status"
} else if (section < 1 + numberOfInterfaceSections) { } else if (section == 1) {
// Interface // Interface
return (section == 1) ? "Interface" : nil return "Interface"
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) { } else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer // Peer
return "Peer" return "Peer"
} else { } else {
// Add peer // Delete tunnel
return nil return nil
} }
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let interfaceData = tunnelViewModel.interfaceData let interfaceData = tunnelViewModel.interfaceData
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
}.count
let numberOfPeerSections = tunnelViewModel.peersData.count let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section let section = indexPath.section
@ -186,32 +168,22 @@ extension TunnelDetailTableViewController {
} }
} }
return cell return cell
} else if (section < 1 + numberOfInterfaceSections) { } else if (section == 1) {
// Interface // Interface
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section - 1])[row] let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[row]
if (field == .copyPublicKey) { let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell // Set key and value
cell.buttonText = field.rawValue cell.key = field.rawValue
cell.onTapped = { cell.value = interfaceData[field]
UIPasteboard.general.string = interfaceData[.publicKey] if (field != .publicKey) {
} cell.detailTextLabel?.allowsDefaultTighteningForTruncation = true
return cell cell.detailTextLabel?.adjustsFontSizeToFitWidth = true
} else { cell.detailTextLabel?.minimumScaleFactor = 0.85
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
// Set key and value
cell.key = field.rawValue
cell.value = interfaceData[field]
if (field != .publicKey) {
cell.detailTextLabel?.allowsDefaultTighteningForTruncation = true
cell.detailTextLabel?.adjustsFontSizeToFitWidth = true
cell.detailTextLabel?.minimumScaleFactor = 0.85
}
return cell
} }
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) { return cell
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer // Peer
let peerIndex = (section - numberOfInterfaceSections - 1) let peerData = tunnelViewModel.peersData[section - 2]
let peerData = tunnelViewModel.peersData[peerIndex]
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row] let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row]
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
@ -226,7 +198,7 @@ extension TunnelDetailTableViewController {
return cell return cell
} else { } else {
assert(section == (1 + numberOfInterfaceSections + numberOfPeerSections)) assert(section == (2 + numberOfPeerSections))
// Delete configuration // Delete configuration
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
cell.buttonText = "Delete tunnel" cell.buttonText = "Delete tunnel"
@ -328,7 +300,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
} }
} }
class TunnelDetailTableViewKeyValueCell: UITableViewCell { class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelDetailTableViewKeyValueCell" static let id: String = "TunnelDetailTableViewKeyValueCell"
var key: String { var key: String {
get { return textLabel?.text ?? "" } get { return textLabel?.text ?? "" }
@ -355,7 +327,7 @@ class TunnelDetailTableViewKeyValueCell: UITableViewCell {
} }
class TunnelDetailTableViewButtonCell: UITableViewCell { class TunnelDetailTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelsEditTableViewButtonCell" static let id: String = "TunnelDetailTableViewButtonCell"
var buttonText: String { var buttonText: String {
get { return button.title(for: .normal) ?? "" } get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) } set(value) { button.setTitle(value, for: .normal) }

View File

@ -361,7 +361,7 @@ extension TunnelEditTableViewController {
} }
} }
class TunnelEditTableViewKeyValueCell: UITableViewCell { class TunnelEditTableViewKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelEditTableViewKeyValueCell" static let id: String = "TunnelEditTableViewKeyValueCell"
var key: String { var key: String {
get { return keyLabel.text ?? "" } get { return keyLabel.text ?? "" }
@ -378,6 +378,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
var isValueEditable: Bool { var isValueEditable: Bool {
get { return valueTextField.isEnabled } get { return valueTextField.isEnabled }
set(value) { set(value) {
super.copyableGesture = !value
valueTextField.isEnabled = value valueTextField.isEnabled = value
keyLabel.textColor = value ? UIColor.black : UIColor.gray keyLabel.textColor = value ? UIColor.black : UIColor.gray
valueTextField.textColor = value ? UIColor.black : UIColor.gray valueTextField.textColor = value ? UIColor.black : UIColor.gray
@ -409,6 +410,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
keyLabel = UILabel() keyLabel = UILabel()
valueTextField = UITextField() valueTextField = UITextField()
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
isValueEditable = true
contentView.addSubview(keyLabel) contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right keyLabel.textAlignment = .right