【準備編】#2: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】
はじめに
iPhone, iPad の無料アプリa-Shell を使って、UIKit を呼び出し、アプリをつくります。
今回の View の表示結果は、前回と変わりありません。1 ファイル(singleFileSample.py
)だったコードをパッケージ化し、煩雑さの回避、取り回しをよくするのが目的です。
今後訪れるアプリ開発においても、実装したいコードに集中できる利点もあります。
分割作業と同時にコードの説明をします。不明瞭な部分の解決に繋がれば幸いです。
全 3 部構成の第 2 部です
この記事を含め、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 へ参照させてください。
ディレクトリ構成
GitHub のリポジトリ は、以下の構成です。
[~a-Shell_Rubicon-ObjC_UIKitSamples]$ tree
.
├── README.md
├── (exampleSFSymbolsViewer.py: 次回)
├── pyrubicon
│ └── objc
│ ├── __init__.py
│ ├── api.py
│ ├── collections.py
│ ├── ctypes_patch.py
│ ├── eventloop.py
│ ├── runtime.py
│ └── types.py
├── rbedge
│ ├── __init__.py
│ ├── app.py
│ ├── enumerations.py
│ ├── functions.py
│ ├── lifeCycle.py
│ ├── objcMainThread.py
│ ├── pdbr.py
│ └── rootNavigationController.py
├── (singleFileSample.py: 前回)
└── withModuleToSample.py
rbedge
ディレクトリが、パッケージの機能を持ちます。singleFileSample.py
を分割したコードです。
pyrubicon
ディレクトリは『【非推奨】Rubicon-ObjC の手動インストール・インポート』の方法で格納しています。
rbedge
パッケージ
「rubicon 川の橋(bridge)の端(edge)」
として、rbedge
と命名してみました。別のパッケージ名でも問題はありません。
__init__.py
__version__ = '0.0.0'
import sys
import os
import ctypes
from pyrubicon.objc.api import ObjCClass, ObjCInstance
from pyrubicon.objc.runtime import Foundation
ObjCClass.auto_rename = True
#######################################################
# --- exception
#######################################################
# todo: from objc_util.py of Pythonista3
ExceptionHandlerFuncType = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
def NSSetUncaughtExceptionHandler(_exc: ExceptionHandlerFuncType) -> None:
_NSSetUncaughtExceptionHandler = Foundation.NSSetUncaughtExceptionHandler
_NSSetUncaughtExceptionHandler.restype = None
_NSSetUncaughtExceptionHandler.argtypes = [
ExceptionHandlerFuncType,
]
_NSSetUncaughtExceptionHandler(_exc)
def _objc_exception_handler(_exc):
exc = ObjCInstance(_exc)
with open(os.path.expanduser('~/Documents/_rubicon_objc_exception.txt'),
'w') as f:
import datetime
f.write(
'The app was terminated due to an Objective-C exception. Details below:\n\n%s\n%s\n'
% (datetime.datetime.now(), exc))
_handler = ExceptionHandlerFuncType(_objc_exception_handler)
NSSetUncaughtExceptionHandler(_handler)
#######################################################
__init__.py
は、空のファイルである事が多いですが、コード体に影響させたいものとして、以下 2 点設定しています。
ObjCClass.auto_rename
のTrue
連続で実行する際の class name 衝突エラーを回避します。
NSSetUncaughtExceptionHandler
関数: _handler = ExceptionHandlerFuncType(_objc_exception_handler)
NSSetUncaughtExceptionHandler(_handler)
Objective-C でのエラー情報を a-Shell の./Documents
ディレクトリへ_rubicon_objc_exception.txt
として吐き出します。
# アプリ本体のディレクトリ直下を指定
with open(os.path.expanduser('~/Documents/_rubicon_objc_exception.txt'), 'w') as f:
Main Thread 外での UI 更新や、配列の index 外の呼び出しなどを教えてくれます。
捕捉してくれるエラーは少ないですが、デバッグする手立てが少ない Rubicon-ObjC では小さな情報でもありがたいです。
Apple Developer Documents のリンクは以下となります:
enumerations.py
# 一部を抜き出し
from dataclasses import dataclass
class UIModalPresentationStyle:
automatic: int = -2
none: int = -1
fullScreen: int = 0
pageSheet: int = 1
formSheet: int = 2
currentContext: int = 3
custom: int = 4
overFullScreen: int = 5
overCurrentContext: int = 6
popover: int = 7
blurOverFullScreen: int = 8
@dataclass
class UIRectEdge:
none: int = 0
top: int = 1 << 0
left: int = 1 << 1
bottom: int = 1 << 2
right: int = 1 << 3
all: int = top | left | bottom | right
Enumeration Case をまとめています。class で要素をまとめ.
(ドットで)呼び出しをしたいので、Swift の表記で統一しています。
演算が必要(に、なりそう)な要素は、@dataclass
で定義しています。
しかし、Apple Developer Documentation は Enumeration Case の(Swift も Objective-C のページも)値が記載されていないこともあります。
その場合は、Rust の Apple frameworks バインディングの objc2
から source を確認しに行っています。
情報が古い可能性はありますが、Xamarin のドキュメントでも稀に見つかる場合があります。
enumerations.py
のコード全体は、こちらです。
functions.py
import ctypes
from pyrubicon.objc.api import ObjCInstance
from pyrubicon.objc.runtime import Foundation, Class
def NSStringFromClass(cls: Class) -> ObjCInstance:
_NSStringFromClass = Foundation.NSStringFromClass
_NSStringFromClass.restype = ctypes.c_void_p
_NSStringFromClass.argtypes = [Class]
return ObjCInstance(_NSStringFromClass(cls))
Function
をまとめている場所です。現在はNSStringFromClass
1 つのみです。
NSStringFromClass
は引数にした Class 名を文字列で返します。必須ではありませんが、今回は View の Life Cycle 確認のためprint
する目的で使用しています。
lifeCycle.py
import asyncio
#import logging
from pyrubicon.objc.eventloop import EventLoopPolicy
__all__ = [
'loop',
]
#logging.basicConfig(level=logging.DEBUG)
asyncio.set_event_loop_policy(EventLoopPolicy())
#loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
#loop.set_debug(True)
非同期の event loop が走ることで、a-Shell でクラッシュせずに View を表示できます。
なお、Pythonista3, Pyto は変数loop
が無くても View は表示されます。
私がasyncio
にわか勢なので、logging
ライブラリで状態を確認しようしているのがわかりますね。
iOSLifecycle()
ではない
変数loop
は、この後に説明するclass App
の中でループを走らせることになります。
class App:
# 中略
def main_loop(self) -> None:
loop.run_forever() # <- ここ
# loop.run_forever(lifecycle=iOSLifecycle()) <- としない
loop.close()
Rubicon-ObjC ドキュメントよると、iOS にはloop.run_forever(lifecycle=iOSLifecycle())
と、ありますがiOSLifecycle
は使いません。
a-Shell 上で動かしているので、Rubicon-ObjC ドキュメントの示している iOS とは違います。
Rubicon-ObjC ドキュメントの iOS は「そのアプリ単体(つまり、通常のアプリ挙動)」で、起動・終了を管理しますが、起動・終了は a-Shell 側が持っていることになります。
new
かget
か
new_event_loop
, get_event_loop
どちらを選んでも、エラーなく実行できます。
今のところget_event_loop
は、logging
でWarning
が出るのでnew_event_loop
としています。どっちが正解かはまだ整理できていません。
#loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
-
get_event_loop
-
new_event_loop
objcMainThread.py
from ctypes import byref, cast, Structure
import functools
from pyrubicon.objc.api import Block, ObjCClass, ObjCInstance
from pyrubicon.objc.runtime import libobjc, objc_block, objc_id
NSThread = ObjCClass('NSThread')
class struct_dispatch_queue_s(Structure):
pass # No _fields_, because this is an opaque structure.
_dispatch_main_q = struct_dispatch_queue_s.in_dll(libobjc, '_dispatch_main_q')
def dispatch_get_main_queue():
return ObjCInstance(cast(byref(_dispatch_main_q), objc_id))
libobjc.dispatch_async.restype = None
libobjc.dispatch_async.argtypes = [
objc_id,
objc_block,
]
def onMainThread(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if NSThread.isMainThread:
func(*args, **kwargs)
block = Block(functools.partial(func, *args, **kwargs), None)
libobjc.dispatch_async(dispatch_get_main_queue(), block)
return wrapper
Main Thread で処理させたい所に、関数onMainThread
をデコレータ@onMainThread
として指定します。
class App
のpresent
メソッド内で、rootViewController
のpresentViewController_animated_completion_
。
そして、UINavigationController
のinitWithRootViewController_
を Main Thread 処理する場面で使用しています。
class App:
# 中略
def present(self) -> None:
@onMainThread
def present_viewController(viewController: UIViewController,
style: int) -> None:
presentViewController = RootNavigationController.alloc(
).initWithRootViewController_(viewController)
presentViewController.setModalPresentationStyle_(style)
self.rootViewController.presentViewController_animated_completion_(
presentViewController, True, None)
present_viewController(self.viewController, self.modalPresentationStyle)
Main Thread 処理の方法として、他にも様々な方法があります:
解決策 2:
dispatch_async
を使う
を参考に今回は実装しています。
rootNavigationController.py
import ctypes
from pyrubicon.objc.api import ObjCClass
from pyrubicon.objc.api import objc_method
from pyrubicon.objc.runtime import send_super, SEL
from .lifeCycle import loop
from .enumerations import UIBarButtonSystemItem
from .functions import NSStringFromClass
UINavigationController = ObjCClass('UINavigationController')
UINavigationBarAppearance = ObjCClass('UINavigationBarAppearance')
UIBarButtonItem = ObjCClass('UIBarButtonItem')
class RootNavigationController(UINavigationController):
@objc_method
def dealloc(self):
# xxx: 呼ばない-> `send_super(__class__, self, 'dealloc')`
#print(f'- {NSStringFromClass(__class__)}: dealloc')
loop.stop()
#print('--- stop')
@objc_method
def loadView(self):
send_super(__class__, self, 'loadView')
#print(f'{NSStringFromClass(__class__)}: loadView')
navigationBarAppearance = UINavigationBarAppearance.new()
navigationBarAppearance.configureWithDefaultBackground()
#navigationBarAppearance.configureWithOpaqueBackground()
#navigationBarAppearance.configureWithTransparentBackground()
navigationBar = self.navigationBar
navigationBar.standardAppearance = navigationBarAppearance
navigationBar.scrollEdgeAppearance = navigationBarAppearance
navigationBar.compactAppearance = navigationBarAppearance
navigationBar.compactScrollEdgeAppearance = navigationBarAppearance
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
#print(f'{NSStringFromClass(__class__)}: viewDidLoad')
self.delegate = self
@objc_method
def viewWillAppear_(self, animated: bool):
send_super(__class__,
self,
'viewWillAppear:',
animated,
argtypes=[
ctypes.c_bool,
])
#print(f'{NSStringFromClass(__class__)}: viewWillAppear_')
@objc_method
def viewDidAppear_(self, animated: bool):
send_super(__class__,
self,
'viewDidAppear:',
animated,
argtypes=[
ctypes.c_bool,
])
#print(f'{NSStringFromClass(__class__)}: viewDidAppear_')
@objc_method
def viewWillDisappear_(self, animated: bool):
send_super(__class__,
self,
'viewWillDisappear:',
animated,
argtypes=[
ctypes.c_bool,
])
#print(f'{NSStringFromClass(__class__)}: viewWillDisappear_')
@objc_method
def viewDidDisappear_(self, animated: bool):
send_super(__class__,
self,
'viewDidDisappear:',
animated,
argtypes=[
ctypes.c_bool,
])
#print(f'{NSStringFromClass(__class__)}: viewDidDisappear_')
@objc_method
def didReceiveMemoryWarning(self):
send_super(__class__, self, 'didReceiveMemoryWarning')
print(f'{NSStringFromClass(__class__)}: didReceiveMemoryWarning')
@objc_method
def closeButtonTapped_(self, sender):
self.dismissViewControllerAnimated_completion_(True, None)
@objc_method
def navigationController_willShowViewController_animated_(
self, navigationController, viewController, animated: bool):
closeButtonItem = UIBarButtonItem.alloc().initWithBarButtonSystemItem(
UIBarButtonSystemItem.close,
target=navigationController,
action=SEL('closeButtonTapped:'))
visibleViewController = navigationController.visibleViewController
navigationItem = visibleViewController.navigationItem
navigationItem.rightBarButtonItem = closeButtonItem
UINavigationController
のサブクラスRootNavigationController
を定義しています。
サブクラス化する追加の機能として:
- 自分自身の View を閉じる close ボタン
-
navigationController_willShowViewController_animated_
closeButtonTapped_
-
- event loop を止める
-
dealloc
loop.stop
-
普通のアプリとは違い、a-Shell で呼び出し a-Shell へ戻すので、その機能をRootNavigationController
に持たせています。
View の Life Cycle
withModuleToSample.py
の Class MainViewController
とも重複しますが、View を表示する Life Cycle 系のメソッドを列挙しています。不要であればコードから削除しても問題ありません。
RootNavigationController
の場合:
viewWillAppear_
viewDidAppear_
viewWillDisappear_
viewDidDisappear_
didReceiveMemoryWarning
loadView
読み込まれた時点で、UINavigationBarAppearance
により NavigationBar の見た目を設定しています。
class RootNavigationController(UINavigationController):
# 中略
@objc_method
def loadView(self):
send_super(__class__, self, 'loadView')
navigationBarAppearance = UINavigationBarAppearance.new()
# configur の選択で表示形態が変わる
navigationBarAppearance.configureWithDefaultBackground()
#navigationBarAppearance.configureWithOpaqueBackground()
#navigationBarAppearance.configureWithTransparentBackground()
navigationBar = self.navigationBar
navigationBar.standardAppearance = navigationBarAppearance
navigationBar.scrollEdgeAppearance = navigationBarAppearance
navigationBar.compactAppearance = navigationBarAppearance
navigationBar.compactScrollEdgeAppearance = navigationBarAppearance
configureWithDefaultBackground
の他に:
configureWithOpaqueBackground
configureWithTransparentBackground
も、あるので、表示の違いを確認してみるのもいいでしょう。
NavigationBar に表示させるタイトルは、withModuleToSample.py
の Class MainViewController
のメソッド内。
もしくは、MainViewController
インスタンス生成後に設定します。
viewDidLoad
class RootNavigationController(UINavigationController):
# 中略
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
# `navigationController_willShowViewController_animated_` メソッドと連携
self.delegate = self
self.delegate
をself
としています。
これは、RootNavigationController
クラスで定義した delegate のメソッドを繋ぐためです。
繋ぐメソッドは、navigationController_willShowViewController_animated_
です。
RootNavigationController
は、UINavigationController
のサブクラスです。
UINavigationControllerDelegate
Protocol に準拠しています。そのため、navigationController:willShowViewController:animated:
を delegate 経由で呼び出すことができます。
navigationController_willShowViewController_animated_
とcloseButtonTapped_
class RootNavigationController(UINavigationController):
# 中略
@objc_method
def closeButtonTapped_(self, sender):
self.dismissViewControllerAnimated_completion_(True, None)
@objc_method
def navigationController_willShowViewController_animated_(
self, navigationController, viewController, animated: bool):
# ボタンアイコンを作成
closeButtonItem = UIBarButtonItem.alloc().initWithBarButtonSystemItem(
UIBarButtonSystemItem.close, # 24: from .enumerations import UIBarButtonSystemItem
target=navigationController,
# タップ時の呼び出すメソッドを指定
action=SEL('closeButtonTapped:'))
# 最前面のものを取得
visibleViewController = navigationController.visibleViewController
navigationItem = visibleViewController.navigationItem
# 右肩にボタンを設置
navigationItem.rightBarButtonItem = closeButtonItem
RootNavigationController
が乗った View が表示される直前、右肩に close ボタンを設置します。
_willShowViewController_animated_
によって表示される ViewController のnavigationItem
として指定します。
close ボタン
close ボタン(UIBarButtonItem
)生成時、タップされた action を selector(SEL
) で指定します。
指定方法は、インスタンスメソッドのcloseButtonTapped_
をSEL('closeButtonTapped:')
と、文字列化し_
を:
に書き換えるだけです。
closeButtonTapped_
は独自のインスタンスメソッドなので、closeButtonTapped_
以外の他の名称でも問題はありません。
close ボタンが押されたら、self.dismissViewControllerAnimated_completion_(True, None)
が発動します。
そして、a-Shell から呼び出した全ての View を閉じます。
dealloc
でloop
を止める
close ボタンが押されたことにより、全ての View が閉じられます。
そのタイミングで、dealloc
が呼ばれるので、走っているloop
をstop
します。
そして、App.main_loop
のloop.run_forever
が終了しloop.close
することで event loop を終了させることができます。
dealloc
では、send_super
は使いません。Rubicon-ObjC がよしなに処理をしてくれます。
app.py
from pyrubicon.objc.api import ObjCClass
from .lifeCycle import loop
from .enumerations import (
UISceneActivationState,
UIModalPresentationStyle,
)
from .objcMainThread import onMainThread
from .rootNavigationController import RootNavigationController
UIApplication = ObjCClass('UIApplication')
UIViewController = ObjCClass('UIViewController') # todo: アノテーション用
class App:
sharedApplication = UIApplication.sharedApplication
__objectEnumerator = sharedApplication.connectedScenes.objectEnumerator()
while (__windowScene := __objectEnumerator.nextObject()):
if __windowScene.activationState == 0:
break
rootViewController = __windowScene.keyWindow.rootViewController
def __init__(self,
viewController: UIViewController,
modalPresentationStyle: UIModalPresentationStyle
| int = UIModalPresentationStyle.pageSheet):
self.viewController = viewController
# xxx: style 指定を力技で確認
_automatic = UIModalPresentationStyle.automatic # -2
_blurOverFullScreen = UIModalPresentationStyle.blurOverFullScreen # 8
_pageSheet = UIModalPresentationStyle.pageSheet # 1
self.modalPresentationStyle = modalPresentationStyle if isinstance(
modalPresentationStyle, int
) and _automatic <= modalPresentationStyle <= _blurOverFullScreen else _pageSheet
def present(self) -> None:
@onMainThread
def present_viewController(viewController: UIViewController,
style: int) -> None:
presentViewController = RootNavigationController.alloc(
).initWithRootViewController_(viewController)
presentViewController.setModalPresentationStyle_(style)
self.rootViewController.presentViewController_animated_completion_(
presentViewController, True, None)
present_viewController(self.viewController, self.modalPresentationStyle)
self.main_loop()
def main_loop(self) -> None:
loop.run_forever()
loop.close()
a-Shell のrootViewController
と、実装した View を繋ぎます。
RootNavigationController
をself.viewController
の NavigationController とすることで、close ボタンを持った View とします。
Controller の接続は、Main Thread 上で行う必要があります。デコレータ@onMainThread
を関数present_viewController
に指定し、その中で接続をします。
rootViewController
の取得
以前は、sharedApplication
のwindows
から keyWindow
経由で取得をしていました。しかし、iOS 15.0 より非推奨となったので、connectedScenes
からwindowScene
経由で取得するようにしています。
様々な取り方があるので、状況に合わせて適宜使い分けできそうです。
modalPresentationStyle
a-Shell から表示させる style で、型はint
です。全画面の表示指定(.fullScreen
)で、エラーが発生し a-Shell に戻れない事態を考慮して、デフォルトは.pageSheet
と、しています。
値の参照先は、Rust の objc2
です:
pdbr.py
Python のdir
のような使い方をします。
今回は使用していませんが、オブジェクトが持っている要素などを確認するのに使います。console に吐き出された結果から、Apple Developer Documentation で検索をしたり、オブジェクトの状態や親の Class を確認したりします。
is_merge_methods
第 2 引数の第 2 引数をbool
で指定することで、Class ごとに分けるか、まとめて表示するか選ぶ事ができます。デフォルトは、False
で、Class ごとに表示されます。
継承が多く、アルファベット順で探したい場合には、True
にすることで、まとめた一覧が表示されます。
pdbr.state(オブジェクト, is_merge_methods:bool)
メインとなる ViewController
rbedge
とpyrubicon
を使って、実装をします。
withModuleToSample.py
import ctypes
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 = ObjCClass('UIViewController')
class MainViewController(UIViewController):
@objc_method
def dealloc(self):
# xxx: 呼ばない-> `send_super(__class__, self, 'dealloc')`
print(f'\t - {NSStringFromClass(__class__)}: dealloc')
@objc_method
def loadView(self):
send_super(__class__, self, 'loadView')
print(f'\t{NSStringFromClass(__class__)}: loadView')
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
print(f'\t{NSStringFromClass(__class__)}: viewDidLoad')
self.navigationItem.title = NSStringFromClass(__class__)
@objc_method
def viewWillAppear_(self, animated: bool):
send_super(__class__,
self,
'viewWillAppear:',
animated,
argtypes=[
ctypes.c_bool,
])
print(f'\t{NSStringFromClass(__class__)}: viewWillAppear_')
@objc_method
def viewDidAppear_(self, animated: bool):
send_super(__class__,
self,
'viewDidAppear:',
animated,
argtypes=[
ctypes.c_bool,
])
print(f'\t{NSStringFromClass(__class__)}: viewDidAppear_')
print('\t↓ ---')
@objc_method
def viewWillDisappear_(self, animated: bool):
print('\t↑ ---')
send_super(__class__,
self,
'viewWillDisappear:',
animated,
argtypes=[
ctypes.c_bool,
])
print(f'\t{NSStringFromClass(__class__)}: viewWillDisappear_')
@objc_method
def viewDidDisappear_(self, animated: bool):
send_super(__class__,
self,
'viewDidDisappear:',
animated,
argtypes=[
ctypes.c_bool,
])
print(f'\t{NSStringFromClass(__class__)}: viewDidDisappear_')
@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
print('--- run ---')
main_vc = MainViewController.new()
#presentation_style = UIModalPresentationStyle.fullScreen
presentation_style = UIModalPresentationStyle.pageSheet
app = App(main_vc, presentation_style)
app.present()
print('--- end ---')
パッケージにまとめることにより、すっきりしました。rootNavigationController
の時と同様に、View を表示する Life Cycle 系のメソッドは不要であれば、削除してしまって問題ありません。
実行すると、第 1 部と同じ View が表示されます。次の第 3 部では、ここの部分にあたるファイルにコードを書いていきます。
おまけの補足: Rubicon-ObjC での Function 定義方法
NSStringFromClass
を事例にします。
import ctypes
from pyrubicon.objc.api import ObjCInstance
from pyrubicon.objc.runtime import Foundation, Class
def NSStringFromClass(cls: Class) -> ObjCInstance:
_NSStringFromClass = Foundation.NSStringFromClass
_NSStringFromClass.restype = ctypes.c_void_p
_NSStringFromClass.argtypes = [Class]
return ObjCInstance(_NSStringFromClass(cls))
なお、わざわざ関数で、囲む必要はありません。開いたかたちでも可能ですが:
# 事前に定義しておいて
NSStringFromClass = Foundation.NSStringFromClass
NSStringFromClass.restype = ctypes.c_void_p
NSStringFromClass.argtypes = [Class]
# 使用場面で呼び出す
str_from_class = ObjCInstance(NSStringFromClass(Class名を取りたいClass))
パッケージの管理として、見通しや利便性の観点でまとめています。
ドキュメントを参照する
Class であれば、ObjCClass({class名})
宣言をするだけで問題ありません。
NSStringFromClass の場合はFunction
なので、自分で実装する部分があります。まず最初に注目する点は赤枠で囲っている、Function
と、Foundation
です。
使用する Framework を確認
Foundation は Framework です。幸いにも Rubicon-ObjC ではrubicon.objc.runtime.Foundation
で定義されているので、import
できます。
from pyrubicon.objc.runtime import Foundation
他に Rubicon-ObjC で、libc
, libobjc
は定義されています:
-
libc
| rubicon.objc.runtime — Low-level Objective-C runtime access - Rubicon 0.5.0 -
libobjc
| rubicon.objc.runtime — Low-level Objective-C runtime access - Rubicon 0.5.0
それ以外の Framework は、load_library
を使い、自身で load する必要があります。
型を確認し、指定
続いて引数の型と、返り値の型を確認しrestype
とargtypes
へ定義します。
NSString * NSStringFromClass(Class aClass);
ここからは、トライアンドエラーで検証を繰り返していくだけです。
引数型がClass
だか、ObjCClass
なのか。とりあえずobjc_id
を指定して結果を見るか。など、(私は本格的に Objective-C や Swift を書いたことがないので)考えられる型を入れて検証します。
_NSStringFromClass.restype = ctypes.c_void_p
_NSStringFromClass.argtypes = [Class]
return ObjCInstance(_NSStringFromClass(cls))
公式が正義
Rubicon-ObjC で実装するにあたり、解説記事ブログも参考になりますが、Apple Developer Documentation で調べないと必要情報を得ることは難しい場面が多いです。
おわりに
a-Shell アプリ自身と、Python 実装での ViewController 連携ができました。繋ぎ込みの部分は多少 hacky になってしまいましたが、各コードの役割を掴んでもらえていたら嬉しいです。
次回はこのパッケージも活用し、オリジナルなアプリを開発します。Xcode は、Storyboard とコード両方を使う開発になりますが、a-Shell では全てコードベースで開発をします。
Discussion