Zenn
🎁

【制作編】#3: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】

に公開

はじめに

a-Shell で、SF Symbols の一覧を出す簡単なアプリをつくります。

image

今まで紹介したパッケージライブラリを使い、端末内にある SF Symbols データの取り出し、表示までをハンズオン形式で実装していきます。

全 3 部構成の第 3 部です

この記事を含め、3 部構成で解説をします。
記事内の掲載コードは、コピーして貼り付ければ動くようにしています。

3 つの記事を通したコードは、GitHub で公開しています。

a-Shell はファイルアプリ経由の参照が可能です

a-Shell でのコード編集が大変、または既に実行したいデータがある場合は、アプリ外ディレクトリにアクセスするコマンドpickFolder や、Git (導入方法は割愛)を使った開発もできます。

こちらの リンク先 より、.zip を直接ダウンロードできます。

「とりあえず、動かしてみたい」という方は、ダウンロード後ファイルアプリで.zip を解凍し、解凍先ディレクトリを a-Shell へ参照させてください。

ハンズオン

空の新規ファイルから、順を追って実装していきます。最終的には公開しているリポジトリexampleSFSymbolsViewer.py のコードとなります。

先に、完成予定のコードです。

exampleSFSymbolsViewer.py | 全体
from pathlib import Path
import plistlib

from pyrubicon.objc.api import ObjCClass
from pyrubicon.objc.api import objc_method, objc_property
from pyrubicon.objc.runtime import send_super

from rbedge.enumerations import UITableViewStyle
from rbedge.functions import NSStringFromClass
from rbedge import pdbr

UIViewController = ObjCClass('UIViewController')
UITableView = ObjCClass('UITableView')
UITableViewCell = ObjCClass('UITableViewCell')
UIImage = ObjCClass('UIImage')

NSLayoutConstraint = ObjCClass('NSLayoutConstraint')
UIColor = ObjCClass('UIColor')


def get_order_list():
  CoreGlyphs_path = '/System/Library/CoreServices/CoreGlyphs.bundle/'

  symbol_order_path = 'symbol_order.plist'
  symbol_order_bundle = Path(CoreGlyphs_path, symbol_order_path)

  order_list = plistlib.loads(symbol_order_bundle.read_bytes())
  return order_list


class SFSymbolsViewController(UIViewController):

  cell_identifier: str = objc_property()
  all_items: list = objc_property()

  @objc_method
  def loadView(self):
    send_super(__class__, self, 'loadView')
    self.cell_identifier = 'customCell'
    self.all_items = get_order_list()

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')

    # --- Navigation
    self.navigationItem.title = NSStringFromClass(__class__) if (
      title := self.navigationItem.title) is None else title
    self.view.backgroundColor = UIColor.systemBackgroundColor()

    # --- Table
    sf_tableView = UITableView.alloc().initWithFrame_style_(
      self.view.bounds, UITableViewStyle.plain)
    sf_tableView.registerClass_forCellReuseIdentifier_(UITableViewCell,
                                                       self.cell_identifier)
    sf_tableView.dataSource = self
    sf_tableView.delegate = self

    # --- Layout
    self.view.addSubview_(sf_tableView)
    sf_tableView.translatesAutoresizingMaskIntoConstraints = False
    areaLayoutGuide = self.view.safeAreaLayoutGuide

    NSLayoutConstraint.activateConstraints_([
      sf_tableView.centerXAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerXAnchor),
      sf_tableView.centerYAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerYAnchor),
      sf_tableView.widthAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.widthAnchor, 1.0),
      sf_tableView.heightAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.heightAnchor, 1.0),
    ])

  @objc_method
  def didReceiveMemoryWarning(self):
    send_super(__class__, self, 'didReceiveMemoryWarning')
    print(f'\t{NSStringFromClass(__class__)}: didReceiveMemoryWarning')

  # --- UITableViewDataSource
  @objc_method
  def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int:
    return len(self.all_items)

  @objc_method
  def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
    cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
      self.cell_identifier, indexPath)

    symbol_name = self.all_items[indexPath.row]

    content = cell.defaultContentConfiguration()
    content.text = symbol_name
    content.textProperties.numberOfLines = 1
    content.image = UIImage.systemImageNamed_(symbol_name)
    cell.contentConfiguration = content

    return cell

  # --- UITableViewDelegate
  @objc_method
  def tableView_didSelectRowAtIndexPath_(self, tableView, indexPath):
    #tableView.deselectRowAtIndexPath_animated_(indexPath, True)
    select_item = self.all_items[indexPath.row]

    print(f'select:\t{select_item}')


