objc_util — オブジェクティブC の APIs との橋渡しのユーティリティー

 objc_util モジュールは、Python からオブジェクティブC の API(Application Programming Interface:プログラムから iOS にアクセスするためのインターフェイス)群への「橋渡し」をします。

 (訳注 この項目については、私自身のオブジェクト指向の理解が不十分であることと、原文も少々乱れ気味であること、サンプルのスクリプトも動作が確認できていないことから、適切な訳になっていない可能性があります。お読みになる際には、是非、原文も併せてご確認をお願いします。)

 C言語と互換性のあるデータタイプを持つ python の関数ライブラリーである ctypes と、オブジェクティブCのランタイムライブラリーをベースに成り立っており、objc_util を使用することによって、既存のオブジェクティブCのクラスを Python から扱えるようになります。この時、Python のメソッド呼び出しをオブジェクティブCのメッセージに自動的に変換するような方法が用いられます。

  簡単な例として、次のオブジェクティブCのコードは、

UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]
[pasteboard setString:@"Hello Objective-C"];

 以下の Python のコードに翻訳することができます:

from objc_util import *
UIPasteboard = ObjCClass('UIPasteboard')

pasteboard = UIPasteboard.generalPasteboard()
pasteboard.setString_('Hello Objective-C')

 (訳注 上記のスクリプトを実行しただけでは、一見、何も起こらないように見えますが、クリップボードに'Hello Objective-C'という文字列を書き込んでいます。スクリプト実行後、例えば python のエディター内の画面の任意の場所をタップして「ペースト」すると、スクリプトが実際に実行されたことが分かります。)

 Python からオブジェクティブCのAPI を呼び出す際に、オブジェクティブCで直接コードを書くのに比べて極端に多量のコードを書く必要がないことが分かります。

 このスクリプトの実行にあたり、objc_util は、Python の関数呼出しをオブジェクティブCのメッセージに変換するため、objc_msgSned() などの適切な関数呼出しを行います。また、Python の一般的な型(例えば、上記のスクリプトでの文字列だけでなく、リストやディクショナリーも)については、このメソッドを呼び出すことによって、オブジェクティブCの同等の基本的な型(NSString, NSMutableArray, NSMutableDictionary など)に変換されます。

 オブジェクティブ Cのセレクター(メソッドの内部表現)から Python のメソッドの名前への変換は、極めて単純に行えます。基本的にはコロン「:」をアンダースコア「_」に置き換えるだけです。例えば、doFoo:withBar: というセレクターは、doFoo_withBar_ というメソッドになります。(注 末尾にもアンダースコアがつくことにご注意ください)

 多くの場合、もう少し「パイソンぽい」文法を使うこともできます。例えば、オブジェクティブCのセレクター名の一部としてのキーワードとなる引数を使って:

# From: UIColor *color = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0]
# ...to:
UIColor = ObjCClass('UIColor')
color = UIColor.colorWithRed(1.0, green=0.0, blue=0.0, alpha=1.0)
# or even:
color = UIColor.color(red=1.0, green=0.0, blue=0.0, alpha=1.0)

 この、より自然な文法は多くの場合動作しますが、メソッド名とキーワードとなる引数の組合せによっては、オブジェクティブCのメソッド呼出しに適切に変換できない場合があります。このような場合、キーワードとなる引数を使わずに、前述のようにコロンをアンダースコアに置き換える方法で呼出し変数を記述してください。

 (注 ctypes モジュールの使用により Python がクラッシュすることがあります。オブジェクティブCのメソッドを呼び出す場合には、正しいパラメーター型を選ぶよう慎重に行ってください。)

例 1 – 画面の明るさを設定する

 この単純な例は、デバイスの画面の明るさを設定するものです:(訳注 このスクリプトは現在のところ動作が確認できていません)

from objc_util import *

# 'Import' an Objective-C class (generate a proxy for the class):
UIScreen = ObjCClass('UIScreen')

