iOS Widget 対応でやったこと

SwiftUI を使って個人開発しているアプリで Widget 対応をしたので、やったことを忘れないようにメモしておきます。

ログの整備

ふつうのアプリ開発を趣味でやっている範囲だとデバッガと print で不自由なく開発できてしまうんですが、 widget 開発する上ではちゃんとログを出力しないと思い通りに動かないときそれだと何もわからんということがあるんですよね。具体的には、アプリと widget 両方の動作を同時に追いたいときや widget をタップして deeplink でアプリに遷移するときなどは デバッガも print も役に立ちません。

そこで、 Unified Logging を使って要所でログを出すようにしました。以下のビデオにログの概念、出力の方法、参照の方法などとりあえず必要なこと全てがまとまってます。

developer.apple.com

deeplink 対応

続いて widget をタップしたときの DeepLink の対応です。今回 widget 対応したのが SwiftUI ライフサイクルのアプリだったので、以下のように関連するいくつか View で onOpenURL を使って対処しました。

www.donnywals.com

UIKit だと DeepLink をハンドリングする1つのコンポーネントがいて、そいつが DeepLink が指示するように ViewController を重ねていくのがよくある DeepLink 対応だと思うんですが、SwiftUI では View 自身が DeepLink をハンドリングしないといけないのでかなりマインドセットが違って苦労しました。

SwiftUI でも以下の記事のように View の階層自体をグローバルな EnvironmentObject で持っていれば、その object の状態を変えることでアプリの表示を一箇所から完全にコントロールできるので UIKit と同様のハンドリングが可能になるのでそれも試してみたのですが、DeepLink に関係なくルーティング自体を追加・改修するコストが高すぎてつらくなったのでやめました。

nalexn.github.io

keychain access group の設定

ログインさせるアプリの widget では、アプリ本体と widget でクレデンシャルを共有する必要があると思います。そのために keychain access group を設定しました。以下のドキュメントを見れば基本的なことはわかります。なんとなく難しそうなイメージがあったけど単に entitlements を追加してグループ名を決めるだけだった。

developer.apple.com

細かい点で以下の記事に助けられました。

dev.classmethod.jp

widget 追加

widget を追加します。以下の一連の WWDC セッションを見れば何をやるかはだいたいわかります。

developer.apple.com

developer.apple.com

developer.apple.com

レイアウトでは普通に SwiftUI を書けばいいだけなので一瞬なんですが、いくつかはまりどころがありました。

まず、画像を非同期で読み込めないので以下の記事のように Data(contentsOf: url) を使う必要があります。

note.com

また、widget と アプリから共有の embedded framework を参照することになったのですが contains disallowed nested bundles というエラーになってしまいました。Build Phases に以下のスクリプトを追加することで回避できましたが、正直なんで直ったのかは雰囲気でしか理解してません。iOS のこの辺の話難しいんですよね...ちゃんとキャッチアップしたい。

stackoverflow.com

まとめ

widget 対応は意外と簡単なところも意外と大変なところもある。人生と同じですね(?)

ミーティング中かどうかを自動で妻に伝える

これは Kyash Advent Calendar 2020 12日目の記事です。今日は箸休め回です。

リモートワークをしていて急にミーティングが入ることがありますよね。自分は妻と一緒に暮らしているので、ミーティングが始まる前にその旨を伝えておかないと、妻がいきなりビデオチャットに登場してしまうリスクや、ミーティング中にいつの間にかご飯の時間を過ぎているなどで家庭内の雰囲気が引き締まるリスクがあります。これまでは都度口頭で伝えていたのですが、伝え忘れてしまうことやタイミングが悪くて伝えられないことがあるので自動で伝わってほしいなと感じ、ちょっと思いついた方法を試してみたら1時間くらいでできたので書き残しておきます。環境はMacです。

まず、伝達手段を決めます。最初、ミーティングが始まったらなんらかの方法でLEDを光らせたり音を出したりして伝えるか...?と思っていたのですが、冷静に考えると家庭内slackがあるのでslackで連絡すればいいということに気付きました。Incoming Webhookを使って適当なチャンネルにミーティング開始時/終了時にそれぞれ投稿することにします。Incoming Webhookについてはネット上にいくらでも情報がありますが、とりあえず以下のドキュメントに従っておけば大丈夫です。

api.slack.com

続いて、ミーティングの開始/終了を検知する方法を考えます。まずミーティング中かどうかですが、基本的にchromeを使ってgoogle meetでミーティングをすることが多いので *1chromeの開いているタブをすべて見て、その中にmeetが存在したらミーティング中と見なすのが良さそうです。調べてみたところ、これには chrome-cli というコマンドラインツールが使えそうでした。使い方はREADMEに載っていますが、とりあえず chrome-cli list tabs で開いているタブのタイトルが列挙されるのでその中に Meet を含むタイトルが存在するかでミーティング中かを判定できます。

github.com

