]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
UI: More elegant copy-to-clipboard behavior
authorJason A. Donenfeld <Jason@zx2c4.com>
Wed, 31 Oct 2018 00:00:27 +0000 (01:00 +0100)
committerJason A. Donenfeld <Jason@zx2c4.com>
Wed, 31 Oct 2018 01:17:47 +0000 (02:17 +0100)
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
WireGuard/WireGuard.xcodeproj/project.pbxproj
WireGuard/WireGuard/UI/TunnelViewModel.swift
WireGuard/WireGuard/UI/iOS/CopyableLabelTableViewCell.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/iOS/TunnelDetailTableViewController.swift
WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift

index 27d11370acee3a2f6e8f4cd4c9f9699ab09e594d..efdf1dd92e837ef002f60986fe2d9a7d13a9f052 100644 (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>"; };
                6F7774DE217181B1006A79B3 /* iOS */ = {
                        isa = PBXGroup;
                        children = (
+                               6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
                                6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
                                6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
                                6F7774DF217181B1006A79B3 /* MainViewController.swift */,
                                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 */,
index fe50cbc3164c46e117848a0511dd1c1b97098619..56fa372076377b0e4d2610cade49de9097169e51 100644 (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 {
diff --git a/WireGuard/WireGuard/UI/iOS/CopyableLabelTableViewCell.swift b/WireGuard/WireGuard/UI/iOS/CopyableLabelTableViewCell.swift
new file mode 100644 (file)
index 0000000..779fe8f
--- /dev/null
@@ -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
+    }
+}
index 0cc78064c75b6647f0437b22e12145001db267cc..fe413c95f413bc165be39019e4e67bdeac690869 100644 (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) }
index 71187f868918d5c269ecacc8f0b051f5c85401ec..e0b11e441d55c21568682cec3cee237c6c8d82bc 100644 (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