# Call a class method, this is equivalent to `[UIScreen mainScreen]` in Objective-C:
screen = UIScreen.mainScreen()

# `screen` is now an ObjCInstance that wraps an Objective-C object, and forwards messages to it.

# The following call is equivalent to `[screen setBrightness:0.6]`:
screen.setBrightness(0.6)

例 2 – Music/iPod アプリの現在の曲にアクセスする

 このコードは、現在再生中の曲のアーティスト名と曲名をコンソールに表示します。(注 純正の Music アプリにのみ対応しており、サードパーティ製の音楽再生アプリでは使用できません)(訳注 このスクリプトは現在のところ動作が確認できていません)

from objc_util import *

MPMusicPlayerController = ObjCClass('MPMusicPlayerController')
player = MPMusicPlayerController.systemMusicPlayer()
now_playing = player.nowPlayingItem()
if now_playing:
    artist = now_playing.valueForProperty_('artist')
    title = now_playing.valueForProperty_('title')
    print('Now playing: %s -- %s' % (artist, title))
else:
    print('No music playing')
Creating New Objective-C Classes

 より高度なオブジェクティブCのAPIの使い方をする場合には、ランタイムのオブジェクティブCのクラスを独自に作る必要が出てきます。この場合には、2つの方法があります:

  •  一般的に使用されているデリゲートパターンを実行する。すなわち、iOS に組み込まれたオブジェクティブCのクラスのコールバックインターフェイスを適用する方法です。
  •  カスタマイズを目的として、オブジェクティブCのクラスのサブクラスに位置付ける。一例として、-drawRect: をオーバーライド(子クラスで親クラスのメソッドを書き換えること)するための UIView のサブクラス化があります。

 これを成し遂げるために、objc_util モジュールには create_objc_class() 関数があります。この関数はオブジェクトCランタイムを新しいクラスに割り当てたり記録したりするほか、上の例の組み込まれたクラスのように使用できるObjCClass オブジェクトをラップします。

 create_objc_class() を使ってオブジェクティブCのクラスを作るには、以下の事柄が必要です:

name – 生成するクラスの名前で文字列型です。文字、数字とアンダースコア(アンダーバー)だけが使用可能で、数字から始めることはできません。実際に生成されるクラスの名前は、これとは異なることがあることに注意して下さい。なぜなら、この名前のクラスは既に存在している場合があるからです。このような場合、新しい名前が自動的に選ばれ、デフォルトで debug パラメーターは True になります。逆に debug が False の場合には、存在しているクラスが返され、他の全てのパラメーターは無視されます。

superclass – 新しいクラスが継承するオブジェクティブCのクラスを決定する ObjCClass オブジェクトです。

methods – 新しいクラスのインスタンスのメソッドを生成するのに使用される関数のリストです。Python の関数からオブジェクティブCのメソッドを生成するためには、オブジェクティブCのランタイムは追加のメタデータを必要とします。セレクターの名前、戻り値の型、引数ごとの型が必要となります。create_objc_class() は、可能な範囲でこのメタデータを引き出そうと努めます。その手順について以下に説明します。

全てのオブジェクティブCのメソッドは、オブジェクティブCから呼び出される際には隠れてしまう、少なくとも2つのパラメーターを必要とします:_self はオブジェクティブCのオブジェクトそのものを示すポインタです。(これはObjCInstance のオブジェクトをラップするものではなく、そのため必要なら別途ラップする必要があることに注意が必要です)そして、_cmdはセレクターを示すポインタです。(通常は必要ありません)これら2つの「隠された」パラメーターの名前は重要ではありません。パラメーターがObjCInstance のオブジェクトではなく、「そのままの」ポインタとしてオブジェクティブCのメソッドに渡されることにご注意ください。但し、オブジェクトのパラメーターはラップするコーディングをすることで簡単に変換することができます。例えば、obj = ObjCInstance(_self)という形で。