それでミーティング中かどうかはわかるとして、ミーティング開始/終了を検知するためには、「ミーティング中かどうか」の状態が切り替わったことを知る必要があります。それには、「ミーティング中かどうか」を調べるスクリプトを定期実行して、

  • 前回実行時にミーティング中でなく、今回はミーティング中だったらミーティングが開始した
  • 前回実行時にミーティング中で、今回はミーティング中でなかったらミーティングが終了した

と考えれば大丈夫そうです。これを実現するためには前回のスクリプト実行時の結果を覚えている必要があります。いくつか方法が考えられますが、スクリプト実行時にミーティング中だった場合は決まったpathに一時ファイルを作ることにして、その次のスクリプト実行時には一時ファイルがあれば前回はミーティング中だったんだなと判断するのが簡単そうです。

ここまでの流れをスクリプトにしたものが以下です。簡単な処理なのでシェルスクリプトで書きました。

#!/bin/sh

flagFile="/path/to/flag_file"
slackWebhookURL="https://url.of/slack/webhook"

# slack()は引数として受け取ったメッセージをslack通知する
slack()
{
  curl \
  -X POST \
  -d "{\"text\": \"$1\"}" \
  $slackWebhookURL
}

# 今ミーティング中かどうか
isOnMeeting=false
if [ $(/usr/local/bin/chrome-cli list tabs | grep -c 'Meet -') -gt 0 ]; then
  isOnMeeting=true
fi

# 前回のスクリプト実行時にミーティング中だったかどうか
wasOnMeeting=false
if [ -e $flagFile ]; then
  wasOnMeeting=true
fi

if "$isOnMeeting" && ! "$wasOnMeeting"; then
  slack ミーティングが始まりました:baby_chick:
  if [ ! -e $flagFile ]; then
    # ここで一時ファイルを作成しミーティング中であることが次回のスクリプト実行時にわかるようにする
    touch $flagFile
  fi
elif ! "$isOnMeeting" && "$wasOnMeeting"; then
  slack ミーティングが終わりました:chicken:
  if [ -e $flagFile ]; then
    rm $flagFile
  fi
fi

あとはこのスクリプトを適当な間隔で実行すればOKです。Macで何らかのスクリプトを定期実行するには launchd を使うのが一番簡単だと思います。自分は以下の記事を参考に設定し、30秒間隔でスクリプトを実行しています。

shikiyura.com

エンジニア間でのちょっとした相談では突発的に短いミーティングが発生することがあると思いますが、そんなときもただミーティングをしているだけで自動的に家庭内slackに以下のような投稿が飛び、平和が維持されています。

f:id:muijp:20201209095814p:plain

*1:2020年11月くらいまで。現在のKyashではzoomへの移行が進んでおり、この記事の方法は引退が近づいています

Issue/PRに起こったイベントを取得できるGithub Timeline API

概要

最近GithubのIssue Trackerを自作してみようかなーと思っており、Issue/PullRequestで発生したイベントを取得できる Timeline API について調べたのでまとめておきます。

Timeline APIとは

GithubのIssue/PullRequestでは、誰かがアサインされたり、コメントがついたり、レビューされたりといったイベントが発生します。このイベントの一覧を取得できるのが Timeline API GET /repos/{owner}/{repo}/issues/{issue_number}/timeline です。Issue/PRの状況を取得してなんらかの通知をするようなシステムを独自で作るときに便利だと思います。完全に正確ではありませんが、GithubのUIで見られる以下の一連の表示がそのままAPIで取得できると思っておけばOKです。

f:id:muijp:20201208195549p:plain

ちなみに、ほぼ同じような機能を持っている Events API というAPIもあって使い分けが謎ですが、調べた限り以下の差分がありました。

  • Events API/Timeline API それぞれどちらかでしか取得できないイベントの種類/情報がある
  • Events APIはIssue/PRに関するイベントに加えてRepositoryに関するイベントを別途取得できるが、Timeline APIはIssue/PRについてしか取得できない

この記事ではEvents APIのことは気にせずTimeline APIについてのみ書きます。

APIの叩き方

Timeline APIを単純に叩くと以下のレスポンスが返ってきます。

{
    "message": "If you would like to help us test the Timeline API during its preview period, you must specify a custom media type in the 'Accept' header. Please see the docs for full details.",
    "documentation_url": "https://docs.github.com/rest/reference/issues#list-timeline-events-for-an-issue"
}

このAPIはまだpreview stateなので、専用のヘッダをつける必要があるとのこと。 ドキュメント にしたがって以下のヘッダをつけて送ることでレスポンスを得ることができます。

Accept: application/vnd.github.mockingbird-preview+json

また、プライベートレポジトリのIssueのTimelineを取得するにはAccess Tokenが必要です。 GithubのSettings からrepoの権限のついたTokenを発行して、以下のヘッダを付与することで、自分が権限を持ったプライベートレポジトリのTimelineもAPIから取得できるようになります。

Authorization: token <発行したToken>

レスポンス

