about summary refs log tree commit diff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/Share-with-Bluesky/Info.plist41
-rw-r--r--modules/Share-with-Bluesky/Share-with-Bluesky.entitlements10
-rw-r--r--modules/Share-with-Bluesky/ShareViewController.swift153
-rw-r--r--modules/expo-receive-android-intents/README.md8
-rw-r--r--modules/expo-receive-android-intents/android/.gitignore15
-rw-r--r--modules/expo-receive-android-intents/android/build.gradle92
-rw-r--r--modules/expo-receive-android-intents/android/src/main/AndroidManifest.xml2
-rw-r--r--modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt119
-rw-r--r--modules/expo-receive-android-intents/expo-module.config.json6
-rw-r--r--modules/react-native-ui-text-view/ios/RNUITextView.swift18
-rw-r--r--modules/react-native-ui-text-view/ios/RNUITextViewManager.m1
-rw-r--r--modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift43
12 files changed, 486 insertions, 22 deletions
diff --git a/modules/Share-with-Bluesky/Info.plist b/modules/Share-with-Bluesky/Info.plist
new file mode 100644
index 000000000..90fe92345
--- /dev/null
+++ b/modules/Share-with-Bluesky/Info.plist
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>NSExtension</key>
+    <dict>
+      <key>NSExtensionPrincipalClass</key>
+      <string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
+      <key>NSExtensionAttributes</key>
+      <dict>
+        <key>NSExtensionActivationRule</key>
+        <dict>
+          <key>NSExtensionActivationSupportsText</key>
+          <true/>
+          <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
+          <integer>1</integer>
+          <key>NSExtensionActivationSupportsImageWithMaxCount</key>
+          <integer>10</integer>
+        </dict>
+      </dict>
+      <key>NSExtensionPointIdentifier</key>
+      <string>com.apple.share-services</string>
+    </dict>
+    <key>MainAppScheme</key>
+    <string>bluesky</string>
+    <key>CFBundleName</key>
+    <string>$(PRODUCT_NAME)</string>
+    <key>CFBundleDisplayName</key>
+    <string>Extension</string>
+    <key>CFBundleIdentifier</key>
+    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+    <key>CFBundleVersion</key>
+    <string>$(CURRENT_PROJECT_VERSION)</string>
+    <key>CFBundleExecutable</key>
+    <string>$(EXECUTABLE_NAME)</string>
+    <key>CFBundlePackageType</key>
+    <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+    <key>CFBundleShortVersionString</key>
+    <string>$(MARKETING_VERSION)</string>
+  </dict>
+</plist>
diff --git a/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements
new file mode 100644
index 000000000..d2253d31f
--- /dev/null
+++ b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>com.apple.security.application-groups</key>
+    <array>
+      <string>group.app.bsky</string>
+    </array>
+  </dict>
+</plist>
diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift
new file mode 100644
index 000000000..4c1d635ce
--- /dev/null
+++ b/modules/Share-with-Bluesky/ShareViewController.swift
@@ -0,0 +1,153 @@
+import UIKit
+
+class ShareViewController: UIViewController {
+  // This allows other forks to use this extension while also changing their
+  // scheme.
+  let appScheme = Bundle.main.object(forInfoDictionaryKey: "MainAppScheme") as? String ?? "bluesky"
+
+  //
+  override func viewDidAppear(_ animated: Bool) {
+    super.viewDidAppear(animated)
+
+    guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
+          let attachments = extensionItem.attachments,
+          let firstAttachment = extensionItem.attachments?.first
+    else {
+      self.completeRequest()
+      return
+    }
+
+    Task {
+      if firstAttachment.hasItemConformingToTypeIdentifier("public.text") {
+        await self.handleText(item: firstAttachment)
+      } else if firstAttachment.hasItemConformingToTypeIdentifier("public.url") {
+        await self.handleUrl(item: firstAttachment)
+      } else if firstAttachment.hasItemConformingToTypeIdentifier("public.image") {
+        await self.handleImages(items: attachments)
+      } else {
+        self.completeRequest()
+      }
+    }
+  }
+
+  private func handleText(item: NSItemProvider) async -> Void {
+    do {
+      if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
+        if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+           let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
+        {
+          _ = self.openURL(url)
+        }
+      }
+      self.completeRequest()
+    } catch {
+      self.completeRequest()
+    }
+  }
+
+  private func handleUrl(item: NSItemProvider) async -> Void {
+    do {
+      if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
+        if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+           let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
+        {
+          _ = self.openURL(url)
+        }
+      }
+      self.completeRequest()
+    } catch {
+      self.completeRequest()
+    }
+  }
+
+  private func handleImages(items: [NSItemProvider]) async -> Void {
+    let firstFourItems: [NSItemProvider]
+    if items.count < 4 {
+      firstFourItems = items
+    } else {
+      firstFourItems = Array(items[0...3])
+    }
+
+    var valid = true
+    var imageUris = ""
+
+    for (index, item) in firstFourItems.enumerated() {
+      var imageUriInfo: String? = nil
+
+      do {
+        if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
+          // We need to duplicate this image, since we don't have access to the outgoing temp directory
+          // We also will get the image dimensions here, sinze RN makes it difficult to get those dimensions for local files
+          let data = try Data(contentsOf: dataUri)
+          let image = UIImage(data: data)
+          imageUriInfo = self.saveImageWithInfo(image)
+        } else if let image = try await item.loadItem(forTypeIdentifier: "public.image") as? UIImage {
+          imageUriInfo = self.saveImageWithInfo(image)
+        }
+      } catch {
+        valid = false
+      }
+
+      if let imageUriInfo = imageUriInfo {
+        imageUris.append(imageUriInfo)
+        if index < items.count - 1 {
+          imageUris.append(",")
+        }
+      } else {
+        valid = false
+      }
+    }
+
+    if valid,
+       let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+       let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)")
+    {
+      _ = self.openURL(url)
+    }
+
+    self.completeRequest()
+  }
+
+  private func saveImageWithInfo(_ image: UIImage?) -> String? {
+    guard let image = image else {
+      return nil
+    }
+
+    do {
+      // Saving this file to the bundle group's directory lets us access it from
+      // inside of the app. Otherwise, we wouldn't have access even though the
+      // extension does.
+      if let dir = FileManager()
+        .containerURL(
+          forSecurityApplicationGroupIdentifier: "group.app.bsky")
+      {
+        let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
+
+        if let newUri = URL(string: filePath),
+           let jpegData = image.jpegData(compressionQuality: 1)
+        {
+          try jpegData.write(to: newUri)
+          return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
+        }
+      }
+      return nil
+    } catch {
+      return nil
+    }
+  }
+
+  private func completeRequest() -> Void {
+    self.extensionContext?.completeRequest(returningItems: nil)
+  }
+
+  @objc func openURL(_ url: URL) -> Bool {
+    var responder: UIResponder? = self
+    while responder != nil {
+      if let application = responder as? UIApplication {
+          return application.perform(#selector(openURL(_:)), with: url) != nil
+      }
+      responder = responder?.next
+    }
+    return false
+  }
+}
diff --git a/modules/expo-receive-android-intents/README.md b/modules/expo-receive-android-intents/README.md
new file mode 100644
index 000000000..7e8506860
--- /dev/null
+++ b/modules/expo-receive-android-intents/README.md
@@ -0,0 +1,8 @@
+# Expo Receive Android Intents
+
+This module handles incoming intents on Android. Handled intents are `text/plain` and `image/*` (single or multiple).
+The module handles saving images to the app's filesystem for access within the app, limiting the selection of images
+to a max of four, and handling intent types. No JS code is required for this module, and it is no-op on non-android
+platforms.
+
+No installation is required. Gradle will automatically add this module on build.
diff --git a/modules/expo-receive-android-intents/android/.gitignore b/modules/expo-receive-android-intents/android/.gitignore
new file mode 100644
index 000000000..877b87e9a
--- /dev/null
+++ b/modules/expo-receive-android-intents/android/.gitignore
@@ -0,0 +1,15 @@
+# OSX
+#
+.DS_Store
+
+# Android/IntelliJ
+#
+build/
+.idea
+.gradle
+local.properties
+*.iml
+*.hprof
+
+# Bundle artifacts
+*.jsbundle
diff --git a/modules/expo-receive-android-intents/android/build.gradle b/modules/expo-receive-android-intents/android/build.gradle
new file mode 100644
index 000000000..3712dda40
--- /dev/null
+++ b/modules/expo-receive-android-intents/android/build.gradle
@@ -0,0 +1,92 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'maven-publish'
+
+group = 'xyz.blueskyweb.app.exporeceiveandroidintents'
+version = '0.4.1'
+
+buildscript {
+  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
+  if (expoModulesCorePlugin.exists()) {
+    apply from: expoModulesCorePlugin
+    applyKotlinExpoModulesCorePlugin()
+  }
+
+  // Simple helper that allows the root project to override versions declared by this library.
+  ext.safeExtGet = { prop, fallback ->
+    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+  }
+
+  // Ensures backward compatibility
+  ext.getKotlinVersion = {
+    if (ext.has("kotlinVersion")) {
+      ext.kotlinVersion()
+    } else {
+      ext.safeExtGet("kotlinVersion", "1.8.10")
+    }
+  }
+
+  repositories {
+    mavenCentral()
+  }
+
+  dependencies {
+    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
+  }
+}
+
+afterEvaluate {
+  publishing {
+    publications {
+      release(MavenPublication) {
+        from components.release
+      }
+    }
+    repositories {
+      maven {
+        url = mavenLocal().url
+      }
+    }
+  }
+}
+
+android {
+  compileSdkVersion safeExtGet("compileSdkVersion", 33)
+
+  def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
+  if (agpVersion.tokenize('.')[0].toInteger() < 8) {
+    compileOptions {
+      sourceCompatibility JavaVersion.VERSION_11
+      targetCompatibility JavaVersion.VERSION_11
+    }
+
+    kotlinOptions {
+      jvmTarget = JavaVersion.VERSION_11.majorVersion
+    }
+  }
+
+  namespace "xyz.blueskyweb.app.exporeceiveandroidintents"
+  defaultConfig {
+    minSdkVersion safeExtGet("minSdkVersion", 21)
+    targetSdkVersion safeExtGet("targetSdkVersion", 34)
+    versionCode 1
+    versionName "0.4.1"
+  }
+  lintOptions {
+    abortOnError false
+  }
+  publishing {
+    singleVariant("release") {
+      withSourcesJar()
+    }
+  }
+}
+
+repositories {
+  mavenCentral()
+}
+
+dependencies {
+  implementation project(':expo-modules-core')
+  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
+}
diff --git a/modules/expo-receive-android-intents/android/src/main/AndroidManifest.xml b/modules/expo-receive-android-intents/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..bdae66c8f
--- /dev/null
+++ b/modules/expo-receive-android-intents/android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<manifest>
+</manifest>
diff --git a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt
new file mode 100644
index 000000000..c2e17fb80
--- /dev/null
+++ b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt
@@ -0,0 +1,119 @@
+package xyz.blueskyweb.app.exporeceiveandroidintents
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import androidx.core.net.toUri
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+import java.io.File
+import java.io.FileOutputStream
+import java.net.URLEncoder
+
+class ExpoReceiveAndroidIntentsModule : Module() {
+  override fun definition() = ModuleDefinition {
+    Name("ExpoReceiveAndroidIntents")
+
+    OnNewIntent {
+      handleIntent(it)
+    }
+  }
+
+  private fun handleIntent(intent: Intent?) {
+    if(appContext.currentActivity == null || intent == null) return
+
+    if (intent.action == Intent.ACTION_SEND) {
+      if (intent.type == "text/plain") {
+        handleTextIntent(intent)
+      } else if (intent.type.toString().startsWith("image/")) {
+        handleImageIntent(intent)
+      }
+    } else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
+      if (intent.type.toString().startsWith("image/")) {
+        handleImagesIntent(intent)
+      }
+    }
+  }
+
+  private fun handleTextIntent(intent: Intent) {
+    intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
+      val encoded = URLEncoder.encode(it, "UTF-8")
+      "bluesky://intent/compose?text=${encoded}".toUri().let { uri ->
+        val newIntent = Intent(Intent.ACTION_VIEW, uri)
+        appContext.currentActivity?.startActivity(newIntent)
+      }
+    }
+  }
+
+  private fun handleImageIntent(intent: Intent) {
+    val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+      intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
+    } else {
+      intent.getParcelableExtra(Intent.EXTRA_STREAM)
+    }
+    if (uri == null) return
+
+    handleImageIntents(listOf(uri))
+  }
+
+  private fun handleImagesIntent(intent: Intent) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+      intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.let {
+        handleImageIntents(it.filterIsInstance<Uri>().take(4))
+      }
+    } else {
+      intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.let {
+        handleImageIntents(it.filterIsInstance<Uri>().take(4))
+      }
+    }
+  }
+
+  private fun handleImageIntents(uris: List<Uri>) {
+    var allParams = ""
+
+    uris.forEachIndexed { index, uri ->
+      val info = getImageInfo(uri)
+      val params = buildUriData(info)
+      allParams = "${allParams}${params}"
+
+      if (index < uris.count() - 1) {
+        allParams = "${allParams},"
+      }
+    }
+
+    val encoded = URLEncoder.encode(allParams, "UTF-8")
+
+    "bluesky://intent/compose?imageUris=${encoded}".toUri().let {
+      val newIntent = Intent(Intent.ACTION_VIEW, it)
+      appContext.currentActivity?.startActivity(newIntent)
+    }
+  }
+
+  private fun getImageInfo(uri: Uri): Map<String, Any> {
+    val bitmap = MediaStore.Images.Media.getBitmap(appContext.currentActivity?.contentResolver, uri)
+    // We have to save this so that we can access it later when uploading the image.
+    // createTempFile will automatically place a unique string between "img" and "temp.jpeg"
+    val file = File.createTempFile("img", "temp.jpeg", appContext.currentActivity?.cacheDir)
+    val out = FileOutputStream(file)
+    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
+    out.flush()
+    out.close()
+
+    return mapOf(
+      "width" to bitmap.width,
+      "height" to bitmap.height,
+      "path" to file.path.toString()
+    )
+  }
+
+  // We will pas the width and height to the app here, since getting measurements
+  // on the RN side is a bit more involved, and we already have them here anyway.
+  private fun buildUriData(info: Map<String, Any>): String {
+    val path = info.getValue("path")
+    val width = info.getValue("width")
+    val height = info.getValue("height")
+    return "file://${path}|${width}|${height}"
+  }
+}
diff --git a/modules/expo-receive-android-intents/expo-module.config.json b/modules/expo-receive-android-intents/expo-module.config.json
new file mode 100644
index 000000000..8f01fb6c9
--- /dev/null
+++ b/modules/expo-receive-android-intents/expo-module.config.json
@@ -0,0 +1,6 @@
+{
+  "platforms": ["android"],
+  "android": {
+    "modules": ["xyz.blueskyweb.app.exporeceiveandroidintents.ExpoReceiveAndroidIntentsModule"]
+  }
+}
diff --git a/modules/react-native-ui-text-view/ios/RNUITextView.swift b/modules/react-native-ui-text-view/ios/RNUITextView.swift
index 9c21d45b5..3fb55873d 100644
--- a/modules/react-native-ui-text-view/ios/RNUITextView.swift
+++ b/modules/react-native-ui-text-view/ios/RNUITextView.swift
@@ -108,14 +108,26 @@ class RNUITextView: UIView {
       fractionOfDistanceBetweenInsertionPoints: nil
     )
 
