20200921のSwiftに関する記事は9件です。

iOSDC 2020 印象に残ったトークまとめ?

はじめに

iOSDC 2020に参加しました!:hugging::sparkles:
https://iosdc.jp/2020/
初のオンライン開催でしたが、会社の仲間と一緒にわいわい見るのは楽しかったです。
SwiftUI関連のトークが多くて、昨今のトレンドを反映してるなぁという印象を受けました。

どれも勉強になるトークでしたが、印象に残ったトークを備忘録としてまとめておきたいと思います。
スライド画像多めでまとめているので、ざーっと流し見するも良し、気になるものがあればURLから飛んで詳細を見るも良しです:grin:(YouTubeのURLもアップされ次第追記予定です)

Day1

iOSには無いmacOS独自機能をCatalystで実装する

概要

内容

スクリーンショット 2020-09-21 17.44.57.png

macOSはもともとAppKitだけだったが、一部にUIKitが利用できるようになった
macOSとiOSの違いも吸収されている
Xcodeでチェックを入れるだけでMacでも使えるようになる

スクリーンショット 2020-09-21 17.47.31.png
macOS独自機能
* メニュー
* タッチバー
* ツールバー

スクリーンショット 2020-09-21 17.49.40.png
タッチバーの一部でSwiftUIと相性が悪いところがあった
SwiftUIを利用したい場合はmacOSネイティブのアプリを開発したほうが良いかも?

スクリーンショット 2020-09-21 17.55.24.png

ひとこと所感

まだ全然macのこととか考えられていませんでしたが、このトークを聞いて、やるべきことのイメージが少し持てるようになりました!

そろそろCombine

概要

内容

スクリーンショット 2020-09-21 18.08.18.png
Combineとは?
連続した多種類の非同期処理などを単一の方法で扱う宣言的なAPI

スクリーンショット 2020-09-21 18.08.49.png
iOSの非同期処理は難しい
* タイミング次第で結果が変わってしまう
* どこで処理を実装しているのか探すのが一苦労

スクリーンショット 2020-09-21 18.09.18.png
非同期を扱う方法もたくさんある

スクリーンショット 2020-09-21 18.09.38.png
Combineでは、あらゆる処理をPublisherにすることで、非同期をスマートに扱うことができる
他のフレームワークとの連携も容易になる

スクリーンショット 2020-09-21 18.43.55.png
宣言的とは?
指示的:「冷蔵庫からペットボトルの水を取り出して、コップに注いで、飲みます」
宣言的:「受け取った水を飲みます」

スクリーンショット 2020-09-21 18.44.05.png
宣言的のメリット

スクリーンショット 2020-09-21 18.10.52.png
Combineは宣言的なので、コードも短くスッキリする

ひとこと所感

Combineってなんぞや?という状態でしたが、RxSwiftと概念的・用語的に似ている部分があるとわかって少し安心しました。勉強せねば…

新規機能開発からモジュール分割を始めてみる

概要

内容

スクリーンショット 2020-09-21 19.03.38.png
機能が増えるとコードベースも巨大化して開発効率の低下につながるので、モジュール分割を行った

スクリーンショット 2020-09-21 19.04.12.png
既存アプリに取り入れたいけど、依存が深くてどこから切り出せばいいのか分からない

スクリーンショット 2020-09-21 19.11.10.png
大規模リファクタが必要だけど、新規機能開発も止めるわけには…(わかりみ)

スクリーンショット 2020-09-21 19.11.18.png
そこで、新規機能からモジュール分割する前提で設計することにした

スクリーンショット 2020-09-21 19.04.27.png
手順はこちら

スクリーンショット 2020-09-21 19.04.41.png
独立した新規画面であったり、既存ドメイン仕様への依存が少ない場合はモジュール分割しやすい

スクリーンショット 2020-09-21 19.06.44.png
スクリーンショット 2020-09-21 19.14.41.png
メリットもあるけど、デメリットもある
コンフリクト回避のために、XcodeGenを先に導入すべきだった
抽象度が上がるので、実装がどこにあるのかわかりずらくなり、新しく入ったメンバーが困る場面もあった

ひとこと所感

新規機能からモジュール分割するの良さそう。いざやろうとしたら「どこから切り出していいか分からない」と絶対なると思うので、その際に再度見返したいトークでした!

効率よくUIKitからSwiftUIへ移行する

概要

内容

スクリーンショット 2020-09-21 19.25.39.png
SwiftUIを使ったほうがいい理由
* SwiftUIではより多くのことが、より少ないコードでできる
* 色んな機能がUIKitよりも楽に実装できる(アクセシビリティ、ダークモード/ライトモード、標準レイアウトなど)
* すぐにUIKitがなくなるわけではないが、いずれSwiftUIでやるしかない場合が出てくるかもしれない

スクリーンショット 2020-09-21 19.29.25.png

SwiftUIへの移行を阻むもの(待ったほうがいい理由)
* UIKitほど歴史がないのでバグがあったり、OSによって違いがあったりする
* UIKitと混ぜて使うのは苦労する

スクリーンショット 2020-09-21 19.33.12.png
SwiftUIへの移行をどこから始めたらいいか?
* SwiftUIからObjective-Cが直接使えない(逆もしかり)ので、早期にSwift化しておく
* 移行が重なると面倒なので、Swiftの最新バージョン、機能を使っておく

スクリーンショット 2020-09-21 19.38.08.png
事前にUIKitをモダンな使い方にしておくのも大事。
* AutoLayoutを使う
* SefeAreaを使う
* 巨大なStoryboardにせず、再利用しやすくするためコンポーネントに分ける(部分的にSwiftUIに移行しやすくなる)
* 宣言的なUIKit APIを使う(StackView、Compositional Layoutsなど)

スクリーンショット 2020-09-21 19.46.29.png
* 本番のアプリにいきなりSwiftUIを導入するのではなく、同じ分野のアプリを小さい規模でプロトタイプとして作ったほうが、メリットデメリットがわかりやすい
* その中で、SwiftUIに向いてない画面が特定できるので、必要に応じてデザインの変更を検討する
* 既存のアプリ内で、iOS13限定の機能をSwiftUIで作るのもおすすめ

スクリーンショット 2020-09-21 19.50.16.png
SwiftUIに向いているアーキテクチャはこの3つ
* Redux
* TCA
* MVVM

