【protocol】objc_util からrubicon-objc へ移行【delegate】
objc_util からrubicon-objc へ乗り換える
前回は、class宣言の方法について整理した。
rubicon-objcはobjc_utilよりも、Pythonの文法に準拠したPythonicなclass宣言。つまり、objc_utilのcreate_objc_class
に依存した書き方は不要となる。
今回は、protocol(delegate)を中心に、コードの書き換えの整理する。
プロトコルを定義し、デリゲートメソッドを呼び出す
使う場面として:
-
今回使用するもの
-
Pythonista3のdocsに例としてあったもの
Apple Developer DocumentationでProtocol
と表記されている場合に使用。
UITableViewDataSource
のようにDelegate
が名前に入っていない場合もある。まずは、(Apple)公式Documentationで調べることが大切。
objc_util の場合
classの宣言と同様に、create_objc_classで宣言。
以下のコードは、閉じるボタンを右肩に持たせたUINavigationController
を実装した例(一部中略)。
ui
モジュールのui.NavigationViewのようなものをイメージしている(ui
モジュールを使わずに、Pythonista3上で独自にUIKitを呼び出す場面で使用)。
このコードで、プロトコル定義とデリゲートメソッドを呼び出方法を整理する。
UINavigationController = ObjCClass('UINavigationController')
class NavigationController:
def __init__(self):
self._navigationController: UINavigationController
def _override_navigationController(self):
# --- `UINavigationController` Methods
def doneButtonTapped_(_self, _cmd, _sender):
"""(自身の)アプリケーション終了
`NavigationController` から生やした`done_btn` ボタンのアクション
"""
this = ObjCInstance(_self)
visibleViewController = this.visibleViewController()
visibleViewController.dismissViewControllerAnimated_completion_(
True, None)
# --- `UINavigationController` set up
_methods = [
doneButtonTapped_,
]
create_kwargs = {
'name': '_nv',
'superclass': UINavigationController,
'methods': _methods,
}
_nv = create_objc_class(**create_kwargs)
self._navigationController = _nv
def create_navigationControllerDelegate(self):
# --- `UINavigationControllerDelegate` Methods
def navigationController_willShowViewController_animated_(
_self, _cmd, _navigationController, _viewController, _animated):
navigationController = ObjCInstance(_navigationController)
viewController = ObjCInstance(_viewController)
"""
長いので中略
"""
done_btn = UIBarButtonItem.alloc(
).initWithBarButtonSystemItem_target_action_(0, navigationController,
sel('doneButtonTapped:'))
visibleViewController = navigationController.visibleViewController()
# --- navigationItem
navigationItem = visibleViewController.navigationItem()
navigationItem.rightBarButtonItem = done_btn
# --- `UINavigationControllerDelegate` set up
_methods = [
navigationController_willShowViewController_animated_,
]
_protocols = [
'UINavigationControllerDelegate',
]
create_kwargs = {
'name': '_nvDelegate',
'methods': _methods,
'protocols': _protocols,
}
_nvDelegate = create_objc_class(**create_kwargs)
return _nvDelegate.new()
@on_main_thread
def _init(self, vc: UIViewController):
self._override_navigationController()
nv = self._navigationController.alloc()
nv.initWithRootViewController_(vc).autorelease()
_delegate = self.create_navigationControllerDelegate()
nv.setDelegate_(_delegate)
return nv
@classmethod
def new(cls, vc: UIViewController) -> ObjCInstance:
_cls = cls()
return _cls._init(vc)
メソッドとする関数が点在する
クラスの下部にある、_init
メソッドに注目してみる。
-
UINavigationController
インスタンスを生成(nv
) -
UINavigationControllerDelegate
インスタンスを生成(_delegate
) -
nv.setDelegate_(_delegate)
で紐付け
create_objc_class
にて、protocols
の引数にdelegateを指定している。superclass
とprotocols
を同時に引数を渡す事も可能ではある(多分)。
「多分」とした理由として、create_objc_class
は関数を引数methods
へ渡す関係上、各関数が点在してしまう。
事例コードの手法は、classで囲う事により点在するのを回避しているが、巻き上げ(ホスティング)の特性によりcreate_objc_class
インスタンス生成の以前に関数を定義する。結果として、各関数とmethods
の関係が掴みにくい(気がしている)。
文字列でプロトコルを指定
ObjCClass
で(もちろん)呼び出せないため、protocols
の指定は、文字列で指定るすることになる。
typoが原因のエラーを検知する脳内リソースが必要となる。
rubicon-objc の場合
UINavigationController = ObjCClass('UINavigationController')
UINavigationControllerDelegate = ObjCProtocol('UINavigationControllerDelegate')
class NavigationController(UINavigationController,
protocols=[UINavigationControllerDelegate]):
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
"""
中略
"""
self.delegate = self
@objc_method
def doneButtonTapped_(self, sender):
visibleViewController = self.visibleViewController
visibleViewController.dismissViewControllerAnimated_completion_(True, None)
@objc_method
def navigationController_willShowViewController_animated_(
self, navigationController, viewController, animated: bool):
viewController.setEdgesForExtendedLayout_(edgeNone)
doneButton = UIBarButtonItem.alloc(
).initWithBarButtonSystemItem_target_action_(done, navigationController,
SEL('doneButtonTapped:'))
visibleViewController = navigationController.visibleViewController
navigationItem = visibleViewController.navigationItem
navigationItem.rightBarButtonItem = doneButton
objc_utilと比較すると、回りくどい書き方が減りPythonicな表現となっている。
プロトコル宣言
ObjCProtocol
にて、プロトコルを呼び出す。
Using and creating Objective-C protocols - Rubicon 0.4.8
ObjCProtocol | rubicon.objc.api — The high-level Rubicon API - Rubicon 0.4.8
objc_utilにはないclassで、typoエラーの発見が早期に見つけることができる。
class NavigationController(UINavigationController,
protocols=[UINavigationControllerDelegate]):
class宣言時に、protocols
へ配列として指定する。
インスタンスメソッド
(objc_utilも同様であるが)delegateが持っているメソッドをオーバーライドし処理を書いていく。
:
とある部分は、_
へ書き換える。
navigationController:willShowViewController:animated:
の場合。navigationController_willShowViewController_animated_
とする。
rubicon-objcでの注意点として、Pythonのアノテーション(型ヒント)が、標準のPythonより強い意味を持つ(プロトコル以前にrubicon-objc全体として)。
今回の場合は、引数animated
の:bool
は必須。
- (void)navigationController:(UINavigationController *)navigationController
willShowViewController:(UIViewController *)viewController
animated:(BOOL)animated;
(私自身C言語の理解が皆無なので、雰囲気理解でしかないが)*
ポインタではない場合には、注意しつつ挙動を確認している。
ここの部分は、C言語を基礎としつつObjective-Cの理解を深めていきたい。
とにかく、実行エラーの場合には、アノテーションに注目してみる。
@objc_method
def navigationController_willShowViewController_animated_(
self, navigationController, viewController, animated: bool):
self.delegate = self
の嬉しさ
Pythonicな書き方ができるので、self
が事実のObjective-Cのclassとなる。
参考とするコード(Objective-CやSwift)の形式と近い状態で書く事ができる。参照先と近しい状態を保持できる。
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
"""
中略
"""
self.delegate = self
さいごに
Pythonista3でRubicon-objcのみで、UIを出す。Pytoの挙動も確認。
メインスレッドの処理については、上記記事を参考にしている。
(特にPytoでは)以下も(必要に応じ)参照。掲載のコードは、from rubicon.objc import 〜
としているが、以下参照時にはpyrubicon
と読み替える必要あり。
【非推奨】Rubicon-ObjC の手動インストール・インポート
from rubicon.objc.api import ObjCClass, ObjCProtocol, objc_method
from rubicon.objc.runtime import SEL, send_super
import pdbr
ObjCClass.auto_rename = True
### --- onMainThread --- ###
from ctypes import byref, cast, Structure
import functools
from rubicon.objc.api import Block, ObjCClass, ObjCInstance
from rubicon.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
### --- ###
# --- UINavigationController
UINavigationController = ObjCClass('UINavigationController')
UINavigationControllerDelegate = ObjCProtocol('UINavigationControllerDelegate')
UINavigationBarAppearance = ObjCClass('UINavigationBarAppearance')
UIBarButtonItem = ObjCClass('UIBarButtonItem')
# --- UIViewController
UIViewController = ObjCClass('UIViewController')
UIColor = ObjCClass('UIColor')
UIButtonConfiguration = ObjCClass('UIButtonConfiguration')
UIButton = ObjCClass('UIButton')
NSLayoutConstraint = ObjCClass('NSLayoutConstraint')
# ref: [UIRectEdge | Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uirectedge?language=objc)
'''
UIRectEdgeNone = 0
'''
edgeNone = 0
# ref: [UIControlEvents | Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uicontrolevents?language=objc)
'''
UIControlEventTouchUpInside = 1 << 6
'''
touchUpInside = 1 << 6
# ref: [UIBarButtonSystemItem | Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uibarbuttonsystemitem?language=objc)
'''
UIBarButtonSystemItemDone
'''
done = 0
# --- NavigationController
class RootNavigationController(UINavigationController,
protocols=[UINavigationControllerDelegate]):
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
appearance = UINavigationBarAppearance.new()
appearance.configureWithDefaultBackground()
navigationBar = self.navigationBar
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.compactScrollEdgeAppearance = appearance
self.delegate = self
@objc_method
def doneButtonTapped_(self, sender):
visibleViewController = self.visibleViewController
visibleViewController.dismissViewControllerAnimated_completion_(True, None)
@objc_method
def navigationController_willShowViewController_animated_(
self, navigationController, viewController, animated: bool):
viewController.setEdgesForExtendedLayout_(edgeNone)
doneButton = UIBarButtonItem.alloc(
).initWithBarButtonSystemItem_target_action_(done, navigationController,
SEL('doneButtonTapped:'))
visibleViewController = navigationController.visibleViewController
navigationItem = visibleViewController.navigationItem
navigationItem.rightBarButtonItem = doneButton
# --- ViewController
class FirstViewController(UIViewController):
@objc_method
def onTap_(self, sender):
svc = SecondViewController.new()
navigationController = self.navigationController
navigationController.pushViewController_animated_(svc, True)
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
# --- Navigation
self.navigationItem.title = 'FirstView'
# --- View
self.view.backgroundColor = UIColor.systemBlueColor()
config = UIButtonConfiguration.tintedButtonConfiguration()
config.title = 'Tap'
config.baseBackgroundColor = UIColor.systemPinkColor()
config.baseForegroundColor = UIColor.systemGreenColor()
tapButton = UIButton.new()
tapButton.configuration = config
tapButton.addTarget_action_forControlEvents_(self, SEL('onTap:'),
touchUpInside)
self.view.addSubview_(tapButton)
# --- Layout
tapButton.translatesAutoresizingMaskIntoConstraints = False
NSLayoutConstraint.activateConstraints_([
tapButton.centerXAnchor.constraintEqualToAnchor_(
self.view.centerXAnchor),
tapButton.centerYAnchor.constraintEqualToAnchor_(
self.view.centerYAnchor),
tapButton.widthAnchor.constraintEqualToAnchor_multiplier_(
self.view.widthAnchor, 0.4),
tapButton.heightAnchor.constraintEqualToAnchor_multiplier_(
self.view.heightAnchor, 0.1),
])
class SecondViewController(UIViewController):
@objc_method
def onTap_(self, sender):
navigationController = self.navigationController
navigationController.popViewControllerAnimated_(True)
@objc_method
def viewDidLoad(self):
send_super(__class__, self, 'viewDidLoad')
# --- Navigation
self.navigationItem.title = 'SecondView'
# --- View
self.view.backgroundColor = UIColor.systemGreenColor()
config = UIButtonConfiguration.tintedButtonConfiguration()
config.title = 'Tap'
config.baseBackgroundColor = UIColor.systemPinkColor()
config.baseForegroundColor = UIColor.systemBlueColor()
tapButton = UIButton.new()
tapButton.configuration = config
tapButton.addTarget_action_forControlEvents_(self, SEL('onTap:'),
touchUpInside)
self.view.addSubview_(tapButton)
# --- Layout
tapButton.translatesAutoresizingMaskIntoConstraints = False
NSLayoutConstraint.activateConstraints_([
tapButton.centerXAnchor.constraintEqualToAnchor_(
self.view.centerXAnchor),
tapButton.centerYAnchor.constraintEqualToAnchor_(
self.view.centerYAnchor),
tapButton.widthAnchor.constraintEqualToAnchor_multiplier_(
self.view.widthAnchor, 0.4),
tapButton.heightAnchor.constraintEqualToAnchor_multiplier_(
self.view.heightAnchor, 0.1),
])
# --- present
@onMainThread
def present_viewController(myVC: UIViewController):
app = ObjCClass('UIApplication').sharedApplication
window = app.keyWindow if app.keyWindow else app.windows[0]
rootVC = window.rootViewController
while _presentedVC := rootVC.presentedViewController:
rootVC = _presentedVC
myNC = RootNavigationController.alloc().initWithRootViewController_(myVC)
presentVC = myNC
# ref: [UIModalPresentationStyle | Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle?language=objc)
'''
UIModalPresentationFullScreen = 0
UIModalPresentationPageSheet = 1
'''
presentVC.setModalPresentationStyle_(1)
rootVC.presentViewController_animated_completion_(presentVC, True, None)
if __name__ == '__main__':
vc = FirstViewController.new()
present_viewController(vc)
Discussion