How to get user selected text in SwiftUI TextEditor - swift

I have this code and I need to get user-selected text from TextEditor. How do I do such a thing in SwiftUI?
struct ContentView: View {
#Binding var document: AppDocument
var body: some View {
TextEditor(text: $document.text)
.disableAutocorrection(true)
}
}

While added in iOS 15 .textSelection modifier enables the end-user of an app to select and copy text, it doesn't help the developer obtain user-selected text or range of selection. I don't think that, as of early 2022, there is a way to do it natively in SwiftUI.
However, UIKit's UITextView has selectedRange property, and UITextViewDelegate has textViewDidChangeSelection(_:) method that fires off every time the user changes the selection. To use that in SwiftUI, we need to build a bridge using UIViewRepresentable protocol like so:
struct ContentView: View {
#State private var text = ""
var body: some View {
UITextViewRepresentable(text: $text)
}
}
struct UITextViewRepresentable: UIViewRepresentable {
let textView = UITextView()
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// SwiftUI -> UIKit
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
// UIKit -> SwiftUI
_text.wrappedValue = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
// Fires off every time the user changes the selection.
print(textView.selectedRange)
}
}
}

Related

How to add line numbers to UITextView in Swift (structure of app built using SwiftUI)

I am making a code editor for iPad (as a fun little project). I am trying to add line numbers to a UITextView that is in the SwiftUI code via a UIViewRepresentable. Any ideas on how to go about this? I should mention, I use an NSAttributedString for syntax hilighting. Here is a paired down version of my code (to fit better).
The view that references the UITextView
struct TestEditorView: View {
#State var text = NSAttributedString(string: "")
#State var selection: String?
var body: some View {
NavigationView {
VStack {
if let _ = selection {
ScrollView {
HStack {
TextView(text: $text)
.padding(.horizontal, 5)
}
}
}
}
}
}
}
The UITextView
struct TextView: UIViewRepresentable {
#Binding var text: NSAttributedString
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.attributedText = TextFormatter.format(text.string)
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = TextFormatter.format(text.string)
}
func makeCoordinator() -> TextViewCoordinator {
TextViewCoordinator(text: $text)
}
}
class TextViewCoordinator: NSObject, UITextViewDelegate {
#Binding private var text: NSAttributedString
init(text: Binding<NSAttributedString>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
text = TextFormatter.format(textView.attributedText.string)
}
}
P.S. I do not believe this applies because it is in objective-c and does not interface with SwiftUI.

SwiftUI: UITextView text does not wrap when inside a ForEach/LazyVGrid (Self-sizing/Dynamic height UITextView)

The issue
What I'm creating is a LazyVGrid with many UITextView as "cells" inside, but the text does not wrap when it exceeds the parent width.
When the text is a single line, all is fine:
But when the text reaches the limit, something weird happens:
What I want to achieve is simply that the text wraps, can anyone help me?
The code
To have more flexibility, I chose to use a UIKit UITextView created using the UIViewRepresentable protocol, rather than using the new TextEditor provided by SwiftUI:
struct TextView: UIViewRepresentable {
#Binding private var text: String
init(text: Binding<String>) {
self._text = text
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.isScrollEnabled = false
textView.backgroundColor = .systemIndigo // debug
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
context.coordinator.parent = self
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// MARK: - Coordinator
class Coordinator: NSObject, UITextViewDelegate {
var parent: TextView
init(_ parent: TextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
This is my SwiftUI View:
struct ContentView: View {
private let columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 0.0), count: 1)
#ObservedObject private var viewModel: ContentViewModel
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach($viewModel.blocks) { $block in
TextView(text: $block.content)
.frame(minHeight: 24.0)
}
}
.padding(.horizontal, 24.0)
}
}
}
I'll skip the ViewModel part because it's not relevant.
I've already seen and tried this Dynamic row hight containing TextEditor inside a List in SwiftUI, without success
Thanks everyone in advance ;)

