- 投稿日:2021-03-14T23:45:02+09:00
[初リリース]Youtubeの複数曲mix動画をSpotifyプレイリストに変換するアプリ
こんにちは!趣味でエンジニアリングをしているcanalunです。こちらが自己紹介です!
去年の12月頃から初めてのiOSアプリ開発に取り組み、紆余曲折を経てなんとかYoutube-Spotify連携型プレイリスト作成アプリを完成・リリースさせることができました!
せっかくなので、作成の経緯や苦労したポイントを自分なりにここに書き残してみようかと思います!なお、アプリストアへのリンクはこちらです?
mixed!どんなアプリなのか??
YouTubeには複数の曲を1つのクリップにまとめた"mix"と呼ばれるタイプの動画が多く存在します。hiphopやlofiと呼ばれるジャンルの音楽が好きな方は、きっとよくご存知ですよね?
例えばこのような動画です!
【ネオシティポップとか】Neo City Pop / Japanese Hip Hop etc. Mix + Video【日本語ラップとか】
[ ???????? ] 코딩할때 듣기 좋은 노래 • lofi type beat • 3 hours playlist今回作成したアプリは、こういった動画をSpotify上のプレイリストに変換するものです!
具体的には、下記のような操作およびUIで動きます。
1. Spotifyでログイン
2. 好きなmix動画のURLを入力し、プレイリスト作成を開始
3. プレイリスト作成の成功/失敗を通知(成功の場合はSpotifyプレイリスト画面への遷移機能も提供)なぜ作ろうと思ったのか??
私はlofiやhiphopが好きなのですが、特にこれらのジャンルはYouTube上にmix動画が多くあげられており、よく再生しています。
ただ、いつも再生しながらmixの曲順や構成を編集したいなあとかバックグラウンドで再生したいなあと思うことがよくありました。(YouTubeプレミアムには入っていないんですわ)
そこである日、「こういうのってもしかしたら動画を音楽プラットフォームのプレイリストに変換できれば解決するのでは??」と考え、アプリ開発を始めてみました!
幸い、いちおう調べた範囲で同じようなアプリはありませんでした?使ってくれる人はいるだろうか??
結論としては「lofiやhiphopのような音楽を好むようなわりかし音楽が好きな人たちを中心に、需要自体はそれなりにあるのではないか」と考えました。
下記のような考え方をしましたが、自分の感覚で推測したに過ぎないので実際にはあまり当たっていない可能性もあるなと考えています?めちゃ単純に"lofi mix"でgoogle検索すれば52,800,000本、"hiphop mix"でgoogle検索すれば12,100,000本の動画が見つかります。動画の多さを需要の大きさであると単純解釈すれば、lofiやhiphopのmixってそんな少ないわけではなくて、そこそこ多くの人に再生されているんだろうなと考えられます。
すなわち、今回のアプリを使ってくれる人が満たしているべき最低条件である「mix動画を好んでよく聴いている人」というのはまあまあいることになります(たぶんだけど)。
問題はこの人たちが下記2点を満たすかです。
1. Spotifyを使っているのか
2. mix動画をプレイリストにしたいと思っているか1「Spotifyを使っているのか」については、十中八九Yesかと考えました。
lofiもhiphopも日常生活で話題に出ると、よく聴く人とあまり聴かない人に二分されるイメージがあります。lofiって言った時、たいてい反応は「lofiって何?」or「lofiいいよね!」です。肌感覚としては、そういう感じです。
そしてlofiなんていうジャンルを知っている人はけっこう音楽が好きなタイプですので、たいていはサブスクリプションサービスに登録していて、結構マニアックでかっこいい曲を知っています。
問題はSpotifyを使っているのかということになりますが、これについてはサブスクリプションの双璧をなすapple musicとのユーザー数比較から、Spotifyにフォーカスしてもよいのではと思わされます。
2.5億人利用のSpotifyが見せつけるApple Musicとの「差」Spotifyは2019年第3四半期の決算報告書を公開し、月間アクティブユーザー(MAU)の合計が2億4800万人に達したことを明らかにした...Appleは今年6月に同社が運営する音楽ストリーミングサービス「Apple Music」のユーザー数を6000万人と発表した。現段階では多少の増加をしていることは想定されるが、それでもSpotifyが世界的に多くのシェアを占めている状況に変わりない。
ただ、今回はiOSアプリを作るというわけで、iPhoneを使っている人はけっこうapple musicなんじゃないのというのは考慮すべきでした……実際、友人たちと話しながら最近そんな気がしてきています?
2「mix動画をプレイリストにしたいと思っているか」については、まあ多分そうなんじゃないのという感じです。
アプリを作ろうと思ったきっかけにも書きましたが、mixの曲順や構成を編集したいなあとかバックグラウンドで再生したいなあという思いはlofiやhiphopのmixを聴いているような人も多く抱えている気持ちだろうと推測できます。
まず編集モチベーションは、先程も述べたlofiやhiphopを聴いている人たちって結構マニアックだというところから、気に入ったプレイリストに曲の追加なんかをしたいということは十分にありうるかと考えました。
また、lofiやhiphopのmixは特に「作業用」という位置づけで動画が作られていることが多いことから、バックグラウンド再生の需要は相当あるのではと推測しました。パソコンの作業中であればYouTubeで事足りますが、電車の移動中や勉強中のバックグラウンド再生に十分に需要がありえます。以上より、lofiやhiphopに限ってもそれなりにいるのではないかと推測し、また実際にはmix動画は他音楽ジャンルにも多くあるので、必ずしも小さいとは言えないアンメットニーズが存在しうると考えました。(いちおう調べた範囲で同じようなアプリはありませんでした!よかった……)
技術的に苦労または工夫したポイント
ここからは技術的に苦労や工夫をしたポイントについて軽く触れていきたいと思います。
これらのポイントについては個別に深堀りした記事を今後作るつもり満々です。一旦、ここでは簡単に自分にとっての現段階での要点をおさえるに留めさせてください!
なお、今回は初めてのiOSアプリ開発ということもあり、4ヶ月前には「storyboardってなんや……」とか「API……アピ??」とか言っていました。そんな人間にとっての「要点」だということを心に留めておいていただけますと幸いです!
……要は、間違っているかもしれませんよということです!もし何かお気づきになったことやアドバイスがあったら、コメントいただけますと幸いです?動画に用いられている曲情報の取得
動画そのものから曲の情報を取得するのは厳しかったので、概要欄から攻めることにしました。
競プロでの経験が活きて、簡単なアルゴリズムではありますが時間をかけずに作成できました!たいていのmix動画は概要欄に曲の情報が記載されており、その多くは下記のような作りになっています。これはあくまでも私の経験則です!
[曲の情報以外のこと]
XX:XX(再生時間) XX(曲名) [区切り文字(/や;や空白)] XX(アーティスト名)
XX:XX(再生時間) XX(曲名) [区切り文字(/や;や空白)] XX(アーティスト名)
……(繰り返し)
[曲の情報以外のこと]ことが少し複雑化する要因は再生時間や曲名の記載順が動画によって前後することです。
例えば先ほど上で貼った動画の概要欄はこうなっています。IV△7 - III7 - VIm7 - Vm7 - I7 とか
Track List
01 スチャダラパー × Nice & Smooth / 今夜はBoogie Down Bronx (original mix) 0:00
02 Yogee New Waves / CLIMAX NIGHT 0:19
03 宇宙ネコ子 / Night Cruising Love (with 入江陽 & lulu & Enjoy Music Club) 2:28
……
30 TOCCHI / これだけで十分なのに 39:24
CITYPOP HIPHOP
MIX
JAPANESECITYPOP JAPANESEHIPHOP
シティポップ 日本語ラップ
当動画で表示される広告は楽曲の使用許可に基づくものであり、動画制作者は管理できません。また、全ての広告収入は著作権者並びに各アーティストの方々に支払われます。ご理解のほど宜しくお願い致します。そこで、今回はこういった概要欄をAPIで取得した上で、以下の手順でプレイリストを作成しています。
1. :が入っている行のみ抽出
2. 抽出した各行に対して、数字と記号を除去する操作を実行
3. "feat."など、Spotify検索時にノイズとなってしまう特定種類の文字列を除去
4. 各行を検索にかけ、検索結果トップの曲をプレイリストに追加各行をそのまま検索にかけることで歌手名と曲名を区別する必要性を回避しました!
ただ当然、現段階ではこれで対応できない動画も多くあります。
1. :が入っていない動画
時間の表記をXX'XX"としている動画もあります。これは今後、対応が簡単です!
2. 時間が書いていない動画
これは相当厄介です。ただのテキストから曲名とアーティスト名だけを抜き出す方法は現段階では思いついていません。連続する単語列を片っ端からSpotify検索にかけてもいいのですが、最適列幅の特定やノイズとして入り込む無関係曲の除去をどうすればいいのでしょうか
3. 概要欄に何も書いていない動画
これが一番きついです。しかもまあまあこういう動画もあります。「今後実装したいこと」で述べるmusic recognitionとの連携が必須になるでしょうこのアルゴリズム部分が当該アプリのクオリティを80%決定する気がするので今後改善していかなければならないなと思っています……!
クロージャーを用いた通信待ち
API通信を主として、外部との通信を行うアプリの制作においては、通信した結果に得られたデータを用いてこの処理がしたい!という瞬間が必ず存在すると考えています。
この時、普通にプログラムを書いてしまうと、通信を実施するのに必要な時間が考慮されないまま、通信している間にプログラムが実行されていってしまい、サーバーからデータが得られたときにはプログラムが終わっているなんていうことがあります。というか、めちゃありました。
そこで、swiftのclosureと呼ばれる関数を用いてこれを解決します。closureには様々な説明の仕方があるようですが、今は一旦処理対象の値が確定してから処理を実行するタイプの関数であると理解しています。
そうです、通信して取得したデータに行いたい処理はclosureの形で書けば、対象であるデータが確定(=取得)されるまでは待っていてくれるようになるのです!初めはclosureってややこしそうだな、使うのやめとことか思っていましたが、こういった用途を学んだときはとても感動しました。ライフサイクルを把握した上での実装
いっとき、viewに配置したボタンがどうがんばってもズレる。いくらコードを見直しても設定値は全く間違っていないという自体に心を折られました。
結局の所、viewのライフサイクルを理解せず、画面のレイアウトが決定される段階以前の部分で座標計算をしようとしていたことが原因でした。
また、UI実装にとどまらず、githubで見つけたライブラリを使用した際にも、iOSのバージョンアップによるライフサイクルチェンジに対応させることが必要でした。
このときも、なぜか全然動かないライブラリを前に途方にくれた記憶があります。デバッグの手段をprint()を配置することしか知らない私は、とりあえずあらゆる部分にprintを配置し、2時間ほど溶かしたあとにappdelegateの関数が呼び出されていないことに気づいた次第です。デバッグの方法は今後勉強していきたいです。どなたかよい文献をご存知でしたら教えて下さい泣SegueとPresentの使い分け
遷移の仕方や値渡しの仕方が異なる2つの画面遷移方法、SegueとPresentをどう使い分ければよいかを考えました。
他論点と同様に詳細は別記事にまわしますが、今回は異なるview間での値渡しが、クロージャーを用いていたとしても容易なPresentを主軸に実装を行いました。ユーザーの動きを想定したUI設計
今回アプリのUI設計は、とても親しいUIデザイナーの方に手伝ってもらいました。私のイメージしていた第一案は、そうとうbrush upされました。なんならもともと、アイコンはipadで作った手書きのニコニコマークでいくつもりでした。正気じゃないです。
この過程は相当勉強になりました。本当にありがとうございます泣
ここはまだ言語化できていない学びが多く、あとでまた記事にしていきたいと思います!!!どう広めるか
アプリを作ったはいいものの、使う可能性のある人にどう届けるかが課題です。
広告を打つとかそんなことをするようなアレではないので、今回は一旦redditといった海外のものも含めたオンラインコミュニティ上で、適当な場所を見つけ、リンクと共に紹介文を投稿するやり方を取りました。
せっかく音楽関連であれば言語の垣根を越えようと思い、積極的に英語での使用を意識して作成しました!
他の方たちはどのような広め方をしているのか、もう少し今後勉強していきたいと思っています?今後実装したいこと
今後実装したいことはいまのところ下記の通りです。
アルゴリズムの向上
先程も述べたアルゴリズムを採用していることから、実際にプレイリストを作成できる動画はかなり限られます。今後のaspirationとして、music recognition技術を連携することで「動画で使用されている音楽をplaylistにするアプリ」を実現できたら面白いなと考えています。
ShazamやSound Houndに代表されるmusic recognition技術はAPIがかなり限られており、いまのところの第一候補はAudDです。まさかの無料です(たぶん)。
もし実現すれば、mix動画だけでなく、例えばファッションショーのBGMをプレイリスト化する、気になる映像作品のBGMを特定するといった幅広い用途が拓かれていきます。
なんとかしてYouTube動画の音声を抽出できればチャンスは十分あると考えていますが、調べてみた範囲ではYouTubeの規約では動画のDLは禁止されており、なんとかできないのかなあと検討しています。ユーザー登録とレコメンド機能
これはもうおなじみですが、ユーザー登録できるようにしてレコメンドができるようにしてみたいです。
これは本当にシンプルなミドルウェアから、所有IPを伴ったミドルウェアへの価値向上という意味でも重要かと考えていますし、DBとサーバー連携の実装を勉強する意味でもかなりいい題材かもなと考えています。変換履歴の蓄積
ユーザーの変換を履歴として蓄積するのも下記の効果をもたらす意味で有効かと思っています。
1. キャッシュの利用によるプレイリスト作成の高速化
これは特に、先ほど書いたようなmusic recognitionの実装を通じてアプリの処理が重くなってきたらクリティカルになるでしょう。計算量を線形から定数にできる(はずですよね流石に??)ので、大事です。
2. ランキングの作成
動画の変換される回数ランキングが作れるようになります。面白いかどうかはわかりませんが、「ユーザー登録とレコメンド機能」でも述べた意味をこれも同様に持つでしょう。以上が初めて作ったiOSアプリの解説記事です!長く書いて疲れてしまいました?
こんな長文で書いて誰か読んでくれるのかな……もしここまで読んでいただいた方がいたら、本当にありがとうございました!!??
- 投稿日:2021-03-14T23:45:02+09:00
Youtube-Spotify連携型プレイリスト作成アプリを作りました!
こんにちは!趣味でエンジニアリングをしているcanalunです。こちらが自己紹介です!
去年の12月頃から初めてのiOSアプリ開発に取り組み、紆余曲折を経てなんとかYoutube-Spotify連携型プレイリスト作成アプリを完成・リリースさせることができました!
せっかくなので、作成の経緯や苦労したポイントを自分なりにここに書き残してみようかと思います!なお、アプリストアへのリンクはこちらです?
mixed!どんなアプリなのか??
YouTubeには複数の曲を1つのクリップにまとめた"mix"と呼ばれるタイプの動画が多く存在します。hiphopやlofiと呼ばれるジャンルの音楽が好きな方は、きっとよくご存知ですよね?
例えばこのような動画です!
【ネオシティポップとか】Neo City Pop / Japanese Hip Hop etc. Mix + Video【日本語ラップとか】
[ ???????? ] 코딩할때 듣기 좋은 노래 • lofi type beat • 3 hours playlist今回作成したアプリは、こういった動画をSpotify上のプレイリストに変換するものです!
具体的には、下記のような操作およびUIで動きます。
1. Spotifyでログイン
2. 好きなmix動画のURLを入力し、プレイリスト作成を開始
3. プレイリスト作成の成功/失敗を通知(成功の場合はSpotifyプレイリスト画面への遷移機能も提供)なぜ作ろうと思ったのか??
私はlofiやhiphopが好きなのですが、特にこれらのジャンルはYouTube上にmix動画が多くあげられており、よく再生しています。
ただ、いつも再生しながらmixの曲順や構成を編集したいなあとかバックグラウンドで再生したいなあと思うことがよくありました。(YouTubeプレミアムには入っていないんですわ)
そこである日、「こういうのってもしかしたら動画を音楽プラットフォームのプレイリストに変換できれば解決するのでは??」と考え、アプリ開発を始めてみました!
幸い、いちおう調べた範囲で同じようなアプリはありませんでした?使ってくれる人はいるだろうか??
結論としては「lofiやhiphopのような音楽を好むようなわりかし音楽が好きな人たちを中心に、需要自体はそれなりにあるのではないか」と考えました。
下記のような考え方をしましたが、自分の感覚で推測したに過ぎないので実際にはあまり当たっていない可能性もあるなと考えています?めちゃ単純に"lofi mix"でgoogle検索すれば52,800,000本、"hiphop mix"でgoogle検索すれば12,100,000本の動画が見つかります。動画の多さを需要の大きさであると単純解釈すれば、lofiやhiphopのmixってそんな少ないわけではなくて、そこそこ多くの人に再生されているんだろうなと考えられます。
すなわち、今回のアプリを使ってくれる人が満たしているべき最低条件である「mix動画を好んでよく聴いている人」というのはまあまあいることになります(たぶんだけど)。
問題はこの人たちが下記2点を満たすかです。
1. Spotifyを使っているのか
2. mix動画をプレイリストにしたいと思っているか1「Spotifyを使っているのか」については、十中八九Yesかと考えました。
lofiもhiphopも日常生活で話題に出ると、よく聴く人とあまり聴かない人に二分されるイメージがあります。lofiって言った時、たいてい反応は「lofiって何?」or「lofiいいよね!」です。肌感覚としては、そういう感じです。
そしてlofiなんていうジャンルを知っている人はけっこう音楽が好きなタイプですので、たいていはサブスクリプションサービスに登録していて、結構マニアックでかっこいい曲を知っています。
問題はSpotifyを使っているのかということになりますが、これについてはサブスクリプションの双璧をなすapple musicとのユーザー数比較から、Spotifyにフォーカスしてもよいのではと思わされます。
2.5億人利用のSpotifyが見せつけるApple Musicとの「差」Spotifyは2019年第3四半期の決算報告書を公開し、月間アクティブユーザー(MAU)の合計が2億4800万人に達したことを明らかにした...Appleは今年6月に同社が運営する音楽ストリーミングサービス「Apple Music」のユーザー数を6000万人と発表した。現段階では多少の増加をしていることは想定されるが、それでもSpotifyが世界的に多くのシェアを占めている状況に変わりない。
ただ、今回はiOSアプリを作るというわけで、iPhoneを使っている人はけっこうapple musicなんじゃないのというのは考慮すべきでした……実際、友人たちと話しながら最近そんな気がしてきています?
2「mix動画をプレイリストにしたいと思っているか」については、まあ多分そうなんじゃないのという感じです。
アプリを作ろうと思ったきっかけにも書きましたが、mixの曲順や構成を編集したいなあとかバックグラウンドで再生したいなあという思いはlofiやhiphopのmixを聴いているような人も多く抱えている気持ちだろうと推測できます。
まず編集モチベーションは、先程も述べたlofiやhiphopを聴いている人たちって結構マニアックだというところから、気に入ったプレイリストに曲の追加なんかをしたいということは十分にありうるかと考えました。
また、lofiやhiphopのmixは特に「作業用」という位置づけで動画が作られていることが多いことから、バックグラウンド再生の需要は相当あるのではと推測しました。パソコンの作業中であればYouTubeで事足りますが、電車の移動中や勉強中のバックグラウンド再生に十分に需要がありえます。以上より、lofiやhiphopに限ってもそれなりにいるのではないかと推測し、また実際にはmix動画は他音楽ジャンルにも多くあるので、必ずしも小さいとは言えないアンメットニーズが存在しうると考えました。(いちおう調べた範囲で同じようなアプリはありませんでした!よかった……)
技術的に苦労または工夫したポイント
ここからは技術的に苦労や工夫をしたポイントについて軽く触れていきたいと思います。
これらのポイントについては個別に深堀りした記事を今後作るつもり満々です。一旦、ここでは簡単に自分にとっての現段階での要点をおさえるに留めさせてください!
なお、今回は初めてのiOSアプリ開発ということもあり、4ヶ月前には「storyboardってなんや……」とか「API……アピ??」とか言っていました。そんな人間にとっての「要点」だということを心に留めておいていただけますと幸いです!
……要は、間違っているかもしれませんよということです!もし何かお気づきになったことやアドバイスがあったら、コメントいただけますと幸いです?動画に用いられている曲情報の取得
動画そのものから曲の情報を取得するのは厳しかったので、概要欄から攻めることにしました。
競プロでの経験が活きて、簡単なアルゴリズムではありますが時間をかけずに作成できました!たいていのmix動画は概要欄に曲の情報が記載されており、その多くは下記のような作りになっています。これはあくまでも私の経験則です!
[曲の情報以外のこと]
XX:XX(再生時間) XX(曲名) [区切り文字(/や;や空白)] XX(アーティスト名)
XX:XX(再生時間) XX(曲名) [区切り文字(/や;や空白)] XX(アーティスト名)
……(繰り返し)
[曲の情報以外のこと]ことが少し複雑化する要因は再生時間や曲名の記載順が動画によって前後することです。
例えば先ほど上で貼った動画の概要欄はこうなっています。IV△7 - III7 - VIm7 - Vm7 - I7 とか
Track List
01 スチャダラパー × Nice & Smooth / 今夜はBoogie Down Bronx (original mix) 0:00
02 Yogee New Waves / CLIMAX NIGHT 0:19
03 宇宙ネコ子 / Night Cruising Love (with 入江陽 & lulu & Enjoy Music Club) 2:28
……
30 TOCCHI / これだけで十分なのに 39:24
CITYPOP HIPHOP
MIX
JAPANESECITYPOP JAPANESEHIPHOP
シティポップ 日本語ラップ
当動画で表示される広告は楽曲の使用許可に基づくものであり、動画制作者は管理できません。また、全ての広告収入は著作権者並びに各アーティストの方々に支払われます。ご理解のほど宜しくお願い致します。そこで、今回はこういった概要欄をAPIで取得した上で、以下の手順でプレイリストを作成しています。
1. :が入っている行のみ抽出
2. 抽出した各行に対して、数字と記号を除去する操作を実行
3. "feat."など、Spotify検索時にノイズとなってしまう特定種類の文字列を除去
4. 各行を検索にかけ、検索結果トップの曲をプレイリストに追加各行をそのまま検索にかけることで歌手名と曲名を区別する必要性を回避しました!
ただ当然、現段階ではこれで対応できない動画も多くあります。
1. :が入っていない動画
時間の表記をXX'XX"としている動画もあります。これは今後、対応が簡単です!
2. 時間が書いていない動画
これは相当厄介です。ただのテキストから曲名とアーティスト名だけを抜き出す方法は現段階では思いついていません。連続する単語列を片っ端からSpotify検索にかけてもいいのですが、最適列幅の特定やノイズとして入り込む無関係曲の除去をどうすればいいのでしょうか
3. 概要欄に何も書いていない動画
これが一番きついです。しかもまあまあこういう動画もあります。「今後実装したいこと」で述べるmusic recognitionとの連携が必須になるでしょうこのアルゴリズム部分が当該アプリのクオリティを80%決定する気がするので今後改善していかなければならないなと思っています……!
クロージャーを用いた通信待ち
API通信を主として、外部との通信を行うアプリの制作においては、通信した結果に得られたデータを用いてこの処理がしたい!という瞬間が必ず存在すると考えています。
この時、普通にプログラムを書いてしまうと、通信を実施するのに必要な時間が考慮されないまま、通信している間にプログラムが実行されていってしまい、サーバーからデータが得られたときにはプログラムが終わっているなんていうことがあります。というか、めちゃありました。
そこで、swiftのclosureと呼ばれる関数を用いてこれを解決します。closureには様々な説明の仕方があるようですが、今は一旦処理対象の値が確定してから処理を実行するタイプの関数であると理解しています。
そうです、通信して取得したデータに行いたい処理はclosureの形で書けば、対象であるデータが確定(=取得)されるまでは待っていてくれるようになるのです!初めはclosureってややこしそうだな、使うのやめとことか思っていましたが、こういった用途を学んだときはとても感動しました。ライフサイクルを把握した上での実装
いっとき、viewに配置したボタンがどうがんばってもズレる。いくらコードを見直しても設定値は全く間違っていないという自体に心を折られました。
結局の所、viewのライフサイクルを理解せず、画面のレイアウトが決定される段階以前の部分で座標計算をしようとしていたことが原因でした。
また、UI実装にとどまらず、githubで見つけたライブラリを使用した際にも、iOSのバージョンアップによるライフサイクルチェンジに対応させることが必要でした。
このときも、なぜか全然動かないライブラリを前に途方にくれた記憶があります。デバッグの手段をprint()を配置することしか知らない私は、とりあえずあらゆる部分にprintを配置し、2時間ほど溶かしたあとにappdelegateの関数が呼び出されていないことに気づいた次第です。デバッグの方法は今後勉強していきたいです。どなたかよい文献をご存知でしたら教えて下さい泣SegueとPresentの使い分け
遷移の仕方や値渡しの仕方が異なる2つの画面遷移方法、SegueとPresentをどう使い分ければよいかを考えました。
他論点と同様に詳細は別記事にまわしますが、今回は異なるview間での値渡しが、クロージャーを用いていたとしても容易なPresentを主軸に実装を行いました。ユーザーの動きを想定したUI設計
今回アプリのUI設計は、とても親しいUIデザイナーの方に手伝ってもらいました。私のイメージしていた第一案は、そうとうbrush upされました。なんならもともと、アイコンはipadで作った手書きのニコニコマークでいくつもりでした。正気じゃないです。
この過程は相当勉強になりました。本当にありがとうございます泣
ここはまだ言語化できていない学びが多く、あとでまた記事にしていきたいと思います!!!どう広めるか
アプリを作ったはいいものの、使う可能性のある人にどう届けるかが課題です。
広告を打つとかそんなことをするようなアレではないので、今回は一旦redditといった海外のものも含めたオンラインコミュニティ上で、適当な場所を見つけ、リンクと共に紹介文を投稿するやり方を取りました。
せっかく音楽関連であれば言語の垣根を越えようと思い、積極的に英語での使用を意識して作成しました!
他の方たちはどのような広め方をしているのか、もう少し今後勉強していきたいと思っています?今後実装したいこと
今後実装したいことはいまのところ下記の通りです。
アルゴリズムの向上
先程も述べたアルゴリズムを採用していることから、実際にプレイリストを作成できる動画はかなり限られます。今後のaspirationとして、music recognition技術を連携することで「動画で使用されている音楽をplaylistにするアプリ」を実現できたら面白いなと考えています。
ShazamやSound Houndに代表されるmusic recognition技術はAPIがかなり限られており、いまのところの第一候補はAudDです。まさかの無料です(たぶん)。
もし実現すれば、mix動画だけでなく、例えばファッションショーのBGMをプレイリスト化する、気になる映像作品のBGMを特定するといった幅広い用途が拓かれていきます。
なんとかしてYouTube動画の音声を抽出できればチャンスは十分あると考えていますが、調べてみた範囲ではYouTubeの規約では動画のDLは禁止されており、なんとかできないのかなあと検討しています。ユーザー登録とレコメンド機能
これはもうおなじみですが、ユーザー登録できるようにしてレコメンドができるようにしてみたいです。
これは本当にシンプルなミドルウェアから、所有IPを伴ったミドルウェアへの価値向上という意味でも重要かと考えていますし、DBとサーバー連携の実装を勉強する意味でもかなりいい題材かもなと考えています。変換履歴の蓄積
ユーザーの変換を履歴として蓄積するのも下記の効果をもたらす意味で有効かと思っています。
1. キャッシュの利用によるプレイリスト作成の高速化
これは特に、先ほど書いたようなmusic recognitionの実装を通じてアプリの処理が重くなってきたらクリティカルになるでしょう。計算量を線形から定数にできる(はずですよね流石に??)ので、大事です。
2. ランキングの作成
動画の変換される回数ランキングが作れるようになります。面白いかどうかはわかりませんが、「ユーザー登録とレコメンド機能」でも述べた意味をこれも同様に持つでしょう。以上が初めて作ったiOSアプリの解説記事です!長く書いて疲れてしまいました?
こんな長文で書いて誰か読んでくれるのかな……もしここまで読んでいただいた方がいたら、本当にありがとうございました!!??
- 投稿日:2021-03-14T23:20:43+09:00
[Swift5]MessageUIを用いたメール機能(お問い合わせ機能)の実装方法
やりたいこと
今回はMessageUIを用いたメール機能の実装方法を紹介します。
メール機能 == お問い合わせ機能
として扱えるかと思います。
ユーザー情報を管理する場合、通報機能としても代用できるのでぜひ参考にしてください!使用するライブラリ
import MessageUIコード紹介
今回はお問い合わせボタンが存在すると仮定して紹介します。
@IBAction func tapMailButton(_ sender: Any)
はお問い合わせボタンがタップされると呼ばれます。// お問い合わせボックスをタップすると呼ばれる @IBAction func tapMailButton(_ sender: Any) { // メールを送信できるかどうかの確認 if !MFMailComposeViewController.canSendMail() { print("Mail services are not available") return } // インスタンスの作成とデリゲートの委託 let mailViewController = MFMailComposeViewController() mailViewController.mailComposeDelegate = self // 宛先の設定(開発者側のアドレス) let toRecipients = ["ここにユーザーから受信するメールアドレスを入力"] // 件名と宛先の表示 mailViewController.setSubject("開発者側で設定する件名") mailViewController.setToRecipients(toRecipients) mailViewController.setMessageBody("開発者側で設定する本文", isHTML: false) // mailViewControllerの反映(メール内容の反映) self.present(mailViewController, animated: true, completion: nil) }これでメールの設定は完了です。
次にメールを開いた後の結果でアクションを分岐します。
// メール機能終了処理 func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { // メールの結果で条件分岐 switch result { // キャンセルの場合 case .cancelled: print("Email Send Cancelled") break // 下書き保存の場合 case .saved: print("Email Saved as a Draft") break // 送信成功の場合 case .sent: print("Email Sent Successfully") break // 送信失敗の場合 case .failed: print("Email Send Failed") break default: break } //メールを閉じる controller.dismiss(animated: true, completion: nil) }これでMessageUIを用いたメール機能(お問い合わせ機能)の実装は完了です。
アドレスや、件名や本文を自身で触ってみると楽しいですよ!備考
メール機能はXcodeのシュミレーターでは動かないので、動作確認は実機で行うようにして下さい。
- 投稿日:2021-03-14T17:37:20+09:00
【RxSwift】KickstarterのViewModelインターフェースをスマートにするラッパー
はじめに
RxSwiftを使ってViewModelを設計する時、
特にKickstarterのViewModelインターフェースでは
プロパティの再定義が冗長で可読性を落としてしまう。var hoge:PublishRelay<Hoge> init() { let _hoge = PublishRelay<Hoge>() self.hoge = _hoge.asObservable() }これをプロパティ1つ1つ再定義していてはかなり無駄であるため、
これを解決するラッパーを使用する。ラッパー
以下の記事を参考にした。
https://qiita.com/yokirin/items/a8df2bc9080ccf853724これを使うと、上記の冗長な再定義がこうなる
いやあ神すぎる。。。個人的な1番の負債が一発で解消できた。@PublishRelayWrapper var hoge:PublishRelay<Hoge> init() { }再定義がいらない。
使用するときは、self.$hogeで使用する。(リンク先を参考に。)終わりに
KickstarterのViewModelインターフェースは使いやすいが、
冗長な再定義でずっと悩んでいた。それをラッパーで解決できたので
これから気持ちよく使っていけそう。
自分でラッパーを用意できるようになりたいところ。
- 投稿日:2021-03-14T15:29:15+09:00
SwiftUIチュートリアル2を丁寧に説明しながらやってみるよ。
おはよう
さてさてSwiftUIのチュートリアル2をやっていくよ。前回
チュートリアル1:Viewを作って、組み合わせてみよう!チュートリアル2: リストとナビゲーション画面を作ってみよう!
SwiftUI Essentials : Building Lists and Navigation
今回はランドマークの一覧画面と、それぞれの詳細ページを表示できるビューを作っていきます。Section1:ランドマークモデルを作ってみよう。
チュートリアル1では各要素を直書きしていたね。
ここではモデルを作ることで、呼び出した値を要素に反映する方法を学ぶよ。呼び出すデータは、チュートリアルページからダウンロードした、「landmarkData.json」を使用するよ。
作っているプロジェクトの上に、該当ファイルをドラッグすると、こんな感じで追加がされるよ。続いて新しく「Landmark.swift」というファイルを作ります。
こちらは、これまで作ってきたViewファイルではなく、データの読込処理のためのファイルなので、こちらのSwift Fileというテンプレートを使用します。こちらには、landmarkData.jsonを呼び出した時の、項目名と値に一致するような要素を記載していきます。
ここでつけているプロパティ「Hashable」と「Codable」は、それぞれ下記のような意味です。
特徴 Hashable ハッシュ値として定義するときに使うよ。これをつけることで、辞書のキー項目や、集合として使用することができるようになる。 Codable JSONなどの外部表現との互換するために、データ型をエンコード・デコード可能にするよ。 参考ページ:
Encoding and Decoding Custom TypesAssets.xcassetsに画像を追加して、Landmark.swiftにもImageファイルを呼び出すための項目を付け足すよ。このとき「import SwiftUI」を追加し忘れないように! Imageがないよーって怒られる。
imageNameがprivateなのは、表示するファイル名を呼び出すのにこの項目を使い、実際画面に表示するのは、Image自体だから、とのこと。
landmarkData.jsonのcoordinates(=座標)は、structsにして保持するよ。
続いて、CoreLocationというデバイスの位置情報を読み取るためのフレームワークを使用します。
CLLocationCoordinate2DというのはWGS 84という測定系に基づいた形で場所を紐づけるための構造体で、そこにCoordinates型の変数coordinatesのlatitude, longitudeの値を渡しています。
イメージとしては、Jsonで読みこんだ値がcoordinatesに格納され、その格納された値をフレームワークに渡している感じかな。最後に、新しくModelData.swiftファイルを作成し、JSONを読み込むメソッドを実装していきます。
プロジェクト内のリソースにアクセスするのに使うのが、「Bundle(バンドル)」。
これを使うことで、アプリ内のリソースを簡単にロードすることができるよ。
特にその中でもBundle.mainは、現在実行中のコードを含むバンドルディレクトリを指しているみたい。
なので、コードとしては下記のような意味になるのかな?ModelData.swiftimport Foundation // 1. 実行しているところ。 var landmarks: [Landmark] = load("landmarkData.json") //2.1で使用しているload関数の中身。データの入ったjsonファイルの名前が引数として渡されている。 func load<T: Decodable>(_ filename: String) -> T { let data: Data //3.Bundle.main = 今動いているアプリのディレクトリ。その中から、"landmarkData.json"というターゲットファイルを取得している。 guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else { fatalError("Couldn't find \(filename) in main bundle.") } do { // 4.Dataはメモリ内のバイトバッファの構造体。取り込んだファイルの中身を、バイト列として読み込みしている感じかな? data = try Data(contentsOf: file) } catch { fatalError("Couldn't load \(filename) from main bundle:\n\(error)") } do { //5.Jsonのデコーダーを作って、dataに格納されたデータをデコード let decoder = JSONDecoder() return try decoder.decode(T.self, from: data) } catch { fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") } }最後に、今プロジェクト直下に作成されているファイルを、Model, Views, Recoursesフォルダに格納し直して、整理整頓ー。
Section2:行表示をしよう!
Section1ではモデルをJSONデータからモデルを作成したね。次はModelからデータを読み出して表示をしてみるよ。
新しいSwiftUIファイル「LandmarkRow.swift」を作って、まずはプレビュー画面にモデルから取得した値を表示指定みよう。実行の流れ1、2、3の順だよ。
LandmarkRow.swiftimport SwiftUI struct LandmarkRow: View { // 2.Landmark.swiftファイルでLandmarkを定義したね。その構造体を元に、変数「landmark」を定義。 var landmark : Landmark var body: some View { // 3.渡されたJSONファイル1行目のname項目の値をTEXTとして表示 Text(landmark.name) } } struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { //1.プレビューをするとLandmarkRowが実行されるよ。そのさいの引数がlandmakrs[0] //ここで指定しているlandmarksはModelDataで定義したものだよ。 //こちらにはJSONファイルからloadしたデータが入っているよ。 //なので、ここでは、landmarkとしてJSONファイルの1行目を渡しているんだね。 LandmarkRow(landmark: landmarks[0]) } }おお、JSONファイルの1行目、nameの項目に格納された値が表示されているね。
あとは第一回で習ったHStackとImageを駆使して、Imageの横に文字が表示されるように変更するよ。
Section2でのプレビューと比べると、サイズが変わっているのがわかるね。Section3: 行プレビューをカスタムしちゃおう!
Section2ではJSONの1行目を呼び出して、画像をつけて表示したね。次は、JSONのデータを有効活用してみよう。
Section2でlandmarks[0]と、jsonの1行目を取り出してその名前を表示したね。0→1に変更すると表示も変わるよ。
このプレビューの表示を変えていくよ。static var previews: some View { LandmarkRow(landmark: landmarks[1]) //previewLayoutはプレビュー用のレイアウトを上書きするのに使うよ。 //ここでは縦横のサイズを指定しているんだね。 .previewLayout(.fixed(width: 300, height: 70)) } }複数設置する場合には、Groupを使ってコンテンツ同士をグループ化。そこに対して、previewLayoutをつけると、すっきり描けるよ。
▼ちょっとまどろっこしいコード
struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { Group { LandmarkRow(landmark: landmarks[0]) .previewLayout(.fixed(width: 300, height: 70)) LandmarkRow(landmark: landmarks[1]) .previewLayout(.fixed(width: 300, height: 70)) } } }▼すっきりしたコード
struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { Group { LandmarkRow(landmark: landmarks[0]) LandmarkRow(landmark: landmarks[1]) } .previewLayout(.fixed(width: 300, height: 70)) } }Section4:リストを作成してみよう!
Listを使って、よくあるメニューリストを作ってみるよ。
LandmarkList.swiftを作成するよ。
Section3ではLandmarkRow_Previewsを主に編集したけれど、今回は本体のViewを編集します。
Section3で作ったLandmarkRowインスタンスを呼び出します。LandmarkList.swiftstruct LandmarkList: View { var body: some View { //List 別個に作られたLandmarkRowをリスト形式で表示するのに使うよ。 List { LandmarkRow(landmark: landmarks[0]) LandmarkRow(landmark: landmarks[1]) } } }Section5:リストを動的に作成しよう!
さてさてアプリ画面ぽくなってきたね。でも、表示する内容は直書き・・とまでは言わないけれど、jsonの1行目、2行目と指定して表示しているよね。今度は、それを動的に作成するよ。
先ほど作った静的なLandmarkRowを消して、かわりにModelData.swiftで定義したlandmarksをListに渡すよ。
ついでに、リストに対する理解を深めるために、
①静的にリストを作る方法
②動的にリストを作る方法
③動的にリストを作る方法(チュートリアルStep2で載っているやり方)
を載せるね。import SwiftUI // リスト② で呼び出しているemojisの型。ここで重要なのが「Identifiable」 // リストに渡す値は一意である必要があるので、「Identifiable」に準拠しているよーと宣言すると良い。 struct Facemark: Identifiable { let name: String let emoji:String let id = UUID()・ } private var emojis = [ Facemark(name:"にっこり",emoji:"?"), Facemark(name:"おこ",emoji:"?"), Facemark(name:"尊い",emoji:"?"), Facemark(name:"いつもの", emoji:"☺️") ] struct LandmarkList: View { var body: some View { // リスト① 静的リストを作るときはこんな感じだよ。 List{ Text("こんな感じで"); Text("Textを配置すると"); Text("静的なリストが"); Text("できるよ"); } // リスト② リストの引数にコレクションを渡すと、Listは値を1つ=1行で読み出して表示をします。 // $0はクロージャーの引数名を省略したときに使われるもので,$0=引数の1個目、$1=引数の2個目という感じで表示がさレます。 List(emojis){ Text($0.name); Text($0.emoji); } // リスト③ チュートリアルに載っているリストの作り方。 // こちらはidをいう一意の値を引数で指定することで、識別可能な状態にしています。 //ちょっと変わった書き方に見えるけれど、これはクロージャーという無名変数を使っているよ。意味はこんな感じ。 // (引数名:引数の型) { XX-戻り値の型-XX in // 処理 // } List(landmarks, id: \.id) { landmark in LandmarkRow(landmark: landmark) } } }ポイントとしてはListは一意な値を引数に設定することが必要!
Section6:ナビゲーションを使ってListから詳細に移動するようにしてみよう!
LandmarkDetail.swiftを作成して、チュートリアル1で作ったContentViewの中身を貼り付けるよ。
代わりにContentView.swiftは、中身をLandmarkListを呼び出すように変更。struct LandmarkList: View { var body: some View { // NavigationViewを使うと、ページ間のビュー移動をカスタマイズできます。 // NavigationLinkで指定しているのが、クリックしたときの遷移先。 //navigationTitleはリスト上部にタイトルを表示するのに使う。 NavigationView { List(landmarks) { landmark in NavigationLink( destination: LandmarkDetail()){ LandmarkRow(landmark: landmark) } } .navigationTitle("Landmarks") } } }動きは実際に作ってみていただきたいところですが、リストからクリックすると詳細ページに飛んで、
詳細ページからリストへの戻りボタンも自動で作成されているのが便利ポイントだなと思いました。Section7:詳細ページもデータから作ってみよう!
続いては、LandmarkDetailの編集だよ。
詳細ページには、テキストの他に画像、MAPもあり、それぞれ、imageNameとcoordinates(座標)をJSONから取得した値を渡すことで実装していきます。ここから複数のファイルを変更することになるので、チュートリアルの順序とあべこべになってしまいますが、
まずはデータの流れをみていきますー。先ほど作成したLandmarkList.swiftで、クリックした時のリンクに、LandmarkDetailを指定したね。
ここに、引数としてlandmarkを渡してあげます。LandmarkList.swiftstruct LandmarkList: View { var body: some View { NavigationView { List(landmarks) { landmark in // ここでlandmark型のlandmark値を渡しているよ。内容としてはそのリスト1行を作っているjsonデータを渡している感じだね。 NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } .navigationTitle("Landmarks") } } }受け取り側のLandmarkDetail.swiftで引数としてlandmarkを定義して、LandmarkListから渡された値を受け取れるようにして、今度はMapViewにlandmark.locationCoordicateを、 CircleImageにlandmark.imageを渡してあげるよ。
合わせて、Detailの直書きしているタイトルや説明文をlandmarkの該当の値に変更します。
完成形としてはこんな感じ。LandmarkDetail.swiftstruct LandmarkDetail: View { var landmark: Landmark var body: some View { ScrollView { // ここでMapViewに引数として座標の値を渡しているよ。 MapView(coordinate: landmark.locationCoordinate) .ignoresSafeArea(edges: .top) .frame(height: 300) //ここでCircleImageに画像名を渡しているよ。 CircleImage(image : landmark.image) .offset(y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { // このあたりは今まで Text("tutlerock")みたいに、直書きだったのを変更しているよ。 Text(landmark.name) .font(.title) .foregroundColor(.primary) HStack { Text(landmark.park) Spacer() Text(landmark.state) } .font(.subheadline) .foregroundColor(.secondary) Divider() Text("About ¥(landmaark.name)") .font(.title2) Text(landmark.description) } .padding() } .navigationTitle(landmark.name) .navigationBarTitleDisplayMode(.inline) } }座標のデータを渡されたMapViewはこんな感じで、変更します。
MapView.swiftstruct MapView: View { // LandmarkDetailからの引数としてcoordinateを受け取れるようにする。 var coordinate : CLLocationCoordinate2D @State private var region = MKCoordinateRegion() var body: some View { Map(coordinateRegion: $region) //onAppearは地図を描画の時に呼ばれるコールバック関数だよ。 //なので、下記の記述は地図を描画する時に、setRegion関数を実行するってことだね。 .onAppear { setRegion(coordinate) } } // 座標に基づいて領域を表示するための計算をprivate関数で記載 private func setRegion(_ coordinate : CLLocationCoordinate2D) { region = MKCoordinateRegion( center : coordinate, span : MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2) ) } }こちらはCricleImage.swift 画像の表示です。
CircleImage.swiftimport SwiftUI struct CircleImage: View { // 引数としてImageを取得 var image: Image // そのImageに対して、形の整形をする。 var body: some View { image .clipShape(Circle()) .overlay(Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 7) } }これでListをクリックすると、詳細ページに移動するようになったよ
Section8: プレビューを動的に作成してみよう!
デバイスのサイズによって実際表示される画面は変わるよね。
なので、ここでは、プレビューデバイスを変更して、いろんなサイズでのアプリ画面を確認できるようにするよ。
プレビューのデバイスを指定するにはpreviewDeviceを追加します。
ここで指定するモデルは、Xcodeのデバイスメニューで選ばれている名前orモデル番号で指定します。
もちろん、ここからプレビューを変えてもOK!LandmarkList() .previewDevice(PreviewDevice(rawValue: "iPhone SE (2nd generation)"))一気に複数端末でプレビューしたい場合には、ForEachを使用して複数回LandmarkListを実行するようにすればOK
// ForEach(Data,ID)のようにデータを渡す。 // プレビューデバイス="iPhone SE(2nd)"として、Listの表示を1回、 // プレビューデバイス= "iPhone XS Max"としてListの表示を1回する。 ForEach(["iPhone SE(2nd)","iPhone XS Max"], id : \.self) {deviceName in LandmarkList() .previewDevice(PreviewDevice(rawValue: deviceName)) }こうすれば色々な端末でアプリを動かした時の様子が一目で分かって作りやすいね。
最後に理解度チェックをやって終了!
まとめ
用意されているフレームワークやオブジェクトを有効活用すると、簡単にそれっぽい画面が作れて良いね!
間違っているところがあれば、ご指摘ください。
- 投稿日:2021-03-14T14:03:45+09:00
ReactiveSwiftのActionで発火したSingalProducerをDisposeする方法の検討
問題
下記のようなActionがあるとします。
class ViewController: UIViewController { private let action = Action<Void, Void, Never> { SignalProducer { observer, lifetime in let workItem = DispatchWorkItem { observer.send(value: ()) observer.sendCompleted() } DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: workItem) lifetime.observeEnded { workItem.cancel() } } } }下記各パターンで
viewDidAppear
で発火して、完了する前にViewController
がdeinit
しても、dispose
されません。
Action.swift#L143のソースからもdispose
を管理していないため、dispose
されないことがわかります。private let disposable = CompositeDisposable() override func viewDidLoad() { super.viewDidLoad() // パターン1 self.action <~ self.reactive.viewDidAppear // パターン2 self.action <~ self.reactive.viewDidAppear.take(during: self.reactive.lifetime) // パターン3 self.disposable += self.action <~ self.reactive.viewDidAppear } // パターン4 override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.action.apply().start() } deinit { self.disposable.dispose() }
dispose
する方法
Action
内で生成したSignalProducer
を直接dispose
するしかなさそうパターン1
<~
のバインディング演算子が使えず、また入力が必要な場合もスマートに値を入力できなさそうです。override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.action.apply().take(during: self.reactive.lifetime).start() }パターン2
<~
のバインディング演算子が使えて、ソースから値を入力できるので、パターン1よりスマートそう。
ただ、余分に拡張関数を追加する必要があるので、コード量が必要。override func viewDidLoad() { super.viewDidLoad() self.reactive.action <~ self.reactive.viewDidAppear } private extension Reactive where Base: ViewController { var action: BindingTarget<Void> { self.makeBindingTarget { base, _ in base.action.apply().take(during: base.reactive.lifetime).start() } } }パターン3
入力に合わせて、
Lifetime
も送ってみる。
Lifetime
しか対応できないため、他のdispose
パターンに対応しきれない。class ViewController: UIViewController { private let action = Action<(Void, Lifetime), Void, Never> { SignalProducer { observer, lifetime in let workItem = DispatchWorkItem { observer.send(value: ()) observer.sendCompleted() } DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: workItem) lifetime.observeEnded { workItem.cancel() } } .take(during: $0.1) } override func viewDidLoad() { super.viewDidLoad() self.action <~ self.reactive.viewDidAppear.map(value: ((), self.reactive.lifetime)) } }パターン4
SignalProducer
をAction
の入力値にしてみる。class ViewController: UIViewController { private let action = Action<SignalProducer<Void, Never>, Void, Never> { $0 } override func viewDidLoad() { super.viewDidLoad() self.action <~ self.reactive.viewDidAppear.compactMap { [weak self] in guard let self = self else { return nil } return SignalProducer { observer, lifetime in let workItem = DispatchWorkItem { observer.send(value: ()) observer.sendCompleted() } DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: workItem) lifetime.observeEnded { workItem.cancel() } } .take(during: self.reactive.lifetime) } } }
- 投稿日:2021-03-14T13:58:04+09:00
[Swift] UITableViewで無限スクロールを実装する方法[初心者用]
概要
UITableViewをスクロールしていき、一番下まで到達したら新しいセルが新たに表示されるようにする。
コード
import UIKit class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! let cellId = "cellId" var numbers:[String] = ["No.1","NO.2","NO.3","No.4","NO.5","NO.6","No.7","NO.8","NO.9","No.10","NO.11","NO.12"] var count:Int = 0 override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. tableView.delegate = self tableView.dataSource = self count = numbers.count } } extension ViewController:UITableViewDelegate,UITableViewDataSource{ //セルの数 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return numbers.count } //セルの登録とラベルの設定 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! TableViewCell cell.numberLabel.text = numbers[indexPath.row] return cell } //セルの高さ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 60 } // 現在取得している最後のセルを表示したら新しく10個のセルを追加するメソッド func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if indexPath.row == numbers.count - 1 { while count <= indexPath.row + 10{ numbers.append("No.\(count + 1)") count += 1 } self.tableView.reloadData() } } } //UITableViewCellの指定 class TableViewCell:UITableViewCell{ @IBOutlet weak var numberLabel: UILabel! }大切なのはwillDisplayのあたりぐらいですかね。willDisplayメソッドの中で使っているif文は「indexPath.rowがnumbers.count - 1に一致したら{}の中の処理を走らせてね」ってことです。
if文の条件が満たされたらwhile文を使って配列のnumbersに新しく要素を追加することでセルの数を増やしていってます。まとめ
willDisplayを使えば、Twitterのプロフィール画面みたいにツイートを遡っていくこともできそうですね。私自身初心者なので誤りがあるかもしれませんが、その時はぜひご指摘のほどよろしくお願いいたします。
もしよろしければ使ってみてください。ありがとうございました。
- 投稿日:2021-03-14T04:05:32+09:00
Alamofire + Combine を試す
はじめに
最近、 iOS開発、SwiftUI の勉強をしているのですが、WebAPI の呼び出しをするため Alamofire を選定しました。
非同期処理では Alamofire + RxSwift などを使用するのが一般的のようですが、Alamofire 5.2.0 で Apple 製の非同期ライブラリ Combine がサポートされましたので、 Alamofire + Combine を試してみました。ライブラリ等
Xcode : 12.4
Alamofire : 5.4.1サンプルアプリ
サンプルアプリとして、Chatwork API トークンを TextField に入力させ、値が取得できたら name に名前をセットして表示する、ミニマムなアプリを作成しました。
WebAPI は Chatwork API の /me を使用しています。
実装
LoginViewModel.swiftimport Foundation import Combine import Alamofire class LoginViewModel: ObservableObject { private var disposeBag = Set<AnyCancellable>() @Published var token = "" @Published var name = "name" func getMe() { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase AF.request("https://api.chatwork.com/v2/me", headers: ["X-ChatWorkToken": token]) .publishDecodable(type: MeModel.self, decoder: decoder) .sink { data in guard let me = data.value else { print("error") return } print(me.name) print(me.accountId) self.name = me.name }.store(in: &disposeBag) } }
.publishDecodable(type: MeModel.self, decoder: decoder)
が DataResponsePublisher を返却するため、Combine で扱うことができるようになります。Chatwork API のレスポンスは、 key がスネークケース
account_id
ですが、Swift ではキャメルケースaccountId
ですので、
decoder.keyDecodingStrategy = .convertFromSnakeCase
で、変換するように指定しています。Decodableを実装したデータモデルの定義
自分自身の情報を表すデータモデルは以下のように定義しました。
データの定義 MeModel.swift
MeModel.swiftimport Foundation struct MeModel: Decodable { var accountId: Int var roomId: Int var name: String var chatworkId: String var organizationId: Int var organizationName: String var department: String var title: String var url: String var introduction: String var mail: String var telOrganization: String var telExtension: String var telMobile: String var skype: String var facebook: String var twitter: String var avatarImageUrl: String var loginMail: String }参考までに、UIの定義はこんな感じです。
UIの定義 ContentView.swift
MeModel.swiftimport SwiftUI struct ContentView: View { @ObservedObject var viewModel = LoginViewModel() var body: some View { NavigationView { VStack(alignment: .center) { VStack(alignment: .leading) { Text("Token") TextField("token", text: $viewModel.token, onCommit: { }) .keyboardType(.alphabet) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.horizontal) Text(viewModel.name) } .padding(10) Divider() Button("Get") { viewModel.getMe() } .padding() } .overlay(RoundedRectangle(cornerRadius: 5).stroke()) .padding(.horizontal, 50) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }Moya は Combine に対応していない
ちなみに、Alamofire をより簡単に利用するための Moya というライブラリがありますが、14.0.0 では Combine にはまだ対応していません。
14.0.0 beta で一度導入されたものの、問題があってリリース前に消えたようです。
- 投稿日:2021-03-14T02:17:13+09:00
Fire Storageを監視してサイレントプッシュ通知を送信
概要
Fire Storageに画像ファイルのアップロードがなされるとCloud Functionsを通してサイレントプッシュ通知を送信します。
今更感はありますが、ググってもpayloadの記述に統一性がなかったりして割と苦戦しました
Stack OverFlowの方でも同じ悩みを抱えていた人がいたので参考になれば幸いです。
なお、本記事ではFirebase CLIや証明書周りについては触れません。
下準備ができている前提で進めます。環境
- Xcode 12.4
- Swift 5
- nodejs 12
Fire Storage
デフォルトバケットを使用
クライアント
Capability
TARGETS > CapabilitiesよりBackground ModesとPush Notificationsを有効にします。
Background ModesはRemote notificationsにチェックを入れます。
(バックグラウンドで通知を受け取りたい場合はBackground fetchもチェック)Swift
今回はTopic購読で実装します。
AppDelegate.swiftimport Firebase @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() // 1. 通知許可 let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization( options: authOptions, completionHandler: { (granted, error) in // todo }) application.registerForRemoteNotifications() return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // 2. Topic購読 Messaging.messaging().subscribe(toTopic: "hoge") { _ in // todo } } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // 3. 画像ファイルの名称を取り出す guard let imageName = userInfo["imageName"] as? String else { return } print("image name: \(imageName)") } }Cloud Functions
onFinalize関数でバケットに画像がアップロードされたことを検知します。
index.js'use strict'; const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(functions.config().firebase); exports.sendSilentNotificationWithTopic = functions.storage.object().onFinalize((object) => { const topic = 'hoge'; const payload = { data: { imageName: object.name, // 画像名をセット }, }; const options = { contentAvailable: true // サイレントプッシュはtrueにする }; return admin.messaging().sendToTopic(topic, payload, options) .then(function(response) { return console.log("Successfully sent message:", response); }) .catch(function(error) { return console.log("Error sending message:", error); }); });firebase deployを実行してデプロイします。
実行
- Xcodeに実機をつないでRun
- Fire Storageに画像ファイルを追加
Xcodeのコンソールログに画像名が表示されれば成功です。
自分はこれで成功したのですが、失敗したらすいません。。。