From ccd8cfe4788c102165a4671ef57339dfbd68d0a7 Mon Sep 17 00:00:00 2001 From: Eric Kuck Date: Fri, 14 Dec 2018 20:02:37 -0600 Subject: [PATCH] KeyValueCells now share code Signed-off-by: Eric Kuck --- WireGuard/WireGuard.xcodeproj/project.pbxproj | 20 +- .../UI/iOS/View/BorderedTextButton.swift | 7 +- .../iOS/View/CopyableLabelTableViewCell.swift | 55 ------ .../UI/iOS/View/EditableKeyValueCell.swift | 158 ---------------- .../WireGuard/UI/iOS/View/KeyValueCell.swift | 172 ++++++++++++++---- .../UI/iOS/View/ScrollableLabel.swift | 48 ----- .../UI/iOS/View/TunnelEditKeyValueCell.swift | 42 +++++ .../View/TunnelEditReadOnlyKeyValueCell.swift | 74 -------- .../TunnelEditTableViewController.swift | 10 +- 9 files changed, 196 insertions(+), 390 deletions(-) delete mode 100644 WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift create mode 100644 WireGuard/WireGuard/UI/iOS/View/TunnelEditKeyValueCell.swift delete mode 100644 WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index dec7701..8120b38 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -8,8 +8,7 @@ /* Begin PBXBuildFile section */ 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */; }; - 5F45418A21C2D45B00994C13 /* EditableKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418921C2D45B00994C13 /* EditableKeyValueCell.swift */; }; - 5F45418C21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418B21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift */; }; + 5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */; }; 5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418F21C2D53800994C13 /* SwitchCell.swift */; }; 5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419121C2D55800994C13 /* CheckmarkCell.swift */; }; 5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419721C2D60500994C13 /* KeyValueCell.swift */; }; @@ -17,8 +16,6 @@ 5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */; }; 5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A521C4449E00994C13 /* ButtonCell.swift */; }; 5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; }; - 6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; }; - 6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; }; 6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; }; 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; }; 6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; }; @@ -105,8 +102,7 @@ /* Begin PBXFileReference section */ 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Reuse.swift"; sourceTree = ""; }; - 5F45418921C2D45B00994C13 /* EditableKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableKeyValueCell.swift; sourceTree = ""; }; - 5F45418B21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditReadOnlyKeyValueCell.swift; sourceTree = ""; }; + 5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditKeyValueCell.swift; sourceTree = ""; }; 5F45418F21C2D53800994C13 /* SwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = ""; }; 5F45419121C2D55800994C13 /* CheckmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkCell.swift; sourceTree = ""; }; 5F45419721C2D60500994C13 /* KeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueCell.swift; sourceTree = ""; }; @@ -114,8 +110,6 @@ 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedTextButton.swift; sourceTree = ""; }; 5F4541A521C4449E00994C13 /* ButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCell.swift; sourceTree = ""; }; 5F4541A821C451D100994C13 /* TunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatus.swift; sourceTree = ""; }; - 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = ""; }; - 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = ""; }; 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = ""; }; 6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = ""; }; 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -198,14 +192,11 @@ isa = PBXGroup; children = ( 5F45419F21C2D6B700994C13 /* TunnelListCell.swift */, - 5F45418B21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift */, - 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */, - 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */, + 5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */, 5F45418F21C2D53800994C13 /* SwitchCell.swift */, 5F45419721C2D60500994C13 /* KeyValueCell.swift */, 5F4541A521C4449E00994C13 /* ButtonCell.swift */, 5F45419121C2D55800994C13 /* CheckmarkCell.swift */, - 5F45418921C2D45B00994C13 /* EditableKeyValueCell.swift */, 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */, ); path = View; @@ -662,17 +653,14 @@ 6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */, 6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, - 6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */, 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */, 5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */, 6FE254FF219C60290028284D /* ZipExporter.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */, - 6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */, 6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */, - 5F45418A21C2D45B00994C13 /* EditableKeyValueCell.swift in Sources */, 6F6899A62180447E0012E523 /* x25519.c in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, 6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */, @@ -683,7 +671,7 @@ 5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */, 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */, 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */, - 5F45418C21C2D48200994C13 /* TunnelEditReadOnlyKeyValueCell.swift in Sources */, + 5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */, 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */, 6FE254FB219C10800028284D /* ZipImporter.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, diff --git a/WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift b/WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift index 94b76d6..ab6dcc5 100644 --- a/WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift +++ b/WireGuard/WireGuard/UI/iOS/View/BorderedTextButton.swift @@ -40,11 +40,12 @@ class BorderedTextButton: UIView { button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + @objc func buttonTapped() { onTapped?() } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } } diff --git a/WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift b/WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift deleted file mode 100644 index 93a9ef7..0000000 --- a/WireGuard/WireGuard/UI/iOS/View/CopyableLabelTableViewCell.swift +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2018 WireGuard LLC. All Rights Reserved. - -import UIKit - -class CopyableLabelTableViewCell: UITableViewCell { - var copyableGesture = true - - var textToCopy: String? { - fatalError("textToCopy must be implemented by subclass") - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) - addGestureRecognizer(gestureRecognizer) - isUserInteractionEnabled = true - } - - // MARK: - UIGestureRecognizer - @objc func handleTapGesture(_ recognizer: UIGestureRecognizer) { - if !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(detailTextLabel?.frame ?? recognizerView.frame, in: 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 = textToCopy - } - - override func prepareForReuse() { - super.prepareForReuse() - copyableGesture = true - } -} diff --git a/WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift deleted file mode 100644 index 48956eb..0000000 --- a/WireGuard/WireGuard/UI/iOS/View/EditableKeyValueCell.swift +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2018 WireGuard LLC. All Rights Reserved. - -import UIKit - -class EditableKeyValueCell: UITableViewCell { - 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 isValueValid = true { - didSet { - if isValueValid { - keyLabel.textColor = .black - } else { - keyLabel.textColor = .red - } - } - } - var keyboardType: UIKeyboardType { - get { return valueTextField.keyboardType } - set(value) { valueTextField.keyboardType = value } - } - - var onValueChanged: ((String) -> Void)? - var onValueBeingEdited: ((String) -> Void)? - - let keyLabel: UILabel = { - let keyLabel = UILabel() - keyLabel.font = UIFont.preferredFont(forTextStyle: .body) - keyLabel.adjustsFontForContentSizeCategory = true - return keyLabel - }() - - let valueTextField: UITextField = { - let valueTextField = UITextField() - valueTextField.font = UIFont.preferredFont(forTextStyle: .body) - valueTextField.adjustsFontForContentSizeCategory = true - valueTextField.autocapitalizationType = .none - valueTextField.autocorrectionType = .no - valueTextField.spellCheckingType = .no - return valueTextField - }() - - var isStackedHorizontally = false - var isStackedVertically = false - var contentSizeBasedConstraints = [NSLayoutConstraint]() - - private var textFieldValueOnBeginEditing: String = "" - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - 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) - // The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE, - // so set a CR priority > the 0.4-constraint's priority. - widthRatioConstraint.priority = .defaultHigh + 1 - keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal) - NSLayoutConstraint.activate([ - keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), - keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5), - widthRatioConstraint - ]) - - contentView.addSubview(valueTextField) - valueTextField.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - valueTextField.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), - contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 0.5) - ]) - valueTextField.delegate = self - - configureForContentSize() - } - - func configureForContentSize() { - var constraints = [NSLayoutConstraint]() - if traitCollection.preferredContentSizeCategory.isAccessibilityCategory { - // Stack vertically - if !isStackedVertically { - constraints = [ - valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5), - valueTextField.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), - keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor) - ] - isStackedVertically = true - isStackedHorizontally = false - } - } else { - // Stack horizontally - if !isStackedHorizontally { - constraints = [ - contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5), - valueTextField.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1), - valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5) - ] - isStackedHorizontally = true - isStackedVertically = false - } - } - if !constraints.isEmpty { - NSLayoutConstraint.deactivate(contentSizeBasedConstraints) - NSLayoutConstraint.activate(constraints) - contentSizeBasedConstraints = constraints - } - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - key = "" - value = "" - placeholderText = "" - isValueValid = true - keyboardType = .default - onValueChanged = nil - onValueBeingEdited = nil - configureForContentSize() - } -} - -extension EditableKeyValueCell: UITextFieldDelegate { - func textFieldDidBeginEditing(_ textField: UITextField) { - textFieldValueOnBeginEditing = textField.text ?? "" - isValueValid = true - } - func textFieldDidEndEditing(_ textField: UITextField) { - let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing) - guard isModified else { return } - if let onValueChanged = onValueChanged { - onValueChanged(textField.text ?? "") - } - } - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if let onValueBeingEdited = onValueBeingEdited { - let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) - onValueBeingEdited(modifiedText) - } - return true - } -} diff --git a/WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift index 78026ea..a456de1 100644 --- a/WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift +++ b/WireGuard/WireGuard/UI/iOS/View/KeyValueCell.swift @@ -3,73 +3,132 @@ import UIKit -class KeyValueCell: CopyableLabelTableViewCell { - var key: String { - get { return keyLabel.text ?? "" } - set(value) { keyLabel.text = value } - } - var value: String { - get { return valueLabel.text } - set(value) { valueLabel.text = value } - } - - override var textToCopy: String? { - return valueLabel.text - } +class KeyValueCell: UITableViewCell { let keyLabel: UILabel = { let keyLabel = UILabel() keyLabel.font = UIFont.preferredFont(forTextStyle: .body) keyLabel.adjustsFontForContentSizeCategory = true keyLabel.textColor = .black + keyLabel.textAlignment = .left return keyLabel }() - let valueLabel: ScrollableLabel = { - let valueLabel = ScrollableLabel() - valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body) - valueLabel.label.adjustsFontForContentSizeCategory = true - valueLabel.textColor = .gray - return valueLabel + let valueLabelScrollView: UIScrollView = { + let scrollView = UIScrollView(frame: .zero) + scrollView.isDirectionalLockEnabled = true + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + return scrollView }() + let valueTextField: UITextField = { + let valueTextField = UITextField() + valueTextField.textAlignment = .right + valueTextField.isEnabled = false + valueTextField.font = UIFont.preferredFont(forTextStyle: .body) + valueTextField.adjustsFontForContentSizeCategory = true + valueTextField.autocapitalizationType = .none + valueTextField.autocorrectionType = .no + valueTextField.spellCheckingType = .no + valueTextField.textColor = .gray + return valueTextField + }() + + var copyableGesture = true + + 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 keyboardType: UIKeyboardType { + get { return valueTextField.keyboardType } + set(value) { valueTextField.keyboardType = value } + } + + var isValueValid = true { + didSet { + if isValueValid { + keyLabel.textColor = .black + } else { + keyLabel.textColor = .red + } + } + } + var isStackedHorizontally = false var isStackedVertically = false var contentSizeBasedConstraints = [NSLayoutConstraint]() + var onValueChanged: ((String) -> Void)? + var onValueBeingEdited: ((String) -> Void)? + + private var textFieldValueOnBeginEditing: String = "" + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(keyLabel) keyLabel.translatesAutoresizingMaskIntoConstraints = false - keyLabel.textAlignment = .left NSLayoutConstraint.activate([ keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5) ]) - contentView.addSubview(valueLabel) - valueLabel.translatesAutoresizingMaskIntoConstraints = false + valueTextField.delegate = self + valueLabelScrollView.addSubview(valueTextField) + valueTextField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), - contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabel.bottomAnchor, multiplier: 0.5) + valueTextField.leftAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leftAnchor), + valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor), + valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor), + valueTextField.rightAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.rightAnchor), + valueTextField.heightAnchor.constraint(equalTo: valueLabelScrollView.heightAnchor) + ]) + let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueTextField, attribute: .width, relatedBy: .equal, toItem: valueLabelScrollView, attribute: .width, multiplier: 1, constant: 0) + expandToFitValueLabelConstraint.priority = .defaultLow + 1 + expandToFitValueLabelConstraint.isActive = true + + contentView.addSubview(valueLabelScrollView) + + contentView.addSubview(valueLabelScrollView) + valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + valueLabelScrollView.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), + contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabelScrollView.bottomAnchor, multiplier: 0.5) ]) keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal) keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) - valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + valueLabelScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal) + + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) + addGestureRecognizer(gestureRecognizer) + isUserInteractionEnabled = true configureForContentSize() } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + func configureForContentSize() { var constraints = [NSLayoutConstraint]() if traitCollection.preferredContentSizeCategory.isAccessibilityCategory { // Stack vertically if !isStackedVertically { constraints = [ - valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5), - valueLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), + valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5), + valueLabelScrollView.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor) ] isStackedVertically = true @@ -80,8 +139,8 @@ class KeyValueCell: CopyableLabelTableViewCell { if !isStackedHorizontally { constraints = [ contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5), - valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1), - valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5) + valueLabelScrollView.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1), + valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5) ] isStackedHorizontally = true isStackedVertically = false @@ -94,14 +153,65 @@ class KeyValueCell: CopyableLabelTableViewCell { } } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + @objc func handleTapGesture(_ recognizer: UIGestureRecognizer) { + if !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(detailTextLabel?.frame ?? recognizerView.frame, in: 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 = valueTextField.text } override func prepareForReuse() { super.prepareForReuse() + copyableGesture = true + placeholderText = "" + isValueValid = true + keyboardType = .default + onValueChanged = nil + onValueBeingEdited = nil key = "" value = "" configureForContentSize() } } + +extension KeyValueCell: UITextFieldDelegate { + + func textFieldDidBeginEditing(_ textField: UITextField) { + textFieldValueOnBeginEditing = textField.text ?? "" + isValueValid = true + } + + func textFieldDidEndEditing(_ textField: UITextField) { + let isModified = textField.text ?? "" != textFieldValueOnBeginEditing + guard isModified else { return } + onValueChanged?(textField.text ?? "") + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let onValueBeingEdited = onValueBeingEdited { + let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + onValueBeingEdited(modifiedText) + } + return true + } + +} diff --git a/WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift b/WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift deleted file mode 100644 index bd6f547..0000000 --- a/WireGuard/WireGuard/UI/iOS/View/ScrollableLabel.swift +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2018 WireGuard LLC. All Rights Reserved. - -import UIKit - -class ScrollableLabel: UIScrollView { - var text: String { - get { return label.text ?? "" } - set(value) { label.text = value } - } - var textColor: UIColor { - get { return label.textColor } - set(value) { label.textColor = value } - } - - let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textAlignment = .right - return label - }() - - init() { - super.init(frame: CGRect.zero) - - isDirectionalLockEnabled = true - showsHorizontalScrollIndicator = false - showsVerticalScrollIndicator = false - - addSubview(label) - label.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - label.leftAnchor.constraint(equalTo: contentLayoutGuide.leftAnchor), - label.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor), - label.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor), - label.rightAnchor.constraint(equalTo: contentLayoutGuide.rightAnchor), - label.heightAnchor.constraint(equalTo: heightAnchor) - ]) - - let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0) - expandToFitValueLabelConstraint.priority = .defaultLow + 1 - expandToFitValueLabelConstraint.isActive = true - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/WireGuard/WireGuard/UI/iOS/View/TunnelEditKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/View/TunnelEditKeyValueCell.swift new file mode 100644 index 0000000..3e18670 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/View/TunnelEditKeyValueCell.swift @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import UIKit + +class TunnelEditKeyValueCell: KeyValueCell { + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + keyLabel.textAlignment = .right + valueTextField.textAlignment = .left + + let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.4, constant: 0) + // In case the key doesn't fit into 0.4 * width, + // so set a CR priority > the 0.4-constraint's priority. + widthRatioConstraint.priority = .defaultHigh + 1 + widthRatioConstraint.isActive = true + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell { + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + valueTextField.textColor = .black + valueTextField.isEnabled = true + valueLabelScrollView.isScrollEnabled = false + valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift b/WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift deleted file mode 100644 index 15d58d6..0000000 --- a/WireGuard/WireGuard/UI/iOS/View/TunnelEditReadOnlyKeyValueCell.swift +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2018 WireGuard LLC. All Rights Reserved. - -import UIKit - -class TunnelEditReadOnlyKeyValueCell: CopyableLabelTableViewCell { - var key: String { - get { return keyLabel.text ?? "" } - set(value) { keyLabel.text = value } - } - var value: String { - get { return valueLabel.text } - set(value) { valueLabel.text = value } - } - - override var textToCopy: String? { - return valueLabel.text - } - - let keyLabel: UILabel = { - let keyLabel = UILabel() - keyLabel.font = UIFont.preferredFont(forTextStyle: .body) - keyLabel.adjustsFontForContentSizeCategory = true - keyLabel.textColor = .gray - return keyLabel - }() - - let valueLabel: ScrollableLabel = { - let valueLabel = ScrollableLabel() - valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body) - valueLabel.label.adjustsFontForContentSizeCategory = true - valueLabel.textColor = .gray - return valueLabel - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - 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) - // In case the key doesn't fit into 0.4 * width, - // so set a CR priority > the 0.4-constraint's priority. - widthRatioConstraint.priority = .defaultHigh + 1 - keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal) - NSLayoutConstraint.activate([ - keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), - widthRatioConstraint - ]) - - contentView.addSubview(valueLabel) - valueLabel.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1), - valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor) - ]) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - key = "" - value = "" - } -} diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift index 393294e..a9e2139 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift @@ -79,8 +79,8 @@ class TunnelEditTableViewController: UITableViewController { tableView.estimatedRowHeight = 44 tableView.rowHeight = UITableView.automaticDimension - tableView.register(EditableKeyValueCell.self) - tableView.register(TunnelEditReadOnlyKeyValueCell.self) + tableView.register(TunnelEditKeyValueCell.self) + tableView.register(TunnelEditEditableKeyValueCell.self) tableView.register(ButtonCell.self) tableView.register(SwitchCell.self) tableView.register(CheckmarkCell.self) @@ -218,14 +218,14 @@ extension TunnelEditTableViewController { } private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { - let cell: TunnelEditReadOnlyKeyValueCell = tableView.dequeueReusableCell(for: indexPath) + let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.rawValue cell.value = tunnelViewModel.interfaceData[field] return cell } private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { - let cell: EditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) + let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.rawValue switch field { @@ -328,7 +328,7 @@ extension TunnelEditTableViewController { } private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { - let cell: EditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) + let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.rawValue switch field {