💭

全てのSF Symbolsを一覧で表示したい!

2024/11/24に公開

はじめに

皆さんは普段 SF Symbols を使っていますか?
個人開発ではアイコンを用意する手間が省けて、Apple 公式のアイコンが手軽に使えるためお世話になっている方も多いかと思います。

Image(systemName: "guitars.fill")

そんな SF Symbols ですが、一覧表示してユーザーにアイコンを選んでもらいたい... と思ったことはないでしょうか?

公式の SF Symbols アプリのような見た目で、ColorPicker のように使えるイメージです。

SF Symbols ColorPicker
スクリーンショット 2024-11-24 11 55 56 Simulator Screenshot - iPhone 16 - 2024-11-24 at 11 54 24

しかし、現状 UIKit や SwiftUI にはそういったコンポーネントは存在しません。
というわけで自作しようと思ったのですが、一覧化するだけでも大変でスマートに解決する方法はありませんでした😇

ちなみに OSS を探してみるといくつかヒットすると思うので、これらを利用しても良いかと思います。自分は UI を好きなように実装したかったため自作の道を選びました...

https://github.com/alessiorubicini/SFSymbolsPickerForSwiftUI
https://github.com/xnth97/SymbolPicker

SF Symbols の名前を全て取得する

一覧化するにあたり端末で利用できる SF Symbols の名前を全て知る必要があります。
OSS の実装を調べたりいろいろ試したところ方針としては以下の 2 つになりそうです。それぞれ詳しくみていきます。

  • SFSymbolsFramework から引っ張り出す
  • 公式の SF Symbols アプリを利用する

SFSymbolsFramework から引っ張り出す

こちらは上で紹介させていただいた SFSymbolsPickerForSwiftUI で利用されている方法になります。

やや強引になりますが、Bundle を利用して SFSymbolsFramework にアクセスし、その中にある CoreGlyphs.bundle から利用可能な SF Symbols の名前を取り出します。

struct SFSymbolList: View {
    @State private var symbolNames: [String]

    var body: some View {
        List(symbolNames, id: \.self) { name in
            HStack {
                Image(systemName: name)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 24, height: 24)
                Text(name)
            }
        }
    }
    
    init() {
        // `SFSymbolsFramework` から `CoreGlyphs.bundle` を取り出して SF Symbols の名前を取得.
        if let sfSymbolsBundle = Bundle(identifier: "com.apple.SFSymbolsFramework"),
           let bundlePath = sfSymbolsBundle.path(forResource: "CoreGlyphs", ofType: "bundle"),
           let bundle = Bundle(path: bundlePath),
           let resourcePath = bundle.path(forResource: "name_availability", ofType: "plist"),
           let dictionary = NSDictionary(contentsOfFile: resourcePath),
           let symbols = dictionary["symbols"] as? [String: String] {
            let sortedKeys = Array(symbols.keys).sorted(by: { $1 > $0 })
            symbolNames = sortedKeys
        } else {
            symbolNames = []
        }
    }
}
実行結果
Simulator Screen Recording - iPhone 16 - 2024-11-24 at 11 59 06

なお、検索すると以下のような CoreGlyphs.bundle に直接アクセスするコードも出てきますが、Xcode16.0 iOS18.0 のシミュレーターと iOS 17.5.1 の実機で試してみたところ Bundle(identifier: "com.apple.CoreGlyphs")nil になってしまってうまく動かない時がありました。

// ❌ `Bundle(identifier: "com.apple.CoreGlyphs")` が取れずにうまく動かない時がある.
guard let bundle = Bundle(identifier: "com.apple.CoreGlyphs"),
      let resourcePath = bundle.path(forResource: "symbol_search", ofType: "plist"),
      let dictionary = NSDictionary(contentsOfFile: resourcePath),
      let symbols = dictionary["symbols"] as? [String: String] else {
    return
}

この方法は確実ではあるものの Framework の構成等に依存するため、グレーかつあまり安全ではない印象です... ご利用の際は自己責任でお願いします🙇‍♂️

SFSymbolsFramework の中身について

今回利用した SFSymbolsFramework には何が入っているのか気になったので少し覗いてみます👀

SFSymbolsFramework CoreGlyphs.bundle
スクリーンショット 2024-11-24 2 31 38 スクリーンショット 2024-11-24 2 15 31

何やらローカライズのファイルと CoreGlyphs 関連のバンドルがいくつか入っているようです。

また、CoreGlyphs.bundle の中を見たところ SF Symbols の名前だけではなく、アイコンごとのカテゴリー情報や検索クエリ的なものも入っていました。
カテゴリー情報と検索クエリに関しては公式の SF Symbols アプリと同じ内容になっているようです。

categories.plist SF Symbols アプリ
スクリーンショット 2024-11-24 2 23 23 スクリーンショット 2024-11-24 2 23 35
symbol_search.plist SF Symbols アプリ
スクリーンショット 2024-11-24 2 44 52 スクリーンショット 2024-11-24 2 45 23

