创建文本视图时:

文本("你好世界")

我不能允许用户在长按时 Select 文本.

我已经考虑过使用文本字段,但这似乎不允许关闭文本编辑.

我只希望能够显示文本主体,并允许用户使用系统文本 Select 器突出显示所选内容.

谢谢

推荐答案

iOS 15.0+, macOS 12.0+, Mac Catalyst 15.0+

Xcode 13.0 beta 2开始,你可以使用

Text("Selectable text")
    .textSelection(.enabled)
Text("Non selectable text")
    .textSelection(.disabled)

// applying `textSelection` to a container
// enables text selection for all `Text` views inside it
VStack {
    Text("Selectable text1")
    Text("Selectable text2")
    // disable selection only for this `Text` view
    Text("Non selectable text")
        .textSelection(.disabled)
}.textSelection(.enabled)

另见textSelection Documentation.

iOS 14 and lower

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

  • 小调: Select 时光标显示
  • 市长:当用户 Select 一些文本时,他可以在上下文菜单cutpaste和其他项目中点击can change the text,而不管使用.constant(...)

我解决这个问题的方法是将UITextField子类化,并使用UIViewRepresentableUIKitSwiftUI之间架起桥梁.

At the end I provide the full code to copy and paste into a playground in Xcode 11.3 on macOS 10.14

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

The full code

在下面的完整代码中,我还实现了一个CustomTextField,其中编辑可以关闭,但仍然可以 Select .

Playground view

Selection of text

Selection of text with context menu

Code

import PlaygroundSupport
import SwiftUI


/// This subclass is needed since we want to customize the cursor and the context menu
class CustomUITextField: UITextField, UITextFieldDelegate {
    
    /// 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
    }
    
}

struct CustomTextField: UIViewRepresentable {
    
    @Binding private var text: String
    private var isEditable: Bool
    
    init(text: Binding<String>, isEditable: Bool = true) {
        self._text = text
        self.isEditable = isEditable
    }
    
    func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> CustomUITextField {
        let textField = CustomUITextField(frame: .zero)
        textField.delegate = textField
        textField.text = self.text
        textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
        return textField
    }
    
    func updateUIView(_ uiView: CustomUITextField, context: UIViewRepresentableContext<CustomTextField>) {
        uiView.text = self.text
        uiView._textBinding = self.$text
        uiView._isEditable = self.isEditable
    }
    
    func isEditable(editable: Bool) -> CustomTextField {
        return CustomTextField(text: self.$text, isEditable: editable)
    }
}

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


struct TextTestView: View {
    
    @State private var selectableText = true
    
    var body: some View {
        VStack {
            
            // Even though the text should be constant, it is not because the user can select and e.g. 'cut' the text
            TextField("", text: .constant("Test SwiftUI TextField"))
                .background(Color(red: 0.5, green: 0.5, blue: 1))
            
            // This view behaves like the `SelectableText` however the layout behaves like a `TextField`
            CustomTextField(text: .constant("Test `CustomTextField`"))
                .isEditable(editable: false)
                .background(Color.green)
            
            // A non selectable normal `Text`
            Text("Test SwiftUI `Text`")
                .background(Color.red)
            
            // A selectable `text` where the selection ability can be changed by the button below
            SelectableText("Test `SelectableText` maybe selectable")
                .selectable(self.selectableText)
                .background(Color.orange)
            
            Button(action: {
                self.selectableText.toggle()
            }) {
                Text("`SelectableText` can be selected: \(self.selectableText.description)")
            }
            
            // A selectable `text` which cannot be changed
            SelectableText("Test `SelectableText` always selectable")
                .background(Color.yellow)
            
        }.padding()
    }
    
}

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

PlaygroundPage.current.liveView = viewController.view

Swift相关问答推荐

编写Swift字符串的属性包装时遇到问题

为什么ClosedRange<;Int&>包含的速度比预期慢340万倍?

正在接收具有不完整数据错误的URL请求

如何将新事例添加到枚举中?

如何通过 Enum 属性使用新的 #Predicate 宏获取

如何在设备(或常量)地址空间中创建缓冲区?

如何删除快速处理消息

是否可以利用 Codable 从 Dictionary 初始化符合类型

如何延迟 swift 属性 didSet 使其每秒只触发一次

SwiftUI 从任务中安全更新状态变量

使用 Async-Await 和 Vapor-Fluent 创建 CRUD 函数 - Swift 5.6

快速递归:函数与闭包

这是 Int64 Swift Decoding 多余的吗?

`let case(value)` 和 `case(let value)` 之间的区别 (Swift)

如何在 Swift 中的两个 UIView 中心之间添加线?

EXC_BREAKPOINT 崩溃的原因范围

'NSLog' 不可用:可变参数函数在 swift 中不可用

在 UItextfield 的右视图上添加一个按钮,文本不应与按钮重叠

不能从非开放类继承 swift

UIButton 标题左对齐问题 (iOS/Swift)