【SwiftUI】ShareLinkで画像を共有する際の落とし穴

#### 目次
* ShareLinkについて
* ShareLinkに渡す画像の型
* 「画像を保存」アクションを表示させる方法
* おまけ(補足)
* 再現可能なコード全部


# ShareLinkについて

https://developer.apple.com/documentation/SwiftUI/ShareLink

![IMG_2124.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/183db418-b2a1-a752-a641-27c4ede83cf3.jpeg)

ShareLinkはiOS16以上で使用可能なSwiftUIのAPIです。いわゆる「共有シート」を簡単に表示することができます。UIKitのUIActivityViewControllerでは、表示や非表示をiPhone・iPadに応じて制御する必要があり少々手間がかかりますが、ShareLinkは1行で共有シートを簡単に表示できます。

今回はShareLinkにおける画像の共有に絞って解説します。

# ShareLinkに渡す画像の型
### 1. URLによる共有
メモリ上の画像ではなく、ドキュメントディレクトリやアプリのプロジェクトに追加された画像など、ディスクに保存されている画像に対してはURLを渡すことで共有が可能です。

### 2. メモリ上の画像の共有
共有する画像が`Image`型や`UIImage`型などメモリ上にある場合の手順です。このケースは公式ドキュメントにはあまり詳しく記載されていません。

まず、前提としてShareLinkに渡す`item`（共有する対象物）は`Transferable`型に準拠している必要があります。

[Transferable ドキュメント](https://developer.apple.com/documentation/coretransferable/choosing-a-transfer-representation-for-a-model-type)

ここで注意すべき点は、`UIImage`は`Transferable`型に準拠していないため、直接渡すことができません。Xcodeで`item`に`UIImage`を渡すと以下のエラーが発生します。

```
Generic struct 'ShareLink' requires that 'UIImage' conform to 'Transferable'
```


一方で、SwiftUIの`Image`型は`Transferable`型に準拠しているため、そのまま画像を共有することができます。`Image(uiImage:)`として`UIImage`を渡すことで、画像を簡単に共有することができます。

ただし、`UIImage`を変換せずとも共有する方法もあります。`UIImage`自体を`Transferable`に準拠させることが可能です。

```swift:Transferableに準拠させる拡張　(pngへの変換)
extension UIImage: Transferable {
    public static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .png) { image in
            if let pngData = image.pngData() {
                return pngData
            } else {
                throw ConversionError.failedToConvertToPNG
            }
        }
    }
    enum ConversionError: Error {
        case failedToConvertToPNG
    }
}
```

この拡張により、`UIImage`がドラッグ&ドロップや共有など、転送時にはPNG形式に変換されるようになります。JPEGやHEIFへの変換も可能で、カスタマイズ性が高いです（`Image`型の場合、デフォルトでPNGに変換されます）。この方法により、`UIImage`型をそのまま`ShareLink`に渡すことが可能です。

Appleのドキュメントではカスタムな`Transferable`型（`Image + String`）の定義が紹介されていますが、画像の共有だけであれば前述の方法が最も簡単です。

### 1. URLによる共有と2. メモリ上の画像の共有の違い
URLで共有する場合、ファイル名、ファイル形式、ファイルサイズが表示されるため、プレビューの設定は不要です。しかし、メモリ上の画像の場合はカスタムプレビューの設定が必要です。
| 1. URLによる共有 | 2. メモリ上の画像の共有 |
|:-:|:-:|
| ![url.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/ecb5c8a4-ce16-1307-e8f9-17bc3d379071.png) |  ![メモリ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/6045e4f7-49b2-7101-4c8c-be2f9ddbd8c1.png)|

# 「画像を保存」アクションを表示させる方法
前述の方法により画像の共有は可能ですが、実機やシミュレータで確認すると、「画像を保存」アクションが表示されないことがあります。コピーやプリント、共有アルバムへの追加などのアクションは表示されますが、画像保存のアクションが欠けている場合があります。

多くのユーザーは、画像の共有ボタンを使ってSNSへの転送や端末への保存を期待しているため、この問題は重要です。これは`URL`型、`Image`型どちらのケースでも発生します。

#### 解決方法
この問題は写真アプリへのアクセス許可が原因です。アプリの`Info.plist`に`Privacy - Photo Library Additions Usage Description`を追加することで、「画像を保存」アクションが表示されるようになります。
[Info.plist Photo Library Add Usage Documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsphotolibraryaddusagedescription)
![画像.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/78da006b-6f4f-b282-9422-00c1b171f3b9.jpeg)
画像を保存アクション
![画像を保存.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/3fe6957b-978f-a184-bc9d-ca5be62134ce.jpeg)

この設定で指定する文字列は、初めて「画像を保存」アクションを実行した際に表示されるアラート文言です。
![アクセス管理.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/b6ab7e16-5e80-d85c-3c32-881c7fa65ea7.jpeg)


カスタムの`ImagePicker`などを使用していて、既に`Privacy - Photo Library Usage Description`を追加している場合はこの問題は発生しないはずです。
[Info.plist Photo Library Usage Documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsphotolibraryusagedescription)


最近では、許可が不要でプライベートアクセスが可能な`PhotosPicker`の利用が増えていますが、アクセス許可の設定を確認することが重要だと思います。

# おまけ(補足)
補足として、SwiftUIの`navigationDocument`を紹介します。これも`ShareLink`と同様に`Transferable`型を渡すことで、インラインスタイルの`navigationTitle`がボタンに変わり、共有オプションが表示されます。これを使うことで`ShareLink`と同等の機能を提供できます（ネイティブなファイルアプリに実装されています）。
![Simulator Screenshot - iPhone 15 Pro - 2024-09-26 at 22.37.34.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/1d9d67a1-6871-0994-a39b-8192a92c430c.jpeg)



# 再現可能なコード全文
```swift:TestView.swift
import SwiftUI

struct TestView: View 
    let imageFromURL = Bundle.main.url(forResource: "logo.png", withExtension: nil)!
    let imageFromAssets = Image("logo")
    let uiImageFromAssets = UIImage(imageLiteralResourceName: "logo")

    var body: some View {
        NavigationStack{
            VStack(spacing: 20) {
                    ShareLink(
                        "URLから共有",
                        item: imageFromURL
                    )
                    ShareLink(
                        "Assetsから共有（UIImage型）",
                        item: uiImageFromAssets,
                        preview: SharePreview(
                            "添付画像",
                            image: uiImageFromAssets
                        )
                    )
                    ShareLink(
                        "Assetsから共有（Image型）",
                        item: imageFromAssets,
                        preview: SharePreview(
                            "添付画像",
                            image: imageFromAssets
                        )
                    )
                    Spacer()
                }
                .navigationTitle("Title")
                .navigationBarTitleDisplayMode(.inline)
                .navigationDocument(imageFromURL)
        }

    }
}
extension UIImage: Transferable {
    public static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .png) { image in
            if let pngData = image.pngData() {
                return pngData
            } else {
                throw ConversionError.failedToConvertToPNG
            }
        }
    }
    enum ConversionError: Error {
        case failedToConvertToPNG
    }
}
```
![Simulator Screenshot - iPhone 15 Pro - 2024-09-26 at 22.37.27.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718653/2a50d541-2a4c-abb9-306e-b11739576007.jpeg)

ここまでご覧いただきありがとうございました。間違い等ありましたらコメントにてご指摘お願いします。

個人的には、ShareLinkでUIActivityViewControllerのようにカスタムアクションを設定できるようになると良いなと思っているところです...