add iOS BookTextView: UIViewRepresentable with NSAttributedString, highlight, tap-to-seek, scroll-to-offset
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
75
Vorleser-iOS/BookTextView.swift
Normal file
75
Vorleser-iOS/BookTextView.swift
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
import VorleserKit
|
||||||
|
|
||||||
|
struct BookTextView: UIViewRepresentable {
|
||||||
|
let attributedText: NSAttributedString
|
||||||
|
let highlightRange: Range<Int>?
|
||||||
|
let onTapCharacter: (CharacterOffset) -> Void
|
||||||
|
var scrollToOffset: CharacterOffset?
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UITextView {
|
||||||
|
let textView = UITextView()
|
||||||
|
textView.isEditable = false
|
||||||
|
textView.isSelectable = false
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
|
||||||
|
textView.addGestureRecognizer(tap)
|
||||||
|
|
||||||
|
return textView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ textView: UITextView, context: Context) {
|
||||||
|
let highlighted = NSMutableAttributedString(attributedString: attributedText)
|
||||||
|
|
||||||
|
if let range = highlightRange,
|
||||||
|
range.lowerBound >= 0,
|
||||||
|
range.upperBound <= attributedText.length {
|
||||||
|
let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
|
||||||
|
highlighted.addAttribute(.backgroundColor, value: UIColor.systemYellow.withAlphaComponent(0.3), range: nsRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
if textView.attributedText != highlighted {
|
||||||
|
textView.attributedText = highlighted
|
||||||
|
}
|
||||||
|
|
||||||
|
if let range = highlightRange,
|
||||||
|
range.lowerBound >= 0,
|
||||||
|
range.upperBound <= attributedText.length {
|
||||||
|
let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
|
||||||
|
textView.scrollRangeToVisible(nsRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let offset = scrollToOffset,
|
||||||
|
offset >= 0,
|
||||||
|
offset < attributedText.length {
|
||||||
|
let nsRange = NSRange(location: offset, length: 1)
|
||||||
|
textView.scrollRangeToVisible(nsRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(onTapCharacter: onTapCharacter)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject {
|
||||||
|
let onTapCharacter: (CharacterOffset) -> Void
|
||||||
|
|
||||||
|
init(onTapCharacter: @escaping (CharacterOffset) -> Void) {
|
||||||
|
self.onTapCharacter = onTapCharacter
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard let textView = gesture.view as? UITextView else { return }
|
||||||
|
let point = gesture.location(in: textView)
|
||||||
|
let characterIndex = textView.offset(
|
||||||
|
from: textView.beginningOfDocument,
|
||||||
|
to: textView.closestPosition(to: point) ?? textView.beginningOfDocument
|
||||||
|
)
|
||||||
|
if characterIndex < textView.attributedText.length {
|
||||||
|
onTapCharacter(characterIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user