【導入編】#1: a-Shell(mini) アプリでアプリをつくる【Rubicon-ObjC】
はじめに
iPhone, iPad の無料アプリa-Shell でアプリを制作します。プログラミング言語は、Swift や Objective-C ではなく Python となります。
全 3 部構成の第 1 部です
この記事を含め、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 へ参照させてください。
a-Shell(mini) の参考ドキュメント
README.md
の日本語訳
有志のガイドブック
コードの大まかな処理の流れを把握しましょう
先に、処理の流れをざっくりと見ていきます。なお、今回の実装では以下の状態を目指します:
- View を閉じた後も、a-Shell を通常動作で使用するとこができる
- 呼び出した View は、自分自身の View を閉じることができる
Python の実行から、View が閉じるまで
- Application(a-Shell)のインスタンスから、
rootViewController
を取得 -
mainThread
上で、実装した ViewController をrootViewController
へpresent
- View が表示される
- Rubicon-ObjC の
event_loop
が走る - View が閉じる際に
event_loop
を停止 - a-Shell に戻る
図に落とすとこのようになります。
アプリの中でアプリを起動させる考え方
通常 Xcode の iOS, iPadOS 開発では、実行時@UIApplicationMain
(Objective-C は、main
関数)が最初に呼ばれます。
しかし、今回の実装は a-Shell が先に立ち上がった状態から、a-Shell でアプリを起動させます。
既に存在している(a-Shell の)ViewController に、新規で実装した ViewController を乗せる。という方針をとっています。
singleFileSample.py
コード: 以降、『【非推奨】Rubicon-ObjC の手動インストール・インポート』の環境設定を前提に進めます。
コードの全体
今回は、Rubicon-ObjC ライブラリと、1 つのファイル(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
されているのが確認できます。
呼び出した View の close ボタンを押し閉じた後、再度実行しても問題なく View が立ち上がることを確認できたでしょうか。
おわりに
1 ファイルで(約 400 行程ありますが)a-Shell を使い View の開閉ができました。全体の処理の流れをざっくり掴んでもらえていたら嬉しいです。
mac を使い Xcode で開発をすると、ビルドなど時間を取られることが多くあります。その点 a-Shell は、フットワーク軽くトライアンドエラーできる面白さがあります。
今回は、1 ファイルにコードを全て詰め込んでしまったので、次回そのコード群を分けてパッケージ化していきます。
Discussion