if __name__ == '__main__':
  from rbedge.app import App
  from rbedge.enumerations import UIModalPresentationStyle

  main_vc = SFSymbolsViewController.new()
  _title = NSStringFromClass(SFSymbolsViewController)
  main_vc.navigationItem.title = _title

  presentation_style = UIModalPresentationStyle.fullScreen

  app = App(main_vc, presentation_style)
  app.present()

まずは、何もしない View を出す

UIViewController で、そのまま

View が出ないと始まらないので、View が出る状態まで持っていきます。
Class UIViewController を継承しますが、最初はUIViewController で、そのまま出してみましょう。

何もしない View | 全体
from pyrubicon.objc.api import ObjCClass
from rbedge.functions import NSStringFromClass

UIViewController = ObjCClass('UIViewController')

if __name__ == '__main__':
  from rbedge.app import App
  from rbedge.enumerations import UIModalPresentationStyle

  main_vc = UIViewController.new()
  _title = NSStringFromClass(UIViewController)
  main_vc.navigationItem.title = _title

  presentation_style = UIModalPresentationStyle.fullScreen

  app = App(main_vc, presentation_style)
  app.present()

image

navigationItem.title をインスタンス生成後に指定しています。NSStringFromClass で、Class UIViewController より文字列を取得します。

継承したクラスSFSymbolsViewController

Class UIViewController を継承しSFSymbolsViewController クラスとします。
今後はこのクラスに実装内容を書いていきます。

継承 SFSymbolsViewController | 全体
from pyrubicon.objc.api import ObjCClass
from pyrubicon.objc.api import objc_method
from pyrubicon.objc.runtime import send_super

from rbedge.functions import NSStringFromClass
from rbedge import pdbr

UIViewController = ObjCClass('UIViewController')


class SFSymbolsViewController(UIViewController):

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')

    # --- Navigation
    self.navigationItem.title = NSStringFromClass(__class__) if (
      title := self.navigationItem.title) is None else title

  @objc_method
  def didReceiveMemoryWarning(self):
    send_super(__class__, self, 'didReceiveMemoryWarning')
    print(f'\t{NSStringFromClass(__class__)}: didReceiveMemoryWarning')


if __name__ == '__main__':
  from rbedge.app import App
  from rbedge.enumerations import UIModalPresentationStyle

  main_vc = SFSymbolsViewController.new()
  _title = NSStringFromClass(SFSymbolsViewController)
  main_vc.navigationItem.title = _title

  presentation_style = UIModalPresentationStyle.fullScreen

  app = App(main_vc, presentation_style)
  app.present()

継承したので書き換える

継承してクラスを定義するので、send_superobjc_methodimport に追加しています。また、View Life Cycle 系のメソッドは必要なメソッドのみ書いていくことにします。

from pyrubicon.objc.api import ObjCClass
+ from pyrubicon.objc.api import objc_method
+ from pyrubicon.objc.runtime import send_super

from rbedge.functions import NSStringFromClass

表示させるインスタンスをUIViewController からSFSymbolsViewController に変更しています。

+ main_vc = SFSymbolsViewController.new()
- main_vc = UIViewController.new()
+ _title = NSStringFromClass(SFSymbolsViewController)
- _title = NSStringFromClass(UIViewController)

SFSymbolsViewController インスタンス生成後に、タイトルを指定しない場合。viewDidLoad 実行時のself.navigationItem.titleNone となります。
そこでif 分岐を使い、タイトルの表示分けができます。
(今回は、インスタンス生成後のタイトルも同じNSStringFromClass を使うので変わりないですが)

他のタイトルにしたい場合は、インスタンス生成後に指定すれば表示されます。

# インスタンス生成後に`title` 指定があるかないかで分岐
self.navigationItem.title = NSStringFromClass(__class__) if (
  title := self.navigationItem.title) is None else title

...

# 今回は、インスタンス生成後に指定するパターンで
_title = NSStringFromClass(SFSymbolsViewController)
main_vc.navigationItem.title = _title

また、Class 継承をしたので、titleNSStringFromClass で指定した場合、出力内容が一部変わります。
連続で実行すると、実行回数ごとに末尾の数値がインクリメントされていきます。

  • 初回実行
    image

  • 2 回目実行
    image

何もしないUITableView の実装

今回は、SF Symbols の一覧を表示させたいので、UITableView を使います。

UITableViewController を使う方法もあります。
しかし、今回は今後の拡張性も考慮し、UIViewControllerviewUITableView を乗せる方針にします。

真ん中あるシアン色の矩形がUITableView です。
image

何もしない UITableView | 全体
from pyrubicon.objc.api import ObjCClass
from pyrubicon.objc.api import objc_method
from pyrubicon.objc.runtime import send_super

from rbedge.enumerations import UITableViewStyle
from rbedge.functions import NSStringFromClass
from rbedge import pdbr

UIViewController = ObjCClass('UIViewController')
UITableView = ObjCClass('UITableView')

