[iOS] シェアシート上のプレビューをあるべき姿に

![ezgif.com-gif-maker4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/78751c55-9733-a6a2-6a5e-2424b4bbfb75.gif)

iOS13 からシェアシートが変わりましたね。変化の一つとして、シェアするアイテムのプレビューが追加されています。素晴らしい機能です。
その一方で、アプリ開発者にとっては新たな刺客ともいえるでしょう。

このプレビューは開発者が望む望まないに関わらず表示されます。しかも、何も対策をしないとユーザにとって違和感のある表示になってしまうことがあります。

この記事では、UIActivityViewController を用いたシェア画面上のプレビューを操作・カスタマイズする方法を紹介します[^1]。プレビューは一見奇妙な動きをしますし実装に関する情報も少ないのですが、ここで紹介する 2 つのポイントを把握しておけば簡単に扱えるようになります。

[^1]: この記事は[potatotips #66 (iOS/Android開発Tips共有会)](https://potatotips.connpass.com/event/149806/)で発表した[Mastering share sheet preview](https://speakerdeck.com/ezura/mastering-share-sheet-preview)をベースに加筆・修正したものです。

## プレビューの動作
まず、基本的な挙動を確認してみましょう。
![poteto20191111_qiita_min.014.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/555bb82c-0b26-5350-6236-c191ce1da2ff.jpeg)
web 上の URL をシェアする場合やファイルパスを指定してデータをシェアする場合などは、特に対応しなくてもシェアするアイテムの種類に沿って適切に表示されています。
次に、修正が必要な場合を見てみましょう。
![poteto20191111_qiita_min.015.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/eecf77b5-adb1-03dc-2bbc-66397237fadf.jpeg)
何をシェアするのかプレビューを見ても分かりにくいですね。
よくあるパターンとして「URL + String(シェア文章やハッシュタグ)」をシェアする場合がありますが、何もしなければ T アイコンとテキストが表示されます。やや奇妙に感じます。

## プレビューの表示ロジックとカスタマイズ方法
では、コードに目を向けてみましょう。
UIActivityViewController の基本的な使い方は下記の 2 ステップです。

```swift
// step1: `UIActivityViewController` にシェアするアイテム(`String`, `URL(web, local file path)`, `Data`,  `UIActivityItemSource` 等)を渡す
let activityViewController = UIActivityViewController( activityItems: [シェアするアイテム],
                                                      applicationActivities: nil)
// step2: 通常の `UIViewController` のように表示する
parentViewController.present(activityVC,  animated: true,  completion: nil)
```

このシンプルな流れの中でどのようにプレビューの制御に介入すれば良いのでしょうか。

## ポイント1: プレビューされるアイテムの優先度
まず、複数のアイテムをシェアする場合、どのようにプレビューされるアイテムが決まるのか考えてみます。
それは、シェアするアイテムの種類に依存して決まります。
![poteto20191111_qiita_min.020.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/dba9fe6b-a1f4-717e-a1a3-557d58d38eca.jpeg)
URL と String をシェアする際に String を対象としたプレビューが表示されるのは String の方が URL よりも優先度が高いためです。
![poteto20191111_qiita_min.021.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/b93ca73f-b5ce-c350-76dd-50473ee2d2ae.jpeg)

では、URL をプレビューの対象にしたい場合はどうすれば良いでしょうか。
![poteto20191111_qiita_min.022.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/cae275a9-01c3-4b38-b2bf-8af0b8ae0232.jpeg)
上述の優先度の理論に従って、最も優先度の高い UIActivityItemSource 経由で URL を渡せば URL がプレビューのターゲットになります。URL と String のパターンに限らず、プレビューの対象にしたいアイテムは基本的に UIActivityItemSource から返すようにすれば良さそうです。

```swift:UIActivityItemSourceに準拠した型を定義
class ShareActivityItemSource: NSObject, UIActivityItemSource {
    private let url: URL

    init(_ url: URL) {
        self.url = url
        super.init()
    }
    
    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
        url
    }
    
    func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
         url
    }
}
```
```swift:UIActivityViewControllerに渡す
let activityViewController = UIActivityViewController(activityItems: ["Text", ShareActivityItemSource(url)],
                                                      applicationActivities: nil)
```

もし、UIActivityItemSource が複数あるなど、「アイテムの種類による優先度でもプレビューする対象が 1 つに決まらない場合」または「LPLinkMetadata を返す UIActivityItemSource がある場合」は下記のルールに従います。

* LPLinkMetadata を返す UIActivityItemSource がある場合、LPLinkMetadata の情報が採用される (このパターンについては後述します)
* 優先度の最も高いアイテムが複数ある場合、UIActivityViewController に与えた activityItems に渡した配列の中で順番が先のアイテムが採用される

## ポイント2: プレビューのタイトル、アイコン等を個別にカスタマイズできる
下記のようにポイント 1 で解決できない問題の解決やよりリッチな表示をしたい場合、プレビューのタイトル、アイコンを自由にカスタマイズしたくなると思います。

* UIImage をシェアする場合、デフォルトではアイコン部分にアプリアイコンが表示されるが、シェアする UIImage をアイコン部分に表示させたい
* 先にシェアシートを表示しておいて、プレビューに表示する情報は非同期に取得して表示したい

プレビューの内容を直接カスタマイズしたい場合 LPLinkMetadata を使用します。 
LPLinkMetadata とは、URL のメタデータを格納する型です。WWDC2019  の [Embedding and Sharing Visually Rich Links](https://developer.apple.com/videos/play/wwdc2019/262/)[^2]でも紹介されていました。
定義を見ると、プレビューに関係しそうなプロパティが直感的にわかると思います。

[^2]: [Embedding and Sharing Visually Rich Links](https://developer.apple.com/videos/play/wwdc2019/262/) のセッションの後半部分で Share sheet の話題になります。その内容を端的にまとめると「LPLinkMetadata を取得済みの場合はそれをそのままプレビューに表示すれば URL のメタデータを何度も取ってこなくて済むし、すぐにプレビューに情報を表示できる」「足りない情報があっても、LPLinkMetadata を先に Share sheet に渡しておいて後で更新すれば良い」という旨の話がされています。

![poteto20191111_qiita_min.024.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/e2c5437a-21b9-4588-99bf-7c99e6c5254d.jpeg)

LPLinkMetadata のインスタンスを UIActivityViewController に渡すためには UIActivityItemSource が必要です。
iOS13 から UIActivityItemSource に LPLinkMetadata? を返す function が増えています。
この function で LPLinkMetadata を返すと LPLinkMetadata の情報がプレビューに使われます。

```swift
public protocol UIActivityItemSource : NSObjectProtocol {
    // 中略

    @available(iOS 13.0, *)
    optional func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? // called to fetch LinkPresentation metadata for the activity item. iOS 13.0
}
```

例えば、LPLinkMetadata にタイトルとアイコンの情報を設定して返り値として返せば、それがプレビューに反映されます。(iconProvider が設定されていなければ imageProvider が使われます)
![poteto20191111_qiita_min.026.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31285/bc7a5242-8a62-0a12-a50c-e62297cc892a.jpeg)

これでプレビューをカスタマイズできそうですね。
LPLinkMetadata はシェア先でも使用される場合があるので、それを念頭に置いて使用しましょう。
そしてもう一つ、LPLinkMetadata とプレビューの間には重要な性質があります。

### プレビューの情報を UIActivityViewController の表示後に更新する
例えば、プレビューに表示する情報を web 上から取得したい場合、情報の取得を待ってから LPLinkMetadata を作ってシェアシートを表示するのではその分ユーザを待たせてしまいます。プレビューは補助情報なので、そのためにユーザの時間を奪うのは避けたいです。
そこで、下記の順で処理されるように実装します。

1. UIActivityViewController を表示する (プレビューには仮の情報を表示しておく)
2. プレビューに表示する情報が取得でき次第プレビューを更新する

方法は簡単で、UIActivityItemSource から返す LPLinkMetadata のインスタンスの property を書き換えるだけです。

```swift:実装例
class ShareActivityLazyLoadItemSource: NSObject, UIActivityItemSource {
    private let linkMetadata: LPLinkMetadata

    init(_ url: URL) {
        linkMetadata = LPLinkMetadata()
        super.init()
        // 完全な情報が取得できるまでプレビューに表示しておく仮の情報を入れておく
        linkMetadata.title = "placeholder title"
        linkMetadata.originalURL = url
        linkMetadata.iconProvider = NSItemProvider(contentsOf: placeholderImageURL)
        // URL から metadata を取得する (非同期処理)
        metadataProvider.startFetchingMetadata(for: originalURL) { [linkMetadata] metadata, error in
            // 非同期で実行されるブロック
            // `linkMetadata` に足りなかった情報を入れてプレビューを完成させる
            linkMetadata.title = metadata?.title
            linkMetadata.url = metadata?.url
            linkMetadata.originalURL = metadata?.originalURL
            linkMetadata.iconProvider = metadata?.iconProvider
            linkMetadata.imageProvider = metadata?.imageProvider
        }
    }
    
    func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
        return linkMetadata
    }
```

このように、プレビュー上の情報は LPLinkMetadata を通して何度でも書き換えることができます。

## まとめ
* ポイント 1: Preview されるアイテムの優先度を利用する
  * UIActivityItemSource < Text < URL < Data

* ポイント 2: LPLinkMetadata でプレビューに表示する情報を柔軟にカスタマイズできる
  * UIActivityItemSource の function から LPLinkMetadata を返す
  * シェアシートが表示された後でもプレビュー上の情報をいつでも何度でも更新できる
  * 注意事項: シェア先で LPLinkMetadata が使われる場合がある