スクリーンショット 2020-09-21 19.51.19.png
Tips
* 1つの画面の中でSwiftUIとUIKitを混ぜすぎないこと
* TableViewなどシンプルな画面から移行すること
* 急ぎすぎないこと
* SwiftUIは深いところまでCombineに依存しているので、SwiftUIを正しく使うにはCombineの知識も必要になる

ひとこと所感

すぐに使える知見がたくさん詰まったトークでした!いずれSwiftUIを本番に導入する日が来るかも?なので、計画的に準備していきたいと思いました。

Day2

Xcode Preview でUIKitベースのアプリ開発を効率化する

概要

内容

スクリーンショット 2020-09-21 20.14.55.png
Xcode PreviewsはXcode11で導入されたプレビュー機能

スクリーンショット 2020-09-21 20.15.23.png
SwiftUIでの実装ではXcode Previewsが使えるので、リアルタイムにレイアウトを確認することができる

スクリーンショット 2020-09-21 20.16.37.png
UIKitベースの実装ではコード修正のたびにSumilatorを起動する必要があって不便…

スクリーンショット 2020-09-21 20.17.05.png
現状多くのアプリがまだUIKitベースなので、そこにXcode Previewsを組み込んだ話

スクリーンショット 2020-09-21 20.18.53.png
スクリーンショット 2020-09-21 20.19.00.png
Previewコードを書くときに使うプロトタイプたち↑

スクリーンショット 2020-09-21 20.19.11.png
外部から状態を注入できるような、「PreviewableなView」にしておけると良い

スクリーンショット 2020-09-21 20.19.47.png
メルペイでの活用例
短いテキストの場合、長いテキストの場合のレイアウト崩れがないか、一度にチェックできる

スクリーンショット 2020-09-21 20.20.06.png
異なる画面サイズの端末だったり

スクリーンショット 2020-09-21 20.20.21.png
多言語対応している場合は、言語ごとに見たりもできる

スクリーンショット 2020-09-21 20.21.20.png
iOS13未満もサポートしている場合はどうしたらいいのかというと、2つのやり方がある
* マクロを使ってPreviewコードをビルドから除外する
* Preview専用のターゲットを追加する

スクリーンショット 2020-09-21 20.21.51.png
Xcode Previewsを導入して既存アプリをSwiftUIの開発スタイルに寄せておくと、SwiftUIへの移行がよりスムーズになるかも?

ひとこと所感

いろんな状態のViewを一度に表示・確認できるのはめちゃくちゃ便利だなと思いました!毎回Simulator起動するのめんどうなので、ぜひ導入したい…

XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する

概要

内容

スクリーンショット 2020-09-21 20.37.37.png
UITestのつらさ。いつの間に壊れている(悲しい)

スクリーンショット 2020-09-21 20.37.50.png
UIは変わりゆくものだから…

スクリーンショット 2020-09-21 20.38.22.png
もともと手動テストのみだったが、QAの工数を減らす手段の一つとしてUITestを導入

スクリーンショット 2020-09-21 20.38.51.png
新機能と既存機能、どっちを対象にするか?という話があるが、メルペイでは既存機能に対象を絞った
UITestは繰り返し実施されるテストと、安定稼働している機能に対して行うと効率が良い

スクリーンショット 2020-09-21 20.39.06.png
リグレッションテストに組み込み、効率化を実現した

スクリーンショット 2020-09-21 20.39.50.png
XCUITestに入門する上で押さえたい3つの重要なクラス

スクリーンショット 2020-09-21 20.40.25.png
実際のコードはこんな感じ
テスト対象アプリを定義 → UI要素検索 → UI要素取得 → UI操作 → UI検索 → 表示チェック

スクリーンショット 2020-09-21 20.41.08.png
UITestを書いていると、重複したコードがあることに気づく
まとめたい

スクリーンショット 2020-09-21 20.41.14.png
Page Object Patternというデザインパターンが有効

スクリーンショット 2020-09-21 20.41.39.png
3つの大きなメリットがある

スクリーンショット 2020-09-21 20.41.53.png
PageObjectable プロトタイプを定義して、重複するコードをまとめる

スクリーンショット 2020-09-21 20.43.32.png
UITestは並列化実行しても時間がかかる

スクリーンショット 2020-09-21 20.43.42.png
メルペイでは夜間実行したり、GitHub ActionsのLabelトリガーを使ったりしている

ひとこと所感

苦難に立ち向かった知見が詰まっていて、これからUITestを始める人にとってぴったりのトーク内容でした。具体的なコードがあったのでとてもわかりすかったです。

Webとネイティブアプリの付き合い方を改めて考える

概要

内容

スクリーンショット 2020-09-21 21.03.30.png
スクリーンショット 2020-09-21 21.03.36.png
新しいサービスを作るようになったときのあるある

スクリーンショット 2020-09-21 21.04.19.png
Webとネイティブアプリの比較
* アプリ単体での起動・動作
* オフライン動作
* プッシュ通知
などはネイティブアプリにしかない機能

スクリーンショット 2020-09-21 21.04.46.png
PWAとは?
ネイティブアプリっぽく動くwebアプリケーション

スクリーンショット 2020-09-21 21.05.09.png
PWAも比較
インストールが可能なので、アプリ単体で立ち上げることができる

スクリーンショット 2020-09-21 21.05.25.png
App Clipsも比較

スクリーンショット 2020-09-21 21.05.44.png
(ここから先はあくまで私見によるもの と伝えつつ)
ネイティブアプリと同じ体験をWebでも提供することは可能か?
プッシュ通知や最新のiOS技術のことを考えると難しいのでは

スクリーンショット 2020-09-21 21.06.05.png
現状、プッシュ通知はネイティブアプリ一択

スクリーンショット 2020-09-21 21.06.17.png
どういうときにWebを活用したら良いか?
* まずは広く機能を提供したいとき
* プッシュ通知がいらないとき
* 最新のOS機能がいらないとき

スクリーンショット 2020-09-21 21.06.35.png
専門知識が必要だったり、iOS/Androidセットで作らないとユーザーが減ってしまうことなどがネイティブアプリのデメリット

スクリーンショット 2020-09-21 21.06.45.png
Googleからの検索流入が見込める、両OSに対応できるのはWebのメリット

ひとこと所感

アプリエンジニアなのでネイティブを推したい気持ちはありつつ、Webのメリットも知った上で正しく技術選択をしていく必要がありますよね…。深く考えさせられるトークでした!

