PythonistaでRubicon-ObjCを使う
PythonistaでObjective-Cの機能を使うときは、基本的にはobjc_util
を使うと思います。
しかし、Rubicon-ObjCはより使いやすく、できることもかなり増えています。
ここでは、PythonistaでRubicon-ObjCを使うための基本的な方法を紹介します。
Rubicon-ObjCとは
Pythonistaでは、標準のobjc_util
モジュールを使うことで、Objective-C(iOSの内部で使われているプログラミング言語)のクラスにアクセスでき、ui
モジュールなどには用意されていない機能も使用することができます。
このobjc_util
モジュールは、Rubicon-ObjCというライブラリの昔のバージョンを元にしたものなのだそうです。
Rubicon-ObjCは、PytoというPythonistaに似たアプリでは標準で使われていて、現在のバージョンはobjc_util
よりもかなり使い勝手がいいです。
また、個人的にはSwiftに近い書き方ができるので楽だと感じています。Rubicon-ObjCの特徴としては、例えば以下のようなものがあります。
- クラスの定義・継承をPythonクラスとして直接行える
- プロトコルがPythonオブジェクトとして扱え、自作もできる
- ポインタから
ObjCInstance
への自動変換がある - プロパティには
()
を付ける必要がない -
send_super
が使える
あと、objc_utilのObjCBlock
は、Pythonista 3.4で使うとクラッシュしてしまいますが、Rubicon-ObjCでは使用できます。
仕組み
Pythonでは、ctypes
モジュールを用いて、C言語の機能へアクセスできます。macOSやiOSでは、Cから間接的にObjective-Cクラスなどの操作ができ、その機能をPythonで使いやすくしたのがRubicon-ObjCです。
rubicon.objc.runtime.libc
は、ctypes.CDLL
を使って/usr/lib/libc.dylib
のライブラリを読み込んだものであり、rubicon.objc.runtime.libobjc
は、同様に/usr/lib/libobjc.dylib
のライブラリを読み込んだものです。
libobjc
には、例えばobjc_allocateClassPair
(Objective-Cのクラスを割り当てる関数)などの、Objective-Cランタイムにアクセスする関数が含まれており、これらを使ってよりPythonで使いやすいような仕組みが作られています。
Pythonistaに導入
Rubicon-ObjCはStaShなどでインストールできます。(詳しい使い方はここでは説明しません)
$ pip install rubicon-objc
インポートしてみてエラーが出る場合は
$ pip install setuptools_scm
こちらもインストールしましょう。
ちなみに、こちらのPythonista3 pip configration toolもおすすめです。
使用方法
ここからは、基本的な使い方を挙げていきます。より詳細な使い方は、ドキュメントを参照してください。
Objective-Cクラスの使用
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = NSObject.new;
これはRubicon-ObjCでは次のようになります。
from rubicon.objc import ObjCClass
NSObject = ObjCClass("NSObject")
obj1 = NSObject.alloc().init()
obj2 = NSObject.new() # alloc().init()と同様
ObjCClass
を使用することで、Objective-Cのクラスを取得できます。
取得したクラスのメソッドは、Pythonのメソッドと同じように実行できます。
ただし、バージョン0.4.8現在は、このような書き方はできません。
NSObject()
NSObject
などのいくつかの基本的なクラスは、from rubicon.objc import NSObject
のようにインポートすることもできます。
メソッドの呼び出しと引数、プロパティ
Rubicon-ObjCでは、基本的にObjective-Cメソッド(セレクタ)の:
を_
に置き換えます。
MyClass *obj2 = [obj someMethod:value1 arg2:value2];
これは次のようになります。
obj2 = obj.someMethod_arg2_(value1, value2)
または
obj2 = obj.someMethod(value1, arg2=value2)
後者のほうがよりPythonらしい書き方になっていて、メソッド名が長くなりにくく、改行がしやすいのでおすすめです。
プロパティを使う場合は、()
を付ける必要はありません。
var1 = obj.someProperty # プロパティを取得
obj.someProperty = var2 # プロパティに代入
Objective-Cクラスの定義
Pythonistaでクラスを定義する場合、注意点があります。
Pythonistaでは、Objective-Cランタイムがアプリを立ち上げ直さない限りリセットされません。そのため、同じPythonファイルを複数回実行すると、クラスの定義が重複してしまい、エラーになってしまいます。
ObjCClass.auto_rename
をTrueにしておくことで、クラス定義が重複した場合、Objective-Cランタイム内の名前を自動で変更するようにできます。(例えば、MyObject
はMyObject_2
になります。)
ObjCClass.auto_rename = True
Rubicon-ObjCでは、Pythonのクラスを定義するようにObjective-Cのクラスを定義できます。
class MyClass(NSObject):
... # メソッド、プロパティなど
Objective-Cのクラスを定義する場合は、必ずNSObject
かそのサブクラスを指定する必要があります。また、複数のクラスを親クラスとして指定することはできません。
定義時に、キーワード引数としてauto_rename=True
を設定すれば、ObjCClass.auto_rename
がFalse
になっていても、重複したクラス名が許容されるようになります。
class MyClass(NSObject, auto_rename=True):
...
クラス定義の際に指定するauto_rename
は、デフォルトではNone
になっており、True
またはFalse
が指定されている場合のみその値が優先され、None
の場合はObjCClass.auto_rename
が優先されます。
メソッドの定義
メソッドの定義は、objc_method
やobjc_classmethod
を用いて行います。
class MyClass(NSObject):
@objc_method
def someMethodWithArg1_Arg2_(self, arg1, arg2):
...
@objc_classmethod
def someClassMethod(cls):
...
obj = MyObject.new()
obj.someMethod("message")
MyObject.someClassMethod("message")
objc_util
とは違って、_cmd
引数は必要なく、self = ObjCInstance(_self)
のようにPythonオブジェクトに変換する必要もありません。
ただし、NSInteger
やCGRect
などは、Objective-Cのオブジェクトではなく、Cの型にあたるので、このように特別にアノテーションを付ける必要があります。
@objc_method
def someMethodWithInteger_(self, integer: NSInteger) -> NSInteger:
...
何も指定していないと、objc_id
の扱いになります。
id
は、Objective-Cオブジェクト全てを示す型のようなものです。
実際のPythonの型とは違いますが、この方法で型を簡単に指定できるのはRubicon-ObjCの利点の一つです。
[super init]
したい場合は、rubicon.objc.runtime.send_super
を使います。
@objc_method
def someMethod(self):
send_super(__class__, self, "someMethod")
...
基本的には第一引数に__class__
、第二引数にself
を指定します。第三引数は、メソッドのセレクタを示す文字列です。
引数を渡したり、引数の型を指定しなければならない場合は、第四引数以降に渡します。
@objc_method
def someMethodWithArg1_Arg2(self, arg1, arg2):
send_super(__class__, self, "someMethodWithArg1:Arg2:", arg1, arg2)
...
イニシャライザの実装
イニシャライザは次のように実装できます。
class MyClass(NSObject):
@objc_method
def init(self):
send_super(__class__, self, "init")
...
return self
@objc_method
def initWithSomeArg_(self, arg):
self.init()
...
return self
obj1 = MyObject.new()
obj2 = MyObject.alloc().initWithSomeArg(value)
Pythonの__init__
と違って、return self
が必要です。書かないとNone
が帰ってきてしまいます。
プロパティの定義
rubicon.objc.api.objc_property
を使用することで、Objective-Cのプロパティを指定できます。
class MyClass(NSObject):
someProperty1 = objc_property()
someProperty2 = objc_property(NSInteger) # 型を指定
someProperty3 = objc_property(weak=True) # 弱参照
@objc_method
def someMethod(self):
print(self.someProperty1) # None
self.someProperty2 = 3
print(self.someProperty2) # 3
第一引数vertype
にはプロパティの型を、第二引数weak
には弱参照にするかどうかを指定します。
プロパティはメソッド内から通常のプロパティのようにアクセス可能です。通常、クラス下に直接変数を書くと、Pythonではクラス変数になってしまいますが、Rubicon-ObjCではインスタンスプロパティになります。
また、someProperty
とsetSomeProperty
のような名前のメソッドを定義することで、プロパティのように扱われるようになり舞す。
プロトコルの使用
rubicon.objc.api.ObjCProtocol
を使用することで、プロトコルに関する様々な操作ができます。
NSCopying = ObjCProtocol("NSCopying")
このように、クラスと同様に取得できます。
クラスの定義時にprotocols=(プロトコル1, プロトコル2, ...)
とすることで、プロトコルに準拠させることができます。
class MyClass(NSObject, protocols=(NSCopying,)):
...
プロトコルの定義
クラスの定義と同様に、Pythonistaではauto_rename
を指定する必要があります。
ObjCProtocol.auto_rename = True
プロトコルを定義する際も、Pythonクラスの定義を使用して行います。
class MyProtocol(metaclass=ObjCProtocol):
...
このようにすることで、プロトコルを定義できます。他のプロトコルを継承する場合は、次のようにします。
class MyProtocol(NSObjectProtocol):
...
プロトコルの場合は、複数継承することができます。
ブロックの使用
Rubicon-ObjCでは、rubicon.objc.api.ObjCBlock
が、ブロックのPythonオブジェクトとなっています。
ObjCBlock
は、通常の関数のように実行可能です。
block = obj.someMethodReturnsBlock()
block(arg1, arg2)
自分で定義する際には、基本的には通常の関数を定義するだけで問題ありません。
def some_handler() -> None:
...
obj.someMethodWithBlock(some_handler)
こうすることで、メソッドに関数が渡された際、自動でブロックに変換されます。
手動で最初からブロックとして定義する場合は、rubicon.objc.api.Block
を使用してください。
@Block
def some_handler() -> None:
...
また、Cの型のブロックは、ObjCBlock
に変換可能です。定義したメソッドにcompletionHandler
が渡されるときなどは、自分で引数と返り値の型を設定する必要があるようです。
block = ObjCBlock(block_ptr, None, NSInteger)
block(10)
第二引数にはブロックの返り値の、第三引数以降にはブロックの引数の型を指定します。
メインスレッド
objc_util
では、on_main_thread
を使うことで、メインスレッドで関数を実行できます。
iOSアプリでは、メインスレッド以外でUIの更新を行うことは禁止されているため、これを使わなければ、UIKit
の機能を使う際にクラッシュしてしまいます。
しかし、この機能はRubicon-ObjCにはありません。
objc_util.on_main_thread
をそのまま使う
解決策1: import objc_util
@objc_util.on_main_thread
def main():
... # ViewControllerの表示など
if __name__ == "__main__":
main()
このようにするだけでなんの問題もありません。
が、せっかくRubicon-ObjCを使っているのでそっちで実装したいと個人的には思います。
dispatch_async
を使う
解決策2: これは、ドキュメントで紹介されている方法です。
dispatch_async
を使った方法は、Objective-Cでは最も標準的なもののようですが、メインスレッドを取得するために使用するdispatch_get_main_queue
はインライン関数として定義されているため、ctypes
から読み込むことができず、自分で定義しないといけません。
from ctypes import byref, cast, Structure
import functools
from rubicon.objc import Block, objc_block, objc_id, ObjCClass, ObjCInstance
from rubicon.objc.runtime import libobjc
class struct_dispatch_queue_s(Structure):
pass
_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)
NSThread = ObjCClass("NSThread")
def main_actor(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
if __name__ == "__main__":
@main_actor
def func(string):
... # ViewControllerの表示など
NSOperationQueue
を使う
解決策3: こちらは、NSOperationQueue
を使った方法です。NSOperation
を継承して処理を書きます。
NSOperation = ObjCClass("NSOperation")
NSOperationQueue = ObjCClass("NSOperationQueue")
class MainOperation(NSOperation):
@objc_method
def main(self):
send_super(__class__, self, "main")
... # ViewControllerの表示など
if __name__ == "__main__":
operation = MainOperation.new()
queue = NSOperationQueue.mainQueue
queue.addOperation(operation)
queue.waitUntilAllOperationsAreFinished()
performSelectorOnMainThread:withObject:waitUntilDone:
を使う
解決策4: from rubicon.objc import NSObject, objc_method, SEL
class MyClass(NSObject):
@objc_method
def someMethod(self):
... # ViewControllerの表示など
if __name__ == "__main__":
obj = MyClass.new()
obj.performSelectorOnMainThread(SEL("someMethod"), withObject=None, waitUntilDone=True)
この方法は、objc_util.on_main_thread
の内部で使われている方法です。
サンプルコード
最後に、ViewControllerを表示するサンプルコードです。
from rubicon.objc import objc_method, ObjCClass, send_super
ObjCClass.auto_rename = True
NSOperation = ObjCClass("NSOperation")
NSOperationQueue = ObjCClass("NSOperationQueue")
UIApplication = ObjCClass('UIApplication')
UIColor = ObjCClass('UIColor')
UIViewController = ObjCClass('UIViewController')
class MyViewController(UIViewController):
@objc_method
def viewDidLoad(self):
send_super(__class__, self, "viewDidLoad")
self.view.backgroundColor = UIColor.blueColor
print("Viewが読み込まれました")
class MainOperation(NSOperation):
@objc_method
def main(self):
send_super(__class__, self, "main")
app = UIApplication.sharedApplication
rootVC = app.keyWindow.rootViewController
while childVC := rootVC.presentedViewController:
rootVC = childVC
mainVC = MyViewController.new().autorelease()
rootVC.presentViewController(mainVC, animated=True, completion=None)
if __name__ == "__main__":
operation = MainOperation.new()
queue = NSOperationQueue.mainQueue
queue.addOperation(operation)
queue.waitUntilAllOperationsAreFinished()
Discussion