classmethods (optional) – クラスのメソッドがなければ、メソッドと同様です。(必要になることは稀です)

protocols (optional) – メソッドの型変換のヒントとして使用する文字列のリストです。デリゲート(またはその他の)プロトコルを実行する場合、全てのメソッドの戻り値と引数の型を確実に正しく推論できるよう、プロトコルの名前(例えば、'UITableViewDataSource')を記述する必要があります。

 以下は、(iOS の標準メールシートを表示するために使用される)MFMailComposeViewController のデリゲートとして動作する、シンプルなクラスを生成する例です。デリゲートはこのクラスの使用に必要不可欠です。なぜなら、他の方法ではメールシートを取り除くことができないためです。(メールシートの使用が終了した際にデリゲートが通知され、それによりシートが開放されます。):

# - (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
def mailComposeController_didFinishWithResult_error_(_self, _cmd, controller, result, error):
    print('Mail composer finished')
    # Wrap the controller parameter in an `ObjCInstance`, so we can send messages:
    mail_vc = ObjCInstance(controller)
    # Set delegate to nil, and release its memory:
    mail_vc.setMailComposeDelegate_(None)
    ObjCInstance(_self).release()
    # Dismiss the sheet:
    mail_vc.dismissViewControllerAnimated_completion_(True, None)

methods = [mailComposeController_didFinishWithResult_error_]
protocols = ['MFMailComposeViewControllerDelegate']
MyMailComposeDelegate = create_objc_class('MyMailComposeDelegate', NSObject, methods=methods, protocols=protocols)

@on_main_thread
def show_mail_sheet():
    MFMailComposeViewController = ObjCClass('MFMailComposeViewController')
    mail_composer = MFMailComposeViewController.alloc().init().autorelease()
    # Use our new delegate class:
    delegate = MyMailComposeDelegate.alloc().init()
    mail_composer.setMailComposeDelegate_(delegate)
    # Present the mail sheet:
    root_vc = UIApplication.sharedApplication().keyWindow().rootViewController()
    root_vc.presentViewController_animated_completion_(mail_composer, True, None)

if __name__ == '__main__':
    show_mail_sheet()

API リファレンス

 クラス

class objc_util.ObjCClass(name)

 引数 name で指定したオブジェクティブCのラッパーです。オブジェクティブCのクラスのメソッドを代理で呼び出します。

 メソッドの呼び出しは、スクリプトの動作の過程でオブジェクティブCのメッセージに変換されます。メソッドの名前の中のアンダースコアがセレクターの名前の中のコロンに置き換えられ、オブジェクティブCのランタイムの中で、セレクターと引数がよりOSに近いレベルの objc_msgSend() 関数を呼び出すことに使われてこの変換が実行されるのです。

 例えば、NSDictionary.dictionaryWithObject_forKey_(obj, key) (Python) の呼び出しは、実際のところ、[NSDictionary dictionaryWithObject:obj forKey:key] (Objective-C)に変換されます。メソッドの呼び出しがオブジェクティブCのオブジェクトを返した場合、それは ObjCInstance をラップしたことになり、呼び出しを結びつけることができます。(ObjCInstance は同様のプロキシの仕組みを使用します)

 いくつかの共通に使用されたクラスはモジュールの要素です。(最後のリストを参照)その他については、クラスの名前を使って単に「インポート」することになります。例:

UIPasteboard = ObjCClass('UIPasteboard')

class objc_util.ObjCInstance(ptr)

 オブジェクティブCのオブジェクトへのポインターのラップです;オブジェクトへメッセージを送る際のプロキシとして動作します。

 メソッドの呼び出しは、スクリプトの動作の過程でオブジェクティブCのメッセージに変換されます。メソッドの名前の中のアンダースコアがセレクターの名前の中のコロンに置き換えられ、オブジェクティブCのランタイムの中で、セレクターと引数が objc_msgSend() 関数を呼び出すことに使われてこの変換が実行されるのです。

