🔰

【導入編】#1: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】

に公開

はじめに

iPhone, iPad の無料アプリa-Shell でアプリを制作します。プログラミング言語は、Swift や Objective-C ではなく Python となります。

全 3 部構成の第 1 部です

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

3 つの記事を通したコードは、GitHub で公開しています。

a-Shell はファイルアプリ経由の参照が可能です

a-Shell でのコード編集が大変、または既に実行したいデータがある場合は、アプリ外ディレクトリにアクセスするコマンドpickFolder や、Git (導入方法は割愛)を使った開発もできます。

こちらの リンク先 より、.zip を直接ダウンロードできます。

「とりあえず、動かしてみたい」という方は、ダウンロード後ファイルアプリで.zip を解凍し、解凍先ディレクトリを a-Shell へ参照させてください。

a-Shell(mini) の参考ドキュメント

README.md の日本語訳

有志のガイドブック

コードの大まかな処理の流れを把握しましょう

先に、処理の流れをざっくりと見ていきます。なお、今回の実装では以下の状態を目指します:

  • View を閉じたも、a-Shell を通常動作で使用するとこができる
  • 呼び出した View は、自分自身の View を閉じることができる

Python の実行から、View が閉じるまで

  1. Application(a-Shell)のインスタンスから、rootViewController を取得
  2. mainThread 上で、実装した ViewController をrootViewControllerpresent
  3. View が表示される
  4. Rubicon-ObjC のevent_loop が走る
  5. View が閉じる際にevent_loop を停止
  6. a-Shell に戻る

図に落とすとこのようになります。

flow image

アプリの中でアプリを起動させる考え方

通常 Xcode の iOS, iPadOS 開発では、実行時@UIApplicationMain(Objective-C は、main 関数)が最初に呼ばれます。
しかし、今回の実装は a-Shell が先に立ち上がった状態から、a-Shell でアプリを起動させます。

既に存在している(a-Shell の)ViewController に、新規で実装した ViewController を乗せる。という方針をとっています。

コード: singleFileSample.py

以降、『【非推奨】Rubicon-ObjC の手動インストール・インポート』の環境設定を前提に進めます。

コードの全体

今回は、Rubicon-ObjC ライブラリと、1 つのファイル(singleFileSample.py)のみの構成です。閲覧するには少々長いですが、以下が全コードです。

singleFileSample.py | 全体
import ctypes

from pyrubicon.objc.api import ObjCClass, ObjCInstance, Block
from pyrubicon.objc.api import objc_method
from pyrubicon.objc.runtime import objc_id, send_super, SEL
# todo: utils ###############################################
from pyrubicon.objc.runtime import Class, Foundation
# todo: lifeCycle ###########################################
import asyncio
import logging
from pyrubicon.objc.eventloop import EventLoopPolicy
# todo: exception ###########################################
import sys
import os
# todo: mainThread ##########################################
import functools
from pyrubicon.objc.runtime import libobjc, objc_block

from rbedge import pdbr

ObjCClass.auto_rename = True


#############################################################
# --- utils
def NSStringFromClass(cls: Class) -> ObjCInstance:
  _NSStringFromClass = Foundation.NSStringFromClass
  _NSStringFromClass.restype = ctypes.c_void_p
  _NSStringFromClass.argtypes = [Class]
  return ObjCInstance(_NSStringFromClass(cls))


#############################################################
# --- 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)

#############################################################
# --- lifeCycle
#############################################################
#logging.basicConfig(level=logging.DEBUG)
asyncio.set_event_loop_policy(EventLoopPolicy())
loop = asyncio.new_event_loop()
#loop.set_debug(True)

#############################################################
# --- mainThread
#############################################################
NSThread = ObjCClass('NSThread')


class struct_dispatch_queue_s(ctypes.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(ctypes.cast(ctypes.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


#############################################################
# --- UINavigationController
#############################################################
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')
    # >>> (5) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    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_')
    print('↓ ---')

  @objc_method
  def viewWillDisappear_(self, animated: bool):
    print('↑ ---')
    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):
    print(f'{NSStringFromClass(__class__)}: doneButtonTapped:')
    self.dismissViewControllerAnimated_completion_(True, None)

  @objc_method
  def navigationController_willShowViewController_animated_(
      self, navigationController, viewController, animated: bool):
    closeButtonItem = UIBarButtonItem.alloc().initWithBarButtonSystemItem(
      24, target=navigationController, action=SEL('closeButtonTapped:'))

    visibleViewController = navigationController.visibleViewController
    navigationItem = visibleViewController.navigationItem
    navigationItem.rightBarButtonItem = closeButtonItem


#############################################################
# --- UIViewController
#############################################################
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')


#############################################################
# --- app present
#############################################################
UIApplication = ObjCClass('UIApplication')


class App:

  # >>> (1) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  sharedApplication = UIApplication.sharedApplication
  __objectEnumerator = sharedApplication.connectedScenes.objectEnumerator()
  while (__windowScene := __objectEnumerator.nextObject()):
    if __windowScene.activationState == 0:
      break
  rootViewController = __windowScene.keyWindow.rootViewController

  def __init__(self, viewController, modalPresentationStyle=1):
    self.viewController = viewController
    self.modalPresentationStyle = modalPresentationStyle

  def present(self):

    # >>> (2) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    @onMainThread
    def present_viewController(viewController: UIViewController, style: int):

      presentViewController = RootNavigationController.alloc(
      ).initWithRootViewController_(viewController)

      presentViewController.setModalPresentationStyle_(style)

      self.rootViewController.presentViewController_animated_completion_(
        presentViewController, True, None)

    # >>> (3) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    present_viewController(self.viewController, self.modalPresentationStyle)
    self.main_loop()

  def main_loop(self):
    # >>> (4) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    loop.run_forever()
    loop.close()


if __name__ == '__main__':
  print('--- run ---')
  main_vc = MainViewController.new()
  presentation_style = 1

  app = App(main_vc, presentation_style)
  app.present()
  print('--- end ---')
  # >>> (6) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

コード内に、## Python の実行から、View が閉じるまで の番号も入っています。処理の流れ把握にお使いください。
(5) 以外は、下部にまとまっていますが、(5) は、class RootNavigationController(中央付近)で処理されています。

import 宣言は、今後のパッケージ分割のために、役割ごとに区分けしています。そのため、推奨される宣言順ではありません。

コード実行

a-Shell の Python 実行は、通常のターミナル操作と同じです:

  • 対象のディレクトリへ移動
[Documents]$ cd ~a-Shell_Rubicon-ObjC_UIKitSamples/
  • python コマンドと実行ファイルを指定
[~a-Shell_Rubicon-ObjC_UIKitSamples]$ python singleFileSample.py

実行後、裏側の console で Life Cycle ごとにprint されているのが確認できます。

preview capture gif

呼び出した View の close ボタンを押し閉じた後、再度実行しても問題なく View が立ち上がることを確認できたでしょうか。

おわりに

1 ファイルで(約 400 行程ありますが)a-Shell を使い View の開閉ができました。全体の処理の流れをざっくり掴んでもらえていたら嬉しいです。

mac を使い Xcode で開発をすると、ビルドなど時間を取られることが多くあります。その点 a-Shell は、フットワーク軽くトライアンドエラーできる面白さがあります。

今回は、1 ファイルにコードを全て詰め込んでしまったので、次回そのコード群を分けてパッケージ化していきます。

Discussion