PythonistaのUI実装で遊ぶ[画面遷移]

# はじめに
更新が遅くなってしまいましたが、引きつづきPythonistaのUI実装についてです！
前回までの記事に引き続いて、今回は画面遷移を実装しようと思います。
いやいやまだPythonista全然触ったこともないんだけどって方は以下の記事も読んでみていただけると幸いです。
（ストーリー性はないので、前提知識さえあれば下記記事読まなくてもこの記事は理解できます）

[過去記事]
[Pythonista3のUI実装で遊ぶ[超超入門]](https://qiita.com/EkatoPgm/items/31c84b458f47752e3517)
[PythonistaのUI実装で遊ぶ[画面要素]](https://qiita.com/EkatoPgm/items/c382996be664cdf64d78)
[PythonistaのUI実装で遊ぶ[Actionの実装]](https://qiita.com/EkatoPgm/items/7b54e850b1b1c64acb13)

# 画面遷移
単一の画面で完結しているアプリはほとんどありませんよね。
PythonistaでUIを実装していると、画面遷移したくて震えがとまらなくなってしまうことがあるのではないでしょうか。わかります。わかりますとも。

というわけで今回は以下のように動く簡単なリマインダアプリを作ってみたいと思います。朝弱いけど、自分で作ったアラームならばなんか起きれる気がすんだよなーって方、必見です！
![153D1C06-5EE3-41BF-A62B-91BDFC658C11.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/3e6edb5c-9ebf-b25b-71a5-31e7734869cc.jpeg)

青い四角が画面です。アラーム一覧画面が最初に開いて、時刻とサウンドを選択するとアラームが設定され、最初の一覧に戻るイメージですね。

# 画面を作る
まず画面をぜんぶ用意しちゃいましょう。
このへん画面作って処理作ってって作り方の順番はいろいろあると思いますが、今回はそんな複雑な画面でもないですし、それぞれの画面作ってから遷移の処理入れます。
## アラーム一覧画面
Pythonista の「Script with UI」でタイトルを`alermlist`とすると、alarmlist.py と alarmlist.pyui が作成されます。pyuiの方を開いて以下のように要素を作成します。
<img width="50%" alt="BFD95721-0D01-402A-A2FC-347859584316.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/8a2bfad9-430f-8d05-4638-50c9542a7db1.jpeg">
画面サイズは適当に500*500ですが、やったことは以下です。
①`Custom View`で画面を覆う
②`Table View`を追加（デフォルト値の`Row 1`とかは消しときます）
③ `Button`を追加しTitleを`New Alarm`に変更

`New Alarm`押して作ったアラームがここに一覧表示されるイメージですね。
今回は何もしていませんが、基本的には背景色変えたりとかすると思うので`Custom View`は入れとけばよいかと思います。

## 時刻設定画面
`alarmlist`と同様にソースファイル`datepick`を作成します。
`datepick.pyui`を以下のように作成します。
<img width="50%" alt="74753B46-594E-42DC-84FA-257EFE83CCF4.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/2cf5a17f-144f-4f35-d53e-70f3d50399d3.jpeg">
やったことは以下です。
①`Custom View`で画面を覆う
②`Date Picker`を追加し、Modeを「Date and Time」に変更
③ `Button`を追加しTitleを`OK`に変更
日付を選択して`OK`押したら次のサウンド選択にいく予定の画面です。

## サウンド選択画面
`soundselect`を作成して、pyuiを以下のように作成します。
<img width="50%" alt="E4D1C086-A16B-467C-9486-272A75E20A1B.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/9337ee79-f2aa-a7b6-1bf4-ad59c86f2f02.jpeg">
やったことは`alarmlist.pyui`と一緒です。
Buttonのタイトルだけ`OK`にしてあります。

# 各画面の処理
さてpyuiができたのでこんどは処理を実装していきます！

## アラーム一覧画面から時刻設定画面への遷移
### 処理の実装
「アラーム一覧を表示する」という一番大事な処理を差し置いて、画面遷移処理だけを実装します。
ようするに、`New Alarm`押下で`datepick`に遷移する処理です。
`alermlist.py`と`datepick.py`を書きます。
まずは遷移先の`datepick.py`。画面をクラス化してやりましょう。

```python:datepick.py
import ui

class Datepick(object):
	def __init__(self):
		self.v = ui.load_view()
	def present_view(self):
		self.v.present('sheet')
```

意図としては、`alermlist.py`で`Datepick`クラスを呼び出して、`New Alarm`ボタンが押下されたときに`Datepick.present_view`を呼び出して画面を出していきたいわけですね。
先に言っときますけど**これだと期待通りの動作にはなりません。**

ではうまくいかないことがわかっているまま`alarmlist.py`の方へ。

```python:alermlist.py
import ui
from datepick import Datepick

class AlarmList(object):
	def __init__(self):
		self.v = ui.load_view()
		self.v['button1'].action = self.tap_new_button
	def tap_new_button(self, sender):
		d = DatePick()
		d.present_view()

a = AlarmList()
a.v.present('sheet')
```

AlarmListクラスの`__init__`の中で`load_view`して、各要素の属性を設定します。ここでは`New Alarm`ボタンにActionをつけていますね。
こうすることで、ボタン押下時に処理が動きます。

ちなみに`present()`の引数`sheet`はiPadだけみたいなので、iPhoneの人は`fullscreen`指定と同じ動作になります。
画面の自動リサイズで表示が変になりますが、pyuiの`Auto-Resizing / Flex`でなんとかしましょう。
（画面サイズをお使いのiPhoneと同じにすると自動リサイズされなくなります）

### 実行
`alarmlist.py`を実行すると以下のような画面が出ます。
<img width="50%" alt="IMG_2317.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/b069f26d-a1c7-248f-4ce2-712ae3e5b3d6.jpeg">

さて、本番はここからです。ドキドキしながら`New Alarm`を押してみましょう。
<img width="50%" alt="IMG_2318.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/444b1afa-adb0-070e-0123-b29feb3499d2.jpeg">

やったー！時刻選択画面に遷移したぞ！
よーし1回閉じて、続きの処理を実装していきましょう。。。ん？

<img width="50%" alt="IMG_2319.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/867b29e2-195f-e326-bb0c-2d80c6825a76.jpeg">

×を押したら時刻選択画面が閉じて、最初の画面が後ろからでてきました。思ってたんと違う！
`present()`は実行すると「新しい画面を生成」するので、先の例のままでは次々と新しい画面が生成されていくだけです。

...てぇ...移してぇ...同じ画面のまま画面遷移してぇ...

### NavigationViewクラスを使う
小見出しではありますがここが今回の記事でいちばん大切な箇所です。
ここまで読んでいただいてありがとうございます。

PythonistaのドキュメントにはNavigationViewクラスの説明としてだいたい以下のようなことが書いてあります。
> NavigationViewは、ビューのスタック/階層を表示するためのインターフェイスを提供します。ナビゲーションビューの上部には、現在のビューの名前と戻るボタンを含むナビゲーションバーが表示されます。

操作ごとにビューを順番に表示したい場合には使ってねってことです。

まずは`alarmlist.py`を以下のように書き換えます。

```python:alarmlist.py
import ui

from datepick import Datepick

class AlarmList(object):
	def __init__(self):
		self.v = ui.load_view()
		self.v['button1'].action = self.tap_new_button
		
	def tap_new_button(self, sender):
		d = Datepick()
		# 画面を遷移させる
		sender.superview.navigation_view.push_view(d.v)

a = AlarmList()
nv = ui.NavigationView(a.v) # ロードしたviewをNavigationViewに埋め込み
nv.height = 500             # nvのサイズ調整もしないと表示が崩れる
nv.width = 500
nv.name = 'Alarm'

nv.present('sheet')
```

`tap_new_button`の引数`sender`には押下したButtonが入っているので、親要素→NavigationViewと辿って`push_view()`で時刻選択画面を表示します。

`datepick.py`で定義していた`present_view()`はせっかく実装しましたが不要です消します。

```python:datepick.py
import ui

class Datepick(object):	
	def __init__(self):
		self.v = ui.load_view()
```

画面自体は`alarmlist.py`側で呼び出しているので、ここは`load_view`するだけでよくなります。`v`は外から見えるようにしましょう。

実行して`New Alarm`を押します。

<img width="50%" alt="IMG_2320.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/1739db5b-bdfa-92ee-40a7-b3c067a0f18d.jpeg">

遷移できました！
上にバーが追加されて、Backボタンが表示されていますね。
画面遷移がNavigationView上で行われているので、Backを押すと前の画面に戻ります。すごい！
×を押しても、前の画面が残ることなく、閉じることができます。すごい！！

## 時刻設定画面からサウンド選択画面への遷移
さて次は時刻設定画面からサウンド選択画面への遷移です。
さっきの遷移と同じように実装しましょう。

まずは遷移先の`soundselect.py`。

```python:soundselect.py
import ui

class SoundSelect(object):
	def __init__(self):
		self.v = ui.load_view()
```
次に呼び出し側の`datepick.py`です。
いま作ったSoundSelectをimportします。

```python:datepick.py
import ui
from soundselect import SoundSelect

class Datepick(object):
	
	def __init__(self):
		self.v = ui.load_view()
		self.v['button1'].action = self.tap_ok_button
	
	def tap_ok_button(self, sender):
		s = SoundSelect()
		sender.superview.navigation_view.push_view(s.v)
```

実行してやると時刻選択画面からサウンド選択画面に遷移できます。
NavigationViewを使っているので、時刻を間違えたことに気付いても戻って修正することができますね！

## 画面間で値を受け渡す
ここで必要になってくるのが、「時刻選択画面で選択した時刻をどこかに持っておく」ということ。
せっかく時刻を選択したのにどこにも残らないのでは、指定した時間にアラームがなりません！これでは起きられません！！

Datepickerで指定された時刻を取り出して、SoundSelectに渡してやりましょう。

```python:datepick.py
import ui
from soundselect import SoundSelect

class Datepick(object):
	def __init__(self):
		self.v = ui.load_view()
		self.v['button1'].action = self.tap_ok_button
	def tap_ok_button(self, sender):
		# DatePickerの時刻を取得
		date = sender.superview['datepicker1'].date.strftime('%y/%m/%d %H:%M')
		s = SoundSelect(date)
		sender.superview.navigation_view.push_view(s.v)
```

日付はpyuiで画面に追加したDatepickerのメンバ`date`に格納されているので取り出して利用します。そのままSoundSelectのコンストラクタに渡します。

SoundSelect側は、受け取った値をコンストラクタが受け取れるようにしましょう。

```python:soundselect.py
import ui
import console

class SoundSelect(object):
	def __init__(self, date):
		self.v = ui.load_view()
		self.date = date
		self.v['button1'].action = self.tap_ok_button
	def tap_ok_button(self, sender):
		console.alert(self.date)
```

これで値渡しはOKです。
上のはお試しで、サウンド選択画面のOKボタンを押すと`console.alert`が動作し、設定した時刻が画面に出るようになっています。
## サウンド選択画面の処理
`soundselect.pyui`のテーブルに値を設定しましょう。
今回は動的に入れたりせず固定値とします。
<img width="50%" alt="IMG_2322.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/bcd23d3e-e367-4b07-91a1-c4714e2ce158.jpeg">

さらに`soundselect.py`を以下のように修正します。

```python:soundselect.py
import ui
import console
from datetime import datetime
import notification

class SoundSelect(object):
	def __init__(self, date):
		self.v = ui.load_view()
		self.date = date
		self.v['button1'].action = self.tap_ok_button
				
	def tap_ok_button(self, sender):
		dir = 'Media/Sounds/drums/'
		sounds = ['Drums_01','Drums_02','Drums_03']
		selected = self.v['tableview1'].selected_row[1]
		
		now = datetime.now()  # 現在時刻を取得
		delay = self.date - now  # 設定時刻との差分
		notification.schedule('test notify', delay=delay.total_seconds(), sound_name=dir+sounds[selected])
```

Pythonistaの`notification`モジュールを利用して通知を設定します。
`datetime`モジュールや`notification`モジュールの詳細は割愛します。

まぁ今回の記事はアラーム設定することが目的ではないので、簡単に済ませちゃってます。この時点でアラーム設定は可能ですが、サウンドもしょぼいのを使っているので、時間になるとドラムが1音、ぺんって鳴ります。

## サウンド選択画面からアラーム一覧画面へ
さて、ここまでの実装だと、アラームを設定したところで画面になんの動きもなく、別のアラームを設定したくなったらBackボタンで戻る必要があります。
（まぁワンオペですが）ちょっとカッコ悪いですよね。
なので、サウンド選択画面で`OK`を押下したら、最初のアラーム一覧画面に戻るようにします。
まずは、サウンド選択画面からアラーム一覧画面を呼び出すようにします。

```python:soundselect.py
import ui
import console
from datetime import datetime
import notification

class SoundSelect(object):
	def __init__(self, date):
		self.v = ui.load_view()
		self.date = date
		self.v['button1'].action = self.tap_ok_button
	def tap_ok_button(self, sender):
		from alarmlist import AlarmList
		dir = 'Media/Sounds/drums/'
		sounds = ['Drums_01','Drums_02','Drums_03']
		selected = self.v['tableview1'].selected_row[1]
		now = datetime.now()
		delay = self.date - now
		notification.schedule('test notify', delay=delay.total_seconds(), sound_name=dir+sounds[selected])
		a = AlarmList()
		sender.superview.navigation_view.push_view(a.v)
```

`notification.schedule`でアラームを設定してから、AlarmListのviewを呼び出していますね。
ここで気づいてほしいのが、**AlarmListをtap_ok_buttonの中でimportしている**ということ。
このimport文をファイルの先頭に書くと、`soundselect.py`の中でAlarmを呼び出した瞬間、循環importになってエラーになります。
まぁこの方法が最善、、、ではないかもしれませんが、循環しますよが言いたかっただけなので上手な避け方は別途調べてください。

っともうひとつ。ここまでの実装ではAlarmListクラスを定義している`alarmlist.py`にメイン処理を書いていますので、`alarmlist.py`の中はクラスだけにして新たにメイン処理のスクリプトを描きましょう。

```python
import ui
from alarmlist import AlarmList

a = AlarmList()
nv = ui.NavigationView(a.v)
nv.height = 500
nv.width = 500
nv.name = 'Alarm'

nv.present('sheet')
```

これでアラームが設定された後、アラーム一覧画面に戻ります！おもしろかっこいいぜ！
## アラームを一覧化する
画面遷移ばかりに気を取られてすっかり忘れていましたね。まぁここまできたら完全にオマケ機能ですが、設定したアラームを一覧画面に出してあげましょう。

`notification.schedule()`で設定したアラームは同じく`notification`モジュールの、`get_scheduled()`で取得することができます。
あんまり拘ってファイル出力とかしてもしょうがないので、今回はこれ使っときます。
`alarmlist.py`を編集しましょう。

```python
import ui
from datepick import Datepick
import notification
from datetime import datetime

class AlarmList(object):
	def __init__(self):
		self.v = ui.load_view()
		self.tv = self.v['tableview1']
		self.v['button1'].action = self.tap_new_button

      # get_sucheduled()はリストを返す
		l = []
		for a in notification.get_scheduled():
          # fire_dateにはtimestamp(float型)が入っている
			d = datetime.fromtimestamp(a['fire_date'])
			l.append(d.strftime('%a %b %d %H %M'))

      # TableViewに表示するデータソースの設定
		self.ds = ui.ListDataSource(l)
		self.v['tableview1'].data_source = self.ds

	def tap_new_button(self, sender):
		d = Datepick()
		sender.superview.navigation_view.push_view(d.v)
```

アラーム自体はどうでもいいですが、これはこれで`notification`の使用方法だったり`TableView`のデータ設定方法だったり、Pythonistaモジュールのあれこれに触れてますね。

ちなみにもうちょっと作り込むと、背景色をつけたりはもちろん、アラーム一覧画面でアラームを削除できるようにしたり、UI実装の極意にいろいろ触れることができます。アラームアプリひとつでもいろんな機能使うんですね。。。

<img width="50%" alt="IMG_2324.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404171/b23c9dff-9e51-8d15-9eab-28a0ac9f4671.jpeg">

（体裁はともかく、）アラーム一覧を表示することができました！
ちなみに通知されたものは`notification.get_scheduled`で取得できなくなるので、表示されなくなります。

# おわりに
今回は決まった順番に画面遷移する方法を書きました。
このくらいできるようになると、UI実装の幅が広がって、いろいろと応用がきくようになってくるのではないかと思います。

また、深く掘り下げられる要素については記事書いていきたいと思います！
だいぶ長い記事になりましたが、読んでいただきありがとうございました！