如何在SwiftUI中的文本视图上选择文本?

时间:2019-09-19 07:01:38

标签: ios swift text swiftui

当我创建文本视图时:

  

文本(“ Hello World”)

长按时,我不允许用户选择文本。

我看过使用TextField的情况,但这似乎不允许关闭文本编辑。

我只希望能够显示正文,并允许用户使用系统文本选择器突出显示选择内容。

谢谢!

5 个答案:

答案 0 :(得分:13)

使用TextField("", text: .constant("Some text"))有两个问题:

  • 次要:选择时会显示光标
  • 市长:当用户选择某些文本时,他可以在上下文菜单cutpaste和其他可以更改文本的项目中轻按,而无需使用{{1 }}

我对这个问题的解决方案涉及子类化.constant(...)并使用UITextFieldUIViewRepresentableUIKit之间架桥。

最后,我提供了完整的代码,可将其复制并粘贴到macOS 10.14上的Xcode 11.3中

SwiftUI子类化:

UITextField

使用/// This subclass is needed since we want to customize the cursor and the context menu class CustomUITextField: UITextField, UITextFieldDelegate { /// (Not used for this workaround, see below for the full code) Binding from the `CustomTextField` so changes of the text can be observed by `SwiftUI` fileprivate var _textBinding: Binding<String>! /// If it is `true` the text field behaves normally. /// If it is `false` the text cannot be modified only selected, copied and so on. fileprivate var _isEditable = true { didSet { // set the input view so the keyboard does not show up if it is edited self.inputView = self._isEditable ? nil : UIView() // do not show autocorrection if it is not editable self.autocorrectionType = self._isEditable ? .default : .no } } // change the cursor to have zero size override func caretRect(for position: UITextPosition) -> CGRect { return self._isEditable ? super.caretRect(for: position) : .zero } // override this method to customize the displayed items of 'UIMenuController' (the context menu when selecting text) override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { // disable 'cut', 'delete', 'paste','_promptForReplace:' // if it is not editable if (!_isEditable) { switch action { case #selector(cut(_:)), #selector(delete(_:)), #selector(paste(_:)): return false default: // do not show 'Replace...' which can also replace text // Note: This selector is private and may change if (action == Selector("_promptForReplace:")) { return false } } } return super.canPerformAction(action, withSender: sender) } // === UITextFieldDelegate methods func textFieldDidChangeSelection(_ textField: UITextField) { // update the text of the binding self._textBinding.wrappedValue = textField.text ?? "" } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { // Allow changing the text depending on `self._isEditable` return self._isEditable } } 来实现UIViewRepresentable

SelectableText

完整代码

在下面的完整代码中,我还实现了struct SelectableText: UIViewRepresentable { private var text: String private var selectable: Bool init(_ text: String, selectable: Bool = true) { self.text = text self.selectable = selectable } func makeUIView(context: Context) -> CustomUITextField { let textField = CustomUITextField(frame: .zero) textField.delegate = textField textField.text = self.text textField.setContentHuggingPriority(.defaultHigh, for: .vertical) textField.setContentHuggingPriority(.defaultHigh, for: .horizontal) return textField } func updateUIView(_ uiView: CustomUITextField, context: Context) { uiView.text = self.text uiView._textBinding = .constant(self.text) uiView._isEditable = false uiView.isEnabled = self.selectable } func selectable(_ selectable: Bool) -> SelectableText { return SelectableText(self.text, selectable: selectable) } } ,可以关闭编辑但仍可以选择编辑。

游乐场视图

Selection of text

Selection of text with context menu

代码

CustomTextField

答案 1 :(得分:5)

我发现一个简单的解决方法是只使用上下文菜单:

Text($someText)
.contextMenu(ContextMenu(menuItems: {
  Button("Copy", action: {
    UIPasteboard.general.string = someText
  })
}))

答案 2 :(得分:4)

我遇到了类似的问题,我本质上希望在不进行编辑的情况下选择文本。就我而言,我想在点按文本时显示UIMenuController,而不允许编辑文本或显示光标或键盘。基于先前的答案:

import SwiftUI
import UIKit


struct SelectableText: UIViewRepresentable {
    var text: String
    @Binding var isSelected: Bool

    func makeUIView(context: Context) -> SelectableLabel {
        let label = SelectableLabel()
        label.textColor = .white
        label.font = .systemFont(ofSize: 60, weight: .light)
        label.minimumScaleFactor = 0.6
        label.adjustsFontSizeToFitWidth = true
        label.textAlignment = .right
        label.numberOfLines = 1
        label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        label.text = text
        return label
    }

