LoginSignup
3
0

More than 1 year has passed since last update.

Pythonista3 のuiモジュールを使ってアプリでアプリをつくる下準備(View編)。

Last updated at Posted at 2022-12-04

この記事は、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 を仕込んで実行してみましょう。

img221119_153753.gif

以下コードは、コメントを消しており、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')

img221119_154207.png

どのタイミングで、どのメソッドが呼ばれているか確認できます。

__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

img221119_154217.png

挙動の順番を把握していないと、呼び出したいオブジェクトや、サイズ調整など、メソッドにより想定と違う挙動になってしまうので注意しましょう。

基本的には:

  • __init__
    • Python class 文法と同様の考え方でOK
    • オブジェクトの初期化
    • 定数的な数値の変数を宣言
  • layout
    • 画面サイズ変更時に呼ばれる
    • サイズに依存した数値の調整
    • (仕様上1回以上呼ばれる事があるので、重複して処理されて困るものは書かない)
  • draw
    • UIViewCAShapeLayerUIBezierPath の描画処理
  • 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 を読んでいただくとします。

stylefull_screen としても、反映されないエラーがあります。Documentation にも訂正がないのですが、fullscreen でフルスクリーンになります。

    • fullscreen
    • full_screen

ちょっとした罠になっているので、気をつけましょう。

ui モジュールで、View を実装する時のイメージ

全然違うのですが HTMLファイルで<body> タグに何も書かずに全部JavaScript で書いているようなイメージです(全然違うのですが)。

View.add_subview(view) で、View やオブジェクトを取り込み親子関係を作ったり、兄弟関係として並列に表示をさせたりして、アプリ全体のレイアウトを設計していくイメージです。

かんたんに設置してみて、挙動をみてみよう

最低限なものを(私の手ぐせで)どんな感じに処理されるか、結果をみて実際に体験してみましょう。

今後、ここのアドベントカレンダーが進んでいくにつれ、ui モジュールを使ったものが出てくるときの、ベースの考え方としてここで認識いただくと、サンプルコードが読みやすくなるかもしれません。

もちろん、意味不明な遠回りな書き方をしていたら、ご指摘くださいませ。

Viewframesize が決定される部分と、想定した操作をする

前提として、私のこだわりなのですが、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'])


img221120_131251.png

main_viewView に、subView をadd しました。

わかりやすくするために、main_view の背景をgraysubred にしています。

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) アイコンをタップします。

img221120_131653.png

img221120_131703.png

main_viewframebounds はそれぞれ:

  • 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_viewy 位置がずれ設置されていることがわかります。

また、main_viewframe, bounds ともに、size が同じなのは、Pythonista3 アプリから見てy 位置はずれるが、表示させたいwidth, height は変わらないことがわかります。

ちなみに、座標起点は左上が(0.0, 0.0) です。

img221120_132245.png

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) となります。

結果、subView は、左上を起点として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)

img221120_141826.png

各メソッドにprint を張って確認した際にも言及しましたが、layout が呼ばれるタイミングでframe 数値が決定されます。そして2回も(しつこい)呼ばれるので、layout メソッド内で処理をさせること、__init__ メソッド等で処理させることを考慮しコードを書きましょう。無駄にインスタンスが2個以上生成して謎の動きになったり、サイズが変わったタイミングで新たに生成されてしまったりします。

layout メソッド内の処理

実際に確認してみましょう。

sub_viewmain_viewsize の半分で出す例です。

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'])

img221120_143222.png

ui モジュールもinspector で状況が確認できるのが素敵ですね。

img221120_143455.png

img221120_143504.png

add_subview したView の配置

  • 上下左右中心
  • 全画面

2パターンほど、実装してみましょう。

frame 状態を取得してこねくり回す方法もありますが、flex メソッドで簡単に配置もできるので合わせて紹介します。

sub_viewsize はデフォルト(100.0, 100.0) です。

上下左右中心

img221121_172327.png

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'])

xy ともに、sub_x = main_width / 2 - sub_width / 2/2 しています。

座標起点が、左上となるのでsub_view 自身のsize1/2 分を起点側移動してあげるイメージです。

_, _, sub_width, sub_height = self.sub.frame の位置前に、sub_viewsize を指定すれば、好きなsize で調整可能です。

flex を指定すると、シンプルに実装できます。

img221121_172839.png

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 を含めずに「上下左右」したい場合は:

img221121_173318.png

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 からxy も取得。

(冗長ですが)sub_y = main_height / 2 - sub_height / 2 - main_y / 2 navigationのsize を含めると、よきように配置されます。

全画面

上下左右中心配置の方が、こねくり回しが難しいので、全画面はサクッといきます。

img221121_174649.png

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_viewframetuple で代入して指定します(配列でも入られるみたいですが)。

flex を使うと:

img221121_175007.png

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
  • WebView WKWebView

は、別途紹介予定です。

前回紹介した、PyKeys は、ui モジュールでつくられています。

ここのView の基本的な内容がわかれば、カスタマイズの幅も広がるかも知れません。

次回もui モジュールの紹介です。

darw で描画をしたり、画面更新の仕組みを見ていきつつ、軽く絵を描いていきます(クリエイティブコーディング的な)。

ここまで、読んでいただきありがとうございました。

せんでん

Discord

Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

書籍

iPhone/iPad でプログラミングする最強の本。

その他

  • サンプルコード

Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。

コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー

  • Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

  • GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0