Selectable UILabel contents - ios

I have a list of URL addresses in a iOS Label object. It does not appear that the user can select and copy an item from the list and paste it into their Safari browser on the iOS device. Is there a way to accomplish this?

This feature is not there in UILabel.
You need to use UITextField or UITextView . Also do not forget to change its appearance and using
[... setEditable:NO];

It is actually possible to do with an UILabel, only you'll have to do some subclassing.
End result:
when your user long presses the label, he or she will see a copy-balloon.
Here are the steps to allow for making a label copy-able (as I can recall):
subclass UILabel
set userInteractionEnabled = YES
override canBecomeFirstResponder and return true
add a UILongPressGestureRecognizer
become first responder & present UIMenuController
Swift 3:
let menu = UIMenuController.shared
if !menu.isMenuVisible {
self.becomeFirstResponder()
menu.setTargetRect(self.bounds, in: self)
menu.setMenuVisible(true, animated: true)
}
override canPerformAction to allow copy
Swift 3:
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(UIResponderStandardEditActions.copy(_:))
}
override copy method, UIPasteboard the text & hide UIMenuController
Swift 3:
let menu = UIMenuController.shared
let labelText = self.text ?? self.attributedText?.string
if let uLabelText = labelText {
let clipBoard = UIPasteboard.general
clipBoard.string = uText
}
menu.setMenuVisible(false, animated: true)

Here is swift 5 version of JoriDor's solution;
class CopyableLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
sharedInit()
}
func sharedInit() {
isUserInteractionEnabled = true
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(showMenu(sender:))))
}
#objc
func showMenu(sender: AnyObject?) {
becomeFirstResponder()
let menu = UIMenuController.shared
if !menu.isMenuVisible {
menu.setTargetRect(bounds, in: self)
menu.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(UIResponderStandardEditActions.copy(_:)) {
return true
}
return false
}
override func copy(_ sender: Any?) {
let board = UIPasteboard.general
board.string = text
let menu = UIMenuController.shared
menu.setMenuVisible(false, animated: true)
}
}

Related

UILabel tap gesture recognizer doesn't seem to work

I know this is an often asked question but I have a specific case of the issue as I abstracted the tapGestureRecognizer so it can be re-used without initialiazing it everytime.
Here is how I use UITapGestureRecognizer on UIlabels :
In UILabel extension :
func setOnClickListener(action: #escaping (_: Any?) -> Void, clickedObject: Any? = nil){
let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked), clickedObject: clickedObject, onClick: action)
self.addGestureRecognizer(tapRecogniser)
}
#objc func onViewClicked(_ sender: ClickListener) {
sender.onClick?(sender.clickedObject)
}
The ClickListener class :
class ClickListener: UITapGestureRecognizer {
var onClick : ((_: Any?) -> Void)?
var clickedObject: Any?
init(target: Any?, action: Selector?, clickedObject: Any? = nil, onClick: ((_: Any?) -> Void)? = nil) {
self.clickedObject = clickedObject
self.onClick = onClick
super.init(target: target, action: action)
}
}
And here how I call it in my view :
class VoteBoxView: UIView {
private var updateVoteButton: UILabel! = UILabel()
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func initLayout() {
self.addSubview(updateVoteButton)
self.updateVoteButton.text = "Modifier"
self.updateVoteButton.setToSecondaryText()
self.updateVoteButton.setToSecondaryTextColor()
self.updateVoteButton.underline()
self.updateVoteButton.snp.makeConstraints{ (make) -> Void in
make.top.equalTo(self).offset(15)
make.right.equalTo(self)
}
self.initActions()
}
func initActions() {
updateVoteButton.isUserInteractionEnabled = true
updateVoteButton.setOnClickListener(action: updateVote, clickedObject: nil)
}
func updateVote(clickedObject: Any?) {
self.toggleResults(active: false)
}
}
I definitely succeeded to make it work in other cases but there is no differences I can find that would cause it not to work here.
The problem was that I didn't set the correct constraints on my view so the tap was blocked by my ViewController view. I found out by adding colored background on everything.

Custom UIMenuController using button event error There can only be one UIMenuController instance