Timeline APIを叩くとだいたいどういうレスポンスが返ってくるのか把握するために、 適当なrails/railsのPullRequest のTimelineを取得してみます。PR番号が40114なので、

GET https://api.github.com/repos/rails/rails/issues/40114/timeline

を叩くと、以下のようなレスポンスが返ってきます。あくまでだいたいどういうレスポンスが返ってくるのかを見るためにここに示しているだけなので、大したことがなさそうなフィールドは全部省略しています。実際のフルのレスポンスをみたい場合は実際にAPIを叩いてみてください。rails/railsはパブリックレポジトリなのでTokenは不要です。

[
    {
        "sha": "e0637a5a503fd624b5346c5c51adb447a23d8035",
        "node_id": "MDY6Q29tbWl0ODUxNDplMDYzN2E1YTUwM2ZkNjI0YjUzNDZjNWM1MWFkYjQ0N2EyM2Q4MDM1",
        "url": "https://api.github.com/repos/rails/rails/git/commits/e0637a5a503fd624b5346c5c51adb447a23d8035",
        "html_url": "https://github.com/rails/rails/commit/e0637a5a503fd624b5346c5c51adb447a23d8035",
        "author": {
            "name": "Adrianna Chang",
            "email": "adrianna.chang@shopify.com",
            "date": "2020-08-26T17:30:29Z"
        },
        "message": "Add attr_writer for credentials to Rails::Application",
        "event": "committed",
        ...
    },
    {
        "id": 475768545,
        "node_id": "MDE3OlB1bGxSZXF1ZXN0UmV2aWV3NDc1NzY4NTQ1",
        "user": {
            "login": "rafaelfranca",
            "id": 47848,
            ...
        },
        "body": "",
        "state": "commented",
        "html_url": "https://github.com/rails/rails/pull/40114#pullrequestreview-475768545",
        "event": "reviewed"
        ...
    },
    {
        "sha": "55a668db2937a5fe7b1c8b2f5e1913e54a16e427",
        "node_id": "MDY6Q29tbWl0ODUxNDo1NWE2NjhkYjI5MzdhNWZlN2IxYzhiMmY1ZTE5MTNlNTRhMTZlNDI3",
        "url": "https://api.github.com/repos/rails/rails/git/commits/55a668db2937a5fe7b1c8b2f5e1913e54a16e427",
        "html_url": "https://github.com/rails/rails/commit/55a668db2937a5fe7b1c8b2f5e1913e54a16e427",
        "author": {
            "name": "Adrianna Chang",
            "email": "adrianna.chang@shopify.com",
            "date": "2020-08-26T17:30:40Z"
        },
        "message": "Remove references to secrets.yml from documentation",
        "event": "committed",
        ...
    },
    {
        "id": 3696199379,
        "node_id": "MDIzOkhlYWRSZWZGb3JjZVB1c2hlZEV2ZW50MzY5NjE5OTM3OQ==",
        "url": "https://api.github.com/repos/rails/rails/issues/events/3696199379",
        "actor": {
            "login": "adrianna-chang-shopify",
            "id": 22918438,
            ...
        },
        "event": "head_ref_force_pushed",
        "created_at": "2020-08-26T18:59:38Z",
        ...
    },
    {
        "id": 3696281850,
        "node_id": "MDE1OlJlZmVyZW5jZWRFdmVudDM2OTYyODE4NTA=",
        "url": "https://api.github.com/repos/rails/rails/issues/events/3696281850",
        "actor": {
            "login": "rafaelfranca",
            "id": 47848,
            ...
        },
        "event": "referenced",
        "created_at": "2020-08-26T19:22:34Z",
        ...
    },
    {
        "id": 3696281853,
        "node_id": "MDExOk1lcmdlZEV2ZW50MzY5NjI4MTg1Mw==",
        "url": "https://api.github.com/repos/rails/rails/issues/events/3696281853",
        "actor": {
            "login": "rafaelfranca",
            "id": 47848,
            ...
        },
        "event": "merged",
        "created_at": "2020-08-26T19:22:34Z",
        ...
    },
    {
        "id": 3696281858,
        "node_id": "MDExOkNsb3NlZEV2ZW50MzY5NjI4MTg1OA==",
        "url": "https://api.github.com/repos/rails/rails/issues/events/3696281858",
        "actor": {
            "login": "rafaelfranca",
            "id": 47848,
            ...
        },
        "event": "closed",
        "created_at": "2020-08-26T19:22:34Z",
        ...
    },
    {
        "id": 3696425655,
        "node_id": "MDE5OkhlYWRSZWZEZWxldGVkRXZlbnQzNjk2NDI1NjU1",
        "url": "https://api.github.com/repos/rails/rails/issues/events/3696425655",
        "actor": {
            "login": "adrianna-chang-shopify",
            "id": 22918438,
            "node_id": "MDQ6VXNlcjIyOTE4NDM4",
            ...
        },
        "event": "head_ref_deleted",
        "created_at": "2020-08-26T20:07:17Z",
        ...
    }
]

