UITextView make isEditable true - ios

I am trying to build a simple text editor using swiftUI and UITextView. In UITextView I am trying to use link detection which requires isEditable property to be false. But when I make isEditable false I am not able to make it true again and I am not able to type any text. No delegate method works when you make isEditable false.
Can anyone tell me how to make isEditable true again ?
UITextView, UIViewRepresentable
import SwiftUI
struct TextView: UIViewRepresentable {
#Binding var text: String
let textView = UITextView()
class Coordinator: NSObject, UITextViewDelegate {
var parent: TextView
init(_ parent: TextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
func textViewDidEndEditing(_ textView: UITextView) {
textView.isEditable = false
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
textView.delegate = context.coordinator
textView.dataDetectorTypes = .link
textView.becomeFirstResponder()
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State private var text = "www.apple.com"
var body: some View {
TextView(text: $text)
}
}
Thank You!

Related

How to get selected range from UIViewRepresentable delegate method of UITextView in SwiftUI

I have a code and I need to get the selected range of TextView from (func textViewDidChangeSelection) to the ContentView. How do I do such a thing in SwiftUI?
this is a basic idea of my code.
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)
}
}
}
Just add an extra binding for it, of type NSRange, as that is the type of textView.selectedRange.
That means adding one to the UIViewRepresentable:
struct UITextViewRepresentable: UIViewRepresentable {
let textView = UITextView()
#Binding var text: String
#Binding var range: NSRange // <---
and one to the coordinator:
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, range: $range) // <---
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var text: String
#Binding var range: NSRange // <---
init(text: Binding<String>, range: Binding<NSRange>) {
_text = text
_range = range
}
// ...
func textViewDidChangeSelection(_ textView: UITextView) {
range = textView.selectedRange
}
Now in ContentView, you can get it as a #State:
#State private var text = ""
#State private var range = NSRange()
var body: some View {
UITextViewRepresentable(text: $text, range: $range)
}

How do I make my own focusable view in SwiftUI using FocusState?