これらの情報を使えば SF Symbols アプリと同じような機能を持ったものを作ることができるかもしれません。

公式の SF Symbols アプリを利用する

SF Symbols の見た目と名前を確認することができるアプリが公式から出ているので、そちらを利用する方法になります。

SF Symbols アプリで全てのアイコンを表示した状態で Command + A で全てを選択します。
そして右クリックして「名前をコピー」を選択します。

名前をコピー
スクリーンショット 2024-11-24 12 06 39

ここでコピーした SF Symbols の名前を適当なテキストファイル等に貼り付けて、リソースとして追加しアプリから読み込みます。

struct SFSymbolList: View {
    @State private var symbolNames: [String]

    var body: some View {
        List(symbolNames, id: \.self) { name in
            HStack {
                Image(systemName: name)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 24, height: 24)
                Text(name)
            }
        }
    }

    init() {
        // SF Symbols の名前をテキストファイルから読み込む.
        if let fileURL = Bundle.main.url(forResource: "SFSymbols", withExtension: "txt"),
           let contents = try? String(contentsOf: fileURL, encoding: .utf8) {
            let splitByNewLine = contents.split(separator: "\n")
            let sortedSymbolNames = splitByNewLine.map { String($0) }
                .sorted(by: { $1 > $0 })
            symbolNames = sortedSymbolNames
        } else {
            symbolNames = []
        }
    }
}
実行結果
Simulator Screen Recording - iPhone 16 - 2024-11-24 at 12 40 40

ただし、この方法では一括で名前をコピーしてしまっているため OS バージョンが考慮されていません。

なので iOS18.0 以降でのみサポートされている SF Symbols を iOS 17 の端末で利用しようとするとアイコンが表示されず、コンソールには以下のように該当の Symbol が見つからなかったというエラーが表示されてしまいます。

No symbol named '10.arrow.trianglehead.clockwise' found in system symbol set
No symbol named '10.arrow.trianglehead.counterclockwise' found in system symbol set
No symbol named '15.arrow.trianglehead.clockwise' found in system symbol set
No symbol named '15.arrow.trianglehead.counterclockwise' found in system symbol set
No symbol named '30.arrow.trianglehead.clockwise' found in system symbol set
No symbol named '30.arrow.trianglehead.counterclockwise' found in system symbol set
No symbol named '30.arrow.trianglehead.clockwise' found in system symbol set
No symbol named '30.arrow.trianglehead.counterclockwise' found in system symbol set
iOS18 iOS17
Simulator Screenshot - iPhone 16 - 2024-11-24 at 12 57 05 スクリーンショット 2024-11-24 12 51 53

よって OS バージョンを考慮したい場合は少し工夫が必要です。

SF Symbols アプリでリスト表示に切り替えた後に「使用可能」のところをクリックすることでサポート OS 順にソートすることができます。

SF Symbols をリスト表示にしてソート
スクリーンショット 2024-11-24 13 03 38

だいぶ泥臭い方法になってしまいますが、この状態で OS バージョンごとに名前をコピーしていき別々のテキストファイルを作成して貼り付けていきます。
これらを OS バージョンごとに読み込むようにすることで Symbol を読み込めない状態を回避することができます。

雑にコードにすると以下のようなイメージです。

struct SFSymbolList: View {
    @State private var symbolNames: [String]

    var body: some View {
        List(symbolNames, id: \.self) { name in
            HStack {
                Image(systemName: name)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 24, height: 24)
                Text(name)
            }
        }
        .navigationTitle("SF Symbols")
    }

    init() {
        var loadedSymbolNames = [String]()
        // iOS 17 以降でサポートされている SF Symbol の名前を読み込む.
        if #available(iOS 17.0, *) {
            let supportedIOS17 = Self.loadSymbolNames(fileName: "iOS17")
            loadedSymbolNames.append(contentsOf: supportedIOS17)
        }
        // iOS 18 以降でサポートされている SF Symbol の名前を読み込む.
        if #available(iOS 18.0, *) {
            let supportedIOS18 = Self.loadSymbolNames(fileName: "iOS18")
            loadedSymbolNames.append(contentsOf: supportedIOS18)
        }
        let sortedSymbolNames = loadedSymbolNames.sorted(by: { $1 > $0 })
        symbolNames = sortedSymbolNames
    }
}

private extension SFSymbolList {
    static func loadSymbolNames(fileName: String) -> [String] {
        guard let fileURL = Bundle.main.url(forResource: fileName, withExtension: "txt"),
              let contents = try? String(contentsOf: fileURL, encoding: .utf8) else {
            return []
        }
        let splitByNewLine = contents.split(separator: "\n")
        return splitByNewLine.map { String($0) }
    }
}

まとめ

SF Symbols を一覧で表示したいだけでしたが、思った以上に労力が必要でした😅

もう少しいい方法がないか引き続き探してみようと思います。もし代替手段等をご存知の方がいればぜひ教えてください🙇‍♂️
いつか公式でサポートしてくれるといいのですが...

参考

Discussion