イベントのリストが返ってきていますね。リスト要素のeventフィールドが、そのオブジェクトが表すイベントの種別です。今回の例だと、イベント種別は、上からcommittedreviewedcommittedhead_ref_force_pushedreferencedmergedclosedhead_ref_deletedですね。それぞれのイベント種別についてはこの記事の次項でまとめますが、 公式のドキュメント によくまとまってます。

event以外のフィールドを見ると、該当のイベントの詳細がわかります。例えば、committedイベントだと、shaからコミットハッシュが、messageからコミットメッセージがわかりますね。

イベント一覧

どのようなイベント種別が存在するか、それぞれのイベント種別にどのようなフィールドが存在するかを簡単にまとめておきます。自分から見て重要そうなイベントの重要そうなフィールドについてのみ書くので、この記事ではだいたいこんな情報が取れるんだなということを把握するのにとどめて、すべてのイベント種別についてちゃんと知りたい場合は 公式ドキュメント を参照してください。ただし、自分が見つけた範囲でもforce pushを表すhead_ref_force_pushedというイベント種別がドキュメントになかったり、イベント種別によって実際のAPIレスポンスとフィールドのキーが異なったりしたのでもしかしたら公式ドキュメントが実際の仕様に追従できていない部分があるかもしれません。

共通

ほとんどのイベント種別に共通のフィールド。

  • event: イベント種別
  • node_id: global node ididというフィールドも存在するが、イベント種別によってはidがないこともあるのでイベントのユニークなIDがほしいときはnode_idを使うのが良さそう(たぶん)
  • created_at: イベントの発生日時

assigned

Issue/PRがアサインされたこと。

  • actor: アサインした人
    • login: Githubのユーザネーム
    • id: GithubのユーザID
    • avatar_url: アイコンのURL
  • assignee: アサインされた人
    • login
    • id
    • node_id
    • avatar_url

committed

コミットのこと。

commented

Issue/PRにつくコメントのこと。

  • url
  • html_url
  • actor: コメントした人
    • login
    • id
    • avatar_url
  • body: コメントの本文

mentioned

メンションのこと。

  • actor: メンションされた人
    • login
    • id
    • avatar_url

メンションされた人の情報しかなく、メンションした人やメンションがあったコメントに関してはこのイベントでは取得できないみたいです。

review_requested

レビューリクエストのこと。

  • review_requester: レビューをリクエストした人
    • login
    • id
    • avatar_url
  • requested_reviewer: レビューをリクエストされた人
    • login
    • id
    • avatar_url

reviewed

レビューのこと。

  • user: レビュアー
    • login
    • id
    • avatar_url
  • body: レビュー時につけたコメント
  • state: レビューのstate。commented changes_requested approved のいずれか
  • html_url
  • author_association: レビュアーレポジトリ権限。 OWNER COLLABORATOR など

merged

PRのマージのこと。

  • actor: マージした人
    • login
    • id
    • avatar_url
  • commit_id: マージコミットのコミットハッシュ

closed

Issue/PRのクローズのこと。

  • actor: クローズした人
    • login
    • id
    • avatar_url

結論

Timeline APIはIssue/PRの流れがわかってうれしい。Issue/PRを定期的に監視してなにかあったらいい感じで通知するみたいなツールを自分で作りたいときに使えそうです。

情熱を失っても夢は諦められないこと

これは SHIROBAKO AdventCalendar 2020 5日目の記事です。

2020年はSHIROBAKOの劇場版が公開されましたね。ぜひとも劇場版について書きたいところなんですが、いろいろな状況により1回しか観ておらず、全体的に最高だったということ以外ほとんどを忘れてしまいました。ただ、平岡が楽しそうにやっていたところが印象的だったので、今日はアニメ版の平岡について振り返ってみようと思います。

劇場版ではやたらと爽やかだった平岡ですが、ムサニに入社してきてしばらくは本当にやばいやつなんですよね。社内でも社外でも態度が悪いし、りーちゃんにいちゃもんを仕掛けたり円さんとの喧嘩で大暴れしたり...。

f:id:muijp:20201202225512p:plain
会社でこんな表情になることあるんだ

平岡くんの夢

もちろん、平岡は最初からそんな人間だったわけではありません。専門学校の同級生の矢野さんから昔の様子が語られています。

f:id:muijp:20201202225917p:plain

平岡くんも磯川くんも絶対アニメの仕事がやりたいって燃えてた。二人とも大学出てきたから私より年上なんだけどね。磯川君は、あんまり授業に出てこなかったけど平岡君は真面目でさ。リーダシップもあって、文化祭でもみんなを仕切ってた。やる気のかたまりで、こんな作品やりたいって熱く語ってたなあ。

ムサニでの平岡は完全にやる気を失っていますが、自分にはやる気のかたまりの平岡の様子もなんとなく想像ができます。何があったのかはわからないですが、大学を出てからわざわざアニメの専門学校に行くなんてよっぽどアニメが好きだったんでしょう。