例えば、obj.setFoo_withBar_(foo, bar) (Python) の呼び出しは、obj setFoo:foo withBar:bar] (Objective-C)に翻訳されます。メソッドの呼び出しによりオブジェクティブCのオブジェクトが返される場合には、ObjCInstance にもラップされ、言わば結びつけられることになります。

  NSObject メソッドの記述を呼び出すことで ObjCInstance は__str__ and __repr__ を実行します。

 インスタンスが標準的なオブジェクティブCの集合型(NSArray, NSDictionary, NSSet)をラップする場合、標準的なPythonの集合と多くの点で似通った動作をします。イテレート(for..in)できますし、「some_dict['key'], some_array[3]...)」のように、角括弧を使ったキーやインデックスでアイテムにアクセスすることもできます。

class objc_util.ObjCBlock(func, restype=None, argtypes=None)

 ブロックのサポートはまだ実験的なものであることにご注意ください。ブロックを必要としない API を使用する選択肢がある場合には、そちらを強くお勧めします。

 ObjCBlock はオブジェクトCメソッドに対して、blocks (“closures”)を渡して使用することができます。前述の通り、この機能は実験的であり、可能であれば、通常はブロック不要のAPIを使用すべきです。戻り値や引数がないブロックの場合、Python の関数を渡せます。そして、ObjCBlock へ自動的に変換されます。そのほかの場合には、ブロックを作る際、戻り値と引数を明確に設定する必要があります。

 引数のあるブロックの例(NSMutableArrayをカスタマイズした比較関数でソート):

from objc_util import *
cheeses = ns(['Camembert', 'Feta', 'Gorgonzola'])
print(cheeses)

def compare(_cmd, obj1_ptr, obj2_ptr):
    obj1 = ObjCInstance(obj1_ptr)
    obj2 = ObjCInstance(obj2_ptr)
    # Sort the strings by length:
    return cmp(obj1.length(), obj2.length())

# Note: The first (hidden) argument `_cmd` is the block itself, so there are three arguments instead of two.
compare_block = ObjCBlock(compare, restype=NSInteger, argtypes=[c_void_p, c_void_p, c_void_p])

sorted_cheeses = cheeses.sortedArrayUsingComparator_(compare_block)
print(sorted_cheeses)

 関 数

objc_util.autoreleasepool()

 NSAutoreleasePool のラッパーとして動作するコンテキストマネージャーです。 (オブジェクトCの @autoreleasepool {...} に似ています)

 使用例:

  with objc_util.autoreleasepool():

   # do stuff...

objc_util.create_objc_class(name, superclass=NSObject, methods=[], classmethods=[], protocols=[], debug=True)

 引数で与えられたメソッドを実行する新しい ObjCClass を生成して返します。

 セレクターの名前は関数の名前に応じて設定されます。関数の名前には、オプションとして、オブジェクティブCのクラスの名前を前につけることができます。例えば、これら2つの関数は、結果として同じセレクターの名前になります:

def MyClass_doSomething_withObject_(_self, _cmd, foo, bar):
    pass

def doSomething_withObject_(_self, _cmd, foo, bar):
    pass

 # これらの関数は両方とも 'doSomething:withObject:'というセレクターの名前になります。

 戻り値の型と引数の型を決めるため、 create_objc_class() は親クラスが同じセレクターのメソッドを持っているか確認します。同じセレクターのメソッドを持っている場合、親クラスのメソッドから型を継承します。この手法により、子クラスのメソッドにも親クラスの型を継承します。これでうまく行かない場合には、引数 protocols で渡した型が使われます。引数 protocols は文字列のリストです。例えば、 ['UIGestureRecognizerDelegate', 'UITableViewDataSource'] というような。これらの protocols は、同じセレクターのメソッドがあるか確認されます。この手法により、例えば、デリゲートプロトコルが設定されます。それとともに、これらの手法により多くの一般的な場合、型の情報が決められます。これらが上手く動作しない場合には、関数の引数として restype, argtypes, encoding を渡す事もできます。

 注 既に存在しているオブジェクティブCのクラスを使用する場合には、単純にObjCClass(name) を使ってリファレンスを取得して下さい。この関数は、新しいクラス生成します。例えば、オブジェクティブCのクラスの子クラスや、デリゲートプロトコルを設定します。

