Rich TextView in SwiftUI - ios

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.

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

UITextView make isEditable true

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!

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

How to hide SearchBar on scroll in SwiftUI?

I'm trying to hide Search bar in my app like Apple did in their messages app:
I've already implemented UISearchBar in SwiftUI:
struct SearchBar: UIViewRepresentable {
#Binding var text: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.searchBarStyle = .minimal
searchBar.placeholder = "Поиск по названию, дедлайну или описанию"
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context _: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
How can I implement hide and hiding animation in SwiftUI?
SwiftUI 3.0 (iOS 15.0+)
Apple made it possible in very native way.
You just need to use .searchable() modifier with view you want to make searchable and ensure that you have NavigationView as parent of your views.
Example
struct CountriesView: View {
let countries = ["United States", "India", "Ukraine", "Russia", "Sweden"]
#State var search: String = ""
var body: some View {
NavigationView {
List(countries.filter({
// we filter our country list by checking whether country name
// does contain search string or not
$0.lowercased().contains(search.lowercased())
})) { country in
Text(country)
}.searchable(text: $search)
}
}
}
By the way, you can use computed properties to filter your array in more convenient way.
Then, the computed property for the filter from the example will be:
var searchResults: [String] {
return countries.filter({$0.lowercased().contains(search.lowercased())})
}
So, List will look like this:
List(searchResults) {
// ...
}
p.s. you can learn more about computed variables & properties in this swift.org article

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