酔っ払った平岡自身もタローにかつての夢を明かしています。

f:id:muijp:20201205000846p:plain
野望を語ってます

俺の野望はな、アニメで初めてカンヌで「ある視点」部門の作品賞と国際批評家連盟賞をとるつもりだったんだよ!

平岡くんの現実

ということで夢と情熱を持ってアニメの仕事をはじめた平岡ですが、会社ではうまくいきませんでした。平岡の回想からは、職場に恵まれなかったことがわかりますね。もっといい会社に行けばよかったのではと思ってしまいますが、大学を出ている平岡は、年齢がそこそこにもかかわらずアニメータのようなわかりやすい専門性がないことがネックになって志望する会社には入れなかったとかかなーと想像しています。面接も苦手そうですし。

描いていた理想とのあまりのギャップに、しばらくすると平岡はこのような顔になってしまいます。

f:id:muijp:20201202230319p:plain
このような顔

最初は環境を恨んでいたと思いますが、そのうちにそんな職場にしか入れなかった自分や、職場を変えることができない自分への絶望に変わっていったのではないでしょうか。実際、平岡は仕事ができないわけではないと思いますが、宮森のように若くしてめちゃくちゃ有能というわけでもなさそうです。専門学校時代に描いていたであろう充実した仕事をすることはできず、夢だったカンヌなんてどう考えても無理だということに気づいた平岡はアニメへの情熱を失ってしまいます。

自分の人生これでいくぞと思ってたものに対する情熱がなくなっていくというのはつらいことです。平岡のムサニでの態度は本当にひどいと思いますが、そうなってしまうのもわかるんですよね。とくに、周りの人がちょっとうまくいったりしていると、昔は同じ状況にいたのにとか自分の方が思いが強いのにとか思っていらいらしたりしてしまうのはなんとなく理解できます。そのいらいらが一番顕著に描かれていたのは、専門時代の同期の磯川さんが会社にきたときです。よければ皆さんにも見直していただきたいんですが、このときの平岡の、名刺をスッ -> 荷物をバン -> 机をドン -> 一拍おいて目がうるうる、の流れが非常にリズミカルで芸術性が高いです(21話の8:30あたりから)。やりたいことをやるために会社を起こして、充実した日々を送っていそうな磯川さんと比べて、もともとは同じように持っていたはずの情熱を失って日々の仕事を雑にこなすだけの平岡がとても悲しく見える瞬間です。

f:id:muijp:20201205000426p:plain
宮森はちょっと平岡のことを気にしてますね

平岡くんは夢を諦めたのか

f:id:muijp:20201202235348p:plain
意外と素直に運転を引き受ける

平岡「あいつ、まだこの仕事に夢持ってんだよ。俺なんて、入って一年も経たずに夢醒めたけどな」

矢野さん「たまにいるよね、何十年もずっと夢が醒めてない人」

平岡「ああいるな、性懲りもなく」

矢野さん「私、そういう人が好き」

平岡「俺は嫌いだな」

矢野さんとの車のシーンで平岡は「夢から醒めた」と言っていますが、平岡がこんな状態になってまでアニメの仕事を続けている理由は逆に夢しかないはずだと思います。もちろん、自分には高い能力があり、社会人になってすぐ、すばらしい職場で、好きなアニメを作って大活躍する、という専門学校時代の理想は現実的じゃなかったんだということには気づいているし、新人時代のつらい経験により情熱も失ってしまっているのでそういうことを指して「夢から醒めた」と言っているんでしょう。それでも、アニメが好きであることはやめられないし、いいアニメを作るとか、カンヌに行くとかいう夢を完全には諦めていないからアニメの仕事を続けていると思うんですよね。アニメの世界を離れてしまえば夢が叶う可能性は0になってしまうし、それは夢を持った人には簡単ではない選択です。

ムサニに入った頃の平岡は、情熱を失ってから夢を諦めるまでの期間にいたのだと思います。矢野さんが宮森に「いま平岡くんを降ろしたら、ここで終わっちゃうような気がする」と言っていますが、終わっちゃうというのは平岡くんが夢を諦めてアニメ業界から去ってしまうということでしょう。

自分がSHIROBAKO全体の中でも好きなシーンに、平岡が夜の自宅で「世界アートアニメーション」なる本を開いている場面があります。

f:id:muijp:20201205001314p:plain

アニメ版の最後の数話では良い感じで仕事をするようになった平岡ですが、この時点では瀬川さんからのクレームで担当を外されそうになった直後だったりしてめちゃくちゃ調子が悪いんですよね...。調子の悪い時期でも、あるいは調子の悪い時期だからこそアニメの本を開いてしまうというのが、ほんとにアニメが好きなんだろうなというのが伝わってきます。想像ですが、きっと「世界アートアニメーション」に載っているような仕事は平岡自身の毎日と地続きには思えないだろうし、そこに近付く具体的な方法もわからないでしょう。それどころか、身近な同業者と比べてもひとまわり遅れている...そういうことを冷静に認識しながらも、心の奥底では専門学校の頃のようにアニメに向き合いたいし、いつか「世界アートアニメーション」に載るようなアニメを自分で作るという夢を持っているのだと感じました。

