Skip to content

Commit d29eacf

Browse files
graycreateclaude
andcommitted
fix: implement proper text selection using UITextView
Replaced SwiftUI Text with UITextView wrapper to enable full text selection and copy/paste functionality. The previous .textSelection(.enabled) modifier didn't work reliably, especially with custom URL handlers. Changes: - Created SelectableTextView using UIViewRepresentable - Wrapped UITextView with isSelectable=true, isEditable=false - Maintained all styling (font size, line spacing) - Preserved link tap handling through UITextViewDelegate - Now supports native iOS text selection gestures and copy/paste 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 914a00f commit d29eacf

File tree

1 file changed

+72
-8
lines changed

1 file changed

+72
-8
lines changed

V2er/Sources/RichView/Views/RichView.swift

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,12 @@ public struct RichView: View {
6666
} else if let error = error {
6767
ErrorView(error: error)
6868
} else if let attributedString = attributedString {
69-
Text(attributedString)
70-
.font(.system(size: configuration.stylesheet.body.fontSize))
71-
.lineSpacing(configuration.stylesheet.body.lineSpacing)
72-
.textSelection(.enabled)
73-
.environment(\.openURL, OpenURLAction { url in
74-
onLinkTapped?(url)
75-
return .handled
76-
})
69+
SelectableTextView(
70+
attributedString: attributedString,
71+
fontSize: configuration.stylesheet.body.fontSize,
72+
lineSpacing: configuration.stylesheet.body.lineSpacing,
73+
onLinkTapped: onLinkTapped
74+
)
7775
} else {
7876
Text("No content")
7977
.foregroundColor(.secondary)
@@ -248,4 +246,70 @@ public struct RenderMetadata {
248246

249247
/// Number of @mentions in the content
250248
public let mentionCount: Int
249+
}
250+
251+
// MARK: - Selectable Text View
252+
253+
/// A UIViewRepresentable wrapper for UITextView that supports text selection
254+
@available(iOS 18.0, *)
255+
struct SelectableTextView: UIViewRepresentable {
256+
let attributedString: AttributedString
257+
let fontSize: CGFloat
258+
let lineSpacing: CGFloat
259+
let onLinkTapped: ((URL) -> Void)?
260+
261+
func makeUIView(context: Context) -> UITextView {
262+
let textView = UITextView()
263+
textView.isEditable = false
264+
textView.isSelectable = true
265+
textView.isScrollEnabled = false
266+
textView.backgroundColor = .clear
267+
textView.textContainerInset = .zero
268+
textView.textContainer.lineFragmentPadding = 0
269+
textView.delegate = context.coordinator
270+
textView.dataDetectorTypes = []
271+
textView.linkTextAttributes = [
272+
.foregroundColor: UIColor.systemBlue,
273+
.underlineStyle: NSUnderlineStyle.single.rawValue
274+
]
275+
return textView
276+
}
277+
278+
func updateUIView(_ textView: UITextView, context: Context) {
279+
// Convert AttributedString to NSAttributedString
280+
var nsAttributedString = NSAttributedString(attributedString)
281+
282+
// Apply line spacing
283+
let paragraphStyle = NSMutableParagraphStyle()
284+
paragraphStyle.lineSpacing = lineSpacing
285+
286+
let mutableAttributedString = NSMutableAttributedString(attributedString: nsAttributedString)
287+
mutableAttributedString.addAttribute(
288+
.paragraphStyle,
289+
value: paragraphStyle,
290+
range: NSRange(location: 0, length: mutableAttributedString.length)
291+
)
292+
293+
textView.attributedText = mutableAttributedString
294+
}
295+
296+
func makeCoordinator() -> Coordinator {
297+
Coordinator(onLinkTapped: onLinkTapped)
298+
}
299+
300+
class Coordinator: NSObject, UITextViewDelegate {
301+
let onLinkTapped: ((URL) -> Void)?
302+
303+
init(onLinkTapped: ((URL) -> Void)?) {
304+
self.onLinkTapped = onLinkTapped
305+
}
306+
307+
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
308+
if interaction == .invokeDefaultAction {
309+
onLinkTapped?(URL)
310+
return false
311+
}
312+
return true
313+
}
314+
}
251315
}

0 commit comments

Comments
 (0)