    func updateUIView(_ uiView: SelectableLabel, context: Context) {
        uiView.text = text
        if isSelected {
            uiView.showMenu()
        } else {
            let _ = uiView.resignFirstResponder()
        }
    }
}

class SelectableLabel: UILabel {
    override var canBecomeFirstResponder: Bool {
        return true
    }

    override init(frame: CGRect) {
        super.init(frame: .zero)
        highlightedTextColor = .gray
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        switch action {
        case #selector(copy(_:)), #selector(paste(_:)), #selector(delete(_:)):
            return true
        default:
            return super.canPerformAction(action, withSender: sender)
        }
    }

    override func copy(_ sender: Any?) {
        UIPasteboard.general.string = self.stringValue
    }

    override func paste(_ sender: Any?) {
        guard let string = UIPasteboard.general.string else { return }
        NotificationCenter.default.post(name: Notification.Name.Paste, object: nil, userInfo: [Keys.PastedString: string])
    }

    override func delete(_ sender: Any?) {
        NotificationCenter.default.post(name: Notification.Name.Delete, object: nil)
    }

    override func resignFirstResponder() -> Bool {
        isHighlighted = false
        return super.resignFirstResponder()
    }

    public func showMenu() {
        becomeFirstResponder()
        isHighlighted = true
        let menu = UIMenuController.shared            
        menu.showMenu(from: self, rect: bounds)
    }
}

我使用自定义粘贴和删除通知来向我的模型对象发送消息,在该对象中处理粘贴和删除操作以适当地更新显示,这对我而言是可行的。绑定也可以使用。

要使用:

SelectableText(text: text, isSelected: self.$isSelected)
    .onTapGesture {
         self.isSelected.toggle()
     }
     .onReceive(NotificationCenter.default.publisher(for: UIMenuController.willHideMenuNotification)) { _ in
         self.isSelected = false
     }

答案 3 :(得分:0)

尝试一下:

TextField("Unused Placeholder", text: .constant("Non-editable text"))

它是不可编辑的,可选的,但不幸的是它显示了一个光标。

答案 4 :(得分:0)

或者,当我们想在不允许编辑的情况下显示文本的“复制”工具提示时,我们可以使用类似的方法。

作为好处,我们将有可能使用原生视图“Text”,这让我们有机会使用原生方法“.font()”、“.foregroundColor()”等。我们也可以将它用于视图组,例如单元格。

以先前的答案为基础。

游乐场代码

import PlaygroundSupport
import SwiftUI

private class SelectableUIView: UIView {

    var text: String?

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

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setup()
    }

    func setup() {
        self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.showMenu)))
    }

    @objc func showMenu(_ recognizer: UILongPressGestureRecognizer) {
        becomeFirstResponder()

        let menu = UIMenuController.shared
        if !menu.isMenuVisible {
            menu.showMenu(from: self, rect: frame)
        }
    }

    override func copy(_ sender: Any?) {
        let board = UIPasteboard.general
        board.string = text

        UIMenuController.shared.hideMenu()
    }

    override var canBecomeFirstResponder: Bool {
        return true
    }

    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return action == #selector(UIResponderStandardEditActions.copy)
    }

}

struct SelectableView: UIViewRepresentable {

    var text: String

    func makeUIView(context: Context) -> UIView {
        let view = SelectableUIView()
        return view
    }
    func updateUIView(_ uiView: UIView, context: Context) {
        guard let view = uiView as? SelectableUIView else {
            return
        }
        view.text = text
    }
}

struct SelectableContainer<Content: View>: View {

    private let content: () -> Content
    private var text: String

    public init(text: String, @ViewBuilder content: @escaping () -> Content) {
        self.text = text
        self.content = content
    }

    public var body: some View {
        ZStack {
            content()
            SelectableView(text: text)
                .layoutPriority(-1)
        }
    }

}

struct SelectableText: View {
    private var text: String

    public init(_ text: String) {
        self.text = text
    }

    public var body: some View {
        ZStack {
            Text(text)
            SelectableView(text: text)
                .layoutPriority(-1)
        }
    }
}


struct TextTestView: View {

    @State private var text = "text"

    var body: some View {
        VStack {
            SelectableContainer(text: text) {
                VStack(alignment: .leading) {
                    Text("Header")
                        .font(.body)

                    Text(text)
                        .background(Color.orange)
                }
            }
            .background(Color.yellow)

            SelectableText(text)
                .background(Color.black)
                .foregroundColor(.white)
                .font(.largeTitle)
        }.padding()
    }

}

let viewController = UIHostingController(rootView: TextTestView())
viewController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 200)

PlaygroundPage.current.liveView = viewController.view

游乐场视图 Playground view