平岡くんと宮森

そんな平岡ですが、ムサニで働くうちに態度を改めていきます。これにはタローの存在とともに、宮森の助けも大きかったと思います。瀬川さんに平岡を担当からはずしてくれと言われた宮森は、平岡を続投させるよう瀬川さんにお願いします。

f:id:muijp:20201204194251p:plain

宮森「瀬川さんのお気持ちもあると思いますが...」

瀬川さん「それって、この次何があったら宮森さんが責任取ってくれるってことで、いいんだよね」

宮森「は、はい」

瀬川さん「具体的には、どうやって?エンドクレジットに作監の名前が出る意味わかる?良いものも悪いものも全部こっちの責任になるんだよ」

...

宮森「瀬川さんの平岡へ不信感の第一は、平岡が集めてきた原画マンへのものだと思います。ですから、瀬川さんがないと判断した原画マンは入れません。そしてリテイクを無責任に瀬川さんに丸投げするのはなしにします。あと、上がった原画は毎日お届けします。ためて渡して一気に上げてくれとか、無茶なお願いは絶対しません」

平岡はないなと思った時にちゃんと担当をはずしてくれと言う瀬川さんもプロだなと思うし、その発言も正しいし重いです。それに対してちゃんと自分の意見を通せる宮森はすばらしいですね。宮森からみた平岡って、年上で態度が悪い、急に入ってきた扱いづらい同僚だと思うんですよね。あと、この状況で失って打撃が大きいのはどう考えても平岡より瀬川さんな気がします。にもかかわらず、デスクとしての本来の業務が忙しいであろう中で平岡をリスクをとってまで救おうとする宮森の姿勢と、それを瀬川さんに受け入れさせる実行力はすごいです。ちなみに、個人的な経験では優秀な人はミーティングへの準備をちゃんとしてくるなと感じることが多いんですが、宮森もちゃんと瀬川さんを納得させるだけの準備を事前にしてますね。対応策を考えておくだけでなく、平岡にもちゃんと瀬川さんを説得する旨と、そのために変えてほしいことを伝えています。

f:id:muijp:20201204195925p:plain
謎のソーシャルディスタンス

宮森「瀬川さんは説得しますので、12話は予定通り、平岡さんと高梨さんでお願いします。ただし原画の割り振りは、瀬川さんや山田さんに相談して下さい。上がったカットは毎日届けて下さい。それから、ちゃんとコミュニケーション取って欲しいです。瀬川さんたちと」

平岡「...分かりました」

ここで、担当を続けられるということを告げられたときに平岡が驚いているのが印象的です。きっと今までの職場では、同じような流れから退職することになっていたんじゃないでしょうか。その状況から救ってくれた宮森の行動を受けて、平岡の行動も変わっていきます。

f:id:muijp:20201204201748p:plain
翌日の朝礼にちゃんと出る平岡

平岡くんの今後

ムサニに入った平岡はもともと持っていた仕事への姿勢を取り戻していきます。アニメ版ではそこまででしたが、劇場版では平岡が完全に情熱を取り戻した姿が描かれていましたね。タローという仲間も得て、その後の平岡が夢にどれくらい近づいているのか楽しみです。

f:id:muijp:20201203003359p:plain

Flutter Widget of the Weekを見たメモ

最近暇なときにFlutterを勉強してるんですが、これできないかなーと思って検索するとだいたいやりたいことができるWidgetがヒットするのですごい。このまま一生検索し続けてもいいが、事前に浅くても網羅的に知っておいた方が楽そうなので、Flutter Widget of the Weekというyoutubeを見ながらWidgetの概要ををメモしていく。

www.youtube.com

SafeArea

ノッチとかにかからない安全な領域にchildを閉じ込めてくれる。Safeにする方向を指定できるんですね...。

Wrap

行をはみ出さないようにいい感じに改行しつつ並べてくれるやつ。chipとかを並べるのに便利らしい。

AnimatedContainer

色とかborder、形みたいなContainerの見た目を変化させた時にinterpolateして自然なアニメーションにしてくれる。

Opacity

透明度を決めるWidget。要素を消したときに隙間を埋めたくないとき、単に消すのではなくOpacityを0にするという風にも使える。AnimatedOpacityというのを使えばこれも自然なアニメーションになる。

FutureBuilder

Futureの状態を見てViewを組み立ててくれる。Loadingアイコンを表示したいときとかに自前で処理を書かなくてもFutureごとこいつに渡すといい感じで表示してくれる。

FloatingActionButton

例のボタン。floatingActionButtonLocationというプロパティで位置を調整できる。

PageView

swipeでページを切り替えることができる。Navigatorで遷移するやつの子のレイヤーの遷移なのかな?