NSLayoutConstraint = ObjCClass('NSLayoutConstraint')
UIColor = ObjCClass('UIColor')


class SFSymbolsViewController(UIViewController):

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')

    # --- Navigation
    self.navigationItem.title = NSStringFromClass(__class__) if (
      title := self.navigationItem.title) is None else title

    # --- Table
    sf_tableView = UITableView.alloc().initWithFrame_style_(
      self.view.bounds, UITableViewStyle.plain)
    sf_tableView.backgroundColor = UIColor.systemCyanColor()

    # --- Layout
    self.view.addSubview_(sf_tableView)
    sf_tableView.translatesAutoresizingMaskIntoConstraints = False
    areaLayoutGuide = self.view.safeAreaLayoutGuide

    NSLayoutConstraint.activateConstraints_([
      sf_tableView.centerXAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerXAnchor),
      sf_tableView.centerYAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerYAnchor),
      sf_tableView.widthAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.widthAnchor, 0.5),
      sf_tableView.heightAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.heightAnchor, 0.5),
    ])

  @objc_method
  def didReceiveMemoryWarning(self):
    send_super(__class__, self, 'didReceiveMemoryWarning')
    print(f'\t{NSStringFromClass(__class__)}: didReceiveMemoryWarning')


if __name__ == '__main__':
  from rbedge.app import App
  from rbedge.enumerations import UIModalPresentationStyle

  main_vc = UIViewController.new()
  _title = NSStringFromClass(UIViewController)
  main_vc.navigationItem.title = _title

  presentation_style = UIModalPresentationStyle.fullScreen

  app = App(main_vc, presentation_style)
  app.present()

initWithFramestyle

UITableView のインスタンスを生成するメソッドです。
style 指定は、rbedge.enumerations 内で定義しています(サンプルリポジトリをコピーしている場合)。

今回は、plain を指定します。

# rbedge にenumerations.py がない場合は
# このクラスを追加してください
class UITableViewStyle:
  plain: int = 0
  grouped: int = 1
  insetGrouped: int = 2

もし、UITableViewinit メソッドがわからない場合。

console に出力
pdbr.state(UITableView.alloc())

しかし、ものすごい量のメソッドたちが出てくるので、まずはドキュメントを探すのをお勧めします。

UIColor で色付け

何もしないUITableView なので、今のところ背面に色をつけてaddSubview_ できているか確認します。
色はどれでもいいので、先ほどのpdbr.stateで、使える色を見てみます。

console に出力
pdbr.state(UIColor)
結果の一部
...
  "systemBackgroundColor",
  "systemBlackColor",
  "systemBlueColor",
  "systemBrownColor",
  "systemCyanColor",
  "systemDarkBlueColor",
  "systemDarkExtraLightGrayColor",
  "systemDarkExtraLightGrayTintColor",
  "systemDarkGrayColor",
  "systemDarkGrayTintColor",
...

一部を抜き出しています、system〜 あたりは使い勝手がいいと思われます。

(少し脱線)

シアンの色をUIColor.systemCyanColor で、呼び出しました。他にUIColor.cyanColor でも呼び出せます。
しかし、メソッドとプロパティと、呼び出し方が違います。

# メソッドで呼び出し
UIColor.systemCyanColor()

# プロパティで呼び出し
UIColor.cyanColor

# 以下の方法では、呼び出せない
'''
UIColor.systemCyanColor
UIColor.cyanColor()
'''

ドキュメントでは、Type Property で違いはありません。
メソッドなのか、プロパティなのか。system の方がメソッド呼び出しと考えられます。
しかし、私は毎回忘れるので、とりあえず実行してみて、エラーが出るかどうかで判断しています。

ちなみに、Rubicon-ObjC と同じ作者が toga というものを作っています。
toga の中でUIColor は、declare_class_property を使いプロパティ呼び出しができるように設定している模様です。

View の Auto Layout

View のサイズや位置は普段、Storyboard で設定をします。
a-Shell の場合は、Storyboard を使わないので、コードで設定します。

今回は Auto Layout で設定します。
NSLayoutConstraint と、NSLayoutAnchor を組み合わせることで簡潔に記述できます。

Auto Layout したい View のtranslatesAutoresizingMaskIntoConstraintsFalse にします。
False にしないと、Auto Layout の設定はできません。

# `False` にする
sf_tableView.translatesAutoresizingMaskIntoConstraints = False

基本的に、数値決め打ちは気持ち悪いので、self.view (SFSymbolsViewController の View) を起点にレイアウトを組みます。
また、今後sf_tableView を全画面に表示することを見据え、self.view の safe area を参照の値とします。

X 軸 Y 軸、幅と高さをsf_tableView に指定します。

# `areaLayoutGuide` を基準に
areaLayoutGuide = self.view.safeAreaLayoutGuide