+    var lastUpperBound: String.Index? = 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)
-
+        
+        // We want to skip over the children we have already checked, otherwise we could run into
+        // collisions of similar strings (i.e. links that get shortened to the same hostname but
+        // different paths)
+        let range = fullText.range(of: childText, options: [], range: (lastUpperBound ?? String.Index(utf16Offset: 0, in: fullText) )..<fullText.endIndex)
+        
         if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
-          if charIndex >= lowerBound.utf16Offset(in: fullText) && charIndex <= upperBound.utf16Offset(in: fullText) {
+          let lowerOffset = lowerBound.utf16Offset(in: fullText)
+          let upperOffset = upperBound.utf16Offset(in: fullText)
+          
+          if charIndex >= lowerOffset,
+             charIndex <= upperOffset
+          {
             return child
+          } else {
+            lastUpperBound = upperBound
           }
         }
       }
diff --git a/modules/react-native-ui-text-view/ios/RNUITextViewManager.m b/modules/react-native-ui-text-view/ios/RNUITextViewManager.m
index 9a6f0285c..32dfb3b28 100644
--- a/modules/react-native-ui-text-view/ios/RNUITextViewManager.m
+++ b/modules/react-native-ui-text-view/ios/RNUITextViewManager.m
@@ -4,6 +4,7 @@
 RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger)
 RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL)
 