I create a button that when you hold, it will show the standard ios menu button for pasting a text, but I'm getting an error saying There can only be one UIMenuController instance. when I hold the button 2 times, how can I fix this?
Here is my code
override init(frame: CGRect) {
super.init(frame: frame)
self.configureView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.configureView()
}
private func configureView() {
guard let view = self.loadViewFromNib(nibName: "CustomView") else { return }
view.frame = self.bounds
self.addSubview(view)
button.addTarget(self, action: #selector(holdButton), for: .touchDown)
}
#objc func holdButton(_ sender: UIButton) {
let menuController = UIMenuController()
menuController.setTargetRect(sender.frame, in: charTextField)
menuController.setMenuVisible(true, animated: true)
}
Also, how can I listen to the user when he clicked the paste button?
I want it to call this function when he clicked paste.
func pasteClick() {
print("pasted", clipboardString())
}
Use the default singleton instance provided (.shared) by UIMenuController instead of creating an instance of your own.
#objc func holdButton(_ sender: UIButton) {
UIMenuController.shared.setTargetRect(sender.frame, in: charTextField)
UIMenuController.shared.setMenuVisible(true, animated: true)
}
Quoting from apple doc:
The singleton UIMenuController instance is referred to as the editing
menu.....

UIMenuController do not hide system items

I show a UIMenuController in a uiviewconroller in this way:
in my class:
override open func canBecomeFirstResponder() -> Bool {
return true
}
open override func canPerformAction(_ action: Selector, withSender sender: Any) -> Bool {
//here I check for my custom action, else return false
return false
}
then to show I use:
//Make this as first responder
self.becomeFirstResponder()
///Build menu
let menu = UIMenuController.shared
///Set item and anchor point, and showit
menu.menuItems = itemsToAdd
menu.setTargetRect(CGRect(x: 0, y: 5, width: bubbleNode.view.bounds.size.width, height: bubbleNode.view.bounds.size.height), in: bubbleImageNode.view)
menu.setMenuVisible(true, animated: true)
the problem is that in a device I show my custom items, but also: "Spell, speak, speck sentence, ecc..." how can I disable it?
override canPerformAction and handle it for each specific action. It working perfectly for me.
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
var canPerform = super.canPerformAction(action, withSender: sender)
if (action == "your action to restrict") {
canPerform = false
}
return canPerform
}
Ok, the problem is "Accessibility" option "speak selection" in my device, if I disabled it, I see only the custom items, but in other app i see only custom items with this option enabled!
class TextViewWithCopyAction: UITextView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(copy(_:)){
return true
} else {
return false
}
}
}
to use it:
let textView: TextViewWithCopyAction = {
let textView = TextViewWithCopyAction()
textView.backgroundColor = .white
return textView
}()
If you see speak option in UIMenuController that is from device go to:
Setting->Accessibility->Speech->Speech
Select and disable it. Automatically Speak option will get removed from menu.

How to set A subclass UILabel text ? THREAD ERROR

I'm trying to set a UILabel text as follows (All the following code is swift3)
// button Action which select a random text
#IBAction func getResultButton(_ sender: TransitionButton) {
if( typeTextField.text == "ABC" && pickedCompanyField.text != "XYZ"){
// query
sender.startAnimation()
// sleep(5)
sender.stopAnimation(animationStyle: .normal)
} else if( typeTextField.text == "DEF" && pickedCompanyField.text != "XYZ"){
// query
sender.startAnimation()
sender.stopAnimation(animationStyle: .shake)
// DispatchQueue.main.async {
self.resultLabel.text = "56.36"
// }
}
}
and I use the following subclass
import UIKit
class CpLabel: UILabel {
override public var canBecomeFirstResponder: Bool {
get {
return true
}
}
override init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sharedInit()
}
func sharedInit() {
isUserInteractionEnabled = true
addGestureRecognizer(UILongPressGestureRecognizer(
target: self,
action: #selector(showMenu(sender:))
))
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = text
UIMenuController.shared.setMenuVisible(false, animated: true)
}
func showMenu(sender: Any?) {
becomeFirstResponder()
let menu = UIMenuController.shared
if !menu.isMenuVisible {
menu.setTargetRect(bounds, in: self)
menu.setMenuVisible(true, animated: true)
}
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(copy(_:)))
}
}
I get this Thread Error ... I don't know what is causing it ? I have tried to use DispatchQue... function to see if the error regarding main thread stuff but it seems not ? SO Any? Help will be appreciated
Error screenshot:
thanks guys I found the issue ... It was declared as UILabel and its sub classed to CpLabel , So I changed the deceleration it to CpLabel and it worked..