NSLayoutConstraint.activateConstraints_([
  # X軸を中心
  sf_tableView.centerXAnchor.constraintEqualToAnchor_(
    areaLayoutGuide.centerXAnchor),
  # Y軸を中心
  sf_tableView.centerYAnchor.constraintEqualToAnchor_(
    areaLayoutGuide.centerYAnchor),
  # 幅を0.5 倍(半分)
  sf_tableView.widthAnchor.constraintEqualToAnchor_multiplier_(
    areaLayoutGuide.widthAnchor, 0.5),
  # 高さを0.5倍(半分)
  sf_tableView.heightAnchor.constraintEqualToAnchor_multiplier_(
    areaLayoutGuide.heightAnchor, 0.5),
])

X 軸 Y 軸は、それぞれの中心点。幅と高さは半分の値で設定しました。

UITableView の要素を出す

配列の要素をsf_tableView に表示させます。普段 Storyboard で実装していると登場しないメソッドが出てきます。

image

配列要素を表示 | 全体
from pyrubicon.objc.api import ObjCClass
from pyrubicon.objc.api import objc_method, objc_property
from pyrubicon.objc.runtime import send_super

from rbedge.enumerations import UITableViewStyle
from rbedge.functions import NSStringFromClass
from rbedge import pdbr

UIViewController = ObjCClass('UIViewController')
UITableView = ObjCClass('UITableView')
UITableViewCell = ObjCClass('UITableViewCell')

NSLayoutConstraint = ObjCClass('NSLayoutConstraint')
UIColor = ObjCClass('UIColor')


class SFSymbolsViewController(UIViewController):

  cell_identifier: str = objc_property()
  all_items: list = objc_property()

  @objc_method
  def loadView(self):
    send_super(__class__, self, 'loadView')
    self.cell_identifier = 'customCell'
    self.all_items = [
      'ほげ',
      'ふが',
      'ぴよ',
    ]

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')

    # --- Navigation
    self.navigationItem.title = NSStringFromClass(__class__) if (
      title := self.navigationItem.title) is None else title

    # --- Table
    sf_tableView = UITableView.alloc().initWithFrame_style_(
      self.view.bounds, UITableViewStyle.plain)
    sf_tableView.registerClass_forCellReuseIdentifier_(UITableViewCell,
                                                       self.cell_identifier)
    sf_tableView.dataSource = self

    sf_tableView.backgroundColor = UIColor.systemCyanColor()

    # --- Layout
    self.view.addSubview_(sf_tableView)
    sf_tableView.translatesAutoresizingMaskIntoConstraints = False
    areaLayoutGuide = self.view.safeAreaLayoutGuide

    NSLayoutConstraint.activateConstraints_([
      sf_tableView.centerXAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerXAnchor),
      sf_tableView.centerYAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerYAnchor),
      sf_tableView.widthAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.widthAnchor, 0.5),
      sf_tableView.heightAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.heightAnchor, 0.5),
    ])

  @objc_method
  def didReceiveMemoryWarning(self):
    send_super(__class__, self, 'didReceiveMemoryWarning')
    print(f'\t{NSStringFromClass(__class__)}: didReceiveMemoryWarning')

  # --- UITableViewDataSource
  @objc_method
  def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int:
    return len(self.all_items)

  @objc_method
  def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
    cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
      self.cell_identifier, indexPath)
    content = cell.defaultContentConfiguration()
    content.text = self.all_items[indexPath.row]
    cell.contentConfiguration = content
    return cell


if __name__ == '__main__':
  from rbedge.app import App
  from rbedge.enumerations import UIModalPresentationStyle

  main_vc = UIViewController.new()
  _title = NSStringFromClass(UIViewController)
  main_vc.navigationItem.title = _title

  presentation_style = UIModalPresentationStyle.fullScreen

  app = App(main_vc, presentation_style)
  app.present()

objc_property

Class に独自のプロパティを持たせる場合、objc_property で宣言をします。
objc_property 宣言しないと、View を閉じた際のメモリ解放がうまく行われません。a-Shell がクラッシュします。

Python クラスのself のように扱えますが、objc_property を使うことにより複雑になるので使用しないようにしています。

複雑さを避けるため:

  • シンプルな型
  • メソッド内で処理できるよう工夫

といった、方針で実装しています。
sf_tableViewobjc_property 宣言したいところですが、複雑さ回避のためviewDidLoad の肥大化とトレードオフとしています。

from pyrubicon.objc.api import objc_method, objc_property

class SFSymbolsViewController(UIViewController):
  # 型指定は無くても問題なし
  cell_identifier: str = objc_property()
  all_items: list = objc_property()

今回は、UITableViewCell の識別子(cell_identifier)と、配列要素(all_items) で宣言します。
型は、必須ではありません。自分の備忘として入れています。

