🏠

【SwiftUI】Menuのコンテンツのアイコンに絵文字を使いたい

2024/02/22に公開
2

SwiftUIのMenuのコンテンツのアイコンに絵文字を使いたいと思い、色々と試してみたので共有します。

環境

  • Xcode 15.2

Menuを使用すると簡単にメニュー画面を表示することが出来ます。

Menu {
    Label("House", systemImage: "house")
} label: {
    Text("メニュー")
}

また、Labelを使用することで、Menu上にテキストとアイコンを表示することが出来ます。

絵文字をアイコンとして表示させたい

絵文字をMenu上のアイコンとして表示させる為に色々と試してみました。

HStack

Menu {
    HStack {
        Text("House")
        Text("🏠")
    }
} label: {
    Text("メニュー")
}

二つのコンテンツとして表示されてしまいました。

Label(title:icon:)

https://developer.apple.com/documentation/swiftui/label/init(title:icon:)

カスタムタイトルとアイコンを指定できるInitialierで試してみると、

Menu {
    Label(title: { Text("House") },
          icon: { Text("🏠") })
} label: {
    Text("メニュー")
}

絵文字を指定したアイコンは表示されませんでした。

絵文字を画像にする

Textだとアイコンとして表示されないようなので、思い切って絵文字を画像に変換しました。

絵文字の判定をする

まずは文字列が絵文字かどうかを判定するエクステンションを作成しました

extension Character {
    /// Characterが絵文字かどうか
    var isEmoji: Bool {
        guard let scalar = unicodeScalars.first else { return false }
        return scalar.properties.isEmoji && (scalar.value > 0x238C || unicodeScalars.count > 1)
    }
}

extension String {
    /// 文字列に絵文字が含まれているか
    func containsEmoji() -> Bool {
        for character in self {
            if character.isEmoji {
                return true
            }
        }
        return false
    }
}

絵文字をUIImageに変換する

絵文字かどうかを判定して、その後受け取った文字列から画像を作成しています。

extension UIImage {
    static func generated(fromEmoji emoji: String, fontSize: CGFloat) -> UIImage? {
        guard emoji.count == 1,
              emoji.containsEmoji() else {
            print("The emoji must be a single-character emoji.")
            return nil
        }
        let attributes: [NSAttributedString.Key : Any] = [.font: UIFont.systemFont(ofSize: fontSize)]
        let size = emoji.size(withAttributes: attributes)
        return UIGraphicsImageRenderer(size: size).image { _ in
            (emoji as NSString).draw(in: CGRect(origin: .zero, size: size),
                                     withAttributes: attributes)
        }
    }
}

結果

import SwiftUI

struct ContentView: View {

    /// 絵文字画像
    var emojiImage: UIImage {
        return UIImage.generated(fromEmoji: "🏠", fontSize: 24) ?? UIImage()
    }
    
    var body: some View {
        Menu {
            Label(title: { Text("House") },
                  icon: { Image(uiImage: emojiImage) })
        } label: {
            Text("メニュー")
        }
    }
}

無事に表示されました🎉

絵文字を画像にする (iOS 16+)

上記だと少し回りくどい処理になってしまいましたが、anz様にコメントいただき、iOS 16から使用できるImageRendererを使用することでより簡潔に書くことが出来ることが分かりました。

var body: some View {
    Menu {
        Label(
            title: { Text("House") },
            icon: { Image(uiImage: ImageRenderer(content: Text("🏠")).uiImage!) }
        )
    } label: {
        Text("メニュー")
    }
}

こちらでも同様の結果が表示されます🎉

まとめ

Menuのコンテンツのアイコンを絵文字にしたい場合は、絵文字から画像を作成して渡すのが良さそうです。
iOS 16+だと、ImageRendererを使用してSwiftUIを簡潔に画像化できるので簡潔に記述できる。

おわりに

もっと最適な方法がある場合やこんな回りくどい方法をしなくても実現できる等あれば教えていただけると嬉しいです🙏🧻

littleossa

参考

https://developer.apple.com/documentation/swiftui/menu
https://zenn.dev/kamimi01/articles/implement_only_emoji_keyboard
https://stackoverflow.com/questions/51100121/how-to-generate-an-uiimage-from-custom-text-in-swift
https://developer.apple.com/documentation/swiftui/imagerenderer

Discussion

anzanz

気になったのでゴニョゴニョやってみた感じ。下記でも同様な表示できました

var body: some View {
    Menu {
        Label(
            title: { Text("House") },
            icon: { Image(uiImage: ImageRenderer(content: Text("🏠")).uiImage!) }
        )
    } label: {
        Text("メニュー")
    }
}

同じことを ImageRenderer 使って簡潔にやっただけなのだけれど。。 ImageRenderer 使うので iOS 16+ になっちゃうのもつらみ

リルオッサリルオッサ

ありがとうございます!🙏
完全にこっちの方が書けており、個人的にも好きです!
追記させていただきます!!