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

この記事は、[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) の05日目の記事です。

https://qiita.com/advent-calendar/2022/pythonista3

一方的な偏った目線で、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](https://omz-software.com/pythonista/docs/ios/ui.html)

https://omz-software.com/pythonista/docs/ios/ui.html


### Building Custom Views

Pythonista3 には、GUI 上でGUI アプリを操作し作成できる`.pyui` ファイルがあります。

しかし、私にとっては`.pyui` 難しく苦手な面の方が強いので、全てコードベースで`ui` モジュールを使用しています。

[Building Custom Views | ui — Native GUI for iOS — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/ui.html#building-custom-views)


https://omz-software.com/pythonista/docs/ios/ui.html#building-custom-views


Building Custom Views のサンプルから、`MyView` class の各メソッドに`print` を仕込んで実行してみましょう。



![img221119_153753.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/5ff26ae3-4445-b6e5-d897-3f21e7de2790.gif)



以下コードは、コメントを消しており、Reformat しているので、インデントがスペース2つになっています。

```py
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/2f0feec1-8e6e-df85-e5b4-ddda698a46c5.png)



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

``` terminal
__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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/8e0d9be5-7d16-85c9-790e-7acafbedab5f.png)




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

基本的には:

- `__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` 引数

```py
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 をみる

```py
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/ad2611c1-0cf1-6ab4-0658-3108c4b9a542.png)


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

![img221120_131653.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/f2dd6509-3cb6-79a8-d75d-5bb0e1e0a22a.png)


![img221120_131703.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/45a00400-f69c-633d-dcd3-e775fe7377c4.png)



`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)` です。

![img221120_132245.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/e9b8a6f8-175e-0534-9d1d-5f68a423e37c.png)




`sub_view` に関しては:

```py
self.sub = ui.View()
```

[class ui.View | ui — Native GUI for iOS — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/ui.html#view)

https://omz-software.com/pythonista/docs/ios/ui.html#view


引数無しで呼んでいるので、`frame` が、`(0.00, 0.00, 100.0, 100.0)` となります。

結果、`sub` の`View` は、左上を起点として`x=0, y=0, width=100, height=100` として赤色の矩形が配置されます。

### `frame` 確定のタイミング

上記サンプルコードでコメントアウトされていた`print` を使って、`frame` の変化を確認してみましょう。

```py
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/645a045f-eebe-0bd0-0190-267328998a11.png)


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

### `layout` メソッド内の処理

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

`sub_view` を`main_view` の`size` の半分で出す例です。

```py
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/7e870b37-90d2-80bc-b23d-d072609ef07c.png)



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


![img221120_143455.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/f05b2467-39fd-f6f5-8325-23c225450308.png)


![img221120_143504.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/e35d1ba1-4120-ab05-71d4-c42bcf8e661e.png)



### `add_subview` したView の配置

- 上下左右中心
- 全画面

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

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

`sub_view` の`size` はデフォルト`(100.0, 100.0)` です。

#### 上下左右中心


![img221121_172327.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/fd5c829a-e903-ad2b-b360-87ada6f506d3.png)



```py
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` を指定すると、シンプルに実装できます。


![img221121_172839.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/5f5d7eac-7b65-5d8d-03e8-d9f50412bbb1.png)




```py
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/7f567bef-a27b-9a61-4f02-f65bbf49ec76.png)


```py
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` を含めると、よきように配置されます。

#### 全画面

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


![img221121_174649.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/3edd8b13-cbd0-a739-d6cd-30018ad9d8a4.png)



```py
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` を使うと:


![img221121_175007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/bb7905d0-8201-bf73-381a-3e94da77c9b5.png)



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

https://t.co/0IOW2hn1VC

### 書籍

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

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「活用篇」：hirax](https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99)

https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「土台篇」：hirax](https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r)

https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r

### その他

- サンプルコード

[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) でのコードをまとめているリポジトリがあります。

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

https://github.com/pome-ta/Pythonista3AdventCalendar2022sampleCode

- Twitter

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

https://twitter.com/pome_ta93

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか？って話よ！（クズ）<br><br>Pythonista3 Advent Calendar 2022 <a href="https://t.co/JKUxA525Pt">https://t.co/JKUxA525Pt</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; pome-ta (@pome_ta93) <a href="https://twitter.com/pome_ta93/status/1588570697777152006?ref_src=twsrc%5Etfw">November 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- GitHub

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

https://github.com/pome-ta