シェアシートで画像とテキストを送る時にハマったこと



# 概要
:::note info
**対象読者：**
- シェアシートの基本的な実装がわからない
- 適切なプレビューを表示できない
- 画像とテキストを同時に送信できないアプリがある
    - 特に Meta 系のアプリで上手くいかない
:::

:::note info
**実装方法：**
- UIActivityViewController を使ったシェアシートの実装
- UIActivityItemSource と LPLinkMetadata を使ったプレビューの実装
- UIActivityItemSource のデリゲートメソッドを使ったアプリごとの挙動分岐
    - Meta 系アプリで画像とテキストを同時送信できない件の今後
:::


# UIActivityViewController を使ったシェアシートの実装

```swift: サンプルコード
func shareSheet(shareImage: UIImage, shareText: String) {

  let controller = UIActivityViewController(
    activityItems: [shareImage, shareText], 
    applicationActivities: nil
  )
  
  viewController.present(controller, animated: true, completion: nil)
}
```
<img width="300" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/9e761b1b-7b6c-3845-faea-7638f70b7dba.jpeg">


```swift: UIActivityViewController
  init(activityItems: [Any], applicationActivities: [UIActivity]?)
```
**- `activityItems: [Any]`**
シェアしたいオブジェクトの配列を指定。
サンプルコードでは `shareImage: UIImage, shareText: String` で画像とテキストを渡した。

**- `applicationActivities: [UIActivity]?`**
シェアアクティビティの配列を指定。サンプルコードでは `nil` とした。
今回は扱わないが、```UIActivity``` をオーバーライドして画像のようなシェアアクティビティを自分で定義することができる。  

<img width="300" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/479e536a-ec31-ee24-02ef-ed4f94c1ab55.png">


（cf. `UIActivity` を自分で定義する時に参考になる記事 ⤵︎）

https://www.hackingwithswift.com/articles/118/uiactivityviewcontroller-by-example

# UIActivityItemSource と LPLinkMetadata を使ったプレビューの実装

プレビューとは以下の枠線部分のことを指す。 
普通に画像とテキストだけを渡すと、シェアされるオブジェクトの優先順位の影響でテキストだけがプレビューされてしまう。
ここにメタデータも一緒に渡すことで、適切なプレビューを表示するように実装する。

#### シェアされる要素が欠落しているプレビュー
<img width="300" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/474cd2ba-d4c4-a195-39a2-410ec8b4faf5.jpeg">

（cf. シェアされるオブジェクトの優先順位について参考になる記事 ⤵︎）

https://qiita.com/ezura/items/6036c6e100599b601482

```swift: サンプルコード
func shareSheet(shareImage: UIImage, shareText: String, shareURL: URL) {

  let sharePreview = SharePreviewItemSource(image: shareImage, title: shareText, url: shareURL)
  
  let controller = UIActivityViewController(
    activityItems: [sharePreview, shareImage, shareText], 
    applicationActivities: nil
  )

  viewController.present(controller, animated: true, completion: nil)
}


class SharePreviewItemSource: NSObject, UIActivityItemSource {

  private let linkMetadata = LPLinkMetadata()

  init(image: UIImage, title: String, url: URL) {
    super.init()
    linkMetadata.imageProvider = NSItemProvider(object: image)
    linkMetadata.title = title
    linkMetadata.url = url
  }

  func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
    linkMetadata
  }
  
  // for protocol compliance
  func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
    []
  }
  // for protocol compliance
  func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
    nil
  }
}
```

### **UIActivityItemSource :** 
```swift: protocol UIActivityItemSource　- activityViewControllerLinkMetadata
activityViewControllerLinkMetadata(UIActivityViewController) -> LPLinkMetadata?
```
シェアするオブジェクトを作成できるプロトコル。サンプルコードでは、メタデータを取り出すデリゲートメソッドに、作成した `linkMetadata: LPLinkMetadata` を渡している。

### **LPLinkMetadata:**
```swift: class LPLinkMetadata
var imageProvider: NSItemProvider?,
var title: String, 
var url: URL
```
リンクが持つタイトルや画像などのメタデータを作成できるクラス。サンプルコードでは、`imageProvider, title, url` でシェアしたい画像、タイトル、URL を埋め込んだ。

#### シェアされる要素を全て備えたプレビュー

<img width="300" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/403b8140-05c9-15a1-63b5-8cd409a2272f.jpeg">


# UIActivityItemSource のデリゲートメソッドを使ったアプリごとの挙動分岐
上記の実装で概ねどのアプリも画像とテキストをシェアできるが、Meta 系のアプリでは画像とテキストの片方のみしかシェアできないことが分かった。
アプリごとにシェアするオブジェクトを分岐することで、どちらも正常に送れるアプリの挙動は変更せずに、Meta 系アプリでは暫定的に画像だけをシェアするように実装する。

#### アプリごとの挙動分岐前
<img width="200" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/265cfd23-8c9d-e509-aebc-08796e6bcdb6.jpeg">
<img width="200" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/1744774e-d443-fb4a-ba48-17c7462cab5e.jpeg">
<img width="200" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/20529b56-3185-2ef2-a1ae-250422828281.jpeg">


