この記事は、Pythonista3 Advent Calendar 2022 の05日目の記事です。
一方的な偏った目線で、Pythonista3 を紹介していきます。
ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。
以下、私の2022年12月時点の環境です。
--- SYSTEM INFORMATION ---
**System Information**
* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.0.2, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。
ちなみに、model iPhone12,1
は、iPhone11 です。
ui
モジュール
グラフィックで結果が出る素晴らしさは、私のプログラミングのモチベーションに大きく影響していると考えています。
プログラミング学習の最初は、consoleにHello World!
を出す事が第一目標になっていますが、初学者がHello World!
を体験しても、次のステップとして何をしたらいいか考えることは、とても難しいと感じています。
他言語から新しい言語を習得する際は、print
することで簡単なデバッグ方法を得て、次のステップとして何をするか、考える事が容易かと思います。しかし、初心はそもそもprint
を使いconsole 画面に文字列が出てきても正直なところちんぷんかんなのです。
そうして、ちんぷんかんのまま、プログラミングから離れていく人たちを結構見てきました。プログラミング学習方法で悩んでいる人は、普段使っているGUI アプリケーションのベースを得ることで、学習意欲が継続する糸口を見つけることができたら幸いです。
ui — Native GUI for iOS — Python 3.6.1 documentation
Building Custom Views
Pythonista3 には、GUI 上でGUI アプリを操作し作成できる.pyui
ファイルがあります。
しかし、私にとっては.pyui
難しく苦手な面の方が強いので、全てコードベースでui
モジュールを使用しています。
Building Custom Views | ui — Native GUI for iOS — Python 3.6.1 documentation
Building Custom Views のサンプルから、MyView
class の各メソッドにprint
を仕込んで実行してみましょう。
以下コードは、コメントを消しており、Reformat しているので、インデントがスペース2つになっています。
import ui
class MyView(ui.View):
def __init__(self):
print('__init__')
def did_load(self):
print('did_load')
def will_close(self):
print('will_close')
def draw(self):
path = ui.Path.oval(0, 0, self.width, self.height)
ui.set_color('red')
path.fill()
img = ui.Image.named('ionicons-beaker-256')
img.draw(0, 0, self.width, self.height)
print('draw')
def layout(self):
print('layout')
def touch_began(self, touch):
print('touch_began')
def touch_moved(self, touch):
print('touch_moved')
def touch_ended(self, touch):
print('touch_ended')
def keyboard_frame_will_change(self, frame):
print('keyboard_frame_will_change')
def keyboard_frame_did_change(self, frame):
print('keyboard_frame_did_change')
v = MyView()
v.present('sheet')
どのタイミングで、どのメソッドが呼ばれているか確認できます。
__init__
layout
layout
draw
touch_began
touch_moved
will_close
keyboard_frame_will_change
keyboard_frame_did_change
keyboard_frame_will_change
keyboard_frame_did_change
keyboard_frame_will_change
keyboard_frame_did_change
keyboard_frame_will_change
keyboard_frame_will_change
keyboard_frame_did_change
keyboard_frame_will_change
keyboard_frame_did_change
keyboard_frame_did_change
挙動の順番を把握していないと、呼び出したいオブジェクトや、サイズ調整など、メソッドにより想定と違う挙動になってしまうので注意しましょう。
基本的には:
-
__init__
- Python class 文法と同様の考え方でOK
- オブジェクトの初期化
- 定数的な数値の変数を宣言
-
layout
- 画面サイズ変更時に呼ばれる
- サイズに依存した数値の調整
- (仕様上1回以上呼ばれる事があるので、重複して処理されて困るものは書かない)
-
draw
-
UIView
のCAShapeLayer
やUIBezierPath
の描画処理
-
-
did_load
-
.pyui
を読み込み(終わり)関係 - 全てコードベース実装であれば、考えなくてもよい
-
-
will_close
-
View
が閉じられたら呼ばれる -
(×)
ボタンを押したら呼ばれる。と思い込んだ方がいい- スワイプで閉じた場合、処理されない場合もある
-
注意点としては、keyboard_frame
関係は、View
をclose した後も、検知してしまうので(私は)Pythonista3 を一度終了させています。
keyboard_frame
を呼ばないのであれば気にしなくて問題ないと思います。
present()
のstyle
引数
View.present(style='default',
animated=True,
popover_location=None,
hide_title_bar=False,
title_bar_color=None,
title_color=None,
orientations=None,
hide_close_button=False
)
で、最終的にView を表示させることになります。
挙動の詳細は、Documentation を読んでいただくとします。
style
をfull_screen
としても、反映されないエラーがあります。Documentation にも訂正がないのですが、fullscreen
でフルスクリーンになります。
- 正
fullscreen
- 誤
full_screen
ちょっとした罠になっているので、気をつけましょう。
ui
モジュールで、View を実装する時のイメージ
全然違うのですが HTMLファイルで<body>
タグに何も書かずに全部JavaScript で書いているようなイメージです(全然違うのですが)。
View.add_subview(view)
で、View やオブジェクトを取り込み親子関係を作ったり、兄弟関係として並列に表示をさせたりして、アプリ全体のレイアウトを設計していくイメージです。
かんたんに設置してみて、挙動をみてみよう
最低限なものを(私の手ぐせで)どんな感じに処理されるか、結果をみて実際に体験してみましょう。
今後、ここのアドベントカレンダーが進んでいくにつれ、ui
モジュールを使ったものが出てくるときの、ベースの考え方としてここで認識いただくと、サンプルコードが読みやすくなるかもしれません。
もちろん、意味不明な遠回りな書き方をしていたら、ご指摘くださいませ。
View
のframe
やsize
が決定される部分と、想定した操作をする
前提として、私のこだわりなのですが、size
をハードコードしたくありません。
よほど特別なサイズ以外は、パーセンテージとして*
や/
で確定させていきたい派です。
また、iPhone でコードを書いている関係上、縦位置(portrait
)で完結させたいと考えています。決して横位置(landscape
)でのレイアウトを無視する訳ではなく「横位置もまぁそれなりにいい感じに」くらいに配慮はします。
コード書く → 実行 → 確認 → View 閉じる → コード書く。。。
のサイクルをリズムよく回したいためです。
(View 確認のためにiPhone 横にしたり面倒じゃないですか、、、)
inspector をみる
import ui
class MainView(ui.View):
def __init__(self):
self.name = 'ただView をadd しただけ'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.bg_color = 'red'
self.add_subview(self.sub)
#print(f'__init__: {self.frame}')
def layout(self):
#print(f'layout: {self.frame}')
pass
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
main_view
のView
に、sub
のView
をadd しました。
わかりやすくするために、main_view
の背景をgray
、sub
をred
にしています。
frame
と、bounds
機械翻訳を貼り付けます。
-
frame
-
スーパービュー(親)の座標系における、ビューの位置とサイズ。矩形は4タプル(x, y, width, height)で表現される。
-
-
bounds
-
4タプル(x, y, width, height)で表される、独自の座標系におけるビューの位置とサイズ。xとyはデフォルトで0ですが、ビューの異なる部分を表示するために0以外の値を設定することができます。この値は、frame属性と連動しています(一方の変更は他方に影響します)。
-
frame
は親のView 依存で決定されるイメージでしょうかね。
inspector を見てみましょう。一度実行しView をclose したら、 console 画面に移動し(i)
アイコンをタップします。
main_view
のframe
とbounds
はそれぞれ:
-
frame
(0.00, 92.00, 414.00, 804.00)
-
bounds
(0.00, 0.00, 414.00, 804.00)
と、表示されています(使っている端末サイズで、数値変わります)。
Pythonista3 アプリのView がroot となり、Pythonista3 のView のヘッダー部分(NavigationView) のheight
サイズ分main_view
のy
位置がずれ設置されていることがわかります。
また、main_view
のframe
, bounds
ともに、size
が同じなのは、Pythonista3 アプリから見てy
位置はずれるが、表示させたいwidth
, height
は変わらないことがわかります。
ちなみに、座標起点は左上が(0.0, 0.0)
です。
sub_view
に関しては:
self.sub = ui.View()
class ui.View | ui — Native GUI for iOS — Python 3.6.1 documentation
引数無しで呼んでいるので、frame
が、(0.00, 0.00, 100.0, 100.0)
となります。
結果、sub
のView
は、左上を起点としてx=0, y=0, width=100, height=100
として赤色の矩形が配置されます。
frame
確定のタイミング
上記サンプルコードでコメントアウトされていたprint
を使って、frame
の変化を確認してみましょう。
import ui
class MainView(ui.View):
def __init__(self):
self.name = 'ただView をadd しただけ'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.bg_color = 'red'
self.add_subview(self.sub)
print(f'__init__: {self.frame}')
def layout(self):
print(f'layout: {self.frame}')
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
__init__
時に、(0.00, 0.00, 100.0, 100.0)
であり、layout
には、 (0.00, 0.00, 414.00, 804.00)
と、変化していることが確認いただけたでしょうか(layout
が2回呼ばれているのは「そんなもんなんだなー」と思っていただき)。
__init__: (0.00, 0.00, 100.00, 100.00)
layout: (0.00, 92.00, 414.00, 804.00)
layout: (0.00, 92.00, 414.00, 804.00)
各メソッドにprint
を張って確認した際にも言及しましたが、layout
が呼ばれるタイミングでframe
数値が決定されます。そして2回も(しつこい)呼ばれるので、layout
メソッド内で処理をさせること、__init__
メソッド等で処理させることを考慮しコードを書きましょう。無駄にインスタンスが2個以上生成して謎の動きになったり、サイズが変わったタイミングで新たに生成されてしまったりします。
layout
メソッド内の処理
実際に確認してみましょう。
sub_view
をmain_view
のsize
の半分で出す例です。
import ui
class MainView(ui.View):
def __init__(self):
self.name = 'sub の縦横を半分に'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.bg_color = 'red'
self.add_subview(self.sub)
def layout(self):
_, _, w, h = self.frame
self.sub.width = w / 2
self.sub.height = h / 2
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
ui
モジュールもinspector で状況が確認できるのが素敵ですね。
add_subview
したView の配置
- 上下左右中心
- 全画面
2パターンほど、実装してみましょう。
frame
状態を取得してこねくり回す方法もありますが、flex
メソッドで簡単に配置もできるので合わせて紹介します。
sub_view
のsize
はデフォルト(100.0, 100.0)
です。
上下左右中心
import ui
class MainView(ui.View):
def __init__(self):
self.name = '上下左右中心: frame'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.bg_color = 'red'
self.add_subview(self.sub)
def layout(self):
_, _, main_width, main_height = self.frame
_, _, sub_width, sub_height = self.sub.frame
sub_x = main_width / 2 - sub_width / 2
sub_y = main_height / 2 - sub_height / 2
self.sub.x = sub_x
self.sub.y = sub_y
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
x
、y
ともに、sub_x = main_width / 2 - sub_width / 2
と/2
しています。
座標起点が、左上となるのでsub_view
自身のsize
の1/2
分を起点側移動してあげるイメージです。
_, _, sub_width, sub_height = self.sub.frame
の位置前に、sub_view
のsize
を指定すれば、好きなsize
で調整可能です。
flex
を指定すると、シンプルに実装できます。
import ui
class MainView(ui.View):
def __init__(self):
self.name = '上下左右中心: flex'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.flex = 'TBLR'
self.sub.bg_color = 'red'
self.add_subview(self.sub)
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
有効なフラグは、"W"(フレキシブル幅)、"H"(フレキシブル高さ)、"L"(フレキシブル左マージン)、"R"(フレキシブル右マージン)、"T"(フレキシブル上マージン)、"B"(フレキシブル下マージン)である。
self.sub.flex = 'TBLR'
フラグとして「上下左右」となります。
navigationView サイズを無視したい
需要があるか不明ですが、アプリ内画面のnavigationView を含めずに「上下左右」したい場合は:
import ui
class MainView(ui.View):
def __init__(self):
self.name = '上下左右中心: frame-navigation'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.bg_color = 'red'
self.add_subview(self.sub)
def layout(self):
main_x, main_y, main_width, main_height = self.frame
_, _, sub_width, sub_height = self.sub.frame
sub_x = main_width / 2 - sub_width / 2 - main_x / 2
sub_y = main_height / 2 - sub_height / 2 - main_y / 2
self.sub.x = sub_x
self.sub.y = sub_y
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
main_x, main_y, main_width, main_height = self.frame
と、frame
からx
、y
も取得。
(冗長ですが)sub_y = main_height / 2 - sub_height / 2 - main_y / 2
navigationのsize
を含めると、よきように配置されます。
全画面
上下左右中心配置の方が、こねくり回しが難しいので、全画面はサクッといきます。
import ui
class MainView(ui.View):
def __init__(self):
self.name = '全画面: frame'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.bg_color = 'red'
self.add_subview(self.sub)
def layout(self):
_, _, main_width, main_height = self.frame
self.sub.frame = (0.0, 0.0, main_width, main_height)
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
self.sub.frame = (0.0, 0.0, main_width, main_height)
と、sub_view
のframe
にtuple
で代入して指定します(配列でも入られるみたいですが)。
flex
を使うと:
import ui
class MainView(ui.View):
def __init__(self):
self.name = '全画面: flex'
self.bg_color = 0.5 # todo: 灰色
self.sub = ui.View()
self.sub.flex = 'WH'
self.sub.bg_color = 'red'
self.add_subview(self.sub)
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
フラグにWH
を指定するだけですね(代わり映えしない絵が続き恐縮です)。
View
以外のものたち
ざっと、どんな機能かを説明します。この機能たちの解説を期待していた方々申し訳ありません(投げぱなしジャーマンとなっており自分でも引いております)。
位置やサイズ調整、どのView に乗せるか?といった事はView
で解説した内容で実装できます。普段お使いのiOS アプリでも実装されているものがPythonista3 でも使えるので、こうやってアプリってできてるのかな?なんて、イメージが持てるかもしれません。
- View
- 今回解説した画面を司る部分
-
add_subview
より、他のui
モジュールで呼び出したものをView に取り込みます
- Button
- タップして、何か処理したい場面で使用
-
action
に処理をさせたい関数を代入
- ButtonItem
- navigationView に乗せたい場面で使用
- ImageView
- 画像のView
- Label
- 1行の文字列を表示
- HTML でのheader のような使い方
- NavigationView
- アプリ上部のView ではない部分
- デフォルトでは、close の
ButtonItem
- ScrollView
- View 内でスクロールさせたい場面で使用
- SegmentedControl
- 一意の選択させたい項目を設定
- 2つ以上でも使える
- Slider
- HTML のスライダーのイメージ
- Switch
- boolean で選択させたい場面で使用
- TableView
- 複数の内容を行で表示させる
- TableViewCell
-
TableView
の行の内容
-
- TextField
- 文字列を入力させる
- TextView
- 複数文字列を表示
- 表示させた文字列を編集させることも可能
- WebView
- View でHTML を表示(ブラウザでWebページを見る)
- 内部的に
UIWebView
を使っているため、あまり推奨しない - 有志で
WKWebView
を呼び出すモジュールあり - 今後
WKWebView
を使った例を紹介予定
- DatePicker
- 日付を選択させる場面で使用
- ActivityIndicator
- 通信中などのクルクル表現
次回は
最後の急足がひど過ぎて申し訳ありません。なんとか、使いたい機能をView に乗せることができれば、Documentation や他の実装例から読み解くことも可能かと思われます。
- TableView
-
WebViewWKWebView
は、別途紹介予定です。
前回紹介した、PyKeys は、ui
モジュールでつくられています。
ここのView の基本的な内容がわかれば、カスタマイズの幅も広がるかも知れません。
次回もui
モジュールの紹介です。
darw
で描画をしたり、画面更新の仕組みを見ていきつつ、軽く絵を描いていきます(クリエイティブコーディング的な)。
ここまで、読んでいただきありがとうございました。
せんでん
Discord
Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。
書籍
iPhone/iPad でプログラミングする最強の本。
その他
- サンプルコード
Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。
コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー
なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー
やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか?って話よ!(クズ)
— pome-ta (@pome_ta93) November 4, 2022
Pythonista3 Advent Calendar 2022 https://t.co/JKUxA525Pt #Qiita
- GitHub
基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。