見出し画像

react-native-share の使い方

「react-native-share」の使い方をまとめました。


前回

1. react-native-share

react-native-share」は、React Nativeアプリにアプリ間共有を追加するためのライブラリです。

・react-native-share : アプリからデータを送信
・react-native-share-intent : 他のアプリからデータを受信

2. アプリからデータを送信

2-1. セットアップ

(1) React Nativeプロジェクトの生成。

npx react-native init my_app
cd my_app

(2) パッケージのインストール。

npm install react-native-share

2-2. Androidのセットアップ

特別なセットアップは必要ありません。

2-3. iOSのセットアップ

(1) podのインストール。

cd ios
pod install
cd ..

(2) Xcodeで生成されたxcworkspaceを開き、署名を行い、iOSにインストールできることを確認。

2-4. コードの編集と実行

(1) コードの編集と実行。

import React from 'react';
import { Button, View } from 'react-native';
import Share from 'react-native-share';

// アプリ
const App = () => {

  // 共有オプション
  const shareOptions = {
    title: 'タイトルです。',
    message: 'メッセージです。',
    url: 'https://example.com',
  };

  // アプリからデータを送信
  const onShare = async () => {
    try {
      const shareResponse = await Share.open(shareOptions);
      console.log('Share Response:', shareResponse);
    } catch (error) {
      console.log('Error =>', error);
    }
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Button title="Share" onPress={onShare} />
    </View>
  );
};

export default App;

サポートされている共有オプションについてはドキュメントを参照。

3. 他のアプリからデータを受信

3-1. セットアップ

(1) パッケージのインストール。

npm install react-native-receive-sharing-intent

3-2. Androidのセットアップ

(1) 「<Project_folder>/android/app/src/main/AndroidManifest.xml」に設定を追加。
今回は、「テキスト共有」の設定のみ追加しています。
他の設定についてはドキュメントを参照。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />

    <!--TODO この行を追加  -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"  
        android:windowSoftInputMode="adjustResize"
        android:exported="true"><!--TODO IMPORTANT: launchModeはsingleTask推奨 -->
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>

        <!--TODO: アプリ内でテキスト共有をサポートする場合は、このフィルタを追加-->
        <intent-filter>
            <action android:name="android.intent.action.SEND" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="text/*" />
        </intent-filter>
      </activity>
    </application>
</manifest>

(2) 「<Project_folder>/android/app/src/main/java/com/YOUR_APP/MainActivity.kt」に以下のコードを追加。

  override fun onNewIntent(intent: Intent?) {
      super.onNewIntent(intent)
      setIntent(intent)
  }

詳しくは、以下のドキュメントを参照。

3-3. iOSのセットアップ

(1) podのインストール。

cd ios
pod install
cd ..

(2) Xcodeで生成されたxcworkspaceを開き、署名を行い、iOSにインストールできることを確認。

(3) 「<project_folder>/ios/<project_name>/info.plist」に以下の設定を追加追加。

<plist version="1.0">
<dict>
  
  :

  <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeRole</key>
      <string>Editor</string>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>ShareMedia</string> <!-- 共有URLプロトコル (アプリごとに一意である必要があり、バンドルID推奨) -->
      </array>
    </dict>
  </array>

  <key>NSPhotoLibraryUsageDescription</key>
  <string>To upload photos, please allow permission to access your photo library.</string>
  
  :
  
</dict>
</plist>  

(4) 「AppDelegate.mm」に以下のコードを追加。

  :

#import <React/RCTLinkingManager.h> // ファイルのヘッダーにこの行を追加

  :
@implementation AppDelegate

  :

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

@end

(5) 「<project_folder>/ios/<your project name>/<your project name>.entitlements」に以下の設定を追加。
今回は、必要ありません。

  :
    <!--TODO:  URLクリックでのアプリ起動をサポートしたい場合は、このタグを追加-->
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:example.com</string>
    </array>
  :

(6) 「Share Extention」を追加。

(7) 「<project_folder>/ios/<Your Share Extension Name>/info.plist」に以下の項目を設定。
今回は「テキスト共有」と「URL共有」のみ追加しています。
他の設定についてはドキュメントを参照。

<?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>NSExtensionAttributes</key>
        <dict>
            <key>PHSupportedMediaTypes</key>
               <array>
               </array>
            <key>NSExtensionActivationRule</key>
            <dict>
              <!--TODO: アプリ内でテキスト共有をサポートする場合は、このフラグを追加-->
              <key>NSExtensionActivationSupportsText</key>
              <true/>
              <!--TODO: アプリ内でのURL共有をサポートする場合は、このタグを追加-->
              <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
              <integer>1</integer>
            </dict>
        </dict>
    <key>NSExtensionMainStoryboard</key>
    <string>MainInterface</string>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.share-services</string>
  </dict>
</dict>
</plist>

