🧳

【準備編】#2: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】

に公開

はじめに

iPhone, iPad の無料アプリa-Shell を使って、UIKit を呼び出し、アプリをつくります。

今回の View の表示結果は、前回と変わりありません。1 ファイル(singleFileSample.py)だったコードをパッケージ化し、煩雑さの回避、取り回しをよくするのが目的です。
今後訪れるアプリ開発においても、実装したいコードに集中できる利点もあります。

image

分割作業と同時にコードの説明をします。不明瞭な部分の解決に繋がれば幸いです。

全 3 部構成の第 2 部です

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

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

__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_renameTrue

連続で実行する際の 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

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

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 の中でループを走らせることになります。

例 app.py 内
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 側が持っていることになります。

newget

new_event_loop , get_event_loop どちらを選んでも、エラーなく実行できます。
今のところget_event_loop は、loggingWarning が出るのでnew_event_loop としています。どっちが正解かはまだ整理できていません。

#loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
  • get_event_loop

  • new_event_loop

objcMainThread.py

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 Apppresent メソッド内で、rootViewControllerpresentViewController_animated_completion_
そして、UINavigationControllerinitWithRootViewController_ を Main Thread 処理する場面で使用しています。

例 app.py 内
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

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.delegateself としています。
これは、RootNavigationController クラスで定義した delegate のメソッドを繋ぐためです。
繋ぐメソッドは、navigationController_willShowViewController_animated_ です。

RootNavigationController は、UINavigationController のサブクラスです。
UINavigationControllerDelegate Protocol に準拠しています。そのため、navigationController:willShowViewController:animated: を delegate 経由で呼び出すことができます。

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 を閉じます。

deallocloop を止める

close ボタンが押されたことにより、全ての View が閉じられます。
そのタイミングで、dealloc が呼ばれるので、走っているloopstop します。
そして、App.main_looploop.run_forever が終了しloop.close することで event loop を終了させることができます。

dealloc では、send_super は使いません。Rubicon-ObjC がよしなに処理をしてくれます。

app.py

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 を繋ぎます。
RootNavigationControllerself.viewController の NavigationController とすることで、close ボタンを持った View とします。
Controller の接続は、Main Thread 上で行う必要があります。デコレータ@onMainThread を関数present_viewController に指定し、その中で接続をします。

rootViewController の取得

以前は、sharedApplicationwindows から 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 を確認したりします。

第 2 引数のis_merge_methods

第 2 引数をbool で指定することで、Class ごとに分けるか、まとめて表示するか選ぶ事ができます。デフォルトは、False で、Class ごとに表示されます。
継承が多く、アルファベット順で探したい場合には、True にすることで、まとめた一覧が表示されます。

pdbr.state(オブジェクト, is_merge_methods:bool)

メインとなる ViewController

rbedgepyrubicon を使って、実装をします。

withModuleToSample.py

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名}) 宣言をするだけで問題ありません。
image UIViewController page capture

NSStringFromClass の場合はFunction なので、自分で実装する部分があります。まず最初に注目する点は赤枠で囲っている、Function と、Foundation です。

image NSStringFromClass page capture
image NSStringFromClass page capture

使用する Framework を確認

Foundation は Framework です。幸いにも Rubicon-ObjC ではrubicon.objc.runtime.Foundation で定義されているので、import できます。

from pyrubicon.objc.runtime import Foundation

他に Rubicon-ObjC で、libc, libobjc は定義されています:

それ以外の Framework は、load_library を使い、自身で load する必要があります。

型を確認し、指定

続いて引数の型と、返り値の型を確認しrestypeargtypes へ定義します。

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