Show iPhone cut copy paste menu on UILabel

前端 未结 12 1222
半阙折子戏
半阙折子戏 2020-11-30 21:05
  1. Can we enable the cut copy paste menu for a UILabel as it is for a UITextField?

  2. If not, and I need to convert my UIL

相关标签:
12条回答
  • 2020-11-30 21:08

    2019 ...

    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 subtlety:

    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".)

    0 讨论(0)
  • 2020-11-30 21:11

    Swift 5.3 and SwiftUI

    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
        }
    }
      
    
    0 讨论(0)
  • 2020-11-30 21:12

    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.

    0 讨论(0)
  • 2020-11-30 21:12

    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(_:))
    }
    
    0 讨论(0)
  • 2020-11-30 21:13

    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 :)

    0 讨论(0)
  • 2020-11-30 21:15

    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;  
    }
    
    0 讨论(0)
提交回复
热议问题