Table

Gridviewみたいなものだけど、スクロールはできない && より細かくレイアウトが指定できる。

SliverAppBar

スクロール位置によって表示/非表示や高さが変わるAppBarかな?CustomScrollViewというやつと一緒に使うみたい。楽しそうなので使ってみたい。

SilverList/SilverList

複数のListView/GridViewを一緒にスクロールさせることができる?遅延読み込みができるのでリストの要素が大量にあるときにも便利。

FadeInImage

ネットワークから画像を表示するときに、placeholderを表示するのに使える。

StreamBuilder

FutureBuilderのStream版かな。streamを渡しておくと決まり切った記述だけでいい感じにviewにしてくれる。ネットワークの状態とかも見られる。

InheritedModel

Providerと同じことを自分でやりたいときに使うと良いのかな?親に状態を持たせておいて子がそれを購読しているとき、子を再ビルドするかを細かい粒度で指定できる。

ClipRRect

render treeに差し込むことで子のborderRadiusを指定できる。ちなみに、同じようなことを他の形でやるClipPathやClipOvalというWidgetもある。

Hero

routes間の遷移で2つのWidgetを紐づけておくとアニメーションでいい感じにしてくれる。

CustomPaint

低レベルなWidget。独自の見た目/動きを持ったWidgetを使いたいときにこいつをextendsして使う。

Tooltip

Widgetをタップしたときに吹き出しをpopupしてくれる...だけでなく、読み上げとかにも対応しているのでアクセシビリティの観点からも良い。

FittedBox

あるWidgetを別のWidgetにfitさせたいときに使う。fitのさせ方は色々選べる。

LayoutBuilder

バイスの大きさ/向きによって表示するWidgetを出し分けたいときに使うのかな?

AbsorbPointer

すべての配下のWidgetについてタッチを無効にする。

Transform

いろいろなWidgetの形を変えたり、移動させたり、角度を変えたりできる。こいつを使うと3Dっぽいやつも含めていい感じのアニメーションが作れそう。

BackdropFilter

ImageFilterを適用するためのWidget。ぼかしたり歪ませたりなどの画像の加工ができる。Stackと合わせて使う(他のWidgetの上にかぶせる)と良い。

Align

Widgetの配置を決めるやつ。Centerのカスタムできる版みたいな感じ。

Positioned

Stackの中でWidgetの配置を細かく決めたいときに使う。top/bottom/left/right/height/widthがプロパティにあってcssのように位置が指定できる。

AnimatedBuilder

アニメーションを行う方法の1つ。animationとchildを渡すとアニメーションしてくれる。

Dissmissible

リストの要素を横にswipeして消すときに使えるやつ。方向を指定できるのでswipeの方向の左/右によって操作を変えることも可能。

SizedBox

Widgetを特定のサイズにしたいときに使う。SizedBox.expandを使うと可能な限り大きくなるWidgetを作れる。2つのWidget間に決まったスペースを作りたい場合にPadding/Marginではなく空のSizedBoxを置くこともある。

Draggable

ドラッグ&ドロップの実装に使う。ドロップ先にはDragTargetを置いておく。ドロップ時に何が起こるかはDragTarget側に指定する。

AnimatedList

リストの要素を削除したり編集したりするとき、急に表示が変わると何が起こったのかわからないことがある。AnimatedListにanimationを渡すことでいい感じにしてくれる。

Flexible

flex値を指定しておくと親の大きさに合わせていい感じにスペースを占有してくれる。Expandedとの違いは、余白を完全に埋めないことができることらしい。

flutterで均等にwidgetを並べる|Captain_PAG|note

MediaQuery

バイスに関する情報を取得してくれる。画面サイズや文字サイズ、アニメーションを有効化してるかなど。

Spacer

Row/Columnの要素の間に差し込むことで並び方をカスタムできる。flexプロパティを指定できる。

AnimatedIcon

animationを渡すことでiconをアニメーションで変化させることができる。

AspectRatio

Widgetを特定のアスペクト比で表示したいときに使う。Expandedのchildとして使うとうまく動かないが、間にAlignを挟むことでちゃんとアスペクト比が保たれるようになる。

LimitedBox

Containerのように親がサイズを指定するWidgetは、子のサイズを指定しないWidgetの子になるとうまく動かない。このときにLimitedBoxを使ってサイズの最大値を指定しておくと良い。親がサイズを指定してきた場合にはLimitedBoxは何もしない。

Placeholder

placeholderです。何も指定しなければ可能な限り余白を埋める。

RichText

1行のテキストの中でTextStyleを変えたいときにはRichTextのchildをTextSpanにするとできる。文中にリンクを差し込みたいときとかにも使える。

ReorderableListView

ドラッグ&ドロップで要素の順番を変えることができるリスト。

AnimatedSwitcher

あるWidgetを別のWidgetに置き換えたいときに使う。例えばAnimatedSwitcherのchildを変数にしておいて、setStateでchildに代入すればFadeアニメーションでWidgetを入れ替えてくれる。遷移のアニメーションは好きなようにカスタムできる。

