about summary refs log tree commit diff
path: root/modules/react-native-ui-text-view/ios/RNUITextView.swift
diff options
context:
space:
mode:
Diffstat (limited to 'modules/react-native-ui-text-view/ios/RNUITextView.swift')
-rw-r--r--modules/react-native-ui-text-view/ios/RNUITextView.swift141
1 files changed, 141 insertions, 0 deletions
diff --git a/modules/react-native-ui-text-view/ios/RNUITextView.swift b/modules/react-native-ui-text-view/ios/RNUITextView.swift
new file mode 100644
index 000000000..9c21d45b5
--- /dev/null
+++ b/modules/react-native-ui-text-view/ios/RNUITextView.swift
@@ -0,0 +1,141 @@
+class RNUITextView: UIView {
+  var textView: UITextView
+
+  @objc var numberOfLines: Int = 0 {
+    didSet {
+      textView.textContainer.maximumNumberOfLines = numberOfLines
+    }
+  }
+  @objc var selectable: Bool = true {
+    didSet {
+      textView.isSelectable = selectable
+    }
+  }
+  @objc var ellipsizeMode: String = "tail" {
+    didSet {
+      textView.textContainer.lineBreakMode = self.getLineBreakMode()
+    }
+  }
+  @objc var onTextLayout: RCTDirectEventBlock?
+
+  override init(frame: CGRect) {
+    if #available(iOS 16.0, *) {
+      textView = UITextView(usingTextLayoutManager: false)
+    } else {
+      textView = UITextView()
+    }
+
+    // Disable scrolling
+    textView.isScrollEnabled = false
+    // Remove all the padding
+    textView.textContainerInset = .zero
+    textView.textContainer.lineFragmentPadding = 0
+
+    // Remove other properties
+    textView.isEditable = false
+    textView.backgroundColor = .clear
+
+    // Init
+    super.init(frame: frame)
+    self.clipsToBounds = true
+
+    // Add the view
+    addSubview(textView)
+
+    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
+    tapGestureRecognizer.isEnabled = true
+    textView.addGestureRecognizer(tapGestureRecognizer)
+  }
+
+  required init?(coder: NSCoder) {
+    fatalError("init(coder:) has not been implemented")
+  }
+
+  // Resolves some animation issues
+  override func reactSetFrame(_ frame: CGRect) {
+    UIView.performWithoutAnimation {
+      super.reactSetFrame(frame)
+    }
+  }
+
+  func setText(string: NSAttributedString, size: CGSize, numberOfLines: Int) -> Void {
+    self.textView.frame.size = size
+    self.textView.textContainer.maximumNumberOfLines = numberOfLines
+    self.textView.attributedText = string
+    self.textView.selectedTextRange = nil
+
+    if let onTextLayout = self.onTextLayout {
+      var lines: [String] = []
+      textView.layoutManager.enumerateLineFragments(
+        forGlyphRange: NSRange(location: 0, length: textView.attributedText.length))
+      { (rect, usedRect, textContainer, glyphRange, stop) in
+        let characterRange = self.textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
+        let line = (self.textView.text as NSString).substring(with: characterRange)
+        lines.append(line)
+      }
+
+      onTextLayout([
+        "lines": lines
+      ])
+    }
+  }
+
+  @IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
+    // If we find a child, then call onPress
+    if let child = getPressed(sender) {
+      if textView.selectedTextRange == nil, let onPress = child.onPress {
+        onPress(["": ""])
+      } 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) -> RNUITextViewChild? {
+    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
+    )
+
+    for child in self.reactSubviews() {
+      if let child = child as? RNUITextViewChild, let childText = child.text {
+        let fullText = self.textView.attributedText.string
+        let range = fullText.range(of: childText)
+
+        if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
+          if charIndex >= lowerBound.utf16Offset(in: fullText) && charIndex <= upperBound.utf16Offset(in: fullText) {
+            return child
+          }
+        }
+      }
+    }
+
+    return nil
+  }
+
+  func getLineBreakMode() -> NSLineBreakMode {
+    switch self.ellipsizeMode {
+    case "head":
+      return .byTruncatingHead
+    case "middle":
+      return .byTruncatingMiddle
+    case "tail":
+      return .byTruncatingTail
+    case "clip":
+      return .byClipping
+    default:
+      return .byTruncatingTail
+    }
+  }
+}