Table of Contents
SwiftUI’s Text() view does not support custom colored highlights and custom menus. Here’s how I did it. The solution may not be up to everyone’s programming standards but it worked.
CustomUITextView
Create a new file called CustomUITextView which will inherit UITextView. The initial code should look like
import UIKit
class CustomTextView: UITextView {
}TextSelectable
Now, let’s create another file called TextSelectable which will inherit UIViewRepresentable.
import SwiftUI
struct TextSelectable: UIViewRepresentable {
var text: NSAttributedString
init(text: NSAttributedString) { self.text = text }
func makeUIView(context: Context) -> CustomTextView { let customTextView = CustomTextView() customTextView.delegate = context.coordinator return customTextView }
func updateUIView(_ uiView: CustomTextView, context: Context) { uiView.attributedText = text }
func makeCoordinator() -> Coordinator { Coordinator(text) }
class Coordinator: NSObject, UITextViewDelegate {
var text: NSAttributedString
init(_ text: NSAttributedString) { self.text = text }
func textViewDidChange(_ textView: UITextView) { self.text = textView.attributedText } }}highlighText()
Let’s create a highlighText() function inside our CustomUITextView(). And we will override the canPerformAction function. This is where we will capture our highlighted text. The new CustomUITextView should be like the one below.
import UIKit
class CustomTextView: UITextView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { if action == #selector(highlightText) { return true } return false }
@objc func highlightText() { if let range = self.selectedTextRange, let selectedText = self.text(in: range) { print("Selected text is \(selectedText)") } }}Custom UIMenu
Next, we will create our own UIMenu when a user long presses or clicks on a highlighted text. We will override the editMenu function.
import UIKit
class CustomTextView: UITextView {
override func editMenu(for textRange: UITextRange, suggestedActions: [UIMenuElement]) -> UIMenu? { let highlightTextAction = UIAction(title: "Highlight Passage") { action in self.highlightText() } let addNotesAction = UIAction(title: "Add Notes") { action in
} var actions = suggestedActions actions.insert(highlightTextAction, at: 0) actions.insert(addNotesAction, at: 1) return UIMenu(children: actions) }
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { if action == #selector(highlightText) { return true } return false }
@objc func highlightText() {
}}SwiftUI View
Time to implement it in our SwiftUI View. Create a new file, I called mine DetailsView. Let’s use a verse from Kendrick Lamar as an example text passed to our TextSelectable.
import SwiftUI
struct DetailsView: View {
@State private var attributedText = NSAttributedString(string: """ This feelin' is brought to you by adrenaline and good rap Black Pendleton ball cap (West, west, west) We don't share the same synonym, fall back (West, west, west) Been in it before Internet had new acts Mimicking radio's nemesis made me wack My innocence limited, the experience lacked Ten of us with no tentative tactic that cracked The mind of a literate writer, but I did it, in fact You admitted it once I submitted it, wrapped in plastic Remember scribblin', scratchin' diligent sentences backwards Visiting freestyle cyphers for your reaction Now, I can live in a stadium, pack it the fastest Gamblin' Benjamin benefits, sinnin' in traffic Spinnin' women in cartwheels, linen fabric on fashion Winnin' in every decision, Kendrick is master that mastered it Isn't it lovely how menaces turned attraction? Pivotin' rappers, finish your fraction while writing blue magic Thank God for rap I would say it got me a plaque, but what's better than that? The fact it brought me back home """)
var body: some View { VStack { TextSelectable(text: NSAttributedString(string: attributedText)) } .padding() }}Now try to run it in your emulator. And drag a portion of the text and click on our Highlight Passage menu.
We should be able to capture the highlighted text.
Colored highlights
Now that we can capture the highlighted texts, we want to add colors to it. Let’s create a file called Highlight.
import SwiftUI
struct Highlight {
let text: String let location: Int let length: Int let color: Color}Now, inside our highlightText() function, let’s create that Highlight object. And we will pass this object to our NotificationCenter, where our DetailsView will listen and capture our object.
import UIKit
class CustomTextView: UITextView {
override func editMenu(for textRange: UITextRange, suggestedActions: [UIMenuElement]) -> UIMenu? { let highlightTextAction = UIAction(title: "Highlight Passage") { action in self.highlightText() } let addNotesAction = UIAction(title: "Add Notes") { action in
} var actions = suggestedActions actions.insert(highlightTextAction, at: 0) actions.insert(addNotesAction, at: 1) return UIMenu(children: actions) }
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { if action == #selector(highlightText) { return true } return false }
@objc func highlightText() { if let range = self.selectedTextRange, let selectedText = self.text(in: range) { print("Selected text is \(selectedText)") let highlight = Highlight(text: selectedText, location: selectedRange.location, length: selectedRange.length, color: .purple) let highlightDict = [ "data": highlight ] NotificationCenter.default.post(name: Notification.Name("highlightAdded"), object: nil, userInfo: highlightDict) } }}I am not sure if this is the best way, please let me know if you have any better solutions.
And now in our DetailsView, let’s capture it using onReceive modifier.
import SwiftUI
struct DetailsView: View {
@State private var attributedText = NSAttributedString(string: """ This feelin' is brought to you by adrenaline and good rap Black Pendleton ball cap (West, west, west) We don't share the same synonym, fall back (West, west, west) Been in it before Internet had new acts Mimicking radio's nemesis made me wack My innocence limited, the experience lacked Ten of us with no tentative tactic that cracked The mind of a literate writer, but I did it, in fact You admitted it once I submitted it, wrapped in plastic Remember scribblin', scratchin' diligent sentences backwards Visiting freestyle cyphers for your reaction Now, I can live in a stadium, pack it the fastest Gamblin' Benjamin benefits, sinnin' in traffic Spinnin' women in cartwheels, linen fabric on fashion Winnin' in every decision, Kendrick is master that mastered it Isn't it lovely how menaces turned attraction? Pivotin' rappers, finish your fraction while writing blue magic Thank God for rap I would say it got me a plaque, but what's better than that? The fact it brought me back home """)
var body: some View { VStack { TextSelectable(text: NSAttributedString(string: attributedText)) } .onReceive(NotificationCenter.default.publisher(for: Notification.Name("highlightAdded"))) { output in if let highlight = output.userInfo!["data"] as? Highlight { let mutableString = NSMutableAttributedString.init(string: attributedText.string) let highlightAttributes: [NSAttributedString.Key: Any] = [ .backgroundColor: highlight.color, ] mutableString.addAttributes(highlightAttributes, range: NSRange(location: highlight.location, length: highlight.length)) attributedText = mutableString } } .padding() }}Run the app and it will throw an error.
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__SwiftValue set]: unrecognized selector sent to instance 0x600000c6c360'*** First throw call stack:( 0 CoreFoundation 0x00000001804ae0f8 __exceptionPreprocess + 172This is because of our color variable in our Highlight. Let’s update our Highlights class to use uiColor instead.
import SwiftUI
struct Highlight {
let text: String let location: Int let length: Int var uiColor: UIColor var color: Color { get { .init(uiColor: uiColor) } set { uiColor = .init(newValue) } }}In our highlighText() function, use uiColor instead of color.
@objc func highlightText() { if let range = self.selectedTextRange, let selectedText = self.text(in: range) { print("Selected text is \(selectedText)") let highlight = Highlight(text: selectedText, location: selectedRange.location, length: selectedRange.length, uiColor: .purple) let highlightDict = [ "data": highlight ] NotificationCenter.default.post(name: Notification.Name("highlightAdded"), object: nil, userInfo: highlightDict) } }Also, update our onReceive highlight object.
if let highlight = output.userInfo!["data"] as? Highlight { let mutableString = NSMutableAttributedString.init(string: attributedText.string) let highlightAttributes: [NSAttributedString.Key: Any] = [ .backgroundColor: highlight.uiColor, ] mutableString.addAttributes(highlightAttributes, range: NSRange(location: highlight.location, length: highlight.length)) attributedText = mutableString } }Everything should work now.
You can find the code at https://github.com/lawgimenez/customtext.