Can we enable the cut copy paste menu for a UILabel
as it is for a UITextField
?
If not, and I need to convert my UIL
Save anyone typing:
public class SomeComplexCustomView: UIView {
@IBOutlet var oneOfYourLabels: UILabel!
... your other labels, boxes, etc
public func makeThatLabelCopyable() {
oneOfYourLabels.isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(
target: self, action: #selector(self.copyMenu(sender:))))
addGestureRecognizer(UILongPressGestureRecognizer(
target: self, action: #selector(self.copyMenu(sender:))))
// or use oneOfYourLabels.addGesture... to touch just on that item
}
public override var canBecomeFirstResponder: Bool { return true }
@objc func copyMenu(sender: Any?) {
becomeFirstResponder()
UIMenuController.shared.setTargetRect(bounds, in: self)
// or any exact point you want the pointy box pointing to
UIMenuController.shared.setMenuVisible(true, animated: true)
}
override public func copy(_ sender: Any?) {
UIPasteboard.general.string = oneOfYourLabels.text
// or any exact text you wish
UIMenuController.shared.setMenuVisible(false, animated: true)
}
override public func canPerformAction(
_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(copy(_:)))
}
}
It's that easy!
One detail for better engineering:
Notice we turn on first responder:
public override var canBecomeFirstResponder: Bool { return true }
Often, on a given screen with such a label, you either will or won't have a copyable link like this.
So you'll very likely have something like:
var linkTurnedOnCurrently: Bool = false
func doShowThatLink( blah ) {
linkAvailableOnThisScreen = true
... the various code above ...
}
func doShowThatLink( blah ) {
linkAvailableOnThisScreen = false
... perhaps de-color the link, etc ...
}
Thus, in fact instead of this:
public override var canBecomeFirstResponder: Bool { return true }
be sure to do this:
public override var canBecomeFirstResponder: Bool {
if linkTurnedOnCurrently { return true }
return super.canBecomeFirstResponder
}
(Note that it is not something like "return linkTurnedOnCurrently".)
To make this work in SwiftUI we can use the method that pableiros created an combine that with a UIViewRepresentable
.
There are two updates that we need to make to the CopyableLabel
class as the following methods were deprecated in iOS 13.
.setTargetRect(_,in:)
.setMenutVisible(_,animated)
We can easily fix this by using the .showMenu(from:rect:)
method instead.
Here is the updated CopyableLabel
class.
class CopyableLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
self.sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.sharedInit()
}
func sharedInit() {
self.isUserInteractionEnabled = true
self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.showMenu)))
}
@objc func showMenu(sender: AnyObject?) {
self.becomeFirstResponder()
let menu = UIMenuController.shared
if !menu.isMenuVisible {
menu.showMenu(from: self, rect: self.bounds) // <- we update the deprecated methods here
}
}
override func copy(_ sender: Any?) {
let board = UIPasteboard.general
board.string = text
let menu = UIMenuController.shared
menu.showMenu(from: self, rect: self.bounds) // <- we update the deprecated methods here
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(UIResponderStandardEditActions.copy)
}
}
Then to get this class to work with SwiftUI all we have to do is create a simple UIViewRepresentable
.
struct CopyableLabelView: UIViewRepresentable {
let text: String
private let label = CopyableLabel(frame: .zero)
init(text: String) {
self.text = text
}
func makeUIView(context: Context) -> UILabel {
// Set the text for the label
label.text = text
// Set the content hugging priority so the UILabel's view is
// kept tight to the text.
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentHuggingPriority(.required, for: .vertical)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
// Handle when the text that is passed changes
uiView.text = text
}
}
Override the UITextField
instance's textFieldShouldBeginEditing
method, and set it to return NO
in order to disable editing.
Take a look at the UITextFieldDelegate protocol for more details.
In Swift 5.0 and Xcode 10.2
Add copy option to your UILabel directly in your ViewController.
//This is your UILabel
@IBOutlet weak var lbl: UILabel!
//In your viewDidLoad()
self.lbl.isUserInteractionEnabled = true
let longPress = UILongPressGestureRecognizer.init(target: self, action: #selector((longPressFunctin(_:))))
self.lbl.addGestureRecognizer(longPress)
//Write these all functions outside the viewDidLoad()
@objc func longPressFunctin(_ gestureRecognizer: UILongPressGestureRecognizer) {
lbl.becomeFirstResponder()
let menu = UIMenuController.shared
if !menu.isMenuVisible {
menu.setTargetRect(CGRect(x: self.lbl.center.x, y: self.lbl.center.y, width: 0.0, height: 0.0), in: view)
menu.setMenuVisible(true, animated: true)
}
}
override func copy(_ sender: Any?) {
let board = UIPasteboard.general
board.string = lbl.text
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(copy(_:))
}
If you have multiline text, you should use UITextView
Set the delegate:
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
return false
}
And it should work magically :)
The sample project on github due to @zoul's answer is the way to go. At the time of this writing, that project does not actually put anything on the clipboard (pasteboard). here is how:
Change @zoul's implementation of this method to:
- (void) copy:(id)sender {
UIPasteboard *pboard = [UIPasteboard generalPasteboard];
pboard.string = self.text;
}