带复制功能的 Label

产品需求需要对 UILabel 展示的文本进行复制操作,针对这一需求想出了两种实现方式: 1.自定义控件,添加复制功能. 2.使用 UItextView 实现.

通过自定义 UILabel 的子类实现

  • 创建 YTTCopyLabel, 使其继承于 UILabel.

    1
    2
    class YTTCopyLabel: UILabel {
    }
  • 使 YTTCopyLabel 可以进行交互, 添加长按手势使其触发复制事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    override var canBecomeFirstResponder: Bool { return true }

    override func awakeFromNib() {
    super.awakeFromNib()
    setup()
    }

    override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
    }

    private func setup() {
    self.isUserInteractionEnabled = true
    let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:)))
    self.addGestureRecognizer(longPressGestureRecognizer)
    }
  • 在长按事件中实现复制功能
    使用 UIMenuController 弹出复制菜单.使用简介

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @objc private func longPressAction(_ sender: UIGestureRecognizer) {

    guard sender.state == .began else {
    return
    }

    // 变为第一响应者
    self.becomeFirstResponder()

    // 菜单控制器
    let menuController = UIMenuController.shared
    // 复制 item
    let copyItem = UIMenuItem(title: "复制", action: #selector(copyText))
    // 添加 item 到 menu 控制器
    menuController.menuItems = [copyItem]
    // 设置菜单控制器点击区域为当前控件 bounds
    menuController.setTargetRect(self.bounds, in: self)
    // 菜单显示器可见
    menuController.setMenuVisible(true, animated: true)

    }
  • 确认 label 具有的操作能力

    1
    2
    3
    4
    5
    6
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    if action == #selector(copyText) {
    return true
    }
    return false
    }
  • 实现将 label 内容放到剪切板

    1
    2
    3
    @objc private func copyText() {
    UIPasteboard.general.string = self.text
    }
  • 在需要的地方使用

    1
    let label = YTTCopyLabel(frame: CGRect(x: 20, y: 100, width: self.view.frame.width - 40, height: 30))

运行效果如下:
copyLabel
YTTCopyLabel.swift

通过设置 UITextView 的属性实现

UITextView 本身就有复制的功能,他有两个属性,一个可控制其是否编辑,一个是可控制其是否可选,只需将其可编辑设为 false

1
isEditable = false

  • 创建 YTTCustomTextView,使其继承与 UITextView

    1
    2
    class YTTCustomTextView: UITextView {
    }
  • 重写父类方法,使其使其编辑功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @IBInspectable
    var isAutoLayout: Bool = true // 是否是自适应布局

    override func awakeFromNib() {
    super.awakeFromNib()
    setup()
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
    super.init(frame: frame, textContainer: textContainer)
    setup()
    }

    private func setup() {
    self.isEditable = false
    self.isScrollEnabled = false // 设为 false 可自适应
    }
  • 重写属性 text 与 attributedText, 进行自适应布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    override var text: String! {
    get {
    return super.text
    }
    set {
    super.text = newValue
    if !isAutoLayout {
    let width = self.bounds.width - (self.contentInset.left + self.contentInset.right + self.textContainerInset.left + self.textContainerInset.right + self.textContainer.lineFragmentPadding * 2)
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.lineBreakMode = self.textContainer.lineBreakMode
    let wordHeight = (self.text as NSString).boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font : self.font!, .paragraphStyle: paragraphStyle ], context: nil).height + self.textContainerInset.top + self.textContainerInset.bottom + self.textContainer.lineFragmentPadding * 2 + self.contentInset.top + self.contentInset.bottom
    let rect = CGRect(x: self.frame.minX, y: self.frame.minY, width: width, height: wordHeight)
    self.frame = rect
    }
    }
    }

    override var attributedText: NSAttributedString! {
    get {
    return super.attributedText
    }
    set {
    super.attributedText = newValue
    if !isAutoLayout {
    let width = self.bounds.width - (self.contentInset.left + self.contentInset.right + self.textContainerInset.left + self.textContainerInset.right + self.textContainer.lineFragmentPadding * 2)
    let wordHeight = newValue.boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).height + self.textContainerInset.top + self.textContainerInset.bottom + self.textContainer.lineFragmentPadding * 2 + self.contentInset.top + self.contentInset.bottom
    let rect = CGRect(x: self.frame.minX, y: self.frame.minY, width: width, height: wordHeight)
    self.frame = rect
    }
    }
    }
  • 判断屏幕点击事件是否在本视图,不是取消选中状态

    1
    2
    3
    4
    5
    6
    7
    8
    // 设置点击空白取消选中效果
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // 判断点击点是否在本视图
    if !self.point(inside: point, with: event) {
    self.selectedRange = NSMakeRange(0, 0) // 设置为 NSMakeRange(0, 0) 取消选中效果
    }
    return super.hitTest(point, with: event)
    }

YTTCustomTextView