セルを登録

UITableView は、1 つ 1 つのUITableViewCell をまとめて表示します。
その 1 つのUITableViewCell は、事前に識別子を定義し、UITableView へ登録する必要があります。

class SFSymbolsViewController(UIViewController):
  # 識別子としての文字列
  cell_identifier: str = objc_property()

  @objc_method
  def loadView(self):
    send_super(__class__, self, 'loadView')
    # 識別子としての文字列
    self.cell_identifier = 'customCell'

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')
    # 中略
    # --- Table
    sf_tableView = UITableView.alloc().initWithFrame_style_(
      self.view.bounds, UITableViewStyle.plain)
    sf_tableView.registerClass_forCellReuseIdentifier_(UITableViewCell,
                                                       self.cell_identifier)

loadView メソッド時に、customCell という文字列を定義。
viewDidLoad メソッドで、customCell を識別子としての登録をUITableViewCell を使う宣言をしています。

# セルはUITableViewCell, 識別子は文字列`customCell` で登録
sf_tableView.registerClass_forCellReuseIdentifier_(
  UITableViewCell, self.cell_identifier)

dataSource との連携

セルをUITableView に反映するため、以下 2 点が必要です:

今回、最小限のメソッドで実装します。「必須」のメソッドです:

class SFSymbolsViewController(UIViewController):

  # 配列要素
  all_items: list = objc_property()

  @objc_method
  def loadView(self):
    send_super(__class__, self, 'loadView')
    # 配列要素
    self.all_items = [
      'ほげ',
      'ふが',
      'ぴよ',
    ]

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')
    # 中略

    # UITableViewDataSource Protocol を紐付け
    sf_tableView.dataSource = self

  # --- UITableViewDataSource
  @objc_method
  def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int:
    # 配列要素の総数を整数で返す
    return len(self.all_items)

  @objc_method
  def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
    # 文字列の識別子と、index で使用するセルを呼び出す
    cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
      self.cell_identifier, indexPath)
    content = cell.defaultContentConfiguration()
    content.text = self.all_items[indexPath.row]
    cell.contentConfiguration = content

    return cell

tableView_numberOfRowsInSection_ メソッド

表示させるセルの総数を整数値で返します。

一部の引数や返り値に型ヒントを付ける必要があり、Rubicon-ObjC のドキュメントType annotations に説明があります。
Rubicon-ObjC のオブジェクトには、型ヒントは不要です。

@objc_method
def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int:
  # Python オブジェクトである`int` で返すため型を記載
  return len(self.all_items)

引数section は、整数int 。返り値も Python の整数型なのでint と指定します。

tableView_cellForRowAtIndexPath_ メソッド

各セルごとの要素を設定して返します。
返り値も、Rubicon-ObjC のオブジェクトになるので、型ヒントは不要です。
型ヒントを入れるのであれば、ObjCInstance あたりでしょうか。エラーが出ないので、詳しくは追ってません。

@objc_method
def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
  cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
    self.cell_identifier, indexPath)
  content = cell.defaultContentConfiguration()
  content.text = self.all_items[indexPath.row]
  cell.contentConfiguration = content

  # print(type(cell))  # -> `<class 'pyrubicon.objc.api.ObjCInstance'>` と出る
  return cell

dequeueReusableCellWithIdentifier で、識別子セルを呼び出します。
indexPath(NSIndexPath) の、インデックス情報から、配列要素を取り出します。

今回は、セルに文字列を反映する簡単な実装です。

sf_tableView.dataSource = self

Class SFSymbolsViewControllerUITableViewDataSource Protocol 要素のメソッドを追加しました。
そのメソッドをsf_tableView.dataSource と、するために= self としています。
= self だと全てのメソッドとイメージしてしまいますが、対応するメソッドのみ認識するようです。

これにより、sf_tableView にセルの要素が反映されます。

SF Symbols 情報を取得する

View の表示として、配列要素をUITableView に反映ができました。配列要素は、Python のlist 形式で問題なさそうです。
そこでここからは、仮の配列要素を、実際に使う SF Symbols 情報に変えていきます。
View の表示は一旦お休みです。

実は、SF Symbols を得るのは簡単です。どこかのサーバーにアクセスしたり、ハードコードで写経する必要はありません。

お手持ちの iPhone や iPad の中にあります。

from pathlib import Path
import plistlib


def get_order_list():
  CoreGlyphs_path = '/System/Library/CoreServices/CoreGlyphs.bundle/'

  symbol_order_path = 'symbol_order.plist'
  symbol_order_bundle = Path(CoreGlyphs_path, symbol_order_path)

  order_list = plistlib.loads(symbol_order_bundle.read_bytes())
  return order_list

