As of Xcode 13.0 beta 2
you need to use
Textual content("Selectable textual content")
.textSelection(.enabled)
Textual content("Non selectable textual content")
.textSelection(.disabled)
// making use of `textSelection` to a container
// permits textual content choice for all `Textual content` views inside it
VStack {
Textual content("Selectable text1")
Textual content("Selectable text2")
// disable choice just for this `Textual content` view
Textual content("Non selectable textual content")
.textSelection(.disabled)
}.textSelection(.enabled)
See additionally the textSelection Documentation.
Utilizing TextField("", textual content: .fixed("Some textual content"))
has two issues:
- Minor: The cursor reveals up when choosing
- Mayor: When a person selects some textual content he can faucet within the context menu
reduce
,paste
and different gadgets which can change the textual content no matter utilizing.fixed(...)
My resolution to this downside entails subclassing UITextField
and utilizing UIViewRepresentable
to bridge between UIKit
and SwiftUI
.
On the finish I present the complete code to repeat and paste right into a playground in Xcode 11.3 on macOS 10.14
Subclassing the UITextField
:
/// This subclass is required since we need to customise the cursor and the context menu
class CustomUITextField: UITextField, UITextFieldDelegate {
/// (Not used for this workaround, see beneath for the complete code) Binding from the `CustomTextField` so modifications of the textual content may be noticed by `SwiftUI`
fileprivate var _textBinding: Binding<String>!
/// Whether it is `true` the textual content subject behaves usually.
/// Whether it is `false` the textual content can't be modified solely chosen, copied and so forth.
fileprivate var _isEditable = true {
didSet {
// set the enter view so the keyboard doesn't present up whether it is edited
self.inputView = self._isEditable ? nil : UIView()
// don't present autocorrection if it's not editable
self.autocorrectionType = self._isEditable ? .default : .no
}
}
// change the cursor to have zero dimension
override func caretRect(for place: UITextPosition) -> CGRect {
return self._isEditable ? tremendous.caretRect(for: place) : .zero
}
// override this methodology to customise the displayed gadgets of 'UIMenuController' (the context menu when choosing textual content)
override func canPerformAction(_ motion: Selector, withSender sender: Any?) -> Bool {
// disable 'reduce', 'delete', 'paste','_promptForReplace:'
// if it's not editable
if (!_isEditable) {
change motion {
case #selector(reduce(_:)),
#selector(delete(_:)),
#selector(paste(_:)):
return false
default:
// don't present 'Substitute...' which may additionally exchange textual content
// Word: This selector is personal and should change
if (motion == Selector("_promptForReplace:")) {
return false
}
}
}
return tremendous.canPerformAction(motion, withSender: sender)
}
// === UITextFieldDelegate strategies
func textFieldDidChangeSelection(_ textField: UITextField) {
// replace the textual content of the binding
self._textBinding.wrappedValue = textField.textual content ?? ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn vary: NSRange, replacementString string: String) -> Bool {
// Enable altering the textual content relying on `self._isEditable`
return self._isEditable
}
}
Utilizing UIViewRepresentable
to implement SelectableText
struct SelectableText: UIViewRepresentable {
personal var textual content: String
personal var selectable: Bool
init(_ textual content: String, selectable: Bool = true) {
self.textual content = textual content
self.selectable = selectable
}
func makeUIView(context: Context) -> CustomUITextField {
let textField = CustomUITextField(body: .zero)
textField.delegate = textField
textField.textual content = self.textual content
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: Context) {
uiView.textual content = self.textual content
uiView._textBinding = .fixed(self.textual content)
uiView._isEditable = false
uiView.isEnabled = self.selectable
}
func selectable(_ selectable: Bool) -> SelectableText {
return SelectableText(self.textual content, selectable: selectable)
}
}
Within the full code beneath I additionally applied a CustomTextField
the place enhancing may be turned off however nonetheless be selectable.
Playground view
Code
import PlaygroundSupport
import SwiftUI
/// This subclass is required since we need to customise the cursor and the context menu
class CustomUITextField: UITextField, UITextFieldDelegate {
/// Binding from the `CustomTextField` so modifications of the textual content may be noticed by `SwiftUI`
fileprivate var _textBinding: Binding<String>!
/// Whether it is `true` the textual content subject behaves usually.
/// Whether it is `false` the textual content can't be modified solely chosen, copied and so forth.
fileprivate var _isEditable = true {
didSet {
// set the enter view so the keyboard doesn't present up whether it is edited
self.inputView = self._isEditable ? nil : UIView()
// don't present autocorrection if it's not editable
self.autocorrectionType = self._isEditable ? .default : .no
}
}
// change the cursor to have zero dimension
override func caretRect(for place: UITextPosition) -> CGRect {
return self._isEditable ? tremendous.caretRect(for: place) : .zero
}
// override this methodology to customise the displayed gadgets of 'UIMenuController' (the context menu when choosing textual content)
override func canPerformAction(_ motion: Selector, withSender sender: Any?) -> Bool {
// disable 'reduce', 'delete', 'paste','_promptForReplace:'
// if it's not editable
if (!_isEditable) {
change motion {
case #selector(reduce(_:)),
#selector(delete(_:)),
#selector(paste(_:)):
return false
default:
// don't present 'Substitute...' which may additionally exchange textual content
// Word: This selector is personal and should change
if (motion == Selector("_promptForReplace:")) {
return false
}
}
}
return tremendous.canPerformAction(motion, withSender: sender)
}
// === UITextFieldDelegate strategies
func textFieldDidChangeSelection(_ textField: UITextField) {
// replace the textual content of the binding
self._textBinding.wrappedValue = textField.textual content ?? ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn vary: NSRange, replacementString string: String) -> Bool {
// Enable altering the textual content relying on `self._isEditable`
return self._isEditable
}
}
struct CustomTextField: UIViewRepresentable {
@Binding personal var textual content: String
personal var isEditable: Bool
init(textual content: Binding<String>, isEditable: Bool = true) {
self._text = textual content
self.isEditable = isEditable
}
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> CustomUITextField {
let textField = CustomUITextField(body: .zero)
textField.delegate = textField
textField.textual content = self.textual content
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.textual content = self.textual content
uiView._textBinding = self.$textual content
uiView._isEditable = self.isEditable
}
func isEditable(editable: Bool) -> CustomTextField {
return CustomTextField(textual content: self.$textual content, isEditable: editable)
}
}
struct SelectableText: UIViewRepresentable {
personal var textual content: String
personal var selectable: Bool
init(_ textual content: String, selectable: Bool = true) {
self.textual content = textual content
self.selectable = selectable
}
func makeUIView(context: Context) -> CustomUITextField {
let textField = CustomUITextField(body: .zero)
textField.delegate = textField
textField.textual content = self.textual content
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: Context) {
uiView.textual content = self.textual content
uiView._textBinding = .fixed(self.textual content)
uiView._isEditable = false
uiView.isEnabled = self.selectable
}
func selectable(_ selectable: Bool) -> SelectableText {
return SelectableText(self.textual content, selectable: selectable)
}
}
struct TextTestView: View {
@State personal var selectableText = true
var physique: some View {
VStack {
// Although the textual content must be fixed, it's not as a result of the person can choose and e.g. 'reduce' the textual content
TextField("", textual content: .fixed("Take a look at SwiftUI TextField"))
.background(Coloration(purple: 0.5, inexperienced: 0.5, blue: 1))
// This view behaves just like the `SelectableText` nevertheless the structure behaves like a `TextField`
CustomTextField(textual content: .fixed("Take a look at `CustomTextField`"))
.isEditable(editable: false)
.background(Coloration.inexperienced)
// A non selectable regular `Textual content`
Textual content("Take a look at SwiftUI `Textual content`")
.background(Coloration.purple)
// A selectable `textual content` the place the choice skill may be modified by the button beneath
SelectableText("Take a look at `SelectableText` possibly selectable")
.selectable(self.selectableText)
.background(Coloration.orange)
Button(motion: {
self.selectableText.toggle()
}) {
Textual content("`SelectableText` may be chosen: (self.selectableText.description)")
}
// A selectable `textual content` which can't be modified
SelectableText("Take a look at `SelectableText` at all times selectable")
.background(Coloration.yellow)
}.padding()
}
}
let viewController = UIHostingController(rootView: TextTestView())
viewController.view.body = CGRect(x: 0, y: 0, width: 400, peak: 200)
PlaygroundPage.present.liveView = viewController.view