(8) 「<project_folder>/ios/<Your Share Extension Name>/ShareViewController.swift」を次のように編集。
hostAppBundleIdentifier」は、自分の環境のホストアプリのバンドルIDを指定します。それ以外は、公式サイトのコードのままになります。


import UIKit
import Social
import MobileCoreServices
import Photos

class ShareViewController: SLComposeServiceViewController {
 // TODO: IMPORTANT: ホストアプリのバンドルIDを指定してください
 let hostAppBundleIdentifier = "com.rnreceivesharingintent"

 let shareProtocol = "ShareMedia" //共有URLプロトコル (アプリごとに一意である必要があり、バンドルID(hostAppBundleIdentifier)推奨)
 let sharedKey = "ShareKey"
 var sharedMedia: [SharedMediaFile] = []
 var sharedText: [String] = []
 let imageContentType = kUTTypeImage as String
 let videoContentType = kUTTypeMovie as String
 let textContentType = kUTTypeText as String
 let urlContentType = kUTTypeURL as String
 let fileURLType = kUTTypeFileURL as String;
 
 override func isContentValid() -> Bool {
   return true
 }
 
 override func viewDidLoad() {
       super.viewDidLoad();
   }

 override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)

   if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
     if let contents = content.attachments {
       for (index, attachment) in (contents).enumerated() {
         if attachment.hasItemConformingToTypeIdentifier(imageContentType) {
           handleImages(content: content, attachment: attachment, index: index)
         } else if attachment.hasItemConformingToTypeIdentifier(textContentType) {
           handleText(content: content, attachment: attachment, index: index)
         } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) {
           handleFiles(content: content, attachment: attachment, index: index)
         } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
           handleUrl(content: content, attachment: attachment, index: index)
         } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) {
           handleVideos(content: content, attachment: attachment, index: index)
         }
       }
     }
   }
 }
 
 override func didSelectPost() {
       print("didSelectPost");
   }

 override func configurationItems() -> [Any]! {
   // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
   return []
 }
 
 private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
   attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in
     
     if error == nil, let item = data as? String, let this = self {
       
       this.sharedText.append(item)
       
       // If this is the last item, save imagesData in userDefaults and redirect to host app
       if index == (content.attachments?.count)! - 1 {
         let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
         userDefaults?.set(this.sharedText, forKey: this.sharedKey)
         userDefaults?.synchronize()
         this.redirectToHostApp(type: .text)
       }
       
     } else {
       self?.dismissWithError()
     }
   }
 }
 
 private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
   attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in
     
     if error == nil, let item = data as? URL, let this = self {
       
       this.sharedText.append(item.absoluteString)
       
       // If this is the last item, save imagesData in userDefaults and redirect to host app
       if index == (content.attachments?.count)! - 1 {
         let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
         userDefaults?.set(this.sharedText, forKey: this.sharedKey)
         userDefaults?.synchronize()
         this.redirectToHostApp(type: .text)
       }
       
     } else {
       self?.dismissWithError()
     }
   }
 }
 
 private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
   attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in
     
     if error == nil, let url = data as? URL, let this = self {
       //  this.redirectToHostApp(type: .media)
       // Always copy
       let fileExtension = this.getExtension(from: url, type: .video)
       let newName = UUID().uuidString
       let newPath = FileManager.default
         .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
         .appendingPathComponent("\(newName).\(fileExtension)")
       let copied = this.copyFile(at: url, to: newPath)
       if(copied) {
         this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image))
       }
       
       // If this is the last item, save imagesData in userDefaults and redirect to host app
       if index == (content.attachments?.count)! - 1 {
         let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
         userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
         userDefaults?.synchronize()
         this.redirectToHostApp(type: .media)
       }
       
     } else {
       self?.dismissWithError()
     }
   }
 }
 
 private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
   attachment.loadItem(forTypeIdentifier: videoContentType, options:nil) { [weak self] data, error in
     
     if error == nil, let url = data as? URL, let this = self {
       
       // Always copy
       let fileExtension = this.getExtension(from: url, type: .video)
       let newName = UUID().uuidString
       let newPath = FileManager.default
         .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
         .appendingPathComponent("\(newName).\(fileExtension)")
       let copied = this.copyFile(at: url, to: newPath)
       if(copied) {
         guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else {
           return
         }
         this.sharedMedia.append(sharedFile)
       }

       // If this is the last item, save imagesData in userDefaults and redirect to host app
       if index == (content.attachments?.count)! - 1 {
         let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
         userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
         userDefaults?.synchronize()
         this.redirectToHostApp(type: .media)
       }
       
     } else {
       self?.dismissWithError()
     }
   }
 }
 
 private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
   attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in
     
     if error == nil, let url = data as? URL, let this = self {
       
       // Always copy
       let newName = this.getFileName(from :url)
       let newPath = FileManager.default
         .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")!
         .appendingPathComponent("\(newName)")
       let copied = this.copyFile(at: url, to: newPath)
       if (copied) {
         this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file))
       }
       
       if index == (content.attachments?.count)! - 1 {
         let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
         userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
         userDefaults?.synchronize()
         this.redirectToHostApp(type: .file)
       }
       
     } else {
       self?.dismissWithError()
     }
   }
 }
 
 private func dismissWithError() {
   print("[ERROR] Error loading data!")
   let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
   
   let action = UIAlertAction(title: "Error", style: .cancel) { _ in
     self.dismiss(animated: true, completion: nil)
   }
   
   alert.addAction(action)
   present(alert, animated: true, completion: nil)
   extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
 }
 
 private func redirectToHostApp(type: RedirectType) {
   let url = URL(string: "\(shareProtocol)://dataUrl=\(sharedKey)#\(type)")
   var responder = self as UIResponder?
   let selectorOpenURL = sel_registerName("openURL:")
   
   while (responder != nil) {
     if (responder?.responds(to: selectorOpenURL))! {
       let _ = responder?.perform(selectorOpenURL, with: url)
     }
     responder = responder!.next
   }
   extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
 }
 
 enum RedirectType {
   case media
   case text
   case file
 }
 
 func getExtension(from url: URL, type: SharedMediaType) -> String {
   let parts = url.lastPathComponent.components(separatedBy: ".")
   var ex: String? = nil
   if (parts.count > 1) {
     ex = parts.last
   }
   
   if (ex == nil) {
     switch type {
     case .image:
       ex = "PNG"
     case .video:
       ex = "MP4"
     case .file:
       ex = "TXT"
     }
   }
   return ex ?? "Unknown"
 }
 
 func getFileName(from url: URL) -> String {
   var name = url.lastPathComponent
   
   if (name == "") {
     name = UUID().uuidString + "." + getExtension(from: url, type: .file)
   }
   
   return name
 }
 
 func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
   do {
     if FileManager.default.fileExists(atPath: dstURL.path) {
       try FileManager.default.removeItem(at: dstURL)
     }
     try FileManager.default.copyItem(at: srcURL, to: dstURL)
   } catch (let error) {
     print("Cannot copy item at \(srcURL) to \(dstURL): \(error)")
     return false
   }
   return true
 }
 
 private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? {
   let asset = AVAsset(url: forVideo)
   let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
   let thumbnailPath = getThumbnailPath(for: forVideo)
   
   if FileManager.default.fileExists(atPath: thumbnailPath.path) {
     return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video)
   }
   
   var saved = false
   let assetImgGenerate = AVAssetImageGenerator(asset: asset)
   assetImgGenerate.appliesPreferredTrackTransform = true
   //        let scale = UIScreen.main.scale
   assetImgGenerate.maximumSize =  CGSize(width: 360, height: 360)
   do {
     let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil)
     try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath)
     saved = true
   } catch {
     saved = false
   }
   
   return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil
   
 }
 
 private func getThumbnailPath(for url: URL) -> URL {
   let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
   let path = FileManager.default
     .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
     .appendingPathComponent("\(fileName).jpg")
   return path
 }
 
 class SharedMediaFile: Codable {
   var path: String; // can be image, video or url path. It can also be text content
   var thumbnail: String?; // video thumbnail
   var duration: Double?; // video duration in milliseconds
   var type: SharedMediaType;
   
   
   init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) {
     self.path = path
     self.thumbnail = thumbnail
     self.duration = duration
     self.type = type
   }
   
   // Debug method to print out SharedMediaFile details in the console
   func toString() {
     print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)")
   }
 }
 
 enum SharedMediaType: Int, Codable {
   case image
   case video
   case file
 }
 
 func toData(data: [SharedMediaFile]) -> Data {
   let encodedData = try? JSONEncoder().encode(data)
   return encodedData!
 }
}

extension Array {
 subscript (safe index: UInt) -> Element? {
   return Int(index) < count ? self[Int(index)] : nil
 }
}

(9) 「ホストアプリ」と「Share Extention」で同一の「App Group」を指定。
「App Group」のIDは、「group.ホストアプリのバンドルID」としてください。

詳しくは、以下のドキュメントを参照。

3-4. コードの編集と実行

(1) コードの編集と実行。

import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import ReceiveSharingIntent from 'react-native-receive-sharing-intent';

// アプリ
const App = () => {

  useEffect(() => {
    // 他のアプリからデータを受信
    ReceiveSharingIntent.getReceivedFiles(
      (files) => {
        // 受信したファイルデータを処理
        console.log(files);
      },
      (error) => {
        console.log(error);
      },
      'ShareMedia' // iOS用のグループID
    );

    return () => {
      ReceiveSharingIntent.clearReceivedFiles();
    };
  }, []);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Waiting for shared data...</Text>
    </View>
  );
};

export default App;

(2) 他のアプリでデータを共有。
ログに受信したデータが出力されます。

次回



この記事が気に入ったらサポートをしてみませんか?