+RCT_EXPORT_VIEW_PROPERTY(numberOfLines, NSInteger)
 RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock)
 RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString)
 RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
diff --git a/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift b/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift
index 4f3eda43c..5a462f6b6 100644
--- a/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift
+++ b/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift
@@ -40,19 +40,19 @@ class RNUITextViewShadow: RCTShadowView {
     self.setAttributedText()
   }
 
-  // Tell yoga not to use flexbox
+  // Returning true here will tell Yoga to not use flexbox and instead use our custom measure func.
   override func isYogaLeafNode() -> Bool {
     return true
   }
 
-  // We only need to insert text children
+  // We should only insert children that are UITextView shadows
   override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) {
     if subview.isKind(of: RNUITextViewChildShadow.self) {
       super.insertReactSubview(subview, at: atIndex)
     }
   }
 
-  // Whenever the subvies update, set the text
+  // Every time the subviews change, we need to reformat and render the text.
   override func didUpdateReactSubviews() {
     self.setAttributedText()
   }
@@ -64,7 +64,7 @@ class RNUITextViewShadow: RCTShadowView {
       return
     }
 
-    // Update the text
+    // Since we are inside the shadow view here, we have to find the real view and update the text.
     self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in
       guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else {
         return
@@ -100,18 +100,25 @@ class RNUITextViewShadow: RCTShadowView {
       // Create the attributed string with the generic attributes
       let string = NSMutableAttributedString(string: child.text, attributes: attributes)
 
-      // Set the paragraph style attributes if necessary
+      // Set the paragraph style attributes if necessary. We can check this by seeing if the provided
+      // line height is not 0.0.
       let paragraphStyle = NSMutableParagraphStyle()
       if child.lineHeight != 0.0 {
-        paragraphStyle.minimumLineHeight = child.lineHeight
-        paragraphStyle.maximumLineHeight = child.lineHeight
+        // Whenever we change the line height for the text, we are also removing the DynamicType
+        // adjustment for line height. We need to get the multiplier and apply that to the
+        // line height.
+        let scaleMultiplier = scaledFontSize / child.fontSize
+        paragraphStyle.minimumLineHeight = child.lineHeight * scaleMultiplier
+        paragraphStyle.maximumLineHeight = child.lineHeight * scaleMultiplier
+
         string.addAttribute(
           NSAttributedString.Key.paragraphStyle,
           value: paragraphStyle,
           range: NSMakeRange(0, string.length)
         )
 
-        // Store that height
+        // To calcualte the size of the text without creating a new UILabel or UITextView, we have
+        // to store this line height for later.
         self.lineHeight = child.lineHeight
       } else {
         self.lineHeight = font.lineHeight
@@ -124,24 +131,22 @@ class RNUITextViewShadow: RCTShadowView {
     self.dirtyLayout()
   }
 
-  // Create a YGSize based on the max width
+  // To create the needed size we need to:
+  // 1. Get the max size that we can use for the view
+  // 2. Calculate the height of the text based on that max size
+  // 3. Determine how many lines the text is, and limit that number if it exceeds the max
+  // 4. Set the frame size and return the YGSize. YGSize requires Float values while CGSize needs CGFloat
   func getNeededSize(maxWidth: Float) -> YGSize {
-    // Create the max size and figure out the size of the entire text
     let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
     let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
 
-    // Figure out how many total lines there are
-    let totalLines = Int(ceil(textSize.height / self.lineHeight))
-
-    // Default to the text size
-    var neededSize: CGSize = textSize.size
+    var totalLines = Int(ceil(textSize.height / self.lineHeight))
 
-    // If the total lines > max number, return size with the max
     if self.numberOfLines != 0, totalLines > self.numberOfLines {
-      neededSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(self.numberOfLines) * self.lineHeight))
+      totalLines = self.numberOfLines
     }
 
-    self.frameSize = neededSize
-    return YGSize(width: Float(neededSize.width), height: Float(neededSize.height))
+    self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
+    return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
   }
 }