- 投稿日:2020-07-22T21:33:48+09:00
初めてのiPhoneアプリをつくってわかったこと。とっかかりは家族の誕生日
2020年中にiOSエンジニア転職を目指している、imafumiです。
「自分でつくってみる」ことに、「とっかかり」がなかったのが、仕事の帰り道にふとした思いつきで、着手できたお話です。
自分と同じくポートフォリオになかなか取り掛かれない初学者の方に、「こんなんでもとりあえず作った方が良さそうだな」と思っていただければ幸いです。
私はとりあえず作った方がいいと思いつつも、自分がつくりたいものと自分のスキルが解離しすぎていて、何から手をつければいいかわからない状態でした。
その頃の自分に言ってあげたい。
「こんなレベルでも、家族には喜んでもらえたよ。」
「具体的な家族(身近な人)に向けてと考えると、必要なスキルはそう多くなく、しかもリアクションがモチベーションと勉強につながるよ」
「とりあえず作ると、改善点はいくらでも思いつくよ。」家族の誕生日に何か特別なことができないか
家族の誕生日当日、仕事の帰り、すでに誕生日プレゼントはその日までに渡していたものの、当日何もしないのもなあ・・・そうだ、世界に一つのアプリを作ってプレゼントしようと、ふと思いつく。
こんなんだったら、作れるかも。こんなんだったら、すぐにできて、喜んでもらえるかも。
- お祝いされる人へ「誕生日おめでとう」と表示
- 家族の名前のボタンがあり、押すと祝う人の写真が表示される。
- お祝いする家族それぞれのお祝いのメッセージが表示される。
すごくシンプル。今ならできそう。
誕生日は残り3時間。その間に作りきる。
主なコードはこれだけ。
import UIKit class ViewController: UIViewController { @IBOutlet weak var person: UIImageView! @IBOutlet weak var textMassage: UILabel! //以下、家族人分。メッセージ内容は恥ずかしいので、変えてます。 var XXXXMessage = "いつも、ありがとう" @IBAction func toXxxx(_ sender: UIButton) { person.image = UIImage(named: "xxx" ) textMassage.text = xxxxMessage } }結果、なんとかつくって23:58に見せることができました(ギリギリ)
相手は、ベッドでうつらうつらしていたので、「すごーい・・・」スヤスヤという感じのリアクションでしたが嬉しかった!(迷惑でごめんなさい)
次の日の朝、「なんか昨日見せてもらった気がするけど、夢かな?」と言っていました。
少し改良したものを見せて、今度はしっかりと喜んでくれました。リアクションから意外と重要だったアプリのポイント
アプリのアイコンの見た目がインパクト半分
寝ぼけてみたときはほとんどここに対する反応(「かわいー」)だった気がする。
※アイコンはネットで探したHappy Birthdayのカラフルな文字画像。どこをさわればいいか、ぱっとわかるようにするのは難しい
構成要素が主に3つしかないのに、それでも一瞬どうすればいいのか迷っていた。同じことを繰り返しても(同じボタンを連打しても)何か変化があるようにする
4歳の娘は同じボタンを連打し、動かない画面に不思議そうにしていた。(当初一つのボタンに一つしか写真とメッセージを用意していなかったので、同じボタンを押しても、同じ画像と同じ文字で動かなかった)//import以降のまったく変えていないところは省略しています。 var num = 0 func numPlus() { num += 1 if num == 3 { num = 0 } } //以下、家族人分。メッセージ内容は恥ずかしいので、変えてます。 var XXXXMessage = ["いつもありがとう。", "お誕生日おめでとう", "これからもよろしくー"] @IBAction func toXxxx(_ sender: UIButton) { //画像3つ(xxx0,xxx1,xxx2)用意。 person.image = UIImage(named: "xxx\(num)" ) textMassage.text = xxxxMessage[num] numPlus() }
ひとまず作りきった。でも、反応をみるとドンドン変えたいところが出てくる!
上にも書きましたが、実際の反応をみると、気づくことがいっぱい。
とりあえず、以下の点を次の日の朝すぐ変えました。
- どこを押せばいいかはっきりと文章で書き、目立つようにした。レイアウトも変えた。
- 一人の家族につき、3つの画像と3つのメッセージを用意して、ループさせた(同じボタンを押しても、どんどん画面が変わる)
- ボタンを押す画面の前に、起動画面(黒い背景に白い文字で 「〇〇、誕生日おめでとう!」)を追加した。
まだまだいっぱいあふれてきました。一部、下の「おまけ」にメモしています。
そのほかに気づいたこと
つまったところ
- 画像が横をむいた
- 背景色と文字色のうまい組み合わせが難しかった。意外と配色は難しい
- 本では見返せるが、Udemyだと見返すのは難しい
- 本の箇所を思い出すのは意外に難しい
- メソッド以外のところで式を書こうとしてエラーになった
- 最初の画面がすぐに消えたので、みえなかった
- エラーが出た(拡張子大文字)
- labelでは一行しか入力できなかった
- UITextViewで明朝体の表示になった
- オートレイアウトしたら、逆にぐちゃぐちゃになった
- アプリのアイコンがうまく変換できなかった文字の切り替えにforを使おうとしたらうまくいかなかった(インクリメントとif文で解決)
今後の改善検討点
- 画像を取得できるようにする 一人3コ (別プランなら10コ?)
- メッセージを取得できるようにする 一人3コ(別プランなら10コ?)
- 画像とメッセージのセットをランダムに取り出す
- 少しずつフェードインするようなエフェクトをかける
- 最初の表示でクラッカーをならす
- 最初の表示でポーン(プロフェッショナルの音のイメージ)と音を出す
- ロウソクをタップで消せるゲームをつくる
- レイアウトをiPhone7以外にも適用する
- Twitterやフェイスブックに投稿できるようにする
- zoomに共有できるようにする
長くなってしましました。
最後まで読んでいただき、ありがとうございました。
- 投稿日:2020-07-22T20:56:09+09:00
leadingAnchor/trailingAnchorとleftAnchor/rightAnchorの違い
コードベースで書いていて少し気になりました。知っている人も多いと思いますが、僕は知らなかったので書いておきます。
はじめに
Anchor:錨。Swiftでは、端っこという認識で大丈夫です。
constraintについては、以下では制約という言葉を使います。本題
leadingAnchorのlead:先頭
trailingAnchorのtrailing:末端察しのいい人ならわかるかもしれません。
フォーム(LabelやTextField)をつくる場合
英語は左から右に読むので、
leading:先頭は 左 trailing:末端は 右
ということになります。逆に、右から左に読むヘブライ語は、
leading:先頭は 右 trailing:末端は 左
ということになります。Appleの公式の見解
基本的には、leftAnchor/trailingAnchorを使うべし。
絶対的な左と右の指定がない限りは、leading/trailing を使うべきだそうです。
例えば、右から左に進んでいくアプリを作るときなどに、ボタンがどうしても左に必要な場合は、leftAnchor/rightAnchor の制約を使うようにしましょう。2020 7/26 追記
英語圏の方のソースコードを拝見したところ、絶対に固定しなければならないケースでは無いのにleftAnchor/rightAnchorのほうが使われている場合もありました。
神経質になる必要もないのかもしれません。終わりに
コードベースで書いていたときに気になったので、調べて忘れないようにアウトプットしておきました。間違いや気になる点がありましたら、コメントにておねがいします!
- 投稿日:2020-07-22T20:56:09+09:00
leadingAnchor/trailingAnchorとleftAnchor/rightAnchorの違いと使い分け
コードベースで書いていて少し気になりました。知っている人も多いと思いますが、僕は知らなかったので書いておきます。
結論
どっちでも良いことが多く、そこまで神経質にならなくていい。
はじめに
Anchor:錨。Swiftでは、端っこという認識で大丈夫です。
constraintについては、以下では制約と言います本題
leadingAnchorのlead:先頭
trailingAnchorのtrailing:末端察しのいい人ならわかるかもしれません。
フォーム(LabelやTextField)をつくる場合
英語は左から右に読むので、
leading:先頭は 左 trailing:末端は 右
ということになります。逆に、右から左に読むヘブライ語は、
leading:先頭は 右 trailing:末端は 左
ということになります。Appleの公式の見解
基本的には、leftAnchor/trailingAnchorを使うべし。
絶対的な左と右の指定がない限りは、leading/trailing を使うべきだそうです。
例えば、右から左に進んでいくアプリを作るときなどに、ボタンがどうしても左に必要な場合は、leftAnchor/rightAnchor の制約を使うようにしましょう。2020 7/26 追記
英語圏の方のソースコードを拝見したところ、絶対に固定しなければならないケースでは無いのにleftAnchor/rightAnchorのほうが使われている場合もありました。
神経質になる必要もないのかもしれません。終わりに
コードベースで書いていたときに気になったので、調べて忘れないようにアウトプットしておきました。間違いや気になる点がありましたら、コメントにておねがいします!
- 投稿日:2020-07-22T20:56:09+09:00
leadingAnchor/trailingAnchorとleftAnchor/rightAnchorについて
コードベースで書いていて少し気になりました。知っている人も多いと思いますが、僕は知らなかったので書いておきます。
はじめに
Anchor:錨。Swiftでは、端っこという認識で大丈夫です。
constraintについては、以下では制約という言葉を使います。本題
leadingAnchorのlead:先頭
trailingAnchorのtrailing:末端察しのいい人ならわかるかもしれません。
フォーム(LabelやTextField)をつくる場合
英語は左から右に読むので、
leading:先頭は 左 trailing:末端は 右
ということになります。逆に、右から左に読むヘブライ語は、
leading:先頭は 右 trailing:末端は 左
ということになります。Appleの公式の見解
基本的には、leftAnchor/trailingAnchorを使うべし。
絶対的な左と右の指定がない限りは、leading/trailing を使うべきだそうです。
例えば、右から左に進んでいくアプリを作るときなどに、ボタンがどうしても左に必要な場合は、leftAnchor/rightAnchor の制約を使うようにしましょう。終わりに
コードベースで書いていたときに気になったので、調べて忘れないようにアウトプットしておきました。間違いや気になる点がありましたら、コメントにておねがいします!
- 投稿日:2020-07-22T18:03:10+09:00
【Flutter】Firebase を使って SNS っぽいアプリを作るために学んだこと
趣味の魚捌きの延長で、魚を捌くのが好きな人のための SNS アプリ、 "Sengyo" を作ってリリースしました。
iOS
https://apps.apple.com/us/app/id1523325680Android
https://play.google.com/store/apps/details?id=com.tsuyoshichujo.sengyoproductFirebase を始めとした今まで雰囲気で使っていた技術の勉強や OS の新機能などの実験台として何かひとつ具体的なアプリがあると良いな、というモチベーションで作ったこのアプリですが、ひとまずアプリとして最低限形になったので、まずはこのアプリを作るために Firebase の使い方で学んだことを振り返ってみたいと思います。
なお、この記事では どのようなページを見てどのような情報を得ながらアプリを作ったか という内容が中心で、「こうプログラムを書けばこう動く」というような具体的なところまでは書いていません。
「同じようなアプリを作ってみたいけど、何をどう調べてどう作り始めたら良いかイメージできない」という方の役に立てればと思って書いた記事です。
学んだこと
このアプリはアプリ本体を Flutter で、サーバーサイドを Firebase で作っています。 Firebase はさらに細かく見ると、
- Firestore
- Cloud Storage
- Firebase Authentication
という3つのプロダクトを利用しています。
この記事では、 Sengyo アプリでこれらの Firebase の各プロダクトを使うために何を見てどのような情報を得たのかをそれぞれまとめていきます。
なお、Sengyo アプリのコードは GitHub に上げてあります。必要に応じて参照してみてください。(ただし「良いコード」ではない点に注意です!)
Firestore
Firestore は文字列や数値、日時など様々な型のデータを保存することができるデータベースです。
Sengyo アプリでは、主にユーザーの入力した投稿データを保存するデータベースとしてこの Firestore を利用しています。
SQL 脳から脱却する
そんな Firestore を使う上でまず知っておく必要があるのが、 Firestore は MySQL や Oracle のような RDBMS とは違い、 JSON 形式でデータを管理する NoSQL データベース であるということです。
NoSQL データベースを使ってすでに何かプロダクトを作ったことがある方にとっては当たり前のことかもしれませんが、表形式の RDB と JSON 形式の NoSQL ではデータ構造の設計方法や考え方が全く異なります。自分は NoSQL でプロダクトを作る知見も経験もほぼなかったため、まずはそこから調べる必要がありました。
ということでまず見つけたのが↓の動画です。
The Firebase Database For SQL Developers | Youtube
この動画では SQL を知っている開発者向けに NoSQL を説明しています。Firestore に限らず「NoSQL とは」を SQL と比較する形で説明してくれているので、アプリの SQLite やバックエンドの MySQL などを触ったことのある自分にとってはちょうど良い内容でした。
詳しくは動画を観ていただければと思いますが、ここでは
- RDB ではサービスの機能に依らずデータそのものに着目して正規化する一方で、 NoSQL ではサービスが一番必要とする形式のままデータを保存すること
- 同じデータが複数箇所に保存されることを怖がる必要はないこと
- SQL を NoSQL に置き換える例
など、 SQL 脳を脱却するための考え方を得ることができました。
動画も1つ数分で終わるライトなものなので、それほど身構えずに観られるのも良い点です。
Firestore という製品を知る
NoSQL データベースというジャンルについての理解ができたら、次は Firestore というプロダクトについて知る必要があります。
これも Firebase が公式で動画を出しているので、順番に観ていきます。
Get to Know Cloud Firestore | Youtube
Firestore は NoSQL の考え方をベースに、様々な機能や設計を取り入れています。例えば
- 「ドキュメント / コレクション」ベースのデータ管理
- 配列型とコレクションの使い分け
- リアルタイム アップデート
- オフライン時のデータアクセス
- セキュリティルール
などです。
上記の動画はこれらの機能をざっと把握するのにとても役に立ちました。若干スピーカーの英語が早口なのと、所々でちょっとしたジョークが入って置いていかれることがあったものの、この内容は Firebase のドキュメントに文章としてもまとまっているため、分からない部分はそちらで補完していけばだいたい大丈夫でした。
Flutter で Firestore を扱う
NoSQL の考え方と Firestore という製品がある程度できたら、あとは Flutter アプリから使ってみるだけです。
Flutter で Firestore を扱うための
cloud_firestore
プラグインが公開されているため、これを使います。単純なデータの出し入れの方法はパッケージのサンプルコードを見ればだいたい使い方は分かるようになっています。まずは自分の Google アカウントで Firebase プロジェクトを作って README の通りに動かしてみると良いでしょう。Firebase はちょっと触ってみる分には無料で使えるようになっています。
「参照型のフィールドを
setData()
したい場合にはDocumentReference
型のインスタンスを value として渡す」など、細々としたところで README には書かれていない部分もありましたが、実際に動かしながら確認すればそれほど難しいことはなかった記憶です。なお、Sengyo アプリでは Firestore のデータを出し入れするコードは以下のあたりです。
https://github.com/chooyan-eng/Sengyo/blob/master/lib/repository/article_repository.dart
何かの参考になれば。
Cloud Storage
Cloud Storage は静的なファイルの保存場所として使える製品です。Sengyo では画像ファイルの保存に利用しています。
Cloud Storage も Flutter 向けにパッケージが公開されていますので、 Firestore と同じようにサンプルコードを見ながら動きを確認すればそれほど大きな問題はないと思います。
Cloud Storage はフォルダを作ってファイルを保存するシンプルなもので、基本的には Firebase のドキュメントとパッケージの README を読めばある程度使えるようになっています。
Sengyo アプリでは以下のあたりのソースコードが参考になるかと思います。
https://github.com/chooyan-eng/Sengyo/blob/master/lib/repository/image_file_repository.dart
Firebase Authentication (メールリンク認証)
Firebase Authentication はユーザーの認証をするための仕組みを提供してくれる製品です。これを使うことでログイン機能が簡単に、安全に実現できます。
一言に「認証」と言っても、ドキュメントを見ると分かる通りその方法は様々で、シンプルな ID/パスワード で認証する方法から電話番号、SNS認証、また匿名認証というものも用意されています。
そのため、まずはそれぞれの認証方法に必要なユーザー情報やメリット・デメリットを理解した上で、サービスの要件(想定ユーザーの使いやすさ、セキュリティ要件など)に応じて利用する方法を取捨選択していく必要があります。
Sengyo アプリでは、管理コストの問題からなるべく少ない情報のやりとりでログインを実現したいことや、まずは分かりやすく1通りだけユーザーにログイン方法を示したかったため、メールリンク認証 を採用しています。
この記事でもメールリンク認証について書いていきます。
メールリンク認証とは
メールリンク認証は、ユーザーがアプリから入力した メールアドレスに対してログイン用リンクを送信 し、メーラーで リンクをタップすればアプリが開いてログインが完了する 、という仕組みです。
ユーザーが入力されたメールアドレスの持ち主であることを根拠に本人を認証する方法で、ユーザーとしてはパスワードを入力する必要もなく手軽に安心してアカウントを作成できる、というわけです。
このあたりは、 Firebase のドキュメントを読むことである程度イメージができるかと思います。
iOS でメールリンクを使用して Firebase 認証を行う | Firebase
Android でメールリンクを使用して Firebase 認証を行う | Firebase一方で実装面では若干手順が多くなるため、ひとつひとつ何が起きているのかを整理して理解する必要があります。
メールリンク認証の流れ
まずは、ユーザーがメールアドレスを入力したあと、裏でどのようなことが起こっているのかを理解します。
同じことは Firebase のドキュメントでも説明されていますが、実際の処理の流れに加えて Firebase コンソール上の設定方法なども織り交ぜて説明されているため、ここでは事前準備などは省いて実際のサービスで何が起きているか、に着目して流れをまとめてみます。
- ユーザーのメールアドレスにログイン用のリンクが含まれたメールを送信する
- ユーザーがメール内のリンクをタップして開く。
- アプリが開かれたリンクを拾ってアプリを起動する
- リンクに含まれるリクエストパラメータとメールアドレスをセットにして Firebase へ送信する
- Firebase がリクエストパラメータとメールアドレスのセットが正しいことを検証する
- 検証の結果がアプリに通知される
ここで、3の「開かれたリンクを拾ってアプリを起動する」を実現するために、もう1つ別の Firebase 製品である Firebase Dynamic Links を利用します。そのため、まずは Firebase Dynamic Links について学ぶ必要があります。
Firebase Dynamic Links とは
アプリには、ユーザーが特定の URL を開こうとした時にブラウザではなくアプリを起動する仕組みがあります。これを、 Android では ディープリンク、iOS では Universal Link と呼んでいます。
この仕組みをアプリに取り入れるためには、アプリ側だけでなくサーバー側にも「その URL の保有者とアプリの開発者が確かに同一であること」を証明するための設定が必要であったり、アプリがインストールされていなかった場合の挙動を定める必要があったり、またそれらの設定を Android / iOS それぞれのプラットフォームの仕様に従って行わなければならないなど、様々な労力が発生します。
Firebase Dynamic Links は、そのあたりの面倒な設定を Firebase コンソール上で一括で行ってくれるサービスです。
Firebase コンソール上の "Dynamic Links" メニューから上記のような設定項目(iOS / Android それぞれについての、リンクで起動するアプリやインストールされていない場合の挙動など)、トラッキングの有無などを設定して Dynamic Link を作成するだけなのでとても便利です。
ただしこれをメールリンク認証で使う場合の設定方法についてはメールリンク認証のドキュメント内に概要だけ書かれていたものの、具体的にどの項目をどう設定すれば良いかの記述は見つからなかったため、今回は割と雰囲気と試行錯誤で設定しました。
このあたりは追って別の記事で詳しくまとめてみたいと思います。
アプリ の実装
メール認証と Dynamic Links の設定が Firebase コンソール上でできたら、次にアプリの方もいろいろ実装する必要があります。
このあたりも先ほどの Firebase のドキュメントを見ながら、 iOS は XCode から
Associated Domain
の設定などを追加、 Android はAndroidManifest.xml
ファイルにIntent-Filter
を追加して、指定したドメインのリンクが開かれたときに、ブラウザではなく自分のアプリが開かれるようにしていきます。次に、「メールリンク認証の流れ」で書いた通り、アプリのプログラムとして
- メールを送信する処理
- Dynamic Link からアプリが開かれたことを判定する処理
- Firebase Authentication へリンクとメールアドレスを送信する処理
を実装します。
これも
firebase_auth
という Firebase Authentication 全般を Flutter で扱うプラグインが公開されているため、これを使います。ただし、メールリンク認証についてはサンプルコードや README では触れられていないため、 API ドキュメントやコードを読んで適切なメソッドを見つける必要があります。
FirebaseAuth class | firebase_auth
結論から言うと、アドレスを指定してログイン用のメールを送るためには
sendSignInWithEmailLink()
を、 Dynamic Link でアプリが開かれたあと FirebaseAuth にリンクとメールアドレスを送って認証するためにはsignInWithEmailAndLink()
メソッドを利用します。さらに、 Dynamic Link からアプリが開かれた際の処理については、
firebase_dynamic_links
というプラグインが別に用意されているため、こちらを使います。firebase_dynamic_links | pub.dev
基本的な使い方や考え方は README に書いてある通りです。
注意点としては、 Dynamic Link からアプリが開かれたとき、すでに裏でアプリが実行中で初期化処理などが実行されないパターンも考慮する必要がある、ということです。この点についてはサンプルコードでも
FirebaseDynamicLinks.instance.onLink
で Dynamic Link から起動された時に呼び出されるコールバックを初回起動時に登録しておくコードが書かれていますので、参考にすると良いでしょう。また、このサンプルコードはメールリンク認証で使うことを考慮したものではないため、うまく
signInWithEmailAndLink()
メソッドとつなげてあげることや、ログイン後の処理に自分のアプリの要件に応じてつなげてあげる必要があります。このあたりはサンプルコードも少なく試行錯誤した上で実装しましたが、一応 Sengyo アプリとしては以下の
LoginBloc
クラスにまとめたような実装になっていますので、何かの参考になれれば嬉しいです。https://github.com/chooyan-eng/Sengyo/blob/master/lib/bloc/login_bloc.dart
まとめ
以上、 Sengyo アプリを作るために利用した Firestore, Cloud Storage, メールリンク認証 をそれぞれ使うために参照したものや学んだことをまとめてみました。
細かなソースコードレベルの説明や具体的な設定の手順などは最初に書いた通り省いていますので、そのうちそれぞれについては別の記事として詳しく書いてみたいと思います。特にメールリンク認証についてはそのまま参考にできる記事などが全然見つからなかったため、優先的に書きたいところです。
Firebase と Flutter は、ちょっとした個人アプリをささっと作ろうと思ったときにとても役に立つ組み合わせだと思います。
初めて触る人にとっては馴染みのないものもあるかもしれませんが(もしかしたら最近は Firebase から入るような人も多いかもしれませんが)、この記事でまとめたように参考にできる記事や動画はたくさん公開されていますので、それらをひとつずつ見ながら実際に触ってみることでとりあえずひとつのアプリとして形にできる環境が整っています。
「それっぽい SNS アプリをとりあえず作ってみる」ために、何をどれくらい知る必要があるのか、この記事を読んでイメージが伝えられれば嬉しいです。
- 投稿日:2020-07-22T17:32:23+09:00
Amazon Kinesis Video Streams WebRTC をiOSとAndroid動かしてみた
はじめに
Amazon Kinesis Video Streams に ビデオチャットサービスが加わりました。
ブラウザ(JavaScript)向けのSDKだけでなく、組み込み用途のC言語SDKや、iOS/Androidといったモバイルアプリ向けのSDKも用意されており、ビデオチャットサービスを実装することができます。
今回は iOS, Android のサンプルを動かし、iOS <-> Android アプリ間でビデオ通話をしてみようと思います。Getting Started
iOS SDK
Android SDKAWSでリソースを作成する
AndroidのSDKのREADMEの手順が分かりやすかったので、それに倣って進めていきます。
Amazon Kinesis Video Streams Android WebRTC SDK README.md
シグナリングチャンネルを作成する
Kinesis Video Streams でシグナリングチャンネルを作ります。
CognitoUserPool を作成する
CognitoUserPool を作成します。
プール名を入力します。今回は[MyUserPool]としました。プール名を入力したら[デフォルトを確認する]を選択し、次のページでデフォルト値を変えずに[プールを作成]を選択します。
新しいアプリクライアントを作成します。まずアプリクライアント名を入力します。ここでは[MyAppClient]という名前で作成しました。
アプリクライアントの作成が完了したら[アプリクライアント ID]と[アプリクライアントのシークレット]をメモします。[詳細を表示]を選択すると表示されます。
CognitoIdentityPool を作成する
CognitoIdentityPool を作成します。
[認証プロバイダー]の[Cognito]のタブを開き、[ユーザープールId]と[アプリクライアントId]に先ほどメモをした値を入力し、作成します。
Cognito_MyIdentityPoolAuth_Role
とCognito_MyIdentityPoolUnauth_Role
の2つロールが作成されます、一つはログイン済みユーザーに付与されるロールで、もう一つはログインされていないユーザーに付与されるロールです。つまりログイン済みのユーザーはkinesisvideo
にアクセスできるように以下のポリシーをアタッチします。{ "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":[ "cognito-identity:*", "kinesisvideo:*" ], "Resource":[ "*" ] } ] }Android
AWSのリソースの作成ができたので、Android のサンプルアプリをビルドしていきます
- Androidのサンプルプロジェクトをダウンロードします。
$ git clone https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-android.git
AndroidStudio を起動し、 Open an existing Android Studio project で先ほどダウンロードしたサンプルプロジェクトを開きます。
/res/raw/awsconfiguration.json
にメモしたIdentityPoolId
,UserPoolId
,UserPoolAppClientSecret
,UserPoolAppClientSecret
,UserPoolAppClientId
を貼り付けます。IdentityPoolId
はリージョンも含めて入力する必要があるので気をつけてください。実機にインストールし、ユーザーを作成します。入力したメールアドレスに二要素認証のコードが送られてくるので間違えずに入力します。
ユーザーの作成とログインができたら [Start Viewer]でビデオチャットを始めます。
PCで Kinesis の demo-channel を開き、Master でビデオを流すと PC と Android アプリでビデオチャットができます。
カビゴンがPCのカメラで、エヴァ初号機がAndroidのカメラで撮ってます。
ブラウザ <-> Androidアプリ間でビデオ通信ができました。iOS
基本的な設定はAndroidで既に終わっているので、設定を追加してビルドします。
$ git clone git@github.com:awslabs/amazon-kinesis-video-streams-webrtc-sdk-ios.git $ cd amazon-kinesis-video-streams-webrtc-sdk-ios $ cd Swift $ pod install # もし cocoapods が入っていなかったらインストールが必要
awsconfiguration.json
を追加。awsconfiguration.json{ "Version": "1.0", "CredentialsProvider": { "CognitoIdentity": { "Default": { "PoolId": "YOUR_IDENTITY_POOL_ID", "Region": "ap-northeast-1" } } }, "IdentityManager": { "Default": {} }, "CognitoUserPool": { "Default": { "AppClientSecret": "YOUR_APP_CLIENT_SECRET", "AppClientId": "YOUR_APP_CLIENT_ID", "PoolId": "YOUR_USER_POOL_ID", "Region": "ap-northeast-1" } } }
Constants.swift
を開いて入力let CognitoIdentityUserPoolRegion: AWSRegionType = AWSRegionType.APNortheast1 let CognitoIdentityUserPoolId = "YOUR_USER_POOL_ID" let CognitoIdentityUserPoolAppClientId = "YOUR_APP_CLIENT_ID" let CognitoIdentityUserPoolAppClientSecret = "YOUR_APP_CLIENT_SECRET" let cognitoIdentityPoolId = "YOUR_IDENTITY_POOL_ID"ログインし、ビデオ通話を開始します。
先ほど作成したユーザーを使いまわしても大丈夫です。TODO: Twilio との料金の比較
参考にさせて頂いた記事
- 投稿日:2020-07-22T16:11:39+09:00
CocoaPodsを含むXcodeプロジェクトがビルドできない時
あるWebサイトからXcodeのサンプルワークスペースをダウンロードし、手元の環境でビルドしようとしたが失敗した。
エラーを調べるとワークスペースに含まれるCocoaPodsが手元のXcodeのバージョンではうまく参照できていないないようだった。
そこでCocoaPodsを再取得したところ現在のビルドできるようになった。その手順を以下に示す。
(全てのケースで上手くいくとは限らないが参考までに)
- プロジェクトのディレクトリは以下のようになっているはず。この時、Sample.xcworkspace をXcodeで開いてもビルドできなかった。
% ls -l total 16 -rw-rw-r--@ 1 koki staff 314 8 8 2018 Podfile -rw-r--r-- 1 koki staff 386 8 8 2018 Podfile.lock drwxr-xr-x 9 koki staff 288 8 8 2018 Pods drwxrwxr-x@ 11 koki staff 352 9 25 2018 Sample drwxrwxr-x@ 5 koki staff 160 8 8 2018 Sample.xcodeproj drwxr-xr-x@ 5 koki staff 160 8 8 2018 Sample.xcworkspace
- サンプル作成時、
pod install
コマンドによって追加されたはずであるファイルを削除する。% rm -rf Podfile.lock Pods RxSample.xcworkspace % ls -l total 16 -rw-rw-r--@ 1 koki staff 314 8 8 2018 Podfile drwxrwxr-x@ 11 koki staff 352 9 25 2018 Sample drwxrwxr-x@ 5 koki staff 160 7 22 12:47 Sample.xcodeproj
- CocoaPodsを再取得する
% pod install % ls -l total 16 -rw-rw-r--@ 1 koki staff 314 8 8 2018 Podfile -rw-r--r-- 1 koki staff 386 7 22 12:47 Podfile.lock drwxr-xr-x 9 koki staff 288 7 22 12:47 Pods drwxrwxr-x@ 11 koki staff 352 9 25 2018 Sample drwxrwxr-x@ 5 koki staff 160 7 22 12:47 Sample.xcodeproj drwxr-xr-x@ 5 koki staff 160 7 22 12:48 Sample.xcworkspace以上の手順を実施した後、Sample.xcworkspace をXCodeで開くとビルドが成功するようになった。
- 投稿日:2020-07-22T15:07:17+09:00
アプリ�APIの負荷テストを自動生成したいと思った
概要
iOSアプリのバックエンドAPIに対して、負荷テストをしたい
アプリの通信内容をキャプチャして、テストシナリオを自動で生成できないか?
- 経験則からテスト対象APIを絞ることはできるが、意図しない組合せでAPIシステムが落ちることを防げない
- アプリのテストケースはない前提
プロトタイプ的にかんたんに動くものを作ってみた
注意事項
- 負荷テストは自身が管理するシステム以外に実行しないでください
登場人物
- APIコールをするやつ
- アプリの通信内容をキャプチャするアプリ
- アプリのバックエンド: REST APIサーバ
- 負荷試験ツール
- 許されることなら何もしたくない
やりたいこと
概要
詳細
負荷テストシナリオを生成する
charles for iosでアプリの通信内容(chlsj)をキャプチャし、エクスポートする
-> charles session json(chlsj)
- 参考: [iOS] Charles for iOSがリリースされたので試してみた
- アプリからcharles session をエクスポートしてpcに送る
charles session json(chlsj)からシナリオテンプレート(CSV)を生成する
変換スクリプト$python chlsj_2_scenario_csv.py {シナリオ名}.chlsj -> {シナリオ名}_template.csvが生成されるchlsj_2_scenario_csv.pyimport json import sys import csv import dateutil.parser import os initial_time = None def session_json_2_scenario_csv(data): request = data['request'] request_body = '' if ('body' in request) and ('text' in request['body']): request_body = request['body']['text'] # 呼び出し開始msecを計算 run_at = dateutil.parser.parse(data['times']['requestBegin']) global initial_time initial_time = initial_time or run_at run_msec_at = int((run_at-initial_time).total_seconds()*1000) # headerを{"name":"Authorizatoin","value":"~~~"}から{"Authorizatoin": "~~~"}方式に変換 request_header={} for kv in data['request']['header']['headers']: request_header[kv['name']] = kv['value'] # 不要なヘッダーを削除 del request_header['Host'] return { "start_msec_at": run_msec_at, "method": data['method'], "request": '?'.join( filter(None,[data['path'],data['query']])), "request_header":json.dumps(request_header), "request_body":request_body, "response_to_variable": None } def chlsj_2_scenario(chlsj_path): scenario = [] with open(chlsj_path) as in_file: for request in json.load(in_file): scenario.append( session_json_2_scenario_csv(request) ) out_file_path = os.path.basename(chlsj_path).split('.', 1)[0] + "_template.csv" with open(out_file_path, 'w') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=scenario[0].keys(),quotechar="'",quoting=csv.QUOTE_NONNUMERIC) writer.writeheader() writer.writerows(scenario) if __name__ == '__main__': chlsj_2_scenario(sys.argv[1])
- シナリオテンプレ(CSV)からシナリオ(CSV)を作る
- 必要に応じてシナリオ_テンプレの項目を変更し、「シナリオ」を作成する
start_msec_at method request request_header request_body response_to_replace テスト開始から何秒でコールするか
※プロトタイプでは未実装HTTP メソッド
GET/POST/PATCHREST path+クエリ HTTP header HTTP request body レスポンス内容を以降のリクエストで使用したい場合、
{変数名: '値の場所'}のjsonで変数をセットする
- 例
- 「POST /api/user_devices」をたたき、response bodyの「oauth_access_token.access_token」を「auth_token」として以降のリクエストで使用する
- header/bodyに"auth_token"の文字列が含まれる場合、「oauth_access_token.access_token」の内容にリプレースされる
start_msec_at method request request_header request_body response_to_replace 0 POST' /api/user_devices' {"X-LANGUAGE": "ja_JP", "X-TIME-ZONE": "Asia/Tokyo", "Authorization": "Bearer ~~~", "Accept": "/"} {"auth_token": "oauth_access_token.access_token"}' 負荷テストシナリオを実行する
- シナリオからlocustテストを実行する
locust起動## 初回のみlocustをインストールする $pip install locust $script_csv={シナリオ}.csv locust -f scenario.py -H https://{サーバアドレス}scenario.pyfrom locust import HttpUser,task, between import os import csv import json def is_json(string): try: json_object = json.loads(string) except ValueError as e: return False return True def xpath_get(mydict, path): elem = mydict try: for x in path.strip(".").split("."): elem = elem.get(x) except: pass return elem class ScenarioLoadTest(HttpUser): wait_time = between(0.500, 1) @task def test(self): api_list_csv_path = os.environ['script_csv'] replaces = {} with open(api_list_csv_path) as f: replace_map={} for row in csv.DictReader(f,quotechar="'",quoting=csv.QUOTE_NONNUMERIC): _request = row['request'] _header = row['request_header'] _request_body = row['request_body'].encode('utf8') # 変数のレンダー(文字リプレース) for key in replace_map: _request = _request.replace(key,replace_map[key]) _header = _header.replace(key,replace_map[key]) request = _request header = json.loads(_header) request_body = _request_body response = None if row['method']=='GET': self.client.get(request,headers=header) elif row['method']=='POST': raw_response = self.client.post(request,headers=header,data=request_body) response = raw_response.json() elif row['method']=='PATCH': self.client.patch(request,headers=header,data=request_body) else: print('called else ' + str(row)) # 変数の保存 if is_json(row['response_to_replace']): replace_map = json.loads(row['response_to_replace']) for key in replace_map: replace_map[key] = xpath_get(response,replace_map[key])今後やりたいこと
- aws device farmでアプリの表示テストを動かしたい
- そのついでにスクリプトを自動生成できない?
- 投稿日:2020-07-22T14:59:59+09:00
【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの三つで実現してみる①
Flutterの状態管理に関する記事の中、カウンター(Counter)を弄るのは多いと思いますが、カウンターの例では少し煩雑だと感じます。
本シリーズではアプリのテーマ(Theme)切替、多言語化を通じて、provider,BLoC,redux三つの実現方式でFlutterの状態管理を説明します。
一、providerでテーマ切替&国際化
provider: ^4.3.1 (providerパッケージをpub.devから取得)
1.テーマ切替
ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
1.1.状態クラス
_themeDataはテーマやカラーに使うが、_colorIndexは選択したカラーの状態の保存に使い、本文ではテーマやカラーの状態を端末側に保存する仕組みを設けていないけど、興味ある方は実践してみてください。
changeThemeData()はコアメソッド。実行したらnotifyListeners()を通じて、グローバルにアナウンスが起こり状態がほぼ遅延なく刷新される。
theme_state.dartimport 'package:flutter/material.dart'; class ProviderThemeState extends ChangeNotifier{ ThemeData _themeData;//テーマ int _colorIndex;//色ボタン選択中の状態を記録 ProviderThemeState(this._themeData, this._colorIndex); void changeThemeData(ThemeData themeData, int colorIndex){ this._themeData = themeData; this._colorIndex = colorIndex; notifyListeners(); } int get colorIndex => this._colorIndex; ThemeData get themeData => this._themeData; }1.2.管理する必要がある部分をラップする(wrap)
MultiProviderは複数のproviderを入れることができる。widgetをwrapperだけでなく他のコンポーネントでもラップやカバーできる。
main.dartimport 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:state_dancer/provider/theme_state.dart'; import 'home_page.dart'; void main() { runApp(Wrapper(child: MyApp())); } class Wrapper extends StatelessWidget{ final Widget child; Wrapper({this.child}); @override Widget build(BuildContext context) { final initThemeData = ThemeData( primaryColor: Colors.green, ); final initIndex = 1; return MultiProvider(providers: [ ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)), ],child: child,); } } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return Consumer<ProviderThemeState>(builder: (context,state,widget) =>MaterialApp( title: "状態管理Demo", theme: state.themeData, debugShowCheckedModeBanner: false, home: HomePage(), )); } }1.3.状態の使用、メソッドの実行
Provider.of(context)よりConsumerのほうは結構細かい対象を絞れるため、widget再ビルドの消費コストが削減される。また、ConsumerはwidgetのBuildContextが不要で使えるのも一大メリット。
ボタンの押下により、changeThemeData()が呼び出されConsumerの状態の状態が更新される。
home_page.dartimport 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'provider/theme_state.dart'; class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; var statusBarH = MediaQuery.of(context).padding.top; var naviBarH = kToolbarHeight; return Scaffold( appBar: AppBar( title: Text("Flutter状態管理",style: TextStyle(color: Colors.white),), ), drawer: Drawer( child: Consumer<ProviderThemeState>(builder: (context, state, widget) { return Column( children: <Widget>[ Container( width: screenWidth, height: statusBarH + naviBarH, color: state.themeData.primaryColor, ), SizedBox(height: 10.0), Row( children: <Widget>[ Padding(padding: EdgeInsets.all(10)), Wrap(children: <Widget>[ RaisedButton( color: Colors.green, onPressed: () { state.changeThemeData(ThemeData(primaryColor: Colors.green), 1); }, child: Text("緑",style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.red, onPressed: () { state.changeThemeData(ThemeData(primaryColor: Colors.red), 2); }, child: Text("赤",style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.blue, onPressed: () { state.changeThemeData(ThemeData(primaryColor: Colors.blue), 3); }, child: Text("青",style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), ],), ], ), Divider(), SizedBox(height: 10,), Row(children: <Widget>[ Padding(padding: EdgeInsets.all(10)), SizedBox(width: 15,), RaisedButton( color: state.themeData.primaryColor, onPressed: () { }, child: Text("日本語",style: TextStyle(color: Colors.white),), ), SizedBox(width: 15.0,), RaisedButton( color: state.themeData.primaryColor, onPressed: () { }, child: Text("English",style: TextStyle(color: Colors.white),), ), ],), ], ); }), ), body: ListView( children: <Widget>[ Consumer<ProviderThemeState>(builder: (context, state, widget) { return Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Container( width: 200, height: 300, color: state.themeData.primaryColor, ), SizedBox(height: 10.0), Text( "学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し", style: TextStyle( color: state.themeData.primaryColor, fontSize: 18.0, fontWeight: FontWeight.bold), ), SizedBox(height: 30.0), FloatingActionButton(onPressed: (){ }, backgroundColor: state.themeData.primaryColor, child: Icon(Icons.check)), ], ), ); }) ], ), ); } }2.言語切替
flutter_localizations:
sdk: flutter (多言語をpubspec.yamlで配置)ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。
2.1.言語データを準備する
language_data.dartclass LanguageData { static final EN = { "title":"Flutter State Management", "greenBtn":"Green", "redBtn":"Red", "blueBtn":"Blue", "analects":"To learn without thinking is blindness, \nto think without learning is idleness (Confucius)." }; static final JP = { "title":"Flutter状態管理", "greenBtn":"緑", "redBtn":"赤", "blueBtn":"青", "analects":"学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し" }; }2.2.言語データを使用するためのクラス
i18n.dartimport 'package:flutter/material.dart'; import 'language_data.dart'; class I18N { final Locale locale; I18N(this.locale); static Map<String, Map<String, String>> _localizedValues = { "en": LanguageData.EN, "ja": LanguageData.JP, }; static I18N of(BuildContext context) { return Localizations.of(context, I18N); } get title { return _localizedValues[locale.languageCode]['title']; } get greenBtn { return _localizedValues[locale.languageCode]['greenBtn']; } get redBtn { return _localizedValues[locale.languageCode]['redBtn']; } get blueBtn { return _localizedValues[locale.languageCode]['blueBtn']; } get analects { return _localizedValues[locale.languageCode]['analects']; } }2.3.多言語のデリゲートクラス
I18nDelegate.dartimport 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'i18n.dart'; class I18nDelegate extends LocalizationsDelegate<I18N> { @override bool isSupported(Locale locale) { ///サポートする言語 return ['en', 'ja'].contains(locale.languageCode); } ///現在言語環境下の文字列をロード @override Future<I18N> load(Locale locale) { return SynchronousFuture<I18N>(I18N(locale)); } @override bool shouldReload(LocalizationsDelegate<I18N> old) { return false; } //グローバル静的デリゲート static I18nDelegate delegate = I18nDelegate(); }2.4.状態クラス
localeというフィールドしか使わない
locale_state.dartimport 'package:flutter/material.dart'; class LocaleState extends ChangeNotifier{ Locale _locale;//地域 LocaleState(this._locale); factory LocaleState.jp()=> LocaleState(Locale('ja', 'JP')); factory LocaleState.en()=> LocaleState(Locale('en', 'US')); void changeLocaleState(LocaleState state){ _locale=state.locale; notifyListeners(); } Locale get locale => _locale; //言語ゲット }2.5.状態の使用、メソッドの実行
Consumer() の他に Consumer2() から Consumer6() まであり、得たい状態値の種類数によって使い分けることができる。
2.5.1.多言語化の配置
main.dartimport 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:state_dancer/provider/theme_state.dart'; import 'home_page.dart'; import 'provider/locale_state.dart'; import 'provider/I18nDelegate.dart'; void main() { runApp(Wrapper(child: MyApp())); } class Wrapper extends StatelessWidget{ final Widget child; Wrapper({this.child}); @override Widget build(BuildContext context) { final initThemeData = ThemeData( primaryColor: Colors.green, ); final initIndex = 1; return MultiProvider(providers: [ ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),//themeのprovider ChangeNotifierProvider(create: (_) => LocaleState.jp()),//localeのprovider ],child: child,); } } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>MaterialApp( title: "状態管理Demo", localizationsDelegates: [GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, I18nDelegate.delegate//言語デリゲート ], locale: localState.locale, supportedLocales: [localState.locale], debugShowCheckedModeBanner: false, home: HomePage(), )); } }2.5.2.多言語化の実行
main.dartimport 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'provider/locale_state.dart'; import 'provider/theme_state.dart'; import 'provider/i18n.dart'; class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; var statusBarH = MediaQuery.of(context).padding.top; var naviBarH = kToolbarHeight; return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>Scaffold( appBar: AppBar( backgroundColor: themeState.themeData.primaryColor, title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),), ), drawer: Drawer( child: Consumer2<ProviderThemeState,LocaleState>(builder: (_,themeState,localState,__) { return Column( children: <Widget>[ Container( width: screenWidth, height: statusBarH + naviBarH, color: themeState.themeData.primaryColor, ), SizedBox(height: 10.0), Row( children: <Widget>[ Padding(padding: EdgeInsets.all(10)), Wrap(children: <Widget>[ RaisedButton( color: Colors.green, onPressed: () { themeState.changeThemeData(ThemeData(primaryColor: Colors.green), 1); }, child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.red, onPressed: () { themeState.changeThemeData(ThemeData(primaryColor: Colors.red), 2); }, child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.blue, onPressed: () { themeState.changeThemeData(ThemeData(primaryColor: Colors.blue), 3); }, child: Text(I18N.of(context).blueBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), ],), ], ), Divider(), SizedBox(height: 10,), Row(children: <Widget>[ Padding(padding: EdgeInsets.all(10)), SizedBox(width: 15,), RaisedButton( color: themeState.themeData.primaryColor, onPressed: () { localState.changeLocaleState(LocaleState.jp()); }, child: Text("日本語",style: TextStyle(color: Colors.white),), ), SizedBox(width: 15.0,), RaisedButton( color: themeState.themeData.primaryColor, onPressed: () { localState.changeLocaleState(LocaleState.en()); }, child: Text("English",style: TextStyle(color: Colors.white),), ), ],), ], ); }), ), body: ListView( children: <Widget>[ Consumer2<ProviderThemeState,LocaleState>(builder: (_, themeState, localeState,__) { return Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Container( width: 200, height: 300, color: themeState.themeData.primaryColor, ), SizedBox(height: 10.0), Text(I18N.of(context).analects, style: TextStyle( color: themeState.themeData.primaryColor, fontSize: 18.0, fontWeight: FontWeight.bold), ), SizedBox(height: 30.0), FloatingActionButton(onPressed: (){ }, backgroundColor: themeState.themeData.primaryColor, child: Icon(Icons.check)), ], ), ); }) ], ), ), ); } }まとめ
本文ではProviderでテーマや多言語の切替という多状態を管理してみました。Flutterの状態管理はandroid, iOSと違い、多分多くの方が悩み続けていると思いますね。Flutterの状態管理について、代表的にprovider,BLoC,reduxがありますので、次回ではBLoCパターンをご紹介していきたいと思います。
- 投稿日:2020-07-22T12:08:03+09:00
Swift iOSアプリ 1つ前の画面に値を渡す方法
前の画面に値を渡したい
プロトコルを作成することで解決できる。
「画面1 ← 画面2」という風に画面遷移するときはこんな感じ画面2.swift//自分で新しいプロトコル作っちゃう。名前はテキトウ protocol CatchProtocol { func catchData(count: Int) } class NextViewController: UIViewController { //自分で定義したプロトコルをdelegateって名前で変数として持つで!! var delegate:CatchProtocol? //渡したい値を宣言しとこかな var count = 1000 //戻るボタン押した時の挙動をここに書いちゃお @IBAction func back(_ sender: Any) { //delegateのcatchDataメソッドを発動してくれよな!引数に渡したい値書いちゃう delegate?.catchData(count: count) //前の画面(画面1)に戻るで dismiss(animated: true, completion: nil) } }画面1.swift//CatchProtcolを使いますよ〜って書く ↓ここ class ViewController: UIViewController, CatchProtocol { //この変数で画面2の値を受け取る var recepter = Int() //画面が遷移した時に呼ばれるメソッド prepare for segue を用意 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { //次の画面を変数に入れる let nextVC = segue.destination as! NextViewController //オイラのクラスでデリゲートを受け持つで!! nextVC.delegate = self } //プロトコルで用意したメソッドの中身をここで書く //戻るボタン押したらここが呼ばれることになっとるぜ。(画面2のコード参照) func catchData(count: Int) { //ここで受け渡しが完了! recepter = count } }
- 投稿日:2020-07-22T10:52:55+09:00
ちょうかんたんStyle Transfer。AppleのCreateMLで
画像を選ぶだけ!お手軽ディープ・スタイル変換
画像のスタイル変換はディープラーニングのおもしろ技術です。
AppleのCreateMLというツールを使うことで、とてもかんたんに試せます。
CreateMLを起動
XcodeをControllキーを押しながらクリック、そしてOpenDeveloperTools、CreateMLを選択。
CreateMLのメニューからStyle Transferを選びます。
それから、好きなプロジェクト名を入力します。スタイル画像とコンテンツ画像を選ぶ
好きなスタイルと、そのスタイルを適用したいコンテンツ画像を選びます。
スタイルは1枚、コンテンツ画像は複数枚選びます。
Training Stylize ImageとContents ImagesとValidation Imageの+ボタンをクリックして、用意した画像(コンテンツ画像は複数枚なのでフォルダ)を指定します。
今回はスタイル画像にひき肉を用意しました。
トレーニング中に確認するためのValidation Imageも1枚必要です。
計算回数とスタイル適用の強度(Strenth)と密度(Density)を指定できます。
今回はデフォルトを採用しています。トレーニングする
それから、▶️Trainボタンを押すと学習が開始されます。
計算ステップ5
計算ステップ250
計算ステップ500
10分ぐらいでトレーニングは終わります。
トレーニングが終わったら、Previewページに好きな画像をドラッグ&ドロップしてスタイルを適用できます。
OutputページでGetボタンを押して、CoreMLモデルを入手できます。
入手したmlmodelをXcodeにドラッグ&ドロップして、iOSアプリで使えます。ね、かんたんでしょ?
・・・
ぼくのTwitterをフォローしてください。お願いします。
https://twitter.com/JackdeS11お仕事のご依頼をこのメールにお願いします。
rockyshikoku@gmail.comあと、Looks Good For Me(わるくないね)、押してください。
ここです ↓
チャオ?!
- 投稿日:2020-07-22T10:52:55+09:00
ちょうかんたんStyle Transfer。AppleのCreate MLで
画像を選ぶだけ!お手軽ディープ・スタイル変換
画像のスタイル変換はディープラーニングのおもしろ技術です。
AppleのCreateMLというツールを使うことで、とてもかんたんに試せます。
CreateMLを起動
XcodeをControllキーを押しながらクリック、そしてOpenDeveloperTools、CreateMLを選択。
CreateMLのメニューからStyle Transferを選びます。
それから、好きなプロジェクト名を入力します。スタイル画像とコンテンツ画像を選ぶ
好きなスタイルと、そのスタイルを適用したいコンテンツ画像を選びます。
スタイルは1枚、コンテンツ画像は複数枚選びます。
Training Stylize ImageとContents ImagesとValidation Imageの+ボタンをクリックして、用意した画像(コンテンツ画像は複数枚なのでフォルダ)を指定します。
今回はスタイル画像にひき肉を用意しました。
トレーニング中に確認するためのValidation Imageも1枚必要です。
計算回数とスタイル適用の強度(Strenth)と密度(Density)を指定できます。
今回はデフォルトを採用しています。トレーニングする
それから、▶️Trainボタンを押すと学習が開始されます。
計算ステップ5
計算ステップ250
計算ステップ500
10分ぐらいでトレーニングは終わります。
トレーニングが終わったら、Previewページに好きな画像をドラッグ&ドロップしてスタイルを適用できます。
OutputページでGetボタンを押して、CoreMLモデルを入手できます。
入手したmlmodelをXcodeにドラッグ&ドロップして、iOSアプリで使えます。ね、かんたんでしょ?
・・・
ぼくのTwitterをフォローしてください。お願いします。
https://twitter.com/JackdeS11お仕事のご依頼をこのメールにお願いします。
rockyshikoku@gmail.comあと、Looks Good For Me(わるくないね)、押してください。
ここです ↓
チャオ?!
- 投稿日:2020-07-22T10:46:13+09:00
Metal を使って10万個のパーティクルを描画しよう
はじめに
Metal を使うとたくさんの計算を並列で行うことができます. 60fps で画面の更新をする場合, 1フレームあたりの処理は約 16ms で収めなければいけません. UIKit を使って10万個の UIView の frame を更新しながら 60fps を保つのは難しいでしょう. Metal を使うとどんな計算が可能なのか, 簡単なサンプルアプリを実装したら勉強になった箇所がたくさんあったので, 共有と備忘録を兼ねてまとめたいと思います.
環境
- Xcode 11.5
- iPhone 11 Pro, iPhone XS, iPad Pro 11inch 第2世代 1
つくったもの
10万個のパーティクル 10万個のパーティクル(白黒) 設定画面 端末の画面いっぱいに最大で10万個のパーティクルがアニメーションします. ある程度可変なパラメータがあった方が理解に繋がると思ったので, パーティクルの色と背景色, パーティクルの個数は設定画面から指定できるようになっています.
ソースコードは naru-jpn/100000-particles で公開しています.
大まかな処理の流れ
大まかに処理の流れを図示すると上の図のようになります.
- それぞれのパーティクルの位置情報などを更新 2
- 1. で更新された情報をもとにして, それぞれのパーティクルを画面上に描画
という流れです. シンプルな構成です. Metal を使用する場合はパイプラインという処理の流れを記述する必要があり, 大まかな流れをイメージすることは大事なことだと思います. 60fps を維持したいので, これらの一連の処理は 16ms 以内で行う必要があります. 記事の後半にパフォーマンスについても記載しています.
実装の解説
具体的な実装について順を追って解説します. 記載しているコードは要点が分かるように部分的に簡略化しています.
パーティクルを表す構造体
ShaderTypes.h#include <simd/simd.h> typedef struct { vector_float4 color; vector_float2 position; vector_float2 velocity; float phase; } particle_t;上の構造体がパーティクルの実体です. RGBA の色情報, 2次元の座標上の位置と速度, 横方向の揺れを制御する為の変数から構成されています. この構造体は Swift のプログラムだけからではなくシェーダプログラムからも使いたいので, Swift の Struct ではなくこのように定義をする必要があります. 今回は定義を共有するものはこれだけなので, 直接このファイルを Bridging Header に指定します.
パーティクルを格納するバッファ
Renderer.swiftlet length: Int = MemoryLayout<particle_t>.size * Renderer.maxNumberOfParticles let buffer: MTLBuffer = device.makeBuffer(length: length, options: .storageModeShared)必要な領域のサイズを指定して, バッファを作成しています.
options
に.storageModeShared
を指定していますが, これは CPU と GPU の両方からこのバッファの内容を編集したいからです. 3パーティクルの初期化
Renderer.swiftlet particleBuffer = particleBuffers[0].contents().bindMemory(to: particle_t.self, capacity: numberOfParticles) for index in 0..<numberOfParticles { particleBuffer[index] = particle_t.create(with: setting, viewportSize: viewportSize) }バッファの内容を編集するために
bindMemory
で型を指定してUnsafeMutablePointer<particle_t>
に変換しています. 位置情報やそれぞれのパーティクルの色情報などをここで初期化しています.描画のサイクル
Renderer.swiftfunc draw(in view: MTKView) { // 定期的に呼ばれるデリゲートメソッド let semaphore = inFlightSemaphore // トリプルバッファリングの制御 _ = semaphore.wait(timeout: DispatchTime.distantFuture) do { let simulateSemaphore = simulationInFlightSemaphore // _ = simulateSemaphore.wait(timeout: DispatchTime.distantFuture) guard let commandBuffer = commandQueue.makeCommandBuffer() else { fatalError("Failed to make command buffer.") } // 1. それぞれのパーティクルの位置情報などを更新 simulate(in: view, commandBuffer: commandBuffer) commandBuffer.addCompletedHandler { _ in simulateSemaphore.signal() } commandBuffer.commit() } do { guard let commandBuffer = commandQueue.makeCommandBuffer() else { fatalError("Failed to make command buffer.") } // 2. 1. で更新された情報をもとにして, それぞれのパーティクルを画面上に描画 render(in: view, commandBuffer: commandBuffer) commandBuffer.addCompletedHandler { _ in semaphore.signal() } commandBuffer.commit() } currentBufferIndex = (currentBufferIndex + 1) % Renderer.maxInFlightRenderingBuffers }上の関数は
MTKViewDelegate
に定義されている関数で,MTKView
の再描画が必要なタイミングでこの関数が呼び出されます. セマフォの操作等 4 をしていますが, ここで大事なのはsimulate
とrender
の2行です. この2つの処理が, 「大まかな処理の流れ」で説明した2つのステップに対応しています.1. それぞれのパーティクルの位置情報などを更新
Swift側の処理
Renderer.swiftguard let function = library.makeFunction(name: "simulate") else { fatalError("Failed to make function simulate.") } do { // 関数 'simulate' を使う ComputePipeline を定義 simulatePipelineState = try device.makeComputePipelineState(function: function) } // ... private func simulate(in view: MTKView, commandBuffer: MTLCommandBuffer) { // ... // 上で定義したパイプライン(simulatePipelineState)に従って処理をする computeEncoder.setComputePipelineState(simulatePipelineState) computeEncoder.setBuffer(particleBuffers[currentBufferIndex], offset: 0, index: 0) // 入力 computeEncoder.setBuffer(particleBuffers[simulatedBufferIndex], offset: 0, index: 1) // 出力 computeEncoder.setBytes(&viewportSize, length: MemoryLayout<vector_float2>.size, index: 2) // 画面サイズ computeEncoder.setThreadgroupMemoryLength(simulatePipelineState.threadExecutionWidth * MemoryLayout<particle_t>.size, index: 0) computeEncoder.dispatchThreads(dispatchThreads, threadsPerThreadgroup: threadsPerThreadgroup) computeEncoder.endEncoding() }
library.makeFunction(name: "simulate")
でsimulate
という関数をとってきて,device.makeComputePipelineState(function: function)
でその関数を使うパイプラインを定義しています. 指定した情報はcomputeEncoder
によって GPU に渡す為の命令にエンコードされます. 関数simulate
の定義はShaders.metal
ファイル内にあり, そのような関数はシェーダ関数と呼ばれます.シェーダ側の処理
Shaders.metalkernel void simulate(device particle_t* currentParticles [[ buffer(0) ]], // 入力 device particle_t* newParticles [[ buffer(1) ]], // 出力 constant vector_uint2 *viewportSize [[ buffer(2) ]], // 画面サイズ const uint gid [[ thread_position_in_grid ]]) { // 更新前のパーティクル情報 float2 position = currentParticles[gid].position; float2 velocity = currentParticles[gid].velocity; float4 color = currentParticles[gid].color; float phase = currentParticles[gid].phase; float end = (vector_float2(*viewportSize) / 2.0).y; position.x += sin(phase); // 横方向の移動 if (position.y < -end) { position.y = end; // 一番下まで到達したら一番上に戻る } // 更新後のパーティクル情報 newParticles[gid].color = color; newParticles[gid].position = position + velocity; // velocityは下向きのy成分のみ. newParticles[gid].velocity = velocity; newParticles[gid].phase = phase + PHASE_INTERVAL; }上の2つのコードを見比べて,
setBuffer
やsetBytes
のindex
とシェーダ関数の引数にある数字とを比較すると対応関係が分かります.gid
というのはパーティクルのインデックスで, それぞれのgid
が並列で計算されていると思ってもらえばよいです. シェーダ関数simulate
に必要な情報を渡して, 関数の中で計算をしているという流れが分かります.2. 1. で更新された情報をもとにして, それぞれのパーティクルを画面上に描画
Swift側の処理
Renderer.swiftguard let vertexFunction = library.makeFunction(name: "particle_vertex") else { fatalError("Failed to make function particle_vertex.") } guard let fragmentFunction = library.makeFunction(name: "particle_fragment") else { fatalError("Failed to make function particle_fragment.") } // 関数 'particle_vertex', 'particle_fragment' を使う RenderPipeline を定義 let renderPipelineStateDescriptor = MTLRenderPipelineDescriptor() renderPipelineStateDescriptor.vertexFunction = vertexFunction renderPipelineStateDescriptor.fragmentFunction = fragmentFunction renderPipelineStateDescriptor.colorAttachments[0].pixelFormat = ... // ... renderPipelineState = try device.makeRenderPipelineState(descriptor: renderPipelineStateDescriptor) // ... private func render(in view: MTKView, commandBuffer: MTLCommandBuffer) { let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = view.currentDrawable?.texture renderPassDescriptor.colorAttachments[0].loadAction = .clear // 描画の前にテクスチャをクリア renderPassDescriptor.colorAttachments[0].clearColor = viewClearColor // 背景色の指定 renderPassDescriptor.colorAttachments[0].storeAction = .store // 描画結果を保存 guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return } // 上で定義したパイプライン(renderPipelineState)に従って処理をする renderEncoder.setRenderPipelineState(renderPipelineState) renderEncoder.setVertexBuffer(particleBuffers[simulatedBufferIndex], offset: 0, index: 0) // パーティクル情報 renderEncoder.setVertexBytes(&viewportSize, length: MemoryLayout<vector_float2>.size, index: 1) renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: numberOfParticles) renderEncoder.endEncoding() if let drawable = view.currentDrawable { commandBuffer.present(drawable) } }先ほどは
ComputePipiline
でしたが, ここではRenderPipeline
というものが出てきました. レンダリングについての説明をするのはとても大変なので割愛するのですが, ここでは2つのシェーダ関数を指定してパイプラインを定義しています. 上ではrenderEncoder
が GPU に渡す為の命令へのエンコードを行います.setVertexBuffer
とsetVertexBytes
で指定した情報を使って,drawable
に.point
のプリミティブを描画しています.
view.currentDrawable
というのはこれから画面上に描画されるコンテンツです.view.currentDrawable?.texture
を RenderPipeline の描画先に指定し,commandBuffer.present(drawable)
によってコンテンツが描画されたdrawable
を画面上に表示しています.シェーダ側の処理
Shaders.metalconstant float PARTICLE_SIZE = 5.0f; struct Point { float4 position [[position]]; // パーティクルの位置 float size [[point_size]]; // パーティクルの大きさ float4 color; }; vertex Point particle_vertex(const device particle_t* particles [[ buffer(0) ]], // パーティクル情報 constant vector_uint2 *viewportSizePointer [[ buffer(1) ]], // 画面サイズ unsigned int vid [[ vertex_id ]]) { Point out; out.position = vector_float4(0.0f, 0.0f, 0.0f, 1.0f); out.position.xy = particles[vid].position / (vector_float2(*viewportSizePointer) / 2.0f); out.size = PARTICLE_SIZE; out.color = particles[vid].color; return out; } fragment float4 particle_fragment(Point in [[stage_in]]) { return in.color; // 領域内の色を指定 };関数
particle_vertex
,particle_fragment
はそれぞれバーテックスシェーダ, フラグメントシェーダと呼ばれます. 先ほど.point
のプリミティブを描画すると書きましたが, ここではポイントの位置や大きさ, 領域内の色などを決めています.[[position]]
や[[point_size]]
は Metal Shading Language 5 の中で定義されている attribute と呼ばれるもので, それぞれポイントの位置, ポイントの大きさに対応しています.ここまでが大まかな処理の流れの実装を追ったものです. 細かい処理の内容は調べればキリがないですが, 全体の処理を俯瞰してみるととても単純なものだと分かります.
パフォーマンス
パーティクルを 100,000 個描画した場合の iPhone 11 Pro 上でのパフォーマンスを Xcode 上で確認しました.
右端のグラフが GPU が1フレームあたりにかけている処理時間を表していますが, まだ制限時間である 16.7ms の半分程度の余力を残しています. すごい.
展望
ここで紹介した処理は計算内容もパーティクルの描画処理も最小限のものでした. もともとはパーティクル同士の相互作用があったりなどの複雑な計算を行いたかったのですが, まずは Metal の操作に慣れるために今回の実装内容にまとめてみました. 物理演算的な計算をコンピュートシェーダ上で行って, リアルタイムシミュレーションのようなものができたら楽しいなと思っています.
もし記事の内容に不備がありましたら, 指摘していただけると幸いです?
今回のプログラムは, GPU Family 5 以上の GPU が搭載されたデバイス上でのみ動作します. プログラム中で使用している
MTLComputeCommandEncoder
の関数dispatchThreads(_:threadsPerThreadgroup:)
が動作する必要があるからです. この並列処理に関する関数は GPU Family 4 でも動作するのですが、手元に端末がなくて動作が確認できなかったので対象外としています.dispatchThreadgroups(_:threadsPerThreadgroup:)
で最適化を行うことによってもある程度は速度が出せると思うのですが, 最適化できるだけの知識がまだないので検証ができていません. ( Metal Feature Set Tables: https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf ) ↩パーティクルの情報を更新する流れは必ずしも今回のようである必要はありません. 例えば Apple のサンプルコード MetalShaderShowcase では,
birthOffset
という時間経過に相当する変数から, 方程式を使ってパーティクルの位置を計算してそのまま描画を行います. 今回, パーティクルの情報を更新して一度バッファに保存するという流れにした理由は, 将来的にパーティクルの描画処理を物理シミュレーションなどに応用したいと思っているからです. ↩Apple によると, 描画をする際にはトリプルバッファリングを使用することが推奨されています. この部分では, トリプルバッファリングで全体の描画を制御し, パーティクル情報の更新の際には同時に1つの処理しか走らないような制御を行っています. 今回のパーティクル情報の更新では一つ前のフレームで更新した情報を計算の入力として用いるので, ここで同時に2つ以上の処理が走ってしまうと正常に計算が行えないからです. Metal Best Practices Guide: Triple Buffering ↩
- 投稿日:2020-07-22T00:01:29+09:00
大学生が1週間でFlutterアプリを学んでリリースした過程(5日目)
こんにちはシオンです。
昨日デザインがやっと決まり、コードを書き始めたところまで行きました。
ここまで来ればあとはコードを勉強しながら決めたデザインのものを形にしていくだけです。
残す課題は時間だけ、期限までに終わらせるというのが今回のチャレンジで一番大事になってくるのでここまできたら何がなんでも間に合わせます。プログラミングの進捗としては昨日FlutterでHelloWorldと表示することに成功しました。このプログラミングを学ぶ人が絶対に最初に通るLv1の状態から今日あすの2日でコードを書き切って、ラスト1日でリリースしてみせます。
少し話は変わりますが、qiitaではこういう投稿はあまり良くないようですね。
qiitaについて深く理解せずブログのような投稿をしてしまいました。この一連の投稿が終わったら改めたいと思います。ではやっていきます。
■まず画面をデザイン通りに構成する
コードを書き切ると言っても、本当にFlutterに対して無知なので闇雲に書いて行っても終わりが見えてきそうにないです。
ちなみにどのくらい無知なのかというと
現在私が持っているFlutterの知識は、
Flutterとは「フルッター」ではなく「フラッター」と読み、表記はアルファベットで構成されている。また、今日の私の朝ごはんは食パンにバターを塗ったトーストであるが、私が持つFlutterの知識と私の今日の朝食については全く関係がない。書くことがなさすぎて今日の朝食を紹介するしかないくらい無知ということです。よろしくお願いいたします。
ということで、一旦画面遷移などの処理は後回しにしてデザインの画面を形だけ再現してそのあとにカウントアップだったりの処理を書いていきたいとおもいます。
ちなみにデザインはこれです。(昨日のです。)
この画面を作っていく上で必要になってくるのは構成部品とそのレイアウトの知識です。
今回必要になってくる部品としては
・テキスト
・テキスト入力フォーム
・ボタン
・画像
この4つの部品があれば大丈夫そうです。では次にFlutterでのレイアウトの仕方を学んでいきます。
昨日Flutterでプログラミングしていく上で重要になってくるのがウィジェットというものだというのを学びました。
おそらくこのウィジェットの中に今回使う、画像だとかボタンだとかを持ったウィジェットがいるはずなので、まずはレイアウトは考えず必要な部品を
表示してみます。また3画面目から先はシンプルな構造ですぐに構成できると思うので、これは処理を書きながらにしていきます。
では1画面目と2画面目をやっていきます。
■1画面目と2画面目の外観を実装
1画面目はこんな感じに。なかなか時間がかかりました。
初めての言語でコードを書くときは勝手がわからない+関数やクラスの名前がわからない、で苦戦するのですが、その分うまく行ったときの「できた!!」っていう快感がたまらないですね。新しくゲームを始めてレベル1からスタートしている感じがしてすごく楽しいです。早くこの一週間チャレンジというチュートリアルをクリアして冒険に出かけます。
2画面目は内容入力のフォームの高さ調節が思うようにいかずこんな感じになってしまいました。
調べながらやっているのですが、まだまだFlutter自体が新しい分日本語記事が少ない印象ですかね。それでもわかりやすコードや使い方を記事にしてくださっている方が何人もいて助かりました。(本当にありがとうございます。)まだ少ししか触ってませんが、Flutterめちゃくちゃいいです。
この2画面を作っただけでもそのコードの書きやすさにびっくりしています。
まだ記事が少ないので初めてプログラミングを学ぶ人にとっては多少難しいかもしれませんが、何かしらやったことがある人にはお勧めしたいです。■まとめ
あとはこの画面に処理を記述していけばなんとか形にはなりそうです。
処理も一通り見たのですが、これくらいの画面遷移とカウントアップだけならすぐにできそうなので明日一気に仕上げてしまいます。なんとなく形になりそうな気配がしてきました。
でも残すは明日と明後日のみ。。。
果たして結果は完成するのかしないのか。残り2日間やっていきます。