- 投稿日:2020-03-21T20:24:03+09:00
2020/3/17にリリースされたGitHubモバイルアプリ
GitHubが3/17にAndroidとiOS向けの公式モバイルアプリをリリース
最近までテスト配信され、ようやくリリースされました。
このアプリはコードの編集はできませんが、Issueの管理やプルリクエストのマージなどができるアプリです。
エンジニアのほとんどがお世話になっているGitHub
が一体どんなライブラリを使用して開発したのか気になりました。iOSの設定アプリにOSSのライセンスが表記されていたので調査してみました。
- 初期画面
- ログイン後
- 謝辞: Acknowledgements
一覧
- Alamofire
- AlamofireNetworkActivityIndicator
- Apollo
- CocoaLumberjack
- ContextMenu
- DropdownTitleView
- Firebase
- Crashlytics
- Fabric
- FirebaseAnalyticsInterop
- FirebaseCore
- FirebaseCoreDiagnostics
- FirebaseCoreDiagnosticsInterop
- FirebaseInstanceID
- FirebaseMessaging
- GoogleDataTransport
- GoogleDataTransportCCTSupport
- GoogleUtilites
- nanopb
- FLEX
- Fuzi
- Protobuf
- R.swift
- R.swift.Library
- SDWebImage
- SDWebImageSVGCoder
- SnapKit
- SnapshotTesting
- SQLite.swift
- SVGKit
- TUSafariActivity
- Valet
ライブラリについて
Alamofire
ど定番のHTTP通信ライブラリ
AlamofireNetworkActivityIndicator
Alamofireが提供しているインジケータライブラリ
NetworkActivityIndicatorManager.shared.isEnabled = trueApollo
GraphQL APIと通信できるライブラリ
CocoaLumberjack
ログフレームワーク
NSLog
よりも高速らしい...ContextMenu
ポップアップのようなコンテキストメニューの表示できるライブラリ
Starはちょっと少なめDropdownTitleView
UINavigationItemを加工できるライブラリ
Starはちょっと少なめFirebase
Googleのなんでも提供してくれる優秀ツール
FLEX
iOS開発用のアプリ内デバッグツール
Fuzi
XML/HTMLのパーサライブラリ
Webページからの情報を取得、分析するために使っているのでしょうProtobuf
GoogleのProtocol Buffer( "protobuf")オブジェクトシリアライズツールをappleが提供
R.swift
コード補完でリソースファイルにアクセスすることができる超便利ライブラリ
Beforelet icon = UIImage(named: "settings-icon") let font = UIFont(name: "San Francisco", size: 42) let color = UIColor(named: "indicator highlight") let viewController = CustomViewController(nibName: "CustomView", bundle: nil) let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")Afterlet icon = R.image.settingsIcon() let font = R.font.sanFrancisco(size: 42) let color = R.color.indicatorHighlight() let viewController = CustomViewController(nib: R.nib.customView) let string = R.string.localizable.welcomeWithName("Arthur Dent")SDWebImage
かなり昔から使われている定番画像キャッシュライブラリ
SDWebImageSVGCoder
SDWebImageのSVGファイル対応プラグイン
SnapKit
AutoLayoutをソースコードで記述できる一番人気のライブラリ
SnapshotTesting
スナップショットテストライブラリ
View以外もテストできるみたい
これをつかってデグレを検知をしているのでしょうSVGKit
SVGファイルを使えるようにするライブラリ
iOS13からガッツリSVG対応されるようになったので今後は使用することないかも...SQLite.swift
データベースSQLiteのライブラリ
TUSafariActivity
「Safariで開く」を簡単に実行できるライブラリ
let URL = NSURL(string:" http://google.com ")! let activity = TUSafariActivity() let activityViewController = UIActivityViewController(activityItems:[URL]、applicationActivities:[activity])Valet
キーチェーンを簡単に操作できるライブラリ
感想
アプリを使用してみて、かなりシンプルでappleが提供しているようなアプリデザインと思った。
UIがシンプルということでUI関係のライブラリが多くない。
しかし、もともとのサービスがWebサービスということからネットワーク関連のライブラリが7割ほどでした。意外だったのが、かなりマイナーなライブラリを使っているところですね。正直、
TUSafariActivity
は普通に実装できる軽量ライブラリですし、Starもかなり少ない。それをあえて導入していたのが驚きでした。対象OSが12.2~ということでUIKitで開発されたのは間違い無いだろう。
でもUIをみるとデフォルトで提供されているUIパーツだらけなので、今後SwiftUIに移行するを検討しているのでは無いかと予想する。とにかくライブラリをよくみる私にとっては最高のモバイルアプリである。
今後の予定
ライブラリ説明の充実
- 投稿日:2020-03-21T18:21:50+09:00
iOSにおけるバックグラウンド処理の全体感
はじめに
WWDC2019で行われた下記のセッションをベースに、実際に実装してみた結果をまとめます。
https://developer.apple.com/videos/play/wwdc2019/707/全体感
以下の図に記載しました。
この中の処理種別
に関して、ナンバリングしてあるものがiOSで利用できるバックグラウンド処理です。
※ Audio, LocationUpdate, VoIP, Bluetoothなどに関しては今回は省いています
各処理の比較
処理種別 実行可能時間 実行可能状態 詳細 1. Background Task Completion 環境依存(※1) フォアグラウンドで実行した直後のみ https://qiita.com/chocoyama/items/8162bf67e452e41b574b 2. Background Notification (Silent Push) 30秒間 ユーザーによって強制終了されていない状態 https://qiita.com/chocoyama/items/56cd3ac2daaf69dffa0f 3-1. Background URL Session 環境依存(※1) 〃 https://qiita.com/chocoyama/items/1440b2e3d69856ba8957 3-2. Discretionary Background URL Session 〃 〃 https://qiita.com/chocoyama/items/1440b2e3d69856ba8957 4-1. BackgroundTasks (App Refresh Tasks) 30秒間 〃 https://qiita.com/chocoyama/items/d69322932f400a5d012b 4-2. BackgroundTasks (Processing Tasks) 数分間
(※2)〃 https://qiita.com/chocoyama/items/d69322932f400a5d012b ※1.
環境依存
と記述している箇所はシステムの状態やOSバージョンによって異なるようです。
たとえば、iOS12以前だと180secほど実行できていたバックグラウンド処理が、iOS13では30secまで縮められていました。※2. 以下のような表記がされているだけで、最長の実行時間については明記がありませんでした。
A request to launch your app in the background to execute a processing task that can take minutes to complete.
https://developer.apple.com/documentation/backgroundtasks/bgprocessingtaskrequest個人的な感想
全体として
バックグラウンド処理は多くの場合以下のような特徴があるため、フォアグラウンド処理と同等に扱うことはできません。
- 実行タイミングがシステムに依存している
- 処理時間が制限されている
- 実行時点での環境が様々(バッテリー状況、通信状況など)
- etc...
「必ず実行されるもの」という前提で実装を組んでしまうと、想定した動作にならないことが容易に起きます。
またVoIPプッシュなどの一部の処理を除いて、ユーザーによって強制的にアプリが終了されている場合は、バックグラウンド処理は起動されません。
そのため、これらの処理は基本的に「実行されるとより便利になるもの」といった位置付けで実装するのが現実的だと思います。1. Background Task Completion
詳細 → https://qiita.com/chocoyama/items/8162bf67e452e41b574b
「処理の途中でアプリをバックグラウンドにされてしまった時の対処」が必要なケースで使えるものです。
WWDCの動画では、メッセージ送信処理を例にしていました。※ この機能を使って無限にバックグラウンド処理を実行させようという試みを、調査の中で見つけることが出来ました。
バックグラウンド処理の完了後に、再度バックグラウンド処理を開始させる方法です。
実際に掲載されているサンプルコードを試してみましたが、バックグラウンドでの実行可能時間は特に延長されず、期待した動作にはなりませんでした。
そもそも、無限にバックグラウンド処理を行うのはiOSが想定している本来の使い方と異なるため、避けた方が良い気がします。
ユーザーとしても、知らない間にアプリが無限にバックグラウンド処理を行っているのは迷惑な挙動になりうるでしょう。2. Background Notification (Silent Push)
詳細 → https://qiita.com/chocoyama/items/56cd3ac2daaf69dffa0f
サーバーサイドから、端末に対して特定の処理を動作させることができる貴重な手段の一つです。
ただし、配信の保証ができないことに加えて、即時で実行されないことも多いので、この仕組みに依存した実装を行うのは危険です。
あくまでオプショナルな機能として、より便利にアプリを使えるためのものとして考えておいた方が良さそうです。例えば、サーバーサイドで何らかのデータ更新が走った際に、
Background Notification
を利用してアプリ側に同期を取ろうとする場合を考えます。
通知がうまく配信された場合、「ユーザーがアプリを起動したタイミングではすでに最新のデータが取れている」といった、より良い体験を与えることができます。
しかし、そうでない場合は起動時に最新のデータを取得する必要があるので、結局Background Notification
だけでは十分な実装にはならないでしょう。
必須で行わなけらばならない処理は実装しておきつつ、さらに便利にするために追加で実装する、といった使い方になるような気がします。また、フォアグランド時に
Background Notification
を受けた場合に限っては、必ずそれをハンドリングできるようです。
この特徴を利用して、「フォアグラウンド時に、画面上には通知を出さずに裏でリアルタイム同期を行う」といった用途にも使えそうです。3-1. Background URL Session
詳細 → https://qiita.com/chocoyama/items/1440b2e3d69856ba8957
これを使うと、バックグラウンドになったタイミングでもフォアグラウンドで開始した通信を継続させることができます。
Background Task Completion
でも同じようなことは実現できるため、自分は用途があまり思い浮かびませんでした。3-2. Discretionary Background URL Session
詳細 → https://qiita.com/chocoyama/items/1440b2e3d69856ba8957
アプリがフォアグラウンドの時に、何らかのイベントに応じて通信の予約をしておける機能です。
WWDCの動画では、「ユーザーがサインインした直後、古いコンテンツをパフォーマンスに影響しないタイミングで取得する」といったユースケースを例にしていました。
端末にデータを溜めないようなアプリではあまり使うことがなさそうですが、必要に応じて使うと良さそうです。
4-1. BackgroundTasks (App Refresh Tasks)
詳細 → https://qiita.com/chocoyama/items/d69322932f400a5d012b
ユーザーがよく使うアプリについて、起動を先回りしてデータを取得しておくことができます。
これを利用して細かいアップデートを事前に行っておくことができますが、実行時間は30秒しかないため、重たい処理を行いたい場合はProcessing Tasks
を使うことになるでしょう。基本的にはDBやUserDefaultsなどにデータを書き込むようなユースケースになりそうです。
オンメモリのデータを更新する用途でも使えなくはなさそうですが、タスク実行からアプリ起動までに時間が空いてしまうと、システムによって終了させられる可能性もあるため、あまり有効ではないかなという気はしました。必ずしも実行される処理ではないので、ここでの処理が行われなくてもフォアグラウンド時に埋め合わせを行えるような実装にしておくことが求められると思います。
4-2. BackgroundTasks (Processing Tasks)
詳細 → https://qiita.com/chocoyama/items/d69322932f400a5d012b
定期的に行いたい処理で、
App Refresh Tasks
では対応できない重たい処理を行いたい場合はこれを使うことになります。
CPUMonitorをOFFにできるなどの強力な機能もついているので、この機能で実現できない処理はあまりないように思います。ただ一般的なバッチ処理とは異なり、「正確な日時を指定することができない」「端末が指定した実行条件に置かれないと実行されない」といった特徴を持っているので、確実に毎日実行させられるとは限らないことは考慮した方が良さそうです。
- 投稿日:2020-03-21T18:12:28+09:00
BackgroundTasks(AppRefreshTasks & ProcessingTasks)
この記事は
iOS13で利用可能なBackgroundTasksフレームワークを利用した、アプリのバックグラウンド処理についてのまとめと挙動の検証記事です。
機能概要
- 特徴
- 以下のようなメンテナンスタイプのタスクに対して最適な仕組み
- サーバーとの同期
- データベースのクリーンアップ
- クラウドへのバックアップ
- アプリがフォアグランドでないタイミングで任意の処理を実行させることができる
- これによりユーザー操作の邪魔をせずに必要なタスクを後回しにして実行させられる
- 以下の2種類のモードがある
- Background App Refresh Tasks (Background Fetch)
- Background Processing Tasks
Background App Refresh Tasks (Background Fetch)
- 特徴
- iOS7で追加された
BackgroundFetch
がiOS13でアップデートされたもの- ユーザーの普段の利用傾向に合わせて、定期的にアプリの状態を最新化させることができる仕組み
- 実行タイミング
- ユーザーの過去の行動からアプリケーションの起動頻度や時間帯が決定される
- ユーザーが普段よくアプリを使うタイミングの少し前に起動される
- 頻繁に使わないアプリケーションの場合には起動も少なくなる
- 最早開始日時の指定は可能だが、1週間以内に設定することが推奨されている
- 起動可能状態
- ユーザーにより強制的にkillされていない状態
- システムによってkillされた場合は、起動し直してくれる
- 実行可能時間
- 30秒間
- 備考
- iOS12以前で利用していた以下のAPIはdeprecatedになった
UIApplication.setMinimumBackgroundFetchInterval(_:)
UIApplicationDelegate.application(_:performFetchWithCompletionHandler:)
- 所感
- 「ユーザーがアプリを起動した際には、すでに最新のデータが取れている」といった体験を作ることができる
- ただし、あくまでシステムがよしなに実行してくれるだけで必ずしも実行されるとは限らないので、必須処理をこれで担保するのは危険
Background Processing Tasks
- 特徴
- iOS13から利用可能な新機能
- 以下のような細かい実行条件を設定できる (
App Refresh Tasks
はできない)
- Wi-Fi接続あり
- 充電中
- CPU Monitor (バックグラウンドでCPUを使いすぎているAppを自動でkillする機能) をOFFにすることができる
- (「充電中のみ実行可能」にすることで実現可能)
- 実行タイミング
- 指定した実行可能条件に応じて、システムが判断して実行する
- フォアグラウンドでリクエストされた場合や、アプリが最近使用されている場合にタスクが実行される
- 起動可能状態
- デバイスがアイドル状態のとき
- ユーザーがデバイスの使用を開始してしまうと、システムは実行中のバックグラウンド処理タスクを終了させる
- (AppRefreshTasksは影響を受けない)
- ユーザーにより強制的にkillされていない状態
- システムによってkillされた場合は、起動し直してくれる
- 実行可能時間
- 数分間に及ぶ実行も可能
- 所感
- AppRefreshTasksに担わせることができない重たい処理を行わせるのに良い
- しかしAppRefreshTasksと同様に確実に実行される保証はないので、実行されることを前提とした実装は避けた方がよさそう
準備
- Capabilityの設定
Background App Refresh Tasksの場合は
Background fetch
をチェックし。
Background Processing Tasksの場合はBackground processing
をチェックする、2. Info.plistの設定
Permitted background task scheduler identifiers
にタスク毎のIDを設定する。
リバースDNSで他のフレームワークとの衝突を避け、ユニークになるように設定する。
(タスクIDは後述するBGTaskScheduler
にタスクを登録・スケジューリングする際に利用する。)実装
サンプルコード
※ 詳細な解説はサンプルコード内にコメント文でも記述しているが、重要な部分のみ抜粋する
1. タスクの登録
タスクの登録は、BackgroundTasksフレームワークが提供する
BGTaskScheduler
に対して行う。
- BGTaskScheduler
- タスクのタイプに応じたTaskRequestオブジェクトを作成することで、アプリ動作中にタスクを登録できる
Background App Refresh Tasks
=>BGAppRefreshTaskRequest
Background Processing Tasks
=>BGProcessingTaskRequest
- 常に「バッテリー残量」「アプリケーション使用時間」「通信状態」のようなシステム状態を監視する
- 必要なシステム状態とポリシーが満たされればタスクが実行される
- タスク実行時は、バックグラウンドでアプリが起動され、対応するBGTaskオブジェクトが届けられる
- タスクの完了は
setTaskCompleted
を呼び出すことで行い、これにより起動されたアプリを停止する- タスク起動は複数同時に行えるが、実行可能時間の割り当てはタスクごとではなく起動ごとに割り当てられるので注意
taskIdentifierにはInfo.plistに設定したタスクのIDを渡す。
launchHandlerに受け渡すクロージャは、システムによってタスクが起動される際に呼び出される。
そのため、ここにはバックグラウンドで行いたい処理自体を記述する。
(ハンドラを呼び出すキューを明示的に指定したい場合は、第二引数に受け渡す)import BackgroundTasks BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil, launchHandler: { task in // タスクが実行された時の処理を記述する }2. 完了のハンドリング
目的とするタスクが完了したら、速やかに
setTaskCompleted
を呼び出す。
これを適切に呼び出さないと、次回の起動パフォーマンスに影響を及ぼしてしまう。
また、expirationHandlerが呼ばれた際もこれは呼び出す必要がある。
※ UISceneのアプリはUIApplication.requestSceneSessionRefresh
も実行する。task.setTaskCompleted(success: true)3. 失効のハンドリング
launchHandlerで受け渡されてくる
BGTask
オブジェクトにはexpirationHandler
プロパティが存在する。
バックグラウンド処理は、タイムアウトやシステム状態の悪化などにより、タスクを完了させられずに終了する可能性が比較的高い。
そのため、このハンドラを設定してタスクが失効したタイミングの処理を予め設定しておく。task.expirationHandler = { // キャンセル処理を実行 }4. タスクのスケジューリング
submit
メソッドでタスクをスケジューリングする。
この時、register
の時と同様にInfo.plistに設定したタスクIDを指定する。既に同一IDのタスクが未実行状態でキューに溜まっていた場合、以前のタスクリクエストは置き換えられる。
また、最大で「AppRefreshTask x 1」と「ProcessingTask x 10」までしかスケジュールできないので、最大数を超えてスケジューリングしようとするとエラーが発生する。BGAppRefreshTaskRequestの場合
func schedule() { let request = BGAppRefreshTaskRequest(identifier: taskIdentifier) // 実行開始時期を遅らせる // iOS12以前の、setMinimumBackgroundFetchIntervalと同じ動きをする // また、設定値としては1週間以内が推奨されており、遠すぎるとユーザーがその間にアプリを開いた際にタスクが起動しない場合がある request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) do { try BGTaskScheduler.shared.submit(request) } catch { print("Could not schedule app refresh: \(error)") } }BGProcessingTaskRequestの場合
let request = BGProcessingTaskRequest(identifier: taskIdentifier) // 通信が必要な場合はtrueにする(デフォルトはfalseで、この場合通信がない時間にも起動される) request.requiresNetworkConnectivity = true // 充電中に実行したい処理の場合はtrueにする // これがtrueの時にCPU Monitorが無効になる request.requiresExternalPower = true do { try BGTaskScheduler.shared.submit(request) } catch { print("Could not schedule database cleaning: \(error)") }注意点
※ BGTaskRequestは1度の起動にしか対応していない。
そのため、実行後に自動で次のタスクをスケジューリングしたい場合は、タスク実行完了前に再度スケジューリングしておく必要がある。BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil, launchHandler: { task in self.schedule() // タスクが実行された時の処理 // ... })デバッグ
タスクを起動させる
- 一度アプリを起動させてタスクをスケジューリングさせる
- タスクのスケジューリングを行ったあと、Xcodeの一時停止ボタンをクリックする(フォアグラウンドで行う必要があった)
- LLDBで
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"]
を打ち込む- 一時停止を解除する
- タスクが実行される
タスクを失効させる
- Xcodeの一時停止ボタンをクリックする
- LLDBで
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"TASK_IDENTIFIER"]
を打ち込む- 一時停止を解除する
- タスクが失効される
検証
準備
シミュレーターでBGTaskSchedulerのsubmitメソッドを呼び出すと、以下のエラーが発生し挙動を確認できなかった。
Could not schedule app refresh: Error Domain=BGTaskSchedulerErrorDomain Code=1 "(null)"ドキュメントに 、デバッグは実機のみ可能との記述があったので、検証は実機で行った。
The debug functions work only on devices.
結果 (AppRefreshTasks)
- LLDBのデバッグコマンドでの動作検証
- タスクの実行
_simulateLaunchForTaskWithIdentifier
コマンドを実行すると、launchHandler
が呼び出される挙動を確認できた。launchHandler
内で次回タスクのスケジューリングを行った場合は、続けてデバッグコマンドを打ち込んでも再度ハンドラが呼び出された
- 追加のスケジューリングを行わなかった場合は、ハンドラは呼び出されなかった
- タスクの失効
- 30秒を超える処理を
launchHandler
内で行っても処理が完了できた
- デバッグはアプリをフォアグラウンドにしておかないとできないため、これが原因の可能性がある
- バックグラウンド状態にしてデバッグコマンドを打つと、Xcodeがフリーズして以降のデバッグを行うことができなくなった
- 失効用のコマンドを実行した場合は、
expirationHandler
が呼び出されるのを確認できた
- この時、一度
_simulateLaunchForTaskWithIdentifier
を呼び出してタスクを起動状態にしておかないと、失効のハンドラは呼び出されなかった- 実機での動作検証
- アプリをバックグラウンド状態にしている際に、AppRefreshTasksが実行されることを確認できた
- アプリをkillした場合はAppRefreshTasksが実行される挙動は確認できなかった
検証 (ProcessingTasks)
- LLDBのデバッグコマンドでの動作検証
- AppRefreshTasksと同様の結果となった
- 実機での動作検証
- 指定した条件にしたあと、すぐに実行はされなかった
- スケジューリング後、2日目あたりからタスクが実行されるようになった
- 実行されはじめた後は、1日に2回以上動作することもあった
- (アプリの利用頻度に応じて実行回数は変わりそう)
参考
- https://developer.apple.com/documentation/backgroundtasks
- https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler
- https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler/3142252-submit
- https://developer.apple.com/documentation/backgroundtasks/refreshing_and_maintaining_your_app_using_background_tasks
- https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
- 投稿日:2020-03-21T17:33:28+09:00
【Swift, iOS】iOS13の新機能 LinkPresentation.frameworkの使い方を調べる
iOS13でLinkPresentationという新機能が追加されました。
これによってURLリンク先の情報を
より表現豊かに表示することができるようになるようです。今回はその新機能について
AppleのWWDC2019の動画と検証結果などから見ていきたいと思います。Embedding and Sharing Visually Rich Links
https://developer.apple.com/videos/play/wwdc2019/262※
あまり情報がなく
検証した結果から記載した部分もありますので
間違っている部分やもっと良い方法ご存知の方いらっしゃいましたらぜひ教えてください??♂️LinkPresentation.frameworkとは?
iOS13で新しく追加された
リンクのプレビューを画像や埋め込み動画、音楽再生と合わせて
リッチに一貫した方法で表示できるようにした
フレームワークです。iOS10とmacOS Sierraから
Appleのメッセージアプリなどでは先行して
このフレームワークに含まれている機能を利用していたようです。https://developer.apple.com/documentation/linkpresentation
主なクラス
非常にシンプルで主に登場するクラスは3です。
- LPMetadataProvider
- LPLinkMetadata
- LPLinkView
LPMetadataProvider
URLのメタ情報を取得します。
https://developer.apple.com/documentation/linkpresentation/lpmetadataprovider※ メタ情報とは?
HTMLタグに含まれるタイトルやアイコン、画像、動画などの情報を読み取ります。
特に
OpenGraphというプロトコルを使用した
<meta og:XXX>
の情報を優先して読み取ります。例えば下記のようなものです。
<html prefix="og: http://ogp.me/ns#"> <head> <title>The Rock (1996)</title> <meta property="og:title" content="The Rock" /> <meta property="og:type" content="video.movie" /> <meta property="og:url" content="http://www.imdb.com/title/tt0117500/" /> <meta property="og:image" content="http://ia.media-imdb.com/images/rock.jpg" /> ... </head> ... </html>詳細は下記をご参照ください。
OpenGraph
https://ogp.me/LPLinkMetadata
URLのメタ情報を保持するクラスです。
https://developer.apple.com/documentation/linkpresentation/lplinkmetadataLPLinkView
URLのメタ情報をリッチに表示するUIViewのサブクラスです。
https://developer.apple.com/documentation/linkpresentation/lplinkview使い方
すごいシンプルです。
LPMetadataProvider
のstartFetchingMetadata
でURLのリンク先からLPLinkMetadata
を取得するLPLinkMetadata
をLPLinkView
に設定するSwiftUIでの実装
UIViewRepresentableに適合したクラスの生成
LPLinkView
に対応する
UIViewRepresentable
に適合したクラスを生成します。import SwiftUI import LinkPresentation struct LinkPresentationView: UIViewRepresentable { typealias UIViewType = LPLinkView func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType { } func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<LinkPresentationView>) { } }メタ情報の取得
LPMetadataProvider
からLPLinkMetadata
を取得します。
取得するURLが必要なので初期化時に引数で受け取るように変数を宣言します。struct LinkPresentationView: UIViewRepresentable { let url: URL private func fetchMetadata(for url: URL, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) { let provider = LPMetadataProvider() provider.startFetchingMetadata(for: url) { metadata, error in if let error = error { completion(.failure(error)) } else if let metadata = metadata { completion(.success(metadata)) } else { completion(.failure(LPError(.unknown))) } } } }エラーが発生した場合は
LPError
が返ってきます。
https://developer.apple.com/documentation/linkpresentation/lperrorネットワークに繋がっていなかったり
接続が遅すぎてタイムアウトになったり
リクエストがキャンセルされた場合に生じます。LPLinkViewの生成
次に
makeUIView
の中でLPLinkView
を生成します。struct LinkPresentationView: UIViewRepresentable { var url: URL func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType { let view = LPLinkView(url: url) // ※ self.fetchMetadata(for: url) { result in switch result { case .success(let metadata): DispatchQueue.main.async { self.update(view: view, with: metadata) } case .failure: let metadata = LPLinkMetadata() metadata.title = "Error" let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!) metadata.iconProvider = NSItemProvider(contentsOf: url) self.update(view: view, with: metadata) } } return view } private func update(view: UIViewType, with metadata: LPLinkMetadata) { view.metadata = metadata view.sizeToFit() } }※
ここは疑問が残っているのですが
ここでurlを引数にLPLinkView
を初期化しないと
画面に何も表示されませんでした。おそらく内部で取得した
LPLinkMetadata
のURLと
LPLinkView
のURLを比較しているんじゃないかと思っているのですが
もし何かご存知の方いらっしゃいましたら
教えていただけると嬉しいです??♂️
update
メソッドの中でprivate func update(view: UIViewType, with metadata: LPLinkMetadata) { .... view.sizeToFit() }としていますが
これは
LPLinkView
自体もintrinsic sizeを持っているものの
sizeToFit
を使用することで
現在のレイアウトに最適なサイズで表示されるようにするためです。エラーが起きた時は
その場でLPLinkMetadata
を生成して表示することもできます。case .failure: let metadata = LPLinkMetadata() metadata.title = "Error" let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!) metadata.iconProvider = NSItemProvider(contentsOf: url) self.update(view: view, with: metadata)実際に表示してみます。
struct ContentView: View { var urls: [URL] = [ URL(string: "https://www.apple.com/mac")!, URL(string: "https://www.apple.com/ipad")!, URL(string: "https://youtu.be/V85CQzsyvj4")!, URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!, ] var body: some View { List(self.urls, id: \.self) { url in LinkPresentationView(url: url) }.onAppear { UITableView.appearance().separatorStyle = .none } } }下記の様に表示されます。
適切なサイズで表示がされていません。
これは
LPLinkView
の初期化時のサイズのままになっており
取得した画像のサイズなどが反映されていないためです。そこで
LPLinkMetadata
の取得が完了した時点で
サイズの再計算を行うように親のViewに伝達するようにします。struct LinkPresentationView: UIViewRepresentable { ... @Binding var redraw: Bool private func update(view: UIViewType, with metadata: LPLinkMetadata) { ... redraw.toggle() } } struct ContentView: View { .... @State var redraw = false var body: some View { List(self.urls, id: \.self) { url in LinkPresentationView(url: url, redraw: self.$redraw) }.onAppear { UITableView.appearance().separatorStyle = .none } } }こうするとこんな形で表示されます。
またYoutubeの動画はクリックすると再生することができたり
Twitterの表示も自動で設定してくれます。(表示時のアニメーションをどうにかしたいですが。。。)
— shiz(しず) (@stzn3) March 21, 2020メタ情報をキャッシュする
URLから毎回メタ情報を取得してくるのは
ユーザにとっては通信料がかかってしまいますし
アプリとして同じリンク先から毎回同じ情報を取得するためには
パフォーマンスコストがかかるため
キャッシュをするべきだと
Appleの動画でも言っていました。そこでローカルにキャッシュするためのクラスを用意します。
LPLinkMetadata
はNSSecureCodingに適合しており
信頼性の高い形でシリアライズ可能になっています。https://developer.apple.com/documentation/foundation/nssecurecoding
※
今回は実装を簡単にするために
シングルトンのUserDefaults.standardやsharedを使用しています。final class MetaCache { static let shared = MetaCache() private init(){} private let storage = UserDefaults.standard private let key = "Metadata" func store(_ metadata: LPLinkMetadata) { do { let data = try NSKeyedArchiver.archivedData(withRootObject: metadata, requiringSecureCoding: true) var metadatas: [String: Data] = storage.dictionary(forKey: key) as? [String: Data] ?? [:] metadatas[metadata.originalURL!.absoluteString] = data storage.set(metadatas, forKey: key) } catch { print("Failed storing metadata with error \(error as NSError)") } } func metadata(for url: URL) -> LPLinkMetadata? { guard let metadatas = storage.dictionary(forKey: key) as? [String: Data] else { return nil } guard let data = metadatas[url.absoluteString] else { return nil } do { return try NSKeyedUnarchiver.unarchivedObject(ofClass: LPLinkMetadata.self, from: data) } catch { print("Failed to unarchive metadata with error \(error)") return nil } } }最後にこれを使用します。
func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType { let view = LPLinkView(url: url) if let cachedData = MetaCache.shared.metadata(for: url) { update(view: view, with: cachedData) } else { self.fetchMetadata(for: url) { result in switch result { case .success(let metadata): MetaCache.shared.store(metadata) case .failure: ... } } } return view }注意点
ドキュメントにも記載がありますが
startFetchingMetadata(for:completionHandler:)
のcompletionHandlerは
バックグラウンドで実行されるため
UI関連の処理を行う場合はメインキューで実行するようにします。The completion handler executes on a background queue. Dispatch any necessary UI updates back to the main queue.また、LPMetadataProviderは一回のリクエストにしか使用できないため
例えばstruct LinkPresentationView: UIViewRepresentable { let provider = LPMetadataProvider() func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<LinkPresentationView>) { provider.startFetchingMetadata(for: url) { metadata, error in } } }などと実装してみると
ListやForEachを使用した時に'Trying to start fetching on an LPMetadataProvider that has already started. LPMetadataProvider is a one-shot object.'といったエラーでクラッシュします。
最終的なコード
import SwiftUI import LinkPresentation struct LinkPresentationView: UIViewRepresentable { typealias UIViewType = LPLinkView let url: URL @Binding var redraw: Bool func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType { let view = LPLinkView(url: url) // 画像を取得するまでの表示されないように設定しています view.isHidden = true if let cachedData = MetaCache.shared.metadata(for: url) { update(view: view, with: cachedData) } else { self.fetchMetadata(for: url) { result in switch result { case .success(let metadata): MetaCache.shared.store(metadata) DispatchQueue.main.async { self.update(view: view, with: metadata) } case .failure: let metadata = LPLinkMetadata() metadata.title = "Error" let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!) metadata.iconProvider = NSItemProvider(contentsOf: url) self.update(view: view, with: metadata) } } } return view } func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<LinkPresentationView>) { } private func fetchMetadata(for url: URL, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) { let provider = LPMetadataProvider() provider.startFetchingMetadata(for: url) { metadata, error in if let error = error { completion(.failure(error)) } else if let metadata = metadata { completion(.success(metadata)) } else { completion(.failure(LPError(.unknown))) } } } private func update(view: UIViewType, with metadata: LPLinkMetadata) { view.metadata = metadata view.sizeToFit() redraw.toggle() view.isHidden = false } } struct ContentView: View { var urls: [URL] = [ URL(string: "https://www.apple.com/mac")!, URL(string: "https://www.apple.com/ipad")!, URL(string: "https://youtu.be/V85CQzsyvj4")!, URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!, ] @State var redraw = false var body: some View { List(self.urls, id: \.self) { url in LinkPresentationView(url: url, redraw: self.$redraw) }.onAppear { UITableView.appearance().separatorStyle = .none } } } struct LinkPresentationView_Previews: PreviewProvider { static var previews: some View { ContentView() } }UIViewでの実装
UITableViewでも実装をしてみました。
やっていることはだいたい同じなので
多くの部分は割愛しますがセルの実装でいくつか疑問に残っている部分があるので
記載します。まず
LPLinkView
は毎回生成しないと
画面に表示されないため
毎回addSubView
をする必要がありました。
そのためprepareForReuse
でsubviewsを一旦クリアする必要がありました。またセルサイズの再計算を行うために
メタ情報取得後にクロージャを実行しています。final class Cell: UITableViewCell { static let identifier = "Cell" // ViewControllerにセルサイズを再計算をさせるためにLPLinkMetadata取得したことを伝える var onUpdate: (() -> Void)? ... // 毎回subViewをクリアする必要がある override func prepareForReuse() { super.prepareForReuse() contentView.subviews.forEach { $0.removeFromSuperview() } } func configure(with url: URL) { setLPLinkView(for: url) } ... private func update(_ view: LPLinkView, with metadata: LPLinkMetadata) { view.metadata = metadata // 毎回addSubViewする必要がある addSubView(linkView: view) view.sizeToFit() // ViewControllerにLPLinkMetadataを取得したことを伝える onUpdate?() } private func addSubView(linkView: LPLinkView) { contentView.addSubview(linkView) linkView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ linkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12), linkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), linkView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12), linkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12), ]) } }下記のように動きます。
(表示時のアニメーションをどうにかしたいですが。。。)
— shiz(しず) (@stzn3) March 21, 2020
最終的なコード
import UIKit import LinkPresentation class ViewController: UIViewController { private var urls: [URL] = [ URL(string: "https://www.apple.com/mac")!, URL(string: "https://www.apple.com/ipad")!, URL(string: "https://youtu.be/V85CQzsyvj4")!, URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!, ] lazy var tableView: UITableView = UITableView() private var loadedIndexPaths: Set<IndexPath> = [] override func viewDidLoad() { super.viewDidLoad() configureTableView() } private func configureTableView() { view.addSubview(tableView) tableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) tableView.dataSource = self tableView.separatorStyle = .none } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return urls.count } func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let url = urls[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier, for: indexPath) as! Cell cell.configure(with: url) cell.onUpdate = { [weak self] in guard let self = self else { return } // LPLinkMetadataが取得されたらreloadする if !self.loadedIndexPaths.contains(indexPath) { self.loadedIndexPaths.insert(indexPath) self.tableView.reloadRows(at: [indexPath], with: .none) } } return cell } } final class Cell: UITableViewCell { static let identifier = "Cell" var onUpdate: (() -> Void)? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() contentView.subviews.forEach { $0.removeFromSuperview() } } func configure(with url: URL) { setLPLinkView(for: url) } private func setLPLinkView(for url: URL) { let linkView = LPLinkView(url: url) if let cachedData = MetaCache.shared.metadata(for: url) { update(linkView, with: cachedData) return } fetchMetadata(for: url) { result in switch result { case .success(let metadata): MetaCache.shared.store(metadata) DispatchQueue.main.async { self.update(linkView, with: metadata) } case .failure: let metadata = LPLinkMetadata() metadata.title = "Error" let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!) metadata.iconProvider = NSItemProvider(contentsOf: url) self.update(linkView, with: metadata) } } } private func fetchMetadata(for url: URL, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) { let provider = LPMetadataProvider() provider.startFetchingMetadata(for: url) { metadata, error in if let error = error { completion(.failure(error)) } else if let metadata = metadata { completion(.success(metadata)) } else { completion(.failure(LPError(.unknown))) } } } private func update(_ view: LPLinkView, with metadata: LPLinkMetadata) { view.metadata = metadata addSubView(linkView: view) view.sizeToFit() onUpdate?() } private func addSubView(linkView: LPLinkView) { contentView.addSubview(linkView) linkView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ linkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12), linkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), linkView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12), linkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12), ]) } }ShareSheetにLPLinkMetadataを利用する
LPLinkMetadata
はShareSheetでも
UIActivityItemSource
の
activityViewControllerLinkMetadata(_:)
から
利用することができ
プレビュー情報をリンク先から取得して表示できるようになりました。UIActivityItemSourceに適合したクラスを作成
ShareSheetに表示するアイテムを表すクラスを定義します
import UIKit import LinkPresentation final class ShareActivityItemSource: NSObject, UIActivityItemSource { private let linkMetadata: LPLinkMetadata init(_ url: URL) { linkMetadata = LPLinkMetadata() super.init() setPlaceholder(for: url) if let cachedData = MetaCache.shared.metadata(for: url) { setMetadata(cachedData) return } let metadataProvider = LPMetadataProvider() metadataProvider.startFetchingMetadata(for: url) { [weak self] metadata, error in guard let self = self, let metadata = metadata else { return } self.setMetadata(metadata) } } private func setPlaceholder(for url: URL) { linkMetadata.title = "loading..." linkMetadata.originalURL = url let loadingImageURL = URL(fileURLWithPath: Bundle.main.path(forResource: "loading", ofType: "png")!) linkMetadata.iconProvider = NSItemProvider(contentsOf: loadingImageURL) } private func setMetadata(_ metadata: LPLinkMetadata) { linkMetadata.title = metadata.title linkMetadata.url = metadata.url linkMetadata.originalURL = metadata.originalURL linkMetadata.iconProvider = metadata.iconProvider linkMetadata.imageProvider = metadata.imageProvider } func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { linkMetadata } func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { linkMetadata as Any } func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { linkMetadata } }ここでは
LPLinkMetadata
に事前のデータを設定しておき
データが取得できたらメタ情報を入れ替えるようにします。SwiftUIでの実装方法
UIActivityViewControllerの機能を有するViewを用意します。
struct ShareSheet: UIViewControllerRepresentable { typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void let activityItems: [Any] let applicationActivities: [UIActivity]? = nil let excludedActivityTypes: [UIActivity.ActivityType]? = nil let callback: Callback? = nil func makeUIViewController(context: Context) -> UIActivityViewController { let controller = UIActivityViewController( activityItems: activityItems, applicationActivities: applicationActivities) controller.excludedActivityTypes = excludedActivityTypes controller.completionWithItemsHandler = callback return controller } func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { } }ナビゲーションバーのボタンを押すと
ShareSheet
を表示するようにします。struct ContentView: View { var urls: [URL] = [ URL(string: "https://www.apple.com/mac")!, URL(string: "https://www.apple.com/ipad")!, URL(string: "https://youtu.be/V85CQzsyvj4")!, URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!, ] @State var redraw = false @State var showShareSheet = false var body: some View { NavigationView { List(self.urls, id: \.self) { url in LinkPresentationView(url: url, redraw: self.$redraw) }.onAppear { UITableView.appearance().separatorStyle = .none } }.navigationBarItems(trailing: Button(action: { self.showShareSheet = true }) { Text("Share").bold() } ).sheet(isPresented: $showShareSheet) { ShareSheet(activityItems: [ShareActivityItemSource(self.urls[Int.random(in: 0..<4)])]) } } }
ShareSheet
のactivityItems
に
ShareActivityItemSource
を渡します。すると下記のような動きをします。
— shiz(しず) (@stzn3) March 21, 20201回目のShareSheetの表示では
事前に準備した情報がまず表示され
取得後に入れ替わっています。2回目はメタ情報をすでに取得しているので
最初からメタ情報が表示されています。UIKitでの実装方法
同じ様にナビゲーションバーのボタンを押すと
ShareSheetが表示されるようにします。class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() ... navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(shareTapped)) } @objc func shareTapped() { showShareSheet(url: urls[Int.random(in: 0..<4)]) } } extension ViewController { func showShareSheet(url: URL) { let item = ShareActivityItemSource(url) let activity = UIActivityViewController(activityItems: [item], applicationActivities: nil) present(activity, animated: true) } }下記のように動きます。
— shiz(しず) (@stzn3) March 21, 2020メタ情報のベストプラクティス
以上のように使い方を見てきましたが
Appleがメタ情報にどういう情報を載せるべきかを
紹介している動画があるので
次に見ていきます。https://developer.apple.com/videos/play/tech-talks/205
メタ情報にはあらゆる情報を含めることができますが
その中でもAppleの公式動画の中でベストプラクティスを紹介しています。タイトルについてのベストプラクティス
- タイトルからリンク先の内容がわかるようにする
<head>
の<title>
からタイトルを読み取ることもできるがサイト名がURLのドメインなどと重複して表示されるのを避けるために<meta og:title="">
を設定する- JavaScriptは動かないので動的なタグの生成をしないようにする
アイコンについてのベストプラクティス
<link rel="icon">
の情報を読み取るが
<meta og:image="">
を指定するとアイコンが表示されなくなるので
アイコンを表示したい場合は<meta og:image="">
を指定しないようにする画像についてのベストプラクティス
- 興味を引かせるような特定のページの内容を表す画像のみ
og:image
を設定する- 画像が取得できなかった場合の対処としてアイコンを設定する
- テキストは含めない方が良い(全サイズのデバイスで表示する際にサイズなどがスケールしない)
動画についてのベストプラクティス
- アイコン、画像、動画合わせて10MBまでなのでサイズに気を付ける
- 自動再生をするためには直接参照したビデオファイルを使用する(不可能な場合、Youtubeの埋め込み動画のURLを指定すればユーザがタップして再生ができる。Youtube以外のサービスでは不可能)
- HTMLやプラグインが必要な埋め込み動画のサポートはしていない
まとめ
LinkPresentation.frameworkについて見てみました。
使い方は非常にシンプルで便利ですが
表示時のアニメーションがいまいちであったり
まだ使い方が完全に把握できていないため
今後も試してみて理解を深めていく必要があります。ここに記載したことはあくまで検証結果に基づいていますので
もし間違いなどございましたらぜひご指摘ください??♂️参照先
https://developer.apple.com/videos/play/wwdc2019/262/
https://developer.apple.com/videos/play/tech-talks/205
https://medium.com/better-programming/ios-13-rich-link-previews-with-swiftui-e61668fa2c69
https://augmentedcode.io/2019/09/15/loading-url-previews-using-linkpresentation-framework-in-swift/
https://nshipster.com/ios-13/
https://www.swiftjectivec.com/linkpresentation-introduction/
https://qiita.com/ezura/items/6036c6e100599b601482
https://forums.developer.apple.com/thread/123951
https://github.com/SDWebImage/SDWebImageLinkPlugin
- 投稿日:2020-03-21T16:01:32+09:00
【Solidity】event の半分は優しさで出来ている話
はじめに
イーサリアムのスマートコントラクト開発で利用される Solidity において、イベントの仕様はとてもシンプルです。
イベントは event をつけて宣言します.
event TransferToken( address indexed from, address indexed to, uint id );イベントは emit をつけて発行します.
emit TransferToken( msg.sender, reciever, tokenId );発行されたイベントはトランザクションのレシートに記録されます.
indexed のついた引数はフィルタリングに利用でき、特定のイベントを捕捉できます.めでたし、めでたし.
ネット上の情報や、書籍等での説明はだいたこんな感じです。わかったような気にはなりますが、「発行後のイベントを どうやって処理したらいいの?」という風にモヤモヤします。
この記事では、DApps 開発では避けて通れないのに、Solidity の解説においては多くを語られない、そんな event についてお話ししてみたいと思います。
Solidity はバックエンド向け、 event はフロントエンド向け
バックエンドという言葉があります。
エンジニア視点で見ると「裏方 = ユーザーの目につかないところで データの管理や操作を行うモノ」というイメージです。DApps 開発におけるバックエンドといえば、イーサリアムブロックチェーンとなります。そして、ブロックチェーン上において、ユーザーの資産やデータを管理するスマートコントラクトも、バックエンドといえるでしょう。
一方、フロントエンドという言葉もあります。
「窓口 = ユーザーと直接やりとりして 裏方とのあいだを取り持つモノ」というイメージです。「顧客」を意味する「クライアント」という言葉が、同じ意味で用いられることもあります。Solidity の event は バックエンドであるスマートコントラクトから、フロントエンドであるクライアントへ向けたメッセージの役割を担います。event を具体的に説明するには、イベントを受け取るクライアント側の前提がないと始まりません。
では、DApps 開発におけるクライアントとはなんでしょう?
DApps が Web 向けのサービスを想定しているのであれば、Chrome 等のウェブブラウザとなりそうです。PC 向けのサービスであれば、Windows や Mac プログラムになりそうですし、スマホ向けであれば、iOS や Android アプリとなるでしょう。サービスの提供先に応じてクライアントの選定が変わってきますし、当然、開発環境も大きく変わります。
Solidity の解説において event の発行後について多くを言及されないのは、バックエンドとフロントエンドというスタンスの違いがあるからだと思われます。それに、イベント発行後の説明をしようとなると、クライアント側での実装の話が必要となってきて、収拾がつかなくなりそうです。
「そこまで紙面を割くことはできない」というのが著者さんの本音なのだと思いいます。
イベントはクライアント本位で考える
では、発行後のイベントについて考えてみましょう。
スマートコントラクトはユーザーの現在の残高や、トークン(資産)の現在の所有者といった、「現在の状況」を扱います。一方で、「昨日受け取った ETH の合計」や「誰が誰にどんなトークンを送った」というような「状況の変化」を扱うのは得意ではありません。
DApps を開発する上で、取引履歴や状況変化の通知は、ユーザーの満足度を上げるためにも手厚く実装したいところです。そして、この状況の変化をクライアント側で検出するために活躍するのが、Solidity の event となります。
例えば、トークンの送信に対応した DApp を開発するとしましょう。
この DApp はユーザー間でトークンを送信しあえるのすが、コスト削減を理由に、下記の仕様でGOサインがでてしまったと仮定します。
・送信者がトークンを送ったら、そのまま黙ってホーム画面へ戻ってよい
(※トランザクションの結果はイーサスキャンで見てくれ!すまん!)
・トークン情報はトークン画面へ遷移した時に読み込めばよい
(※トークンの情報を知りたければトークン画面を開いてくれ!お願い!)さて、運悪くこの DApp を利用してトークンを送ったAさんは、次のように思うはずです。
「送信完了のポップすらでないけど、本当に送れてるの…?」同じく、この DApp を利用してトークンを受け取った(ことに気づいていない)Bさんは、次のように思うはずです。
「Aさんからのトークン、なかなか来ないな…」(※ホーム画面を眺めながら)トークンの送信自体が無事に完了していたとしても、その過程の説明が不足していると、ユーザーを不安にさせてしまいます。この場合、Aさんに対する「送信が完了しました」通知、Bさんに対する「トークンが届きました」通知ぐらいは実装しておきたいところです。
では、この DApp に通知機能を加えるとしたらどうしたらよいでしょう?
イベントによる通知の実装の流れは、おおよそ下記のようになると思います。1.コントラクトにてトークンの送信完了イベントを定義する(※例えば冒頭に挙げた TransferToken イベント)
2.コントラクトにて、トークンの送信が正常終了したら「送信完了イベント」を発行する(※ TransferToken の emit)
3.ブロックチェーン上にて、トランザクションのレシートに「送信完了イベント」が記録される
4.クライアントにて、定期的にブロックをチェックし、ユーザーに関わる「送信完了イベント」がないかを監視する
5.クライアントにて、ユーザーに関わる「送信完了」イベントを見つけたら、適切な表示を行う
(※ユーザーが送信者であれば「送信が完了しました」通知、ユーザーが受信者であれば「トークンが届きました」通知を出す)これならば、トークンの送信完了後にクライアント上で通知が行われるため、Aさんの不安は解消され、Bさんはホーム画面でトークンの取得に気づいたはずです。
処理の結果が同じだったとしても、その過程を丁寧に通知する DApp と、そうでない DApp では、お客さんに与える安心感に大きな差が出ることでしょう。
「どのような通知があれば利用者が 安心&満足してくれるか?」
クライアント側の実装、とくにイベントの設計において、お客さんへの優しさは欠かせません。
イベント実装の具体例
サンプル DApp により、イベントの流れを具体的に見てみましょう。このサンプルのサービス内容はミニゲーム、クライアントは iOS アプリを想定します。
サービス内容
サービス内容は「みんなで カモったり カモられたりする ゲーム」です。
ゲーム内では、参加するプレイヤーのうち1人がカモにされ、周りのプレイヤーから所持ゴールドを盗まれます。逆に、カモられているプレイヤーは、最後にカモったプレイヤーを容疑者として通報することで示談金を得られます。抜け目なくカモり、仮にカモられても示談金でやり返し、所持ゴールドを高めるのがゲームの目的です。
具体的なルール(タップで開閉します)
・プレイヤーの中から「カモ」と「容疑者」が、それぞれ1人づつ認定される ・ゲームに参加するにはETHを送金してゴールドを購入する必要がある ・ゴールドを購入したプレイヤーは、新たなカモに認定される ・カモ以外のプレイヤーはカモの所持ゴールドの「5%」をカモれる(盗める) ・一番最後にカモった(盗みを働いた)プレイヤーは、新たな容疑者に認定される ・カモは容疑者を通報することで、相手の所持ゴールドの「30%」を示談金として分捕れる ・通報されたプレイヤーはペナルティとして、新たなカモに認定される ・通報したプレイヤーは逆恨みされ、新たな容疑者に認定されるゲームに必要なデータ
ゲームを構成するためにスマートコントラクト側で管理しないといけない情報としては下記となります。
// 管理データ mapping( address => bool ) internal valids; // プレイヤーの有効性 mapping( address => uint ) internal golds; // プレイヤーの所持ゴールド address internal targetPlayer; // 現在のカモ address internal suspectPlayer; // 現在の容疑者これらの情報がブロックチェーン上で保持され、ゲームの流れによって更新されていきます。クライアント側ではこれらの情報をスマートコントラクトから読み込んで、画面へ表示します。
ですが、上記のデータだけでは「誰が誰をカモった」等の状況説明ができません。そのための情報はイベントにより通知することになります。
イベント設計と定義
さて、このゲームのフローにおいてどのようなイベントが必要になるでしょうか?
別の言い方をすると、どのような通知があるとプレイヤーが楽しんでくれそうでしょうか?まず、自分がカモった時にはいくら盗めたか通知して欲しいですよね。それに、カモられた時は被害額を知りたいです。また、他人同士のカモりカモられも見られた方が、ワイワイやっている感じがして楽しそうです。
この通知に使うイベントを Steal として定義しましょう。
event Steal( address indexed player, address indexed target, uint ammount );同様に、通報された時の情報も欲しいので、Report イベントも用意しましょう。
event Report( address indexed player, address indexed suspect, uint ammount );あとは、ゲームに参加(ETH を送ってゴールドを取得)した際に、いくら入手したかのお知らせを出さないと不親切そうです。
このイベントを BuyGold としましょう。event BuyGold( address indexed player, uint ammount );加えて、ゴールドを入手したユーザーはカモにされるので、良くも悪くも目立たせてドキドキしてもらいましょう。
このイベントを Target としましょう。event Target( address player, uint gold );以上、4つのイベントがあれば、クライアント側でゲームを盛り上げることができそうです。
通知の具体的なイメージとしては、ゲームに参加するとまず、「BuyGold:ゴールドを入手しました」と表示され、続いて「Target:気をつけろ!カモられるぞ!」と表示します。その後は、自分含めた全てのプレイヤーの「Steal:カモった/カモられた」と「Report:通報した/通報された」が次々と表示されていくことになります。
メモ:
このサンプルでは、イベントをプレイヤー別にフィルタリングできるよう、address に indexed をつけて宣言しています。が、Target イベントのアドレスには indexed がついていません。これは、Target イベントは全プレイヤーに対して必ず通知されることを想定しており、フィルタリングは不要と考えたからです(ようするにどうでもよいこだわり分です)。Solidityでの実装
ゲームの肝である、「カモる」、「通報する」、「入金する」ための処理を、「steal 関数」、「report 関数」、「ETH の入金を受け取るフォールバック関数」として用意しましょう。
steal 関数(タップで開閉します)
//-------------------------------- // 盗む(※カモをカモる) //-------------------------------- function steal() external{ // 未参加、自身がカモなら失敗 require( valids[msg.sender], "you need to join the game" ); require( msg.sender != targetPlayer, "you can not steal your own gold" ); // 最後に盗みを働いたプレイヤーが容疑者となる suspectPlayer = msg.sender; // カモの所持ゴールドを5%盗む(※端数入り揚げ) uint ammount = (golds[targetPlayer]+19) / 20; golds[suspectPlayer] += ammount; golds[targetPlayer] -= ammount; emit Steal( suspectPlayer, targetPlayer, ammount ); }
report 関数(タップで開閉します)
//-------------------------------- // 通報する(※カモがカモる) //-------------------------------- function report() external{ // 未参加、自身がカモでなければ失敗 require( valids[msg.sender], "you need to join the game" ); require( msg.sender == targetPlayer, "you can not report your own crime" ); // 容疑者がカモになり、逆恨みされた通報者が容疑者となる targetPlayer = suspectPlayer; suspectPlayer = msg.sender; // 示談金としてカモ(旧容疑者)が所持ゴールドの30%を支払う(※端数入り揚げ) uint ammount = (3*golds[targetPlayer]+9) / 10; golds[suspectPlayer] += ammount; golds[targetPlayer] -= ammount; emit Report( suspectPlayer, targetPlayer, ammount ); }
入金(フォールバック)関数(タップで開閉します)
//------------------------------------------- // ETHを送ったら参加 & ゴールドを得る & 狙われる //------------------------------------------- function () external payable { // 参加費は 1 gwei 以上 if( !valids[msg.sender] ){ require( msg.value >= 1000000000, "please send 1 gwei or more, to join the game" ); valids[msg.sender] = true; } // ゴールドの入手 golds[msg.sender] += msg.value; emit BuyGold( msg.sender, msg.value ); // 早速狙われる targetPlayer = msg.sender; emit Target( targetPlayer, golds[msg.sender] ); }これらの関数が正常に終了すると、それぞれに対応するイベントが emit にて発行され、クライアント側で検出できるようになります。
また、クライアントが管理データを参照できるように、status 関数も用意しましょう。クライアントがイベントを捕捉した際、ゲームの状況を画面へ反映させるために、この関数が呼ばれる想定です。
status 関数(タップで開閉します)
//-------------------------------- // ゲームの状況 //-------------------------------- function status() external view returns( uint retGold, address retTarget, uint retTargetGold, address retSupsect, uint retSuspectGold ){ // 未参加なら失敗 require( valids[msg.sender], "you need to join the game" ); retGold = golds[msg.sender]; // プレイヤーの所持ゴールド retTarget = targetPlayer; // カモのアドレス retTargetGold = golds[targetPlayer]; // カモの所持金 retSupsect = suspectPlayer; // 容疑者のアドレス retSuspectGold = golds[suspectPlayer]; // 容疑者の所持金 }status 関数の返値は、クライアントの表示の都合によるものです。クライアント側が要求するのは、プレイーヤーの所持ゴールド、現在のカモと容疑者のプレイヤー、それぞれの所持ゴールドとなります。画面仕様等がかわれば、返値の内容はその都度修正されることになります。
これにて、Solidity 側の実装は終了です。
最終的なコードはこちらとなります。発行されたイベントの登録先
クライアントからスマートコントラクトの関数が呼ばれるとイベントが発行され、トラザクションのレシートに登録されます。この情報はイーサリアム上に保持され、クライアント側から参照することが可能となります。
保存内容は、イベント定義(イベント名と引数の型の羅列)のハッシュ値と、各引数の値を羅列したものとなります。indexed をつけた引数は Topics 枠に登録され、ついていない引数は Data 枠に登録されます。この時、イベント定義のハッシュ値が Topics[0] に登録され、イベント名によるフィルタリングに対応されます。また、event 定義で indexed で指定された引数は、Topics[0] の後ろから定義順に並ぶことになります。
例えば、steal イベントをイーサスキャンで見ると下記のような内容となります。
Topics [0] 0xc4521c77cd8558a7102424919c444912561b76d8973dfc5482ea96932e5a9a47 [1] 0x00000000000000000000000093231c1b82547b20e85b70b87f1c98dcbd35d88a [2] 0x000000000000000000000000431e16c88dc8a449b93fb67d2bb08f1f6695348c Data Hex 00000000000000000000000000000000000000000000000000000000170bb4c9indexed で指定した player と target の引数は Topics[1] と Topics[2] として登録されています。一方で、indexed 指定のない ammount の引数は Data に登録されています。
さて、indexed をつけることで、この Topics 配列に追加するメリットはというと、指定の引数が添字で参照可能になるため、クライアント側でイベントのフィルタリングに利用できるという点です。
クライアント側の実装
このサンプルでのクライアントは iOS での実装となります。
先にお話した通り、どのプラットフォームをターゲットにするかにより開発環境は大きく変わります。ここでは、iOS 特有の実装には深入りせず、クライアントがどのようにイベントの処理をするかの流れにしぼって説明します。まず、イベント処理の基本は、新しく発行されるイベントがないかの監視なので、監視ループを開始します(※コードの詳細はわからなくても雰囲気をつかんでくだされば十分です)。
イベント監視ループの開始(タップで開閉します)
// イベントループの開始 let web = helper.getCurWeb3()! let functionToCall: web3.Eventloop.EventLoopCall = onNewBlock let monitoredProperty = web3.Eventloop.MonitoredProperty.init( name: "onNewBlock", queue: web.requestDispatcher.queue, calledFunction: functionToCall ) web.eventLoop.monitoredProperties.append( monitoredProperty ) web.eventLoop.start( 1.0 )イベントの監視ループでは一定間隔おきに新規ブロックが監視され、新しいブロックが検出されたら、そのブロック中のイベントををフィルタリングします。
イベントのフィルタリング(タップで開閉します)
var filter = EventFilter() filter.fromBlock = .blockNumber(UInt64(blockNumber)) filter.toBlock = .blockNumber(UInt64(blockNumber)) // フィルター:BuyGold if eventName == "BuyGold" { // 自身のアドレスでフィルタ filter.parameterFilters = [ ([self.helper.getCurAddress()!] as [EventFilterable]) ] } do{ guard let result = try contract?.getIndexedEvents( eventName:eventName, filter:filter ) else { return }サンプルコードでは、BuyGold はプレイヤーのアドレスでフィルタリングしています。これにより、プレイヤーが発行した BuyGold イベントだけが抽出され、プレイヤー自身のゴールド購入通知のみが表示されることになります。
一方で、Steal と Report イベントはイベント名のみでフィルタリングしています。結果として、全ての「カモられた」、「通報された」イベントが検出されることになりますが、検出後、イベントの引数のプレイヤーが自身であるかどうかにより、表示の割り振りを行なっています。
同様に、Target イベントもイベント名のみでフィルタリングを行い、検出後、自分がターゲットにされたのか否かで表示を割り振っています。
検出したイベントの通知(タップで開閉します)
if result.count > 0 { for event in result{ switch event.eventName { case "BuyGold": addLog( "購入完了 \(event.decodedResult["1"]!) GOLDを手に入れた!" ) break; case "Target": let addressTarget = (event.decodedResult["0"] as? EthereumAddress)!.address if( addressTarget != self.helper.getCurEthereumAddress() ){ addLog( "カモ(\(addressTarget.prefix(10))...)発見! \(event.decodedResult["1"]!) GOLD所持している!" ) }else{ addLog( "カモ認定された! 気をつけろ!" ) } break; case "Steal": let addressPlayer = (event.decodedResult["0"] as? EthereumAddress)!.address let addressTarget = (event.decodedResult["1"] as? EthereumAddress)!.address if( addressPlayer == self.helper.getCurEthereumAddress()! ){ addLog( "カモ(\(addressTarget.prefix(10))...)から \(event.decodedResult["2"]!) GOLD 盗んだ!" ) }else if( addressTarget == self.helper.getCurEthereumAddress()! ){ addLog( "容疑者(\(addressPlayer.prefix(10))...)に \(event.decodedResult["2"]!) GOLD 盗まれた..." ) }else{ addLog( "カモ(\(addressTarget.prefix(10))...)が容疑者(\(addressPlayer.prefix(10))...)にカモられてるw" ) } break; case "Report": let addressPlayer = (event.decodedResult["0"] as? EthereumAddress)!.address let addressTarget = (event.decodedResult["1"] as? EthereumAddress)!.address if( addressPlayer == self.helper.getCurEthereumAddress()! ){ addLog( "容疑者(\(addressTarget.prefix(10))...)から \(event.decodedResult["2"]!) GOLD の示談金を奪った!" ) }else if( addressTarget == self.helper.getCurEthereumAddress()! ){ addLog( "カモ(\(addressPlayer.prefix(10))...)に \(event.decodedResult["2"]!) GOLD の示談金を奪われた..." ) }else{ addLog( "カモ(\(addressPlayer.prefix(10))...)が容疑者(\(addressTarget.prefix(10))...)を通報してるw" ) } break; default: addLog( "エラー:知らないイベント \(event.eventName)" ) break; } } }上記の処理において、Topics のフィルタリングによりイベントを抽出し、Data 連想配列から詳細情報が取り出されて処理する流れを、イメージしていただけたことと思います。
これにて、クライアント側での実装も終了です。
最終的なサンプルプロジェクトはこちらとなります。
(※iOS アプリ上でのイーサリアムへの接続に関してはこちらの記事を参照ください)ゲームの動作確認
複数台で並べてゲームを遊んでみた様子です。
プレイヤーCの様子:両者の様子を眺めつつ、姑息にカモっている
画面下にでている時間付きのログが、イベントによって通知された情報です。各クライアントによる「カモる/通報する」行為の結果がイベントを発生させ、クライアント毎にフィルタリングされて新しい順に表示されているのが見て取れるかと思います。
このサンプル DApp の通知方法はシンプルなログの垂れ流しですが、イベント処理としては十分な機能が備わっています。あとは、絵的/音的な演出を作り込んでいくことで、ゲームとしての面白みを向上していけると思われます。
まとめ
Solidity の仕様としては ほんの小さな event の存在ですが、クライアント側で果たす役割の大きさを具体的にイメージしていただけたのであれば幸いです。
DApps 開発において、クライアントの使い勝手は、ユーザーの満足度に大きく影響してきます。ユーザー本位のクライアントフローを検討し、優しさあふれるイベント設計を心がけたいものです。
- 投稿日:2020-03-21T14:59:24+09:00
AppleMusicのような、TabBarにDockされたツールバーを構築する
AppleMusicのようなTabBarにDockされたツールバーを構築する
AppleMusicのiOSアプリには、TabBarの上に再生中の音楽を示したツールバーが表示されます。
今回はタブを切り替えたり、UINavigationControllerでpush遷移をしても単一のインスタンスを使用して表示ができるような実装をしてみます。完成イメージ
実装概要
UITabBarControllerを継承したTabBarController側にPlayer用のUIViewをレイアウトし、公開させておきます。
UITabBarControllerに埋め込まれたUIViewControllerからはUITabBarControllerが取得できるので、それを経由してPlayerインスタンスにアクセスし、
レイアウトに使用することができます。実装を簡易化するため、TabBarVisible protocolを作成し、protocol extensionでplayerを取得できるようにします。
注意点としてはtabBarControllerはviewDidLoad時点では取得することができないので、updateViewConstraints等でレイアウトをする必要があります。
実装
import UIKit final class TabBarController: UITabBarController { enum Const { static let playerHeight: CGFloat = 64 } let player: UILabel = { let label = UILabel() label.text = "This is Music Player" label.font = .boldSystemFont(ofSize: 18) label.backgroundColor = UIColor.purple return label }() override func viewDidLoad() { super.viewDidLoad() player.translatesAutoresizingMaskIntoConstraints = false view.addSubview(player) NSLayoutConstraint.activate([ player.heightAnchor.constraint(equalToConstant: Const.playerHeight), player.leftAnchor.constraint(equalTo: view.leftAnchor), player.rightAnchor.constraint(equalTo: view.rightAnchor), player.bottomAnchor.constraint(equalTo: tabBar.topAnchor) ]) viewControllers = [ TopViewController(title: "Page1", color: .green), TopViewController(title: "Page2", color: .yellow) ] } }import UIKit protocol TabBarVisible {} extension TabBarVisible where Self: UIViewController { var player: UIView { guard let tabBarController = tabBarController as? TabBarController else { fatalError("Must to be embbeded in UITabBarController") } return tabBarController.player } }import UIKit final class TopViewController: UIViewController, TabBarVisible { private let label: UILabel = { let label = UILabel() label.numberOfLines = 0 label.minimumScaleFactor = 0.1 label.adjustsFontSizeToFitWidth = true label.translatesAutoresizingMaskIntoConstraints = false return label }() init(title: String, color: UIColor) { super.init(nibName: nil, bundle: nil) self.title = title label.text = Array(repeating: title, count: 100).joined() view.backgroundColor = color } required init?(coder: NSCoder) { super.init(coder: coder) } override func viewDidLoad() { super.viewDidLoad() view.addSubview(label) } override func updateViewConstraints() { NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: view.topAnchor), label.rightAnchor.constraint(equalTo: view.rightAnchor), label.leftAnchor.constraint(equalTo: view.leftAnchor), label.bottomAnchor.constraint(equalTo: player.topAnchor) ]) super.updateViewConstraints() } }
- 投稿日:2020-03-21T14:12:29+09:00
�[Firebase]: Firestoreのアクセス期限切れへの対処法
最近サーバとのデータ通信が必要なiOSアプリを作成しているのですが、FirebaseのデータベースサービスであるFirestoreを利用してから数週間経ったある日、こんなメールが、
データベースが攻撃に対して脆弱であり、セキュリティルールを厳しくせよとのことでした。
認証されたユーザーからのみアクセスを許可するルールにすれば解決なのですが、ログイン機能を持たせるアプリでもないため、どうしたものか、、、と思案していたところ、Firebaseの匿名認証という機能を発見しました。匿名認証を利用することで、ユーザーにログインを強制することなく、データベースのセキュリティを安全に保つことができます。
Firebase で認証する一時的な匿名アカウントを、Firebase Authentication で作成して使用できます。一時的な匿名アカウントを使用すると、アプリに登録していないユーザーが、セキュリティ ルールで保護されているデータを使用できるようになります。
https://firebase.google.com/docs/auth/ios/anonymous-auth?hl=ja設定方法は下記の通りです。
匿名認証の実装方法
Authenificaion → ログイン方法 から匿名認証を有効にします。
AppDelegaet.swiftにsiginInAnonymously()メソッドを追加すれば、起動時に自動で匿名ユーザーが作成されます。
AppDelegaete.swiftfunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. FirebaseApp.configure() // 匿名認証(下記のメソッドがエラーなく終了すれば、認証完了する) Auth.auth().signInAnonymously() { (authResult, error) in if error != nil{ print("Auth Error :\(error!.localizedDescription)") } // 認証情報の取得 guard let user = authResult?.user else { return } let isAnonymous = user.isAnonymous // true let uid = user.uid return } return true }セキュリティルールの設定方法
認証ユーザーのみ読み取りを許可するよう、Firestoreのセキュリティルールを修正します。
service cloud.firestore { match /databases/{database}/documents { match/collectionName/{document=**}{ allow read: if request.auth != null; } } }
- 投稿日:2020-03-21T13:45:41+09:00
Firestore データの追加 現在時刻を配列に格納する (Swift)
現在時刻を何度も取得して、Firestoreドキュメントの配列に格納していくやり方で困ったので、メモ&共有。
ポイント1. 現在時刻はTimestamp(date: Date())で取得。
Fieldvalue.timestamp() で時刻を取得することもできるんですが、
これをFirestoreのTimestamp配列フィールドに入れようとするとエラーになります。2020年3月時点では、Fieldvalue.timestamp()は配列に対応していないようです。
よって、公式ドキュメントにさらっと書いてある「Timestamp(date: Date())」で現在時刻を取得してTimestamp配列に入れると、こちらは問題なし。
ポイント2.arrayUnionは配列の新規作成でも使える
公式ドキュメントでは、arrayUnionは「配列がある場合に」と記載されていますが、以下のように、配列がない場合でも1番目の配列要素を作成してくれました。
以上、ご参考まで。
let historyRef = db.collection("history").document(String(user!.uid)) historyRef.setData([ "userid" : String(user!.uid),//ユーザID "objectid" : objectid, //arrayUnionで、初回配列がない場合でもた1個目の配列要素を自動で追加してくれる "history" : FieldValue.arrayUnion([Timestamp(date: Date())]) ], merge: true ) { err in if let err = err { print("Error adding document: \(err)") } else { print("Document added with ID") } }
- 投稿日:2020-03-21T05:01:22+09:00
Firestore × iOS(Swift)【環境構築編】
前置き
今回書く事は公式ドキュメントに全て書いてあります。
しかし以前の僕がそうだったように、「実際使ってるところを見ないと分からん!」という人もいると思います。
なので、当時の僕に向けて写真多めで説明していきます。Firestoreでプロジェクトを作成
名前を付けます。(今回は適当に付けましたが、どのアプリか後々分かるようにした方が良いです)
料金プランを確認。使用量が少ないうちは無料です。→参考Xcode側の準備
今回は「FirestoreSample」というプロジェクトを予め作っておきました。
このプロジェクトにFirestoreを紐付けていきます。
先ほど作成したXcodeプロジェクトのバンドルIDをコピペします。
「GoogleService-Info.plist」をダウンロードします。
zipファイルを解凍し、Xcodeプロジェクトに追加します。
このようになっていれば正常に追加されています。
(注意)何回もこのファイルをダウンロードしていると「GoogleService-Info(1).plist」のように数字がくっつきます。このファイルでは正常に動作しませんので、一度削除してもう一度ダウンロードしてください。
CocoaPodsを使ってFirebaseSDKを追加します。CocoaPodsの使い方はここでは割愛します。
Xcodeプロジェクトの「Appdelegate.swift」に「import Firebase」、「FirebaseApp.configure()」を追加します。
⌘Rでビルドしてみましょう。正常に設定できていればこのような画面になるはずです。
お疲れ様でした。終わりに
次回はデータの追加や読み取りについて書ければと思います。
間違いがあればご指摘頂けると幸いです。Twitterやってます→@_4ryunity
未経験からエンジニアへ転職活動中の25歳コンビニ店員です。
時間を見つけて投稿していきます。
- 投稿日:2020-03-21T00:08:48+09:00
【Swift】よく分からなかったdelegateについて整理してみた(初心者が初心者のために)
そもそもdelegateってなんだ
言葉を調べてみる
delegateを辞書で調べてみると、
1.委任する
2.代表[代理]に立てるという意味らしい。
なるほど、言葉の意味は分かった。
Swift的観点で調べる
では、Swiftにおいて、delegateってなんだ?ともう少し調べてみると、
「delegateとは、デザインパターンのひとつです。」
なんて出てきた。
なるほど、分からないようで分からない。
分からんかったからさらに調べる
さらに調べていくと、TECHACADEMYにたどり着いた。
https://techacademy.jp/magazine/15055ここでは、
『実際のイメージとしては「ある特定の事項について、問い合わせ先を指定する」操作と覚えておくと分かりやすいです。』
と書いてあった。
delegate = 問い合わせ先とも書いてあって、これが一番理解しやすいような気がする。(なんとなく)
まあ、とりあえず実装してみないと分からないイメージつかないので、実装に移りたい。
delegateを実装するために必要なもの
1.protocol
委任する処理をmethodとして定義する。これをデリゲートメソッドと呼ぶ。
言わば「仕様書」と言ったところ。2.処理を委任するclass
デリゲートメソッドを使用して、処理の流れを記述する。
3.処理を委任されるclass
protocolで定義されたデリゲートメソッドを実装する。
実際に実装してみる
1.protocol
メソッドを定義する。
HorseDataProtocol.swiftprotocol horseDataDelegate { func horseName() // 競走馬名 func horseBirthYear() // 競走馬の誕生年 func horseWinNumber() // 競走馬の勝利数 func horseIsLeadingSire() // 競走馬の種牡馬状況 }2.処理を委任するclass
今回はViewController内に実装した。
ViewController.swiftimport UIKit class ViewController: UIViewController { var delegate: horseDataDelegate? //delegateを定義 override func viewDidLoad() { super.viewDidLoad() //使用するdelegateをここで呼び出す delegate = DeepImpact() //delegate内のメソッドを呼び出す delegate?.horseName() delegate?.horseBirthYear() delegate?.horseWinNumber() delegate?.horseIsLeadingSire() } }3.処理を委任されるclass
protocolに記載したメソッドを使用して記載する。
DeepImpact.swiftclass DeepImpact: horseDataDelegate { func horseName() { print("Horse Name is Deep Impact") } func horseBirthYear() { print("Horse was born in 2002") } func horseWinNumber() { print("Horse won 12") } func horseIsLeadingSire() { print("Horse is Leading Sire") } }これでRunすると、こんな感じで出力される。
出力Horse Name is Deep Impact Horse was born in 2002 Horse won 12 Horse is Leading Sire共通なメソッドをprotocolに用意する
ここでまとめても良かったが、もう少し突っ込んでみる。
horseDataDelegateのクラスをもう一つ作成してみた。
Orfevre.swiftclass Orfevre: horseDataDelegate { func horseName() { print("Horse Name is Orfevre") } func horseBirthYear() { print("Horse was born in 2008") } func horseWinNumber() { print("Horse won 12") } }ViewControllerのdelegateもこれに伴って変更してみる。
ViewController.swiftimport UIKit class ViewController: UIViewController { var delegate: horseDataDelegate? //delegateを定義 override func viewDidLoad() { super.viewDidLoad() //delegate先を変更 delegate = Orfevre() delegate?.horseName() delegate?.horseBirthYear() delegate?.horseWinNumber() delegate?.horseIsLeadingSire() } }これでRunすると、Orfevreクラスでエラーが出る。
出力Type 'Orfevre' does not conform to protocol 'horseDataDelegate'horseIsLeadingSireメソッドないよ!って言われてる。
基本的にprotocolに記載されているものは全部書かないといけない。
が、共通でもいいメソッドがある時は、わざわざ全部のメソッドを書く必要もない.
そこで、protocolを少しだけ修正する。
HorseDataProtocol.swiftprotocol horseDataDelegate { func horseName() // 競走馬名 func horseBirthYear() // 競走馬の誕生年 func horseWinNumber() // 競走馬の勝利数 func horseIsLeadingSire() // 競走馬の種牡馬状況 } extension horseDataDelegate { // このメソッドはデフォルトで実装しておく func horseIsLeadingSire() { print("Horse is not Leading Sire") } }共通で使用するメソッドは、このように、extensionで用意しておけばいい。
DeepImpact.swift内のhorseIsLeadingSire()は、これをオーバーライドしているという形になる。
これでRunが通る。出力はこんな感じ。
出力Horse Name is Orfevre Horse was born in 2008 Horse won 12 Horse is not Leading Sire本当は、もう少し書きたいことがあったけど、長くなりそうなので今回はここまでで。
最後に
今回作成したものはGithubに載せています。
https://github.com/taichi6930/SwiftDelegateSample初めてQiitaに投稿してみました。
やはり文章を書くのは難しいですね。とはいえ、自分の頭の中で整理しながら書くことができました。
これからも続けていきたいと思います。