So I want to implement a custom control as a UIViewRepresentable which correctly handles focus using an #FocusState binding.
So I want to be able to manage the focus like so:
struct MyControl: UIViewRepresentable { ... }
struct Container: View {
#FocusState var hasFocus: Bool = false
var body: some View {
VStack {
MyControl()
.focused($hasFocus)
Button("Focus my control") {
hasFocus = true
}
}
}
}
What do I have to implement in MyControl to have it respond to the focus state properly? Is there a protocol or something which must be implemented?
Disclaimer: the solution is not suitable for full custom controls. For these cases, you can try to pass the FocusState as binding: let isFieldInFocus: FocusState<Int?>.Binding
In my case, I have wrapped UITextView. In order to set the focus, I only used .focused($isFieldInFocus).
Some of the information can be obtained through the property wrapper(#Environment(\.)), but this trick does not work with focus.
struct ContentView: View {
#FocusState var isFieldInFocus: Bool
#State var text = "Test message"
#State var isDisabled = false
var body: some View {
VStack {
Text("Focus for UITextView")
.font(.headline)
AppTextView(text: $text)
.focused($isFieldInFocus)
.disabled(isDisabled)
.frame(height: 200)
HStack {
Button("Focus") {
isFieldInFocus = true
}
.buttonStyle(.borderedProminent)
Button("Enable/Disable") {
isDisabled.toggle()
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AppTextView: UIViewRepresentable {
#Binding var text: String
#Environment(\.isEnabled) var isEnabled: Bool
#Environment(\.isFocused) var isFocused: Bool // doesn't work
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.preferredFont(forTextStyle: .body)
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
print("isEnabled", isEnabled, "isFocused", isFocused)
}
class Coordinator : NSObject, UITextViewDelegate {
private var parent: AppTextView
init(_ textView: AppTextView) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}

Rich TextView in SwiftUI

I'm trying to do a simple notes app in SwiftUI and I'd like to know if there's any library for having a simple rich text view that allows users to write in bold, italics, strikethrough, underline, etc.
I've tried LEOTextView, but it is very buggy, and I'd like to see if there's a 100% SwiftUI way to achieve this so it would be easier to integrate with my project. I also don't know a lot of UIKit so it would be easier to add more features if it is made in SwiftUI.
Sorry if this is a stupid question and thanks in advance.
TL;DR: I just want something like this
I guess this would be the starting point:
import SwiftUI
// first wrap a UITextView in a UIViewRepresentable
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
return textView
}
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("new text: \(String(describing: textView.text!))")
self.parent.text = textView.text
}
}
}
struct ContentView: View {
#State var text = ""
var body: some View {
TextView(
text: $text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There is a well documented library that for SwiftUI text editor. it supports iOS 13.0+ and macOS 10.15+, here is the link for it!
HighlightedTextEditor
It's also very simple to use.

Re-focusing a first responder in SwiftUI

I know how to create a UIViewRepresentable in SwiftUI in order to enable first responder functionality for a TextField:
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (!uiView.isFirstResponder) {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
My problem is attempting to "re-focus" this custom text field. So while this text field DOES get focused when my ContentView is initialized, I want to know how I can re-focus this text field programmatically, AFTER it has lost focus.
Here is my ContentView:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
// ???
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
Here is what I've tried. I thought maybe I could create a #State variable which can control the FirstResponderTextField, so I went ahead and changed my structs as follows:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
#State var isFocused: Bool = true
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1, isFocused: $isFocused)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
self.isFocused = true
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
#Binding var isFocused: Bool
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (isFocused) {
uiView.becomeFirstResponder()
isFocused = false
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
It does not appear to be working. I mean, it works when I first click the button, but stops working afterwards.
Also, I am now getting this warning:
Modifying state during view update, this will cause undefined behaviour.
Is it possible to create a UIViewRepresentable that can be re-focused whenever I want?
You should not be setting the isFocused to false in your FirstResponderTextField.
Instead, in FirstResponderTextField, observe the value changes in the isFocused binding, and set your control to being first responder or not accordingly.
I created a solution for you with an ObserableObject corresponding to the TextFieldState in this gist on github
For future reference, this is what I changed:
instead of #State var isFocused: Bool = true in ContentView, use #ObservedObject var textFieldState = TextFieldState()
have a property #ObservedObject var state: TextFieldState in FirstResponderTextField
simply do this in FirstResponderTextField.updateUIView:
if state.isFirstResponder {
uiView.becomeFirstResponder()
}

View coordinator/delegate disappear function?

Is there a function for the view coordinator/delegate for when the view disappears?
I'm trying to create a textview that "autosaves" notes people enter into core data. Basically it just waits 2 seconds since the last textViewDidChange and then saves the data.
It uses a simple Boolean variable for the "queue" to ensure that it won't try and save multiple times within those 2 seconds.
Here is the code for that:
struct BodyTextView: UIViewRepresentable {
#Environment(\.managedObjectContext) var managedObjectContext
#Binding var text: String
var note: Note
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator : NSObject, UITextViewDelegate {
var textView: BodyTextView
var saveQueued: Bool = false
init(_ textView: BodyTextView) {
self.textView = textView
}
func textViewDidChange(_ textView: UITextView) {
self.textView.text = textView.text
if (!saveQueued) {
saveQueued = true
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) {_ in
print("saving from textViewDidChange: " + textView.text)
self.textView.note.body = textView.text
saveContext(self.textView.managedObjectContext)
self.saveQueued = false
}
}
}
}
}
This works well so far.
My problem is that if the view disappears before the 2 seconds is up it doesn't save the data. I'm guessing it is because when the view is dismissed it cancels the timer within the struct.
I'd like it to also save when the BodyTextView disappears as well.
Something like this:
func textViewDidDissapear(_ textView: UITextView) {
print("saving from textViewDidDissapear: " + textView.text)
self.textView.note.body = textView.text
saveContext(self.textView.managedObjectContext)
}
How do I do this?
The life cycle methods for SwiftUI.View's are a modifier on the view itself. so If you want to know when the view disappears add an .onDisappear() modifier to it.

Resources