Does UILabel have any value to make it selectable?

Does UILabel have any value that can be set in order to make it selectable?
I have a label that I want to be selectable, (long press and a copy btn shows up) kinda like in Safari.
Self-contained Solution (Swift 5)
You can adapt the solution from #BJHSolutions and NSHipster to make the following self-contained SelectableLabel:
import UIKit
/// Label that allows selection with long-press gesture, e.g. for copy-paste.
class SelectableLabel: UILabel {
override func awakeFromNib() {
super.awakeFromNib()
isUserInteractionEnabled = true
addGestureRecognizer(
UILongPressGestureRecognizer(
target: self,
action: #selector(handleLongPress(_:))
)
)
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(copy(_:))
}
// MARK: - UIResponderStandardEditActions
override func copy(_ sender: Any?) {
UIPasteboard.general.string = text
}
// MARK: - Long-press Handler
#objc func handleLongPress(_ recognizer: UIGestureRecognizer) {
if recognizer.state == .began,
let recognizerView = recognizer.view,
let recognizerSuperview = recognizerView.superview {
recognizerView.becomeFirstResponder()
UIMenuController.shared.setTargetRect(recognizerView.frame, in: recognizerSuperview)
UIMenuController.shared.setMenuVisible(true, animated:true)
}
}
}
Yes, you need to implement a UIMenuController from a long press gesture applied to your UILabel. There is an excellent article about this on NSHipster, but the gist of the article is the following.
Create a subclass of UILabel and implement the following methods:
override func canBecomeFirstResponder() -> Bool {
return true
}
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
return (action == "copy:")
}
// MARK: - UIResponderStandardEditActions
override func copy(sender: AnyObject?) {
UIPasteboard.generalPasteboard().string = text
}
Then in your view controller, you can add a long press gesture to your label:
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleLongPressGesture:")
label.addGestureRecognizer(gestureRecognizer)
and handle the long press with this method:
func handleLongPressGesture(recognizer: UIGestureRecognizer) {
if let recognizerView = recognizer.view,
recognizerSuperView = recognizerView.superview
{
let menuController = UIMenuController.sharedMenuController()
menuController.setTargetRect(recognizerView.frame, inView: recognizerSuperView)
menuController.setMenuVisible(true, animated:true)
recognizerView.becomeFirstResponder()
}}
NOTE: This code is taken directly from the NSHipster article, I am just including it here for SO compliance.
UILabel inherits from UIView so you can just add a long press gesture recognizer to the label. Note that you have to change isUserInteractionEnabled to true, because it defaults to false for labels.
import UIKit
class ViewController: UIViewController {
let label = UILabel()
override func viewDidLoad() {
view.addSubview(label)
label.text = "hello"
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressLabel(longPressGestureRecognizer:)))
label.addGestureRecognizer(longPressGestureRecognizer)
label.isUserInteractionEnabled = true
}
#objc private func longPressLabel (longPressGestureRecognizer: UILongPressGestureRecognizer) {
if longPressGestureRecognizer.state == .began {
print("long press began")
} else if longPressGestureRecognizer.state == .ended {
print("long press ended")
}
}
}
I've implemented a UILabel subclass that provides all of the functionality needed. Note that if you're using this with interface builder, you'll need to adjust the init methods.
/// A label that can be copied.
class CopyableLabel: UILabel
{
// MARK: - Initialisation
/// Creates a new label.
init()
{
super.init(frame: .zero)
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:)))
self.addGestureRecognizer(gestureRecognizer)
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
// MARK: - Responder chain
override var canBecomeFirstResponder: Bool
{
return true
}
// MARK: - Actions
/// Method called when a long press is triggered.
func handleLongPressGesture(_ gestuerRecognizer: UILongPressGestureRecognizer)
{
guard let superview = self.superview else { return }
let menuController = UIMenuController.shared
menuController.setTargetRect(self.frame, in: superview)
menuController.setMenuVisible(true, animated:true)
self.becomeFirstResponder()
}
override func copy(_ sender: Any?)
{
UIPasteboard.general.string = self.text
}
}

Resources