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 = {
/* 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 */; };
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, ); }; };
@ -72,6 +73,7 @@
/* End PBXCopyFilesBuildPhase 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>"; };
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>"; };
@ -186,6 +188,7 @@
6F7774DE217181B1006A79B3 /* iOS */ = {
isa = PBXGroup;
children = (
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
@ -439,6 +442,7 @@
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */,

View File

@ -10,7 +10,6 @@ class TunnelViewModel {
case privateKey = "Private key"
case publicKey = "Public key"
case generateKeyPair = "Generate keypair"
case copyPublicKey = "Copy public key"
case addresses = "Addresses"
case listenPort = "Listen port"
case mtu = "MTU"
@ -18,7 +17,7 @@ class TunnelViewModel {
}
static let interfaceFieldsWithControl: Set<InterfaceField> = [
.generateKeyPair, .copyPublicKey
.generateKeyPair
]
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 {
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
[.name],
[.publicKey, .copyPublicKey],
[.addresses, .listenPort, .mtu, .dns]
let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
]
let peerFields: [TunnelViewModel.PeerField] = [
@ -95,32 +94,22 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
let interfaceData = tunnelViewModel.interfaceData
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
return 3 + tunnelViewModel.peersData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let interfaceData = tunnelViewModel.interfaceData
let numberOfInterfaceSections = (0 ..< interfaceFieldsBySection.count).filter { section in
(!interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section]).isEmpty)
}.count
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) {
// Status
return 1
} else if (section < (1 + numberOfInterfaceSections)) {
} else if (section == 1) {
// Interface
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section - 1]).count
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) {
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
let peerIndex = (section - numberOfInterfaceSections - 1)
let peerData = tunnelViewModel.peersData[peerIndex]
let peerData = tunnelViewModel.peersData[section - 2]
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
} else {
// Delete tunnel
@ -129,32 +118,25 @@ extension TunnelDetailTableViewController {
}
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
if (section == 0) {
// Status
return "Status"
} else if (section < 1 + numberOfInterfaceSections) {
} else if (section == 1) {
// Interface
return (section == 1) ? "Interface" : nil
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) {
return "Interface"
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
return "Peer"
} else {
// Add peer
// Delete tunnel
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
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 section = indexPath.section
@ -186,32 +168,22 @@ extension TunnelDetailTableViewController {
}
}
return cell
} else if (section < 1 + numberOfInterfaceSections) {
} else if (section == 1) {
// Interface
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFieldsBySection[section - 1])[row]
if (field == .copyPublicKey) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
cell.buttonText = field.rawValue
cell.onTapped = {
UIPasteboard.general.string = interfaceData[.publicKey]
}
return cell
} else {
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
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[row]
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
}
} else if ((numberOfPeerSections > 0) && (section < (1 + numberOfInterfaceSections + numberOfPeerSections))) {
return cell
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
let peerIndex = (section - numberOfInterfaceSections - 1)
let peerData = tunnelViewModel.peersData[peerIndex]
let peerData = tunnelViewModel.peersData[section - 2]
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row]
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
@ -226,7 +198,7 @@ extension TunnelDetailTableViewController {
return cell
} else {
assert(section == (1 + numberOfInterfaceSections + numberOfPeerSections))
assert(section == (2 + numberOfPeerSections))
// Delete configuration
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
cell.buttonText = "Delete tunnel"
@ -328,7 +300,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
}
}
class TunnelDetailTableViewKeyValueCell: UITableViewCell {
class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelDetailTableViewKeyValueCell"
var key: String {
get { return textLabel?.text ?? "" }
@ -355,7 +327,7 @@ class TunnelDetailTableViewKeyValueCell: UITableViewCell {
}
class TunnelDetailTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelsEditTableViewButtonCell"
static let id: String = "TunnelDetailTableViewButtonCell"
var buttonText: String {
get { return button.title(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"
var key: String {
get { return keyLabel.text ?? "" }
@ -378,6 +378,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
var isValueEditable: Bool {
get { return valueTextField.isEnabled }
set(value) {
super.copyableGesture = !value
valueTextField.isEnabled = value
keyLabel.textColor = value ? UIColor.black : UIColor.gray
valueTextField.textColor = value ? UIColor.black : UIColor.gray
@ -409,6 +410,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
keyLabel = UILabel()
valueTextField = UITextField()
super.init(style: style, reuseIdentifier: reuseIdentifier)
isValueEditable = true
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right