標準ライブラリpathlib で、端末内部のディレクトリデータにアクセスします。
CoreGlyphs.bundle/ 内に SF Symbols 情報の.plist ファイルがあります。
今回だと、symbol_order.plist の要素が扱いやすそうなので、こちらから情報を取得します。
取得には、標準ライブラリのplistlib を使います。

なお、他の.plist ファイルは、dump したものを以下のリポジトリにまとめています。

plistlib なんて初めて使いました。

symbol_order.plist の中身

print(get_order_list())

とすれば、確認できます。しかし、7000 以上あるので dump データを確認しながら進めます。

以下のように文字列の配列が確認できます:

[
  "square.and.arrow.up",
  "square.and.arrow.up.fill",
  "square.and.arrow.up.circle",
  "square.and.arrow.up.circle.fill",
  "square.and.arrow.up.badge.clock",
  "square.and.arrow.up.badge.clock.fill",
  "square.and.arrow.up.trianglebadge.exclamationmark",
  "square.and.arrow.up.trianglebadge.exclamationmark.fill",
  "square.and.arrow.down",
  "square.and.arrow.down.fill",
  ...
  "48.square.hi",
  "48.square.fill.hi",
  "49.circle.hi",
  "49.circle.fill.hi",
  "49.square.hi",
  "49.square.fill.hi",
  "50.circle.hi",
  "50.circle.fill.hi",
  "50.square.hi",
  "50.square.fill.hi",
  "apple.logo"
]

文字列が、それぞれ SF Symbols の名前となります。

各セルへ SF Symbols を表示

SF Symbols の情報を関数get_order_list により取得できるようになりました。
次は、各セルに SF Symbols のアイコンと名前を反映させます。
各 SF Symbols の名前がわかれば、UIImage より簡単に取得できます。

image

SF Symbols 表示 | 全体
from pathlib import Path
import plistlib

from pyrubicon.objc.api import ObjCClass
from pyrubicon.objc.api import objc_method, objc_property
from pyrubicon.objc.runtime import send_super

from rbedge.enumerations import UITableViewStyle
from rbedge.functions import NSStringFromClass
from rbedge import pdbr

UIViewController = ObjCClass('UIViewController')
UITableView = ObjCClass('UITableView')
UITableViewCell = ObjCClass('UITableViewCell')
UIImage = ObjCClass('UIImage')

NSLayoutConstraint = ObjCClass('NSLayoutConstraint')
UIColor = ObjCClass('UIColor')


def get_order_list():
  CoreGlyphs_path = '/System/Library/CoreServices/CoreGlyphs.bundle/'

  symbol_order_path = 'symbol_order.plist'
  symbol_order_bundle = Path(CoreGlyphs_path, symbol_order_path)

  order_list = plistlib.loads(symbol_order_bundle.read_bytes())
  return order_list


class SFSymbolsViewController(UIViewController):

  cell_identifier: str = objc_property()
  all_items: list = objc_property()

  @objc_method
  def loadView(self):
    send_super(__class__, self, 'loadView')
    self.cell_identifier = 'customCell'
    self.all_items = get_order_list()

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')

    # --- Navigation
    self.navigationItem.title = NSStringFromClass(__class__) if (
      title := self.navigationItem.title) is None else title

    self.view.backgroundColor = UIColor.systemDarkPinkColor()

    # --- Table
    sf_tableView = UITableView.alloc().initWithFrame_style_(
      self.view.bounds, UITableViewStyle.plain)
    sf_tableView.registerClass_forCellReuseIdentifier_(UITableViewCell,
                                                       self.cell_identifier)
    sf_tableView.dataSource = self

    sf_tableView.backgroundColor = UIColor.systemCyanColor()

    # --- Layout
    self.view.addSubview_(sf_tableView)
    sf_tableView.translatesAutoresizingMaskIntoConstraints = False
    areaLayoutGuide = self.view.safeAreaLayoutGuide

    NSLayoutConstraint.activateConstraints_([
      sf_tableView.centerXAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerXAnchor),
      sf_tableView.centerYAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerYAnchor),
      sf_tableView.widthAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.widthAnchor, 0.5),
      sf_tableView.heightAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.heightAnchor, 0.5),
    ])

  @objc_method
  def didReceiveMemoryWarning(self):
    send_super(__class__, self, 'didReceiveMemoryWarning')
    print(f'\t{NSStringFromClass(__class__)}: didReceiveMemoryWarning')

  # --- UITableViewDataSource
  @objc_method
  def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int:
    return len(self.all_items)

  @objc_method
  def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
    cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
      self.cell_identifier, indexPath)

    symbol_name = self.all_items[indexPath.row]

    content = cell.defaultContentConfiguration()
    content.text = symbol_name
    content.image = UIImage.systemImageNamed_(symbol_name)
    cell.contentConfiguration = content

    return cell