```swift: サンプルコード
func shareSheet(shareImage: UIImage, shareText: String, shareURL: URL) {

  let sharePreview = SharePreviewItemSource(image: shareImage, title: shareText, url: shareURL)
  let _shareText = ShareTextItemSource(shareText: shareText)
  
  let controller = UIActivityViewController(
    activityItems: [sharePreview, shareImage, _shareText], 
    applicationActivities: nil
  )

  viewController.present(controller, animated: true, completion: nil)
}

class ShareTextItemSource: NSObject, UIActivityItemSource {

  private let shareText: String

  init(shareText: String) {
    self.shareText = shareText
    super.init()
  }

  func activityViewController(
    _ activityViewController: UIActivityViewController,
    itemForActivityType activityType: UIActivity.ActivityType?
  ) -> Any? {

    guard let activityType = activityType else {
      return nil
    }

    switch activityType {
    /// Set nil. To ensure that only image is shared on SNSs 
    /// where images and text cannot be sent together.
    case .postToFacebook, .postToInstagram, .postToThreads:
      return nil

    default:
      return shareText
    }
  }
  
  // for protocol compliance
  func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
    []
  }
}

extension UIActivity.ActivityType {
  static let postToInstagram: UIActivity.ActivityType = .init("com.burbn.instagram.shareextension")
  static let postToThreads: UIActivity.ActivityType = .init("com.burbn.barcelona.ShareExtension")
}

```


### **UIActivityItemSource:** 
```swift: protocol UIActivityItemSource - activityViewController
activityViewController(
  _ activityViewController: UIActivityViewController,
  itemForActivityType activityType: UIActivity.ActivityType?
) -> Any?
```

選択されたシェアアクティビティに応じた操作を行うことができるデリゲートメソッド。サンプルコードでは、Meta 系のアプリの場合は `nil` を返すことでテキストを空にしている。

### **UIActivity.ActivityType:** 
```swift: UIActivity.ActivityType
init(_ rawValue: String)
```

シェアアクティビティを格納している型。プロパティは任意に追加することができ、サンプルコードでは、システム定義にない Instagram と Threads を加えた。

#### アプリごとの挙動分岐後
<img width="200" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/b6b422cd-1b3f-d5a9-d468-cbc2a8f098e1.jpeg">
<img width="200" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/1744774e-d443-fb4a-ba48-17c7462cab5e.jpeg">
<img width="200" alt="スクリーンショット 2024-04-03 12 32 09" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/30c99888-4bf9-97cb-fb44-f3ea59f6d82c.jpeg">


# Meta 系アプリで画像とテキストを同時送信できない件の今後

- 現在 Facebook SDK にて 画像 + ハッシュタグの組み合わせは可能
- Threads API が 2024年6月に発表らしいので、何か方針転換があるかもしれない

### 現在 Facebook SDK にて 画像 + ハッシュタグの組み合わせは可能
ネットサーフィンしていて「Facebook はテキスト送信を認めていない」という噂がちらほらあったので調べてみると実際に公式の Developer Policy に記載があった。

https://developers.facebook.com/docs/development/terms-and-policies/prefill-policy-deep-dive/

>キャプション、コメント、メッセージ、または投稿のユーザーメッセージパラメータにコンテンツをプリフィルしないでください。
ただし、共有ダイアログを通じて共有された投稿のハッシュタグ1つである場合（ただし、APIを経由しない場合）、アプリを使用しているユーザーによって作成された場合、またはアプリの従業員がFacebook上の企業の存在を管理するためにアプリを使用している企業によって作成された場合は除きます。

「ハッシュタグ1つである場合」など気になる文言があったので、実際に Facebook SDK を使って実装してみた結果を簡略に記載する。

公式の開発者向けドキュメントによると、Facebook SDK では 
- Link, Image, Movie, Image + Movie の単位でのみシェアが可能
- どのシェア単位からも contentURL や hashtag といったプロパティが生えているので、Image + URL + hashtag という単位で一見それらしい実装ができそう
- 実際には Image + hashtag , URL + hashtag という組み合わせのみ可能（Image と URL は共存できない）

https://developers.facebook.com/docs/sharing/ios

**結論：
Facebook では Developer Policy にて投稿全般にテキストを仕込むことが禁止されていて、画像とテキストを同時に送りたい場合は「画像 + ハッシュタグ」の組み合わせのみ実装できる。**

僕のプロジェクトでは X でシェアできればよいということで、やむなく Meta 系アプリでは画像のみ仕込むという仕様を許容した。

### Threads API が 2024年6月に発表らしいので、何か方針転換があるかもしれない

同じ Meta 系のアプリでは Threads が API の一般提供を開始する発表があった。

https://techcrunch.com/2024/03/01/threads-says-it-will-make-its-api-broadly-available-by-june/

https://developers.facebook.com/docs/threads/posts#single-threads-posts

この公式ドキュメントを見ると画像とテキストの両方をパラメータに指定できるように見える。
```　git:シングルスレッドの投稿の POST（公式ドキュメントより）
curl - i - X POST \
 "https://graph.instagram.com/ v19.0/<IG_USER_ID>/threads?media_type=IMAGE&image_url=https://www.example.com/images/bronz-fonz.jpg&text=#BronzFonz&access_token= <ACCESS_TOKEN>"
```

こちらは現在すぐには対応できないが、対応可能になったら試したい。