aa96bba066
what are you doing there? go away fix recognizer to clear selected text on tap remove jank/hacks update readme remove android stuff (?) don't remove clipped subview on android for selection enable selection of alt text add numberOfLines properly apply container styles handle both selection and expand press events in alt text far better implementation revert link changes revert lightbox changes for now fix file name
193 lines
6.1 KiB
Swift
193 lines
6.1 KiB
Swift
import ExpoModulesCore
|
|
|
|
class ExpoUITextView: ExpoView {
|
|
var textView: UITextView
|
|
var textChildren: [ExpoUITextViewChild] = []
|
|
|
|
var tapGestureRecognizer: UITapGestureRecognizer?
|
|
|
|
public required init(appContext: AppContext? = nil) {
|
|
if #available(iOS 16.0, *) {
|
|
textView = UITextView(usingTextLayoutManager: false)
|
|
} else {
|
|
textView = UITextView()
|
|
}
|
|
|
|
// Configure default appearance
|
|
textView.scrollsToTop = false
|
|
textView.isEditable = false
|
|
textView.isScrollEnabled = false
|
|
textView.backgroundColor = .clear
|
|
textView.textContainer.lineBreakMode = .byTruncatingTail
|
|
textView.isSelectable = true
|
|
|
|
// Remove all of the padding from the view
|
|
textView.textContainerInset = UIEdgeInsets.zero
|
|
textView.textContainer.lineFragmentPadding = 0
|
|
|
|
super.init(appContext: appContext)
|
|
|
|
addSubview(textView)
|
|
|
|
// Configure the tap gesture recognizer
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
|
|
tapGestureRecognizer.isEnabled = true
|
|
self.tapGestureRecognizer = tapGestureRecognizer
|
|
textView.addGestureRecognizer(tapGestureRecognizer)
|
|
}
|
|
|
|
// Update children whenever new react subviews are added
|
|
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
|
if subview.isKind(of: ExpoUITextViewChild.self) {
|
|
insertSubview(subview, at: atIndex)
|
|
self.getTextChildren()
|
|
}
|
|
}
|
|
|
|
// Do the same whenever subviews are removed
|
|
override func removeReactSubview(_ subview: UIView!) {
|
|
if subview.isKind(of: ExpoUITextViewChild.self) {
|
|
subview.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
override func reactSubviews() -> [UIView]! {
|
|
return subviews
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
// Get the width from the bounds
|
|
let maxWidth = bounds.width
|
|
// Calculate the size of the text
|
|
let sizeThatFits = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat(MAXFLOAT)))
|
|
let size = CGSize(width: maxWidth, height: sizeThatFits.height)
|
|
|
|
// Set the textview's frame
|
|
textView.frame.size = size
|
|
self.appContext?.reactBridge?.uiManager.setSize(size, for: self)
|
|
|
|
}
|
|
|
|
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
|
|
// If we find a child, then call onPress
|
|
if let child = getPressed(sender) {
|
|
if textView.selectedTextRange == nil {
|
|
child.onTextPress()
|
|
} else {
|
|
// Clear the selected text range if we are not pressing on a link
|
|
textView.selectedTextRange = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to get the pressed segment
|
|
func getPressed(_ sender: UITapGestureRecognizer) -> ExpoUITextViewChild? {
|
|
let layoutManager = textView.layoutManager
|
|
var location = sender.location(in: textView)
|
|
|
|
// Remove the padding
|
|
location.x -= textView.textContainerInset.left
|
|
location.y -= textView.textContainerInset.top
|
|
|
|
// Get the index of the char
|
|
let charIndex = layoutManager.characterIndex(
|
|
for: location,
|
|
in: textView.textContainer,
|
|
fractionOfDistanceBetweenInsertionPoints: nil
|
|
)
|
|
|
|
let text = textView.attributedText.string
|
|
var foundChild: ExpoUITextViewChild?
|
|
|
|
// Check each segment
|
|
self.textChildren.forEach { child in
|
|
let range = text.range(of: child.text ?? "")
|
|
// Figure out the bounds
|
|
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
|
|
if charIndex >= lowerBound.utf16Offset(in: text), charIndex <= upperBound.utf16Offset(in: text) {
|
|
foundChild = child
|
|
}
|
|
}
|
|
}
|
|
|
|
return foundChild
|
|
}
|
|
|
|
// Get the children. Always use getTextChildren() so that we ensure the correct order of views
|
|
func getTextChildren() -> Void {
|
|
var children: [ExpoUITextViewChild] = []
|
|
|
|
self.reactSubviews().forEach { view in
|
|
if view.isKind(of: ExpoUITextViewChild.self) {
|
|
children.append(view as! ExpoUITextViewChild)
|
|
}
|
|
}
|
|
|
|
// Save the children for our onPress handler
|
|
self.textChildren = children
|
|
// Update the UITextView with the styled text
|
|
self.setText()
|
|
}
|
|
|
|
func setText() -> Void {
|
|
// Create an attributed string to store each of the segments
|
|
let finalAttributedString = NSMutableAttributedString()
|
|
|
|
self.textChildren.forEach { child in
|
|
// If we don't have any text in this child, move to the next one
|
|
guard let text = child.text else {
|
|
return
|
|
}
|
|
|
|
// Set some generic attributes that don't need ranges
|
|
let attributes: [NSAttributedString.Key:Any] = [
|
|
.font: UIFont.systemFont(
|
|
ofSize: child.style?.fontSize ?? 12.0,
|
|
weight: child.style?.fontWeight?.toFontWeight() ?? .regular
|
|
),
|
|
.foregroundColor: TextUtil.hexToUIColor(hex: child.style?.color),
|
|
]
|
|
|
|
// Create the attributed string with the generic attributes
|
|
let string = NSMutableAttributedString(string: text, attributes: attributes)
|
|
|
|
// Set the paragraph style attributes if necessary
|
|
if let lineHeight = child.style?.lineHeight {
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.minimumLineHeight = lineHeight
|
|
paragraphStyle.maximumLineHeight = lineHeight
|
|
string.addAttribute(
|
|
NSAttributedString.Key.paragraphStyle,
|
|
value: paragraphStyle,
|
|
range: NSMakeRange(0, string.length)
|
|
)
|
|
}
|
|
|
|
if let textDecorationLine = child.style?.textDecorationLine {
|
|
if textDecorationLine == .underline || textDecorationLine == .underlineLineThrough {
|
|
string.addAttribute(
|
|
NSAttributedString.Key.underlineStyle,
|
|
value: NSUnderlineStyle.single.rawValue,
|
|
range: NSMakeRange(0, string.length)
|
|
)
|
|
}
|
|
|
|
if textDecorationLine == .lineThrough || textDecorationLine == .underlineLineThrough {
|
|
string.addAttribute(
|
|
NSAttributedString.Key.strikethroughStyle,
|
|
value: NSUnderlineStyle.single.rawValue,
|
|
range: NSMakeRange(0, string.length)
|
|
)
|
|
}
|
|
}
|
|
|
|
finalAttributedString.append(string)
|
|
}
|
|
|
|
textView.attributedText = finalAttributedString
|
|
textView.selectedTextRange = nil
|
|
|
|
self.setNeedsLayout()
|
|
}
|
|
}
|