if __name__ == '__main__':
  from rbedge.app import App
  from rbedge.enumerations import UIModalPresentationStyle

  main_vc = UIViewController.new()
  _title = NSStringFromClass(UIViewController)
  main_vc.navigationItem.title = _title

  presentation_style = UIModalPresentationStyle.fullScreen

  app = App(main_vc, presentation_style)
  app.present()

背面色をsystemDarkPinkColor にしています。
sf_tableView との境界と、スクロール挙動時のナビゲーション透過の確認用です。

systemImageNamed_ メソッド

UIImagesystemImageNamed: へ、文字列で SF Symbols の名前を引数に渡せば取得できます。

# 新規に宣言
UIImage = ObjCClass('UIImage')


class SFSymbolsViewController(UIViewController):

  all_items: list = objc_property()

  @objc_method
  def loadView(self):
    send_super(__class__, self, 'loadView')
    # 配列要素をSF Symbols のものに変更
    self.all_items = get_order_list()


  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')

    # 背景色追加
    self.view.backgroundColor = UIColor.systemDarkPinkColor()


  @objc_method
  def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
    cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
      self.cell_identifier, indexPath)

    # SF Symbols 文字列のアイコン名
    symbol_name = self.all_items[indexPath.row]

    content = cell.defaultContentConfiguration()
    content.text = symbol_name
    # 文字列でUIImage 取得
    content.image = UIImage.systemImageNamed_(symbol_name)
    cell.contentConfiguration = content

    return cell

配列self.all_items より、インデックスで該当の情報を取得します。
そして、defaultContentConfiguration の、.image へ定義すれば反映されます。

仕上げに微調整

仕上げに:

  • 見た目の整え
    • sf_tableView の全画面表示
    • セルの文字列を 1 行で表示
  • 押したセルのアイコン名を出力
  • 不要な処理の整理
    • backgroundColor

をして、完成とします。

image

(再掲)exampleSFSymbolsViewer.py | 全体
from pathlib import Path
import plistlib

from pyrubicon.objc.api import ObjCClass
from pyrubicon.objc.api import objc_method, objc_property
from pyrubicon.objc.runtime import send_super

from rbedge.enumerations import UITableViewStyle
from rbedge.functions import NSStringFromClass
from rbedge import pdbr

UIViewController = ObjCClass('UIViewController')
UITableView = ObjCClass('UITableView')
UITableViewCell = ObjCClass('UITableViewCell')
UIImage = ObjCClass('UIImage')

NSLayoutConstraint = ObjCClass('NSLayoutConstraint')
UIColor = ObjCClass('UIColor')


def get_order_list():
  CoreGlyphs_path = '/System/Library/CoreServices/CoreGlyphs.bundle/'

  symbol_order_path = 'symbol_order.plist'
  symbol_order_bundle = Path(CoreGlyphs_path, symbol_order_path)

  order_list = plistlib.loads(symbol_order_bundle.read_bytes())
  return order_list


class SFSymbolsViewController(UIViewController):

  cell_identifier: str = objc_property()
  all_items: list = objc_property()

  @objc_method
  def loadView(self):
    send_super(__class__, self, 'loadView')
    self.cell_identifier = 'customCell'
    self.all_items = get_order_list()

  @objc_method
  def viewDidLoad(self):
    send_super(__class__, self, 'viewDidLoad')

    # --- Navigation
    self.navigationItem.title = NSStringFromClass(__class__) if (
      title := self.navigationItem.title) is None else title
    self.view.backgroundColor = UIColor.systemBackgroundColor()

    # --- Table
    sf_tableView = UITableView.alloc().initWithFrame_style_(
      self.view.bounds, UITableViewStyle.plain)
    sf_tableView.registerClass_forCellReuseIdentifier_(UITableViewCell,
                                                       self.cell_identifier)
    sf_tableView.dataSource = self
    sf_tableView.delegate = self

    # --- Layout
    self.view.addSubview_(sf_tableView)
    sf_tableView.translatesAutoresizingMaskIntoConstraints = False
    areaLayoutGuide = self.view.safeAreaLayoutGuide

    NSLayoutConstraint.activateConstraints_([
      sf_tableView.centerXAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerXAnchor),
      sf_tableView.centerYAnchor.constraintEqualToAnchor_(
        areaLayoutGuide.centerYAnchor),
      sf_tableView.widthAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.widthAnchor, 1.0),
      sf_tableView.heightAnchor.constraintEqualToAnchor_multiplier_(
        areaLayoutGuide.heightAnchor, 1.0),
    ])

  @objc_method
  def didReceiveMemoryWarning(self):
    send_super(__class__, self, 'didReceiveMemoryWarning')
    print(f'\t{NSStringFromClass(__class__)}: didReceiveMemoryWarning')

  # --- UITableViewDataSource
  @objc_method
  def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int:
    return len(self.all_items)

  @objc_method
  def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
    cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
      self.cell_identifier, indexPath)

    symbol_name = self.all_items[indexPath.row]

    content = cell.defaultContentConfiguration()
    content.text = symbol_name
    content.textProperties.numberOfLines = 1
    content.image = UIImage.systemImageNamed_(symbol_name)
    cell.contentConfiguration = content

    return cell

  # --- UITableViewDelegate
  @objc_method
  def tableView_didSelectRowAtIndexPath_(self, tableView, indexPath):
    #tableView.deselectRowAtIndexPath_animated_(indexPath, True)
    select_item = self.all_items[indexPath.row]

    print(f'select:\t{select_item}')