Why does a binding in UIViewRepresentables Coordinator have a constant read value

I have been writing a UIViewRepresentable and noticing some curios effects in regards to a binding I'm passing into the view.
When I read the bindings value in the coordinator through the saved UIViewRepresentable the value is always the value that it was initialized with. Trying to update the same binding however triggers an update in the surrounding UI.
This is code produces this behavior:
struct NativeTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.borderStyle = .roundedRect
view.addTarget(
context.coordinator,
action: #selector(Coordinator.updateText(sender:)),
for: .editingChanged
)
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.updateUI(uiView)
}
func makeCoordinator() -> Coordinator {
Coordinator(_text)
}
class Coordinator: NSObject {
#Binding var text: String
init(_ text: Binding<String>){
_text = text
}
#objc func updateText(sender: UITextField){
text=sender.text!
}
func updateUI(_ uiView: UITextField) {
uiView.text = text
}
}
}
If I hover give my updateUI method a NativeTextView parameter, and use the .text field of it through the parameter, I read the correct value and the UI works correctly:
struct NativeTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.borderStyle = .roundedRect
view.addTarget(
context.coordinator,
action: #selector(Coordinator.updateText(sender:)),
for: .editingChanged
)
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.updateUI(uiView, view: self)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var myView: NativeTextView
init(_ view: NativeTextView){
self.myView=view
}
#objc func updateText(sender: UITextField){
myView.text=sender.text!
}
func updateUI(_ uiView: UITextField, view: NativeTextView) {
uiView.text = view.text
}
}
}
It seems that the binding retains the ability to write to the outside #State variable but does not manage to access the current states value correctly. I'm guessing that this has something to do with the recreation of the NativeTextView view when SwiftUI notices an update of the #State, but I have not been able to find any documentation that would explain this behavior.
Does anyone know why this happens?
PS: for completeness this is my ContentViews body:
ZStack {
Color.red
VStack {
Text(test)
.padding()
.onTapGesture() {
test = "Bla"
}
NativeTextView(text: $test)
}
}

SwiftUI: Hide keyboard but show cursor

I want to use custom buttons to input text into a TextField, but still show and move the cursor. Is there a way to hide the default keyboard while still showing the cursor?
I was hoping for something like this:
TextField("", text: $text)
.keyboardType(.none)
Here is what it currently looks like.
You can use UIViewRepresentable class and pass the input view as an empty view.
struct HideKeyboardTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<HideKeyboardTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.placeholder = placeholder
textField.inputView = UIView()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<HideKeyboardTextField>) {
uiView.text = text
}
func makeCoordinator() -> HideKeyboardTextField.Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: HideKeyboardTextField
init(parent: HideKeyboardTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
parent.text = textField.text ?? ""
}
}
}
}
Usage:
struct ContentView: View {
#State var text: String = ""
var body: some View {
HideKeyboardTextField(placeholder: "Input", text: $text)
}
}

How to create a multiline TextField in SwiftUi ? like the notes app?

I want to create a multiline textfield that automatically ads line breaks and with scroll view, just like the notes app, is there any direct way we can do this in SwiftUI ?
This works in Xcode Version 11.0 beta 6:
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Text("text is: \(text)")
TextView(
text: $text
)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
}
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.font = UIFont(name: "HelveticaNeue", size: 15)
myTextView.isScrollEnabled = true
myTextView.isEditable = true
myTextView.isUserInteractionEnabled = true
myTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
print("text now: \(String(describing: textView.text!))")
self.parent.text = textView.text
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use
TextEditor(text: $text)
What you're looking for is essentially a UITextView. There is no current equivalent in SwiftUI, so you'll have to use a UIViewRepresentable struct to achieve this. See Apple's SwiftUI + UIKit tutorial for how to wrap UIKit views and view controllers for use in SwiftUI and see this question for a possible implementation (and solution to an easy to overlook detail.)