待ちに待ったReactNativeで部分選択コピーとURLクリックを同時に満たす

## はじめに
こちらの記事を元に完成するとiOSでも部分選択とURLクリックの両方を満たすことが可能です。

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630810/4fe3239c-dcd8-db7b-93a5-43f275d48ed7.gif" />

<details>
<summary>完成コードはこちら</summary>

```
yarn add react-native-uitextview
yarn add linkifyjs
```

```javascript
import React from 'react';
import { UITextView } from 'react-native-uitextview';
import { tokenize } from 'linkifyjs';
import { Platform, Text, TextStyle } from 'react-native';
import Hyperlink from 'react-native-hyperlink';

type SelectableTextWithLinkProps = {
  text: string;
  style?: TextStyle;
  linkStyle?: TextStyle;
  onPressLink?: (url: string) => void;
};

/**
 * 文字列内のURLをクリック可能かつ部分選択可能なコンポーネント
 */
export const SelectableTextWithLink = ({ text, style, linkStyle, onPressLink }: SelectableTextWithLinkProps) => {
  const textWithUrl = convertTextWithUrl(text);
  if (Platform.OS == 'android') {
    return (
      <Hyperlink linkStyle={linkStyle} onPress={(url) => onPressLink?.(url)}>
        <Text selectable>{text}</Text>
      </Hyperlink>
    );
  }
  return (
    <UITextView selectable uiTextView>
      {textWithUrl.map(({ text, isUrl }, index) => {
        return (
          <UITextView
            key={`selectable-text-with-link-${index}`}
            onPress={() => isUrl && onPressLink?.(text)}
            style={[style, isUrl && linkStyle]}
          >
            {text}
          </UITextView>
        );
      })}
    </UITextView>
  );
};

type TextWithUrl = {
  text: string;
  isUrl: boolean;
};

/**
 * 文字列をURLと非URLとに分割する処理
 */
const convertTextWithUrl = (text: string): TextWithUrl[] => {
  const result: TextWithUrl[] = [];
  const tokens = tokenize(text);

  for (const token of tokens) {
    result.push({ text: token.toString(), isUrl: token.isLink });
  }

  return result;
};

```
</details>

## これまで

モバイルアプリをメインに開発をしているとよく出てくる要件に、
「ネイティブ機能を使った部分選択コピーと同時に、URLやメンションなども柔軟に扱いたい」
といったケースが多々あります。

これまでReactNativeでは公式のTextコンポーネントでは要件を満たせませんでした。

なので以下どちらかを採用、またはLINEのようにモーダル表示したときのみ部分コピーを可能に実装することがほとんどでした。

重要度が高い場合は工数を確保しネイティブでの実装を行うこともありますがほとんど優先度が低く後回しにされている印象です。

```javascript
import React from 'react';
import { Text, TextInput } from 'react-native';
import Hyperlink from 'react-native-hyperlink';

type Props = {
  text: string;
};

/** 文字列内のURLをクリックできるように */
export const TextWithLink = ({ text }: Props) => {
  return (
    <Hyperlink linkStyle={{ color: 'blue' }} onPress={(url) => console.log('on press link', url)}>
      <Text>{text}</Text>
    </Hyperlink>
  );
};

/** 文字列を部分選択可能に */
export const SelectableText = ({ text }: Props) => {
  return <TextInput value={text} editable={false} multiline={true} />;
};
```

## UITextViewベースのコンポーネントが登場


[react-native-uitextview](https://github.com/bluesky-social/react-native-uitextview)というBlueskyのクライアントアプリのため開発されたコンポーネントが2024年3月に誕生していました。割と衝撃。というかなぜいままでなかったのか…（見過ごしていただけかもしれませんが😭）

使い方はシンプルで標準のTextとほとんど同じように利用が可能です。
Androidの場合は標準のTextで要件を満たせるためライブラリのメンテナンス依存度を下げるためにも別途実装するのが無難かもしれません。

以下が実装例です。

```javascript
import React from 'react';
import { UITextView } from 'react-native-uitextview';
import { Platform, Text, TextStyle } from 'react-native';
import Hyperlink from 'react-native-hyperlink';

type SelectableTextWithLinkProps = {
  text: string;
  style?: TextStyle;
  linkStyle?: TextStyle;
  onPressLink?: (url: string) => void;
};

/** 文字列内のURLをクリック可能にかつ部分選択可能なText */
export const SelectableTextWithLink = ({ text, style, linkStyle, onPressLink }: SelectableTextWithLinkProps) => {
  const textWithUrl = convertTextWithUrl(text);
  if (Platform.OS == 'android') {
    return (
      <Hyperlink linkStyle={linkStyle} onPress={(url) => onPressLink?.(url)}>
        <Text selectable>{text}</Text>
      </Hyperlink>
    );
  }
  return (
    <UITextView selectable uiTextView>
      {textWithUrl.map(({ text, isUrl }, index) => {
        return (
          <UITextView
            key={`selectable-text-with-link-${index}`}
            onPress={() => isUrl && onPressLink?.(text)}
            style={[style, isUrl && linkStyle]}
          >
            {text}
          </UITextView>
        );
      })}
    </UITextView>
  );
};
```

上記処理で利用する**URL分割**も実装してみたので試してみてください🙏

```javascript
import { tokenize } from 'linkifyjs';

type TextWithUrl = {
  text: string;
  isUrl: boolean;
};

/** 文字列をURLと非URLとに分割する処理 */
const convertTextWithUrl = (text: string): TextWithUrl[] => {
  const result: TextWithUrl[] = [];
  const tokens = tokenize(text);

  for (const token of tokens) {
    result.push({ text: token.toString(), isUrl: token.isLink });
  }

  return result;
};
```

## さいごに

React Nativeを触り出してもう7年くらいになりますが普及率とExpoの進化が最近特に勢いを増しており、毎年ワクワクしています。

BlueskyがReactNativeで開発されていることは有名ですがそれ以外([Showcase参考](https://reactnative.dev/showcase))でも多方面で採用されています。国内での事例も少しずつ増えてきていますが特に海外での需要が伸びている印象ですね。

最近はEU対応やiOSのPrivacyManifest対応、GooglePlayConsoleでの審査が厳しくなったりと慌ただしいですが、少しずつ確実にキャッチアップしていきたいですね。

それでは良きReact Nativeライフを🖐️

(追記)
UITextViewを利用時にfontFamilyが反映されない問題があったので修正しておきました。
https://github.com/bluesky-social/react-native-uitextview/pull/7