if __name__ == '__main__':
  from rbedge.app import App
  from rbedge.enumerations import UIModalPresentationStyle

  main_vc = SFSymbolsViewController.new()
  _title = NSStringFromClass(SFSymbolsViewController)
  main_vc.navigationItem.title = _title

  presentation_style = UIModalPresentationStyle.fullScreen

  app = App(main_vc, presentation_style)
  app.present()

sf_tableView の全画面表示

viewDidLoadNSLayoutConstraint を使って位置とサイズを指定していました。

widthAnchorheightAnchor の値を0.5 から1.0 に変更するだけです。

NSLayoutConstraint.activateConstraints_([
  sf_tableView.centerXAnchor.constraintEqualToAnchor_(
    areaLayoutGuide.centerXAnchor),
  sf_tableView.centerYAnchor.constraintEqualToAnchor_(
    areaLayoutGuide.centerYAnchor),
  # 幅の倍率を0.5 -> 1.0 に変更
  sf_tableView.widthAnchor.constraintEqualToAnchor_multiplier_(
    areaLayoutGuide.widthAnchor, 1.0),
  # 高さの倍率を0.5 -> 1.0 に変更
  sf_tableView.heightAnchor.constraintEqualToAnchor_multiplier_(
    areaLayoutGuide.heightAnchor, 1.0),
])

セルの文字列を 1 行で表示

各 SF Symbols の名前が、短いのと長いのが極端です。セル内が 2 行となったりして、統一感がありません。
そこで、セル幅を越えた文字列を... と省略することにします。

@objc_method
def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
  cell = tableView.dequeueReusableCellWithIdentifier_forIndexPath_(
    self.cell_identifier, indexPath)

  symbol_name = self.all_items[indexPath.row]

  content = cell.defaultContentConfiguration()
  content.text = symbol_name
  # セル内を1行表示、セル範囲を越える場合`...` で省略
  content.textProperties.numberOfLines = 1
  content.image = UIImage.systemImageNamed_(symbol_name)
  cell.contentConfiguration = content

  return cell

textProperties.numberOfLines = 1 の部分です。追加します。

押したセルのアイコン名を出力

UITableViewDelegate Protocol を使って、セルがタップされた時の挙動を、指定します。

# --- UITableViewDelegate
@objc_method
def tableView_didSelectRowAtIndexPath_(self, tableView, indexPath):
  #tableView.deselectRowAtIndexPath_animated_(indexPath, True)
  # タップ時のセルのindex 情報から、配列要素のindex で、アイコン名を取得
  select_item = self.all_items[indexPath.row]

  print(f'select:\t{select_item}')

タップしたセルのアイコン名を console に出力します。

コメントアウトしているdeselectRowAtIndexPath_ は、以下を参照してください。
タップ時のハイライト挙動です。

また、UITableViewDelegate を使うことになったので:

sf_tableView.dataSource = self
sf_tableView.delegate = self

と、dataSource と同様に、delegate へもself とします。

backgroundColor 設定

確認用の色でガチャガチャしているので:

self.view.backgroundColor = UIColor.systemBackgroundColor()

端末の外観設定がライトかダークにより背景色を変更してくれます。

おわりに

大体 120 行ほどで、実装ができました。
今回は、テーブルに一覧を表示する簡単なアプリでしたが、ここから独自に実装をして改良してみてください。

例えば:

  • プレビューの表示
  • 一覧の検索用のバーを追加
  • clipboard にアイコン名をコピー

他のアプリ制作時に、SF Symbols をアイコンを使う場面でのリファレンスとしても機能しそうですね。

Rubicon-ObjC の作者が、briefcase というライブラリを制作しており、これを使うとストアへも公開できるとか。
私は、まだ検証していませんが。

実装が面倒でも、コードをコピーして貼り付けして実行。View を出して閉じるだけでも普段と違う端末の遊びをしているみたいで楽しいですよ。

私のリポジトリや X など、フィードバックもらえると嬉しいので、お気軽に声かけてください。

Discussion

ログインするとコメントできます