Open1

SwiftUI: LPLinkMetadata.iconProviderにアイコンを渡したい

kabeyakabeya

アクティビティシート?シェアシート?の話です。アプリで「送る」みたいなアイコンを押すと画面下部に表示される他のアプリにデータを送るシート。

あれにアイコンが表示されたりされなかったりしてるので(前に上下反転して表示されてたこともありました)、ちゃんとアイコンを表示させようと思って調べてたんですね。

ざっくり流れは以下の感じです。

  • UIActivityViewControllerUIViewControllerRepresentableでラップする。
  • UIActivityItemSourceに従った自前のActivityItemSourceを実装する。
  • 自前のソースクラスにactivityViewControllerLinkMetadata(:)を実装する。

アイコンの指定部分は以下のような感じです。

 func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
        let metaData = LPLinkMetadata()
        // (中略)
        let iconURL = self.getIconURL()
        metaData.iconProvider = NSItemProvider(contentsOf: iconURL)
        return metaData
}

以下の記事を参考にしました。

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

問題は、このNSItemProvider(contentsOf: iconURL)にどのURLを指定するかです。

いくつかの記事で、以下のようにバンドルからアイコンファイル名が取れるよ、みたいなのを読みました。

func getIconFileName(bundle: Bundle = .main) -> String? {
        guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any] else {
            print("Could not find icons in CFBundleIcons")
            return nil
        }
        guard let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any] else {
            print("Could not find icons in CFBundlePrimaryIcon")
            return nil
        }
        guard let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String] else {
            print("Could not find icons in CFBundleIconFiles")
            return nil
        }
        guard let iconFileName = iconFiles.last else {
            print("CFBundleIconFiles is empty")
            return nil
        }
        return iconFileName
}

ですが実際に動かすとうまく行きません。

具体的に言うと、

  • iPhoneの場合はこの関数で「AppIcon60x60」という文字列が返ってきます。
  • じゃあ、てことでBundle.main.url(forResource: withExtension:)を使って取得しようとしても、ないよって言われてしまいます。
  • iPadの場合は「AppIcon76x76」です。これもないよと言われます。

で、よくよく調べると、以下の記事に「AppIcon60x60@2x.png」「AppIcon76x76@2x~ipad.png」でメインバンドルに保存されている、って見つけました。

https://qiita.com/Hackenbacker/items/ff0499c00eac34d024bc

で、取れたファイル名に、"@2x"ってつけてメインバンドルからURLを取得すると、うまく行くんですね。
ただ、これをハードコーディングするとヤな感じですね。
なので、メインバンドルのPNGファイルを検索して、ファイル名の先頭でマッチングして、それで取得するようにしました。

func getIconFileName(bundle: Bundle = .main) -> String? {
        guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any] else {
            print("Could not find icons in CFBundleIcons")
            return nil
        }
        guard let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any] else {
            print("Could not find icons in CFBundlePrimaryIcon")
            return nil
        }
        guard let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String] else {
            print("Could not find icons in CFBundleIconFiles")
            return nil
        }
        guard let iconFileName = iconFiles.last else {
            print("CFBundleIconFiles is empty")
            return nil
        }
        let paths = bundle.paths(forResourcesOfType: "png", inDirectory: nil)
        for path in paths {
            let nsStringPath = path as NSString
            let fileName = (nsStringPath.lastPathComponent as NSString).deletingPathExtension
            if fileName.hasPrefix(iconFileName) {
                return fileName
            }
        }
        return iconFileName
}

ちなみに、これはAppIconがSingle Sizeの場合の話です。
All Sizesの場合も同じ処理で済む可能性はありますが、検証はしていません。