objc_util.load_framework(name)

 引数 name で指定したシステムフレームワークを読み込みます。(例えば、'SceneKit')

 得られるシステムフレームワークは、オブジェクトCのコードに相当します。[[NSBundle bundleWithPath:@"/System/Frameworks/.framework"] load]

objc_util.ns(obj)

 Python のオブジェクトを ObjC の同等のオブジェクトに変換します。すなわち、str => NSString, int/float => NSNumber, list => NSMutableArray, dict => NSMutableDictionary, bytearray => NSData, set => NSMutableSet という変換をします。リストやディクショナリー、集合型のような入れ子構造にも対応しています。既にObjCInstance のインスタンスになっているオブジェクトについては、変換せずそのまま返します。

 オブジェクティブCのメソッドがパラメーターとしてオブジェクトと推定される場合、Python オブジェクトのパラメーターは自動的にこの関数を使って変換され、例えば、Python の文字列は NSString としてオブジェクティブCのメソッドに渡すことができるようになります。

objc_util.nsurl(url_or_path)

 Python の文字列をNSURLのオブジェクトに変換します。(ObjCInstance でラップします)

 引数 url_or_path の文字列にコロン ‘:’ が含まれている場合、文字列はフルURLとして扱われ、+URLWithString: を使って NSURL に変換されます。それ以外の場合、+fileURLWithPath: を使ってファイルのURLが生成されます。

objc_util.nsdata_to_bytes(data)

 ObjCInstance でラップされた NSData オブジェクトを Python のバイト文字列に変換します。

objc_util.uiimage_to_png(img)

 ObjCInstance でラップされた UIImage オブジェクトを Python の PNG データで構成されたバイト文字列に変換します。

objc_util.on_main_thread(func)

 UIKit のメインスレッド上の関数(引数 func で指定)をデコレートする関数です。多くのオブジェクティブCの API、特に UIKit は、メインスレッドから呼び出す必要があります。一般的に、その他の関数をデコレートするために使われますが、例えば on_main_thread(my_function)(param1, param2) のように、特別にメインスレッドへの関数呼び出しを割り当てるために使用することもできます。

 以下はデコレーターの例です:

from objc_util import *

@on_main_thread
def post_notification(name):
    NSNotificationCenter = ObjCClass('NSNotificationCenter')
    center = NSNotificationCenter.defaultCenter()
    center.postNotificationName_object_(name, None)

# 全ての post_notification(...) の呼び出しは、on_the_thread で発生します。 

objc_util.sel(name)

 sel_registerName の便利なラッパーです。(Python の文字列をオブジェクティブCのセレクターに変換します)

 オブジェクティブCのクラスと構造体

 利便性を考えて、いくつかの共通して使われるオブジェクティブCのクラスや構造体は、モジュールレベルのオブジェクトとして使用可能になっています。そのため、以下のクラスと構造体については、明確にラップする必要はありません:

class objc_util.CGPoint

class objc_util.CGSize

class objc_util.CGVector

class objc_util.CGRect

class objc_util.CGAffineTransform

class objc_util.UIEdgeInsets

class objc_util.NSRange

class objc_util.NSDictionary

class objc_util.NSMutableDictionary

class objc_util.NSArray

class objc_util.NSMutableArray

class objc_util.NSSet

class objc_util.NSMutableSet

class objc_util.NSString

class objc_util.NSMutableString

class objc_util.NSData

class objc_util.NSMutableData

class objc_util.NSNumber

class objc_util.NSURL

class objc_util.NSEnumerator