AnimatedPositioned

paddingをアニメーションで変えることができる。curve/durationを指定して動きをカスタムできる。

IndexedStack

表示するWidgetを切り替えたいときに使う。切り替えたときに状態を保存しておいてくれる。タブ切り替えとかに使うのかな?

Semantics

Widgetにメタ情報を足すためのWidget。足した情報はアクセシビリティツールや検索エンジンが使う。

ConstrainedBox

Widgetのサイズの最大値や最小値を決める。

AnimatedOpacity

WidgetをFade in/Fade outさせることができる。これを使ったより高レベルなWidgetにFadeTransitionがある

FractionallySizedBox

サイズを親Widgetに対する割合で指定することができる。

DataTable

いい感じの表を作れる。項目でソートできたりする。

Slider

スライダーです。RangeSliderで両端をスライドさせて範囲を選べるようにできる。

AlertDialog

alertです。警告を表示するだけでも良いし、ユーザアクションを取らせることもできる。showDialogで描画する。iOS風にしたければCupertinoAlertDialogを使う。

DefaultTabController & TabBar

タブ切り替えを作れるやつ。

モデリングカフェの問題をやってみる:第5回〜第8回

誰にも頼まれてないのに新卒研修みたいなことをやっていくシリーズの第2弾です。

第5回:すごろく

問題

すごろくをモデリングしてください。

不足する情報は適宜補っていただいて結構です。補った情報は、コンセプトに記述してください。

f:id:muijp:20200104114439p:plain

自分の回答

コンセプト

  • すごろくは「ふりだし」「あがり」を含めて2つ以上のコマからなる
  • コマの内容は以下
    • ふりだし/あがり
    • Nコマ進む/戻る
    • ふりだしに戻る
    • M回休み
  • すごろくのプレイヤーは一度に1つのすごろくまでしか遊べないものとする
  • プレイヤーは次にサイコロを降るプレイヤーを持つ。ゲームの途中でプレイヤーが抜けたり入ったりした時はこの関係を付け替える

クラス図

f:id:muijp:20200104143137p:plain

回答例・解説を読んだ感想

  • すごろくに分岐がないということをコンセプトに明記するべきだった(すごろくのドメイン知識がなさすぎる)
  • 全体的に1つのクラスに役割を負わせすぎる傾向がある気がしてきたので、もうちょっと役割ごとにクラスを分ける方針のほうが良さそう
    • コマと指示を分けたほうがよかった?
    • プレイヤーとプレイヤーのコマを分けたほうがよかった?
  • コマをサブクラスで分類したのはよかった

第6回:登山ルート

問題

登山ルートをモデリングしてください。

奥岳山系の登山ルートは次のようになっています。

f:id:muijp:20200104142755p:plain

自分の回答

コンセプト

  • 登山ルートは一つの山に対して複数定義される
    • ここで言う山は、例にある「柳ヶ原山」「屏風岳」といった単位ではなくそれらをまとめたもの(一回の登山でいける範囲)をさす
  • 登山地点間を結ぶ道には注意事項をつけることができる

クラス図

f:id:muijp:20200104143119p:plain

回答例・解説を読んだ感想

  • 全体的によくできた気がする
  • 山という概念がわかりにくかったかも。山系とかにしたらコンセプトでごちゃごちゃ言う必要がなかった
  • 注意事項を地点関経路にひもづけたけど、往路・復路で注意事項は同じだと思うので冗長だった。マスターの回答のように区間と順路を分けて定義した方がきれい

第7回:カレーの作り方

問題

カレーの作り方をモデリングしてください。

不足する情報は適宜補っていただいて結構です。補った情報は、コンセプトに記述してください。

f:id:muijp:20200104143348p:plain

自分の回答

クラス図

f:id:muijp:20200104145652p:plain

回答例・解説を読んだ感想

  • 根本的に勘違いしていて、手順は一連の文章で良いかと思ったらそこをちゃんとモデリングしろという話だった。まあこういうこともあるでしょう
  • 回答が高度すぎる

第8回:サンドイッチ

問題

サンドイッチをモデリングしてください。

不足する情報は適宜補っていただいて結構です。補った情報は、コンセプトに記述してください。

f:id:muijp:20200104145905p:plain

自分の回答

コンセプト

  • サンドイッチは形・パン・中身の材料から決まる
  • 材料は素材とその量、挟む順番で決まる

クラス図

f:id:muijp:20200104150904p:plain

回答例・解説を読んだ感想

  • 中間にパンが挟まれるサンドイッチのことを考慮漏れしていた...
  • サンドイッチクラスにパンと材料をまとめさせるよりも、みんなの回答みたいにパンと材料の関係だけでサンドイッチを表現させる方が良いのかな?でもその場合サンドイッチの名前とか形を表すのが難しくなると思う
  • 分量をわざわざクラスにする必要はなかったのかも