テストコードが増えるとバグは減るのだろうか? - 「0% → 60.3%」で見えた世界の話

概要

内容

スクリーンショット 2020-09-21 21.22.37.png
仕様は決まった!と言っても、実は完全には決まっていないことも…(あるある)

スクリーンショット 2020-09-21 21.23.45.png
テストを書くことで仕様の理解が深まる

スクリーンショット 2020-09-21 21.23.55.png
テストには動的テストと静的テストがある

スクリーンショット 2020-09-21 21.24.15.png
仕様書のチーム内レビューは静的テストに分類され、動的・静的を併用することで大きな成果が得られる

スクリーンショット 2020-09-21 21.26.42.png
UI,ロジック、APIなどそれぞれテストが書けていれば、不具合の原因特定も容易になる

スクリーンショット 2020-09-21 21.26.58.png
モックが書けていれば、意図しないレスポンスが返ってきたケースなども再現できる

スクリーンショット 2020-09-21 21.27.47.png
スクリーンショット 2020-09-21 21.27.56.png
しかし、適切なテストを書くには、経験や慣れ、様々な知識が必要

スクリーンショット 2020-09-21 21.29.05.png
長い目で見たら役に立つが、開発時点では間違いなく工数がかかる
それを許容できる開発環境が必要

スクリーンショット 2020-09-21 21.39.32.png
現状の課題をよく見つめ、様々なテスト手法がある中で最適なテスト方法を選択すること
費用対効果があるかを考えるのがポイント

スクリーンショット 2020-09-21 21.29.26.png
スクリーンショット 2020-09-21 21.29.36.png
テストコードが増えるとバグが減るか?
→ △

スクリーンショット 2020-09-21 21.29.46.png
テストコードにバグがないなど複数の前提条件を満たすとき、バグは減らせる

ひとこと所感

△というのがリアルでいいですね。経験が浅いとテストコードのテストコードが必要になってきそうで、テスト難しいなって改めて思いました。

おわりに

(トークの内容が濃すぎて)気づいたら大作になってしまいました!

見たかったけど見れなかったトークのスライドをここに置いておきます。
YouTube動画が配信されたら見るぞ〜:yum:

iOSDC本当に楽しかったです!みなさまお疲れ様でした!!!!:tada:
スクリーンショット 2020-09-21 18.28.17.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSDC2020初参加して

iOSDC参加しました

今回自分はiOSDC初参加しまして、参加した記事を書くまでがiOSDCだということで

あまりまとまった内容はかけないですが、とりあえず感想だけでもと思い書いています。

iOSDCとは

iOSDCは、iOS周辺技術に関わるエンジニアが、技術的なノウハウを共有するためのイベントです。

2016年から開催されていて今年で4年目のようです。

iOSDC2020公式サイト

今年は初のオンライン開催でした。

ノベルティの量

下世話な話ですがノベルティの量すごい!

スマホスタンドからマスクケースからお水まで。

色々とあってすごいなーと思いました。

参加しての感想

本題の参加しての感想ですが、

いや〜楽しかった!

自分はエンジニアとしてまだまだで、正直聴きながらだと分からずに

タイムシフトで見返してやっと分かる、みたいなものもたくさんありました。

ただ、これは自社の別の現場で使えそうだな〜とか

自分自身試してみたい技術やそれを使って作りたいものも発見というか気づきを得れてとても有意義なものでした!

あとは、スピーカーの皆さんのスライド作るの凝っていたり、真面目過ぎずにクスッとするような作りのものもあって

発表資料としての参考にもなりました。

本当にお疲れ様でした!

もちろん、このカンファレンスを開催してくださった方々もお疲れ様でした!

さいごに

まだまだ、みれていない視聴させていただいたのに、フィードバックできていないトークもありますので

そちらにフィードバックを返したり、発表資料をTwitter等で上げていただけているので資料まとめみたいな記事を

自分用に、備忘録的にまた上げたいなと思います。(トークで気になった記事なども)

勢いで書いているので拙い文章がさらに拙く拙く拙くネストしていっていますが

自分のiOSDC Japan 2020 はいったんこれで区切りとします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

辞書について

1.はじめに


