Files
bsky-social-app/modules/expo-selectable-text/ios/ExpoSelectableTextView.swift
T
2024-01-12 19:17:51 -08:00

142 lines
4.9 KiB
Swift

import Foundation
import ExpoModulesCore
class ExpoSelectableTextView: ExpoView {
let textView: UITextView = UITextView()
var segments: Array<TextSegment> = [] {
didSet {
// We don't want to set the text if the root style has not been set yet
self.setText()
}
}
var style: TextStyle? {
didSet {
// If the text has not been set and there are segments, set the text
self.setText()
}
}
let onTextPress = EventDispatcher()
let onTextLongPress = EventDispatcher()
let onTextLayout = EventDispatcher()
public required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
// Configure default appearance
textView.scrollsToTop = false
textView.isEditable = false
textView.isScrollEnabled = false
textView.backgroundColor = .clear
// Remove all of the padding from the view
textView.textContainerInset = UIEdgeInsets.zero
textView.textContainer.lineFragmentPadding = 0
// Add the text view to the root view
addSubview(textView)
// Configure the press recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
textView.addGestureRecognizer(tapGestureRecognizer)
}
override func layoutSubviews() -> Void {
// Set the textView's frame on layout
setSize()
}
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
if let segment = getPressedSegment(sender) {
if segment.handlePress, textView.selectedTextRange == nil {
onTextPress([
"index": segment.index
])
} else {
// Clear the selected text range if we are not pressing on a link
textView.selectedTextRange = nil
}
}
}
func setSize() -> Void {
// Figure out the height of our text and create a CGRect
let maxWidth = bounds.width
let sizeThatFits = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat(MAXFLOAT)))
let size = CGSize(width: maxWidth, height: sizeThatFits.height)
textView.frame.size = size
onTextLayout([
"height": sizeThatFits.height,
"width": maxWidth
])
}
func setText() -> Void {
let finalAttributedString = NSMutableAttributedString()
segments.forEach { segment in
// Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [
.font: UIFont.systemFont(ofSize: segment.style?.fontSize ?? self.style?.fontSize ?? 12.0, weight: segment.style?.fontWeight?.toFontWeight() ?? self.style?.fontWeight?.toFontWeight() ?? .regular),
.foregroundColor: ExpoSelectableTextUtil.hexToUIColor(hex: segment.style?.color),
]
// Create the attributed string with the generic attributes
let string = NSMutableAttributedString(string: segment.text, attributes: attributes)
// Set the paragraph style attributes if necessary
if let lineHeight = segment.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 = segment.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
self.setNeedsLayout()
}
func getPressedSegment(_ sender: UITapGestureRecognizer) -> TextSegment? {
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 foundSegment: TextSegment?
// Check each segment
segments.forEach { segment in
let range = text.range(of: segment.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) {
foundSegment = segment
}
}
}
return foundSegment
}
}