【制作編】#3: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】
はじめに
a-Shell で、SF Symbols の一覧を出す簡単なアプリをつくります。
今まで紹介したパッケージライブラリを使い、端末内にある SF Symbols データの取り出し、表示までをハンズオン形式で実装していきます。
全 3 部構成の第 3 部です
この記事を含め、3 部構成で解説をします。
記事内の掲載コードは、コピーして貼り付ければ動くようにしています。
-
【導入編】#1: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】
- 1 ファイルのみの簡易な実装
- a-Shell 実行の大まかな流れ
-
【準備編】#2: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】
- アプリ制作のための環境整備
- 1 ファイルだったコードをパッケージへ分割
- パッケージ内のコード説明
-
【制作編】#3: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】
- (今回はここ)
- Rubicon-ObjC を使った実装の流れ
- Swift, Objective-C のコートドを Rubicon-ObjC へ落とし込む方法
3 つの記事を通したコードは、GitHub で公開しています。
a-Shell はファイルアプリ経由の参照が可能です
a-Shell でのコード編集が大変、または既に実行したいデータがある場合は、アプリ外ディレクトリにアクセスするコマンドpickFolder
や、Git (導入方法は割愛)を使った開発もできます。
こちらの リンク先 より、.zip
を直接ダウンロードできます。
「とりあえず、動かしてみたい」という方は、ダウンロード後ファイルアプリで.zip
を解凍し、解凍先ディレクトリを a-Shell へ参照させてください。
ハンズオン
空の新規ファイルから、順を追って実装していきます。最終的には公開しているリポジトリ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
で、そのまま出してみましょう。
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()
navigationItem.title
をインスタンス生成後に指定しています。NSStringFromClass
で、Class UIViewController
より文字列を取得します。
SFSymbolsViewController
継承したクラスClass UIViewController
を継承し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_super
とobjc_method
をimport
に追加しています。また、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)
navigationItem.title
の指定
SFSymbolsViewController
インスタンス生成後に、タイトルを指定しない場合。viewDidLoad
実行時のself.navigationItem.title
はNone
となります。
そこで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 継承をしたので、title
をNSStringFromClass
で指定した場合、出力内容が一部変わります。
連続で実行すると、実行回数ごとに末尾の数値がインクリメントされていきます。
-
初回実行
-
2 回目実行
UITableView
の実装
何もしない今回は、SF Symbols の一覧を表示させたいので、UITableView
を使います。
UITableViewController
を使う方法もあります。
しかし、今回は今後の拡張性も考慮し、UIViewController
のview
にUITableView
を乗せる方針にします。
真ん中あるシアン色の矩形が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()
initWithFrame
とstyle
UITableView
のインスタンスを生成するメソッドです。
style
指定は、rbedge.enumerations
内で定義しています(サンプルリポジトリをコピーしている場合)。
今回は、plain
を指定します。
# rbedge にenumerations.py がない場合は
# このクラスを追加してください
class UITableViewStyle:
plain: int = 0
grouped: int = 1
insetGrouped: int = 2
もし、UITableView
のinit
メソッドがわからない場合。
pdbr.state(UITableView.alloc())
しかし、ものすごい量のメソッドたちが出てくるので、まずはドキュメントを探すのをお勧めします。
UIColor
で色付け
何もしないUITableView
なので、今のところ背面に色をつけてaddSubview_
できているか確認します。
色はどれでもいいので、先ほどのpdbr.state
で、使える色を見てみます。
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 のtranslatesAutoresizingMaskIntoConstraints
をFalse
にします。
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 で実装していると登場しないメソッドが出てきます。
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_tableView
もobjc_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 点が必要です:
- 対応するメソッドの実装
- Protocol
UITableViewDataSource
- Protocol
-
sf_tableView
のdataSource
へ通知
今回、最小限のメソッドで実装します。「必須」のメソッドです:
-
tableView:numberOfRowsInSection:
| Apple Developer Documentation- 表示させるセルの総数の提示
- (正確には、section の row の数ですが、詳細はドキュメント参照)
- 表示させるセルの総数の提示
-
tableView:cellForRowAtIndexPath:
| Apple Developer Documentation- 各セルごとの設定
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 SFSymbolsViewController
にUITableViewDataSource
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
より簡単に取得できます。
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_
メソッド
UIImage
のsystemImageNamed:
へ、文字列で 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
をして、完成とします。
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
の全画面表示
viewDidLoad
でNSLayoutConstraint
を使って位置とサイズを指定していました。
widthAnchor
とheightAnchor
の値を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