前回配列について説明したので、これまた重要な要素である辞書について説明していこうと思います。前回の配列について解説した記事のURLは以下になります。
(URL:https://qiita.com/0901_yasyun/items/6cc4c0175082e9b63d31)

2.辞書とは


辞書とは、配列と同様に複数のインスタンスを格納できるコレクションの1種です。Swiftの辞書はDictionary型ですが、配列と同様、構造体として実現されており、変数への代入などの動作によって、必要に応じて新しいインスタンスが作成されます。
1つの辞書には、キーと値の組を複数個格納でき、キーから値を検索して参照、格納を行うので、1つの辞書に含まれるキーはすべて異なります。
キーに対応する値は1つですが、異なるキーに同じ値が設定されていても問題はありません。
ただし、キーと値はそれぞれ同一の型で統一されている必要があります。

var y = ["Swift":2014, "Objective-C":1983]

この例ではString型をキーとし、Int型を値とするエントリを2個もった辞書インスタンスを生成しています。

型のみを宣言する場合は、次のように記述します。

var y : [String : Int]

次のように記述すると、String型をキーとし、Int型を値とする辞書のインスタンスが代入されますが、格納されている要素は0個です。

var y = [String : Int]()  // イニシャライザの呼び出し

空の辞書を表すには[:]と記述します。変数を型宣言し、空の辞書を代入するように記述しても、先ほどと同様な初期化が行われます。

var y : [String : Int] = [:]  // 型を指定した変数に空の辞書を代入

なお、辞書型はパラメータ付き型指定で次のように記述することもできます。

var y : Dictionary<String, Int>  // [String:Int]と同じ意味
var y : Dictionary<String, Int>()  // イニシャライザの呼び出し

3.辞書へのアクセス


すでに要素をもつ辞書型の定数や変数に対し、キーを指定して値を取り出すことができます。そのために、配列にアクセスするときと同じように[]を使い、[]の中にキーを指定します。ただし返り値はオプショナル型で、存在しないキーを指定するとnilが返ってきます。
if-let文を使用した次に例を見てください。

var y = ["Swift":2014, "Objective-C":1983]
if let v = d["Swift"] { print(v) }    // 存在するキーなので"2014"と出力
if let v = d["Ruby"] { print(v) }     // 存在しないキーなので出力なし

この例では辞書は[String:Int]型なので、返される値はInt?型です。
辞書に新しい値を追加するには、[]の中にキーを指定して値を代入します。
また、既に存在しているキーの要素を削除するには、そのキーを指定してnilを代入します。以下の例を見てください。

var e = ["Ruby":1995]
print(e)   // ["Ruby": 1995]と出力
e["Java"] = 1995
e["Python"] = 1991
print(e)   // ["Java": 1995, "Ruby": 1995, "Python": 1991]と出力
e["Java"] = nil
print(e)   // ["Ruby": 1995, "Python": 1991]と出力

ここまでの例ではキーが文字列で、値が整数でしたが、他の型でももちろん大丈夫です。
しかし、Swiftの基本的なデータ型の中で辞書のキーとして利用できるものは、各種整数型、実数型、Bool型、文字列、文字などです。実数もキーにできますが、実数値には誤差がつきものなので注意が必要です。

4.辞書の比較


辞書同士を比較する際には、演算子「==」と「!=」が使えます。互いに同じキーの集合を持ち、同じキーに等しい値が割り当てられている辞書を等しいと判断します。以下の例を確認してください。

var a = ["one":"I", "two":"II", "three":"III"]
let b = ["two":"II", "one":"I"]
a == b   // false
a["three"] = nil
a == b   // true

5.辞書から要素を取り出す


辞書は付属型としてのキーを表すKey型、値の型を表すValue型を持ちます。
またキーだけからなるコレクション、値だけからなるコレクションを提供するプロパティがあり、それぞれの型はKeys型、Values型となっています。
辞書自体もコレクションなので、要素を表す型であるElement型、添字の型であるIndex型を持っています。Element型は次のタプルの別名となっています。

(key: Key, value: Value)

for-in文を使って、辞書から要素を1つずつ取り出すことができます。その際、取り出されるのが上記のタプルです。以下の例を確認してください。

var dic = [String: Int]()
var n = 1
for ch in "ありがとう" {
    dic[String(ch)] = n; n += 1
}

print(dic)  
for t in dic {  // タプルで取り出す
    print("\(t.key)=\(t.value)", terminator:" ")
}

print()
for (k, v) in dic {  // キーと値を取り出す
    print("\(k)=\(v)", terminator:" ")
}

print()

これを実行するとこのような結果が得られます。

["が": 3, "と": 4, "り": 2, "う": 5, "あ": 1]
が=3 と=4 り=2 う=5 あ=1
が=3 と=4 り=2 う=5 あ=1

この結果より、辞書の要素が取り出される順番は、実行するたびに異なるのが分かります。
この仕組みについて解説すると少し長くなってしまうので、ひとまず取り出される順番はランダムだということを覚えておいてください。
気になる方は、ハッシュ値などについて調べれば仕組みが分かると思います。

6.おわりに


今回は辞書の基本的な仕組みと主な使い方について解説しました。
今回の記事ではプロパティやメソッドについて、詳しく説明することができなかったので、いつかまた説明する記事が書ければと思います。
読んでくれた方、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】機械学習(=ML)とAIとの違いを学んだのち、Core ML を実装してみる。

機械学習(= ML) とは?

機械学習は英語で、「Machine Learning
簡単に言うと、以下を指します。

  • 『AIが自律的に物事を学ぶための技術
  • 『機械に大量のデータ・パターン・ルールを学習させることにより、判別や予測をする技術』

ML.png

ML は、意外と歴史のある AI 分野のひとつ

機械学習はAIという概念の中の、1つの分野です。

1959年、機械学習の「父」とされている Arthur Samuel は、
機械学習を以下のように定義しています。

「明示的にプログラムしなくても学習する能力」を、コンピュータに与える研究分野。
“Field of study that gives computers the ability to learn without being explicitly programmed”
-- Arthur Samuel --

「AI=機械学習」ではなく、
AI > 機械学習 > ディープラーニングのイメージです。

スクリーンショット 2020-09-21 15.13.26.png

なぜ近年、「機械学習」が大きな話題となっているのか

必要性

これまで人間はデータを分析し、そのデータに基づいてシステムや手順を変更してきました。

しかし、世界のデータ量が増大し、管理できなくなってきており、
データから学習し、それに応じて適応できる自動システムが必要とされています。

技術の進歩

  • AI技術の進歩
  • 大量データの出現
  • コンピューター処理能力の向上

使用例

  • アマゾンエコーは、機械を使用してトレーニングされた音声テキストと音声認識を使用します
  • 疾患の早期発見のために、医学界でも使用されています
  • 自動運転車は、機械学習に依存して自分自身を運転します

AI と ML と DL の違い

人工知能【Artificial Intelligence】

人間のような知能をもつアルゴリズム。

アルゴリズム ...
「何を」「どのような順番で」「何に対して行うのか」を記述したもの。

機械学習【Machine Learning】

AIが自律的に物事を学ぶための技術。

ディープラーニング(= 深層学習)【Deep Learning】

多層のニューラルネットワークを活用し、物事の特徴を抽出する技術。

因みに...

機械学習が「人間が判断・調整する」のに対し、
ディープラーニングは「機械が自動的に行う」ことが特徴。

ディープラーニングで、人間が見つけられない パターンやルールの発見、特徴量の設定が可能になり、

人の認識・判断では限界があった 画像認識・翻訳・自動運転 といった技術が飛躍的に上がった。

ディープラーニングと機械学習の違いとは?

機械学習は、3つに分けられる

機械学習の主な手法には、「教師あり学習」 「教師なし学習」 「強化学習」がある。

教師あり学習 (= Supervised Learning)

正解データを元に、入力データの特徴やルールを学習します。

「過去のデータから、将来起こりそうな事象を予測すること」に使われます。

  • 回帰: 連続する数値を予測する
  • 分類: あるデータがどのクラスに属するかを予測する

例.
【回帰】 "天候"と"お弁当の販売個数" の関係を学習し、お弁当の販売個数を予測する、
不動産価値、商品価格、株価、会社業績 etc

【分類】 果物をサイズ別に分ける、画像や音声を種類別に分ける、
電子メールがスパム(迷惑メール)かどうかを判定する etc

教師なし学習 (= Unsupervised Learning)

正解データなしでデータの特徴やルールを学習します。

「データに潜む傾向を、見つけ出すため」に使われます。

  • クラスタリング: データのグループ分け
  • アソシエーション分析: データ間の関連を発見する
  • 異常検知: 人による指導なく、正常なものと不正常なもの(異常)を検知する

例. 【クラスタリング】 FacebookやInstagramの「あなたの友達かも..?」機能
【アソシエーション分析】 紙おむつを購入する人はビールも購入するetc

強化学習 (= Reinforcement Learning)

失敗や成功を繰り返させ、どの行動が最適か学習します。

✅ 成功に対して「報酬」を与えることで学習効率を上げる方法です。

ロボットの歩行制御

ロボットに「歩けた距離」を報酬として与えます。するとロボットは、
歩行距離を最大化するために、自らさまざまな歩き方を試行錯誤します。
そうすることで、歩行可能距離の長いアルゴリズムが構築されます。

囲碁AIの「Alpha Go」

囲碁は手のパターンが膨大過ぎて、既存の最新のコンピュータでも、手を読み切ることは不可能です。
よって、強化学習により、勝ちまでの手を読み切る代わりに、「どの手を打てば勝ちに近づくか」を学習させます。
試合にて失敗や成功を繰り返すと、最適な行動のみを選択するようになります。こうして「Alpha Go」は強くなっていったのです。

参考サイト

© 2020 データアーティスト株式会社
機械学習をどこよりもわかりやすく解説!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCodeでswiftの環境設定をする方法

はじめに

VSCodeでSwiftを使うための環境設定の方法を丁寧に紹介します。
競技プログラミングでSwiftを使いたい、見慣れたVSCodeで使いたい、Xcodeで標準入力の仕方がよくわからないといった方におすすめです。

開発環境

OS: macOS Catalina Version 10.15.6
Visual Studio Code (VSCode): Version 1.49.1

手順

  1. Xcodeのインストール
  2. VSCodeのインストール
  3. NodeとNPMのインストール
  4. SourceKit-LSP Extension for Visual Studio Codeのインストールとビルド
  5. SourceKit-LSPの設定

1. Xcodeのインストール

Xcodeがすでにインストールされている場合は飛ばして構いません。
Xcodeのインストール方法: https://techacademy.jp/magazine/1409

2. VSCodeのインストール

VSCodeがすでにインストールされている場合は飛ばして構いません。
VSCodeのインストール方法: https://qiita.com/watamura/items/51c70fbb848e5f956fd6

3. NodeとNPMのインストール

Homebrewを使ってnodeをインストールします。同時にnpmもインストールされます。

$ brew install node

インストールされたことを確認するために以下のコマンドを実行してみてください。

$ npm --version                                                            
6.14.8

4. SourceKit-LSP Extension for Visual Studio Codeのインストールとビルド

コマンドラインからSourceKit-LSPをクローンします。

$ git clone https://github.com/apple/sourcekit-lsp.git

次にインストールされたフォルダへ移動します。

$ cd sourcekit-lsp/Editors/vscode/

拡張パックをビルドします。

$ npm run createDevPackage

ビルドしたものをインストールします。

$ code --install-extension out/sourcekit-lsp-vscode-dev.vsix

5. SourceKit-LSPの設定

VSCodeでSwiftのファイルを実行しようとした際に以下のエラーが出た場合には、VSCodeの設定を変更します。

Couldn't start client SourceKit Language Server

Cmd+Shift+Pでコマンドパレットを開いたあとに
基本設定: 設定(JSON)を開く (Preferences: Open Settings (JSON)"
を選択します。
そしたら、すでにあるJSONに以下の文章を追加しましょう。(書き換えではなく追加です。)

"sourcekit-lsp.serverPath": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp"

6. 確認

hello_world.swiftというファイルを作成しましょう。
作成したファイルに以下のコードを記入してみます。

hello_world.swift
print("Hello, world!")

ターミナルで以下のように書くと実行することができます。

swift hello_world.swift

または、Control+Option+nでも実行することができます。
ちなみに実行結果は

Hello, world!

です。

7. 型推論

今のままでは型推論がされない(と思う)ので、以下のように実行してください。ファイルをまず開きます。左下に

(HDの名前) > ユーザ > (あなたのユーザ名) > ...

と表示されていると思います。その中から、(あなたのユーザ名)をクリックします。以下順番にファイルを開いていきます。
sourcekit-lspを開きます。
Editorsを開きます。
vscodeを開きます。
outを開きます。
sourcekit-lsp-vscode-dev.vsixというファイルが有ることを確認してください。
確認できたらこのページを開きっぱなしにしておきます。
VSCodeの拡張機能タブから右上の・・・を選択し、VSIXからのインストールをクリックします。
VSIXからのインストール.png
すると、インストールするファイルを選ぶ画面が表示されるので先程開いたsourcekit-lsp-vscode-dev.vsixをドラッグ&ドロップします。
最後に、開いたファイルをインストールしましょう。
これで型推論ができるようになりました!

8.追記(C言語やC++のコンパイルにsourcekit-lspを用いたくない人へ)

lsp-sourcekitのページには以下のように、Swiftだけでなく、C、C++、Objective-Cにも対応していると書かれています。
lsp-sourcekit is a client for SourceKit-lsp, a Swift/C/C++/Objective-C language server created by Apple.
そのため、今までC言語やC++で使っていたコンパイラでは動くのにlsp-sourcekitでは動かないということが発生する可能性があります。そんなときは、Swiftを使うファイルをC言語、C++を使うファイルとは分けた上で、Swiftを使うワークスペースだけでlsp-sourcekitの拡張機能を有効にしましょう。
① SourceKit-LSPを無効にする

② 再読み込みを行う

③ 拡張機能→SourceKit-LSP→有効にする→有効にする(ワークスペース)

9. 参考文献

https://nshipster.com/vscode/
https://medium.com/swlh/ios-development-on-vscode-27be37293fe1
https://scior.hatenablog.com/entry/2019/10/27/215827

10. 終わりに

私は、まだまだ初心者なのですが参考文献に上げた3つのページを参考にVSCodeに導入してみたらうまく行ったので、共有させていただきました。
間違っているところがあったらご指摘ください。また、詳しい人がいたらぜひ記事を書いてください。私は、日本語でまとまっている記事を見つけ出すことができず、苦労しました...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【入門】iOS アプリ開発 #9【ゲームの状態遷移とシーケンス動作】

はじめに

今回はゲームとしてプレイできるように、ゲームの開始からゲームオーバーなどの状態遷移や各シーケンスの動作を作成する。以下が完成イメージ。ソースコードは GitHub に公開しているので参照してほしい。

※YouTube動画
IMAGE ALT TEXT HERE

状態遷移に関する仕様書

Image100.png

スタートモードとして、ゲームを開始する時のシーケンスが詳細に定義されている。

プレイモードはゲームプレイ中の状態で、プレイヤーがミスするとそのままのエサの状態からスタートし、残りのパックマンがいなくなるとゲーム・オーバーとなる。

またプレイフィールドのエサを全て食べるとラウンド・クリアとなる。

状態遷移とシーケンス動作の設計

ゲームプレイ中での必要なシーケンスを考慮して、下記の状態遷移図を作成した。

Image41.png

スタートモードは Start, Ready, Go の3つの状態に分けてシーケンスを作成する。

プレイモードは、主に Updating, ReturnToUpdating の2つの状態からなり、
プレイヤーがミスした場合は、PlayerMiss→PlayerDisappeared→PlayerRestartの状態/シーケンスを実行し、残りのパックマンがいれば Ready状態へ、なくなれば GameOver状態となる。

またプレイフィールドのエサを全て食べると、RoundClear→PrepareFlashMaze→FlashMazeの状態/シーケンスを実行しラウンドクリアとなり、Ready状態へ移って次のラウンドが開始される。

状態遷移のソースコード

CgSceneMaze の handleSequenceメソッドに、状態遷移とシーケンス処理を実装していく。

/// Maze scene class for play mode
/// This class has some methods to draw a maze and starting messages.
class CgSceneMaze: CgSceneFrame, ActorDeligate {

    var player: CgPlayer!
    var blinky: CgGhostBlinky!
    var pinky: CgGhostPinky!
    var inky: CgGhostInky!
    var clyde: CgGhostClyde!
    var ptsManager: CgScorePtsManager!
    var specialTarget: CgSpecialTarget!
    var ghosts = CgGhostManager()
    var counter_judgeGhostsWavyChase: Int = 0

    convenience init(object: CgSceneFrame) {
        self.init(binding: object, context: object.context, sprite: object.sprite, background: object.background, sound: object.sound)
        player = CgPlayer(binding: self, deligateActor: self)
        blinky = CgGhostBlinky(binding: self, deligateActor: self)
        pinky  = CgGhostPinky(binding: self, deligateActor: self)
        inky   = CgGhostInky(binding: self, deligateActor: self)
        clyde  = CgGhostClyde(binding: self, deligateActor: self)
        ptsManager = CgScorePtsManager(binding: self, deligateActor: self)
        specialTarget = CgSpecialTarget(binding: self, deligateActor: self)

        ghosts.append(blinky)
        ghosts.append(pinky)
        ghosts.append(inky)
        ghosts.append(clyde)
    }

    /// States of game model
    enum EnGameModelState: Int {
        case Init = 0
        case Start, Ready, Go, Updating, ReturnToUpdating, RoundClear, PrepareFlashMaze, FlashMaze,
             PlayerMiss, PlayerDisappeared, PlayerRestart, GameOver
    }

    /// Handle sequence
    /// To override in a derived class.
    /// - Parameter sequence: Sequence number
    /// - Returns: If true, continue the sequence, if not, end the sequence.
    override func handleSequence(sequence: Int) -> Bool {
        guard let state: EnGameModelState = EnGameModelState(rawValue: sequence) else { return false }

        switch state {
            case .Init: sequenceInit()
            case .Start: sequenceStart()
            case .Ready: sequenceReady()
            case .Go: sequenceGo()
            case .Updating: sequenceUpdating()
            case .ReturnToUpdating: sequenceReturnToUpdating()
            case .RoundClear: sequenceRoundClear()
            case .PrepareFlashMaze: sequencePrepareFlashMaze()
            case .FlashMaze: sequenceFlashMaze()
            case .PlayerMiss: sequencePlayerMiss()
            case .PlayerDisappeared: seauencePlayerDisappeared()
            case .PlayerRestart: sequencePlayerRestart()

            default:
                // Stop and exit running sequence.
                return false
        }

        // Continue running sequence.
        return true
    }

    // ============================================================
    //  Execute sequence in each state.
    // ============================================================
    func sequenceInit() {
        drawBackground()
        goToNextSequence()
    }

    func sequenceStart() {
        context.resetGame()
        context.resetRound()
        context.numberOfFeeds = drawMazeWithSettingValuesAndAttributes()
        printBlinking1Up()
        printPlayers(appearance: false)
        printStateMessage(.PlayerOneReady)
        sound.enableOutput(true)
        sound.playSE(.Beginning)
        goToNextSequence(.Ready, after: 2240)
    }

    func sequenceReady() {
        printStateMessage(.ClearPlayerOne)
        printPlayers(appearance: true)
        player.reset()
        ghosts.reset()
        specialTarget.reset()
        ptsManager.reset()
        goToNextSequence(.Go, after: 1880)
    }

    func sequenceGo() {
        printStateMessage(.ClearReady)
        drawPowerFeed(state: .Blinking)
        player.start()
        ghosts.start()

        // Reset counter for wavy attack of ghosts
        counter_judgeGhostsWavyChase = 0
        goToNextSequence()
    }

    // 〜 以下、省略 〜
}

Start, Ready, Go それぞれの状態に対応するシーケンスを sequenceStart(), sequenceReady(), sequenceGo()メソッドで実装する。

仕様書とメソッドを対応させると以下の通り。

  • ”1UP”表示が点滅 → printBlinking1Up()
  • 巣の上に”PLAYER ONE”表示、巣の下に”READY”表示 → printStateMessage(.PlayerOneReady)
  • 設定パックマンの数だけ”●”がプレイフィールド外の左下に表示される →  printPlayers(appearance: false)
  • スタートミュージック → sound.playSE(.Beginning)
  • ”PLAYER ONE”表示が赤モンスターに代わり → printStateMessage(.ClearPlayerOne), ghosts.reset()
  • 設定パックマン(プレイフィールド外の左下)が1つ減る → printPlayers(appearance: true)
  • パックマンがスタート位置に表示される → player.reset()
  • ”READY!”表示が消えて、プレイモードに移る → printStateMessage(.ClearReady)

プレイモードの仕様書

プレイモードにおいては、モンスターの出現タイミングや波状攻撃、アカモンスターのスパートといった細かい仕様が定義されている。これらによってモンスターは単調な追いかけ動作にならず、またプレイヤーのゲーム進行に合わせて難易度が調整される仕組みになっている。

Image11.png

Image12.png

Image13.png

ラウンドに対して更にモンスター出現タイミングのレベル、波状攻撃のスピードレベル、スパートする残りエサ数が定義されているが、今回は固定のレベル「A」、スパートは「①イ」のみ実装していく。

プレイモードのソースコード

プレイモードの仕様は sequenceUpdating() に動作を実装していく。

    func sequenceUpdating() {
        // Player checks to collide ghost.
        let collisionResult = ghosts.detectCollision(playerPosition: player.position)

        switch collisionResult {
            case .None:
                // When it's no eat time, ghost goes out one by one.
                if player.timer_playerNotToEat.isEventFired()  {
                    player.timer_playerNotToEat.restart()
                    ghosts.setStateToGoOut(numberOfGhosts: 4, forcedOneGhost: true)
                }

                // Appearance Timing of Ghosts
                ghosts.setStateToGoOut(numberOfGhosts: context.getNumberOfGhostsForAppearace(), forcedOneGhost: false)

                // Wavy Attack of ghosts
                // - Do not count timer when Pac-Man has power.
                if !player.timer_playerWithPower.isCounting() {
                    counter_judgeGhostsWavyChase += SYSTEM_FRAME_TIME
                }

                // Select either Scatter or Chase mode.
                let chaseMode = context.judgeGhostsWavyChase(time: counter_judgeGhostsWavyChase)

                if chaseMode {
                    pinky.chase(playerPosition: player.position, playerDirection: player.direction.get())
                    inky.chase(playerPosition: player.position, blinkyPosition: blinky.position)
                    clyde.chase(playerPosition: player.position)

                } else {
                    pinky.setStateToScatter()
                    inky.setStateToScatter()
                    clyde.setStateToScatter()
                }

                // If Blinky becomes spurt or not.
                let blinkySpurt: Bool = context.judgeBlinkySpurt() && !ghosts.isGhostInNest()
                blinky.state.setSpurt(blinkySpurt)

                // Blinky doesn't become scatter mode when he spurts.
                if blinkySpurt || chaseMode {
                    blinky.chase(playerPosition: player.position)
                } else {
                    blinky.setStateToScatter()
                }

                // For debug
                ghosts.drawTargetPosition(show: true)

            case .PlayerEatsGhost:
                let pts = context.ghostPts
                ptsManager.start(kind: pts, position: ghosts.collisionPosition, interval: 1000) //ms
                context.ghostPts = pts.get2times()
                addScore(pts: pts.getScore())
                player.stop()
                player.clear()
                specialTarget.enabled = false
                ghosts.stopWithoutEscaping()
                sound.playSE(.EatGhost)
                sound.stopBGM()  // REMARKS: To change playBGM(.BgmEscaping) immediately.
                goToNextSequence(.ReturnToUpdating, after: 1000)

            case .PlayerMiss:
                goToNextSequence(.PlayerMiss)
        }

        playBGM()
    }

はじめにプレイヤーとモンスターの衝突判定を ghosts.detectCollision(playerPosition: player.position) で行う。

衝突がなければ(.None)、ノーイートタイムによって、ゴーストが巣から出ている処理を行う。4匹の中から必ず1匹は外に出して、ノーイートタイムをリスタートさせる。

次は、食べたエサの数でゴーストを巣から出していく。すでに指定の数が出ていたら何もしない。ミスして新たにスタートするときは、ミスバイパスシーケンスを通す。

CgContextクラスの getNumberOfGhostsForAppearace()メソッドは以下の通り。

    func getNumberOfGhostsForAppearace() -> Int {
        let numberOfGhosts: Int
        // Miss Bypass Sequence
        if playerMiss {
            // Level A
            if numberOfFeedsEatedByMiss < 7 {
                numberOfGhosts = 1
            } else if numberOfFeedsEatedByMiss < 17 {
                numberOfGhosts = 2
            } else if numberOfFeedsEatedByMiss < 32 {
                numberOfGhosts = 3
            } else {
                playerMiss = false
                numberOfGhosts = getNumberOfGhostsForAppearace()
            }
        } else {
            // Level A
            if numberOfFeedsEated < 30 {
                numberOfGhosts = 2
            } else if numberOfFeedsEated < 90 {
                numberOfGhosts = 3
            } else {
                numberOfGhosts = 4
            }
        }
        return numberOfGhosts
    }

sequenceUpdating() メソッドの処理フローに戻る。

次の波状攻撃は、パターンスタートからカウントしている時間 counter_judgeGhostsWavyChase によって Scatter と Chase を切り替える。
ただしプレイヤーが逆転している時は、カウントをストップする。

CgContextクラスの judgeGhostsWavyChase()メソッドは以下の通り。

    func judgeGhostsWavyChase(time: Int) -> Bool {
        let mode: Bool
        // Level A
        if time < 7000 || (time >= 27000 && time < 34000) ||
           (time >= 54000 && time < 59000) || (time >= 79000 && time < 84000) {
            mode = false
        } else {
            mode = true
        }
        return mode
    }

sequenceUpdating() メソッドの最後の処理フローでは、アカモンスター(Blinky)のスパート処理を行う。残りエサ数に達した時かつ全モンスターが巣から出ている時(!ghosts.isGhostInNest())にスパートする。

残りは衝突判定で、パックマンがモンスターを噛み付いた時(.PlayerEatsGhost)
、逆に捕まった時(.PlayerMiss)の処理をそれぞれ実装する。

まとめ

ようやくゲームとしてプレイができるようになってきた。現在のソースコードは約5000行。
次はラウンドによって変化する難易度レベルやスピードレベルの詳細を作り込んでいく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS14 CoreGraphics 描画互換問題

開発アプリのiOS14の動作検証をしている時に、CoreGraphics の描画に互換性の問題がある事を発見したので、ここに記録しておくことにします。第三者が確認できるように、サンプルプロジェクトへのリンクも貼っておく事にします。

以下のように、iOS13とiOS14 では描画結果に違いがある場合があります。どちらも不透明度付きで一筆書きで書かれていますが、結果に違いがある事が窺えます。

iOS 13

iOS14

検証用プロジェクト

http://electricwoods.com/download/T2020-09-CG.zip

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CATechChallengeで動画を簡単にGIFにできる追加機能をつくってきた

はじめに

サイバーエージェントの3daysインターンに参加してきました。
せっかくなのでこちらで技術的な振り返りをしていきたいと思います!技術的なこと以外の振り返りはnoteにまとめるのでそちらをご覧ください。
noteの記事はあとで書いて頑張って足します。
embwfSW2Q8X4Y0c1600629713_1600629757.png

CATechChallengeとは

今回参加したインターンはCATechChallenge(3daysiOS/Android向け開発型インターン)というもので、AbemaTVに架空の追加機能を実装するハッカソン形式のインターンです。

作成した機能

視聴中の動画を長押しすることでその部分のGIFが作成されツイートされる機能を実装しました(機能名募集中ですw)

使用した技術

言語: Swift
アーキテクチャ: MVC
ライブラリ: Regift, Swifter
を使用しました。アーキテクチャに関しては元々MVCのコードが用意されていたのでそのまま実装しました。
ライブラリはGIF生成のためにRegift, ツイートのためにSwifterを使用しました。

苦戦した箇所

ライブラリがm3u8に対応してなかった

元々用意されていた動画の形式がm3u8だったのですがRegiftがmp4にしか対応していなかったため、危うくアイデア自体が終わるところでしたw
メンターさんがmp4の動画も用意してくださっていたので入れ替えることでこちらは無事解決しました。

長押しした時の秒数の取得

これは完全に僕の勉強不足なのですが、AVFoundationを使ったことがなかったので長押し中の秒数の取得に苦戦しました。
AVFoundationではCMTimeという独自の時間表現をするため、currentTimeがCMTimeで表示されていました。
GIFを作成する際には通常の秒数を取得する必要があったので、親切な方記事をガン見しながらそれっぽく変換しました()

GIFの容量がデカすぎてツイートできない

Twitter的に15MB以上のメディアはアップロードできないらしくGIFがツイートできなくて詰みました。
意図せぬ変更ではあるんですがGIFの画質を落とすことによってこの制限をクリアできたので画質を一番低いものにしました。

Swifterからツイートするとユーザーが文章を足せない

これは使うまで気がつかなかったのですが、アプリからSwifterを使ってツイートすると一瞬safariに飛んでアプリにコールバックするため、ツイート時にユーザーが文章を足すことができません。
個人的に不便だなーと思ったので間に自作のAlertを挟むことでユーザーが文章を足せるようにしました。

工夫したポイント

今回一番のアピールポイントは動画視聴という体験をほぼ中断せずに機能が完結する点です。
ツイートする際にアプリとTwitterを連携させる部分以外は動画を視聴しながら使用できる機能にしました。

実際にツイートすると

ちなみに実際にツイートしたらこんな感じになります!
catweetimg.png

結果

そして気になる結果は...
BestEngineer賞をいただくことができました!!!めちゃ嬉しかったです:clap:

GIFのプレビュー機能や番組へのリンクなどまだまだやりたい機能もあったのでホントはあと1ヶ月ぐらい開発したかったですが、とても楽しめたので参加してよかったです!ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CATechChallengeで動画を簡単にGIFにできる追加機能をつくった

はじめに

サイバーエージェントの3daysインターンに参加してきました。
せっかくなのでこちらで技術的な振り返りをしていきたいと思います!技術的なこと以外の振り返りはnoteにまとめるのでそちらをご覧ください。
https://note.com/lsk4f5/n/nd28c28cfc088
embwfSW2Q8X4Y0c1600629713_1600629757.png

CATechChallengeとは

今回参加したインターンはCATechChallenge(3daysiOS/Android向け開発型インターン)というもので、AbemaTVに架空の追加機能を実装するハッカソン形式のインターンです。

作成した機能

視聴中の動画を長押しすることでその部分のGIFが作成されツイートされる機能を実装しました(機能名募集中ですw)

使用した技術

言語: Swift
アーキテクチャ: MVC
ライブラリ: Regift, Swifter
を使用しました。アーキテクチャに関しては元々MVCのコードが用意されていたのでそのまま実装しました。
ライブラリはGIF生成のためにRegift, ツイートのためにSwifterを使用しました。

苦戦した箇所

ライブラリがm3u8に対応してなかった

元々用意されていた動画の形式がm3u8だったのですがRegiftがmp4にしか対応していなかったため、危うくアイデア自体が終わるところでしたw
メンターさんがmp4の動画も用意してくださっていたので入れ替えることでこちらは無事解決しました。

長押しした時の秒数の取得

これは完全に僕の勉強不足なのですが、AVFoundationを使ったことがなかったので長押し中の秒数の取得に苦戦しました。
AVFoundationではCMTimeという独自の時間表現をするため、currentTimeがCMTimeで表示されていました。
GIFを作成する際には通常の秒数を取得する必要があったので、親切な方記事をガン見しながらそれっぽく変換しました()

GIFの容量がデカすぎてツイートできない

Twitter的に15MB以上のメディアはアップロードできないらしくGIFがツイートできなくて詰みました。
意図せぬ変更ではあるんですがGIFの画質を落とすことによってこの制限をクリアできたので画質を一番低いものにしました。

Swifterからツイートするとユーザーが文章を足せない

これは使うまで気がつかなかったのですが、アプリからSwifterを使ってツイートすると一瞬safariに飛んでアプリにコールバックするため、ツイート時にユーザーが文章を足すことができません。
個人的に不便だなーと思ったので間に自作のAlertを挟むことでユーザーが文章を足せるようにしました。

工夫したポイント

今回一番のアピールポイントは動画視聴という体験をほぼ中断せずに機能が完結する点です。
ツイートする際にアプリとTwitterを連携させる部分以外は動画を視聴しながら使用できる機能にしました。

実際にツイートすると

ちなみに実際にツイートしたらこんな感じになります!
catweetimg.png

結果

そして気になる結果は...
BestEngineer賞をいただくことができました!!!めちゃ嬉しかったです:clap:

GIFのプレビュー機能や番組へのリンクなどまだまだやりたい機能もあったのでホントはあと1ヶ月ぐらい開発したかったですが、とても楽しめたので参加してよかったです!ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む